#![cfg(all(feature = "xpath", feature = "std"))]
use oxml::{Document, Limits, XPath, XPathError};
const STACK: usize = 2 * 1024 * 1024;
fn on_small_stack<T: Send + 'static>(
f: impl FnOnce() -> T + Send + 'static,
) -> T {
std::thread::Builder::new()
.stack_size(STACK)
.spawn(f)
.expect("spawn")
.join()
.expect("the thread must not panic or abort")
}
fn doc() -> Document {
oxml::parse("<r><x/><x/></r>").expect("well-formed")
}
fn compile_eval_drop(expr: &str) -> Result<(), XPathError> {
let compiled = XPath::compile(expr)?;
let d = doc();
let value = compiled.evaluate(&d);
drop(value);
drop(compiled);
Ok(())
}
#[test]
fn a_million_unary_minuses_is_an_error_not_an_abort() {
let expr = "-".repeat(1_000_000) + "1";
let r = on_small_stack(move || compile_eval_drop(&expr));
assert!(r.is_err(), "rejected by the operator budget");
}
#[test]
fn a_million_term_addition_is_an_error_not_an_abort() {
let expr = "1+".repeat(1_000_000) + "1";
let r = on_small_stack(move || compile_eval_drop(&expr));
assert!(r.is_err());
}
#[test]
fn a_million_term_or_chain_is_an_error_not_an_abort() {
let expr = "1 or ".repeat(1_000_000) + "1";
let r = on_small_stack(move || compile_eval_drop(&expr));
assert!(r.is_err());
}
#[test]
fn a_million_term_union_is_an_error_not_an_abort() {
let expr = "//x|".repeat(1_000_000) + "//x";
let r = on_small_stack(move || compile_eval_drop(&expr));
assert!(r.is_err());
}
#[test]
fn a_hundred_thousand_parentheses_is_an_error_not_an_abort() {
let expr = "(".repeat(100_000) + "1" + &")".repeat(100_000);
let r = on_small_stack(move || compile_eval_drop(&expr));
let e = r.expect_err("beyond the depth limit");
assert!(e.to_string().contains("deep"), "{e}");
}
#[test]
fn a_hundred_thousand_nested_calls_is_an_error_not_an_abort() {
let expr = "not(".repeat(100_000) + "1" + &")".repeat(100_000);
let r = on_small_stack(move || compile_eval_drop(&expr));
let e = r.expect_err("beyond the depth limit");
assert!(e.to_string().contains("deep"), "{e}");
}
#[test]
fn nested_predicates_count_towards_depth() {
let expr =
"//x".to_owned() + &"[.".repeat(100_000) + "[1]" + &"]".repeat(100_000);
let r = on_small_stack(move || compile_eval_drop(&expr));
let e = r.expect_err("beyond the depth limit");
assert!(e.to_string().contains("deep"), "{e}");
}
#[test]
fn strict_limits_bound_the_operator_count() {
let strict = Limits::strict();
assert_eq!(
strict.max_xpath_operators, 1_000,
"the fixture assumes this"
);
let under = "1+".repeat(999) + "1";
assert!(XPath::compile_with_limits(&under, &strict).is_ok(), "999");
let over = "1+".repeat(1_001) + "1";
let e =
XPath::compile_with_limits(&over, &strict).expect_err("1001 operators");
assert!(e.to_string().contains("operator"), "{e}");
assert!(XPath::compile(&over).is_ok(), "10_000 is the default");
}
#[test]
fn strict_limits_bound_the_nesting_depth() {
let strict = Limits::strict();
assert_eq!(strict.max_xpath_depth, 32, "the fixture assumes this");
let under = "(".repeat(31) + "1" + &")".repeat(31);
assert!(XPath::compile_with_limits(&under, &strict).is_ok(), "31");
let over = "(".repeat(33) + "1" + &")".repeat(33);
let e = XPath::compile_with_limits(&over, &strict).expect_err("depth 33");
assert!(e.to_string().contains("deep"), "{e}");
assert!(XPath::compile(&over).is_ok(), "256 is the default");
}
#[test]
fn namespaces_and_limits_combine() {
let strict = Limits::strict();
let x = XPath::compile_with_namespaces_and_limits(
"//m:a",
&[("m", "urn:u")],
&strict,
)
.expect("bound prefix, within limits");
let d = oxml::parse(r#"<r xmlns:m="urn:u"><m:a>yes</m:a></r>"#).unwrap();
assert_eq!(x.evaluate(&d).to_str(&d), "yes");
assert!(
XPath::compile_with_namespaces_and_limits("//q:a", &[], &strict)
.is_err(),
"an unbound prefix is still an error"
);
let over = "1+".repeat(1_001) + "1";
assert!(
XPath::compile_with_namespaces_and_limits(&over, &[], &strict).is_err(),
"the operator budget still applies"
);
}
#[test]
fn a_long_chain_within_the_default_budget_evaluates_on_a_small_stack() {
let expr = "1+".repeat(9_000) + "1";
let n = on_small_stack(move || {
let x = XPath::compile(&expr).expect("9_000 operators is under 10_000");
let d = doc();
x.evaluate(&d).to_number(&d)
});
assert!((n - 9_001.0).abs() < f64::EPSILON, "{n}");
}
#[test]
fn a_long_or_chain_short_circuits() {
let expr = "1 or ".repeat(5_000) + "0 div 0";
let v = on_small_stack(move || {
let x = XPath::compile(&expr).expect("5_000 operators");
let d = doc();
x.evaluate(&d).to_boolean()
});
assert!(v);
let expr = "0 and ".repeat(5_000) + "1";
let v = on_small_stack(move || {
let x = XPath::compile(&expr).expect("5_000 operators");
let d = doc();
x.evaluate(&d).to_boolean()
});
assert!(!v);
}
#[test]
fn chain_semantics_match_the_nested_form() {
let d = doc();
let eval = |e: &str| XPath::compile(e).expect("compiles").evaluate(&d);
let num = |e: &str| -> i64 {
i64::try_from(eval(e).to_number(&d) as i128).expect("integral")
};
assert_eq!(num("8 - 3 - 2"), 3);
assert_eq!(num("100 div 10 div 5"), 2);
assert_eq!(num("17 mod 10 mod 4"), 3);
assert_eq!(num("2 * 3 + 4 * 5 - 6"), 20);
assert!(eval("2 = 2 = 1").to_boolean());
assert!(!eval("2 = 2 != 1").to_boolean());
assert!(eval("1 < 2 < 3").to_boolean());
assert!(!eval("3 > 2 > 1").to_boolean());
assert_eq!(num("count(//x | //x | //r)"), 3);
assert_eq!(num("1 - -1"), 2);
assert_eq!(num("- - 1"), 1);
assert_eq!(num("---1"), -1);
assert!(eval("--'abc'").to_number(&d).is_nan());
assert!(eval("1 and 1 and 0 or 1").to_boolean());
assert!(!eval("1 and 0 or 0").to_boolean());
}
#[test]
fn a_chain_clones_compares_and_debugs_without_deep_recursion() {
let expr = "1+".repeat(9_000) + "1";
on_small_stack(move || {
let x = XPath::compile(&expr).expect("compiles");
let copy = x.clone();
assert_eq!(x, copy);
assert!(!format!("{:?}", x.expr()).is_empty());
drop(copy);
drop(x);
});
}
#[test]
fn operators_steps_predicates_and_calls_all_count() {
let mut one = Limits::default();
one.max_xpath_operators = 3;
let accepts = |e: &str| XPath::compile_with_limits(e, &one).is_ok();
assert!(accepts("/a/b/c"));
assert!(!accepts("/a/b/c/d"));
assert!(accepts("/a/b[1]"));
assert!(!accepts("/a/b[1]/c"));
assert!(accepts("not(1 + 1 + 1)"));
assert!(!accepts("not(1 + 1 + 1 + 1)"));
assert!(accepts("---1"));
assert!(!accepts("----1"));
assert!(accepts("//a/b"));
assert!(!accepts("//a//b"));
assert!(accepts("((((('x')))))"));
}