Files
vectorcraft/crates/js/src/xml_tests.rs
T
dotCooCoo 28042c2e92 JavaScript: test that new XML copies a list item's attributes and children
The test for new XML with a one-item list now starts from an item that
has a parent, an attribute and a child. It checks that the copy has no
parent and that changing the copy's attribute and child leaves the
original unchanged, so a copy that drops or shares the children fails.
2026-11-11 09:47:33 -08:00

1231 lines
58 KiB
Rust

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 = <bookstore>\
<book category=\"COOKING\"><title lang=\"en\">Cookbook</title><author>Farmer</author><year>1896</year></book>\
<book category=\"CHILDREN\"><title lang=\"en\">Oz</title><author>Baum</author><year>1900</year></book>\
<book category=\"CHILDREN\"><title lang=\"en\">Alice</title><author>A</author><author>B</author><author>C</author><year>1865</year></book>\
<book category=\"MUSIC\"><title lang=\"en\">Opera</title><author>Walbrook</author><year>1922</year></book>\
</bookstore>;";
const TOP: &str = "var x = new XML('<top>\\n<one>one text</one>\\n<two>\\ntwo text\\n<inside>inside text</inside>\\n</two>\\ntop text\\n</top>');";
// ---- default isolation ----
#[test]
fn without_the_option_e4x_syntax_is_rejected_and_the_globals_are_absent() {
for src in ["<x/>", "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 && /a<b/.test('a<b')");
truth("var a = 1, b = 2; a<b && !(b<a)");
truth("var o = {x: 1}; var t = o.x ? 1 : 2; t === 1");
truth("var f = new Function('return (<a>1</a>).toString()'); f() === '1'");
truth("eval('(<a><b/></a>).b.length()') === 1");
let mut plain = Realm::new();
assert_eq!(plain.run("try { eval('<x/>') } catch (e) { e.name }"), Ok(JsValue::String("SyntaxError".into())));
assert_eq!(plain.run("try { new Function('return <x/>') } 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 = <person><name>John</name><age>25</age></person>; person.name.toString()"), "John");
assert_eq!(run("<><name>Phil</name><age>35</age><hobby>skiing</hobby></>.length()"), JsValue::Number(3.0));
let item = "var item = <item><description>Laptop Computer</description><price>2799.95</price><quantity>1</quantity></item>;";
assert_eq!(
string(&format!("{item} 'Description stored as ' + item.description.toXMLString()")),
"Description stored as <description>Laptop Computer</description>"
);
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('<a p=\"1\" q=\"&lt;\">x<!--c--><?pi d?><b/>&amp;y<![CDATA[<z>]]></a>');";
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<z>'"));
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("(<a><b>1</b><c/></a>).toXMLString()"), "<a>\n <b>1</b>\n <c/>\n</a>");
assert_eq!(string("XML.prettyPrinting = false; (<a><b>1</b><c/></a>).toXMLString()"), "<a><b>1</b><c/></a>");
assert_eq!(string("XML.prettyIndent = 4; (<a><b/></a>).toXMLString()"), "<a>\n <b/>\n</a>");
assert_eq!(string("(<a p='x&quot;&#10;'>&lt;&amp;&gt;</a>).toXMLString()"), "<a p=\"x&quot;&#xA;\">&lt;&amp;&gt;</a>");
assert_eq!(string("(<a xmlns:n=\"u\"><n:b n:c=\"d\"/></a>).toXMLString()"), "<a xmlns:n=\"u\">\n <n:b n:c=\"d\"/>\n</a>");
assert_eq!(string("(<><a/><b/></>).toXMLString()"), "<a/>\n<b/>");
}
#[test]
fn embedded_expressions_are_escaped_by_position() {
assert_eq!(string("var v = '<&>'; (<a>{v}</a>).toXMLString()"), "<a>&lt;&amp;&gt;</a>");
assert_eq!(string("var v = 'a\"<'; (<a b={v}/>).toXMLString()"), "<a b=\"a&quot;&lt;\"/>");
assert_eq!(string("var n = 'tag'; (<{n} x=\"1\">t</{n}>).toXMLString()"), "<tag x=\"1\">t</tag>");
assert_eq!(string("var c = <b>1</b>; (<a>{c}</a>).toXMLString()"), "<a>\n <b>1</b>\n</a>");
assert_eq!(string("var a = 'p=\"1\"'; (<x {a}/>).toXMLString()"), "<x p=\"1\"/>");
truth("var i = 0; var x = <a>{i++}{i++}</a>; x == '01' && i === 2");
truth("var o = {t: 1}; (<a>{o.t ? 'y' : 'n'}</a>) == 'y'");
truth("(<a>{'}'}</a>) == '}'");
truth("(<a>{1 < 2}</a>) == 'true'");
}
#[test]
fn a_computed_tag_name_cannot_add_markup() {
for src in [
r#"(<{'a x="1"'}/>)"#,
"(<{'a><c/'}></{'a'}>)",
"(<a><b>t</{'b><c/'}></a>)",
"(<{''}/>)",
"(<{'1a'}/>)",
"(<a></{'a '}>)",
r#"(<x {'a="1" b'}="2"/>)"#,
] {
assert_eq!(thrown_name(src), "SyntaxError", "{src}");
}
}
#[test]
fn a_computed_name_may_be_any_xml_name() {
assert_eq!(string("(<{'a.b-c'}/>).toXMLString()"), "<a.b-c/>");
assert_eq!(string("(<{'tag'}>t</{'tag'}>).toXMLString()"), "<tag>t</tag>");
assert_eq!(string("var n = 'n:b'; (<a xmlns:n=\"u\"><{n}/></a>).toXMLString()"), "<a xmlns:n=\"u\">\n <n:b/>\n</a>");
}
#[test]
fn a_computed_attribute_name_matches_the_standard_example() {
let vars = "var tagname = 'name', attributename = 'id', attributevalue = 5, content = 'Fred';";
let literal = "<{tagname} {attributename}={attributevalue}>{content}</{tagname}>";
assert_eq!(string(&format!("{vars} ({literal}).toXMLString()")), "<name id=\"5\">Fred</name>");
assert_eq!(string(r#"var a = 'id'; (<x {a} = "1" b="2"/>).toXMLString()"#), "<x id=\"1\" b=\"2\"/>");
}
#[test]
fn a_computed_attribute_run_cannot_add_markup() {
for src in [
r#"(<x {'a="1"><y/'}></x>)"#,
r#"(<><x {'a="1"/><y b="2"'}/></>)"#,
"(<x {'b'}/>)",
"(<x {'a=1'}/>)",
r#"(<x {'a="1'}/>)"#,
r#"(<x {'a="1"b="2"'}/>)"#,
r#"(<x {'a="<"'}/>)"#,
] {
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}/>; x.@a == '1' && x.@b == '>' && x.attributes().length() === 2 && x.children().length() === 0"#,
);
assert_eq!(string("(<x {''}/>).toXMLString()"), "<x/>");
assert_eq!(string("(<x {' '} c=\"3\"/>).toXMLString()"), "<x c=\"3\"/>");
}
// ---- 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 = <><a>1</a><a>2</a><a>3</a></>; var r = l.(seen.push(toString()), true); seen.join() === '1,2,3' && r.length() === 3",
);
truth("(<a/>).(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] = <book id='new'/>; xml.book.length() === 4 && xml.book[0].@id == 'new' && xml.book[0].parent() === xml");
assert_eq!(thrown_name("var x = <a><b/></a>; x[0] = 1"), "TypeError");
assert_eq!(thrown_name("var x = <a><b/></a>; delete x[0]"), "TypeError");
truth("var x = <a/>; x.b.c = 'd'; x.b.c == 'd'");
truth("var x = <a/>; x.b += 'v'; x.b == 'v'");
truth("var x = <a><n>1</n></a>; x.n++; x.n == '2'");
truth("var x = <a t='1'/>; x.@t++; x.@t == '2'");
}
#[test]
fn operators_follow_the_standard() {
truth("var l = <a/> + <b/>; typeof l === 'xml' && l.length() === 2");
truth("var l = <><a/></> + <b/>; l.length() === 2");
truth("typeof <a/> === 'xml' && typeof <><a/></> === 'xml' && typeof new Namespace() === 'object'");
truth("<a><b>1</b></a> == <a><b>1</b></a> && !(<a><b>1</b></a> == <a><b>2</b></a>)");
truth("<a p='1' q='2'/> == <a q='2' p='1'/>");
truth("<a>1</a> == '1' && '1' == <a>1</a> && <a>1</a> == 1");
truth("!(<a>01</a> == 1) && <a>true</a> == true && <a>null</a> == null && <a>undefined</a> == undefined && !(<a>x</a> == null)");
truth("<><a/><b/></> == <><a/><b/></> && !(<><a/></> == <><a/><b/></>)");
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 <><a/><b/></>) s += i; s === '01'");
truth("var s = ''; for each (var v in <><a>1</a><b>2</b></>) 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 <a>7</a>) s += v; s === '7'");
truth("var x = <a><length>5</length></a>; x.length == '5' && x.length() === 1");
truth("var x = <a>abc</a>; x.charAt(1) === 'b'");
truth("var l = <><a>abc</a></>; l.toUpperCase() === 'ABC'");
assert_eq!(thrown_name("(<a><b/></a>).noSuchMethod()"), "TypeError");
assert_eq!(thrown_name("(<><a/><b/></>).nodeKind()"), "TypeError");
truth("with (<a><b>1</b></a>) { b == '1' }");
}
#[test]
fn the_default_namespace_is_scoped_to_the_function() {
truth(
"var ns = new Namespace('u'); default xml namespace = ns; var x = <a><b/></a>; x.name().uri === 'u' && x.b.length() === 1 && x.b.name().uri === 'u'",
);
truth("default xml namespace = 'u'; (<a p='1'/>).@p.name().uri === ''");
truth(
"function f() { default xml namespace = 'u'; return (<a/>).name().uri; } \
f() === 'u' && (<a/>).name().uri === ''",
);
truth("function f() { default xml namespace = 'u'; return function () { return (<a/>).name().uri; }; } f()() === 'u'");
truth("setDefaultXMLNamespace(new Namespace('p', 'v')); (<a/>).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 = <a xmlns='u'><b/></a>; x.b.length() === 0");
truth("var x = <a xmlns='u'><b/></a>; 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 = <Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' soap:encodingStyle='e'><Body/></Envelope>; \
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 = <a/>; XML(x) === x && new XML(x) !== x && new XML(x) == x");
truth("var l = <><a/></>; XML(l) === l[0]");
truth("var p = <p><a q='1'><c/></a></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 = <p><a q='1'><c/></a></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(<><a/><b/></>)"), "TypeError");
assert_eq!(thrown_name("XML('<a><b></a>')"), "SyntaxError");
assert_eq!(thrown_name("XML('<a/><b/>')"), "SyntaxError");
truth("XMLList().length() === 0 && XMLList('<a/><b/>').length() === 2");
truth("var l = <><a/></>; 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("(<a.b/>).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('<a><!--c--><?p q?></a>')).children().length() === 2");
truth("XML.ignoreComments = true; XML.ignoreProcessingInstructions = true; (new XML('<a><!--c--><?p q?></a>')).children().length() === 0");
truth("XML.ignoreWhitespace = false; (new XML('<a> <b/> </a>')).children().length() === 3");
truth("(new XML('<a> <b/> </a>')).children().length() === 1");
truth("(new XML('<a> t </a>')).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 = <a><b/></a>; x.appendChild(<c/>) === x && x.*[1].localName() === 'c' && x.c.parent() === x");
truth("var x = <a><b/></a>; x.appendChild('t'); x.text() == 't'");
truth("var x = <a><b/></a>; x.prependChild(<c/>); x.*[0].localName() === 'c'");
truth("var x = <a><b/><d/></a>; x.insertChildAfter(x.b[0], <c/>); x.*[1].localName() === 'c'");
truth("var x = <a><b/><d/></a>; x.insertChildBefore(x.d[0], <c/>); x.*[1].localName() === 'c'");
truth("var x = <a><b/></a>; x.insertChildAfter(null, <c/>); x.*[0].localName() === 'c'");
truth("var x = <a><b/></a>; x.insertChildBefore(null, <c/>); x.*[1].localName() === 'c'");
truth("var x = <a><b/></a>; x.insertChildAfter(<z/>, <c/>) === undefined && x.*.length() === 1");
truth("var x = <a><b/><c/></a>; x.c[0].childIndex() === 1 && x.childIndex() === -1");
truth("var x = <a p='1'/>; x.attribute('p') == '1' && x.attributes().length() === 1");
truth("var x = <a><b/>t</a>; x.child('b').length() === 1 && x.child(1) == 't' && x.child(9).length() === 0");
truth("var x = <a><b/></a>; x.copy() !== x && x.copy() == x && x.copy().parent() === null && x.b.copy().parent() === undefined");
truth("var c = (<a><b/></a>).b[0].copy(); c.parent() === null");
truth("var x = <a><b/></a>; x.contains(<a><b/></a>)");
truth("var x = <a><b/></a>; x.hasOwnProperty('b') && !x.hasOwnProperty('c') && x.hasComplexContent() && !x.hasSimpleContent()");
truth("var x = <a>t</a>; !x.hasComplexContent() && x.hasSimpleContent() && x.length() === 1");
truth(
"var x = <a xmlns:p='u'><b/></a>; x.b[0].inScopeNamespaces().length === 1 && x.namespaceDeclarations()[0].prefix === 'p' && x.b[0].namespaceDeclarations().length === 0",
);
truth("var x = <a xmlns:p='u'/>; x.namespace('p').uri === 'u' && x.namespace('q') === undefined && x.namespace().uri === ''");
truth("var x = <a/>; x.addNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
truth("var x = <a xmlns:p='u'/>; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p') === undefined");
truth("var x = <a xmlns:p='u' p:q='1'/>; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
truth("var x = <a/>; x.name().localName === 'a' && x.localName() === 'a'");
truth("(<a>t</a>).*[0].name() === null && (<a>t</a>).*[0].localName() === null");
truth("var x = <a><b>1</b></a>; x.b[0].appendChild('2'); x.b[0].*.length() === 2 && (x.normalize(), x.b[0].*.length() === 1) && x.b == '12'");
truth("var x = <a><b/></a>; x.b[0].parent() === x && x.parent() === null");
truth("var x = <a><?p q?><?r s?></a>; x.processingInstructions().length() === 2 && x.processingInstructions('r').length() === 1");
truth("var x = <a/>; x.propertyIsEnumerable(0) && !x.propertyIsEnumerable(1)");
truth("var x = <a><b/><c/><b/></a>; x.replace('b', <z/>); x.*.length() === 2 && x.*[0].localName() === 'z' && x.*[1].localName() === 'c'");
truth("var x = <a><b/></a>; x.replace(0, 't'); x.* == 't'");
truth("var x = <a><b/></a>; x.replace('nothing', <z/>) === x && x.b.length() === 1");
truth("var x = <a><b/></a>; x.setChildren(<c/> + <d/>) === x && x.*.length() === 2 && x.c.length() === 1");
truth("var x = <a/>; x.setLocalName('b'); x.localName() === 'b'");
truth("var x = <a/>; x.setName('b'); x.localName() === 'b'");
truth("var x = <a/>; x.setNamespace(new Namespace('p', 'u')); x.name().uri === 'u' && x.toXMLString() === '<p:a xmlns:p=\"u\"/>'");
assert_eq!(thrown_name("(<a/>).setLocalName('1 x')"), "TypeError");
assert_eq!(thrown_name("(<a/>).setName('a b')"), "TypeError");
assert_eq!(string("(<a>1</a>).toString()"), "1");
assert_eq!(string("(<a><b/></a>).toString()"), "<a>\n <b/>\n</a>");
truth("var x = <a/>; x.valueOf() === x");
truth("!Object.prototype.hasOwnProperty.call(XML.prototype, 'xpath') && !Object.prototype.hasOwnProperty.call(XMLList.prototype, 'xpath')");
truth("var x = <a><b/></a>; x.b.attributes !== undefined");
}
#[test]
fn the_xml_list_methods_work_item_by_item() {
let l = "var l = <><a p='1'><x/>s</a><a p='2'><y/><!--c--></a></>;";
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(<a p='1'><x/>s</a>) && !l.contains(<z/>)"));
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 = <r><a/><b/></r>; x.*.parent() === x");
truth("new XMLList().hasSimpleContent() && !new XMLList().hasComplexContent() && new XMLList().parent() === undefined");
truth("var l = <><a>1</a><a>2</a></>; l.normalize() === l && l.toString() === '<a>1</a>\\n<a>2</a>'");
truth("var r = <r><a>1</a></r>; var l = r.a; l[0] = <a>2</a>; r.a == '2' && l[0] == '2'");
truth("var r = <r><a>1</a></r>; r.a[1] = 'n'; r.a.length() === 2 && r.a[1] == 'n' && r.*[1].parent() === r");
truth("var r = <r><a>1</a></r>; var list = r.b; list.c = 'v'; r.b.c == 'v'");
truth("var r = <r><a t='1'/></r>; r.a.@t[0] = '2'; r.a.@t == '2'");
truth("var r = <r><a>1</a><b/></r>; r.a[0] = <><p/><q/></>; r.*.length() === 3 && r.*[0].localName() === 'p'");
}
#[test]
fn inserting_a_node_moves_it_out_of_its_old_parent() {
truth("var b = <b><c/></b>; var a = <a/>; var c = b.c[0]; a.appendChild(c); b.c.length() === 0 && a.c.length() === 1 && c.parent() === a");
truth("var a = <a><b/><c/></a>; a.appendChild(a.b[0]); a.*.length() === 2 && a.*[0].localName() === 'c'");
truth("var b = <b><c/></b>; var a = <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 = <a xmlns='u'><b xmlns=''/></a>; x.toXMLString()"), "<a xmlns=\"u\">\n <b xmlns=\"\"/>\n</a>");
assert_eq!(string("var x = <a xml:lang='en'/>; x.toXMLString()"), "<a xml:lang=\"en\"/>");
truth("var x = <a xmlns:p='u'><p:b/></a>; 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('<r/>'); x.appendChild(<a/>); x.*[0].setName(q); (new XML(x.toXMLString())).*[0].name().uri === 'u'",
);
truth(
"var x = <a/>; 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!("<a {declaration}/>")), "SyntaxError", "{declaration}");
assert_eq!(thrown_name(&format!("new XML(\"<a {declaration}/>\")")), "SyntaxError", "{declaration}");
}
assert_eq!(string(&format!("(<a xmlns:xml='{XML_NS}'/>).toXMLString()")), format!("<a xmlns:xml=\"{XML_NS}\"/>"));
assert_eq!(string("(<a xmlns=''/>).toXMLString()"), "<a xmlns=\"\"/>");
}
#[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 = <a/>; 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()")), "<a/>", "{prefix} {uri}");
}
assert_eq!(
string(&format!("var x = <a/>; x.addNamespace(new Namespace('xml', '{XML_NS}')); x.toXMLString()")),
format!("<a xmlns:xml=\"{XML_NS}\"/>")
);
}
#[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 = <a/>; 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()")), "<a/>", "{setter}");
}
let q = |prefix: &str| format!("new QName(new Namespace('{prefix}', 'u'), 'b')");
assert_eq!(string(&format!("var x = <a/>; x[{}] = '1'; x.toXMLString()", q("xmlns"))), "<a>\n <b xmlns=\"u\">1</b>\n</a>");
assert_eq!(string(&format!("var x = <a/>; x.@[{}] = '1'; x.toXMLString()", q("xml"))), "<a ns0:b=\"1\" xmlns:ns0=\"u\"/>");
}
#[test]
fn an_attribute_cannot_be_renamed_xmlns() {
for setter in ["setName('xmlns')", "setLocalName('xmlns')"] {
let src = format!("var x = <a b='1'/>; 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()")), "<a b=\"1\"/>", "{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 = <a/>; 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()")), "<a/>", "{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 = <a><b t='1'>text</b></a>; 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 = <x>",
"<x",
"<x></y>",
"<x><y></x></y>",
"<x a=></x>",
"<x a=\"1></x>",
"<x a b=\"1\"/>",
"<x>&bad;</x>",
"<x>&#xFFFFFFFFF;</x>",
"<x>{1</x>",
"<x>{</x>",
"<1x/>",
"<x a=\"<\"/>",
"<!-- a -- b -->",
"<x/><y/>,<",
"<![CDATA[x",
"<?1 x?>",
"</x>",
"<>",
"<x></>",
];
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 <x></y>;", &e4x()).unwrap_err();
assert_eq!(e.line, 2);
}
#[test]
fn bad_xml_text_is_a_catchable_syntax_error() {
for s in [
"<a",
"<a></b>",
"<a b/>",
"<a>&nope;</a>",
"<a b='1' b='2'",
"<1/>",
"<p:a/>",
"<a><!-- -- --></a>",
"<!DOCTYPE a><a/>",
"<a>]]></a>",
"<?xml?>",
] {
assert_eq!(thrown_name(&format!("new XML({s:?})")), "SyntaxError", "{s}");
}
assert_eq!(thrown_name("new XML('<a b=\"1\" b=\"2\"/>')"), "TypeError");
}
#[test]
fn unpaired_surrogates_are_rejected_in_xml_text() {
let lone = "String.fromCharCode(0xD800)";
let low = "String.fromCharCode(0xDC00)";
for value in [
format!("'<a>' + {lone} + '</a>'"),
format!("'<a>' + {low} + '</a>'"),
format!("'<a b=\"' + {lone} + '\"/>'"),
format!("'<a><![CDATA[' + {lone} + ']]></a>'"),
format!("'<a><!--' + {lone} + '--></a>'"),
format!("'<a><?p ' + {lone} + '?></a>'"),
format!("'<' + {lone} + '/>'"),
] {
assert_eq!(thrown_name(&format!("new XML({value})")), "SyntaxError", "{value}");
}
assert_eq!(thrown_name(&format!("(<a/>).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('<a b=\"' + p + '\">' + p + '</a>'); 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('<a p=\"1\" q=\"2\"><b r=\"3\"/>t<!--c--></a>'); 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('<a p=\"1\" q=\"2\"><b r=\"3\"/>t<!--c--></a>'); var c = x.copy(); \
var l = <><u v='w'/>{x.b}<y>{'t'}</y></>; \
c.appendChild(<z k='1'/>); c.prependChild('s'); c.insertChildAfter(c.b[0], <m/>); c.insertChildBefore(c.b[0], 'n'); \
c.replace(0, <r>1</r>); c.n.o = 'v'; c.@t = 'w'; c.z[0] = <w/>; c.g.h = 'deep'; \
var f = x.*.(nodeKind() === 'element'); var d = x..*; var j = x + c; var m = x.b + <q/>; \
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(<i/>); \
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 = <o id='1'/>; var inner = <><i/></>; with (outer) with (inner) { @id == '1' }");
truth("var outer = <o id='1'/>; var inner = <><i id='2'/></>; with (outer) with (inner) { @id == '2' }");
}
#[test]
fn a_bracket_key_converts_to_a_string_before_it_names_a_child() {
truth("var x = <a><true>1</true></a>; x[true] == '1' && (true in x) && !(false in x)");
truth("var x = <a><null>1</null></a>; x[null] == '1' && x[undefined].length() === 0 && x[1.5].length() === 0 && x[-1].length() === 0");
truth("var x = <a/>; x[false] = 'v'; x['false'] == 'v' && (delete x[false], x.*.length() === 0)");
assert_eq!(thrown_name("(<a/>).attribute(true)"), "TypeError");
assert_eq!(thrown_name("(<a/>).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<T: Send + 'static>(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!("{}{}", "<a>".repeat(tags), "</a>".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 = <a/>; for (var i = 0; i < 100000; i++) { var y = <a/>; y.appendChild(x); x = y; }";
assert_eq!(kind_of(Limits::default(), chain), RunErrorKind::Limit(LimitKind::Heap));
let within = "var x = <a/>; for (var i = 0; i < 150; i++) { var y = <a/>; 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 = <a><b><c/></b></a>; 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 = <a><b><c/></b></a>; 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('<a {}/>');", (0..50_000).map(|i| format!("a{i}=\"1\"")).collect::<Vec<_>>().join(" "));
assert_eq!(kind_of(Limits { fuel: 10_000_000, ..TIGHT }, &attrs), RunErrorKind::Limit(LimitKind::Heap));
let wide = "var l = <><a/></>; 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('<a>' + s + '</a>')";
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; (<a>{s}</a>).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 = <r/>; for (var i = 0; i < 2000; i++) x.appendChild(<a/>);";
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 = <a><b/></a>; var last = x.b[0];";
let wide = "var x = <a/>; for (var i = 0; i < 2000; i++) x.appendChild(<b/>); var last = x.b[1999];";
for action in
["x.insertChildAfter(<z/>, <c/>)", "x.insertChildBefore(<z/>, <c/>)", "x.insertChildAfter(last, <c/>)", "x.insertChildBefore(last, <c/>)"]
{
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 (;;) (<a/>).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<crate::value::Value> {
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 = <o><b/><c/></o>; var t = <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 = <o><b/><c/></o>; var t = <t><d/></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 = <x/>; var v = <v/>;", action, check) > 0);
}
#[test]
fn limits_during_deletes_and_puts_keep_the_other_nodes() {
let setup = "var x = <a p='1' q='2'><b>old</b><c/></a>;";
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 = <a><b/><z/></a>; var v = <><p/><q/></>;";
let replaced = tree_is("x", "<a><b/><z/></a>", "<a><p/><q/><z/></a>");
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", "<a><b/><z/></a>", "<a><p/><q/></a>");
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 = <o><p/><q/></o>; var x = <a><b/></a>; var v = o.*;";
let check = |after: &str| {
format!("{} && (o.*.length() === 0 ? x.*.length() === 3 : o.*.length() === 2 && x.*.length() === 1)", tree_is("x", "<a><b/></a>", after))
};
for (action, after) in [
("x.appendChild(v)", "<a><b/><p/><q/></a>"),
("x.prependChild(v)", "<a><p/><q/><b/></a>"),
("x.insertChildAfter(null, v)", "<a><p/><q/><b/></a>"),
("x.insertChildBefore(null, v)", "<a><b/><p/><q/></a>"),
] {
assert!(fuel_stops(setup, action, &check(after)) > 0, "{action}");
}
assert!(heap_stops(setup, "x.appendChild((SPARE, v))", &check("<a><b/><p/><q/></a>")) > 0);
}
#[test]
fn a_heap_limit_on_a_list_put_at_an_index_keeps_the_list_and_tree_in_step() {
let setup = "var x = <a><b/><z/></a>; var l = x.*; var v = <><p/><q/></>;";
let check = "var s = x.toXMLString().replace(/\\s/g, ''); \
(s === '<a><b/><z/></a>' && l.length() === 2 && l[0].name() == 'b') \
|| (s === '<a><p/><q/><z/></a>' && 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 = <a><b/><c/><d/><e/></a>; 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 += '<d>'; e += '</d>'; } 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 = <a><b/><c/><b/></a>;").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 = <a><b/><c/><b/></a>; var n = <n/>; var l = <><p/><q/></>;";
let two = "<a><b/><c/><b/></a>";
for (action, after) in [
("x.replace('b', n)", "<a><n/><c/></a>"),
("x.replace('b', l)", "<a><p/><q/><c/></a>"),
("x.b = n", "<a><n/><c/></a>"),
("x.b = l", "<a><p/><q/><c/></a>"),
("x.b = 'v'", "<a><b>v</b><c/></a>"),
] {
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, "<a><n/><c/></a>")) > 0);
assert!(heap_stops(setup, "x.b = (SPARE, 'v')", &tree_is("x", two, "<a><b>v</b><c/></a>")) > 0);
}
#[test]
fn matches_are_replaced_in_document_order() {
truth("var x = <a><b>1</b><c/><b>2</b><d/><b>3</b></a>; x.replace('b', <n/>); x.*.length() === 3 && x.*[0].name() == 'n' && x.d.length() === 1");
truth("var x = <a><b>1</b><c/><b>2</b></a>; var old = x.b[1]; x.b = 'v'; x.b.length() === 1 && x.b == 'v' && old.parent() === null");
truth("var x = <a><c/></a>; 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 = <y p='v'/>; var x = <x><b/></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 = <y p='v' q='w'/>; var x = <x><b/></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 = "<a>".repeat(7);
let close = "</a>".repeat(7);
let src = format!(
"var x = new XML('{open}<a/>{close}'); var c = x; while (c.a.length()) c = c.a[0]; \
var y = <y p='v'/>; 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 = <a/>; 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 = <r><a/><b/></r>; var v = <c/> + <d/>; var w = undefined;";
let set = tree_is("x", "<r><a/><b/></r>", "<r><c/><d/></r>");
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 = <r/>; var v = <v/>;";
for (target, value, after) in [
("x.a.b.c", "v", "<r><a><b><v/></b></a></r>"),
("x.a.b", "'text'", "<r><a><b>text</b></a></r>"),
("x.a.@p", "'text'", "<r><ap=\"text\"/></r>"),
("x.a.b[0]", "'text'", "<r><a><b>text</b></a></r>"),
] {
let check = tree_is("x", "<r/>", 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 = <a p='1' q='2'><b r='3'/>t</a>; 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<JsValue, RunErrorKind> {
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 = <r/>; 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 = <r/>; 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 = <r/>; for (var i = 0; i < 500; i++) x.appendChild(<a/>);").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] = &[
"<",
">",
"/",
"</",
"/>",
"<a>",
"</a>",
"<b c='d'>",
"</b>",
"<>",
"</>",
"{",
"}",
"{1}",
"'",
"\"",
"&",
"&amp;",
"&#x41;",
"&bad;",
"<!--",
"-->",
"<![CDATA[",
"]]>",
"<?p",
"?>",
"@",
"::",
"..",
".(",
")",
"*",
"/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<usize> = Vec::new();
out.push_str("<r>");
for (i, &b) in shape.iter().enumerate() {
match b % 6 {
0 if open.len() < 20 => {
out.push_str(&format!("<e{i} a='{b}'>"));
open.push(i);
}
1 => {
if let Some(e) = open.pop() {
out.push_str(&format!("</e{e}>"));
}
}
2 => out.push_str(&format!("<s{i}/>")),
3 => out.push_str(&format!("t{b}&amp;")),
4 => out.push_str(&format!("<!--c{b}-->")),
_ => out.push_str(&format!("<?p{b} v?>")),
}
}
while let Some(e) = open.pop() {
out.push_str(&format!("</e{e}>"));
}
out.push_str("</r>");
out
}
const TAG_PIECES: [&str; 11] = ["a", "b", " ", "=", "\"", "'", "<", "/", "&lt;", "a=\"1\"", "><y/"];
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn computed_tag_parts_give_no_child_or_throw(parts in proptest::collection::vec(0..TAG_PIECES.len(), 0..5)) {
let text: String = parts.iter().map(|&i| TAG_PIECES[i]).collect();
let quoted = format!("{text:?}");
for src in [
format!("var v = <{{{quoted}}}/>; v.children().length() === 0 && v.attributes().length() === 0"),
format!("var v = <x {{{quoted}}}></x>; v.children().length() === 0 && v.name().localName === 'x'"),
format!("(<><x {{{quoted}}}/></>).length() === 1"),
format!("var v = <a><b></{{{quoted}}}></a>; 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 = <a><y/></a>; {}", parts.iter().map(|&i| PIECES[i]).collect::<String>());
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::<u8>(), 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('<n' + i + '/>')); 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);
}
}