Skip to main content

sup_xml_core/xpath/
pattern.rs

1//! A small subset of XPath 1.0 sized for fast per-node matching.
2//!
3//! This is the engine behind libxml2's `xmlPattern` family — a stripped
4//! XPath dialect that the SAX/pull-parser side of libxml2 uses for
5//! questions like "does this node match `//book[@id]`?" during a
6//! traversal.  It's much faster than firing the full XPath engine
7//! per node, and the grammar covers most real-world streaming
8//! selectors.
9//!
10//! # Grammar
11//!
12//! ```text
13//! Pattern    := Branch ('|' Branch)*
14//! Branch     := '/'?  Step ('/' Step | '//' Step)*
15//! Step       := AxisStep Predicate*
16//! AxisStep   := '@' NCName
17//!             | '@' '*'
18//!             | NCName
19//!             | '*'
20//! Predicate  := '[' PredExpr ']'
21//! PredExpr   := Integer                     -- positional, 1-based
22//!             | 'last' '(' ')'              -- last position
23//!             | '@' NCName                  -- attribute exists
24//!             | '@' NCName '=' Literal      -- attribute equals
25//! Literal    := '"' …no " … '"' | "'" …no ' … "'"
26//! ```
27//!
28//! Not supported (intentional — fall outside libxml2's pattern subset):
29//! XPath function calls in predicates beyond `last()`, other axes
30//! (`parent::`, `ancestor::`, …), numeric expressions, variable
31//! references.  Patterns that use them won't compile.
32//!
33//! # Two evaluation modes
34//!
35//! - **Backward walk**: [`Pattern::matches`] — given a node with parent
36//!   pointers, walk upward through ancestors checking each step.
37//!   Random access; works on any DOM tree.
38//! - **Forward streaming**: [`Pattern::streaming`] — TODO.  An NFA-shaped
39//!   state machine that tracks pattern match progress as SAX-style
40//!   events flow past, without needing parent pointers.
41
42use sup_xml_tree::dom::{Node, NodeKind};
43
44/// A compiled libxml2-flavour pattern.  Match nodes via
45/// [`Self::matches`].
46#[derive(Debug, Clone)]
47pub struct Pattern {
48    /// One or more `|`-separated branches.  A node matches the pattern
49    /// iff it matches at least one branch.
50    branches: Vec<Branch>,
51}
52
53#[derive(Debug, Clone)]
54struct Branch {
55    /// Steps in right-to-left order: `steps[0]` matches the candidate
56    /// node; `steps[i]` (for `i > 0`) matches the ancestor reached
57    /// from `steps[i-1]` via `links[i-1]`.
58    steps: Vec<Step>,
59    /// Length is `steps.len() - 1`.  `links[i]` says how step `i`
60    /// connects to step `i+1` (one parent up, or any ancestor up).
61    links: Vec<Link>,
62    /// True iff the branch begins with `/`.  Anchored to the document
63    /// root — after consuming every step, the cursor must be the
64    /// document node.
65    absolute: bool,
66}
67
68#[derive(Debug, Clone)]
69struct Step {
70    test:       Test,
71    predicates: Vec<Predicate>,
72}
73
74#[derive(Debug, Clone)]
75enum Test {
76    /// `foo` — element name match.
77    Element(String),
78    /// `*` — any element.
79    AnyElement,
80    /// `@foo` — attribute name match.  Step targets an attribute node.
81    Attribute(String),
82    /// `@*` — any attribute.
83    AnyAttribute,
84}
85
86#[derive(Debug, Clone)]
87enum Predicate {
88    /// `[N]` — 1-based positional, evaluated against same-name siblings.
89    Position(usize),
90    /// `[last()]` — last among same-name siblings.
91    Last,
92    /// `[@foo]` — attribute exists.
93    AttrExists(String),
94    /// `[@foo='x']` or `[@foo="x"]` — attribute equals literal.
95    AttrEquals(String, String),
96}
97
98#[derive(Debug, Clone, Copy)]
99enum Link {
100    /// `/` — the next step must match the immediate parent.
101    Parent,
102    /// `//` — the next step must match some ancestor (any distance).
103    Ancestor,
104}
105
106impl Pattern {
107    /// Compile a pattern source string into a matcher.
108    ///
109    /// Returns `Err` on syntax errors or grammar features outside the
110    /// libxml2 pattern subset.
111    pub fn compile(src: &str) -> Result<Self, String> {
112        let mut p = Parser::new(src);
113        let pat = p.parse_pattern()?;
114        p.skip_ws();
115        if !p.at_eof() {
116            return Err(format!("unexpected trailing input near {:?}", &p.src[p.pos..]));
117        }
118        Ok(pat)
119    }
120
121    /// Does `node` match this pattern?  Walks upward via parent
122    /// pointers, so the node must be attached to a tree.
123    pub fn matches(&self, node: &Node<'_>) -> bool {
124        self.branches.iter().any(|b| b.matches(node))
125    }
126
127    /// Like [`matches`](Self::matches), but for an attribute candidate.
128    ///
129    /// Attributes are a distinct node type in this tree, so they cannot be
130    /// reinterpreted as an `&Node` to feed [`matches`](Self::matches) — the
131    /// two have different layouts.  The caller supplies the attribute's
132    /// name and owner element; the leftmost (`@name` / `@*`) step is
133    /// matched against the name and the rest of the pattern against the
134    /// owner-element ancestor chain.
135    pub fn matches_attribute(&self, attr_name: &str, parent: Option<&Node<'_>>) -> bool {
136        self.branches.iter().any(|b| b.matches_attribute(attr_name, parent))
137    }
138}
139
140// ── matching ────────────────────────────────────────────────────────────
141
142impl Branch {
143    fn matches(&self, node: &Node<'_>) -> bool {
144        self.run_from(0, node)
145    }
146
147    /// Match an attribute candidate: step 0 (which must be an attribute
148    /// test) against `attr_name`, then the remaining steps against the
149    /// owner-element chain rooted at `parent`.
150    fn matches_attribute(&self, attr_name: &str, parent: Option<&Node<'_>>) -> bool {
151        let Some(step0) = self.steps.first() else { return false; };
152        let test_ok = match &step0.test {
153            Test::Attribute(name) => name.as_str() == attr_name,
154            Test::AnyAttribute    => true,
155            _                     => false,
156        };
157        if !test_ok {
158            return false;
159        }
160        // A predicate on the attribute step would need the attribute's own
161        // node view, which this safe path deliberately doesn't form;
162        // decline rather than evaluate it against a partial picture.
163        if !step0.predicates.is_empty() {
164            return false;
165        }
166        match self.links.first() {
167            // A lone `@foo` / `@*` step matches unless anchored at the
168            // document root, which an element-owned attribute never is.
169            None => !self.absolute,
170            // `…/@foo`: the immediate parent must match the next step.
171            Some(Link::Parent) => match parent {
172                Some(p) => self.run_from(1, p),
173                None    => false,
174            },
175            // `…//@foo`: some ancestor must match the next step.
176            Some(Link::Ancestor) => {
177                let (Some(next_step), Some(p)) = (self.steps.get(1), parent) else {
178                    return false;
179                };
180                let mut up = Some(p);
181                while let Some(anc) = up {
182                    if next_step.test.matches(anc) && predicates_hold(&next_step.predicates, anc) {
183                        return self.run_from(1, anc);
184                    }
185                    up = anc.parent.get();
186                }
187                false
188            }
189        }
190    }
191
192    /// Run the pattern's steps `[start..]` against `start_cursor` and its
193    /// ancestor chain.  `matches` enters at step 0; the attribute path
194    /// enters at step 1 after handling the attribute step itself.
195    fn run_from(&self, start: usize, start_cursor: &Node<'_>) -> bool {
196        let mut cursor = start_cursor;
197
198        for i in start..self.steps.len() {
199            let step = &self.steps[i];
200            // Step's test applies to whatever the cursor currently points at.
201            if !step.test.matches(cursor) {
202                return false;
203            }
204            if !predicates_hold(&step.predicates, cursor) {
205                return false;
206            }
207
208            // After the rightmost step there's no further link.
209            let Some(link) = self.links.get(i) else { break; };
210
211            cursor = match link {
212                Link::Parent => match cursor.parent.get() {
213                    Some(p) => p,
214                    None    => return false,
215                },
216                Link::Ancestor => {
217                    // We need an ancestor that satisfies the NEXT
218                    // step's test+predicates.  Walk upward until we
219                    // find one (or run out of ancestors).
220                    let Some(next_step) = self.steps.get(i + 1) else {
221                        return false;
222                    };
223                    let mut found = None;
224                    let mut up = cursor.parent.get();
225                    while let Some(anc) = up {
226                        if next_step.test.matches(anc) && predicates_hold(&next_step.predicates, anc) {
227                            found = Some(anc);
228                            break;
229                        }
230                        up = anc.parent.get();
231                    }
232                    match found {
233                        Some(a) => {
234                            // We already matched the test+predicates;
235                            // skip the test step in the next iteration.
236                            // Easiest way: short-circuit by overwriting
237                            // cursor and continuing.
238                            //
239                            // But the outer for loop will re-test in
240                            // the next iteration, which is correct AND
241                            // already true.  No duplicate work hazard.
242                            a
243                        }
244                        None => return false,
245                    }
246                }
247            };
248        }
249
250        if self.absolute {
251            // After matching the leftmost step, the cursor's parent
252            // must be the document root — i.e. the leftmost step is at
253            // depth 1.  Equivalently: cursor has no element parent.
254            match cursor.parent.get() {
255                None    => true,
256                Some(p) => matches!(p.kind, NodeKind::Document),
257            }
258        } else {
259            true
260        }
261    }
262}
263
264impl Test {
265    fn matches(&self, node: &Node<'_>) -> bool {
266        match self {
267            Test::Element(name) => {
268                matches!(node.kind, NodeKind::Element) && node.name() == name.as_str()
269            }
270            Test::AnyElement => matches!(node.kind, NodeKind::Element),
271            // Under the `c-abi` layout an `xmlAttr*` is passed as an
272            // `xmlNode*` carrying `NodeKind::Attribute` (offset-8 `type`),
273            // and `parent` points at the owning element — so an attribute
274            // step matches it directly and the parent-walk continues
275            // upward.  In the pure-Rust DOM attributes are never nodes, so
276            // these arms are simply never satisfied there.
277            Test::Attribute(name) => {
278                matches!(node.kind, NodeKind::Attribute) && node.name() == name.as_str()
279            }
280            Test::AnyAttribute => matches!(node.kind, NodeKind::Attribute),
281        }
282    }
283}
284
285fn predicates_hold(preds: &[Predicate], node: &Node<'_>) -> bool {
286    preds.iter().all(|p| predicate_holds(p, node))
287}
288
289fn predicate_holds(pred: &Predicate, node: &Node<'_>) -> bool {
290    match pred {
291        Predicate::Position(want) => sibling_position(node) == Some(*want),
292        Predicate::Last => {
293            let pos = sibling_position(node);
294            let cnt = sibling_count(node);
295            matches!((pos, cnt), (Some(p), Some(c)) if p == c)
296        }
297        Predicate::AttrExists(name) => node.attributes().any(|a| a.name() == name.as_str()),
298        Predicate::AttrEquals(name, val) => node
299            .attributes()
300            .any(|a| a.name() == name.as_str() && a.value() == val.as_str()),
301    }
302}
303
304/// 1-based position of `node` among siblings of the same kind+name.
305fn sibling_position(node: &Node<'_>) -> Option<usize> {
306    let parent = node.parent.get()?;
307    let target_name = node.name();
308    let mut idx = 0usize;
309    for sib in parent.children() {
310        if !matches!(sib.kind, NodeKind::Element) { continue; }
311        if sib.name() != target_name { continue; }
312        idx += 1;
313        if std::ptr::eq(sib as *const _, node as *const _) {
314            return Some(idx);
315        }
316    }
317    None
318}
319
320/// Count of same-name element siblings (inclusive of `node`).
321fn sibling_count(node: &Node<'_>) -> Option<usize> {
322    let parent = node.parent.get()?;
323    let target_name = node.name();
324    let mut n = 0usize;
325    for sib in parent.children() {
326        if !matches!(sib.kind, NodeKind::Element) { continue; }
327        if sib.name() != target_name { continue; }
328        n += 1;
329    }
330    Some(n)
331}
332
333// ── parsing ─────────────────────────────────────────────────────────────
334
335struct Parser<'a> {
336    src: &'a str,
337    pos: usize,
338}
339
340impl<'a> Parser<'a> {
341    fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
342
343    fn at_eof(&self) -> bool { self.pos >= self.src.len() }
344
345    fn peek(&self) -> Option<char> { self.src[self.pos..].chars().next() }
346
347    fn bump(&mut self) -> Option<char> {
348        let c = self.peek()?;
349        self.pos += c.len_utf8();
350        Some(c)
351    }
352
353    fn skip_ws(&mut self) {
354        while let Some(c) = self.peek() {
355            if c.is_ascii_whitespace() { self.bump(); } else { break; }
356        }
357    }
358
359    fn eat(&mut self, lit: &str) -> bool {
360        if self.src[self.pos..].starts_with(lit) {
361            self.pos += lit.len();
362            true
363        } else { false }
364    }
365
366    fn parse_pattern(&mut self) -> Result<Pattern, String> {
367        let mut branches = vec![self.parse_branch()?];
368        loop {
369            self.skip_ws();
370            if !self.eat("|") { break; }
371            branches.push(self.parse_branch()?);
372        }
373        Ok(Pattern { branches })
374    }
375
376    fn parse_branch(&mut self) -> Result<Branch, String> {
377        self.skip_ws();
378        let absolute_or_descendant_root = self.peek() == Some('/');
379        let mut leading_descendant = false;
380        if absolute_or_descendant_root {
381            self.bump();
382            if self.peek() == Some('/') {
383                self.bump();
384                leading_descendant = true;
385            }
386        }
387        // Source-order steps (leftmost first).
388        let mut steps_lr: Vec<Step> = vec![self.parse_step()?];
389        // Inter-step links, source-order.  links_lr[i] connects steps_lr[i] to steps_lr[i+1].
390        let mut links_lr: Vec<Link> = Vec::new();
391        loop {
392            self.skip_ws();
393            if !self.eat("/") {
394                break;
395            }
396            let link = if self.peek() == Some('/') { self.bump(); Link::Ancestor } else { Link::Parent };
397            links_lr.push(link);
398            steps_lr.push(self.parse_step()?);
399        }
400
401        // Reverse so steps[0] is the rightmost (matches candidate).
402        steps_lr.reverse();
403        let mut links: Vec<Link> = links_lr.into_iter().rev().collect();
404
405        // A leading `//` means the leftmost step is reached from the
406        // doc root via Ancestor, but since we're walking backward this
407        // is equivalent to "no constraint on what's above the leftmost
408        // step."  Drop the absolute flag in that case.
409        let absolute = if leading_descendant {
410            false
411        } else {
412            absolute_or_descendant_root
413        };
414
415        // Validate: any attribute step must be the rightmost (libxml2
416        // pattern grammar restriction — attributes can't have child
417        // steps below them).
418        if steps_lr.len() > 1 {
419            for s in &steps_lr[1..] {
420                if matches!(s.test, Test::Attribute(_) | Test::AnyAttribute) {
421                    return Err("attribute step must be the rightmost step".into());
422                }
423            }
424        }
425
426        // Belt-and-braces: trim trailing Link that has no destination
427        // (shouldn't happen if parser is correct).
428        links.truncate(steps_lr.len().saturating_sub(1));
429
430        Ok(Branch { steps: steps_lr, links, absolute })
431    }
432
433    fn parse_step(&mut self) -> Result<Step, String> {
434        self.skip_ws();
435        let test = if self.peek() == Some('@') {
436            self.bump();
437            if self.eat("*") { Test::AnyAttribute }
438            else {
439                let n = self.parse_ncname()?;
440                Test::Attribute(n)
441            }
442        } else if self.eat("*") {
443            Test::AnyElement
444        } else {
445            let n = self.parse_ncname()?;
446            Test::Element(n)
447        };
448        let mut predicates = Vec::new();
449        loop {
450            self.skip_ws();
451            if self.peek() != Some('[') { break; }
452            predicates.push(self.parse_predicate()?);
453        }
454        Ok(Step { test, predicates })
455    }
456
457    fn parse_ncname(&mut self) -> Result<String, String> {
458        // Accepts NCName (ASCII-friendly form for the libxml2 pattern
459        // subset).  Permissive: letter | '_' to start, then word chars
460        // plus '-'.  Also accept `prefix:local` (treated as a single
461        // name for matching).
462        let start = self.pos;
463        let first = match self.peek() {
464            Some(c) if c.is_ascii_alphabetic() || c == '_' => c,
465            _ => return Err(format!("expected NCName at offset {}", self.pos)),
466        };
467        self.bump();
468        let _ = first;
469        while let Some(c) = self.peek() {
470            if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':' {
471                self.bump();
472            } else { break; }
473        }
474        Ok(self.src[start..self.pos].to_string())
475    }
476
477    fn parse_predicate(&mut self) -> Result<Predicate, String> {
478        if !self.eat("[") { return Err("expected '['".into()); }
479        self.skip_ws();
480        let pred = if self.peek().is_some_and(|c| c.is_ascii_digit()) {
481            let start = self.pos;
482            while let Some(c) = self.peek() {
483                if c.is_ascii_digit() { self.bump(); } else { break; }
484            }
485            let n: usize = self.src[start..self.pos].parse().map_err(|e| format!("bad position: {e}"))?;
486            Predicate::Position(n)
487        } else if self.eat("last") {
488            self.skip_ws();
489            if !self.eat("(") || { self.skip_ws(); !self.eat(")") } {
490                return Err("expected 'last()'".into());
491            }
492            Predicate::Last
493        } else if self.peek() == Some('@') {
494            self.bump();
495            let name = self.parse_ncname()?;
496            self.skip_ws();
497            if self.eat("=") {
498                self.skip_ws();
499                let val = self.parse_string_literal()?;
500                Predicate::AttrEquals(name, val)
501            } else {
502                Predicate::AttrExists(name)
503            }
504        } else {
505            return Err(format!("unsupported predicate at offset {}", self.pos));
506        };
507        self.skip_ws();
508        if !self.eat("]") { return Err("expected ']'".into()); }
509        Ok(pred)
510    }
511
512    fn parse_string_literal(&mut self) -> Result<String, String> {
513        let q = self.bump().ok_or("expected quoted string")?;
514        if q != '"' && q != '\'' {
515            return Err(format!("expected quote, got {q:?}"));
516        }
517        let start = self.pos;
518        while let Some(c) = self.peek() {
519            if c == q { break; }
520            self.bump();
521        }
522        let s = self.src[start..self.pos].to_string();
523        if !self.eat(&q.to_string()) {
524            return Err("unterminated string literal".into());
525        }
526        Ok(s)
527    }
528}
529
530// ── tests ───────────────────────────────────────────────────────────────
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::{parse_str, ParseOptions};
536
537    fn parse(s: &str) -> sup_xml_tree::dom::Document {
538        parse_str(s, &ParseOptions::default()).unwrap()
539    }
540
541    fn root<'a>(d: &'a sup_xml_tree::dom::Document) -> &'a Node<'a> {
542        d.root()
543    }
544
545    fn nth_child<'a>(parent: &'a Node<'a>, i: usize) -> &'a Node<'a> {
546        parent.children().nth(i).unwrap()
547    }
548
549    #[test]
550    fn compile_basic_shapes() {
551        for src in [
552            "foo",
553            "*",
554            "@id",
555            "@*",
556            "foo/bar",
557            "foo//bar",
558            "//book",
559            "/catalog/book",
560            "foo[1]",
561            "foo[@id]",
562            "foo[@id='x']",
563            "foo[last()]",
564            "a | b | c",
565        ] {
566            Pattern::compile(src).unwrap_or_else(|e| panic!("failed on {src:?}: {e}"));
567        }
568    }
569
570    #[test]
571    fn rejects_unsupported() {
572        for src in ["foo[contains(@id,'x')]", "parent::*", "..", "foo[bar=1]"] {
573            assert!(Pattern::compile(src).is_err(), "should reject {src:?}");
574        }
575    }
576
577    #[test]
578    fn match_simple_element() {
579        let d = parse("<catalog><book/></catalog>");
580        let book = nth_child(root(&d), 0);
581        assert!(Pattern::compile("book").unwrap().matches(book));
582        assert!(!Pattern::compile("catalog").unwrap().matches(book));
583    }
584
585    #[test]
586    fn match_child_chain() {
587        let d = parse("<catalog><book/></catalog>");
588        let book = nth_child(root(&d), 0);
589        assert!(Pattern::compile("catalog/book").unwrap().matches(book));
590        assert!(!Pattern::compile("library/book").unwrap().matches(book));
591    }
592
593    #[test]
594    fn match_descendant() {
595        let d = parse("<r><a><b><c/></b></a></r>");
596        let c = nth_child(nth_child(nth_child(root(&d), 0), 0), 0);
597        assert!(Pattern::compile("//c").unwrap().matches(c));
598        assert!(Pattern::compile("r//c").unwrap().matches(c));
599        assert!(Pattern::compile("a//c").unwrap().matches(c));
600        assert!(!Pattern::compile("a/c").unwrap().matches(c));
601    }
602
603    #[test]
604    fn match_wildcard() {
605        let d = parse("<r><a/></r>");
606        let a = nth_child(root(&d), 0);
607        assert!(Pattern::compile("*").unwrap().matches(a));
608        assert!(Pattern::compile("r/*").unwrap().matches(a));
609    }
610
611    #[test]
612    fn match_absolute() {
613        let d = parse("<r><a/></r>");
614        let r = root(&d);
615        let a = nth_child(r, 0);
616        assert!(Pattern::compile("/r").unwrap().matches(r));
617        assert!(Pattern::compile("/r/a").unwrap().matches(a));
618        // Absolute path: candidate's ancestry must reach the doc root with
619        // no extra ancestors above the leftmost step.
620        assert!(!Pattern::compile("/a").unwrap().matches(a));
621    }
622
623    #[test]
624    fn match_position_predicate() {
625        let d = parse("<catalog><book/><book/><book/></catalog>");
626        let books: Vec<_> = root(&d).children().collect();
627        assert!(Pattern::compile("book[1]").unwrap().matches(books[0]));
628        assert!(!Pattern::compile("book[1]").unwrap().matches(books[1]));
629        assert!(Pattern::compile("book[2]").unwrap().matches(books[1]));
630        assert!(Pattern::compile("book[last()]").unwrap().matches(books[2]));
631        assert!(!Pattern::compile("book[last()]").unwrap().matches(books[0]));
632    }
633
634    #[test]
635    fn match_attr_predicate() {
636        let d = parse(r#"<catalog><book id="b1"/><book/></catalog>"#);
637        let books: Vec<_> = root(&d).children().collect();
638        assert!(Pattern::compile("book[@id]").unwrap().matches(books[0]));
639        assert!(!Pattern::compile("book[@id]").unwrap().matches(books[1]));
640        assert!(Pattern::compile(r#"book[@id="b1"]"#).unwrap().matches(books[0]));
641        assert!(!Pattern::compile(r#"book[@id="b2"]"#).unwrap().matches(books[0]));
642    }
643
644    #[test]
645    fn match_union() {
646        let d = parse("<r><a/><b/></r>");
647        let a = nth_child(root(&d), 0);
648        let b = nth_child(root(&d), 1);
649        let p = Pattern::compile("a | b").unwrap();
650        assert!(p.matches(a));
651        assert!(p.matches(b));
652        let p2 = Pattern::compile("a | c").unwrap();
653        assert!(p2.matches(a));
654        assert!(!p2.matches(b));
655    }
656}