mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 23:32:14 +00:00
JavaScript: a limit during an XML insert, delete or put leaves the tree intact
insertChild, prependChild, replace and list assignment take their heap and fuel before moving a node out of its old parent. Replace past the last child charges before the push, so a child is never left without its parent link. Deleting a child or attribute by name, assigning an attribute or assigning text to an element take the fuel for the whole step before emptying the node. normalize removes the next text node before it stores the joined text.
This commit is contained in:
+45
-20
@@ -616,22 +616,22 @@ impl Interp {
|
||||
}
|
||||
|
||||
fn node_delete_at(&mut self, x: ObjId, i: usize) -> JsResult<()> {
|
||||
let node = self.xml_node_mut(x)?;
|
||||
if i < node.children.len() {
|
||||
let c = node.children.remove(i);
|
||||
let len = node.children.len();
|
||||
self.xml_node_mut(c)?.parent = None;
|
||||
let len = self.children_len(x)?;
|
||||
if i < len {
|
||||
self.xml_text_fuel(len)?;
|
||||
let c = self.xml_node_mut(x)?.children.remove(i);
|
||||
self.xml_node_mut(c)?.parent = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn node_delete_name(&mut self, x: ObjId, n: &NameRef) -> JsResult<()> {
|
||||
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 {
|
||||
self.use_fuel()?;
|
||||
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;
|
||||
@@ -642,10 +642,11 @@ impl Interp {
|
||||
self.xml_node_mut(x)?.attrs = 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 {
|
||||
self.use_fuel()?;
|
||||
if n.matches_child(self.xml_node(c)?) {
|
||||
self.xml_node_mut(c)?.parent = None;
|
||||
} else {
|
||||
@@ -842,15 +843,21 @@ impl Interp {
|
||||
Ok(if parent == x && q < at { at - 1 } else { at })
|
||||
}
|
||||
|
||||
/// Charges for one more child of `x`. `x` and `v` are held as roots across any collection.
|
||||
fn xml_charge_child(&mut self, x: ObjId, v: ObjId) -> JsResult<()> {
|
||||
self.rooted(&[Value::Object(x), Value::Object(v)], |i| i.xml_charge(XML_REF_BYTES))
|
||||
}
|
||||
|
||||
/// Takes the fuel and heap for the insertion first, so a limit leaves `v` where it was.
|
||||
fn insert_node(&mut self, x: ObjId, at: usize, v: ObjId) -> JsResult<usize> {
|
||||
self.xml_charge_child(x, v)?;
|
||||
let len = self.children_len(x)?;
|
||||
self.xml_text_fuel(len + 1)?;
|
||||
let at = self.detach(x, v, at)?;
|
||||
self.xml_charge(XML_REF_BYTES)?;
|
||||
let node = self.xml_node_mut(x)?;
|
||||
let at = at.min(node.children.len());
|
||||
node.children.insert(at, v);
|
||||
let len = node.children.len();
|
||||
self.xml_node_mut(v)?.parent = Some(x);
|
||||
self.xml_text_fuel(len)?;
|
||||
Ok(at)
|
||||
}
|
||||
|
||||
@@ -883,6 +890,15 @@ impl Interp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Charges for a [`Self::set_child`] of `v` at `at` that adds a child.
|
||||
fn charge_set_child(&mut self, x: ObjId, at: usize, v: ObjId) -> JsResult<()> {
|
||||
if at < self.children_len(x)? {
|
||||
return Ok(());
|
||||
}
|
||||
self.xml_charge_child(x, v)
|
||||
}
|
||||
|
||||
/// Puts `v` at `at`, or last when `at` is past the end, after [`Self::charge_set_child`].
|
||||
fn set_child(&mut self, x: ObjId, at: usize, v: ObjId) -> JsResult<()> {
|
||||
let node = self.xml_node_mut(x)?;
|
||||
let old = match node.children.get_mut(at) {
|
||||
@@ -892,9 +908,6 @@ impl Interp {
|
||||
None
|
||||
}
|
||||
};
|
||||
if old.is_none() {
|
||||
self.xml_charge(XML_REF_BYTES)?;
|
||||
}
|
||||
if let Some(old) = old {
|
||||
self.xml_node_mut(old)?.parent = None;
|
||||
}
|
||||
@@ -914,6 +927,7 @@ impl Interp {
|
||||
if self.child_at(x, at)? == Some(*v) {
|
||||
return Ok(());
|
||||
}
|
||||
self.charge_set_child(x, at, *v)?;
|
||||
let at = self.detach(x, *v, at)?;
|
||||
self.set_child(x, at, *v)
|
||||
}
|
||||
@@ -926,11 +940,13 @@ impl Interp {
|
||||
let s = self.xml_node(*v)?.value.clone();
|
||||
let t = self.new_text_node(s, Some(x))?;
|
||||
self.xml_keep(t);
|
||||
self.charge_set_child(x, at, t)?;
|
||||
self.set_child(x, at, t)
|
||||
}
|
||||
Content::Text(s) => {
|
||||
let t = self.new_text_node(s.clone(), Some(x))?;
|
||||
self.xml_keep(t);
|
||||
self.charge_set_child(x, at, t)?;
|
||||
self.set_child(x, at, t)
|
||||
}
|
||||
}
|
||||
@@ -983,10 +999,11 @@ impl Interp {
|
||||
self.content_to_str(c)?
|
||||
};
|
||||
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 {
|
||||
self.use_fuel()?;
|
||||
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;
|
||||
@@ -1054,6 +1071,7 @@ impl Interp {
|
||||
})?;
|
||||
self.xml_keep(y);
|
||||
self.check_attach_node(x, y)?;
|
||||
self.charge_set_child(x, at, y)?;
|
||||
self.set_child(x, at, y)?;
|
||||
}
|
||||
at
|
||||
@@ -1061,15 +1079,22 @@ impl Interp {
|
||||
};
|
||||
if primitive {
|
||||
let Some(target) = self.child_at(x, at)? else { return Ok(()) };
|
||||
let s = self.content_to_str(c)?;
|
||||
let count = self.children_len(target)?;
|
||||
self.xml_take_fuel(count)?;
|
||||
let text = if s.len() > 0 {
|
||||
let t = self.new_text_node(s, Some(target))?;
|
||||
self.xml_keep(t);
|
||||
self.xml_charge_child(target, t)?;
|
||||
Some(t)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let children = std::mem::take(&mut self.xml_node_mut(target)?.children);
|
||||
for child in children {
|
||||
self.use_fuel()?;
|
||||
self.xml_node_mut(child)?.parent = None;
|
||||
}
|
||||
let s = self.content_to_str(c)?;
|
||||
if s.len() > 0 {
|
||||
self.xml_replace(target, 0, &Content::Text(s))?;
|
||||
}
|
||||
self.xml_node_mut(target)?.children.extend(text);
|
||||
Ok(())
|
||||
} else {
|
||||
self.xml_replace(x, at, c)
|
||||
@@ -1621,8 +1646,8 @@ impl Interp {
|
||||
let (p, q) = (self.xml_node(child)?.value.clone(), self.xml_node(next)?.value.clone());
|
||||
self.concat(&p, &q)?
|
||||
};
|
||||
self.xml_node_mut(child)?.value = joined;
|
||||
self.node_delete_at(x, i + 1)?;
|
||||
self.xml_node_mut(child)?.value = joined;
|
||||
}
|
||||
if self.xml_node(child)?.value.len() == 0 {
|
||||
self.node_delete_at(x, i)?;
|
||||
|
||||
@@ -589,6 +589,109 @@ fn limits_cannot_be_caught_by_the_script() {
|
||||
assert_eq!(kind_of(TIGHT, "try { for (;;) (<a/>).toXMLString(); } catch (e) { 1 }"), RunErrorKind::Limit(LimitKind::Fuel));
|
||||
}
|
||||
|
||||
/// Collects garbage and sets the heap limit to the live size plus the argument, so a script can
|
||||
/// run out of heap at a chosen charge. A negative argument puts the heap over its limit, which
|
||||
/// also happens when a lowered limit meets a larger heap.
|
||||
fn squeeze(
|
||||
i: &mut crate::interp::Interp,
|
||||
_: crate::value::Value,
|
||||
args: &[crate::value::Value],
|
||||
_: bool,
|
||||
) -> crate::interp::JsResult<crate::value::Value> {
|
||||
i.collect_garbage();
|
||||
if let Some(crate::value::Value::Number(spare)) = args.first() {
|
||||
i.limits.heap_bytes = (i.heap.bytes as f64 + *spare) as usize;
|
||||
}
|
||||
Ok(crate::value::Value::Undefined)
|
||||
}
|
||||
|
||||
/// Runs `setup`, then the action for each step from 0 until one completes, and after each stop
|
||||
/// asserts that `check` is true with the limits lifted. Returns how many runs stopped at `kind`.
|
||||
fn stops_with_the_tree_intact(setup: &str, action: impl Fn(usize) -> String, check: &str, kind: LimitKind, limit: impl Fn(usize) -> Limits) -> usize {
|
||||
let mut r = realm();
|
||||
let interp = r.interp();
|
||||
let global = interp.global;
|
||||
assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok());
|
||||
let mut stops = 0;
|
||||
for step in 0..200_000 {
|
||||
r.set_limits(Limits::default());
|
||||
r.run(setup).unwrap();
|
||||
r.set_limits(limit(step));
|
||||
let action = action(step);
|
||||
let got = r.run(&action);
|
||||
r.set_limits(Limits::default());
|
||||
assert_eq!(r.run(check), Ok(JsValue::Bool(true)), "{action}: {got:?}");
|
||||
match got {
|
||||
Ok(_) => return stops,
|
||||
Err(e) if e.kind == RunErrorKind::Limit(kind) => stops += 1,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
panic!("{check} never completed");
|
||||
}
|
||||
|
||||
/// Runs `action`, whose `SPARE` is replaced by a call that leaves each number of bytes free in
|
||||
/// turn, from 512 too few. Actions put it after the steps whose allocations a collection could
|
||||
/// free: an argument is evaluated after the callee is looked up.
|
||||
fn heap_stops(setup: &str, action: &str, check: &str) -> usize {
|
||||
let action = |spare: usize| action.replace("SPARE", &format!("squeeze({})", spare as i64 - 512));
|
||||
stops_with_the_tree_intact(setup, action, check, LimitKind::Heap, |_| Limits::default())
|
||||
}
|
||||
|
||||
fn fuel_stops(setup: &str, action: &str, check: &str) -> usize {
|
||||
stops_with_the_tree_intact(setup, |_| action.to_string(), check, LimitKind::Fuel, |fuel| Limits { fuel: fuel as u64, ..Limits::default() })
|
||||
}
|
||||
|
||||
const MOVE_SETUP: &str = "var o = <o><b/><c/></o>; var t = <t/>; var n = o.b[0];";
|
||||
const MOVE_CHECK: &str =
|
||||
"o.b.length() + t.b.length() === 1 && (o.b.length() === 0 || o.b[0].parent() === o) && (t.b.length() === 0 || t.b[0].parent() === t)";
|
||||
|
||||
#[test]
|
||||
fn a_heap_limit_during_a_move_leaves_the_node_in_its_old_parent() {
|
||||
for action in
|
||||
["t.appendChild((SPARE, n))", "t.prependChild((SPARE, n))", "t.insertChildAfter(null, (SPARE, n))", "t.insertChildBefore(null, (SPARE, n))"]
|
||||
{
|
||||
assert!(heap_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fuel_limit_during_a_move_leaves_the_node_in_its_old_parent() {
|
||||
for action in ["t.appendChild(n)", "t.prependChild(n)"] {
|
||||
assert!(fuel_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}");
|
||||
}
|
||||
let replaced = "var l = t.*; l[0] = o.b[0]";
|
||||
let setup = "var o = <o><b/><c/></o>; var t = <t><d/></t>;";
|
||||
assert!(fuel_stops(setup, replaced, MOVE_CHECK) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_heap_limit_on_replace_past_the_end_leaves_no_child_without_a_parent() {
|
||||
let check = "var n = x.*.length(); if (n === 0) { true } else { \
|
||||
var v = x.*[n - 1]; var name; try { v.appendChild(x); name = 'none' } catch (e) { name = e.name } v.parent() === x && name === 'TypeError' }";
|
||||
let action = "x.replace({ toString: function () { SPARE; return '5'; } }, v)";
|
||||
assert!(heap_stops("var x = <x/>; var v = <v/>;", action, check) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_during_deletes_and_puts_keep_the_other_nodes() {
|
||||
let setup = "var x = <a p='1' q='2'><b>old</b><c/></a>;";
|
||||
let others = "x.c.length() === 1 && x.@q == '2'";
|
||||
for action in ["delete x.b", "delete x.@p", "x.@p = 'v'", "x.@n = 'v'", "x.b = 'v'"] {
|
||||
assert!(fuel_stops(setup, action, others) > 0, "{action}");
|
||||
}
|
||||
let held = format!("{others} && x.b.length() === 1 && (x.b == 'old' || x.b == 'v') && x.b.*[0].parent() === x.b[0]");
|
||||
assert!(fuel_stops(setup, "x.b = 'v'", &held) > 0);
|
||||
assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &held) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_limit_during_normalize_does_not_repeat_text() {
|
||||
let setup = "var x = <a/>; x.appendChild('p'); x.appendChild('q'); x.appendChild('r');";
|
||||
let check = "x.toString() === 'pqr'";
|
||||
assert!(fuel_stops(setup, "x.normalize()", check) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_interrupt_stops_serialization() {
|
||||
let mut r = realm();
|
||||
|
||||
Reference in New Issue
Block a user