use proptest::prelude::*;
use crate::{JsValue, LimitKind, Limits, Options, Realm, RunErrorKind, parse};
fn e4x() -> Options {
Options { e4x: true, ..Options::default() }
}
fn realm() -> Realm {
Realm::with_options(e4x(), Limits::default())
}
fn limited(limits: Limits) -> Realm {
Realm::with_options(e4x(), limits)
}
fn run(src: &str) -> JsValue {
match realm().run(src) {
Ok(v) => v,
Err(e) => panic!("{src}\n{e:?}"),
}
}
fn string(src: &str) -> String {
match run(src) {
JsValue::String(s) => s,
other => panic!("{src}: {other:?}"),
}
}
fn truth(src: &str) {
assert_eq!(run(src), JsValue::Bool(true), "{src}");
}
fn thrown_name(src: &str) -> String {
string(&format!("try {{ {src}; 'no error' }} catch (e) {{ e.name }}"))
}
const BOOKS: &str = "var xml = \
CookbookFarmer1896\
OzBaum1900\
AliceABC1865\
OperaWalbrook1922\
;";
const TOP: &str = "var x = new XML('\\none text\\n\\ntwo text\\ninside text\\n\\ntop text\\n');";
// ---- default isolation ----
#[test]
fn without_the_option_e4x_syntax_is_rejected_and_the_globals_are_absent() {
for src in ["", "x..y", "x.@a", "x.(y)", "x.*", "a::b", "default xml namespace = 'u';", "for each (var v in o);"] {
assert!(parse(src, &Options::default()).is_err(), "{src}");
assert!(parse(src, &e4x()).is_ok(), "{src}");
}
let mut plain = Realm::new();
for name in ["XML", "XMLList", "Namespace", "QName", "isXMLName", "setDefaultXMLNamespace"] {
assert_eq!(plain.run(&format!("typeof {name}")), Ok(JsValue::String("undefined".into())), "{name}");
}
}
#[test]
fn ordinary_expressions_mean_the_same_with_the_option() {
truth("1 < 2 && 3 > 2 && 4 / 2 / 1 === 2 && /a1).toString()'); f() === '1'");
truth("eval('().b.length()') === 1");
let mut plain = Realm::new();
assert_eq!(plain.run("try { eval('') } catch (e) { e.name }"), Ok(JsValue::String("SyntaxError".into())));
assert_eq!(plain.run("try { new Function('return ') } catch (e) { e.name }"), Ok(JsValue::String("SyntaxError".into())));
}
// ---- results stated by ECMA-357 ----
#[test]
fn literals_match_the_standard_examples() {
assert_eq!(string("var person = John25; person.name.toString()"), "John");
assert_eq!(run("<>Phil35skiing>.length()"), JsValue::Number(3.0));
let item = "var item = - Laptop Computer2799.951
;";
assert_eq!(
string(&format!("{item} 'Description stored as ' + item.description.toXMLString()")),
"Description stored as Laptop Computer"
);
assert_eq!(string(&format!("{item} 'Thank you for purchasing a ' + item.description + '!'")), "Thank you for purchasing a Laptop Computer!");
}
#[test]
fn parsed_markup_has_the_expected_nodes() {
let src = "XML.ignoreComments = false; var t = new XML('x&y]]>');";
truth(&format!("{src} t.children().length() === 5"));
truth(&format!("{src} t.children()[0].nodeKind() === 'text' && t.children()[0].toString() === 'x'"));
truth(&format!("{src} t.children()[1].nodeKind() === 'comment' && t.comments().length() === 1"));
truth(&format!("{src} t.children()[2].nodeKind() === 'processing-instruction' && t.children()[2].name().localName === 'pi'"));
truth(&format!("{src} t.children()[3].nodeKind() === 'element'"));
truth(&format!("{src} t.children()[4].toString() === '&y'"));
truth(&format!("{src} t.@p == '1' && t.@q == '<' && t.attributes().length() === 2 && t.@p.nodeKind() === 'attribute'"));
}
#[test]
fn serialization_matches_the_fixed_markup() {
assert_eq!(string("(1).toXMLString()"), "\n 1\n \n");
assert_eq!(string("XML.prettyPrinting = false; (1).toXMLString()"), "1");
assert_eq!(string("XML.prettyIndent = 4; ().toXMLString()"), "\n \n");
assert_eq!(string("(<&>).toXMLString()"), "<&>");
assert_eq!(string("().toXMLString()"), "\n \n");
assert_eq!(string("(<>>).toXMLString()"), "\n");
}
#[test]
fn embedded_expressions_are_escaped_by_position() {
assert_eq!(string("var v = '<&>'; ({v}).toXMLString()"), "<&>");
assert_eq!(string("var v = 'a\"<'; ().toXMLString()"), "");
assert_eq!(string("var n = 'tag'; (<{n} x=\"1\">t{n}>).toXMLString()"), "t");
assert_eq!(string("var c = 1; ({c}).toXMLString()"), "\n 1\n");
assert_eq!(string("var a = 'p=\"1\"'; ().toXMLString()"), "");
truth("var i = 0; var x = {i++}{i++}; x == '01' && i === 2");
truth("var o = {t: 1}; ({o.t ? 'y' : 'n'}) == 'y'");
truth("({'}'}) == '}'");
truth("({1 < 2}) == 'true'");
}
#[test]
fn a_computed_tag_name_cannot_add_markup() {
for src in [
r#"(<{'a x="1"'}/>)"#,
"(<{'a>{'a'}>)",
"(t{'b>)",
"(<{''}/>)",
"(<{'1a'}/>)",
"({'a '}>)",
r#"()"#,
] {
assert_eq!(thrown_name(src), "SyntaxError", "{src}");
}
}
#[test]
fn a_computed_name_may_be_any_xml_name() {
assert_eq!(string("(<{'a.b-c'}/>).toXMLString()"), "");
assert_eq!(string("(<{'tag'}>t{'tag'}>).toXMLString()"), "t");
assert_eq!(string("var n = 'n:b'; (<{n}/>).toXMLString()"), "\n \n");
}
#[test]
fn a_computed_attribute_name_matches_the_standard_example() {
let vars = "var tagname = 'name', attributename = 'id', attributevalue = 5, content = 'Fred';";
let literal = "<{tagname} {attributename}={attributevalue}>{content}{tagname}>";
assert_eq!(string(&format!("{vars} ({literal}).toXMLString()")), "Fred");
assert_eq!(string(r#"var a = 'id'; ().toXMLString()"#), "");
}
#[test]
fn a_computed_attribute_run_cannot_add_markup() {
for src in [
r#"()"#,
r#"(<>>)"#,
"()",
"()",
r#"()"#,
r#"()"#,
r#"()"#,
] {
assert_eq!(thrown_name(src), "SyntaxError", "{src}");
}
}
#[test]
fn a_computed_attribute_run_may_hold_any_attributes() {
truth(
r#"var a = " a=\"1\"\n b = '>' "; var x = ; x.@a == '1' && x.@b == '>' && x.attributes().length() === 2 && x.children().length() === 0"#,
);
assert_eq!(string("().toXMLString()"), "");
assert_eq!(string("().toXMLString()"), "");
}
// ---- selectors and updates ----
#[test]
fn selectors_return_the_documented_lists() {
let s = |rest: &str| truth(&format!("{BOOKS} {rest}"));
s("xml.book.length() === 4 && xml.book[0].author == 'Farmer'");
s("xml.book[0].@category == 'COOKING' && xml.book.@category.length() === 4");
s("xml.book[1].@category.toString() === 'CHILDREN'");
s("xml.*.length() === 4 && xml.book[0].*.length() === 3");
s("xml.book[0].@*.length() === 1");
s("xml.nothing.length() === 0 && xml.@nothing.length() === 0");
s("xml..title.length() === 4 && xml..title == xml.descendants('title')");
s("xml..@lang.length() === 4 && xml..@category.length() === 4");
s("xml.book.(@category == 'CHILDREN').length() === 2");
s("xml.book.(year > 1899).title.length() === 2");
s("xml.book.(author == 'Baum').title == 'Oz'");
s("xml.*::book.length() === 4");
s("var n = null; xml.n::book.length() === 4");
s("xml['book'].length() === 4 && xml.book[0]['author'] == 'Farmer' && xml.book[0]['@category'] == 'COOKING'");
s("var k = 'category'; xml.book[0].@[k] == 'COOKING'");
}
#[test]
fn the_top_sample_has_three_children_two_elements_and_seven_descendants() {
truth(&format!("{TOP} x.children().length() === 3"));
truth(&format!("{TOP} x.elements().length() === 2"));
truth(&format!("{TOP} x.descendants().length() === 7"));
}
#[test]
fn a_filter_tests_each_item_once_in_order() {
truth(
"var seen = []; var l = <>123>; var r = l.(seen.push(toString()), true); seen.join() === '1,2,3' && r.length() === 3",
);
truth("().(false).length() === 0");
assert_eq!(thrown_name("({}).(1)"), "TypeError");
assert_eq!(thrown_name("var o = {}; o.@a"), "TypeError");
assert_eq!(thrown_name("var o = {}; o..a"), "TypeError");
}
#[test]
fn assignment_and_delete_update_the_tree() {
let s = |rest: &str| truth(&format!("{BOOKS} {rest}"));
s(
"xml.book[2].author = 'Dodgeson'; xml.book[2].author.length() === 1 && xml.book[2].author == 'Dodgeson' && xml.book[2].*[1].localName() === 'author'",
);
s("xml.book[2].author[0] = 'First'; xml.book[2].author.length() === 3 && xml.book[2].author[0] == 'First'");
s("xml.book[1].year = 1901; xml.book[1].year == '1901'");
s("xml.book[1].rating = '*****'; xml.book[1].*[3].localName() === 'rating' && xml.book[1].rating == '*****'");
s("xml.book[1].@category = 'FANTASY'; xml.book[1].@category == 'FANTASY'");
s("xml.book[1].@added = 'y'; xml.book[1].@added == 'y' && xml.book[1].attributes().length() === 2");
s("delete xml.book[2].author; xml.book[2].author.length() === 0");
s("delete xml.book[2].author[1]; xml.book[2].author.length() === 2 && xml.book[2].author[1] == 'C'");
s("delete xml.book[2].@category; xml.book[2].@category.length() === 0");
s("xml.book[0] = ; xml.book.length() === 4 && xml.book[0].@id == 'new' && xml.book[0].parent() === xml");
assert_eq!(thrown_name("var x = ; x[0] = 1"), "TypeError");
assert_eq!(thrown_name("var x = ; delete x[0]"), "TypeError");
truth("var x = ; x.b.c = 'd'; x.b.c == 'd'");
truth("var x = ; x.b += 'v'; x.b == 'v'");
truth("var x = 1; x.n++; x.n == '2'");
truth("var x = ; x.@t++; x.@t == '2'");
}
#[test]
fn operators_follow_the_standard() {
truth("var l = + ; typeof l === 'xml' && l.length() === 2");
truth("var l = <>> + ; l.length() === 2");
truth("typeof === 'xml' && typeof <>> === 'xml' && typeof new Namespace() === 'object'");
truth("1 == 1 && !(1 == 2)");
truth(" == ");
truth("1 == '1' && '1' == 1 && 1 == 1");
truth("!(01 == 1) && true == true && null == null && undefined == undefined && !(x == null)");
truth("<>> == <>> && !(<>> == <>>)");
truth("new XMLList() == undefined");
truth("new Namespace('p', 'u') == new Namespace('q', 'u') && new QName('u', 'a') == new QName('u', 'a')");
truth("var s = ''; for (var i in <>>) s += i; s === '01'");
truth("var s = ''; for each (var v in <>12>) s += v; s === '12'");
truth("var s = ''; for each (var v in [3, 4]) s += v; s === '34'");
truth("var s = ''; for each (var v in 7) s += v; s === '7'");
truth("var x = 5; x.length == '5' && x.length() === 1");
truth("var x = abc; x.charAt(1) === 'b'");
truth("var l = <>abc>; l.toUpperCase() === 'ABC'");
assert_eq!(thrown_name("().noSuchMethod()"), "TypeError");
assert_eq!(thrown_name("(<>>).nodeKind()"), "TypeError");
truth("with (1) { b == '1' }");
}
#[test]
fn the_default_namespace_is_scoped_to_the_function() {
truth(
"var ns = new Namespace('u'); default xml namespace = ns; var x = ; x.name().uri === 'u' && x.b.length() === 1 && x.b.name().uri === 'u'",
);
truth("default xml namespace = 'u'; ().@p.name().uri === ''");
truth(
"function f() { default xml namespace = 'u'; return ().name().uri; } \
f() === 'u' && ().name().uri === ''",
);
truth("function f() { default xml namespace = 'u'; return function () { return ().name().uri; }; } f()() === 'u'");
truth("setDefaultXMLNamespace(new Namespace('p', 'v')); ().name().uri === 'v' && new QName('b').uri === 'v'");
truth("default xml namespace = 'u'; new QName('a').uri === 'u' && new QName('*').uri === null");
truth("var x = ; x.b.length() === 0");
truth("var x = ; var n = new Namespace('u'); x.n::b.length() === 1");
truth(
"var soap = new Namespace('http://schemas.xmlsoap.org/soap/envelope/'); default xml namespace = soap; \
var m = ; \
m.@soap::encodingStyle == 'e' && m.Body.length() === 1",
);
}
// ---- built-ins ----
#[test]
fn the_constructors_keep_identity_or_copy_as_specified() {
truth("XML().nodeKind() === 'text' && XML(null) == '' && XML(undefined) == ''");
truth("var x = ; XML(x) === x && new XML(x) !== x && new XML(x) == x");
truth("var l = <>>; XML(l) === l[0]");
truth("var p =
; var l = p.a; var y = new XML(l); y !== l[0] && y == l[0] && y.parent() === null && l[0].parent() === p");
truth(
"var p =
; var l = p.a; var y = new XML(l); y.@q = '2'; y.c.@r = '3'; l[0].@q == '1' && l[0].c.@r.length() === 0 && y.c.@r == '3'",
);
assert_eq!(thrown_name("XML(<>>)"), "TypeError");
assert_eq!(thrown_name("XML('')"), "SyntaxError");
assert_eq!(thrown_name("XML('')"), "SyntaxError");
truth("XMLList().length() === 0 && XMLList('').length() === 2");
truth("var l = <>>; XMLList(l) === l && new XMLList(l) !== l && new XMLList(l)[0] === l[0]");
truth("var n = new Namespace('p', 'u'); Namespace(n) === n && new Namespace(n) !== n && new Namespace(n).prefix === 'p'");
truth("var n = new Namespace(); n.prefix === '' && n.uri === '' && n.toString() === ''");
truth("var n = new Namespace('u'); n.prefix === undefined && n.uri === 'u'");
truth("new Namespace('1bad', 'u').prefix === undefined");
assert_eq!(thrown_name("new Namespace('p', '')"), "TypeError");
truth("var q = new QName('u', 'a'); QName(q) === q && new QName(q) !== q && q.uri === 'u' && q.localName === 'a'");
truth("var q = new QName(new Namespace('p', 'u'), 'a'); q.uri === 'u' && q.toString() === 'u::a'");
truth("new QName('*').toString() === '*::*' && new QName(null, 'a').uri === null && new QName().localName === ''");
truth("isXMLName('abc') && isXMLName('a-b') && !isXMLName('a.b') && !isXMLName('$a') && !isXMLName('1a') && !isXMLName('')");
truth("().localName() === 'a.b'");
}
#[test]
fn the_settings_are_per_realm_and_restore() {
truth(
"var d = XML.defaultSettings(); !d.ignoreComments && !d.ignoreProcessingInstructions && d.ignoreWhitespace && d.prettyPrinting && d.prettyIndent === 2",
);
truth("var s = XML.settings(); XML.prettyIndent = 7; XML.settings().prettyIndent === 7 && (XML.setSettings(s), XML.prettyIndent === 2)");
truth("XML.prettyIndent = 7; XML.setSettings(); XML.prettyIndent === 2");
truth("XML.setSettings({ignoreWhitespace: false, prettyIndent: 'bad'}); !XML.ignoreWhitespace && XML.prettyIndent === 2");
truth("(new XML('')).children().length() === 2");
truth("XML.ignoreComments = true; XML.ignoreProcessingInstructions = true; (new XML('')).children().length() === 0");
truth("XML.ignoreWhitespace = false; (new XML(' ')).children().length() === 3");
truth("(new XML(' ')).children().length() === 1");
truth("(new XML(' t ')).toString() === ' t '");
let mut first = realm();
first.run("XML.prettyIndent = 9").unwrap();
assert_eq!(realm().run("XML.prettyIndent"), Ok(JsValue::Number(2.0)));
}
#[test]
fn the_xml_methods_behave_as_specified() {
truth("var x = ; x.appendChild() === x && x.*[1].localName() === 'c' && x.c.parent() === x");
truth("var x = ; x.appendChild('t'); x.text() == 't'");
truth("var x = ; x.prependChild(); x.*[0].localName() === 'c'");
truth("var x = ; x.insertChildAfter(x.b[0], ); x.*[1].localName() === 'c'");
truth("var x = ; x.insertChildBefore(x.d[0], ); x.*[1].localName() === 'c'");
truth("var x = ; x.insertChildAfter(null, ); x.*[0].localName() === 'c'");
truth("var x = ; x.insertChildBefore(null, ); x.*[1].localName() === 'c'");
truth("var x = ; x.insertChildAfter(, ) === undefined && x.*.length() === 1");
truth("var x = ; x.c[0].childIndex() === 1 && x.childIndex() === -1");
truth("var x = ; x.attribute('p') == '1' && x.attributes().length() === 1");
truth("var x = t; x.child('b').length() === 1 && x.child(1) == 't' && x.child(9).length() === 0");
truth("var x = ; x.copy() !== x && x.copy() == x && x.copy().parent() === null && x.b.copy().parent() === undefined");
truth("var c = ().b[0].copy(); c.parent() === null");
truth("var x = ; x.contains()");
truth("var x = ; x.hasOwnProperty('b') && !x.hasOwnProperty('c') && x.hasComplexContent() && !x.hasSimpleContent()");
truth("var x = t; !x.hasComplexContent() && x.hasSimpleContent() && x.length() === 1");
truth(
"var x = ; x.b[0].inScopeNamespaces().length === 1 && x.namespaceDeclarations()[0].prefix === 'p' && x.b[0].namespaceDeclarations().length === 0",
);
truth("var x = ; x.namespace('p').uri === 'u' && x.namespace('q') === undefined && x.namespace().uri === ''");
truth("var x = ; x.addNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
truth("var x = ; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p') === undefined");
truth("var x = ; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
truth("var x = ; x.name().localName === 'a' && x.localName() === 'a'");
truth("(t).*[0].name() === null && (t).*[0].localName() === null");
truth("var x = 1; x.b[0].appendChild('2'); x.b[0].*.length() === 2 && (x.normalize(), x.b[0].*.length() === 1) && x.b == '12'");
truth("var x = ; x.b[0].parent() === x && x.parent() === null");
truth("var x = ; x.processingInstructions().length() === 2 && x.processingInstructions('r').length() === 1");
truth("var x = ; x.propertyIsEnumerable(0) && !x.propertyIsEnumerable(1)");
truth("var x = ; x.replace('b', ); x.*.length() === 2 && x.*[0].localName() === 'z' && x.*[1].localName() === 'c'");
truth("var x = ; x.replace(0, 't'); x.* == 't'");
truth("var x = ; x.replace('nothing', ) === x && x.b.length() === 1");
truth("var x = ; x.setChildren( + ) === x && x.*.length() === 2 && x.c.length() === 1");
truth("var x = ; x.setLocalName('b'); x.localName() === 'b'");
truth("var x = ; x.setName('b'); x.localName() === 'b'");
truth("var x = ; x.setNamespace(new Namespace('p', 'u')); x.name().uri === 'u' && x.toXMLString() === ''");
assert_eq!(thrown_name("().setLocalName('1 x')"), "TypeError");
assert_eq!(thrown_name("().setName('a b')"), "TypeError");
assert_eq!(string("(1).toString()"), "1");
assert_eq!(string("().toString()"), "\n \n");
truth("var x = ; x.valueOf() === x");
truth("typeof XML.prototype.xpath === 'function' && typeof XMLList.prototype.xpath === 'function' && XML.prototype.xpath.length === 1");
truth("var x = ; x.b.attributes !== undefined");
}
#[test]
fn the_xml_list_methods_work_item_by_item() {
let l = "var l = <>s>;";
truth(&format!("{l} l.length() === 2 && l.attribute('p').length() === 2 && l.attributes().length() === 2"));
truth(&format!("{l} l.child('x').length() === 1 && l.children().length() === 4 && l.child(0).length() === 2"));
truth(&format!("{l} l.comments().length() === 1 && l.elements().length() === 2 && l.text() == 's'"));
truth(&format!("{l} l.descendants().length() === 4"));
truth(&format!("{l} l.contains(s) && !l.contains()"));
truth(&format!("{l} l.copy()[0] !== l[0] && l.copy() == l"));
truth(&format!("{l} l.hasOwnProperty(0) && !l.hasOwnProperty(5) && l.propertyIsEnumerable(1) && !l.propertyIsEnumerable(2)"));
truth(&format!("{l} l.hasComplexContent() && !l.hasSimpleContent()"));
truth(&format!("{l} l.parent() === undefined && l.valueOf() === l"));
truth("var x = ; x.*.parent() === x");
truth("new XMLList().hasSimpleContent() && !new XMLList().hasComplexContent() && new XMLList().parent() === undefined");
truth("var l = <>12>; l.normalize() === l && l.toString() === '1\\n2'");
truth("var r = 1; var l = r.a; l[0] = 2; r.a == '2' && l[0] == '2'");
truth("var r = 1; r.a[1] = 'n'; r.a.length() === 2 && r.a[1] == 'n' && r.*[1].parent() === r");
truth("var r = 1; var list = r.b; list.c = 'v'; r.b.c == 'v'");
truth("var r = ; r.a.@t[0] = '2'; r.a.@t == '2'");
truth("var r = 1; r.a[0] = <>>; r.*.length() === 3 && r.*[0].localName() === 'p'");
}
#[test]
fn inserting_a_node_moves_it_out_of_its_old_parent() {
truth("var b = ; var a = ; var c = b.c[0]; a.appendChild(c); b.c.length() === 0 && a.c.length() === 1 && c.parent() === a");
truth("var a = ; a.appendChild(a.b[0]); a.*.length() === 2 && a.*[0].localName() === 'c'");
truth("var b = ; var a = ; var c = b.c[0]; a.appendChild(c); c.appendChild(b); a..*.length() === 2 && b.parent() === c");
}
#[test]
fn namespaces_survive_serialization() {
assert_eq!(string("var x = ; x.toXMLString()"), "\n \n");
assert_eq!(string("var x = ; x.toXMLString()"), "");
truth("var x = ; var y = new XML(x.toXMLString()); x == y && y.*[0].name().uri === 'u'");
truth(
"var q = new QName(new Namespace('u'), 'a'); var x = new XML(''); x.appendChild(); x.*[0].setName(q); (new XML(x.toXMLString())).*[0].name().uri === 'u'",
);
truth(
"var x = ; x.@[new QName('u', 'b')] = '1'; var y = new XML(x.toXMLString()); y.attributes()[0].name().uri === 'u' && y.attributes()[0] == '1'",
);
}
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
#[test]
fn markup_cannot_declare_a_reserved_namespace_binding() {
let declarations = [
"xmlns:xml='wrong'".to_string(),
"xmlns:xmlns='u'".to_string(),
format!("xmlns:xmlns='{XMLNS_NS}'"),
format!("xmlns:p='{XML_NS}'"),
format!("xmlns:p='{XMLNS_NS}'"),
format!("xmlns='{XML_NS}'"),
format!("xmlns='{XMLNS_NS}'"),
];
for declaration in declarations {
assert_eq!(thrown_name(&format!("")), "SyntaxError", "{declaration}");
assert_eq!(thrown_name(&format!("new XML(\"\")")), "SyntaxError", "{declaration}");
}
assert_eq!(string(&format!("().toXMLString()")), format!(""));
assert_eq!(string("().toXMLString()"), "");
}
#[test]
fn add_namespace_rejects_a_reserved_binding_and_keeps_the_element() {
for (prefix, uri) in [("xmlns", "u"), ("xmlns", XMLNS_NS), ("xml", "u"), ("p", XML_NS), ("p", XMLNS_NS)] {
let src =
format!("var x = ; var name = 'none'; try {{ x.addNamespace(new Namespace('{prefix}', '{uri}')); }} catch (e) {{ name = e.name; }}");
assert_eq!(string(&format!("{src} name")), "TypeError", "{prefix} {uri}");
assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{prefix} {uri}");
}
assert_eq!(
string(&format!("var x = ; x.addNamespace(new Namespace('xml', '{XML_NS}')); x.toXMLString()")),
format!("")
);
}
#[test]
fn names_cannot_write_a_reserved_namespace_binding() {
for setter in [
"setNamespace(new Namespace('xmlns', 'u'))",
"setName(new QName(new Namespace('xml', 'u'), 'b'))",
"setNamespace(new Namespace('p', 'http://www.w3.org/2000/xmlns/'))",
"setNamespace('http://www.w3.org/2000/xmlns/')",
] {
let src = format!("var x = ; var name = 'none'; try {{ x.{setter}; }} catch (e) {{ name = e.name; }}");
assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}");
assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}");
}
let q = |prefix: &str| format!("new QName(new Namespace('{prefix}', 'u'), 'b')");
assert_eq!(string(&format!("var x = ; x[{}] = '1'; x.toXMLString()", q("xmlns"))), "\n 1\n");
assert_eq!(string(&format!("var x = ; x.@[{}] = '1'; x.toXMLString()", q("xml"))), "");
}
#[test]
fn an_attribute_cannot_be_renamed_xmlns() {
for setter in ["setName('xmlns')", "setLocalName('xmlns')"] {
let src = format!("var x = ; var name = 'none'; try {{ x.@b[0].{setter}; }} catch (e) {{ name = e.name; }}");
assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}");
assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}");
}
}
#[test]
fn new_nodes_cannot_take_a_reserved_name() {
let q = format!("new QName('{XMLNS_NS}', 'b')");
for update in ["x.@xmlns = 'u'".to_string(), format!("x[{q}] = '1'"), format!("x.@[{q}] = '1'"), format!("x[{q}][0] = '1'")] {
let src = format!("var x = ; var name = 'none'; try {{ {update}; }} catch (e) {{ name = e.name; }}");
assert_eq!(string(&format!("{src} name")), "TypeError", "{update}");
assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{update}");
}
}
#[test]
fn the_xmlns_uri_cannot_be_the_default_namespace() {
assert_eq!(thrown_name(&format!("default xml namespace = '{XMLNS_NS}'")), "TypeError");
assert_eq!(thrown_name(&format!("setDefaultXMLNamespace('{XMLNS_NS}')")), "TypeError");
}
#[test]
fn the_garbage_collector_keeps_trees_and_lists_alive() {
let mut r = realm();
r.run("var x = text; var l = x.b; var ns = new Namespace('p', 'u'); var q = new QName('u', 'n');").unwrap();
r.collect_garbage();
assert_eq!(r.run("x.b.@t == '1' && l[0] == 'text' && l[0].parent() === x && ns.uri === 'u' && q.localName === 'n'"), Ok(JsValue::Bool(true)));
r.run("x = l = ns = q = null;").unwrap();
let live = r.collect_garbage();
assert!(live < 100_000, "{live}");
}
// ---- hostile input ----
#[test]
fn malformed_literals_are_located_syntax_errors() {
let cases = [
"var a = ",
"",
"",
"",
"",
"",
"&bad;",
"",
"{1",
"{",
"<1x/>",
"",
"",
",<",
"",
"",
"<>",
">",
];
for src in cases {
let e = parse(src, &e4x()).expect_err(src);
assert!(e.line >= 1 && e.column >= 1, "{src}: {e:?}");
}
let e = parse("var a =\n ;", &e4x()).unwrap_err();
assert_eq!(e.line, 2);
}
#[test]
fn bad_xml_text_is_a_catchable_syntax_error() {
for s in [
"",
"",
"&nope;",
"",
"",
"",
"",
"]]>",
"",
] {
assert_eq!(thrown_name(&format!("new XML({s:?})")), "SyntaxError", "{s}");
}
assert_eq!(thrown_name("new XML('')"), "TypeError");
}
#[test]
fn an_xml_declaration_is_checked_against_its_grammar() {
for s in [
"",
"",
"",
"",
"\n",
"",
" ",
] {
truth(&format!("new XML({s:?}).localName() === 'a'"));
truth(&format!("var l = new XMLList({s:?}); l.length() === 1 && l[0].localName() === 'a'"));
}
for s in [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
] {
assert_eq!(thrown_name(&format!("new XML({s:?})")), "SyntaxError", "{s}");
assert_eq!(thrown_name(&format!("new XMLList({s:?})")), "SyntaxError", "{s}");
}
let message = |s: &str| string(&format!("try {{ new XML({s}); 'no error' }} catch (e) {{ e.message }}"));
assert_eq!(message("''"), "invalid XML declaration");
assert_eq!(message("''"), "invalid character in XML text");
assert_eq!(message("''"), "invalid character in XML text");
assert_eq!(message("''"), "'xml' cannot name a processing instruction");
truth(
"XML.ignoreProcessingInstructions = false; var x = new XML(''); x.children()[0].nodeKind() === 'processing-instruction'",
);
}
#[test]
fn unpaired_surrogates_are_rejected_in_xml_text() {
let lone = "String.fromCharCode(0xD800)";
let low = "String.fromCharCode(0xDC00)";
for value in [
format!("'' + {lone} + ''"),
format!("'' + {low} + ''"),
format!("''"),
format!("''"),
format!("''"),
format!("''"),
format!("'<' + {lone} + '/>'"),
] {
assert_eq!(thrown_name(&format!("new XML({value})")), "SyntaxError", "{value}");
}
assert_eq!(thrown_name(&format!("().setLocalName({lone})")), "TypeError");
let e = parse("var a = <\u{FFFD}/>;", &e4x()).unwrap_err();
assert_eq!((e.line, e.column), (1, 10));
truth("var p = String.fromCharCode(0xD83D, 0xDE00); var x = new XML('' + p + ''); x == p && x.@b == p");
truth("var p = String.fromCharCode(0xD83D, 0xDE00); new XML('<' + p + '/>').localName() === p");
}
#[test]
fn collections_during_parsing_and_copying_keep_new_nodes() {
let src = "for (var i = 0; i < 200; i++) { \
var x = new XML('t'); var c = x.copy(); \
if (c.@p != '1' || c.b.@r != '3' || c.*.length() !== 3 || x.@q != '2') throw new Error('lost'); \
} 'done'";
for spare in (16_000..20_000).step_by(37) {
let mut r = realm();
let live = r.collect_garbage();
r.set_limits(Limits { heap_bytes: live + spare, ..Limits::default() });
assert_eq!(r.run(src), Ok(JsValue::String("done".into())), "{spare}");
}
}
#[test]
fn collections_during_updates_and_queries_keep_new_values() {
let src = "for (var i = 0; i < 60; i++) { \
var x = new XML('t'); var c = x.copy(); \
var l = <>{x.b}{'t'}>; \
c.appendChild(); c.prependChild('s'); c.insertChildAfter(c.b[0], ); c.insertChildBefore(c.b[0], 'n'); \
c.replace(0, 1); c.n.o = 'v'; c.@t = 'w'; c.z[0] = ; c.g.h = 'deep'; \
var f = x.*.(nodeKind() === 'element'); var d = x..*; var j = x + c; var m = x.b + ; \
var ns = new Namespace('p', 'u'); var qn = new QName(ns, 'a'); c.addNamespace(ns); c.setNamespace(ns); c.setName(qn); \
var scope = c.inScopeNamespaces(); var decl = c.namespaceDeclarations(); c.normalize(); \
var text = c.toXMLString(); var again = new XML(text); var children = c.children(); var el = c.elements(); var attrs = c.attributes(); \
var filled = new XMLList(l); var copied = l.copy(); c.setChildren(); \
if (x.@p != '1' || x.*.length() !== 3 || f.length() !== 1 || j.length() !== 2 || m.length() !== 2) throw new Error('lost'); \
if (l.length() !== 3 || copied.length() !== 3 || c.*.length() !== 1 || scope.length < 1 || d.length() !== 3) throw new Error('lost'); \
} 'done'";
let mut first_error = None;
let mut completed = 0;
for spare in (40_000..56_000).step_by(211) {
let mut r = realm();
let live = r.collect_garbage();
r.set_limits(Limits { heap_bytes: live + spare, ..Limits::default() });
let got = r.run(src);
if got == Ok(JsValue::String("done".into())) {
completed += 1;
} else if !matches!(&got, Err(e) if e.kind == RunErrorKind::Limit(LimitKind::Heap)) {
first_error = Some(format!("{spare}: {got:?}"));
break;
}
}
assert_eq!(first_error, None);
assert!(completed > 0);
}
#[test]
fn a_scope_name_skips_a_list_without_that_name() {
truth("var outer = ; var inner = <>>; with (outer) with (inner) { @id == '1' }");
truth("var outer = ; var inner = <>>; with (outer) with (inner) { @id == '2' }");
}
#[test]
fn bare_attribute_assignment_uses_nearest_holder() {
truth("var x = ; with (x) { @b = '2'; } x.@b == '2'");
truth("var outer = ; var inner = ; with (outer) with (inner) { @id = '9'; } outer.@id == '9' && inner.@id.length() === 0");
truth("var outer = ; var inner = ; with (outer) with (inner) { @id = '9'; } outer.@id == '1' && inner.@id == '9'");
truth("var outer = ; var inner = <>>; with (outer) with (inner) { @id = '9'; } outer.@id == '9' && inner.@id.length() === 0");
truth("var x = ; with (x) with ({ b: 7 }) { @b = '3'; } x.@b == '3'");
truth("with () { (@b = 5) === 5 }");
truth("var x = ; with (x) { @b = @c = 'v'; } x.@b == 'v' && x.@c == 'v'");
truth("var n = 0; var x = ; with (x) { @[(n++, 'b')] = 'z'; } n === 1 && x.@b == 'z' && x.attributes().length() === 1");
truth("var x = 1; with (x) { *::c = '2'; } x.c == '2' && x.*.length() === 1");
truth("var x = ; x.@b = '4'; x.@b == '4'");
}
#[test]
fn bare_qualified_assignment_keeps_namespace() {
truth(
"var ns = new Namespace('u'); var x = 10; with (x) { ns::c = '2'; } \
x.ns::c == '2' && x.ns::c.name().uri === 'u' && x.c == '0' && x.*.length() === 2",
);
truth(
"var ns = new Namespace('u'); var x = ; with (x) { @ns::t = '5'; } \
x.@ns::t == '5' && x.@ns::t.name().uri === 'u' && x.@t == '0'",
);
}
#[test]
fn bare_assignment_in_filter_mutates_each_matching_item() {
truth("var l = <>>; var r = l.(@id = 'z', true); r.length() === 2 && l[0].@id == 'z' && l[1].@id == 'z'");
truth("var l = <>>; var r = l.(@n = 0); r.length() === 0 && l[0].@n == '0' && l[1].@n == '0'");
truth("var l = <>>; var r = l.(@n = 'k'); r.length() === 2");
truth("var o = ; var l = <>>; with (o) { l.(@k += 'x', true); } o.@k == '1xx' && l[0].@k.length() === 0");
}
#[test]
fn bare_assignment_without_holder_throws_reference_error_before_rhs() {
assert_eq!(thrown_name("@a = 1"), "ReferenceError");
assert_eq!(thrown_name("with ({ a: 1 }) { @a = 1 }"), "ReferenceError");
assert_eq!(thrown_name("with () { @a = 1 }"), "ReferenceError");
assert_eq!(thrown_name("with () { @a += 1 }"), "ReferenceError");
assert_eq!(thrown_name("with () { @a++ }"), "ReferenceError");
assert_eq!(thrown_name("var ns = new Namespace('u'); with () { ns::c = 1 }"), "ReferenceError");
truth("var n = 0; var e = ''; try { with () { @a = (n++, 1); } } catch (err) { e = err.name; } e === 'ReferenceError' && n === 0");
truth(
"var b = 'g'; var x = ; var e = ''; try { with (x) { @b = 'w'; } } catch (err) { e = err.name; } \
e === 'ReferenceError' && b === 'g' && x.@b.length() === 0 && x.*.length() === 0",
);
}
#[test]
fn bare_name_read_without_holder_throws_reference_error() {
assert_eq!(thrown_name("with () { @a }"), "ReferenceError");
assert_eq!(thrown_name("var l = <>>; l.(@id == '1')"), "ReferenceError");
truth("var o = ; var l = <>>; var r; with (o) { r = l.(@id == '1'); } r.length() === 1");
}
#[test]
fn bare_scope_compound_and_updates_resolve_once() {
truth("var n = 0; var x = ; with (x) { @[(n++, 't')] += 1; } n === 1 && x.@t == '11'");
truth("var n = 0; var x = ; with (x) { @[(n++, 't')]++; } n === 1 && x.@t == '2'");
truth("var x = ; var r; with (x) { r = ++@t; } r === 2 && x.@t == '2'");
truth("var x = ; var r; with (x) { r = @t++; } r === 1 && x.@t == '2'");
truth("var x = ; var r; with (x) { r = --@t; } r === 4 && x.@t == '4'");
truth("var x = ; var r; with (x) { r = @t--; } r === 5 && x.@t == '4'");
}
#[test]
fn bare_scope_delete_removes_attribute() {
truth("var x = ; var r; with (x) { r = delete @t; } r === true && x.@t.length() === 0 && x.@u == '2'");
truth("var x = ; with (x) { delete *::c; } x.c.length() === 0 && x.d.length() === 1");
assert_eq!(thrown_name("with () { delete @t }"), "ReferenceError");
}
#[test]
fn bare_scope_for_targets_assign_each_iteration() {
truth("var x = ; var s = ''; with (x) { for (@k in { p: 1, q: 2 }) s += @k; } s === 'pq' && x.@k == 'q'");
truth("var x = ; var s = ''; with (x) { for each (@k in [3, 4]) s += @k; } s === '34' && x.@k == '4'");
assert_eq!(thrown_name("with () { for (@k in { p: 1 }); }"), "ReferenceError");
}
#[test]
fn for_each_evaluates_its_target_before_reading_the_value() {
truth("var o = { p: 1 }; var t = {}; for each ((o.p = 2, t).v in o); t.v === 2");
truth("var o = { p: 1 }; var t = {}; for each (t[(o.p = 3, 'w')] in o); t.w === 3");
truth("var x = ; var o = { p: 1 }; with (x) { for each (@[(o.p = 2, 'k')] in o); } x.@k == '2'");
truth("var o = { p: 1, q: 2 }; var s = ''; for each (var v in o) s += v; var t; for each (t in o) s += t; s === '1212' && t === 2");
}
#[test]
fn bare_scope_targets_need_e4x_option() {
for src in ["@a = 1", "ns::a = 1", "@a += 1", "++@a", "@a--", "for (@a in o);", "for each (@a in o);", "\"use strict\"; @a = 1"] {
assert!(parse(src, &Options::default()).is_err(), "{src}");
assert!(parse(src, &e4x()).is_ok(), "{src}");
}
truth("'use strict'; var l = <>>; l.(@b = '2', true); l[0].@b == '2'");
}
#[test]
fn a_bracket_key_converts_to_a_string_before_it_names_a_child() {
truth("var x = 1; x[true] == '1' && (true in x) && !(false in x)");
truth("var x = 1; x[null] == '1' && x[undefined].length() === 0 && x[1.5].length() === 0 && x[-1].length() === 0");
truth("var x = ; x[false] = 'v'; x['false'] == 'v' && (delete x[false], x.*.length() === 0)");
assert_eq!(thrown_name("().attribute(true)"), "TypeError");
assert_eq!(thrown_name("().descendants(null)"), "TypeError");
}
fn kind_of(limits: Limits, src: &str) -> RunErrorKind {
match limited(limits).run(src) {
Ok(v) => panic!("{} completed with {v:?}", &src[..src.len().min(80)]),
Err(e) => e.kind,
}
}
fn on_small_stack(f: impl FnOnce() -> T + Send + 'static) -> T {
std::thread::Builder::new().stack_size(512 << 10).spawn(f).unwrap().join().unwrap()
}
#[test]
fn deep_trees_end_in_errors_on_a_small_stack() {
on_small_stack(|| {
let tags = 100_000;
let literal = format!("{}{}", "".repeat(tags), "".repeat(tags));
let e = parse(&literal, &e4x()).unwrap_err();
assert_eq!(e.message, crate::TOO_DEEP);
let text = format!("new XML('{literal}')");
assert_eq!(limited(Limits::default()).run(&text).map_err(|e| e.message.contains(crate::TOO_DEEP)), Err(true));
let chain = "var x = ; for (var i = 0; i < 100000; i++) { var y = ; y.appendChild(x); x = y; }";
assert_eq!(kind_of(Limits::default(), chain), RunErrorKind::Limit(LimitKind::Heap));
let within = "var x = ; for (var i = 0; i < 150; i++) { var y = ; y.appendChild(x); x = y; } \
var c = x.copy(); [c == x, x.descendants().length(), x.toXMLString().length > 0, (x.normalize(), 1)].join()";
assert_eq!(limited(Limits::default()).run(within), Ok(JsValue::String("true,150,true,1".into())));
});
}
#[test]
fn cyclic_insertion_throws_and_leaves_the_tree_alone() {
for src in [
"x.appendChild(x)",
"x.b[0].appendChild(x)",
"x.b[0].c[0].appendChild(x.b[0])",
"x.b[0].prependChild(x)",
"x.b[0].insertChildAfter(null, x)",
"x.b[0].insertChildBefore(null, x)",
] {
let setup = "var x = ; var before = x.toXMLString();";
let script = format!("{setup} var n; try {{ {src}; n = 'none' }} catch (e) {{ n = e.name }} x.toXMLString() === before ? n : 'changed'");
assert_eq!(string(&script), "TypeError", "{src}");
}
}
#[test]
fn replace_inserts_a_copy_so_an_ancestor_can_be_the_value() {
truth("var x = ; x.b[0].replace(0, x); x.b.a.b.c.length() === 1 && x.b.a[0] !== x && x.descendants().length() === 4");
}
const TIGHT: Limits = Limits { fuel: 200_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 };
#[test]
fn big_values_end_with_the_heap_limit() {
let attrs = format!("var x = new XML('');", (0..50_000).map(|i| format!("a{i}=\"1\"")).collect::>().join(" "));
assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, &attrs), RunErrorKind::Limit(LimitKind::Heap));
let wide = "var l = <>>; for (;;) { l = l + l; }";
assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, wide), RunErrorKind::Limit(LimitKind::Heap));
let giant = "var s = 'x'; for (var i = 0; i < 40; i++) s += s; new XML('' + s + '')";
assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, giant), RunErrorKind::Limit(LimitKind::Heap));
let escaped = "var s = '<'; for (var i = 0; i < 17; i++) s += s; ({s}).toXMLString().length";
assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, escaped), RunErrorKind::Limit(LimitKind::Heap));
}
#[test]
fn catastrophic_queries_end_with_the_fuel_limit() {
let build = "var x = ; for (var i = 0; i < 2000; i++) x.appendChild();";
let limits = Limits { fuel: 1_500_000, heap_bytes: 64 << 20, frames: 200, reentry: 8 };
let filter = format!("{build} for (;;) x.a.(true);");
assert_eq!(kind_of(limits.clone(), &filter), RunErrorKind::Limit(LimitKind::Fuel));
let descendants = format!("{build} for (;;) x..a;");
assert_eq!(kind_of(limits.clone(), &descendants), RunErrorKind::Limit(LimitKind::Fuel));
let serialize = format!("{build} for (;;) x.toXMLString();");
assert_eq!(kind_of(limits, &serialize), RunErrorKind::Limit(LimitKind::Fuel));
}
/// The fuel that `action` takes when it runs after `setup`.
fn fuel_of(setup: &str, action: &str) -> u64 {
let mut r = realm();
r.run(setup).unwrap();
r.run(action).unwrap();
Limits::default().fuel - r.fuel_left()
}
#[test]
fn insert_child_before_and_after_take_fuel_for_each_child_they_scan() {
let narrow = "var x = ; var last = x.b[0];";
let wide = "var x = ; for (var i = 0; i < 2000; i++) x.appendChild(); var last = x.b[1999];";
for action in
["x.insertChildAfter(, )", "x.insertChildBefore(, )", "x.insertChildAfter(last, )", "x.insertChildBefore(last, )"]
{
let (n, w) = (fuel_of(narrow, action), fuel_of(wide, action));
assert!(w >= n + 1999, "{action}: {n} with one child, {w} with 2000");
}
}
#[test]
fn namespace_lookups_take_fuel_for_each_ancestor_they_climb() {
let shallow = "var x = ; var leaf = x.children()[0];";
let deep = "var x = ; var leaf = x; for (var i = 0; i < 150; i++) { var c = ; leaf.appendChild(c); leaf = c; }";
for action in ["leaf.inScopeNamespaces()", "leaf.namespaceDeclarations()", "leaf.namespace()", "leaf.namespace('p')"] {
let (s, d) = (fuel_of(shallow, action), fuel_of(deep, action));
assert!(d >= s + 149, "{action}: {s} one level down, {d} 150 levels down");
}
}
#[test]
fn xml_text_takes_fuel_for_every_run_it_scans() {
// `pad` is white space and `name` is name characters, each one unit long in the short run and
// 16,384 units long in the long run.
let setup = |doublings: u32, text: &str| {
format!("var pad = ' ', name = 'n'; for (var i = 0; i < {doublings}; i++) {{ pad += pad; name += name; }} var s = {text};")
};
let action = "try { new XML(s) } catch (e) {}";
let mut flat = Vec::new();
for text in [
"''",
"pad + ''",
"''",
"''",
"''",
"''",
"''",
"''",
"''",
"''",
"''",
"''",
"'' + pad + ']]>'",
"'; (x.@p + x.comments()).xpath('.').length() === 0");
truth("new XMLList().xpath('a').length() === 0 && new XMLList().xpath('z:a').length() === 0");
truth("var l = <>>; l.xpath('p:b').length() === 2");
assert_eq!(thrown_name("var l = <>>; l.xpath('p:b')"), "TypeError");
}
#[test]
fn xpath_expressions_of_any_depth_use_no_native_stack() {
let deep = format!("{}1{}", "(".repeat(100_000), ")".repeat(100_000));
assert_eq!(thrown_name(&format!("().xpath('{deep}')")), "SyntaxError");
let predicates = format!("{}{}", "a[".repeat(100_000), "]".repeat(100_000));
assert_eq!(thrown_name(&format!("().xpath('{predicates}')")), "SyntaxError");
let sum = format!("1{}", " + 1".repeat(100_000));
truth(&format!("().xpath('self::*[{sum} = 100001]').length() === 1"));
let negated = format!("{}1", "-".repeat(100_001));
truth(&format!("().xpath('self::*[{negated} = -1]').length() === 1"));
let union = format!("a{}", " | a".repeat(50_000));
truth(&format!("().xpath('{union}').length() === 1"));
let mut shallow = Realm::with_options(Options { max_depth: 8, ..e4x() }, Limits::default());
let seven = format!("{}1{}", "(".repeat(6), ")".repeat(6));
assert_eq!(shallow.run(&format!("().xpath('self::*[{seven}]').length()")), Ok(JsValue::Number(1.0)));
let eight = format!("{}1{}", "(".repeat(8), ")".repeat(8));
assert_eq!(shallow.run(&format!("try {{ ().xpath('{eight}') }} catch (e) {{ e.message }}")), Ok(JsValue::String("nesting too deep".into())));
}
#[test]
fn xpath_stops_at_the_fuel_and_heap_limits_leaving_the_tree() {
let setup = "var x = ; for (var i = 0; i < 6; i++) x.appendChild({i});";
let check = "x.a.length() === 6 && x.a[5].b == '5' && x.*.length() === 6";
assert!(fuel_stops(setup, "x.xpath('//b[. > 2]/preceding::*')", check) > 0);
assert!(heap_stops(setup, "x.xpath((SPARE, '//b[contains(concat(., ., .), \"3\")] | //a'))", check) > 0);
let mut r = realm();
r.run("var x = ; for (var i = 0; i < 500; i++) x.appendChild();").unwrap();
let interrupt = r.interrupt();
r.define_function("stop", move |_| {
interrupt.set();
Ok(JsValue::Undefined)
});
assert_eq!(r.run("stop(); x.xpath('//a/following::a')").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Interrupted)));
}
#[test]
fn xpath_walks_of_deep_trees_and_long_lists_count_against_the_heap_limit() {
let deep = "var s = ''; for (var i = 0; i < 5000; i++) s += ''; s += ''; for (var i = 0; i < 5000; i++) s += ''; var x = new XML(s);";
for (action, room) in [
("x.xpath('descendant::c').length()", JsValue::Number(0.0)),
("x.xpath('self::*[string() = \"q\"]').length()", JsValue::Number(0.0)),
("x.xpath('descendant::b | self::a').length()", JsValue::Number(2.0)),
] {
for (spare, want) in [(20_000, Err(RunErrorKind::Limit(LimitKind::Heap))), (50_000_000, Ok(room.clone()))] {
let mut r = Realm::with_options(Options { max_depth: 1_000_000, ..e4x() }, Limits::default());
let interp = r.interp();
let global = interp.global;
assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok());
r.run(deep).unwrap();
assert_eq!(r.run(&format!("squeeze({spare}); {action}")).map_err(|e| e.kind), want, "{action} {spare}");
}
}
let long = "var s = ''; for (var i = 0; i < 20000; i++) s += ''; var l = new XML(s + '').a;";
assert_eq!(after_setup(long, "squeeze(50000); l.xpath('self::c').length()"), Err(RunErrorKind::Limit(LimitKind::Heap)));
assert_eq!(after_setup(long, "squeeze(50000000); l.xpath('self::c').length()"), Ok(JsValue::Number(0.0)));
}
#[test]
fn xpath_reading_a_long_string_takes_fuel_for_its_length() {
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var x = ; for (var i = 0; i < 200; i++) x.appendChild(y);";
for expr in ["a[string-length($s) = 1]", "a[starts-with($s, $s) = false()]", "a[$s = concat('', 'x')]", "a[number($s) = 1]", "a[. = $s]"] {
let mut r = realm();
r.run(setup).unwrap();
let src = format!("x.xpath({expr:?}, {{ s: s }}).length()");
assert_eq!(r.run(&src), Ok(JsValue::Number(0.0)), "{expr}");
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
assert_eq!(r.run(&src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)), "{expr}");
}
}
#[test]
fn xpath_string_functions_hold_the_bytes_they_build() {
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var d = '1'; for (var i = 0; i < 20; i++) d += d; var o = { s: s, d: d };";
for (expr, spare) in [("translate('a', 'b', $s) = 'a'", 1_000_000), ("number($d) > 1", 500_000), ("substring($s, 1) != 'z'", 4_000_000)] {
let action = |spare: u32| format!("squeeze({spare}); ().xpath(\"self::*[{expr}]\", o).length()");
assert_eq!(after_setup(setup, &action(spare)), Err(RunErrorKind::Limit(LimitKind::Heap)), "{expr}");
assert_eq!(after_setup(setup, &action(100_000_000)), Ok(JsValue::Number(1.0)), "{expr}");
}
}
#[test]
fn xpath_string_variables_hold_the_bytes_of_their_flat_copy() {
// The setup ends with `0` so that its completion value does not flatten `s`.
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var o = { s: s }; 0;";
let action = |spare: u32| format!("squeeze({spare}); ().xpath('self::*[$s]', o).length()");
assert_eq!(after_setup(setup, &action(1_000_000)), Err(RunErrorKind::Limit(LimitKind::Heap)));
assert_eq!(after_setup(setup, &action(100_000_000)), Ok(JsValue::Number(1.0)));
}
#[test]
fn xpath_substring_searches_take_fuel_for_every_position_tried() {
let setup = "var h = 'a'; for (var i = 0; i < 16; i++) h += h; var n = h.substring(0, 4095) + 'b'; var o = { h: h, n: n };";
for expr in ["contains($h, $n)", "substring-before($h, $n) = ''", "substring-after($h, $n) = ''"] {
let mut r = realm();
r.run(setup).unwrap();
let src = format!("().xpath(\"self::*[{expr}]\", o).length()");
let want = if expr.starts_with("contains") { 0.0 } else { 1.0 };
assert_eq!(r.run(&src), Ok(JsValue::Number(want)), "{expr}");
r.set_limits(Limits { fuel: 5_000_000, ..Limits::default() });
assert_eq!(r.run(&src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)), "{expr}");
}
}
#[test]
fn xpath_lang_takes_fuel_for_every_attribute_it_reads() {
let setup = "var s = ''; \
for (var i = 0; i < 400; i++) s += ''; var x = new XML(s + '');";
let mut r = realm();
r.run(setup).unwrap();
let src = "x.xpath('a[lang(\"en\")]').length()";
assert_eq!(r.run(src), Ok(JsValue::Number(0.0)));
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
assert_eq!(r.run(src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)));
}
#[test]
fn xpath_on_a_large_tree_ends_within_the_default_limits() {
let setup = "var x = ; for (var i = 0; i < 3000; i++) x.appendChild();";
truth(&format!("{setup} x.xpath('//a[@n = 2999]/b').length() === 1 && x.xpath('//b/..').length() === 3000"));
let mut r = realm();
r.run("var x = ; for (var i = 0; i < 2000; i++) x.appendChild();").unwrap();
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
assert_eq!(r.run("x.xpath('//a/following::a')").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)));
}
const XPATH_PIECES: &[&str] = &[
"/",
"//",
"a",
"b",
"*",
"@",
"@*",
".",
"..",
"[",
"]",
"(",
")",
"1",
"'x'",
"$v",
"|",
"+",
"-",
"=",
"!=",
"<",
">=",
" and ",
" or ",
" div ",
" mod ",
"child::",
"following::",
"preceding::",
"ancestor::",
"namespace::",
"node()",
"text()",
"count(",
"last()",
"position()",
"concat(",
"substring(",
",",
"p:",
"p:*",
"xml:lang",
"::",
":",
"'",
" ",
];
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn random_xpath_expressions_select_nodes_or_throw(parts in proptest::collection::vec(0..XPATH_PIECES.len(), 0..30)) {
let expr: String = parts.iter().map(|&i| XPATH_PIECES[i]).collect();
let quoted = format!("{expr:?}");
let src = format!("var x = 23t; typeof XML.prototype.xpath === 'function' && x.a[0].xpath({quoted}, {{ v: 1 }}).length() >= 0");
let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 });
match r.run(&src) {
Ok(v) => prop_assert_eq!(v, JsValue::Bool(true), "{}", expr),
Err(e) => prop_assert!(matches!(e.kind, RunErrorKind::Thrown | RunErrorKind::Limit(_)), "{expr:?}: {e:?}"),
}
}
}
// ---- generated input ----
const PIECES: &[&str] = &[
"<",
">",
"/",
"",
"/>",
"",
"",
"",
"",
"<>",
">",
"{",
"}",
"{1}",
"'",
"\"",
"&",
"&",
"A",
"&bad;",
"",
"",
"",
"@",
"::",
"..",
".(",
")",
"*",
"/x/",
" ",
"\n",
"a",
"1",
"=",
"ns",
":",
"xmlns",
"x.@a",
"x..y",
"default xml namespace",
];
fn tree_markup(shape: &[u8]) -> String {
let mut out = String::new();
let mut open: Vec = Vec::new();
out.push_str("");
for (i, &b) in shape.iter().enumerate() {
match b % 6 {
0 if open.len() < 20 => {
out.push_str(&format!(""));
open.push(i);
}
1 => {
if let Some(e) = open.pop() {
out.push_str(&format!(""));
}
}
2 => out.push_str(&format!("")),
3 => out.push_str(&format!("t{b}&")),
4 => out.push_str(&format!("")),
_ => out.push_str(&format!("")),
}
}
while let Some(e) = open.pop() {
out.push_str(&format!(""));
}
out.push_str("");
out
}
const TAG_PIECES: [&str; 11] = ["a", "b", " ", "=", "\"", "'", "<", "/", "<", "a=\"1\"", ">; v.children().length() === 0 && v.attributes().length() === 0"),
format!("var v = ; v.children().length() === 0 && v.name().localName === 'x'"),
format!("(<>>).length() === 1"),
format!("var v = {{{quoted}}}>; v.children().length() === 1"),
] {
let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 });
match r.run(&src) {
Ok(v) => prop_assert_eq!(v, JsValue::Bool(true), "{}", src),
Err(e) => prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}"),
}
}
}
#[test]
fn source_mixes_parse_or_give_a_located_error(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) {
let src: String = parts.iter().map(|&i| PIECES[i]).collect();
if let Err(e) = parse(&src, &e4x()) {
prop_assert!(e.line >= 1 && e.column >= 1, "{src:?}: {e:?}");
}
}
#[test]
fn random_programs_never_reach_an_internal_error(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) {
let src: String = format!("var x = ; {}", parts.iter().map(|&i| PIECES[i]).collect::());
let mut r = limited(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 });
if let Err(e) = r.run(&src) {
prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}");
}
}
#[test]
fn xml_text_parses_or_throws(parts in proptest::collection::vec(0..PIECES.len(), 0..40)) {
let text: String = parts.iter().map(|&i| PIECES[i]).collect();
let quoted = format!("{text:?}");
let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 });
if let Err(e) = r.run(&format!("new XML({quoted})")) {
prop_assert!(e.kind == RunErrorKind::Thrown, "{quoted}: {e:?}");
}
}
#[test]
fn serialized_trees_parse_to_an_equal_tree(shape in proptest::collection::vec(any::(), 0..80)) {
let markup = tree_markup(&shape);
let quoted = format!("{markup:?}");
let plain = format!("XML.prettyPrinting = false; XML.ignoreWhitespace = false; var x = new XML({quoted}); var y = new XML(x.toXMLString()); x.normalize(); y.normalize(); x == y");
prop_assert_eq!(limited(Limits::default()).run(&plain), Ok(JsValue::Bool(true)), "{}", markup);
let pretty = format!("var x = new XML({quoted}); var y = new XML(x.toXMLString()); x.toXMLString() === y.toXMLString()");
prop_assert_eq!(limited(Limits::default()).run(&pretty), Ok(JsValue::Bool(true)), "{}", markup);
}
#[test]
fn insert_sequences_stay_acyclic(steps in proptest::collection::vec((0usize..8, 0usize..8), 0..40)) {
let mut src = String::from("var n = []; for (var i = 0; i < 8; i++) n.push(new XML('')); var bad = 0;");
for (a, b) in steps {
src.push_str(&format!("try {{ n[{a}].appendChild(n[{b}]); }} catch (e) {{ if (e.name !== 'TypeError') bad++; }}"));
}
src.push_str("var ok = bad === 0; for (var j = 0; j < 8; j++) { ok = ok && n[j].toXMLString().length > 0 && n[j].descendants().length() < 8; } ok");
let got = limited(Limits::default()).run(&src);
prop_assert_eq!(got, Ok(JsValue::Bool(true)), "{}", src);
}
}