diff --git a/crates/js/src/builtins/json.rs b/crates/js/src/builtins/json.rs index 8e27dec30..14c370f03 100644 --- a/crates/js/src/builtins/json.rs +++ b/crates/js/src/builtins/json.rs @@ -158,9 +158,12 @@ 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 = super::global::units_of(interp, args)?; + let source = Value::String(interp.to_str(&arg(args, 0))?); let mark = interp.temp_roots.len(); - let r = parse_text(interp, &text).and_then(|v| { + // The text stays rooted for the whole parse, so a collection still counts it. + interp.temp_roots.push(source.clone()); + let r = super::global::units_of(interp, std::slice::from_ref(&source)).and_then(|text| { + let v = parse_text(interp, &text)?; let reviver = arg(args, 1); if !interp.is_callable(&reviver) { return Ok(v); @@ -214,7 +217,12 @@ fn member_name(interp: &mut Interp, p: &mut Parser<'_>, first: bool) -> JsResult return Err(syntax(interp, "a property name must be a string", at)); } let units = p.string().map_err(|why| syntax(interp, why, at))?; - interp.reserve_heap(string_bytes(units.len()))?; + let bytes = string_bytes(units.len()); + interp.reserve_heap(bytes)?; + // The name is counted while it waits for its value; `parse_text` releases it before `define` + // accounts for it, and the native call's return releases it on every other path. + interp.heap.bytes = interp.heap.bytes.saturating_add(bytes); + interp.held_bytes = interp.held_bytes.saturating_add(bytes); p.skip_space(); if !p.eat(b':') { return Err(syntax(interp, "a property name must be followed by ':'", p.pos)); @@ -286,6 +294,9 @@ fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult { let obj = *obj; let Some(k) = name.take() else { return Err(syntax(interp, "a member has no name", p.pos)) }; interp.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?; + let held = string_bytes(k.len()); + interp.heap.bytes = interp.heap.bytes.saturating_sub(held); + interp.held_bytes = interp.held_bytes.saturating_sub(held); interp.define(obj, k, Prop::Data { value: v, flags: OPEN })?; p.skip_space(); if p.eat(b',') { @@ -571,7 +582,7 @@ fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult /// Whether looking up `name` from `o` reaches the object `proto` on the prototype chain before /// any object on the way, `proto` included, has its own property `name`. -pub(super) fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult { +fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult { let k = key(name); let mut cur = Some(o); while let Some(id) = cur { diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index 9fefc209b..703bb9626 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -388,13 +388,25 @@ fn to_locale_string_writes_the_primitive_of_a_boxed_value() { // Object.prototype.toString comes after the standard prototypes on the chain. assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; [1, 'x', true].toLocaleString()"), "1,x,true"); // A toString on the object or on its standard prototype is called. - assert_eq!(string("var n = Object(1); n.toString = function () { return 'own'; }; [n].toLocaleString()"), "own"); + assert_eq!(string("var b = Object(true); b.toString = function () { return 'own'; }; [b].toLocaleString()"), "own"); assert_eq!(string("Object.getPrototypeOf(true).toString = function () { return 'p'; }; [true].toLocaleString()"), "p"); - assert_eq!(string("Object.getPrototypeOf(1).toString = Object.prototype.toString; [1].toLocaleString()"), "[object Number]"); + assert_eq!(string("Object.getPrototypeOf(true).toString = Object.prototype.toString; [true].toLocaleString()"), "[object Boolean]"); // Other objects still reach Object.prototype.toString. assert_eq!(string("Object.prototype.toLocaleString.call({})"), "[object Object]"); } +#[test] +fn boolean_prototype_converts_booleans() { + assert_eq!(string("true.toString() + false.toString()"), "truefalse"); + assert_eq!(string("Object(false).toString()"), "false"); + truth("true.valueOf() === true && Object(false).valueOf() === false"); + assert_eq!(num("Object(true) + 1"), 2.0); + assert_eq!(string("'' + Object(false)"), "false"); + truth("Object.getPrototypeOf(true).valueOf() === false && Object.getPrototypeOf(true).toString() === 'false'"); + throws("Object.getPrototypeOf(true).toString.call(1)", "TypeError"); + throws("Object.getPrototypeOf(true).valueOf.call({})", "TypeError"); +} + #[test] fn array_methods_overwriting_elements_need_no_heap_for_new_ones() { // With the heap exactly at its limit, an array method's write over an existing element @@ -598,7 +610,7 @@ fn math_functions_follow_the_special_cases() { #[test] fn json_parse_reads_the_grammar_of_section_15_12_1() { assert_eq!(num("var v = JSON.parse(' {\"a\": [1, -2.5e1, true, null, \"x\\\\u0041\\\\n\"], \"b\": {}} '); v.a[1] + v.a.length"), -20.0); - assert_eq!(string("JSON.parse('\"\\\\ud800\"').charCodeAt === undefined ? 'no string methods' : ''"), "no string methods"); + assert_eq!(num("JSON.parse('\"\\\\ud800\"').charCodeAt(0)"), 55296.0); truth("JSON.parse('[]').length === 0 && JSON.parse('0') === 0 && 1 / JSON.parse('-0') === -Infinity && JSON.parse('{\"a\":1,\"a\":2}').a === 2"); for bad in ["'{a: 1}'", "'[1,]'", "'01'", "'1.'", "'.5'", "'\"\\t\"'", "\"'x'\"", "'[1] 2'", "''", "'+1'", "'\"\\\\x\"'", "'{\"a\" 1}'"] { throws(&format!("JSON.parse({bad})"), "SyntaxError"); @@ -693,6 +705,42 @@ fn json_parse_counts_a_string_while_it_reserves_the_property_for_it() { } } +#[test] +fn json_parse_keeps_its_text_counted_for_the_whole_parse() { + // The text exists only as the ToString result of the argument, and the parsed value needs a + // collection. Without the text the value fits under the limit; with it the value does not. + let setup = "var ws = new Array(8193).join(' '); var items = new Array(257).join('{},'); \ + var o = {toString: function () { return ws + '[' + items + '{}]'; }}; 0"; + let parse = "var a = JSON.parse(o); 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 text = 2 * (8192 + 2 + 3 * 256 + 2); + let mut realm = small(Limits { heap_bytes: full - text / 2, ..Limits::default() }); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + realm.interp().collect_garbage(); + assert_eq!(limit(&mut realm, parse), LimitKind::Heap); +} + +#[test] +fn json_parse_counts_names_that_wait_for_their_values() { + // An unterminated text: every name read waits for a value that never comes, so nothing but + // the parser holds them, and the names together pass the limit before the text ends. + let setup = "var k = new Array(2049).join('k'); var text = new Array(33).join('{\"' + k + '\":'); 0"; + let mut measure = small(Limits::default()); + assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0))); + let heap_bytes = measure.collect_garbage() + (96 << 10); + let mut realm = small(Limits { heap_bytes, ..Limits::default() }); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + assert_eq!(limit(&mut realm, "JSON.parse(text)"), LimitKind::Heap); + // A complete text gives its names back as it stores them, so one that fits parses. + let mut realm = small(Limits { heap_bytes: heap_bytes + (64 << 10), ..Limits::default() }); + assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0))); + assert_eq!(realm.run("var a = JSON.parse('{\"' + k + '\":{\"' + k + 'x\":1}}'); 0"), Ok(JsValue::Number(0.0))); +} + #[test] fn deep_json_nesting_needs_no_native_stack() { let handle = std::thread::Builder::new() diff --git a/crates/js/tests/json2.rs b/crates/js/tests/json2.rs index c998e55a9..af408a4fe 100644 --- a/crates/js/tests/json2.rs +++ b/crates/js/tests/json2.rs @@ -6,6 +6,8 @@ //! written by the polyfill and by the built-in `JSON.stringify` (ECMA-262 5.1 section 15.12.3), //! which must give the same text, and the text is read back by the polyfill's `parse` and by the //! built-in `JSON.parse` (section 15.12.2), which must give values that write the same text again. +//! The samples leave out U+2028 and U+2029: the polyfill escapes them, and section 15.12.3 does not +//! escape them. //! //! The polyfill uses `Date`, `String.prototype.replace` and regular expression `test`. While the //! interpreter lacks any of them, the test prints that it skipped and passes. @@ -36,7 +38,7 @@ const BEFORE: &str = "var NATIVE = JSON; delete this.JSON; this.JSON = undefined const ROUND_TRIP: &str = r#" var POLY = JSON; var samples = [ - 0, -1.5, 1e21, "", "plain", "quote \" backslash \\ newline \n tab \t \u0001 
 é", + 0, -1.5, 1e21, "", "plain", "quote \" backslash \\ newline \n tab \t \u0001 é", true, false, null, [], {}, [1, [2, [3, []]]], {a: 1, b: [true, null, "x"], c: {d: {}}}, [undefined, function () {}], {skip: undefined, f: function () {}, keep: 2}, [NaN, Infinity, -Infinity] ];