mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:47:43 +00:00
A literal makes an extensible object whose properties are writable and configurable, and an array whose length is writable. toSource wrote a property defined with writable or configurable false, a sealed, frozen or non-extensible object or array, and an array with a read-only length as an ordinary literal, so evaluating the text gave an open value. toSource now throws a TypeError for each of these, at any depth, as it already does for getters, hidden properties and other prototypes. Properties defined with every flag on are written as before.
242 lines
13 KiB
Rust
242 lines
13 KiB
Rust
//! The script extensions: `localize`, `toSource`, host properties with setters and the items that
|
|
//! read the interpreter's own state.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use super::*;
|
|
|
|
fn realm() -> Realm {
|
|
let mut r = Realm::new();
|
|
r.add_script_extensions().unwrap();
|
|
r
|
|
}
|
|
|
|
fn text(r: &mut Realm, source: &str) -> String {
|
|
match r.run(source) {
|
|
Ok(JsValue::String(s)) => s,
|
|
other => panic!("{source}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn thrown(r: &mut Realm, source: &str) -> String {
|
|
match r.run(source) {
|
|
Err(e) if e.kind == RunErrorKind::Thrown => e.message,
|
|
other => panic!("{source}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn localize_looks_up_the_locale_with_its_platform_then_its_language_then_english() {
|
|
let mut r = realm();
|
|
let l = r.localization();
|
|
l.set_locale("de_DE");
|
|
l.set_platform(Some("Win"));
|
|
assert_eq!(l.keys(), ["de_DE_Win", "de_DE", "de_Win", "de", "en"]);
|
|
let all = "({en: 'e', de: 'd', de_Win: 'dw', de_DE: 'dd', de_DE_Win: 'ddw'})";
|
|
assert_eq!(text(&mut r, &format!("localize({all})")), "ddw");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', de_Win: 'dw', de_DE: 'dd'})"), "dd");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', de_Win: 'dw'})"), "dw");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', DE: 'x'})"), "d", "names are case sensitive");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', fr: 'f'})"), "e");
|
|
l.set_platform(None);
|
|
assert_eq!(text(&mut r, &format!("localize({all})")), "dd");
|
|
l.set_locale("en");
|
|
assert_eq!(l.keys(), ["en"]);
|
|
}
|
|
|
|
#[test]
|
|
fn localize_replaces_numbered_values_once_and_leaves_the_rest() {
|
|
let mut r = realm();
|
|
assert_eq!(text(&mut r, "localize({en: '%2 before %1, %1 again'}, 'a', 'b')"), "b before a, a again");
|
|
assert_eq!(text(&mut r, "localize('%1%% %3 %0 %', 'x', '%1')"), "x%% %3 %0 %");
|
|
assert_eq!(text(&mut r, "localize('%2', 'x', '%1')"), "%1", "a value is not replaced again");
|
|
assert_eq!(text(&mut r, "localize('%1', {toString: function () { return 'o'; }})"), "o");
|
|
assert_eq!(text(&mut r, "var n = 0; var v = {toString: function () { n++; return 'v'; }}; localize('%1%1', v) + n"), "vv1");
|
|
}
|
|
|
|
#[test]
|
|
fn localize_gives_back_an_object_without_the_locale_and_runs_no_getter() {
|
|
let mut r = realm();
|
|
r.localization().set_locale("fr_FR");
|
|
assert_eq!(r.run("var o = {de: 'd'}; localize(o) === o").unwrap(), JsValue::Bool(true));
|
|
let source = "var hit = false; var o = {get fr() { hit = true; return 'g'; }, en: 'e'}; localize(o) + ',' + hit";
|
|
assert_eq!(text(&mut r, source), "e,false");
|
|
assert_eq!(text(&mut r, "localize({fr: 1, en: 'e'})"), "e", "a value that is not a string is passed over");
|
|
for bad in ["localize()", "localize(1)", "localize(null)"] {
|
|
assert!(thrown(&mut r, bad).starts_with("TypeError"), "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn to_string_localizes_an_object_only_while_localization_is_on() {
|
|
let mut r = realm();
|
|
r.localization().set_locale("ja");
|
|
let source = "String({en: 'Hello', ja: 'こんにちは'}) + '|' + String({x: 1})";
|
|
assert_eq!(text(&mut r, source), "[object Object]|[object Object]");
|
|
r.localization().set_enabled(true);
|
|
assert_eq!(text(&mut r, source), "こんにちは|[object Object]");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_writes_plain_data_as_an_expression_that_makes_it_again() {
|
|
let mut r = realm();
|
|
let source = r#"toSource({a: [1, "x\n\"", null, true, -0, 1.5e300], "b c": {d: undefined}, e: {}, f: []})"#;
|
|
let want = r#"({"a":[1, "x\n\"", null, true, -0, 1.5e+300], "b c":({"d":(void 0)}), "e":({}), "f":[]})"#;
|
|
assert_eq!(text(&mut r, source), want);
|
|
assert_eq!(text(&mut r, "[1, , 3].toSource() + '|' + [1, ,].toSource() + '|' + [,].toSource()"), "[1, , 3]|[1, ,]|[,]");
|
|
assert_eq!(
|
|
text(&mut r, "toSource(NaN) + toSource(-Infinity) + toSource() + toSource('\\u2028\\ud800\\x7f')"),
|
|
"NaN-Infinity(void 0)\"\\u2028\\uD800\\u007F\""
|
|
);
|
|
assert_eq!(text(&mut r, "(5).toSource() + 'a'.toSource() + true.toSource()"), "5\"a\"true");
|
|
// Parsed back, the text makes the same value: holes, -0 and undefined included.
|
|
let round = r#"var v = {a: [1, , "q\u0001'\\"], b: {"0": -0, "": undefined, "x y": [[]]}};
|
|
var w = eval(toSource(v));
|
|
[JSON.stringify(w) === JSON.stringify(v), w.a.length, 1 in w.a, 1 / w.b[0], "" in w.b].join()"#;
|
|
assert_eq!(text(&mut r, round), "true,3,false,-Infinity,true");
|
|
let shared = "var s = {k: 1}; toSource([s, s])";
|
|
assert_eq!(text(&mut r, shared), r#"[({"k":1}), ({"k":1})]"#, "a value seen twice that is not its own ancestor is written twice");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_refuses_what_is_not_plain_data() {
|
|
let mut r = realm();
|
|
for (source, kind) in [
|
|
("var o = {}; o.o = o; toSource(o)", "TypeError"),
|
|
("var a = [1]; a.push([a]); toSource(a)", "TypeError"),
|
|
("toSource({f: function () {}})", "TypeError"),
|
|
("toSource({get g() { return 1; }})", "TypeError"),
|
|
("toSource(new Date(0))", "TypeError"),
|
|
("toSource([/x/])", "TypeError"),
|
|
("toSource(new Error('e'))", "TypeError"),
|
|
("toSource(new String('s'))", "TypeError"),
|
|
("var a = [1]; a.extra = 2; toSource(a)", "TypeError"),
|
|
("toSource(Object.create(Array.prototype))", "TypeError"),
|
|
("var o = {}; Object.defineProperty(o, 'hidden', {value: 1}); toSource(o)", "TypeError"),
|
|
("var a = [0]; Object.defineProperty(a, '0', {value: 1, enumerable: false}); toSource(a)", "TypeError"),
|
|
("toSource(Object.create({a: 1}))", "TypeError"),
|
|
("toSource(Object.create(null))", "TypeError"),
|
|
("function F() { this.a = 1; } toSource(new F())", "TypeError"),
|
|
("var d = []; for (var i = 0; i < 70; i++) d = [d]; toSource(d)", "RangeError"),
|
|
("var a = []; a.length = 2000000; toSource(a)", "RangeError"),
|
|
] {
|
|
assert!(thrown(&mut r, source).starts_with(kind), "{source}");
|
|
}
|
|
assert!(thrown(&mut r, "toSource({get g() { throw 'ran'; }})").starts_with("TypeError"), "the getter does not run");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_refuses_flags_and_extensibility_a_literal_does_not_make() {
|
|
let mut r = realm();
|
|
let property = "TypeError: toSource: a property that is not writable or configurable cannot be written as source";
|
|
let extensible = "TypeError: toSource: a non-extensible object cannot be written as source";
|
|
let length = "TypeError: toSource: an array with a read-only length cannot be written as source";
|
|
for (source, want) in [
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, configurable: true}); toSource(o)", property),
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, writable: true}); toSource(o)", property),
|
|
("var a = [1, 2]; Object.defineProperty(a, '1', {writable: false}); toSource(a)", property),
|
|
("var a = [1, 2]; Object.defineProperty(a, '0', {configurable: false}); a.toSource()", property),
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true}); toSource({x: [{b: o}]})", property),
|
|
("toSource(Object.preventExtensions({}))", extensible),
|
|
("toSource(Object.preventExtensions([]))", extensible),
|
|
("toSource(Object.seal({a: 1}))", extensible),
|
|
("toSource(Object.freeze({a: 1}))", extensible),
|
|
("toSource(Object.seal([1]))", extensible),
|
|
("toSource(Object.freeze([1]))", extensible),
|
|
("toSource([1, {b: Object.freeze({})}])", extensible),
|
|
("var a = []; Object.defineProperty(a, 'length', {writable: false}); toSource(a)", length),
|
|
("var a = [1, , 3]; Object.defineProperty(a, 'length', {writable: false}); toSource({a: a})", length),
|
|
] {
|
|
assert_eq!(thrown(&mut r, source), want, "{source}");
|
|
}
|
|
// The refused states are ones the literal would lose: evaluated, it is open again.
|
|
let lost = "var o = Object.freeze({a: 1}); var w = eval('({\"a\":1})');
|
|
[Object.isExtensible(o), Object.isExtensible(w), Object.getOwnPropertyDescriptor(w, 'a').writable].join()";
|
|
assert_eq!(text(&mut r, lost), "false,true,true");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_writes_properties_defined_with_every_flag_on() {
|
|
let mut r = realm();
|
|
let source = "var o = {}; Object.defineProperty(o, 'a', {value: 1, writable: true, enumerable: true, configurable: true});
|
|
var a = [0]; Object.defineProperty(a, '0', {value: 2, writable: true, enumerable: true, configurable: true});
|
|
toSource({o: o, a: a, e: {}, f: [], s: [1, , 3]})";
|
|
assert_eq!(text(&mut r, source), r#"({"o":({"a":1}), "a":[2], "e":({}), "f":[], "s":[1, , 3]})"#);
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_of_a_string_with_many_escapes_throws_a_range_error_before_the_heap_limit() {
|
|
let mut r = realm();
|
|
// A run's heap in the web build. 2^23 control characters, each written as six, pass a quarter
|
|
// of it, which is all one string may take.
|
|
r.set_limits(Limits { heap_bytes: 128 << 20, ..Limits::default() });
|
|
let source = "var s = '\\x01'; for (var i = 0; i < 23; i++) s = s + s;
|
|
try { toSource(s); 'no error'; } catch (e) { e.name + ': ' + e.message; }";
|
|
assert_eq!(text(&mut r, source), "RangeError: toSource: the source is too long");
|
|
assert_eq!(text(&mut r, "toSource('a\\x01a\\x01a\\x01a\\x01').length + ''"), "30", "short strings are written whole");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_stops_at_the_fuel_limit() {
|
|
let mut r = realm();
|
|
r.set_limits(Limits { fuel: 200_000, ..Limits::default() });
|
|
let e = r.run("var a = []; for (var i = 0; i < 1000; i++) a.push(i); while (true) toSource(a);").unwrap_err();
|
|
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel));
|
|
}
|
|
|
|
#[test]
|
|
fn script_items_read_the_global_object_the_last_error_the_line_and_the_stack() {
|
|
let mut r = realm();
|
|
let items = ["global", "error", "line", "stack"]
|
|
.into_iter()
|
|
.zip([ScriptItem::Global, ScriptItem::LastError, ScriptItem::Line, ScriptItem::Stack])
|
|
.map(|(n, i)| (n.to_string(), HostItem::Script(i)))
|
|
.collect();
|
|
r.define_host_object("h", items).unwrap();
|
|
assert_eq!(r.run("var later = 2; h.global === this && h.global.later === 2").unwrap(), JsValue::Bool(true));
|
|
let source = "var before = h.error; try { null.x; } catch (e) {} var caught = h.error; try { throw 'text'; } catch (e) {}
|
|
[before === null, caught instanceof TypeError, h.error === caught].join()";
|
|
assert_eq!(text(&mut r, source), "true,true,true");
|
|
let source = "try { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } } catch (e) {}
|
|
h.error.message";
|
|
assert_eq!(text(&mut r, source), "second", "an exception a finally block passes on is not thrown again");
|
|
let source = "try { [1].map(function () { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } }); } catch (e) {}
|
|
h.error.message";
|
|
assert_eq!(text(&mut r, source), "second", "nor is one that leaves a built-in's callback");
|
|
let source = "var kept; try { throw new RangeError('first'); } catch (e) { kept = e; } try { throw new TypeError('second'); } catch (e) {}
|
|
try { throw kept; } catch (e) {} h.error.message";
|
|
assert_eq!(text(&mut r, source), "first", "throwing a caught error again makes it the last one");
|
|
assert_eq!(r.run("h.error").unwrap(), JsValue::Null, "each run starts without one");
|
|
assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(3.0));
|
|
let source = "function outer() {\n return inner();\n}\nfunction inner() {\n return h.stack;\n}\nvar f = function () { return outer(); };\nf()";
|
|
assert_eq!(text(&mut r, source), "[global]:8\n[anonymous]:7\nouter:2\ninner:5\n");
|
|
let source = "function maker() {\n function named() {\n return h.stack;\n }\n return named;\n}\nvar later = maker();\nlater()";
|
|
assert_eq!(text(&mut r, source), "[global]:8\nnamed:3\n", "a declared function keeps its name after its declaring function returns");
|
|
let deep = "function d(n) { return n ? d(n - 1) : h.stack; } var s = d(100).split('\\n'); s.length + ',' + s[0]";
|
|
let lines = text(&mut r, deep);
|
|
assert_eq!(lines, "66,…", "64 frames, the line that some are left out, and the last line's end");
|
|
assert!(thrown(&mut r, "'use strict'; h.line = 1").starts_with("TypeError"), "the items cannot be assigned");
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_property_reads_its_getter_and_gives_assignments_to_its_setter() {
|
|
let mut r = Realm::new();
|
|
let seen: Rc<RefCell<Vec<Data>>> = Rc::default();
|
|
let log = seen.clone();
|
|
let get: HostFunction = Rc::new(move |_| Ok(Data::String("got".into())));
|
|
let set: HostFunction = Rc::new(move |args| match args.first() {
|
|
Some(Data::Number(n)) if *n < 0.0 => Err(HostError::Range("no negatives".into())),
|
|
Some(d) => {
|
|
log.borrow_mut().push(d.clone());
|
|
Ok(Data::Undefined)
|
|
}
|
|
None => Ok(Data::Undefined),
|
|
});
|
|
r.define_host_object("h", vec![("p".into(), HostItem::Property { get, set })]).unwrap();
|
|
assert_eq!(r.run("h.p = 5; h.p = {a: [1]}; h.p").unwrap(), JsValue::String("got".into()));
|
|
assert_eq!(*seen.borrow(), [Data::Number(5.0), Data::Object(vec![("a".into(), Data::Array(vec![Data::Number(1.0)]))])]);
|
|
let e = r.run("try { h.p = -1; } catch (e) { e instanceof RangeError && e.message }").unwrap();
|
|
assert_eq!(e, JsValue::String("no negatives".into()));
|
|
}
|