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).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>)", "(t)", "(<{''}/>)", "(<{'1a'}/>)", "()", 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).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}"; 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#"()"#, ] { 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 = '');"; 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 = ; 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); } }