mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 12:24:54 +00:00
Merge pull request #6788 from dotCooCoo/feat/e4x-xml-xpath
JS: XML and XMLList xpath evaluates XPath 1.0 expressions
This commit is contained in:
@@ -41,9 +41,11 @@
|
||||
//! With [`Options::e4x`], the parser also reads E4X (ECMA-357, second edition, December 2005):
|
||||
//! XML and XMLList literals, the `.@`, `..`, `::` and `.( )` operators, `for each … in` and
|
||||
//! `default xml namespace`; the realm then provides `XML`, `XMLList`, `Namespace`, `QName`,
|
||||
//! `isXMLName` and `setDefaultXMLNamespace`. Without the option, the language and the built-ins are
|
||||
//! `isXMLName` and `setDefaultXMLNamespace`, and XML and XMLList values have an `xpath` method that
|
||||
//! evaluates an XPath 1.0 expression. Without the option, the language and the built-ins are
|
||||
//! those of ECMAScript 5.1 above. XML trees live in the heap, and parsing, copying, comparing and
|
||||
//! serializing them use no native recursion and take fuel for each node.
|
||||
//! serializing them, and evaluating XPath on them, use no native recursion and take fuel for each
|
||||
//! node.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod ast;
|
||||
@@ -70,6 +72,7 @@ mod token;
|
||||
mod value;
|
||||
mod xml;
|
||||
mod xml_builtins;
|
||||
mod xpath;
|
||||
|
||||
pub use ast::{
|
||||
AssignOp, Ast, BinaryOp, CatchClause, Expr, ExprId, ForInTarget, ForInit, Function, FunctionId, FunctionKind, LogicalOp, Name, Program, Property,
|
||||
|
||||
@@ -98,7 +98,7 @@ fn check_string_room(interp: &mut Interp, len: usize) -> JsResult<()> {
|
||||
|
||||
/// Pushes `item` onto `list`, first holding the bytes of the list's new capacity when it must
|
||||
/// grow.
|
||||
fn push_held<T>(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec<T>, item: T) -> JsResult<()> {
|
||||
pub(crate) fn push_held<T>(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec<T>, item: T) -> JsResult<()> {
|
||||
if list.len() == list.capacity() {
|
||||
let more = list.capacity().max(4);
|
||||
held.add(interp, more.saturating_mul(size_of::<T>()))?;
|
||||
@@ -180,7 +180,7 @@ fn clamp(n: f64, len: usize) -> usize {
|
||||
|
||||
/// The first index at `from` or after where `needle` occurs in `hay`. Every position tried takes
|
||||
/// fuel.
|
||||
fn find(interp: &mut Interp, hay: &[u16], needle: &[u16], from: usize) -> JsResult<Option<usize>> {
|
||||
pub(crate) fn find(interp: &mut Interp, hay: &[u16], needle: &[u16], from: usize) -> JsResult<Option<usize>> {
|
||||
if needle.len() > hay.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::value::{Str, Value};
|
||||
use crate::xml::{
|
||||
Access, Content, NameRef, RESERVED_NS_BINDING, STAR, Xv, canonical_index, is_ncname, is_script_xml_name, ns_binding_allowed, xml_name_writable,
|
||||
};
|
||||
use crate::xpath::xpath;
|
||||
|
||||
const SETTINGS: [(&str, Value); 5] = [
|
||||
("ignoreComments", Value::Bool(false)),
|
||||
@@ -45,7 +46,7 @@ pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
method(interp, xml, "settings", settings, 0)?;
|
||||
method(interp, xml, "setSettings", set_settings, 1)?;
|
||||
method(interp, xml, "defaultSettings", default_settings, 0)?;
|
||||
let xml_methods: [(&'static str, NativeFn, u32); 39] = [
|
||||
let xml_methods: [(&'static str, NativeFn, u32); 40] = [
|
||||
("addNamespace", add_namespace, 1),
|
||||
("appendChild", append_child, 1),
|
||||
("attribute", attribute, 1),
|
||||
@@ -85,6 +86,7 @@ pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
("toString", to_string, 0),
|
||||
("toXMLString", to_xml_string, 0),
|
||||
("valueOf", value_of, 0),
|
||||
("xpath", xpath, 1),
|
||||
];
|
||||
for (name, f, n) in xml_methods {
|
||||
method(interp, xml_proto, name, f, n)?;
|
||||
@@ -92,7 +94,7 @@ pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
|
||||
let list = native(interp, "XMLList", xml_list_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "XMLList", list, list_proto)?;
|
||||
let list_methods: [(&'static str, NativeFn, u32); 21] = [
|
||||
let list_methods: [(&'static str, NativeFn, u32); 22] = [
|
||||
("attribute", attribute, 1),
|
||||
("attributes", attributes, 0),
|
||||
("child", child, 1),
|
||||
@@ -114,6 +116,7 @@ pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
("toString", to_string, 0),
|
||||
("toXMLString", to_xml_string, 0),
|
||||
("valueOf", value_of, 0),
|
||||
("xpath", xpath, 1),
|
||||
];
|
||||
for (name, f, n) in list_methods {
|
||||
method(interp, list_proto, name, f, n)?;
|
||||
@@ -665,7 +668,7 @@ fn has_simple_content(interp: &mut Interp, this: Value, _: &[Value], _: bool) ->
|
||||
|
||||
/// The namespaces in scope at `x`, nearest declarations first, one for each prefix. Takes one unit
|
||||
/// of fuel for each node it climbs through and for each declaration it reads.
|
||||
fn scope_of(interp: &mut Interp, x: Option<ObjId>) -> JsResult<Vec<XmlNs>> {
|
||||
pub(crate) fn scope_of(interp: &mut Interp, x: Option<ObjId>) -> JsResult<Vec<XmlNs>> {
|
||||
let mut out: Vec<XmlNs> = Vec::new();
|
||||
let mut seen: HashSet<Option<Key>> = HashSet::new();
|
||||
let mut cur = x;
|
||||
|
||||
+466
-1
@@ -372,7 +372,7 @@ fn the_xml_methods_behave_as_specified() {
|
||||
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("typeof XML.prototype.xpath === 'function' && typeof XMLList.prototype.xpath === 'function' && XML.prototype.xpath.length === 1");
|
||||
truth("var x = <a><b/></a>; x.b.attributes !== undefined");
|
||||
}
|
||||
|
||||
@@ -1279,6 +1279,471 @@ fn an_interrupt_stops_serialization() {
|
||||
assert_eq!(r.run("stop(); x.toXMLString()").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Interrupted)));
|
||||
}
|
||||
|
||||
// ---- xpath ----
|
||||
|
||||
/// A script function that joins the local names of a list's items.
|
||||
const NAMES: &str = "function names(l) { var s = ''; for each (var n in l) s += n.localName(); return s; }";
|
||||
|
||||
#[test]
|
||||
fn xpath_selects_child_attribute_and_text_nodes() {
|
||||
for check in [
|
||||
"xml.xpath('book').length() === 4",
|
||||
"xml.xpath('book[2]/title') == 'Oz'",
|
||||
"xml.xpath('book[last()]/title') == 'Opera'",
|
||||
"xml.xpath('book[position() < 3]').length() === 2",
|
||||
"xml.xpath('book[year > 1890]/title').length() === 3",
|
||||
"xml.xpath('book[count(author) > 1]/title') == 'Alice'",
|
||||
"xml.xpath('book[@category = \"CHILDREN\"]/title').length() === 2",
|
||||
"xml.xpath('//author').length() === 6",
|
||||
"xml.xpath('book/@category').length() === 4 && xml.xpath('book/@category')[0].nodeKind() === 'attribute'",
|
||||
"xml.xpath('book[1]/title/text()')[0].nodeKind() === 'text' && xml.xpath('book[1]/title/text()') == 'Cookbook'",
|
||||
"xml.xpath('book[1]/title/@lang') == 'en'",
|
||||
"xml.xpath('nothing').length() === 0",
|
||||
"xml.xpath('child::book[3]/child::author[2]') == 'B'",
|
||||
"xml.xpath('*').length() === 4 && xml.xpath('book/node()').length() === 14",
|
||||
] {
|
||||
truth(&format!("{BOOKS} {check}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_returns_the_nodes_of_the_tree() {
|
||||
truth(&format!("{BOOKS} xml.book[1].title[0].xpath('..')[0] === xml.book[1]"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book[2]')[0] === xml.book[1] && xml.xpath('.')[0] === xml"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book[1]/year')[0].setChildren('2000'); xml.book[0].year == '2000'"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book/@category')[1].setName('kind'); xml.book[1].@kind == 'CHILDREN'"));
|
||||
truth("var x = <r><a>t</a></r>; x.a.text()[0].xpath('..')[0] === x.a[0]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_absolute_paths_start_above_the_topmost_element() {
|
||||
truth(&format!("{BOOKS} xml.book[0].title[0].xpath('/bookstore/book').length() === 4"));
|
||||
truth(&format!("{BOOKS} xml.book[0].title[0].xpath('/*')[0] === xml"));
|
||||
truth(&format!("{BOOKS} xml.book[0].title[0].xpath('//title').length() === 4"));
|
||||
truth(&format!("{BOOKS} xml.book[0].title[0].xpath('/title').length() === 0"));
|
||||
truth("var d = <d><e/></d>; d.e[0].xpath('/d')[0] === d");
|
||||
for src in ["(<r/>).xpath('/')", "(<r/>).xpath('..')", "(<r/>).xpath('ancestor-or-self::node()')"] {
|
||||
assert_eq!(thrown_name(src), "TypeError", "{src}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_walks_every_axis() {
|
||||
let tree = "var x = <r><a><b/><c/></a><d><e/></d><f/></r>;";
|
||||
for (expr, want) in [
|
||||
("x.a[0].xpath('following-sibling::*')", "df"),
|
||||
("x.f[0].xpath('preceding-sibling::*')", "ad"),
|
||||
("x.f[0].xpath('preceding-sibling::*[1]')", "d"),
|
||||
("x.d.e[0].xpath('ancestor::*')", "rd"),
|
||||
("x.d.e[0].xpath('ancestor::*[1]')", "d"),
|
||||
("x.d.e[0].xpath('ancestor-or-self::*')", "rde"),
|
||||
("x.a.b[0].xpath('following::*')", "cdef"),
|
||||
("x.d.e[0].xpath('preceding::*')", "abc"),
|
||||
("x.d.e[0].xpath('preceding::*[1]')", "c"),
|
||||
("x.xpath('descendant::*')", "abcdef"),
|
||||
("x.xpath('descendant-or-self::*')", "rabcdef"),
|
||||
("x.a[0].xpath('self::a')", "a"),
|
||||
("x.a[0].xpath('self::b')", ""),
|
||||
("x.a.b[0].xpath('parent::*')", "a"),
|
||||
("x.xpath('//*[2]')", "cd"),
|
||||
("x.xpath('(//*)[2]')", "a"),
|
||||
("x.xpath('d/e/../preceding-sibling::*/c')", "c"),
|
||||
] {
|
||||
assert_eq!(string(&format!("{NAMES} {tree} names({expr})")), want, "{expr}");
|
||||
}
|
||||
let attr = "var x = <r><a p='1'><b/></a><c/></r>;";
|
||||
assert_eq!(string(&format!("{NAMES} {attr} names(x.a.@p[0].xpath('following::*'))")), "bc");
|
||||
assert_eq!(string(&format!("{NAMES} {attr} names(x.a.@p[0].xpath('ancestor::*'))")), "ra");
|
||||
truth(&format!("{attr} x.a.@p[0].xpath('following-sibling::node()').length() === 0 && x.a.@p[0].xpath('preceding::*').length() === 0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_results_are_in_document_order_without_repeats() {
|
||||
let tree = "var x = <r p='1'><a><b/></a><b/><c/></r>;";
|
||||
for (expr, want) in [
|
||||
("x.xpath('c | a | //b')", "abbc"),
|
||||
("x.xpath('a | a')", "a"),
|
||||
("x.xpath('//b/..')", "ra"),
|
||||
("x.xpath('//*/ancestor::*')", "ra"),
|
||||
("x.c[0].xpath('preceding::*[position() < 3]')", "bb"),
|
||||
] {
|
||||
assert_eq!(string(&format!("{NAMES} {tree} names({expr})")), want, "{expr}");
|
||||
}
|
||||
truth(&format!("{tree} x.xpath('c | @p')[0].nodeKind() === 'attribute' && x.xpath('c | @p').length() === 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_names_match_by_namespace_uri() {
|
||||
let tree = "var x = <r xmlns:p='urn:p'><p:a/><a/><q:a xmlns:q='urn:p'/><s:a xmlns:s='urn:s'/></r>;";
|
||||
for check in [
|
||||
"x.xpath('p:a').length() === 2",
|
||||
"x.xpath('a').length() === 1 && x.xpath('a')[0].name().uri === ''",
|
||||
"x.xpath('p:*').length() === 2",
|
||||
"x.xpath('*').length() === 4",
|
||||
"x.xpath('*[local-name() = \"a\"]').length() === 4",
|
||||
"x.xpath('*[namespace-uri() = \"urn:s\"]').length() === 1",
|
||||
"x.xpath('*[name() = \"p:a\"]').length() === 1",
|
||||
] {
|
||||
truth(&format!("{tree} {check}"));
|
||||
}
|
||||
truth("var x = <r xmlns='urn:d'><a/></r>; x.xpath('a').length() === 0 && x.xpath('*').length() === 1");
|
||||
truth("var x = <r><a/></r>; default xml namespace = 'urn:d'; x.xpath('a').length() === 1");
|
||||
truth("var x = <r><a xml:lang='en'/></r>; x.xpath('a/@xml:lang') == 'en'");
|
||||
let rebound = "var x = <r xmlns:p='urn:1'><p:a/><b xmlns:p='urn:2'><p:a/></b></r>;";
|
||||
truth(&format!("{rebound} x.xpath('//p:a').length() === 1 && x.xpath('//p:a')[0].parent() === x"));
|
||||
truth(&format!("{rebound} x.b[0].xpath('p:a').length() === 1 && x.b[0].xpath('/r/p:a').length() === 0"));
|
||||
assert_eq!(thrown_name("(<r/>).xpath('z:a')"), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_namespace_nodes_can_be_tested_but_not_returned() {
|
||||
let tree = "var x = <r xmlns:p='urn:p'><a xmlns:q='urn:q'/></r>;";
|
||||
truth(&format!("{tree} x.xpath('self::*[namespace::xml]').length() === 1"));
|
||||
truth(&format!("{tree} x.xpath('a[count(namespace::*) = 3]').length() === 1"));
|
||||
truth(&format!("{tree} x.xpath('a[namespace::q = \"urn:q\"]').length() === 1 && x.xpath('self::*[namespace::q]').length() === 0"));
|
||||
truth(&format!("{tree} x.xpath('a[namespace::*[name() = \"p\"]/.. = .]').length() === 1"));
|
||||
assert_eq!(thrown_name(&format!("{tree} x.xpath('namespace::*')")), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_variables_come_from_the_object_properties() {
|
||||
truth(&format!("{BOOKS} xml.xpath('book[title = $t]/author', {{ t: 'Oz' }}) == 'Baum'"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book[$n]/title', {{ n: 3 }}) == 'Alice'"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book[$yes]', {{ yes: true }}).length() === 4 && xml.xpath('book[$no]', {{ no: false }}).length() === 0"));
|
||||
truth(&format!("{BOOKS} xml.xpath('$b/title', {{ b: xml.book[1] }}) == 'Oz'"));
|
||||
truth(&format!("{BOOKS} xml.xpath('$l/year', {{ l: xml.book.(@category == 'CHILDREN') }}).length() === 2"));
|
||||
truth(&format!("{BOOKS} xml.xpath('book[year = $y]/title', {{ y: {{ toString: function () {{ return '1922'; }} }} }}) == 'Opera'"));
|
||||
truth(&format!(
|
||||
"{BOOKS} var o = {{}}; Object.defineProperty(o, 'v', {{ get: function () {{ return 'Alice'; }} }}); xml.xpath('book[title = $v]', o).length() === 1"
|
||||
));
|
||||
for src in
|
||||
["(<r/>).xpath('$v')", "(<r/>).xpath('$v', {})", "(<r/>).xpath('$v', { v: undefined })", "(<r/>).xpath('a', 5)", "(<r/>).xpath('a', null)"]
|
||||
{
|
||||
assert_eq!(thrown_name(src), "TypeError", "{src}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_keeps_the_items_of_a_list_variable_that_a_later_getter_empties() {
|
||||
let setup = "var l = <><a><b/></a></>;";
|
||||
let action = "var o = { get v() { return l; }, get w() { delete l[0]; l = null; squeeze(100000000); return 1; } };
|
||||
var r = (<r/>).xpath('$v/b | $v[$w]', o); r.length() === 2 && r[0].localName() === 'a' && r[1].localName() === 'b'";
|
||||
assert_eq!(after_setup(setup, action), Ok(JsValue::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_functions_and_operators_follow_xpath_1() {
|
||||
let tree = "var x = <r xml:lang='en-US'><a>1</a><a>2</a><b>x y</b><n>12.5</n></r>;";
|
||||
let checks = [
|
||||
"last() = 1 and position() = 1",
|
||||
"count(a) = 2",
|
||||
"count(id('a')) = 0",
|
||||
"local-name() = 'r' and name(a) = 'a' and namespace-uri() = ''",
|
||||
"local-name(nothing) = '' and name(@xml:lang) = 'xml:lang'",
|
||||
"string(a) = '1' and string() = '12x y12.5' and string(nothing) = ''",
|
||||
"concat('a', 'b', 'c') = 'abc'",
|
||||
"starts-with('abc', 'ab') and not(starts-with('abc', 'b'))",
|
||||
"contains('abc', 'bc') and contains('abc', '') and not(contains('abc', 'd'))",
|
||||
"substring-before('1999/04/01', '/') = '1999' and substring-before('abc', 'd') = ''",
|
||||
"substring-after('1999/04/01', '/') = '04/01' and substring-after('abc', '') = 'abc'",
|
||||
"substring('12345', 2, 3) = '234' and substring('12345', 2) = '2345'",
|
||||
"substring('12345', 1.5, 2.6) = '234' and substring('12345', 0, 3) = '12'",
|
||||
"substring('12345', 0 div 0, 3) = '' and substring('12345', 1, 0 div 0) = ''",
|
||||
"substring('12345', -42, 1 div 0) = '12345' and substring('12345', -1 div 0, 1 div 0) = ''",
|
||||
"string-length('abc') = 3 and string-length(b) = 4 and string-length() = 10",
|
||||
"normalize-space(b) = 'x y' and normalize-space(' x \t y ') = 'x y' and normalize-space(' ') = ''",
|
||||
"translate('bar', 'abc', 'ABC') = 'BAr' and translate('--aaa--', 'abc-', 'ABC') = 'AAA'",
|
||||
"boolean(a) and not(boolean(nothing)) and boolean('x') and not(boolean('')) and not(boolean(0)) and not(boolean(0 div 0))",
|
||||
"true() and not(false())",
|
||||
"lang('en') and lang('EN-us') and not(lang('fr')) and a[lang('en')]",
|
||||
"number(n) = 12.5 and number(' -3 ') = -3 and string(number('1e3')) = 'NaN' and string(number('+1')) = 'NaN'",
|
||||
"sum(a) = 3 and sum(nothing) = 0",
|
||||
"floor(2.5) = 2 and ceiling(2.1) = 3 and floor(-2.5) = -3",
|
||||
"round(2.5) = 3 and round(-2.5) = -2 and round(2.4) = 2 and string(round(-0.4)) = '0'",
|
||||
"string(1 div 0) = 'Infinity' and string(-1 div 0) = '-Infinity' and string(0 div 0) = 'NaN'",
|
||||
"string(1.5) = '1.5' and string(2) = '2' and string(-0.25) = '-0.25' and string(1 div 3) = '0.3333333333333333'",
|
||||
"string(1000000 * 1000000 * 1000000 * 1000) = '1000000000000000000000'",
|
||||
"7 mod 3 = 1 and -7 mod 3 = -1 and 5 div 2 = 2.5 and 2 - 3 = -1 and --1 = 1 and -(1 + 1) = -2",
|
||||
"1 + 2 * 3 = 7 and (1 + 2) * 3 = 9 and 3 > 2 > 1 = false()",
|
||||
"a = 2 and a = '1' and a != 1 and a > 1 and not(a > 2) and a < 2 and 1 < a and 2 >= a",
|
||||
"a = a and a != a and not(a[1] != a[1]) and not(nothing = nothing) and not(nothing != a) and a < a",
|
||||
"true() = a and false() = nothing and '1' = 1 and true() = 'x' and 1 = true() and not(0 = true())",
|
||||
"a > false() and not(a > true()) and nothing < true() and not(nothing >= true())",
|
||||
"1 < 2 and 2 > 1 or false()",
|
||||
"false() and 1 div 0 or true()",
|
||||
"'a' < 'b' = false() and '1' < '2'",
|
||||
"*[2] = 2 and *[last()] = 12.5",
|
||||
"(a | b)[2] = 2",
|
||||
];
|
||||
for check in checks {
|
||||
let expr = format!("self::*[{check}]");
|
||||
truth(&format!("{tree} x.xpath({expr:?}).length() === 1"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_throws_a_type_error_for_a_value_that_is_not_a_node_set() {
|
||||
for expr in ["count(book)", "string(book)", "1", "'a'", "true()", "book = 1", "-book"] {
|
||||
let src = format!("{BOOKS} try {{ xml.xpath({expr:?}); 'no error' }} catch (e) {{ e.name + ': ' + e.message }}");
|
||||
assert_eq!(string(&src), "TypeError: the XPath expression does not evaluate to a node-set", "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_errors_name_their_cause() {
|
||||
for expr in [
|
||||
"",
|
||||
" ",
|
||||
"/[1]",
|
||||
"book[",
|
||||
"book]",
|
||||
"(",
|
||||
")",
|
||||
"1 +",
|
||||
"book/",
|
||||
"@",
|
||||
"foo::bar",
|
||||
".[1]",
|
||||
"..[1]",
|
||||
"'abc",
|
||||
"$",
|
||||
"book book",
|
||||
"!",
|
||||
"a//",
|
||||
",",
|
||||
"count(",
|
||||
"count(1,",
|
||||
"a::",
|
||||
"::a",
|
||||
"a:",
|
||||
"1..2",
|
||||
"book[1]]",
|
||||
"count(book))",
|
||||
"//",
|
||||
"/ /a",
|
||||
"(1)(2)",
|
||||
"processing-instruction(1)",
|
||||
"node(1)",
|
||||
"*:a",
|
||||
"a:b:c",
|
||||
] {
|
||||
assert_eq!(thrown_name(&format!("{BOOKS} xml.xpath({expr:?})")), "SyntaxError", "{expr:?}");
|
||||
}
|
||||
for expr in
|
||||
["foo()", "count()", "count(1)", "p:count(book)", "sum('1')", "1 | book", "'a'/b", "'a'[1]", "concat('a')", "name(1)", "substring('a')"]
|
||||
{
|
||||
assert_eq!(thrown_name(&format!("{BOOKS} xml.xpath({expr:?})")), "TypeError", "{expr:?}");
|
||||
}
|
||||
for src in ["(<r/>).xpath()", "(<r/>).xpath(5)", "(<r/>).xpath(new String('a'))", "XML.prototype.xpath.call({}, 'a')"] {
|
||||
assert_eq!(thrown_name(src), "TypeError", "{src}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_on_an_xml_list_runs_on_each_element_in_turn() {
|
||||
truth(
|
||||
"var l = <><a><b>1</b></a><a><b>2</b><b>3</b></a></>; l.xpath('b').length() === 3 && l.xpath('b').toXMLString() === '<b>1</b>\\n<b>2</b>\\n<b>3</b>'",
|
||||
);
|
||||
truth("var l = <><a><b>1</b></a><a><b>2</b><b>3</b></a></>; l.xpath('b[1]').length() === 2");
|
||||
truth("var x = <r><a/><b/></r>; var l = x.*; var p = l.xpath('..'); p.length() === 2 && p[0] === x && p[1] === x");
|
||||
truth("var x = <r><a>t</a></r>; var l = x.a.text() + x.a; l.xpath('.').length() === 1 && l.xpath('.')[0] === x.a[0]");
|
||||
truth("var x = <r p='1'><!--c--><a/></r>; (x.@p + x.comments()).xpath('.').length() === 0");
|
||||
truth("new XMLList().xpath('a').length() === 0 && new XMLList().xpath('z:a').length() === 0");
|
||||
truth("var l = <><a xmlns:p='urn:1'><p:b/></a><a xmlns:p='urn:2'><p:b/></a></>; l.xpath('p:b').length() === 2");
|
||||
assert_eq!(thrown_name("var l = <><a xmlns:p='urn:1'/><a/></>; l.xpath('p:b')"), "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_expressions_of_any_depth_use_no_native_stack() {
|
||||
let deep = format!("{}1{}", "(".repeat(100_000), ")".repeat(100_000));
|
||||
assert_eq!(thrown_name(&format!("(<r/>).xpath('{deep}')")), "SyntaxError");
|
||||
let predicates = format!("{}{}", "a[".repeat(100_000), "]".repeat(100_000));
|
||||
assert_eq!(thrown_name(&format!("(<r/>).xpath('{predicates}')")), "SyntaxError");
|
||||
let sum = format!("1{}", " + 1".repeat(100_000));
|
||||
truth(&format!("(<r/>).xpath('self::*[{sum} = 100001]').length() === 1"));
|
||||
let negated = format!("{}1", "-".repeat(100_001));
|
||||
truth(&format!("(<r/>).xpath('self::*[{negated} = -1]').length() === 1"));
|
||||
let union = format!("a{}", " | a".repeat(50_000));
|
||||
truth(&format!("(<r><a/></r>).xpath('{union}').length() === 1"));
|
||||
let mut shallow = Realm::with_options(Options { max_depth: 8, ..e4x() }, Limits::default());
|
||||
let seven = format!("{}1{}", "(".repeat(6), ")".repeat(6));
|
||||
assert_eq!(shallow.run(&format!("(<r/>).xpath('self::*[{seven}]').length()")), Ok(JsValue::Number(1.0)));
|
||||
let eight = format!("{}1{}", "(".repeat(8), ")".repeat(8));
|
||||
assert_eq!(shallow.run(&format!("try {{ (<r/>).xpath('{eight}') }} catch (e) {{ e.message }}")), Ok(JsValue::String("nesting too deep".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_stops_at_the_fuel_and_heap_limits_leaving_the_tree() {
|
||||
let setup = "var x = <r/>; for (var i = 0; i < 6; i++) x.appendChild(<a><b>{i}</b></a>);";
|
||||
let check = "x.a.length() === 6 && x.a[5].b == '5' && x.*.length() === 6";
|
||||
assert!(fuel_stops(setup, "x.xpath('//b[. > 2]/preceding::*')", check) > 0);
|
||||
assert!(heap_stops(setup, "x.xpath((SPARE, '//b[contains(concat(., ., .), \"3\")] | //a'))", check) > 0);
|
||||
let mut r = realm();
|
||||
r.run("var x = <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.xpath('//a/following::a')").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Interrupted)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_walks_of_deep_trees_and_long_lists_count_against_the_heap_limit() {
|
||||
let deep = "var s = ''; for (var i = 0; i < 5000; i++) s += '<a>'; s += '<b/>'; for (var i = 0; i < 5000; i++) s += '</a>'; var x = new XML(s);";
|
||||
for (action, room) in [
|
||||
("x.xpath('descendant::c').length()", JsValue::Number(0.0)),
|
||||
("x.xpath('self::*[string() = \"q\"]').length()", JsValue::Number(0.0)),
|
||||
("x.xpath('descendant::b | self::a').length()", JsValue::Number(2.0)),
|
||||
] {
|
||||
for (spare, want) in [(20_000, Err(RunErrorKind::Limit(LimitKind::Heap))), (50_000_000, Ok(room.clone()))] {
|
||||
let mut r = Realm::with_options(Options { max_depth: 1_000_000, ..e4x() }, Limits::default());
|
||||
let interp = r.interp();
|
||||
let global = interp.global;
|
||||
assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok());
|
||||
r.run(deep).unwrap();
|
||||
assert_eq!(r.run(&format!("squeeze({spare}); {action}")).map_err(|e| e.kind), want, "{action} {spare}");
|
||||
}
|
||||
}
|
||||
let long = "var s = '<r>'; for (var i = 0; i < 20000; i++) s += '<a/>'; var l = new XML(s + '</r>').a;";
|
||||
assert_eq!(after_setup(long, "squeeze(50000); l.xpath('self::c').length()"), Err(RunErrorKind::Limit(LimitKind::Heap)));
|
||||
assert_eq!(after_setup(long, "squeeze(50000000); l.xpath('self::c').length()"), Ok(JsValue::Number(0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_reading_a_long_string_takes_fuel_for_its_length() {
|
||||
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var x = <r/>; for (var i = 0; i < 200; i++) x.appendChild(<a>y</a>);";
|
||||
for expr in ["a[string-length($s) = 1]", "a[starts-with($s, $s) = false()]", "a[$s = concat('', 'x')]", "a[number($s) = 1]", "a[. = $s]"] {
|
||||
let mut r = realm();
|
||||
r.run(setup).unwrap();
|
||||
let src = format!("x.xpath({expr:?}, {{ s: s }}).length()");
|
||||
assert_eq!(r.run(&src), Ok(JsValue::Number(0.0)), "{expr}");
|
||||
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
|
||||
assert_eq!(r.run(&src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)), "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_string_functions_hold_the_bytes_they_build() {
|
||||
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var d = '1'; for (var i = 0; i < 20; i++) d += d; var o = { s: s, d: d };";
|
||||
for (expr, spare) in [("translate('a', 'b', $s) = 'a'", 1_000_000), ("number($d) > 1", 500_000), ("substring($s, 1) != 'z'", 4_000_000)] {
|
||||
let action = |spare: u32| format!("squeeze({spare}); (<r/>).xpath(\"self::*[{expr}]\", o).length()");
|
||||
assert_eq!(after_setup(setup, &action(spare)), Err(RunErrorKind::Limit(LimitKind::Heap)), "{expr}");
|
||||
assert_eq!(after_setup(setup, &action(100_000_000)), Ok(JsValue::Number(1.0)), "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_string_variables_hold_the_bytes_of_their_flat_copy() {
|
||||
// The setup ends with `0` so that its completion value does not flatten `s`.
|
||||
let setup = "var s = 'x'; for (var i = 0; i < 20; i++) s += s; var o = { s: s }; 0;";
|
||||
let action = |spare: u32| format!("squeeze({spare}); (<r/>).xpath('self::*[$s]', o).length()");
|
||||
assert_eq!(after_setup(setup, &action(1_000_000)), Err(RunErrorKind::Limit(LimitKind::Heap)));
|
||||
assert_eq!(after_setup(setup, &action(100_000_000)), Ok(JsValue::Number(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_substring_searches_take_fuel_for_every_position_tried() {
|
||||
let setup = "var h = 'a'; for (var i = 0; i < 16; i++) h += h; var n = h.substring(0, 4095) + 'b'; var o = { h: h, n: n };";
|
||||
for expr in ["contains($h, $n)", "substring-before($h, $n) = ''", "substring-after($h, $n) = ''"] {
|
||||
let mut r = realm();
|
||||
r.run(setup).unwrap();
|
||||
let src = format!("(<r/>).xpath(\"self::*[{expr}]\", o).length()");
|
||||
let want = if expr.starts_with("contains") { 0.0 } else { 1.0 };
|
||||
assert_eq!(r.run(&src), Ok(JsValue::Number(want)), "{expr}");
|
||||
r.set_limits(Limits { fuel: 5_000_000, ..Limits::default() });
|
||||
assert_eq!(r.run(&src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)), "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_lang_takes_fuel_for_every_attribute_it_reads() {
|
||||
let setup = "var s = '<r'; for (var i = 0; i < 10000; i++) s += ' a' + i + '=\"\"'; s += '>'; \
|
||||
for (var i = 0; i < 400; i++) s += '<a/>'; var x = new XML(s + '</r>');";
|
||||
let mut r = realm();
|
||||
r.run(setup).unwrap();
|
||||
let src = "x.xpath('a[lang(\"en\")]').length()";
|
||||
assert_eq!(r.run(src), Ok(JsValue::Number(0.0)));
|
||||
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
|
||||
assert_eq!(r.run(src).map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_on_a_large_tree_ends_within_the_default_limits() {
|
||||
let setup = "var x = <r/>; for (var i = 0; i < 3000; i++) x.appendChild(<a n={i}><b/></a>);";
|
||||
truth(&format!("{setup} x.xpath('//a[@n = 2999]/b').length() === 1 && x.xpath('//b/..').length() === 3000"));
|
||||
let mut r = realm();
|
||||
r.run("var x = <r/>; for (var i = 0; i < 2000; i++) x.appendChild(<a/>);").unwrap();
|
||||
r.set_limits(Limits { fuel: 2_000_000, ..Limits::default() });
|
||||
assert_eq!(r.run("x.xpath('//a/following::a')").map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Fuel)));
|
||||
}
|
||||
|
||||
const XPATH_PIECES: &[&str] = &[
|
||||
"/",
|
||||
"//",
|
||||
"a",
|
||||
"b",
|
||||
"*",
|
||||
"@",
|
||||
"@*",
|
||||
".",
|
||||
"..",
|
||||
"[",
|
||||
"]",
|
||||
"(",
|
||||
")",
|
||||
"1",
|
||||
"'x'",
|
||||
"$v",
|
||||
"|",
|
||||
"+",
|
||||
"-",
|
||||
"=",
|
||||
"!=",
|
||||
"<",
|
||||
">=",
|
||||
" and ",
|
||||
" or ",
|
||||
" div ",
|
||||
" mod ",
|
||||
"child::",
|
||||
"following::",
|
||||
"preceding::",
|
||||
"ancestor::",
|
||||
"namespace::",
|
||||
"node()",
|
||||
"text()",
|
||||
"count(",
|
||||
"last()",
|
||||
"position()",
|
||||
"concat(",
|
||||
"substring(",
|
||||
",",
|
||||
"p:",
|
||||
"p:*",
|
||||
"xml:lang",
|
||||
"::",
|
||||
":",
|
||||
"'",
|
||||
" ",
|
||||
];
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(256))]
|
||||
|
||||
#[test]
|
||||
fn random_xpath_expressions_select_nodes_or_throw(parts in proptest::collection::vec(0..XPATH_PIECES.len(), 0..30)) {
|
||||
let expr: String = parts.iter().map(|&i| XPATH_PIECES[i]).collect();
|
||||
let quoted = format!("{expr:?}");
|
||||
let src = format!("var x = <r xmlns:p='urn:p' xml:lang='en'><a p:q='1'><b>2</b><!--c--></a><p:b>3</p:b>t</r>; typeof XML.prototype.xpath === 'function' && x.a[0].xpath({quoted}, {{ v: 1 }}).length() >= 0");
|
||||
let mut r = limited(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 });
|
||||
match r.run(&src) {
|
||||
Ok(v) => prop_assert_eq!(v, JsValue::Bool(true), "{}", expr),
|
||||
Err(e) => prop_assert!(matches!(e.kind, RunErrorKind::Thrown | RunErrorKind::Limit(_)), "{expr:?}: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- generated input ----
|
||||
|
||||
const PIECES: &[&str] = &[
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -778,7 +778,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- `vectorcraft-js` provides the ECMAScript 5.1 built-ins `Object` with its property-descriptor functions, `create`, `keys`, `seal` and `freeze` (section 15.2), `Function.prototype.bind` (15.3.4.5), `Array` with every ES5 method (15.4), the error types (15.11), `Math` (15.8), `JSON` with revivers, replacers and indentation (15.12), and the global functions with Annex B's `escape` and `unescape` (15.1, B.2). The array methods work on array-like objects. A method visits only the indices that exist when its range is longer than 4,096 indices and more than 8 times the properties on the object and its prototypes, so `new Array(4294967295).join("")` ends at once; a shorter range is visited index by index, holes included, and each index takes fuel. `join` writes a cycle back to an array as an empty string, `JSON.stringify` throws a `TypeError` on a cycle, and deep `JSON` nesting uses no native stack. Every loop takes fuel and every result is checked against the heap limit. `cargo xtask test262` runs the suite's ES5 built-in tests that need only these built-ins: 6,798 of 6,866 runs pass (the 34 failing tests expect rules of later editions). It also fetches the `json2.js` polyfill at a pinned commit and checks that its `stringify` and `parse` agree with the built-in `JSON`. Tests: `crates/js/src/builtins_tests.rs`, `crates/js/tests/test262.rs`, `crates/js/tests/json2.rs`.
|
||||
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,379 of 1,411 pass for `String`, 298 of 298 for `Number`, 954 of 988 for `RegExp` and 8 of 8 for `Date`. The others expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- With `Options::e4x` set, an E4X name standing alone (`@a`, `@ns::a`, `@[expr]`, `ns::a`, `*`) inside `with` or a filter predicate can be assigned, with `=` or a compound operator, incremented, decremented, deleted and used as a `for` or `for each` variable, as well as read. Each of these uses the nearest XML or XMLList value in scope that holds the name and evaluates the name once. When no XML value in scope holds the name, the read or write throws a `ReferenceError` before the right-hand side runs and creates no child or attribute, as ECMA-357 section 11.1 (https://ecma-international.org/publications-and-standards/standards/ecma-357/) specifies, so a filter such as `list.(@id == '1')` throws when an item has no `@id` and no outer XML value holds one. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- `vectorcraft-js` runs ECMAScript for XML (ECMA-357, 2nd edition, https://ecma-international.org/publications-and-standards/standards/ecma-357/) when `Options::e4x` is set, which it is not by default: XML and XMLList literals with embedded `{}` expressions, the `..`, `.@`, `.*`, `.()` and `::` selectors, assignment and `delete` on selected nodes, `for each`, `default xml namespace`, and the `XML`, `XMLList`, `Namespace` and `QName` objects with their methods and settings (`xpath` is not provided). `<!DOCTYPE>` in an XML string gives a `SyntaxError`. An XML declaration at the start of an XML string, after optional white space, is checked against XML 1.0's grammar (https://www.w3.org/TR/xml/#NT-XMLDecl): a version of `1.` and digits, then an optional encoding name and an optional standalone `yes` or `no`, in that order; any other declaration, or one with a character XML does not allow, gives a `SyntaxError`. The parser takes fuel for the length of every name, white space run and declaration it scans, and for the text it searched when a quote, CDATA section, comment or processing instruction is not closed. Parsing, serializing, comparing and copying trees use no native stack, and each takes fuel and counts against the heap limit. Inserting a node into itself or its descendants throws a `TypeError` before the tree changes, a literal or XML string nested past `Options::max_depth` gives a `SyntaxError`, and a tree built past that depth stops at the heap limit. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- `vectorcraft-js` runs ECMAScript for XML (ECMA-357, 2nd edition, https://ecma-international.org/publications-and-standards/standards/ecma-357/) when `Options::e4x` is set, which it is not by default: XML and XMLList literals with embedded `{}` expressions, the `..`, `.@`, `.*`, `.()` and `::` selectors, assignment and `delete` on selected nodes, `for each`, `default xml namespace`, and the `XML`, `XMLList`, `Namespace` and `QName` objects with their methods and settings. `xml.xpath(expression[, variables])` evaluates an XPath 1.0 expression (https://www.w3.org/TR/1999/REC-xpath-19991116/) with all 13 axes and the 27 core functions, and returns the selected nodes as an XMLList in document order without repeats. Each property of `variables` binds the variable of its name, an XMLList runs the expression on each of its elements in turn, and a prefix in a name test resolves through the context element's in-scope namespaces. An expression that breaks the grammar gives a `SyntaxError`; an unknown function, a wrong argument count, an unbound prefix or variable, and a result that is not a node-set or that holds the root or a namespace node give a `TypeError`. `id()` selects nothing, and evaluation uses no native stack, takes fuel and counts against the heap limit. `<!DOCTYPE>` in an XML string gives a `SyntaxError`. An XML declaration at the start of an XML string, after optional white space, is checked against XML 1.0's grammar (https://www.w3.org/TR/xml/#NT-XMLDecl): a version of `1.` and digits, then an optional encoding name and an optional standalone `yes` or `no`, in that order; any other declaration, or one with a character XML does not allow, gives a `SyntaxError`. The parser takes fuel for the length of every name, white space run and declaration it scans, and for the text it searched when a quote, CDATA section, comment or processing instruction is not closed. Parsing, serializing, comparing and copying trees use no native stack, and each takes fuel and counts against the heap limit. Inserting a node into itself or its descendants throws a `TypeError` before the tree changes, a literal or XML string nested past `Options::max_depth` gives a `SyntaxError`, and a tree built past that depth stops at the heap limit. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
|
||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||
|
||||
Reference in New Issue
Block a user