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("!Object.prototype.hasOwnProperty.call(XML.prototype, 'xpath') && !Object.prototype.hasOwnProperty.call(XMLList.prototype, 'xpath')"); 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 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 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 limits_cannot_be_caught_by_the_script() { assert_eq!(kind_of(TIGHT, "try { for (;;) ().toXMLString(); } catch (e) { 1 }"), RunErrorKind::Limit(LimitKind::Fuel)); } /// Collects garbage and sets the heap limit to the live size plus the argument, so a script can /// run out of heap at a chosen charge. A negative argument puts the heap over its limit, which /// also happens when a lowered limit meets a larger heap. fn squeeze( i: &mut crate::interp::Interp, _: crate::value::Value, args: &[crate::value::Value], _: bool, ) -> crate::interp::JsResult { i.collect_garbage(); if let Some(crate::value::Value::Number(spare)) = args.first() { i.limits.heap_bytes = (i.heap.bytes as f64 + *spare) as usize; } Ok(crate::value::Value::Undefined) } /// Runs `setup`, then the action for each step from 0 until one completes, and after each stop /// asserts that `check` is true with the limits lifted. Returns how many runs stopped at `kind`. fn stops_with_the_tree_intact(setup: &str, action: impl Fn(usize) -> String, check: &str, kind: LimitKind, limit: impl Fn(usize) -> Limits) -> usize { let mut r = realm(); let interp = r.interp(); let global = interp.global; assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); let mut stops = 0; for step in 0..200_000 { r.set_limits(Limits::default()); r.run(setup).unwrap(); r.set_limits(limit(step)); let action = action(step); let got = r.run(&action); r.set_limits(Limits::default()); assert_eq!(r.run(check), Ok(JsValue::Bool(true)), "{action}: {got:?}"); match got { Ok(_) => return stops, Err(e) if e.kind == RunErrorKind::Limit(kind) => stops += 1, Err(_) => {} } } panic!("{check} never completed"); } /// Runs `action`, whose `SPARE` is replaced by a call that leaves each number of bytes free in /// turn, from 512 too few. Actions put it after the steps whose allocations a collection could /// free: an argument is evaluated after the callee is looked up. fn heap_stops(setup: &str, action: &str, check: &str) -> usize { let action = |spare: usize| action.replace("SPARE", &format!("squeeze({})", spare as i64 - 512)); stops_with_the_tree_intact(setup, action, check, LimitKind::Heap, |_| Limits::default()) } fn fuel_stops(setup: &str, action: &str, check: &str) -> usize { stops_with_the_tree_intact(setup, |_| action.to_string(), check, LimitKind::Fuel, |fuel| Limits { fuel: fuel as u64, ..Limits::default() }) } const MOVE_SETUP: &str = "var o = ; var t = ; var n = o.b[0];"; const MOVE_CHECK: &str = "o.b.length() + t.b.length() === 1 && (o.b.length() === 0 || o.b[0].parent() === o) && (t.b.length() === 0 || t.b[0].parent() === t)"; #[test] fn a_heap_limit_during_a_move_leaves_the_node_in_its_old_parent() { for action in ["t.appendChild((SPARE, n))", "t.prependChild((SPARE, n))", "t.insertChildAfter(null, (SPARE, n))", "t.insertChildBefore(null, (SPARE, n))"] { assert!(heap_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}"); } } #[test] fn a_fuel_limit_during_a_move_leaves_the_node_in_its_old_parent() { for action in ["t.appendChild(n)", "t.prependChild(n)"] { assert!(fuel_stops(MOVE_SETUP, action, MOVE_CHECK) > 0, "{action}"); } let replaced = "var l = t.*; l[0] = o.b[0]"; let setup = "var o = ; var t = ;"; assert!(fuel_stops(setup, replaced, MOVE_CHECK) > 0); } #[test] fn a_heap_limit_on_replace_past_the_end_leaves_no_child_without_a_parent() { let check = "var n = x.*.length(); if (n === 0) { true } else { \ var v = x.*[n - 1]; var name; try { v.appendChild(x); name = 'none' } catch (e) { name = e.name } v.parent() === x && name === 'TypeError' }"; let action = "x.replace({ toString: function () { SPARE; return '5'; } }, v)"; assert!(heap_stops("var x = ; var v = ;", action, check) > 0); } #[test] fn limits_during_deletes_and_puts_keep_the_other_nodes() { let setup = "var x = old;"; let others = "x.c.length() === 1 && x.@q == '2'"; for action in ["delete x.b", "delete x.@p", "x.@p = 'v'", "x.@n = 'v'", "x.b = 'v'"] { assert!(fuel_stops(setup, action, others) > 0, "{action}"); } let held = format!("{others} && x.b.length() === 1 && (x.b == 'old' || x.b == 'v') && x.b.*[0].parent() === x.b[0]"); assert!(fuel_stops(setup, "x.b = 'v'", &held) > 0); assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &held) > 0); } /// True when the markup of `tree` is `before` or `after`, ignoring white space. fn tree_is(tree: &str, before: &str, after: &str) -> String { format!("(function () {{ var s = {tree}.toXMLString().replace(/\\s/g, ''); return s === '{before}' || s === '{after}'; }})()") } #[test] fn a_limit_while_a_list_replaces_a_child_leaves_the_children() { let setup = "var x = ; var v = <>

