1#![forbid(unsafe_code)] pub mod ast;
35pub mod context;
36pub mod eval;
37pub mod exslt;
38pub mod pattern;
39pub mod rtf;
40mod index;
41mod lexer;
42mod parser;
43mod bindings_builder;
44
45pub use bindings_builder::XPathBindingsBuilder;
46
47pub use ast::{Axis, Expr, LocationPath, NodeTest, Step};
48pub use ast::{FunctionSig, ItemType, Occurrence, SequenceType};
49pub use parser::parse_sequence_type_str;
50pub use context::{DocIndex, INodeKind, is_synthetic_id};
51pub use eval::Value as XPathValue;
52pub use eval::compile_xpath_2_0_regex;
53pub use index::{DocIndexLike, NodeId, XPathNodeKind};
54
55use crate::error::Result;
56use sup_xml_tree::dom::Document as ArenaDocument;
57
58#[derive(Debug, Clone)]
76pub struct XPathOptions {
77 pub libxml2_compatible: bool,
78 pub xpath_2_0: bool,
88 pub max_eval_steps: u64,
98}
99
100impl Default for XPathOptions {
101 fn default() -> Self {
102 Self {
106 libxml2_compatible: false,
107 xpath_2_0: false,
108 max_eval_steps: eval::DEFAULT_MAX_EVAL_STEPS,
109 }
110 }
111}
112
113pub fn parse_xpath(src: &str) -> Result<Expr> {
117 parse_xpath_with(src, &XPathOptions::default())
118}
119
120pub fn parse_xpath_with(src: &str, opts: &XPathOptions) -> Result<Expr> {
124 let allow_exponent = opts.libxml2_compatible || opts.xpath_2_0;
131 let (tokens, spans) = lexer::tokenize_with(src, allow_exponent)?;
132 let mut p = parser::Parser::new_with_spans(tokens, spans, src);
133 p.set_xpath_2_0(opts.xpath_2_0);
134 let expr = p.parse_expr()?;
135 p.expect_eof()?;
136 let depth = ast::max_predicate_nesting(&expr);
137 if depth > MAX_PREDICATE_NESTING_DEPTH {
138 use crate::error::{ErrorDomain, ErrorLevel, XmlError};
139 return Err(XmlError::new(
140 ErrorDomain::XPath,
141 ErrorLevel::Error,
142 format!(
143 "XPath predicate nesting depth ({depth}) exceeds limit \
144 ({MAX_PREDICATE_NESTING_DEPTH}); evaluator complexity is \
145 O(N^k) in document size N and predicate-nesting depth k"
146 ),
147 ));
148 }
149 Ok(expr)
150}
151
152pub const MAX_PREDICATE_NESTING_DEPTH: u32 = 8;
164
165pub struct XPathContext<'doc> {
172 pub index: DocIndex<'doc>,
177 doc: &'doc ArenaDocument,
181 options: XPathOptions,
186}
187
188impl<'doc> XPathContext<'doc> {
189 pub fn new(doc: &'doc ArenaDocument) -> Self {
193 Self::new_with(doc, XPathOptions::default())
194 }
195
196 pub fn new_with(doc: &'doc ArenaDocument, options: XPathOptions) -> Self {
200 Self { index: DocIndex::build(doc), doc, options }
201 }
202
203 pub fn eval(&self, src: &str) -> Result<XPathValue> {
204 self.eval_with(src, 0, &eval::NoBindings)
205 }
206
207 pub fn eval_at(&self, src: &str, context_node: NodeId) -> Result<XPathValue> {
211 self.eval_with(src, context_node, &eval::NoBindings)
212 }
213
214 pub fn eval_with(
221 &self,
222 src: &str,
223 context_node: NodeId,
224 bindings: &dyn eval::XPathBindings,
225 ) -> Result<XPathValue> {
226 let expr = parse_xpath_with(src, &self.options)?;
227 eval::validate_prefixes(&expr, bindings)?;
232 eval::set_eval_budget(self.options.max_eval_steps);
237 eval::reset_eval_budget();
238 eval::refresh_stable_now();
241 let static_ctx = eval::StaticContext {
242 xpath_2_0: self.options.xpath_2_0,
243 xpath_3_0: false,
244 libxml2_compatible: self.options.libxml2_compatible,
245 current_node: Some(context_node),
248 };
249 eval::eval_expr(
250 &expr,
251 &eval::EvalCtx {
252 context_node, pos: 1, size: 1, bindings,
253 static_ctx: &static_ctx,
254 },
255 &self.index,
256 )
257 }
258
259 pub fn id_for_element(&self, ptr: *const sup_xml_tree::dom::Node<'_>) -> Option<NodeId> {
271 use crate::xpath::context::INodeKind;
272 if ptr.is_null() { return Some(0); }
273 let target = ptr as *const ();
274 for (i, n) in self.index.nodes.iter().enumerate() {
275 let matches = match n.kind {
276 INodeKind::Element(p) | INodeKind::Text(p) | INodeKind::Comment(p)
277 | INodeKind::CData(p) | INodeKind::PI(p)
278 => (p as *const _ as *const ()) == target,
279 INodeKind::Attribute(a)
280 => (a as *const _ as *const ()) == target,
281 _ => false,
282 };
283 if matches { return Some(i); }
284 }
285 None
286 }
287 pub fn eval_bool(&self, src: &str) -> Result<bool> {
288 Ok(eval::value_to_bool(&self.eval(src)?, &self.index))
289 }
290 pub fn eval_str(&self, src: &str) -> Result<String> {
291 Ok(eval::value_to_string(&self.eval(src)?, &self.index))
292 }
293 pub fn eval_num(&self, src: &str) -> Result<f64> {
294 Ok(eval::value_to_number(&self.eval(src)?, &self.index))
295 }
296 pub fn eval_strings(&self, src: &str) -> Result<Vec<String>> {
297 let v = self.eval(src)?;
298 Ok(match v {
299 XPathValue::NodeSet(ns) => ns.iter().map(|&id| self.index.string_value(id)).collect(),
300 other => vec![eval::value_to_string(&other, &self.index)],
301 })
302 }
303 pub fn eval_count(&self, src: &str) -> Result<usize> {
304 let v = self.eval(src)?;
305 Ok(match v { XPathValue::NodeSet(ns) => ns.len(), _ => 0 })
306 }
307
308 pub fn eval_node_xml(&self, src: &str) -> Result<Vec<String>> {
325 use crate::serializer::{serialize_node_to_string, serialize_with, SerializeOptions};
326 use crate::xpath::context::INodeKind;
327 let v = self.eval(src)?;
328 let ns = match v {
329 XPathValue::NodeSet(ns) => ns,
330 other => return Ok(vec![eval::value_to_string(&other, &self.index)]),
331 };
332 let opts = SerializeOptions::default();
333 let mut out = Vec::with_capacity(ns.len());
334 for id in ns {
335 let node = &self.index.nodes[id];
336 let serialized = match &node.kind {
337 INodeKind::Element(n) | INodeKind::Text(n)
338 | INodeKind::Comment(n) | INodeKind::CData(n)
339 | INodeKind::PI(n) => serialize_node_to_string(n, &opts),
340 INodeKind::Attribute(a) => {
341 let mut s = String::with_capacity(a.name().len() + a.value().len() + 3);
342 s.push_str(a.name());
343 s.push_str("=\"");
344 for ch in a.value().chars() {
347 match ch {
348 '&' => s.push_str("&"),
349 '<' => s.push_str("<"),
350 '"' => s.push_str("""),
351 c => s.push(c),
352 }
353 }
354 s.push('"');
355 s
356 }
357 INodeKind::Namespace { prefix: Some(p), uri } =>
358 format!("xmlns:{}=\"{}\"", p, uri),
359 INodeKind::Namespace { prefix: None, uri } =>
360 format!("xmlns=\"{}\"", uri),
361 INodeKind::Document => serialize_with(self.doc, &opts),
362 };
363 out.push(serialized);
364 }
365 Ok(out)
366 }
367}
368
369pub fn xpath_eval(doc: &ArenaDocument, src: &str) -> Result<XPathValue> {
372 XPathContext::new(doc).eval(src)
373}
374pub fn xpath_bool(doc: &ArenaDocument, src: &str) -> Result<bool> {
375 XPathContext::new(doc).eval_bool(src)
376}
377pub fn xpath_str(doc: &ArenaDocument, src: &str) -> Result<String> {
378 XPathContext::new(doc).eval_str(src)
379}
380pub fn xpath_num(doc: &ArenaDocument, src: &str) -> Result<f64> {
381 XPathContext::new(doc).eval_num(src)
382}
383pub fn xpath_strings(doc: &ArenaDocument, src: &str) -> Result<Vec<String>> {
384 XPathContext::new(doc).eval_strings(src)
385}
386pub fn xpath_count(doc: &ArenaDocument, src: &str) -> Result<usize> {
387 XPathContext::new(doc).eval_count(src)
388}
389
390#[cfg(test)]
393mod arena_tests {
394 use super::*;
395 use crate::{parse_str, ParseOptions};
396
397 fn parse(xml: &str) -> ArenaDocument {
398 parse_str(xml, &ParseOptions::default()).expect("parse")
399 }
400
401 fn parse_ns(xml: &str) -> ArenaDocument {
402 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
403 parse_str(xml, &opts).expect("parse")
404 }
405
406 #[test]
409 fn absolute_path() {
410 let doc = parse(r#"<catalog><book id="1"/><book id="2"/></catalog>"#);
411 assert_eq!(xpath_count(&doc, "/catalog/book").unwrap(), 2);
412 assert_eq!(xpath_count(&doc, "/catalog").unwrap(), 1);
413 }
414
415 #[test]
416 fn wildcard_children() {
417 let doc = parse("<r><a/><b/><c/></r>");
418 assert_eq!(xpath_count(&doc, "/r/*").unwrap(), 3);
419 }
420
421 #[test]
424 fn attribute_predicate() {
425 let doc = parse(r#"<r><book id="1"/><book id="2"/><book id="1"/></r>"#);
426 assert_eq!(xpath_count(&doc, "/r/book[@id='1']").unwrap(), 2);
427 assert!(xpath_bool(&doc, "/r/book[@id='1']").unwrap());
428 }
429
430 #[test]
431 fn current_in_nested_predicate_is_fixed_to_the_expression_context() {
432 let doc = parse("<s><phase id='m'><active pat='p1'/></phase>\
440 <p id='p1'/><p id='p2'/></s>");
441 let ctx = XPathContext::new(&doc);
442 let node_of = |q: &str| match ctx.eval(q).unwrap() {
443 XPathValue::NodeSet(ns) => ns[0],
444 other => panic!("expected node-set, got {other:?}"),
445 };
446 let expr = "../phase[@id='m']/active[@pat=current()/@id]";
447 let hit = ctx.eval_at(expr, node_of("//p[@id='p1']")).unwrap();
449 assert!(matches!(hit, XPathValue::NodeSet(ref ns) if ns.len() == 1),
450 "current() in the nested predicate must resolve to p1: {hit:?}");
451 let miss = ctx.eval_at(expr, node_of("//p[@id='p2']")).unwrap();
453 assert!(matches!(miss, XPathValue::NodeSet(ref ns) if ns.is_empty()),
454 "expected no match from p2: {miss:?}");
455 }
456
457 #[test]
458 fn attribute_value_extraction() {
459 let doc = parse(r#"<r id="42"/>"#);
460 assert_eq!(xpath_str(&doc, "/r/@id").unwrap(), "42");
461 }
462
463 #[test]
466 fn descendant_or_self() {
467 let doc = parse("<r><a><b><c/></b></a></r>");
468 assert_eq!(xpath_count(&doc, "//c").unwrap(), 1);
469 assert_eq!(xpath_count(&doc, "//*").unwrap(), 4); }
471
472 #[test]
475 fn string_value_extraction() {
476 let doc = parse("<r><title>Hello</title></r>");
477 assert_eq!(xpath_str(&doc, "/r/title").unwrap(), "Hello");
478 assert_eq!(xpath_str(&doc, "string(/r/title)").unwrap(), "Hello");
479 }
480
481 #[test]
482 fn string_concat_children() {
483 let doc = parse("<r>foo<b>bar</b>baz</r>");
484 assert_eq!(xpath_str(&doc, "string(/r)").unwrap(), "foobarbaz");
485 }
486
487 #[test]
488 fn name_functions() {
489 let doc = parse("<r><a/></r>");
490 assert_eq!(xpath_str(&doc, "name(/r)").unwrap(), "r");
491 assert_eq!(xpath_str(&doc, "name(/r/a)").unwrap(), "a");
492 }
493
494 #[test]
504 fn substring_basic() {
505 let doc = parse("<r/>");
506 assert_eq!(xpath_str(&doc, r#"substring("12345", 2, 3)"#).unwrap(), "234");
507 assert_eq!(xpath_str(&doc, r#"substring("12345", 2)"#).unwrap(), "2345");
508 assert_eq!(xpath_str(&doc, r#"substring("12345", 1.5, 2.6)"#).unwrap(), "234");
510 assert_eq!(xpath_str(&doc, r#"substring("12345", 0, 3)"#).unwrap(), "12");
512 }
513
514 #[test]
515 fn substring_negative_start() {
516 let doc = parse("<r/>");
517 assert_eq!(xpath_str(&doc, r#"substring("12345", -3, 5)"#).unwrap(), "1");
519 assert_eq!(xpath_str(&doc, r#"substring("abc", 10, 5)"#).unwrap(), "");
521 assert_eq!(xpath_str(&doc, r#"substring("abc", 1, 0)"#).unwrap(), "");
523 }
524
525 #[test]
526 fn substring_infinite_start_does_not_panic() {
527 let doc = parse("<r/>");
530 assert_eq!(xpath_str(&doc, r#"substring("hello", 1 div 0, 5)"#).unwrap(), "");
531 assert_eq!(xpath_str(&doc, r#"substring("hello", -1 div 0, 5)"#).unwrap(), "");
532 }
533
534 #[test]
535 fn substring_infinite_length() {
536 let doc = parse("<r/>");
537 assert_eq!(
539 xpath_str(&doc, r#"substring("12345", -42, 1 div 0)"#).unwrap(),
540 "12345",
541 );
542 assert_eq!(xpath_str(&doc, r#"substring("abc", 1, -1 div 0)"#).unwrap(), "");
544 }
545
546 #[test]
547 fn substring_nan_yields_empty() {
548 let doc = parse("<r/>");
550 assert_eq!(xpath_str(&doc, r#"substring("hello", 0 div 0, 5)"#).unwrap(), "");
551 assert_eq!(xpath_str(&doc, r#"substring("hello", 1, 0 div 0)"#).unwrap(), "");
552 assert_eq!(
554 xpath_str(&doc, r#"substring("hello", -1 div 0, 1 div 0)"#).unwrap(),
555 "",
556 );
557 }
558
559 #[test]
562 fn count_function() {
563 let doc = parse("<r><i/><i/><i/></r>");
564 assert_eq!(xpath_num(&doc, "count(/r/i)").unwrap(), 3.0);
565 }
566
567 #[test]
568 fn arithmetic() {
569 let doc = parse("<r/>");
570 assert_eq!(xpath_num(&doc, "2 + 3 * 4").unwrap(), 14.0);
571 assert_eq!(xpath_num(&doc, "10 div 4").unwrap(), 2.5);
572 }
573
574 #[test]
577 fn position_predicate() {
578 let doc = parse("<r><i>1</i><i>2</i><i>3</i></r>");
579 assert_eq!(xpath_str(&doc, "/r/i[1]").unwrap(), "1");
580 assert_eq!(xpath_str(&doc, "/r/i[2]").unwrap(), "2");
581 assert_eq!(xpath_str(&doc, "/r/i[last()]").unwrap(), "3");
582 }
583
584 #[test]
585 fn numeric_predicate_on_text() {
586 let doc = parse("<r><i>5</i><i>15</i><i>25</i></r>");
587 assert_eq!(xpath_count(&doc, "/r/i[. > 10]").unwrap(), 2);
588 }
589
590 struct TestNs(&'static [(&'static str, &'static str)]);
596 impl eval::XPathBindings for TestNs {
597 fn resolve_prefix(&self, prefix: &str) -> Option<String> {
598 self.0.iter().find(|(p, _)| *p == prefix).map(|(_, u)| (*u).to_string())
599 }
600 }
601
602 #[test]
603 fn prefixed_element_addressable_by_qname() {
604 let doc = parse_ns(r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>X</dc:title></r>"#);
605 let ctx = XPathContext::new(&doc);
606 let bind = TestNs(&[("dc", "http://purl.org/dc/elements/1.1/")]);
607 let v = ctx.eval_with("namespace-uri(/r/dc:title)", 0, &bind).unwrap();
608 assert_eq!(eval::value_to_string(&v, &ctx.index),
609 "http://purl.org/dc/elements/1.1/");
610 let v = ctx.eval_with("local-name(/r/dc:title)", 0, &bind).unwrap();
611 assert_eq!(eval::value_to_string(&v, &ctx.index), "title");
612 }
613
614 #[test]
619 fn undefined_prefix_in_qname_errors() {
620 let doc = parse_ns(r#"<r xmlns:fa="urn:x"><fa:d/></r>"#);
621 let err = xpath_str(&doc, "/fa:d").unwrap_err();
622 assert!(
623 err.message.contains("Undefined namespace prefix"),
624 "expected undefined-prefix error, got: {}", err.message,
625 );
626 }
627
628 #[test]
629 fn undefined_prefix_in_prefix_wildcard_errors() {
630 let doc = parse_ns(r#"<r xmlns:fa="urn:x"><fa:d/></r>"#);
631 let err = xpath_str(&doc, "/fa:*").unwrap_err();
632 assert!(
633 err.message.contains("Undefined namespace prefix"),
634 "expected undefined-prefix error, got: {}", err.message,
635 );
636 }
637
638 #[test]
641 fn undefined_prefix_in_predicate_errors() {
642 let doc = parse_ns("<r><a/></r>");
643 let err = xpath_str(&doc, "/r/a[fa:flag]").unwrap_err();
644 assert!(
645 err.message.contains("Undefined namespace prefix"),
646 "expected undefined-prefix error, got: {}", err.message,
647 );
648 }
649
650 #[test]
654 fn namespace_axis_implicit_xml_prefix() {
655 let doc = parse_ns("<r/>");
656 assert_eq!(xpath_count(&doc, "/r/namespace::*").unwrap(), 1);
657 assert_eq!(
660 xpath_str(&doc, "string(/r/namespace::*[1])").unwrap(),
661 "http://www.w3.org/XML/1998/namespace",
662 );
663 }
664
665 #[test]
668 fn namespace_axis_one_declared_plus_xml() {
669 let doc = parse_ns(
670 r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"><c/></r>"#,
671 );
672 assert_eq!(xpath_count(&doc, "/r/namespace::*").unwrap(), 2);
673 assert_eq!(xpath_count(&doc, "/r/c/namespace::*").unwrap(), 2);
675 }
676
677 #[test]
680 fn namespace_axis_name_test_selects_by_prefix() {
681 let doc = parse_ns(
682 r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"/>"#,
683 );
684 assert_eq!(
685 xpath_str(&doc, "string(/r/namespace::dc)").unwrap(),
686 "http://purl.org/dc/elements/1.1/",
687 );
688 assert_eq!(
690 xpath_str(&doc, "local-name(/r/namespace::dc)").unwrap(),
691 "dc",
692 );
693 }
694
695 #[test]
698 fn namespace_axis_default_namespace_has_empty_prefix() {
699 let doc = parse_ns(r#"<r xmlns="http://example.com/ns"/>"#);
700 let ctx = XPathContext::new(&doc);
703 let bind = TestNs(&[("x", "http://example.com/ns")]);
704 let v = ctx.eval_with("count(/x:r/namespace::*)", 0, &bind).unwrap();
706 assert_eq!(eval::value_to_number(&v, &ctx.index), 2.0);
707 let v = ctx
709 .eval_with("string(/x:r/namespace::*[local-name()=''])", 0, &bind)
710 .unwrap();
711 assert_eq!(eval::value_to_string(&v, &ctx.index), "http://example.com/ns");
712 }
713
714 #[test]
716 fn namespace_axis_inherits_from_ancestor() {
717 let doc = parse_ns(
718 r#"<r xmlns:a="urn:a"><mid xmlns:b="urn:b"><leaf/></mid></r>"#,
719 );
720 assert_eq!(xpath_count(&doc, "/r/mid/leaf/namespace::*").unwrap(), 3);
722 assert_eq!(
723 xpath_str(&doc, "string(/r/mid/leaf/namespace::a)").unwrap(),
724 "urn:a",
725 );
726 assert_eq!(
727 xpath_str(&doc, "string(/r/mid/leaf/namespace::b)").unwrap(),
728 "urn:b",
729 );
730 }
731
732 #[test]
736 fn namespace_axis_shadows_ancestor() {
737 let doc = parse_ns(
738 r#"<r xmlns:p="urn:outer"><c xmlns:p="urn:inner"/></r>"#,
739 );
740 assert_eq!(
741 xpath_str(&doc, "string(/r/namespace::p)").unwrap(),
742 "urn:outer",
743 );
744 assert_eq!(
745 xpath_str(&doc, "string(/r/c/namespace::p)").unwrap(),
746 "urn:inner",
747 );
748 assert_eq!(xpath_count(&doc, "/r/c/namespace::*").unwrap(), 2);
750 }
751
752 #[test]
755 fn namespace_axis_default_undeclaration() {
756 let doc = parse_ns(
757 r#"<r xmlns="urn:outer"><c xmlns=""/></r>"#,
758 );
759 let ctx = XPathContext::new(&doc);
760 let bind = TestNs(&[("x", "urn:outer")]);
761 let r_count = ctx.eval_with("count(/x:r/namespace::*)", 0, &bind).unwrap();
764 assert_eq!(eval::value_to_number(&r_count, &ctx.index), 2.0);
765 let c_count = ctx.eval_with("count(/x:r/c/namespace::*)", 0, &bind).unwrap();
766 assert_eq!(eval::value_to_number(&c_count, &ctx.index), 1.0);
767 }
768
769 const EXSLT_NS: &[(&str, &str)] = &[
776 ("math", "http://exslt.org/math"),
777 ("date", "http://exslt.org/dates-and-times"),
778 ("str", "http://exslt.org/strings"),
779 ("set", "http://exslt.org/sets"),
780 ];
781
782 #[test]
783 fn exslt_math_max_via_xpath() {
784 let doc = parse("<r><i>3</i><i>7</i><i>1</i><i>5</i></r>");
785 let ctx = XPathContext::new(&doc);
786 let v = ctx.eval_with("math:max(/r/i)", 0, &TestNs(EXSLT_NS)).unwrap();
787 assert_eq!(eval::value_to_number(&v, &ctx.index), 7.0);
788 }
789
790 #[test]
791 fn exslt_str_padding_via_xpath() {
792 let doc = parse("<r/>");
793 let ctx = XPathContext::new(&doc);
794 let v = ctx.eval_with("str:padding(5, '*')", 0, &TestNs(EXSLT_NS)).unwrap();
795 assert_eq!(eval::value_to_string(&v, &ctx.index), "*****");
796 }
797
798 #[test]
799 fn exslt_set_distinct_via_xpath() {
800 let doc = parse("<r><i>x</i><i>y</i><i>x</i><i>y</i><i>z</i></r>");
801 let ctx = XPathContext::new(&doc);
802 let v = ctx.eval_with("count(set:distinct(/r/i))", 0, &TestNs(EXSLT_NS)).unwrap();
803 assert_eq!(eval::value_to_number(&v, &ctx.index), 3.0);
805 }
806
807 #[test]
808 fn exslt_date_year_via_xpath() {
809 let doc = parse("<r/>");
810 let ctx = XPathContext::new(&doc);
811 let v = ctx.eval_with(
812 "date:year('2024-07-04T12:00:00Z')", 0, &TestNs(EXSLT_NS),
813 ).unwrap();
814 assert_eq!(eval::value_to_number(&v, &ctx.index), 2024.0);
815 }
816
817 #[test]
821 fn exslt_prefix_is_user_choice() {
822 let doc = parse("<r><i>5</i><i>3</i></r>");
823 let ctx = XPathContext::new(&doc);
824 let bind = TestNs(&[("MATHS", "http://exslt.org/math")]);
826 let v = ctx.eval_with("MATHS:min(/r/i)", 0, &bind).unwrap();
827 assert_eq!(eval::value_to_number(&v, &ctx.index), 3.0);
828 }
829
830 #[test]
833 fn unknown_exslt_function_in_registered_ns_errors() {
834 let doc = parse("<r/>");
835 let ctx = XPathContext::new(&doc);
836 let err = ctx.eval_with(
837 "math:does-not-exist()", 0, &TestNs(EXSLT_NS),
838 ).unwrap_err();
839 assert!(
840 err.message.contains("Unregistered XPath function"),
841 "unexpected error: {}", err.message,
842 );
843 }
844
845 #[test]
848 fn union_dedups() {
849 let doc = parse("<r><a/><b/></r>");
850 assert_eq!(xpath_count(&doc, "/r/a | /r/b | /r/a").unwrap(), 2);
851 }
852
853 #[test]
854 fn boolean_and_or() {
855 let doc = parse("<r><a/><b/></r>");
856 assert!(xpath_bool(&doc, "/r/a or /r/missing").unwrap());
857 assert!(!xpath_bool(&doc, "/r/a and /r/missing").unwrap());
858 }
859
860 #[test]
863 fn context_amortises_index_build() {
864 let doc = parse("<r><book/><book/><book/></r>");
865 let ctx = XPathContext::new(&doc);
866 assert_eq!(ctx.eval_count("/r/book").unwrap(), 3);
867 assert_eq!(ctx.eval_count("//book").unwrap(), 3);
868 assert!(ctx.eval_bool("/r").unwrap());
869 }
870
871 fn eval31(src: &str) -> String {
874 let doc = parse("<r/>");
875 let opts = XPathOptions { xpath_2_0: true, ..XPathOptions::default() };
876 let ctx = XPathContext::new_with(&doc, opts);
877 let bind = TestNs(&[
881 ("map", "http://www.w3.org/2005/xpath-functions/map"),
882 ("array", "http://www.w3.org/2005/xpath-functions/array"),
883 ("math", "http://www.w3.org/2005/xpath-functions/math"),
884 ]);
885 eval::value_to_string(&ctx.eval_with(src, 0, &bind).expect("eval"), &ctx.index)
886 }
887
888 #[test]
889 fn inline_function_and_dynamic_call() {
890 assert_eq!(eval31("let $f := function($x) { $x * 2 } return $f(21)"), "42");
891 }
892
893 #[test]
894 fn inline_function_captures_closure() {
895 assert_eq!(
896 eval31("let $n := 10 return (function($x) { $x + $n })(5)"),
897 "15");
898 }
899
900 #[test]
901 fn let_single_and_chained_bindings() {
902 assert_eq!(eval31("let $x := 40 return $x + 2"), "42");
903 assert_eq!(eval31("let $x := 3, $y := $x * 4 return $x + $y"), "15");
905 }
906
907 #[test]
908 fn let_binds_whole_sequence() {
909 assert_eq!(eval31("let $s := (1, 2, 3, 4) return count($s)"), "4");
911 assert_eq!(eval31("let $s := 1 to 5 return sum($s)"), "15");
912 }
913
914 #[test]
915 fn let_nested_in_for() {
916 assert_eq!(
917 eval31("string-join(for $i in 1 to 3 return \
918 let $sq := $i * $i return string($sq), ' ')"),
919 "1 4 9");
920 }
921
922 #[test]
923 fn parse_json_objects_arrays_scalars() {
924 assert_eq!(eval31("parse-json('[1,2,3]')?2"), "2");
925 assert_eq!(eval31("parse-json('{\"a\":10,\"b\":20}')?b"), "20");
926 assert_eq!(eval31("parse-json('{\"a\":[5,6,7]}')?a?3"), "7");
927 assert_eq!(eval31("parse-json('\"hi\\u0041\"')"), "hiA");
928 assert_eq!(eval31("parse-json('true')"), "true");
929 assert_eq!(eval31("count(parse-json('null'))"), "0");
931 assert_eq!(eval31("parse-json('{\"x\":null}')?x => count()"), "0");
932 }
933
934 #[test]
935 fn parse_json_duplicate_key_policies() {
936 assert_eq!(eval31("parse-json('{\"k\":1,\"k\":2}')?k"), "1");
938 assert_eq!(
939 eval31("parse-json('{\"k\":1,\"k\":2}', map{'duplicates':'use-last'})?k"),
940 "2");
941 }
942
943 #[test]
944 fn xml_to_json_round_trips_vocabulary() {
945 let doc = parse_ns(concat!(
946 r#"<map xmlns="http://www.w3.org/2005/xpath-functions">"#,
947 r#"<string key="a">hi</string>"#,
948 r#"<number key="n">42</number>"#,
949 r#"<boolean key="b">true</boolean>"#,
950 r#"<array key="xs"><number>1</number><number>2</number></array>"#,
951 r#"<null key="z"/>"#,
952 r#"</map>"#));
953 let opts = XPathOptions { xpath_2_0: true, ..XPathOptions::default() };
954 let ctx = XPathContext::new_with(&doc, opts);
955 let got = ctx.eval_str("xml-to-json(/)").unwrap();
956 assert_eq!(got, r#"{"a":"hi","n":42,"b":true,"xs":[1,2],"z":null}"#);
957 }
958
959 #[test]
960 fn let_rejected_under_xpath_1_0() {
961 let doc = parse("<r/>");
967 let ctx = XPathContext::new(&doc); assert!(
969 ctx.eval("let $x := 1 return $x").is_err(),
970 "`let` must not parse under XPath 1.0",
971 );
972 assert_eq!(eval31("let $x := 1 return $x"), "1");
974 }
975
976 #[test]
977 fn named_function_reference_via_for_each() {
978 assert_eq!(
979 eval31("string-join(for-each(('a','bb','ccc'), string-length#1), ',')"),
980 "1,2,3");
981 }
982
983 #[test]
984 fn fn_for_each_and_filter() {
985 assert_eq!(
986 eval31("string-join(for-each(1 to 4, function($x){ $x * $x }), ' ')"),
987 "1 4 9 16");
988 assert_eq!(
989 eval31("string-join(filter(1 to 6, function($x){ $x mod 2 = 0 }), ' ')"),
990 "2 4 6");
991 }
992
993 #[test]
994 fn fn_fold_left_right() {
995 assert_eq!(
996 eval31("fold-left(1 to 5, 0, function($a,$b){ $a + $b })"),
997 "15");
998 assert_eq!(
999 eval31("fold-right(('a','b','c'), '', function($x,$a){ concat($x,$a) })"),
1000 "abc");
1001 }
1002
1003 #[test]
1004 fn map_for_each_higher_order() {
1005 assert_eq!(
1007 eval31("sum(map:for-each(map{'a':1,'b':2,'c':3}, function($k,$v){ $v }))"),
1008 "6");
1009 }
1010
1011 #[test]
1012 fn array_for_each_and_fold() {
1013 assert_eq!(
1014 eval31("array:fold-left([1,2,3,4], 0, function($a,$b){ $a + $b })"),
1015 "10");
1016 assert_eq!(
1017 eval31("array:size(array:for-each([1,2,3], function($x){ $x * $x }))"),
1018 "3");
1019 }
1020
1021 #[test]
1022 fn fn_apply_and_arity() {
1023 assert_eq!(eval31("function-arity(function($a,$b){ $a }) "), "2");
1024 assert_eq!(eval31("apply(concat#3, ['a','b','c'])"), "abc");
1025 }
1026
1027 #[test]
1028 fn array_put_insert_remove() {
1029 assert_eq!(eval31("array:size(array:put([1,2,3], 2, 9))"), "3");
1031 assert_eq!(eval31("array:get(array:put([1,2,3], 2, 9), 2)"), "9");
1032 assert_eq!(eval31("array:size(array:insert-before([1,2,3], 2, 9))"), "4");
1034 assert_eq!(eval31("array:get(array:insert-before([1,2,3], 2, 9), 2)"), "9");
1035 assert_eq!(eval31("array:get(array:insert-before([1,2,3], 4, 9), 4)"), "9");
1036 assert_eq!(eval31("array:size(array:remove([1,2,3,4], (2,4)))"), "2");
1038 assert_eq!(eval31("array:get(array:remove([1,2,3,4], (2,4)), 2)"), "3");
1039 assert_eq!(eval31("array:size(array:remove([1,2,3], 2))"), "2");
1040 }
1041}