From b1c684e912686c4ef34db8595378ea202f42014d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 06:24:53 -0800 Subject: [PATCH] Scripts: E4X operations count their temporary storage against the heap limit Assigning to a named child counts its list of the matching children, and assigning through an empty XMLList counts the chain of targets it climbs, before either changes the tree. copy(), toXMLString(), normalize(), the `..` selector and the depth check on insertion hold the stack they keep for each open level of the tree. Inserting an XMLList, and assigning one to an item of an XMLList, hold their copy of the list's items. copy() reads attributes in place, and toXMLString() reads attributes and namespace declarations in place, instead of copying the lists. copy() charges the declarations it copies before it copies them. Deleting by name or assigning an attribute compacts the remaining links in place instead of building a second list. toXMLString() holds the set of prefixes a namespace lookup passes, and picks a new nsN prefix in one pass over the declarations in scope instead of rescanning them for each candidate. The parser holds its open elements and their names, the text it gathers, each tag's attribute values, the sets that check declarations and attribute names, and its one copy of an element's namespaces, and it charges the strings of the namespace declarations it makes. The allowance it holds from the source's length covers the copy a value takes as it becomes a string. Comparing XML values, converting them to strings, assigning an XMLList to an attribute, and reading a name, a qualifier, a namespace URI or embedded text from a string built by joining strings flatten it with flat_charged. An attribute name read from a string reserves its copy, and an element name shares the string. Docs: docs/target-app-parity.md. --- crates/js/src/xml.rs | 471 ++++++++++++++++++++++++------------- crates/js/src/xml_tests.rs | 245 +++++++++++++++++++ docs/target-app-parity.md | 1 + 3 files changed, 549 insertions(+), 168 deletions(-) diff --git a/crates/js/src/xml.rs b/crates/js/src/xml.rs index 8d83162a50..f49c94eb46 100644 --- a/crates/js/src/xml.rs +++ b/crates/js/src/xml.rs @@ -7,6 +7,7 @@ //! each new node, name, reference or output unit is charged to the heap before it is made. use std::collections::{HashMap, HashSet}; +use std::hash::Hash; use std::rc::Rc; use crate::LimitKind; @@ -18,7 +19,7 @@ use crate::heap::{ XmlListData, XmlName, XmlNode, XmlNs, key, string_bytes, }; use crate::interp::{Abrupt, ErrorType, HeldBytes, Interp, JsResult}; -use crate::string_builtins::{UNITS_PER_FUEL, flat_charged}; +use crate::string_builtins::{UNITS_PER_FUEL, flat_charged, push_held, reserve_units}; use crate::value::{Str, Value, index_key}; fn parser_units(s: &str) -> Vec { @@ -295,26 +296,40 @@ fn name_bytes(n: &XmlName) -> usize { string_bytes(n.local.len()) + n.uri.as_ref().map_or(0, |u| string_bytes(u.len())) + n.prefix.as_ref().map_or(0, |p| string_bytes(p.len())) } -/// The namespace `stack` declares for `uri`, nearest first, skipping prefixes redeclared nearer. -/// `prefer` picks that prefix when several match; `need_prefix` rejects the default namespace. -fn ns_lookup(stack: &[XmlNs], uri: &Key, prefer: Option<&Key>, need_prefix: bool) -> Option { - let mut seen: HashSet<&[u16]> = HashSet::new(); - let mut best = None; - for ns in stack.iter().rev() { - let Some(p) = &ns.prefix else { continue }; - if !seen.insert(p) { - continue; - } - if ns.uri == *uri && !(need_prefix && p.is_empty()) { - if prefer == Some(p) { - return Some(ns.clone()); - } - if best.is_none() { - best = Some(ns.clone()); - } - } +/// Inserts `item` into `set`, first holding an upper estimate of the bytes of the set's new table +/// when it must grow. Returns whether `item` was new. +fn insert_held(interp: &mut Interp, held: &mut HeldBytes, set: &mut HashSet, item: T) -> JsResult { + if set.len() == set.capacity() && !set.contains(&item) { + let more = set.capacity().max(4); + held.add(interp, more.saturating_mul(size_of::() + 1).saturating_mul(16) / 7)?; + set.reserve(more); } - best + Ok(set.insert(item)) +} + +/// Makes room in `list` for `more` items, first holding the bytes of its new capacity when it must +/// grow. The capacity at least doubles, as a push's would. +fn reserve_held(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec, more: usize) -> JsResult<()> { + if list.capacity() - list.len() < more { + let cap = list.len().saturating_add(more).max(list.capacity().saturating_mul(2)); + held.add(interp, (cap - list.capacity()).saturating_mul(size_of::()))?; + list.reserve_exact(cap - list.len()); + } + Ok(()) +} + +/// The number `n` of a prefix written `ns` in decimal without leading zeros, as +/// [`Interp::fresh_prefix`] makes them. +fn prefix_number(prefix: &[u16]) -> Option { + let digits = prefix.strip_prefix(&[u16::from(b'n'), u16::from(b's')])?; + let (&first, _) = digits.split_first()?; + if digits.len() > 9 || (first == u16::from(b'0') && digits.len() > 1) { + return None; + } + digits.iter().try_fold(0usize, |n, &c| { + let d = c.checked_sub(u16::from(b'0')).filter(|d| *d < 10)?; + Some(n * 10 + usize::from(d)) + }) } fn prefix_in_scope<'a>(stack: &'a [XmlNs], prefix: &[u16]) -> Option<&'a XmlNs> { @@ -345,18 +360,6 @@ fn units_are(units: &[u16], text: &str) -> bool { units.iter().copied().eq(text.encode_utf16()) } -/// A prefix no entry of `stack` uses. -fn fresh_prefix(stack: &[XmlNs]) -> Key { - let mut n = 0u32; - loop { - let candidate = key(&format!("ns{n}")); - if prefix_in_scope(stack, &candidate).is_none() { - return candidate; - } - n += 1; - } -} - /// One open element while serializing. struct OutFrame { node: ObjId, @@ -579,13 +582,16 @@ impl Interp { // ---- names ---- - fn name_from_string(&mut self, s: &[u16]) -> JsResult { + /// The name `s` gives: an attribute name after `@`, which is copied after its units are + /// reserved, and otherwise an element name that shares `s`. + fn name_from_string(&mut self, s: &Key) -> JsResult { if let Some((&0x40, rest)) = s.split_first() { let uri = if rest == STAR { None } else { Some(Rc::from(Vec::new())) }; + reserve_units(self, rest.len())?; return Ok(NameRef { uri, local: Rc::from(rest), attribute: true }); } - let uri = if s == STAR { None } else { Some(self.xml_default_uri()?) }; - Ok(NameRef { uri, local: Rc::from(s), attribute: false }) + let uri = if **s == *STAR { None } else { Some(self.xml_default_uri()?) }; + Ok(NameRef { uri, local: s.clone(), attribute: false }) } /// ToXMLName, with a canonical array index read as a position (ECMA-357 section 10.6). @@ -593,7 +599,7 @@ impl Interp { match k { Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 && *n < 4_294_967_295.0 => Ok(Access::Index(*n as u32)), Value::String(s) => { - let flat = s.flat(); + let flat = flat_charged(self, s)?; if let Some(i) = canonical_index(&flat) { return Ok(Access::Index(i)); } @@ -603,7 +609,8 @@ impl Interp { if let Some(Class::QName { name, attribute, .. }) = self.heap.object(*o).map(|ob| &ob.class) { return Ok(Access::Name(NameRef { uri: name.uri.clone(), local: name.local.clone(), attribute: *attribute })); } - let s = self.to_str(k)?.flat(); + let s = self.to_str(k)?; + let s = flat_charged(self, &s)?; if let Some(i) = canonical_index(&s) { return Ok(Access::Index(i)); } @@ -787,31 +794,33 @@ impl Interp { if n.attribute { let count = self.xml_node(x)?.attrs.len(); self.xml_take_fuel(count)?; - let attrs = std::mem::take(&mut self.xml_node_mut(x)?.attrs); - let mut kept = Vec::with_capacity(attrs.len()); - for a in attrs { + let mut kept = 0; + for k in 0..count { + let Some(a) = self.xml_node(x)?.attrs.get(k).copied() else { break }; let hit = self.xml_node(a)?.name.as_ref().is_some_and(|q| n.matches(q)); if hit { self.xml_node_mut(a)?.parent = None; } else { - kept.push(a); + self.xml_node_mut(x)?.attrs.swap(kept, k); + kept += 1; } } - self.xml_node_mut(x)?.attrs = kept; + self.xml_node_mut(x)?.attrs.truncate(kept); return Ok(()); } let count = self.children_len(x)?; self.xml_take_fuel(count)?; - let children = std::mem::take(&mut self.xml_node_mut(x)?.children); - let mut kept = Vec::with_capacity(children.len()); - for c in children { + let mut kept = 0; + for k in 0..count { + let Some(c) = self.child_at(x, k)? else { break }; if n.matches_child(self.xml_node(c)?) { self.xml_node_mut(c)?.parent = None; } else { - kept.push(c); + self.xml_node_mut(x)?.children.swap(kept, k); + kept += 1; } } - self.xml_node_mut(x)?.children = kept; + self.xml_node_mut(x)?.children.truncate(kept); Ok(()) } @@ -939,7 +948,7 @@ impl Interp { self.use_fuel()?; cur = self.xml_node(a)?.parent; } - let height = self.node_height(v)?; + let height = self.rooted(&[Value::Object(x), Value::Object(v)], |i| i.node_height(v))?; if depth.saturating_add(height) > self.options.max_depth { return Err(Abrupt::Limit(LimitKind::Heap)); } @@ -971,7 +980,9 @@ impl Interp { /// The levels of the tree at `root`, counting it. fn node_height(&mut self, root: ObjId) -> JsResult { - let mut stack = vec![(root, 0usize)]; + let mut held = HeldBytes::new(self); + let mut stack = Vec::new(); + push_held(self, &mut held, &mut stack, (root, 0usize))?; let mut height = 1; while let Some(&(node, next)) = stack.last() { self.use_fuel()?; @@ -980,7 +991,7 @@ impl Interp { if let Some(top) = stack.last_mut() { top.1 += 1; } - stack.push((c, 0)); + push_held(self, &mut held, &mut stack, (c, 0))?; height = height.max(stack.len()); } None => { @@ -1049,19 +1060,22 @@ impl Interp { } /// Takes the fuel and heap to put every item of `l` under `x` and returns the nodes to place, - /// from [`Self::child_for_item`]. The tree does not change here, so a limit leaves it as it - /// was. - fn reserve_list_insert(&mut self, x: ObjId, l: ObjId) -> JsResult> { + /// from [`Self::child_for_item`], with the hold on the list of them, which the caller keeps + /// until they are placed. The tree does not change here, so a limit leaves it as it was. + fn reserve_list_insert(&mut self, x: ObjId, l: ObjId) -> JsResult<(Vec, HeldBytes)> { + let count = self.list_len(l)?; + let roots = [Value::Object(x), Value::Object(l)]; + let mut held = HeldBytes::new(self); + self.rooted(&roots, |i| held.add(i, size_of::().saturating_mul(count)))?; let mut items = self.xml_list(l)?.items.clone(); let bytes = XML_REF_BYTES.saturating_mul(items.len()); - let roots = [Value::Object(x), Value::Object(l)]; self.rooted(&roots, |i| i.xml_charge(bytes))?; let len = self.children_len(x)?; for (j, item) in items.iter_mut().enumerate() { *item = self.child_for_item(*item, &roots)?; self.place_fuel(len + j, *item)?; } - Ok(items) + Ok((items, held)) } /// Puts `items` at `at` in order, after [`Self::reserve_list_insert`]. @@ -1100,7 +1114,7 @@ impl Interp { self.insert_node(x, at, *v)?; } Content::List(l) => { - let items = self.reserve_list_insert(x, *l)?; + let (items, _held) = self.reserve_list_insert(x, *l)?; self.place_children(x, at, &items)?; } } @@ -1152,7 +1166,7 @@ impl Interp { } Content::List(l) => { self.check_attach(x, c)?; - let items = self.reserve_list_insert(x, *l)?; + let (items, _held) = self.reserve_list_insert(x, *l)?; let len = self.children_len(x)?; self.xml_text_fuel(len)?; if at < len { @@ -1229,7 +1243,8 @@ impl Interp { s.push_units(self, &[SPACE])?; } let Some(item) = self.list_at(*l, i)? else { break }; - let text = self.node_to_str(item)?.flat(); + let text = self.node_to_str(item)?; + let text = flat_charged(self, &text)?; s.push_units(self, &text)?; } let Value::String(s) = s.finish(self) else { return Err(Abrupt::Internal("not a string".into())) }; @@ -1240,9 +1255,9 @@ impl Interp { let mut found = None; let count = self.xml_node(x)?.attrs.len(); self.xml_take_fuel(count)?; - let attrs = std::mem::take(&mut self.xml_node_mut(x)?.attrs); - let mut kept = Vec::with_capacity(attrs.len()); - for a in attrs { + let mut kept = 0; + for k in 0..count { + let Some(a) = self.xml_node(x)?.attrs.get(k).copied() else { break }; let hit = self.xml_node(a)?.name.as_ref().is_some_and(|q| n.matches(q)); if hit && found.is_some() { self.xml_node_mut(a)?.parent = None; @@ -1250,10 +1265,11 @@ impl Interp { if hit { found = Some(a); } - kept.push(a); + self.xml_node_mut(x)?.attrs.swap(kept, k); + kept += 1; } } - self.xml_node_mut(x)?.attrs = kept; + self.xml_node_mut(x)?.attrs.truncate(kept); if let Some(a) = found { self.xml_node_mut(a)?.value = value; return Ok(()); @@ -1280,11 +1296,13 @@ impl Interp { } let len = self.children_len(x)?; self.xml_take_fuel(len)?; + self.xml_keep(x); + let mut held = HeldBytes::new(self); let mut matches = Vec::new(); for k in 0..len { let Some(child) = self.child_at(x, k)? else { break }; if n.matches_child(self.xml_node(child)?) { - matches.push(child); + push_held(self, &mut held, &mut matches, child)?; } } match c { @@ -1378,6 +1396,8 @@ impl Interp { /// The steps of [`Self::resolve_value`], setting `created` to the first element they add. fn resolve_value_adding(&mut self, x: ObjId, created: &mut Option) -> JsResult> { + self.xml_keep(x); + let mut held = HeldBytes::new(self); let mut chain = Vec::new(); let mut cur = x; let base = loop { @@ -1391,7 +1411,7 @@ impl Interp { if *name.local == *STAR { return Ok(None); } - chain.push(l); + push_held(self, &mut held, &mut chain, l)?; cur = target; }; let mut base = base; @@ -1615,6 +1635,9 @@ impl Interp { Some(parent) => self.index_in_parent(parent, item)?.map(|q| (parent, q)), None => None, }; + let count = self.list_len(c)?; + let mut held = HeldBytes::new(self); + self.rooted(&[Value::Object(x), Value::Object(c)], |i| held.add(i, size_of::().saturating_mul(count)))?; let mut items = self.xml_list(c)?.items.clone(); if place.is_some() { let roots = [Value::Object(x), Value::Object(c)]; @@ -1682,6 +1705,8 @@ impl Interp { // ---- copies ---- fn copy_shallow(&mut self, src: ObjId, parent: Option) -> JsResult { + let declared = self.xml_node(src)?.ns.len(); + self.xml_charge(XML_NS_BYTES.saturating_mul(declared))?; let node = self.xml_node(src)?; let copy = XmlNode { kind: node.kind, @@ -1692,11 +1717,11 @@ impl Interp { children: Vec::new(), ns: node.ns.clone(), }; - let attrs = node.attrs.clone(); - self.xml_charge(XML_NS_BYTES.saturating_mul(copy.ns.len()))?; let c = self.new_xml_node(copy)?; self.xml_keep(c); - for a in attrs { + let mut k = 0; + while let Some(a) = self.xml_node(src)?.attrs.get(k).copied() { + k += 1; self.use_fuel()?; let n = self.xml_node(a)?; let attr = XmlNode { @@ -1721,7 +1746,9 @@ impl Interp { let top = i.copy_shallow(root, None)?; i.xml_keep(top); let mark = i.temp_roots.len(); - let mut stack = vec![(root, top, 0usize)]; + let mut held = HeldBytes::new(i); + let mut stack = Vec::new(); + push_held(i, &mut held, &mut stack, (root, top, 0usize))?; while let Some(&(src, dst, next)) = stack.last() { i.use_fuel()?; i.temp_roots.truncate(mark); @@ -1733,7 +1760,7 @@ impl Interp { let c = i.copy_shallow(child, Some(dst))?; i.xml_charge_link(c)?; i.xml_node_mut(dst)?.children.push(c); - stack.push((child, c, 0)); + push_held(i, &mut held, &mut stack, (child, c, 0))?; } None => { stack.pop(); @@ -1778,9 +1805,18 @@ impl Interp { // ---- comparison ---- + /// The namespace, local name and value of the attribute `id`, flattening a value that is a + /// rope with [`flat_charged`]. + fn attr_key(&mut self, id: ObjId) -> JsResult<(Option, Key, Key)> { + let n = self.xml_node(id)?; + let name = n.name.as_ref().ok_or_else(|| Abrupt::Internal("attribute without a name".into()))?; + let (uri, local, value) = (name.uri.clone(), name.local.clone(), n.value.clone()); + Ok((uri, local, flat_charged(self, &value)?)) + } + fn same_shallow(&mut self, a: ObjId, b: ObjId) -> JsResult { let (x, y) = (self.xml_node(a)?, self.xml_node(b)?); - if x.kind != y.kind || x.attrs.len() != y.attrs.len() || x.children.len() != y.children.len() || x.value.flat() != y.value.flat() { + if x.kind != y.kind || x.attrs.len() != y.attrs.len() || x.children.len() != y.children.len() || x.value.len() != y.value.len() { return Ok(false); } match (&x.name, &y.name) { @@ -1788,26 +1824,25 @@ impl Interp { (Some(p), Some(q)) if p.local == q.local && p.uri == q.uri => {} _ => return Ok(false), } - if x.attrs.is_empty() { + let (xv, yv) = (x.value.clone(), y.value.clone()); + if flat_charged(self, &xv)? != flat_charged(self, &yv)? { + return Ok(false); + } + let count = self.xml_node(a)?.attrs.len(); + if count == 0 { return Ok(true); } - let count = x.attrs.len(); self.xml_take_fuel(count)?; let mut held = HeldBytes::new(self); held.add(self, ATTR_COUNT_BYTES.saturating_mul(count))?; - let (x, y) = (self.xml_node(a)?, self.xml_node(b)?); - let (xa, ya) = (x.attrs.clone(), y.attrs.clone()); let mut counts: HashMap<(Option, Key, Key), usize> = HashMap::new(); - let attr_key = |i: &Self, id: ObjId| -> JsResult<(Option, Key, Key)> { - let n = i.xml_node(id)?; - let name = n.name.as_ref().ok_or_else(|| Abrupt::Internal("attribute without a name".into()))?; - Ok((name.uri.clone(), name.local.clone(), n.value.flat())) - }; - for a in xa { - *counts.entry(attr_key(self, a)?).or_default() += 1; + for k in 0..count { + let Some(id) = self.xml_node(a)?.attrs.get(k).copied() else { break }; + *counts.entry(self.attr_key(id)?).or_default() += 1; } - for b in ya { - match counts.get_mut(&attr_key(self, b)?) { + for k in 0..count { + let Some(id) = self.xml_node(b)?.attrs.get(k).copied() else { break }; + match counts.get_mut(&self.attr_key(id)?) { Some(n) if *n > 0 => *n -= 1, _ => return Ok(false), } @@ -1884,7 +1919,8 @@ impl Interp { let (xk, yk) = (self.xml_kind(x)?, self.xml_kind(y)?); let textual = |k| matches!(k, XmlKind::Text | XmlKind::Attribute); if (textual(xk) && self.node_simple(y)?) || (textual(yk) && self.node_simple(x)?) { - return Ok(Some(self.node_to_str(x)?.flat() == self.node_to_str(y)?.flat())); + let (s, other) = (self.node_to_str(x)?, self.node_to_str(y)?); + return Ok(Some(flat_charged(self, &s)? == flat_charged(self, &other)?)); } return self.node_equals(x, y).map(Some); } @@ -1900,9 +1936,9 @@ impl Interp { if let Xv::Node(n) = self.classify(x) && self.node_simple(n)? { - let s = self.node_to_str(n)?.flat(); - let other = self.to_str(y)?.flat(); - return Ok(Some(s == other)); + let s = self.node_to_str(n)?; + let other = self.to_str(y)?; + return Ok(Some(flat_charged(self, &s)? == flat_charged(self, &other)?)); } } Ok(None) @@ -1951,7 +1987,9 @@ impl Interp { /// The nodes under `x` matching `n`, in document order ([[Descendants]], section 9.1.1.8). pub(crate) fn node_descendants(&mut self, x: ObjId, n: &NameRef, out: ObjId) -> JsResult<()> { - let mut stack = vec![(x, 0usize)]; + let mut held = HeldBytes::new(self); + let mut stack = Vec::new(); + push_held(self, &mut held, &mut stack, (x, 0usize))?; if n.attribute { self.collect_matching(x, n, out)?; } @@ -1969,14 +2007,16 @@ impl Interp { } else if n.matches_child(self.xml_node(child)?) { self.list_push(out, child)?; } - stack.push((child, 0)); + push_held(self, &mut held, &mut stack, (child, 0))?; } Ok(()) } /// Merges adjacent text and removes empty text under `root` (ECMA-357 section 13.4.4.26). pub(crate) fn node_normalize(&mut self, root: ObjId) -> JsResult<()> { - let mut stack = vec![(root, 0usize)]; + let mut held = HeldBytes::new(self); + let mut stack = Vec::new(); + push_held(self, &mut held, &mut stack, (root, 0usize))?; while let Some(&(x, mut i)) = stack.last() { self.use_fuel()?; let Some(child) = self.child_at(x, i)? else { @@ -2010,7 +2050,7 @@ impl Interp { frame.1 = i + 1; } if kind == XmlKind::Element { - stack.push((child, 0)); + push_held(self, &mut held, &mut stack, (child, 0))?; } } Ok(()) @@ -2032,7 +2072,8 @@ impl Interp { let Some(c) = self.child_at(x, i)? else { break }; let node = self.xml_node(c)?; if !matches!(node.kind, XmlKind::Comment | XmlKind::Pi) { - let text = node.value.flat(); + let text = node.value.clone(); + let text = flat_charged(self, &text)?; out.push_units(self, &text)?; } } @@ -2069,7 +2110,8 @@ impl Interp { self.use_fuel()?; let Some(item) = self.list_at(l, i)? else { break }; if !matches!(self.xml_kind(item)?, XmlKind::Comment | XmlKind::Pi) { - let text = self.node_to_str(item)?.flat(); + let text = self.node_to_str(item)?; + let text = flat_charged(self, &text)?; out.push_units(self, &text)?; } } @@ -2107,6 +2149,46 @@ impl Interp { out.push_units(self, text.get(from..).unwrap_or_default()) } + /// The namespace `stack` declares for `uri`, nearest first, skipping prefixes redeclared nearer. + /// `prefer` picks that prefix when several match; `need_prefix` rejects the default namespace. + /// The set of prefixes it has passed is held against the heap limit. + fn ns_lookup(&mut self, stack: &[XmlNs], uri: &Key, prefer: Option<&Key>, need_prefix: bool) -> JsResult> { + let mut held = HeldBytes::new(self); + let mut seen: HashSet<&[u16]> = HashSet::new(); + let mut best = None; + for ns in stack.iter().rev() { + let Some(p) = &ns.prefix else { continue }; + if !insert_held(self, &mut held, &mut seen, &p[..])? { + continue; + } + if ns.uri == *uri && !(need_prefix && p.is_empty()) { + if prefer == Some(p) { + return Ok(Some(ns.clone())); + } + if best.is_none() { + best = Some(ns.clone()); + } + } + } + Ok(best) + } + + /// The first of `ns0`, `ns1`, ... that no entry of `stack` uses. One of the first + /// `stack.len() + 1` is free, so one pass over `stack` marks the used ones. + fn fresh_prefix(&mut self, stack: &[XmlNs]) -> JsResult { + self.xml_take_fuel(stack.len() + 1)?; + let mut held = HeldBytes::new(self); + held.add(self, stack.len() + 1)?; + let mut used = vec![false; stack.len() + 1]; + for ns in stack { + if let Some(slot) = ns.prefix.as_deref().and_then(prefix_number).and_then(|n| used.get_mut(n)) { + *slot = true; + } + } + let n = used.iter().position(|u| !u).unwrap_or(stack.len()); + Ok(key(&format!("ns{n}"))) + } + /// Picks the prefix of a name in `scope`, adding a declaration to `scope` when none is there. fn out_prefix(&mut self, name: &XmlName, scope: &mut Vec, attribute: bool) -> JsResult { self.xml_take_fuel(scope.len() + 1)?; @@ -2122,13 +2204,13 @@ impl Interp { if *uri == *key(XML_URI) { return Ok(key("xml")); } - if let Some(ns) = ns_lookup(scope, &uri, name.prefix.as_ref(), attribute) { + if let Some(ns) = self.ns_lookup(scope, &uri, name.prefix.as_ref(), attribute)? { return Ok(ns.prefix.unwrap_or(empty)); } let prefix = match &name.prefix { Some(p) if !(attribute && p.is_empty()) && prefix_in_scope(scope, p).is_none() => p.clone(), _ if !attribute && prefix_in_scope(scope, &empty).is_none() => empty, - _ => fresh_prefix(scope), + _ => self.fresh_prefix(scope)?, }; self.xml_charge(XML_NS_BYTES)?; scope.push(XmlNs { prefix: Some(prefix.clone()), uri }); @@ -2143,8 +2225,7 @@ impl Interp { out.push_units(self, local) } - /// Writes `x`; an element with children is left open on `stack`. - #[allow(clippy::too_many_arguments)] + /// Writes `x`, and returns the frame of an element with children, which is left open. fn write_enter( &mut self, out: &mut Builder, @@ -2153,11 +2234,11 @@ impl Interp { settings: &Settings, pretty: bool, scope: &mut Vec, - stack: &mut Vec, - ) -> JsResult<()> { + ) -> JsResult> { self.use_fuel()?; let node = self.xml_node(x)?; - let (kind, value) = (node.kind, node.value.flat()); + let (kind, value) = (node.kind, node.value.clone()); + let value = flat_charged(self, &value)?; self.xml_text_fuel(value.len())?; if pretty { out.push_repeated(self, &[SPACE], indent)?; @@ -2165,13 +2246,18 @@ impl Interp { match kind { XmlKind::Text => { let trimmed: &[u16] = if pretty { trim_xml_space(&value) } else { &value }; - return self.write_escaped(out, trimmed, false); + self.write_escaped(out, trimmed, false)?; + return Ok(None); + } + XmlKind::Attribute => { + self.write_escaped(out, &value, true)?; + return Ok(None); } - XmlKind::Attribute => return self.write_escaped(out, &value, true), XmlKind::Comment => { out.push_str(self, ""); + out.push_str(self, "-->")?; + return Ok(None); } XmlKind::Pi => { let target = self.xml_node(x)?.name.as_ref().map(|n| n.local.clone()).unwrap_or_else(|| Rc::from(Vec::new())); @@ -2179,47 +2265,46 @@ impl Interp { out.push_units(self, &target)?; out.push_units(self, &[SPACE])?; out.push_units(self, &value)?; - return out.push_str(self, "?>"); + out.push_str(self, "?>")?; + return Ok(None); } XmlKind::Element => {} } let ns_mark = scope.len(); let node = self.xml_node(x)?; - let declared = node.ns.clone(); - let attrs = node.attrs.clone(); + let declared = node.ns.len(); let name = node.name.clone().ok_or_else(|| Abrupt::Internal("element without a name".into()))?; - self.xml_take_fuel(declared.len().saturating_mul(scope.len() + 1))?; - let mut unnamed = Vec::new(); - for ns in declared { - if ns.prefix.is_none() { - unnamed.push(ns); - } else if !scope.iter().any(|a| a.uri == ns.uri && a.prefix == ns.prefix) { - self.xml_charge(XML_NS_BYTES)?; - scope.push(ns); - } - } - for ns in unnamed { - if !scope.iter().any(|a| a.uri == ns.uri) { - let prefix = fresh_prefix(scope); - self.xml_charge(XML_NS_BYTES)?; - scope.push(XmlNs { prefix: Some(prefix), uri: ns.uri }); + self.xml_take_fuel(declared.saturating_mul(scope.len() + 1))?; + for unnamed in [false, true] { + let mut k = 0; + while let Some(ns) = self.xml_node(x)?.ns.get(k).cloned() { + k += 1; + if ns.prefix.is_none() != unnamed { + continue; + } + if !unnamed && !scope.iter().any(|a| a.uri == ns.uri && a.prefix == ns.prefix) { + self.xml_charge(XML_NS_BYTES)?; + scope.push(ns); + } else if unnamed && !scope.iter().any(|a| a.uri == ns.uri) { + let prefix = self.fresh_prefix(scope)?; + self.xml_charge(XML_NS_BYTES)?; + scope.push(XmlNs { prefix: Some(prefix), uri: ns.uri }); + } } } let prefix = self.out_prefix(&name, scope, false)?; - let mut attr_prefixes = Vec::new(); - for &a in &attrs { - self.use_fuel()?; - let an = self.xml_node(a)?.name.clone().ok_or_else(|| Abrupt::Internal("attribute without a name".into()))?; - attr_prefixes.push(self.out_prefix(&an, scope, true)?); - } out.push_units(self, &[LT])?; self.write_name(out, &prefix, &name.local)?; - for (&a, attr_prefix) in attrs.iter().zip(&attr_prefixes) { - let an = self.xml_node(a)?; - let local = an.name.as_ref().map(|q| q.local.clone()).unwrap_or_else(|| Rc::from(Vec::new())); - let value = an.value.flat(); + let mut k = 0; + while let Some(a) = self.xml_node(x)?.attrs.get(k).copied() { + k += 1; + self.use_fuel()?; + let an = self.xml_node(a)?.name.clone().ok_or_else(|| Abrupt::Internal("attribute without a name".into()))?; + let attr_prefix = self.out_prefix(&an, scope, true)?; + let value = self.xml_node(a)?.value.clone(); + let value = flat_charged(self, &value)?; out.push_units(self, &[SPACE])?; - self.write_name(out, attr_prefix, &local)?; + self.write_name(out, &attr_prefix, &an.local)?; out.push_units(self, &[EQ, DQUOTE])?; self.write_escaped(out, &value, true)?; out.push_units(self, &[DQUOTE])?; @@ -2239,7 +2324,8 @@ impl Interp { let count = self.children_len(x)?; if count == 0 { scope.truncate(ns_mark); - return out.push_str(self, "/>"); + out.push_str(self, "/>")?; + return Ok(None); } out.push_units(self, &[GT])?; let first_is_text = match self.child_at(x, 0)? { @@ -2248,14 +2334,17 @@ impl Interp { }; let pretty_children = pretty && (count > 1 || !first_is_text); let child_indent = if pretty_children { indent.saturating_add(u64::from(settings.indent)) } else { 0 }; - stack.push(OutFrame { node: x, next: 0, indent, child_indent, pretty_children, ns_mark, prefix, local: name.local }); - Ok(()) + Ok(Some(OutFrame { node: x, next: 0, indent, child_indent, pretty_children, ns_mark, prefix, local: name.local })) } + /// Writes the tree at `root`. Its open elements are held against the heap limit. fn write_node(&mut self, out: &mut Builder, root: ObjId, indent: u64, settings: &Settings, pretty: bool) -> JsResult<()> { let mut scope: Vec = Vec::new(); + let mut held = HeldBytes::new(self); let mut stack: Vec = Vec::new(); - self.write_enter(out, root, indent, settings, pretty, &mut scope, &mut stack)?; + if let Some(frame) = self.write_enter(out, root, indent, settings, pretty, &mut scope)? { + push_held(self, &mut held, &mut stack, frame)?; + } while let Some(frame) = stack.last_mut() { let (node, next, child_indent, pretty_children) = (frame.node, frame.next, frame.child_indent, frame.pretty_children); frame.next += 1; @@ -2264,7 +2353,9 @@ impl Interp { if pretty_children { out.push_units(self, &[LF])?; } - self.write_enter(out, child, child_indent, settings, pretty && pretty_children, &mut scope, &mut stack)?; + if let Some(frame) = self.write_enter(out, child, child_indent, settings, pretty && pretty_children, &mut scope)? { + push_held(self, &mut held, &mut stack, frame)?; + } } None => { let Some(frame) = stack.pop() else { break }; @@ -2311,8 +2402,11 @@ impl Interp { self.throw(ErrorType::Syntax, message) } - fn decode_into(&mut self, src: &[u16], from: usize, to: usize, out: &mut Vec, attribute: bool) -> JsResult<()> { + /// Decodes `src[from..to]` onto `out`, holding the room it takes in `out` with `held` first. The + /// decoded text is never longer than the source. + fn decode_into(&mut self, src: &[u16], from: usize, to: usize, out: &mut Vec, attribute: bool, held: &mut HeldBytes) -> JsResult<()> { self.xml_text_fuel(to.saturating_sub(from))?; + reserve_held(self, held, out, to.saturating_sub(from))?; let mut pos = from; while pos < to { let Some(&c) = src.get(pos) else { break }; @@ -2427,7 +2521,16 @@ impl Interp { Ok(()) } - fn flush_text(&mut self, text: &mut Vec, ignore_whitespace: bool, open: &[ObjId], roots: &mut Vec) -> JsResult<()> { + /// Makes a text node of `text`, unless it is empty or ignored white space. Taking the buffer + /// releases `text_held`, its hold. + fn flush_text( + &mut self, + text: &mut Vec, + text_held: &mut HeldBytes, + ignore_whitespace: bool, + open: &[ObjId], + roots: &mut Vec, + ) -> JsResult<()> { if text.is_empty() { return Ok(()); } @@ -2435,16 +2538,22 @@ impl Interp { text.clear(); return Ok(()); } - let t = self.new_text_node(Str::from_units(std::mem::take(text)), None)?; + let value = Str::from_units(std::mem::take(text)); + *text_held = HeldBytes::new(self); + let t = self.new_text_node(value, None)?; self.place_parsed(t, open, roots) } + /// Parses the open tag at `src[pos]` and returns the offset after it. An element left open is + /// pushed onto `open` and its name onto `names`, both held with `stack_held`. The attributes, + /// sets and copies it works with are held until it returns. fn parse_open_tag( &mut self, src: &[u16], pos: usize, open: &mut Vec, names: &mut Vec<(usize, usize)>, + stack_held: &mut HeldBytes, roots: &mut Vec, ) -> JsResult { if open.len() >= self.options.max_depth { @@ -2455,6 +2564,7 @@ impl Interp { let raw_name = src.get(name_start..name_end).unwrap_or_default(); let (elem_prefix, elem_local) = self.split_name(raw_name)?; let mut at = name_end; + let mut held = HeldBytes::new(self); let mut raw_attrs: Vec<(usize, usize, Vec)> = Vec::new(); let closes = loop { let before = at; @@ -2492,27 +2602,31 @@ impl Interp { return Err(self.syntax("'<' cannot appear in an attribute value")); } let mut value = Vec::new(); - self.decode_into(src, value_start, value_end, &mut value, true)?; - raw_attrs.push((at, end, value)); + self.decode_into(src, value_start, value_end, &mut value, true, &mut held)?; + push_held(self, &mut held, &mut raw_attrs, (at, end, value))?; at = value_end + 1; } } }; self.xml_charge(XML_NS_BYTES.saturating_mul(raw_attrs.len()))?; - let mut ns: Vec = Vec::new(); - let mut seen_decl: HashSet> = HashSet::new(); + let xmlns = key("xmlns"); + let declares = |raw: &[u16]| raw == &xmlns[..] || raw.strip_prefix(&xmlns[..]).is_some_and(|rest| rest.first() == Some(&COLON)); + let decls = raw_attrs.iter().filter(|(start, end, _)| declares(src.get(*start..*end).unwrap_or_default())).count(); + let mut ns: Vec = Vec::with_capacity(decls); + let mut seen_decl: HashSet<&[u16]> = HashSet::new(); for (start, end, value) in &raw_attrs { let raw = src.get(*start..*end).unwrap_or_default(); - if raw == key("xmlns").as_ref() { + if raw == &xmlns[..] { if !ns_binding_allowed(&[], value) { return Err(self.syntax(RESERVED_NS_BINDING)); } - if !seen_decl.insert(Vec::new()) { + if !insert_held(self, &mut held, &mut seen_decl, &[] as &[u16])? { return Err(self.syntax("a namespace is declared twice")); } + self.xml_charge(string_bytes(0).saturating_add(string_bytes(value.len())))?; ns.push(XmlNs { prefix: Some(Rc::from(Vec::new())), uri: Rc::from(value.as_slice()) }); } else if let (Some(p), local) = self.split_name(raw)? - && p == key("xmlns").as_ref() + && p == &xmlns[..] { if value.is_empty() { return Err(self.syntax("a namespace prefix cannot be declared with an empty URI")); @@ -2520,9 +2634,10 @@ impl Interp { if !ns_binding_allowed(local, value) { return Err(self.syntax(RESERVED_NS_BINDING)); } - if !seen_decl.insert(local.to_vec()) { + if !insert_held(self, &mut held, &mut seen_decl, local)? { return Err(self.syntax("a namespace is declared twice")); } + self.xml_charge(string_bytes(local.len()).saturating_add(string_bytes(value.len())))?; ns.push(XmlNs { prefix: Some(Rc::from(local)), uri: Rc::from(value.as_slice()) }); } } @@ -2547,7 +2662,8 @@ impl Interp { ns, })?; self.place_parsed(element, open, roots)?; - let mut unique: HashSet<(Key, Vec)> = HashSet::new(); + let mut unique: HashSet<(Key, &[u16])> = HashSet::new(); + let mut own: Option> = None; for (start, end, value) in raw_attrs { self.use_fuel()?; let raw = src.get(start..end).unwrap_or_default(); @@ -2557,15 +2673,19 @@ impl Interp { } let (auri, stored_prefix): (Key, Key) = match prefix { Some(p) => { - let own = self.xml_node(element)?.ns.clone(); - match self.resolve_prefix(p, &own, open)? { + if own.is_none() { + let count = self.xml_node(element)?.ns.len(); + held.add(self, size_of::().saturating_mul(count))?; + own = Some(self.xml_node(element)?.ns.clone()); + } + match self.resolve_prefix(p, own.as_deref().unwrap_or_default(), open)? { Some(u) => (u, Rc::from(p)), None => return Err(self.syntax("an XML name uses an undeclared namespace prefix")), } } None => (Rc::from(Vec::new()), Rc::from(Vec::new())), }; - if !unique.insert((auri.clone(), local.to_vec())) { + if !insert_held(self, &mut held, &mut unique, (auri.clone(), local))? { return Err(self.type_error("an XML element has two attributes with the same name")); } let a = self.new_xml_node(XmlNode { @@ -2581,8 +2701,8 @@ impl Interp { self.xml_node_mut(element)?.attrs.push(a); } if !closes { - open.push(element); - names.push((name_start, name_end)); + push_held(self, stack_held, open, element)?; + push_held(self, stack_held, names, (name_start, name_end))?; } Ok(at) } @@ -2605,10 +2725,14 @@ impl Interp { } /// Parses XML text into nodes, kept reachable through the temporary roots until the caller's - /// scope ends. + /// scope ends. The open elements and their names, the text being gathered and each tag's + /// attributes are held as they grow. The allowance held from the source's length covers the + /// copy a value's units take as it becomes a string, before the node that holds it is charged. pub(crate) fn xml_parse(&mut self, source: &[u16]) -> JsResult> { let mut held = HeldBytes::new(self); held.add(self, string_bytes(source.len()).saturating_mul(2))?; + let mut stack_held = HeldBytes::new(self); + let mut text_held = HeldBytes::new(self); let settings = self.xml_settings()?; let mut src = source; let offset_start = skip_xml_space(src, 0); @@ -2638,7 +2762,7 @@ impl Interp { self.xml_text_fuel(end - pos)?; return Err(self.syntax("']]>' cannot appear in XML text")); } - self.decode_into(src, pos, end, &mut text, false)?; + self.decode_into(src, pos, end, &mut text, false, &mut text_held)?; pos = end; continue; } @@ -2650,11 +2774,13 @@ impl Interp { }; self.xml_text_fuel(end - pos)?; self.check_units(src, pos + 9, end)?; - text.extend_from_slice(src.get(pos + 9..end).unwrap_or_default()); + let section = src.get(pos + 9..end).unwrap_or_default(); + reserve_held(self, &mut text_held, &mut text, section.len())?; + text.extend_from_slice(section); pos = end + 3; continue; } - self.flush_text(&mut text, settings.ignore_whitespace, &open, &mut roots)?; + self.flush_text(&mut text, &mut text_held, settings.ignore_whitespace, &open, &mut roots)?; if rest.starts_with(&parser_units("