diff --git a/crates/js/src/builtins/global.rs b/crates/js/src/builtins/global.rs index 261bfa146..182f3aad5 100644 --- a/crates/js/src/builtins/global.rs +++ b/crates/js/src/builtins/global.rs @@ -35,7 +35,7 @@ fn trim_start(s: &[u16]) -> &[u16] { /// ToString of the first argument, as code units, with each one taking a unit of fuel. A /// concatenation is flattened into a new buffer, which is charged to the heap. -fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult> { +pub(super) fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult> { let s = interp.to_str(&arg(args, 0))?; for _ in 0..s.len() { interp.use_fuel()?; diff --git a/crates/js/src/builtins/json.rs b/crates/js/src/builtins/json.rs index b9f4a6c6f..8e27dec30 100644 --- a/crates/js/src/builtins/json.rs +++ b/crates/js/src/builtins/json.rs @@ -158,10 +158,7 @@ fn syntax(interp: &mut Interp, why: &str, at: usize) -> crate::interp::Abrupt { /// `JSON.parse` (section 15.12.2). fn parse(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { - let text = interp.to_str(&arg(args, 0))?.flat(); - for _ in 0..text.len() { - interp.use_fuel()?; - } + let text = super::global::units_of(interp, args)?; let mark = interp.temp_roots.len(); let r = parse_text(interp, &text).and_then(|v| { let reviver = arg(args, 1); @@ -273,7 +270,7 @@ fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult { Open::Array { obj, next } => { let (obj, i) = (*obj, *next); *next = next.saturating_add(1); - interp.reserve_heap(ELEMENT_BYTES)?; + interp.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?; interp.define(obj, key_of(f64::from(i)), Prop::Data { value: v, flags: OPEN })?; p.skip_space(); if p.eat(b',') { @@ -288,7 +285,7 @@ fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult { Open::Object { obj, name } => { let obj = *obj; let Some(k) = name.take() else { return Err(syntax(interp, "a member has no name", p.pos)) }; - interp.reserve_heap(ELEMENT_BYTES)?; + interp.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?; interp.define(obj, k, Prop::Data { value: v, flags: OPEN })?; p.skip_space(); if p.eat(b',') { diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index 8d9ad318c..9fefc209b 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -659,6 +659,40 @@ fn json_stringify_unwraps_number_string_and_boolean_objects() { assert_eq!(string("Object.getPrototypeOf(Object('')).toString = function () { return 'q'; }; JSON.stringify(Object('abc'))"), "\"q\""); } +#[test] +fn json_parse_counts_a_string_while_it_reserves_the_property_for_it() { + // The limits around the parsed value's size include one where the reservation for the string's + // property is what starts a collection. The text is a concatenation, so the parse also flattens + // it. Whenever the parse fits, the heap total counts everything that is live. + let setup = |text: &str| format!("var s = new Array(4097).join('x'); var text = {text}; 0"); + for text in ["'[\"' + s + '\"]'", "'{\"k\":\"' + s + '\"}'"] { + let setup = setup(text); + let parse = "var a = JSON.parse(text); 0"; + let mut measure = small(Limits::default()); + assert_eq!(measure.run(&setup), Ok(JsValue::Number(0.0))); + measure.interp().collect_garbage(); + assert_eq!(measure.run(parse), Ok(JsValue::Number(0.0))); + let full = measure.interp().heap.bytes; + let (mut fits, mut over) = (0, 0); + for heap_bytes in (full - 8192..=full + 256).step_by(4) { + let mut realm = small(Limits { heap_bytes, ..Limits::default() }); + assert_eq!(realm.run(&setup), Ok(JsValue::Number(0.0))); + realm.interp().collect_garbage(); + match realm.run(parse) { + Ok(_) => { + let counted = realm.interp().heap.bytes; + let live = realm.collect_garbage(); + assert!(counted >= live, "{text}: {counted} bytes counted with {live} live under a limit of {heap_bytes}"); + fits += 1; + } + Err(RunError { kind: RunErrorKind::Limit(LimitKind::Heap), .. }) => over += 1, + Err(e) => panic!("{text}: {e:?}"), + } + } + assert!(fits > 0 && over > 0, "{text}: {fits} limits fit and {over} did not"); + } +} + #[test] fn deep_json_nesting_needs_no_native_stack() { let handle = std::thread::Builder::new()