;"; let replaced = tree_is("x", "", "

"); for action in ["x.replace(0, v)", "x.b = v"] { assert!(fuel_stops(setup, action, &replaced) > 0, "{action}"); } let spare = "x.replace({ toString: function () { SPARE; return '0'; } }, v)"; assert!(heap_stops(setup, spare, &replaced) > 0); let set = tree_is("x", "", "

"); assert!(fuel_stops(setup, "x.setChildren(v)", &set) > 0); } #[test] fn a_limit_while_a_list_is_inserted_leaves_the_children() { let setup = "var o =

; var x = ; var v = o.*;"; let check = |after: &str| { format!("{} && (o.*.length() === 0 ? x.*.length() === 3 : o.*.length() === 2 && x.*.length() === 1)", tree_is("x", "", after)) }; for (action, after) in [ ("x.appendChild(v)", "

"), ("x.prependChild(v)", "

"), ("x.insertChildAfter(null, v)", "

"), ("x.insertChildBefore(null, v)", "

"), ] { assert!(fuel_stops(setup, action, &check(after)) > 0, "{action}"); } assert!(heap_stops(setup, "x.appendChild((SPARE, v))", &check("

")) > 0); } #[test] fn a_heap_limit_on_a_list_put_at_an_index_keeps_the_list_and_tree_in_step() { let setup = "var x = ; var l = x.*; var v = <>

;"; let check = "var s = x.toXMLString().replace(/\\s/g, ''); \ (s === '' && l.length() === 2 && l[0].name() == 'b') \ || (s === '

' && l.length() === 3 && l[0].name() == 'p')"; assert!(heap_stops(setup, "l[(SPARE, 0)] = v", check) > 0); } #[test] fn an_index_put_of_an_earlier_sibling_keeps_that_node_in_the_list() { let setup = "var x = ; var l = x.*;"; truth(&format!("{setup} l[2] = x.b; l[2] === x.b[0] && l[3] === x.e[0]")); truth(&format!("{setup} l[2] = x.b[0]; l[2] === x.b[0] && l[3] === x.e[0]")); } /// A realm whose trees may be 32 levels deep, and a script line making `deep`, a tree of 32 levels /// that cannot go under any element. fn shallow_with_deep_value() -> (Realm, &'static str) { let deep = "var s = ''; var e = ''; for (var i = 0; i < 32; i++) { s += ''; e += ''; } var deep = new XML(s + e);"; (Realm::with_options(Options { max_depth: 32, ..e4x() }, Limits::default()), deep) } #[test] fn a_too_deep_value_leaves_every_match_in_place() { for action in ["x.replace('b', deep)", "x.b = deep"] { let (mut r, deep) = shallow_with_deep_value(); r.run(deep).unwrap(); r.run("var x = ;").unwrap(); assert_eq!(r.run(action).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Heap)), "{action}"); assert_eq!(r.run("x.b.length() === 2 && x.*.length() === 3"), Ok(JsValue::Bool(true)), "{action}"); } } #[test] fn a_limit_while_matches_are_replaced_keeps_every_match() { let setup = "var x = ; var n = ; var l = <>

