mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 03:48:01 +00:00
JavaScript: JSON.parse counts its text and pending names toward the heap limit
The text a non-string argument converts to stays rooted while the parse runs, so a collection still counts it. A property name read from the text is charged to the heap until its property is defined. Boolean.prototype.toString and valueOf are added, so a Boolean object writes `true` or `false`. The json2 sample no longer includes U+2028: the polyfill escapes it and section 15.12.3 does not.
This commit is contained in:
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<bool> {
|
||||
fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult<bool> {
|
||||
let k = key(name);
|
||||
let mut cur = Some(o);
|
||||
while let Some(id) = cur {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user