;"; let two = ""; for (action, after) in [ ("x.replace('b', n)", ""), ("x.replace('b', l)", "

"), ("x.b = n", ""), ("x.b = l", "

"), ("x.b = 'v'", "v"), ] { assert!(fuel_stops(setup, action, &tree_is("x", two, after)) > 0, "{action}"); } let by_name = "x.replace({ toString: function () { SPARE; return 'b'; } }, n)"; assert!(heap_stops(setup, by_name, &tree_is("x", two, "")) > 0); assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &tree_is("x", two, "v")) > 0); } #[test] fn matches_are_replaced_in_document_order() { truth("var x = 123; x.replace('b', ); x.*.length() === 3 && x.*[0].name() == 'n' && x.d.length() === 1"); truth("var x = 12; var old = x.b[1]; x.b = 'v'; x.b.length() === 1 && x.b == 'v' && old.parent() === null"); truth("var x = ; x.b = 'v'; x.b == 'v' && x.*.length() === 2 && x.b[0].parent() === x"); } #[test] fn an_attribute_put_among_children_goes_in_as_text_and_stays_on_its_owner() { let setup = "var y = ; var x = ;"; let check = "x.b.length() === 0 && x.children().length() === 1 && x.children()[0].nodeKind() === 'text' && x.toString() === 'v' \ && y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y"; for edit in ["x.replace(0, y.@p)", "x.replace(0, y.@p[0])", "x.b = y.@p", "x.setChildren(y.@p)", "x.*[0] = y.@p"] { truth(&format!("{setup} {edit}; {check}")); } let owner = "y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y"; for (edit, k) in [ ("x.prependChild(y.@p)", "0"), ("x.prependChild(y.@p[0])", "0"), ("x.appendChild(y.@p)", "1"), ("x.appendChild(y.@p[0])", "1"), ("x.insertChildAfter(x.b[0], y.@p)", "1"), ("x.insertChildBefore(x.b[0], y.@p[0])", "0"), ] { truth(&format!( "{setup} {edit}; x.children().length() === 2 && x.children()[{k}].nodeKind() === 'text' && x.children()[{k}] == 'v' && {owner}" )); } } #[test] fn a_list_assigned_attributes_holds_the_text_nodes_in_the_tree() { truth( "var y = ; var x = ; var l = x.*; l[0] = y.@*; \ l.length() === 2 && l[0].nodeKind() === 'text' && l[0].parent() === x && l[1].nodeKind() === 'text' && l[1].parent() === x \ && x.toString() === 'vw' && y.@*.length() === 2", ); } #[test] fn an_attribute_list_goes_in_as_text_at_the_depth_limit() { let open = "".repeat(7); let close = "".repeat(7); let src = format!( "var x = new XML('{open}{close}'); var c = x; while (c.a.length()) c = c.a[0]; \ var y = ; c.prependChild(y.@p); c.toString() === 'v'" ); let mut realm = Realm::with_options(Options { max_depth: 8, ..e4x() }, Limits::default()); assert_eq!(realm.run(&src), Ok(JsValue::Bool(true))); } #[test] fn a_limit_during_normalize_does_not_repeat_text() { let setup = "var x = ; x.appendChild('p'); x.appendChild('q'); x.appendChild('r');"; let check = "x.toString() === 'pqr'"; assert!(fuel_stops(setup, "x.normalize()", check) > 0); } /// Runs `action` as [`heap_stops`] does, and asserts that every run completes or stops at the heap /// limit and that `check` is true after it. Returns how many runs stopped. fn heap_stops_only(setup: &str, action: &str, check: &str) -> usize { let mut r = realm(); let interp = r.interp(); let global = interp.global; assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); let mut stops = 0; for spare in 0..100_000i64 { r.set_limits(Limits::default()); r.run(setup).unwrap(); let action = action.replace("SPARE", &format!("squeeze({})", spare - 512)); let got = r.run(&action); r.set_limits(Limits::default()); assert_eq!(r.run(check), Ok(JsValue::Bool(true)), "{action}: {got:?}"); match got { Ok(_) => return stops, Err(e) if e.kind == RunErrorKind::Limit(LimitKind::Heap) => stops += 1, Err(e) => panic!("{action}: {e:?}"), } } panic!("{action} never completed"); } #[test] fn collections_while_a_list_is_copied_keep_every_copy() { let setup = "var x = ; var v = + ; var w = undefined;"; let set = tree_is("x", "", ""); for action in ["x.setChildren((SPARE, v))", "x.* = (SPARE, v)"] { assert!(heap_stops_only(setup, action, &set) > 0, "{action}"); } let copied = "w === undefined || (w.length() === 2 && w[0].name() == 'c' && w[1].name() == 'd' && w[0] !== v[0])"; assert!(heap_stops_only(setup, "w = (SPARE, v).copy()", copied) > 0); } #[test] fn a_limit_during_path_assignment_leaves_no_new_element() { let setup = "var x = ; var v = ;"; for (target, value, after) in [ ("x.a.b.c", "v", ""), ("x.a.b", "'text'", "text"), ("x.a.@p", "'text'", ""), ("x.a.b[0]", "'text'", "text"), ] { let check = tree_is("x", "", after); assert!(fuel_stops(setup, &format!("{target} = {value}"), &check) > 0, "{target}"); assert!(heap_stops_only(setup, &format!("{target} = (SPARE, {value})"), &check) > 0, "{target}"); } } #[test] fn comparing_xml_values_is_charged_to_the_heap() { let setup = "var x = t; var y = x.copy();"; assert!(heap_stops_only(setup, "(SPARE, x == y)", "x == y && y.@q == '2'") > 0); } /// Runs `setup` with no limit, then `action` in the same realm, and returns how `action` ended. fn after_setup(setup: &str, action: &str) -> Result { let mut r = realm(); let interp = r.interp(); let global = interp.global; assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok()); r.run(setup).unwrap(); r.run(action).map_err(|e| e.kind) } #[test] fn namespace_declarations_and_qname_strings_are_charged_to_the_heap() { let heap = Err(RunErrorKind::Limit(LimitKind::Heap)); let distinct = "var x = ; var a = []; for (var i = 0; i < 400; i++) a.push(new Namespace('p' + i, 'u' + i));"; for method in ["addNamespace", "setNamespace"] { let action = format!("squeeze(4096); for (var i = 0; i < 400; i++) x.{method}(a[i]); 1"); assert_eq!(after_setup(distinct, &action), heap, "{method}"); } let same = "var x = ; var n = new Namespace('p', 'u');"; let action = "squeeze(4096); for (var i = 0; i < 400; i++) x.addNamespace(n); 1"; assert_eq!(after_setup(same, action), Ok(JsValue::Number(1.0))); let long = "var s = 'x'; for (var i = 0; i < 12; i++) s += s; var q = new QName('u', s);"; let action = "squeeze(12000); var t1 = q.toString(); var t2 = q.toString(); 1"; assert_eq!(after_setup(long, action), heap); } #[test] fn an_interrupt_stops_serialization() { 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.toXMLString()").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Interrupted))); } // ---- 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); } }