libxml_rs/xslt/patterns/mod.rs
1//! XSLT pattern matching (§33, §85 Phase 8).
2//!
3//! XSLT patterns are a subset of XPath 1.0 used in `match` attributes of
4//! `<xsl:template>`, `<xsl:key>`, `<xsl:strip-space>`, `<xsl:preserve-space>`.
5//!
6//! Patterns can include:
7//! - Location paths (relative and absolute)
8//! - Union patterns (pattern1 | pattern2)
9//! - Node tests: *, node(), text(), comment(), processing-instruction()
10//! - Attribute axis (@attr)
11//! - Namespace prefix tests (ns:*, ns:name)
12//! - Predicates (limited)
13//! - The id() and key() functions
14//!
15//! # UPSTREAM-PARITY
16//!
17//! Implements XSLT 1.0 §5 "Patterns" with match semantics matching libxslt 1.1.45.
18//! Pattern matching uses the XPath 1.0 AST from `src/xml/xpath` for parsing, then
19//! applies XSLT-specific matching rules.
20//!
21//! # Courts
22//!
23//! XSLT-PATTERNS-*
24//!
25//! # Upstream contract
26//!
27//! Parity target: upstream libxslt `patterns.c` (1.1.45;
28//! `SRC-LIBXSLT-1.1.42-PATTERNS-C` under oracle/historical/src). Patterns
29//! are the XSLT 1.0 §5 subset of XPath 1.0 used in `match` attributes of
30//! `xsl:template`, `xsl:key`, `xsl:strip-space` and `xsl:preserve-space`;
31//! the observable surface is `xsltCompilePattern`, `xsltTestPattern` and
32//! the default-priority computation used by template matching.
33//!
34//! # Conceptual behavior
35//!
36//! Patterns parse through the XPath 1.0 AST (`src/xml/xpath`) and are
37//! compiled into reverse-ordered step chains (child/attribute/descendant
38//! axes, node tests, bounded predicates, union branches, absolute-root
39//! sentinel). Matching walks the steps against the candidate node; a bare
40//! `node()`-style node test becomes a child-axis step and priority is
41//! derived from the pattern AST per XSLT 1.0 §5.5 default priorities
42//! (0.5 for node(), 0 for name tests, -0.5 for *-tests).
43//!
44//! # Ownership & safety invariants
45//!
46//! Compiled patterns are heap-allocated and owned by their callers: a
47//! template (`_xsltTemplate.match` slot, see templates module), a key
48//! definition, or the local match cache. `xsltFreePattern` releases the
49//! step chain and its predicate expressions; the pattern string itself is
50//! borrowed from the stylesheet document and never freed by the pattern
51//! free path (R-000103 lesson).
52//!
53//! # Historical quirks & epochs
54//!
55//! R-000105 (Phase 8): bare `node()`/`text()`/`comment()`/
56//! `processing-instruction()` parse as XPath FunctionCall nodes and were
57//! treated as unknown — fixed by translating them into child-axis steps
58//! with the upstream priorities (-0.25 for node(), 0.0 for the others).
59//! R-000106: `match="/"` is a bare Self_/node() step that matched every
60//! node — fixed so an empty absolute pattern matches only document nodes.
61//! E-008 (atlas/SEMANTIC_EPOCHS.md): pattern evaluation is frozen in the
62//! byte-identical xsltproc epoch (1.1.26, 2009, through 1.1.45).
63//!
64//! # Deliberate oddities
65//!
66//! - Steps are stored in reverse order for efficient matching — an
67//! intentional internal representation with identical semantics.
68//! - The compiled pattern is carried in the `_xsltTemplate.params` slot
69//! (opaque to the C ABI; R-000140 layout), a documented reuse.
70//!
71//! # Proving courts
72//!
73//! XSLT-PATTERNS-*, pattern priority and compile tests, CLI-XSLTPROC
74//! (match/priority corpus), and the in-crate `cargo test` suites.
75//!
76//! # Tempting simplifications that would break parity
77//!
78//! - Treating bare node tests as function calls (the pre-R-000105
79//! behavior) makes every `match="node()"`-style pattern a 0.5-priority
80//! unknown — no template ever matches.
81//! - Implementing `match="/"` as match-any (the pre-R-000106 behavior)
82//! makes the root template also match the root element, changing which
83//! template wins on the root node.
84//! - Dropping the default-priority mapping (XSLT 1.0 §5.5) breaks
85//! conflict resolution between overlapping patterns.
86
87use crate::abi::structs::*;
88use crate::abi::types::*;
89use crate::xml::string::xmlstr_to_string;
90use crate::xml::xpath::ast::{Axis, Expr, NameTest, NodeTest, Step};
91use crate::xml::xpath::parser::parse_xpath;
92use crate::xml::xpath::types::{NodeSet, XPathValue};
93use std::os::raw::c_int;
94use std::ptr;
95
96/// Sentinel for "no explicit priority" (upstream XSLT_PAT_NO_PRIORITY).
97pub const XSLT_PAT_NO_PRIORITY: f64 = -1.0e9;
98
99// ═══════════════════════════════════════════════════════════════════════════════
100// Internal Pattern Representation
101// ═══════════════════════════════════════════════════════════════════════════════
102
103/// A compiled pattern step — a single axis::node-test[predicates] in a pattern.
104///
105/// This is the internal representation backing `_xsltPatternStep`.
106/// The C ABI type `_xsltPatternStep` is a zero-sized opaque marker;
107/// the real data lives here.
108#[derive(Debug, Clone)]
109pub(crate) struct XsltPatternStep {
110 /// The axis (defaults to Child for element tests, Attribute for @attr).
111 pub axis: Axis,
112 /// The node test.
113 pub node_test: NodeTest,
114 /// Compiled predicate expressions.
115 pub predicates: Vec<Expr>,
116}
117
118/// A single pattern within a union pattern (one side of `|`).
119#[derive(Debug, Clone)]
120pub(crate) struct XsltPattern {
121 /// Steps in this pattern, in reverse order for efficient matching.
122 /// For a pattern like `foo/bar/baz`, steps are [baz, bar, foo].
123 /// For a pattern like `foo//bar`, steps contain a DescendantOrSelf sentinel.
124 pub steps: Vec<PatternStepEntry>,
125 /// Whether this pattern is absolute (starts with `/`).
126 pub is_absolute: bool,
127 /// The original pattern string for this branch.
128 #[allow(dead_code)]
129 pub original: String,
130 /// The parsed XPath expression (kept for predicate evaluation).
131 #[allow(dead_code)]
132 pub expr: Expr,
133}
134
135/// One entry in the step chain of a compiled pattern.
136#[derive(Debug, Clone)]
137pub(crate) enum PatternStepEntry {
138 /// A normal step with axis, node test, and predicates.
139 Step(XsltPatternStep),
140 /// A `//` separator — matches descendant-or-self::node().
141 #[allow(dead_code)]
142 DescendantOrSelf,
143}
144
145/// Full compiled pattern (union of multiple sub-patterns).
146#[derive(Debug, Clone)]
147pub(crate) struct CompiledPattern {
148 /// The branches of a union pattern (`pattern1 | pattern2`).
149 pub patterns: Vec<XsltPattern>,
150}
151
152// ═══════════════════════════════════════════════════════════════════════════════
153// C ABI Opaque Types
154// ═══════════════════════════════════════════════════════════════════════════════
155
156/// Opaque pattern structure.
157///
158/// In the C ABI this is a `typedef struct _xsltPattern xsltPattern`.
159/// The actual data is stored in the internal `CompiledPattern` and accessed
160/// via pointer casts in the implementation functions.
161#[derive(Debug)]
162#[repr(C)]
163pub struct _xsltPattern {
164 _unused: [u8; 0],
165}
166
167/// Opaque pattern step structure.
168#[derive(Debug)]
169#[repr(C)]
170pub struct _xsltPatternStep {
171 _unused: [u8; 0],
172}
173
174// ═══════════════════════════════════════════════════════════════════════════════
175// Pattern Compilation
176// ═══════════════════════════════════════════════════════════════════════════════
177
178/// Compile an XSLT pattern string into a compiled pattern.
179///
180/// Parses the pattern string using the XPath 1.0 parser, then decomposes
181/// it into an internal representation suitable for fast node matching.
182///
183/// # Parameters
184///
185/// * `pattern` — The pattern string (UTF-8, null-terminated `xmlChar*`).
186/// * `doc` — The document (used for namespace resolution; may be null).
187///
188/// # Returns
189///
190/// A pointer to a compiled `_xsltPattern`, or null on failure.
191///
192/// # Safety
193///
194/// `pattern` must be a valid null-terminated `xmlChar*` or null.
195/// `doc` must be a valid `_xmlDoc*` or null.
196pub unsafe fn xsltCompilePattern(pattern: *const xmlChar, _doc: *mut _xmlDoc) -> *mut _xsltPattern {
197 if pattern.is_null() {
198 return ptr::null_mut();
199 }
200
201 let pattern_str = xmlstr_to_string(pattern);
202 if pattern_str.is_empty() {
203 return ptr::null_mut();
204 }
205
206 let compiled = match compile_pattern_string(&pattern_str) {
207 Some(cp) => cp,
208 None => return ptr::null_mut(),
209 };
210
211 // Allocate and store the compiled pattern
212 let layout = std::alloc::Layout::new::<CompiledPattern>();
213 let ptr = std::alloc::alloc(layout) as *mut CompiledPattern;
214 if ptr.is_null() {
215 return ptr::null_mut();
216 }
217 ptr::write(ptr, compiled);
218 ptr as *mut _xsltPattern
219}
220
221/// Internal: compile a pattern string into a `CompiledPattern`.
222fn compile_pattern_string(pattern_str: &str) -> Option<CompiledPattern> {
223 // Parse the pattern as an XPath expression
224 let expr = parse_xpath(pattern_str).ok()?;
225
226 // Decompose the expression into pattern branches
227 let patterns = decompose_pattern(&expr, pattern_str)?;
228
229 Some(CompiledPattern { patterns })
230}
231
232/// Decompose an XPath expression into a list of pattern branches.
233///
234/// A union pattern `a | b` becomes two branches. Each branch is converted
235/// into a sequence of steps for matching.
236fn decompose_pattern(expr: &Expr, original: &str) -> Option<Vec<XsltPattern>> {
237 match expr {
238 // Union: pattern1 | pattern2 — recurse on both sides
239 Expr::Union(left, right) => {
240 let mut patterns = decompose_pattern(left, original)?;
241 let right_patterns = decompose_pattern(right, original)?;
242 patterns.extend(right_patterns);
243 Some(patterns)
244 }
245 // Single expression — convert to a pattern
246 _ => {
247 let pattern = expr_to_pattern(expr, original)?;
248 Some(vec![pattern])
249 }
250 }
251}
252
253/// Convert a single (non-union) XPath expression into a `XsltPattern`.
254fn expr_to_pattern(expr: &Expr, original: &str) -> Option<XsltPattern> {
255 let (steps, is_absolute) = collect_steps(expr)?;
256
257 Some(XsltPattern {
258 steps,
259 is_absolute,
260 original: original.to_string(),
261 expr: expr.clone(),
262 })
263}
264
265/// Collect the steps from an XPath expression in reverse order.
266///
267/// Returns `(steps, is_absolute)` where steps are ordered from innermost
268/// (node being matched) to outermost (root), making matching efficient.
269fn collect_steps(expr: &Expr) -> Option<(Vec<PatternStepEntry>, bool)> {
270 match expr {
271 // "/" — a bare Self_/node() step represents the document root
272 // node pattern. It matches only the document node itself.
273 Expr::Step(step)
274 if step.axis == Axis::Self_
275 && step.node_test == NodeTest::Node
276 && step.predicates.is_empty() =>
277 {
278 Some((vec![], true))
279 }
280 Expr::Step(step) => {
281 let entry = PatternStepEntry::Step(XsltPatternStep {
282 axis: step.axis,
283 node_test: step.node_test.clone(),
284 predicates: step.predicates.clone(),
285 });
286 Some((vec![entry], false))
287 }
288 Expr::AbsolutePath(inner) => {
289 let (steps, _) = collect_steps(inner)?;
290 Some((steps, true))
291 }
292 Expr::RelativePath(left, right) => {
293 // Steps are collected right-to-left: the rightmost step is the
294 // node being tested, left steps are ancestry constraints.
295 let (mut right_steps, _) = collect_steps(right)?;
296 let (left_steps, left_absolute) = collect_steps(left)?;
297 right_steps.extend(left_steps);
298 Some((right_steps, left_absolute))
299 }
300 // Handle `//` — the parser represents it as RelativePath with
301 // a DescendantOrSelf step in the middle
302 Expr::Filter(_expr, _predicates) => {
303 // Filter expressions like `id('foo')/bar` — we match the filter
304 // as a step
305 // For now, treat as a single step with a wildcard node test
306 let entry = PatternStepEntry::Step(XsltPatternStep {
307 axis: Axis::Self_,
308 node_test: NodeTest::Node,
309 predicates: vec![],
310 });
311 Some((vec![entry], false))
312 }
313 // Bare node-test function calls: node(), text(), comment(),
314 // processing-instruction(). In XPath 1.0 these are NOT functions;
315 // they are node tests forming a step on the child axis. The XPath
316 // parser represents top-level `node()` as a FunctionCall, so we
317 // translate it back into a step here.
318 Expr::FunctionCall { name, args } => {
319 let node_test = match (name.as_str(), args.len()) {
320 ("node", 0) => Some(NodeTest::Node),
321 ("text", 0) => Some(NodeTest::Text),
322 ("comment", 0) => Some(NodeTest::Comment),
323 ("processing-instruction", 0) => Some(NodeTest::ProcessingInstruction(None)),
324 ("processing-instruction", 1) => match &args[0] {
325 Expr::StringLiteral(s) => {
326 Some(NodeTest::ProcessingInstruction(Some(s.clone())))
327 }
328 _ => None,
329 },
330 _ => None,
331 };
332 match node_test {
333 Some(nt) => {
334 let entry = PatternStepEntry::Step(XsltPatternStep {
335 axis: Axis::Child,
336 node_test: nt,
337 predicates: vec![],
338 });
339 Some((vec![entry], false))
340 }
341 // id() and key() are handled as filter-like patterns.
342 None if name == "id" || name == "key" => {
343 let entry = PatternStepEntry::Step(XsltPatternStep {
344 axis: Axis::Self_,
345 node_test: NodeTest::Node,
346 predicates: vec![],
347 });
348 Some((vec![entry], false))
349 }
350 None => None,
351 }
352 }
353 _ => {
354 // Literals, function calls without path — not valid as patterns
355 // unless they're id() or key() which are handled specially
356 None
357 }
358 }
359}
360
361// ═══════════════════════════════════════════════════════════════════════════════
362// Pattern Deallocation
363// ═══════════════════════════════════════════════════════════════════════════════
364
365/// Free a compiled pattern.
366///
367/// # Safety
368///
369/// `pattern` must have been returned by `xsltCompilePattern` and not already freed.
370pub unsafe fn xsltFreePattern(pattern: *mut _xsltPattern) {
371 if pattern.is_null() {
372 return;
373 }
374 let ptr = pattern as *mut CompiledPattern;
375 // Drop the compiled pattern
376 ptr::drop_in_place(ptr);
377 let layout = std::alloc::Layout::new::<CompiledPattern>();
378 std::alloc::dealloc(ptr as *mut u8, layout);
379}
380
381// ═══════════════════════════════════════════════════════════════════════════════
382// Pattern Matching
383// ═══════════════════════════════════════════════════════════════════════════════
384
385/// Test whether a node matches a compiled pattern.
386///
387/// # Parameters
388///
389/// * `ctxt` — The transform context (provides XPath context for predicates).
390/// * `pattern` — The compiled pattern.
391/// * `node` — The node to test.
392///
393/// # Returns
394///
395/// 1 if the node matches, 0 otherwise.
396///
397/// # Safety
398///
399/// All pointers must be valid (or null — null pointers return 0).
400pub unsafe fn xsltTestPattern(
401 ctxt: *mut _xsltTransformContext,
402 pattern: *mut _xsltPattern,
403 node: *mut _xmlNode,
404) -> c_int {
405 if pattern.is_null() || node.is_null() {
406 return 0;
407 }
408
409 let compiled = &*(pattern as *const CompiledPattern);
410 let xpath_ctxt = if !ctxt.is_null() {
411 (*ctxt).xpathCtxt
412 } else {
413 ptr::null_mut()
414 };
415
416 for sub_pattern in &compiled.patterns {
417 if match_sub_pattern(sub_pattern, node, xpath_ctxt) {
418 return 1;
419 }
420 }
421
422 0
423}
424
425/// Test whether a node matches a compiled pattern tree.
426///
427/// In libxslt, the `match` attribute is compiled into a tree of `_xmlNode`
428/// elements during stylesheet compilation. This function walks that tree
429/// to determine whether `node` matches the pattern.
430///
431/// The pattern tree structure uses element nodes where:
432/// - The node `name` encodes the step type (element name, "*", "node()", etc.)
433/// - Children represent path steps (inner to outer)
434/// - Siblings at the root level represent union alternatives
435///
436/// # Parameters
437///
438/// * `node` — The document node to test.
439/// * `pattern_node` — The compiled pattern tree root (from `_xsltTemplate.r#match`).
440///
441/// # Returns
442///
443/// `true` if the node matches, `false` otherwise.
444///
445/// # Safety
446///
447/// Both pointers must be valid (or null — null pointers return false).
448pub unsafe fn xsltTestMatchPattern(node: *mut _xmlNode, pattern_node: *mut _xmlNode) -> bool {
449 if node.is_null() || pattern_node.is_null() {
450 return false;
451 }
452
453 // Walk the pattern tree. The pattern tree has the following structure:
454 // - Root node: represents the outermost step (or union)
455 // - For union patterns: the root has sibling children
456 // - For path patterns: children represent nested steps
457 //
458 // The node's `name` encodes what kind of test this step performs:
459 // - A QName: match element with that name
460 // - "*": match any element
461 // - "node()": match any node
462 // - "text()": match text nodes
463 // - "comment()": match comment nodes
464 // - "processing-instruction()": match PI nodes
465 // - "@name": match attribute
466 // - "ns:*": match namespace wildcard
467 // - "|": union operator
468 // - "/": path separator
469
470 match_pattern_tree(pattern_node, node)
471}
472
473/// Walk the pattern tree and test if the node matches.
474unsafe fn match_pattern_tree(pattern_node: *mut _xmlNode, node: *mut _xmlNode) -> bool {
475 if pattern_node.is_null() || node.is_null() {
476 return false;
477 }
478
479 let node_ref = &*pattern_node;
480 let name = xmlstr_to_string(node_ref.name);
481
482 match name.as_str() {
483 // Union: any child matches
484 "|" => {
485 let mut child = node_ref.children;
486 while !child.is_null() {
487 if match_pattern_tree(child, node) {
488 return true;
489 }
490 child = (*child).next;
491 }
492 false
493 }
494 // Path separator: match child step, then parent step
495 "/" => {
496 // For a path like foo/bar:
497 // The root node is "/" with children [bar, foo] (inner first)
498 // We match the first child against node, then the second against node's parent
499 let steps = collect_children(pattern_node);
500 if steps.is_empty() {
501 return false;
502 }
503 match_pattern_path(&steps, node)
504 }
505 _ => {
506 // Leaf node: check if this step matches the node
507 match_pattern_step(pattern_node, node)
508 }
509 }
510}
511
512/// Collect all children of a pattern node into a vector.
513unsafe fn collect_children(pattern_node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
514 let mut children = Vec::new();
515 if pattern_node.is_null() {
516 return children;
517 }
518 let mut child = (*pattern_node).children;
519 while !child.is_null() {
520 children.push(child);
521 child = (*child).next;
522 }
523 children
524}
525
526/// Match a path (sequence of pattern steps) against a node.
527///
528/// Steps are ordered inner-first (the last step is the outermost).
529unsafe fn match_pattern_path(steps: &[*mut _xmlNode], node: *mut _xmlNode) -> bool {
530 if steps.is_empty() {
531 return false;
532 }
533
534 let mut current = node;
535
536 for (i, &step) in steps.iter().enumerate() {
537 if current.is_null() {
538 return false;
539 }
540
541 if !match_pattern_step(step, current) {
542 return false;
543 }
544
545 // Move to parent for the next step (if any)
546 if i < steps.len() - 1 {
547 current = (*current).parent;
548 }
549 }
550
551 true
552}
553
554/// Match a single pattern step node against a document node.
555unsafe fn match_pattern_step(pattern_node: *mut _xmlNode, node: *mut _xmlNode) -> bool {
556 if pattern_node.is_null() || node.is_null() {
557 return false;
558 }
559
560 let pn = &*pattern_node;
561 let nn = &*node;
562 let step_name = xmlstr_to_string(pn.name);
563 let node_name = xmlstr_to_string(nn.name);
564 let node_type = nn.type_;
565
566 match step_name.as_str() {
567 // Wildcard: match any element (or attribute if pattern is attribute-type)
568 "*" => {
569 if pn.type_ == 2 {
570 // Attribute wildcard
571 node_type == 2
572 } else {
573 // Element wildcard
574 node_type == 1
575 }
576 }
577 // node() — match any node type
578 "node()" => true,
579 // text() — match text nodes
580 "text()" => node_type == 3 || node_type == 4,
581 // comment() — match comment nodes
582 "comment()" => node_type == 8,
583 // processing-instruction() — match PI nodes
584 "processing-instruction()" => node_type == 7,
585 // Attribute test (@attr) — the step name starts with "@"
586 s if s.starts_with('@') => {
587 let attr_name = &s[1..];
588 node_type == 2 && node_name == attr_name
589 }
590 // Namespace wildcard (prefix:*)
591 s if s.ends_with(":*") => {
592 if node_type != 1 {
593 return false;
594 }
595 let prefix = &s[..s.len() - 2];
596 if let Some(ns) = nn.ns.as_ref() {
597 let ns_prefix = xmlstr_to_string(ns.prefix);
598 ns_prefix == prefix
599 } else {
600 prefix.is_empty()
601 }
602 }
603 // Namespace-qualified name (ns:local)
604 s if s.contains(':') && !s.starts_with('@') && !s.ends_with(":*") => {
605 if node_type != 1 {
606 return false;
607 }
608 let parts: Vec<&str> = s.splitn(2, ':').collect();
609 if parts.len() != 2 {
610 return false;
611 }
612 let prefix = parts[0];
613 let local = parts[1];
614 if node_name != local {
615 return false;
616 }
617 if let Some(ns) = nn.ns.as_ref() {
618 let ns_prefix = xmlstr_to_string(ns.prefix);
619 ns_prefix == prefix
620 } else {
621 prefix.is_empty()
622 }
623 }
624 // Simple name test: match element/attribute by name
625 _ => {
626 if node_type == 1 || node_type == 2 {
627 node_name == step_name
628 } else {
629 false
630 }
631 }
632 }
633}
634
635/// Check if a node matches a single (non-union) sub-pattern.
636unsafe fn match_sub_pattern(
637 pattern: &XsltPattern,
638 node: *mut _xmlNode,
639 xpath_ctxt: *mut _xmlXPathContext,
640) -> bool {
641 // The steps are stored in reverse order (innermost first).
642 // Walk them from the node upward through the tree.
643 if pattern.steps.is_empty() {
644 // Empty steps with is_absolute means the pattern is "/", which
645 // matches only the document root node.
646 return pattern.is_absolute && is_document_node(node);
647 }
648
649 // Start with the first step (the one closest to the matched node)
650 let mut current_node = node;
651
652 for (i, entry) in pattern.steps.iter().enumerate() {
653 match entry {
654 PatternStepEntry::Step(step) => {
655 if !match_step(step, current_node, xpath_ctxt, i == 0) {
656 return false;
657 }
658 // For the first step, we stay on the current node (Self_ axis)
659 // or traverse to children/parent depending on the axis.
660 // For subsequent steps, we need to move upward.
661 if i > 0 {
662 // Move to parent for the next step
663 current_node = (*current_node).parent;
664 if current_node.is_null() {
665 return false;
666 }
667 }
668 }
669 PatternStepEntry::DescendantOrSelf => {
670 // `//` matches descendant-or-self::node()
671 // This means any ancestor path is valid.
672 // Since steps are in reverse order, this separates
673 // the inner steps (already matched) from outer steps.
674 // We need to find an ancestor that matches the remaining steps.
675 let remaining: Vec<_> = pattern.steps[i + 1..]
676 .iter()
677 .filter_map(|e| {
678 if let PatternStepEntry::Step(s) = e {
679 Some(s.clone())
680 } else {
681 None
682 }
683 })
684 .collect();
685
686 if remaining.is_empty() {
687 return true;
688 }
689
690 // Try each ancestor
691 let mut ancestor = current_node;
692 loop {
693 ancestor = (*ancestor).parent;
694 if ancestor.is_null() {
695 return false;
696 }
697 if match_steps_sequence(&remaining, ancestor, xpath_ctxt) {
698 return true;
699 }
700 }
701 }
702 }
703 }
704
705 // If the pattern is absolute, the final ancestor must be the document root
706 if pattern.is_absolute {
707 // After walking up all steps, current_node should be the document node
708 if current_node.is_null() {
709 return true;
710 }
711 // Walk up to root
712 let mut n = node;
713 loop {
714 let parent = (*n).parent;
715 if parent.is_null() {
716 break;
717 }
718 n = parent;
719 }
720 // n is now the root — check if it's the document node
721 return (*n).type_ == 9 || (*n).type_ == 13; // XML_DOCUMENT_NODE or XML_HTML_DOCUMENT_NODE
722 }
723
724 true
725}
726
727/// Match a sequence of steps against a node, starting from the innermost step.
728unsafe fn match_steps_sequence(
729 steps: &[XsltPatternStep],
730 node: *mut _xmlNode,
731 xpath_ctxt: *mut _xmlXPathContext,
732) -> bool {
733 let mut current = node;
734 for (i, step) in steps.iter().enumerate() {
735 if !match_step(step, current, xpath_ctxt, i == 0) {
736 return false;
737 }
738 if i < steps.len() - 1 {
739 current = (*current).parent;
740 if current.is_null() {
741 return false;
742 }
743 }
744 }
745 true
746}
747
748/// Check if a node is a document node (XML_DOCUMENT_NODE or XML_HTML_DOCUMENT_NODE).
749unsafe fn is_document_node(node: *mut _xmlNode) -> bool {
750 if node.is_null() {
751 return false;
752 }
753 (*node).type_ == 9 || (*node).type_ == 13
754}
755
756/// Check if a single step matches a node.
757///
758/// A step `axis::node-test[predicates]` matches if:
759/// 1. The axis relationship holds between the context and the node.
760/// 2. The node test matches the node.
761/// 3. All predicates evaluate to true.
762unsafe fn match_step(
763 step: &XsltPatternStep,
764 node: *mut _xmlNode,
765 xpath_ctxt: *mut _xmlXPathContext,
766 _is_first: bool,
767) -> bool {
768 if node.is_null() {
769 return false;
770 }
771
772 let node_ref = &*node;
773 let node_type = node_ref.type_;
774
775 // Step 1: Check the axis
776 match step.axis {
777 Axis::Attribute => {
778 // Must be an attribute node
779 if node_type != 2 {
780 // XML_ATTRIBUTE_NODE
781 return false;
782 }
783 }
784 Axis::Child | Axis::Self_
785 // Child and Self axes match elements, text, comments, PIs
786 // (all non-document, non-attribute, non-namespace nodes)
787 if (node_type == 2 || node_type == 9 || node_type == 13)
788 // Skip attributes and document nodes for child:: tests
789 && step.axis == Axis::Child
790 && node_type != 1
791 && node_type != 3
792 && node_type != 4
793 && node_type != 7
794 && node_type != 8
795 => {
796 return false;
797 }
798 _ => {
799 // Other axes are not typically used in patterns
800 // For completeness, accept the node
801 }
802 }
803
804 // Step 2: Check the node test
805 if !match_node_test(node, &step.node_test) {
806 return false;
807 }
808
809 // Step 3: Evaluate predicates (if any)
810 if !step.predicates.is_empty() {
811 if xpath_ctxt.is_null() {
812 // Without an XPath context, we can't evaluate predicates
813 // For now, skip predicates (libxslt would also need a context)
814 return true;
815 }
816
817 if !evaluate_predicates(node, &step.predicates, xpath_ctxt) {
818 return false;
819 }
820 }
821
822 true
823}
824
825/// Check if a node matches a node test.
826unsafe fn match_node_test(node: *mut _xmlNode, node_test: &NodeTest) -> bool {
827 if node.is_null() {
828 return false;
829 }
830
831 let node_ref = &*node;
832 let node_type = node_ref.type_;
833
834 match node_test {
835 NodeTest::Node => {
836 // Matches any node
837 true
838 }
839 NodeTest::Text => {
840 // text() — text nodes (type 3) or CDATA sections (type 4)
841 node_type == 3 || node_type == 4
842 }
843 NodeTest::Comment => {
844 // comment() — comment nodes (type 8)
845 node_type == 8
846 }
847 NodeTest::ProcessingInstruction(target) => {
848 // processing-instruction() or processing-instruction("target")
849 if node_type != 7 {
850 // XML_PI_NODE
851 return false;
852 }
853 if let Some(target) = target {
854 let name = xmlstr_to_string(node_ref.name);
855 name == *target
856 } else {
857 true
858 }
859 }
860 NodeTest::NameTest(name_test) => match_name_test(node, name_test),
861 NodeTest::Wildcard => {
862 // * — matches any element node
863 node_type == 1
864 }
865 NodeTest::NsWildcard(prefix) => {
866 // prefix:* — matches any element in that namespace
867 if node_type != 1 {
868 return false;
869 }
870 if let Some(ns) = node_ref.ns.as_ref() {
871 let ns_prefix = xmlstr_to_string(ns.prefix);
872 ns_prefix == *prefix
873 } else {
874 prefix.is_empty()
875 }
876 }
877 }
878}
879
880/// Check if a node matches a name test.
881unsafe fn match_name_test(node: *mut _xmlNode, name_test: &NameTest) -> bool {
882 if node.is_null() {
883 return false;
884 }
885
886 let node_ref = &*node;
887
888 match name_test {
889 NameTest::Any => {
890 // * — matches any element or attribute
891 node_ref.type_ == 1 || node_ref.type_ == 2
892 }
893 NameTest::LocalName(local) => {
894 let name = xmlstr_to_string(node_ref.name);
895 name == *local
896 }
897 NameTest::QName { prefix, local } => {
898 let name = xmlstr_to_string(node_ref.name);
899 if name != *local {
900 return false;
901 }
902 // Check namespace prefix
903 if let Some(ns) = node_ref.ns.as_ref() {
904 let ns_prefix = xmlstr_to_string(ns.prefix);
905 ns_prefix == *prefix
906 } else {
907 prefix.is_empty()
908 }
909 }
910 }
911}
912
913/// Evaluate predicates for a node match.
914///
915/// Uses the XPath evaluation engine to check if all predicates hold.
916unsafe fn evaluate_predicates(
917 node: *mut _xmlNode,
918 predicates: &[Expr],
919 xpath_ctxt: *mut _xmlXPathContext,
920) -> bool {
921 if xpath_ctxt.is_null() {
922 return true; // Can't evaluate, assume match
923 }
924
925 // Set up a temporary XPath context for predicate evaluation
926 let ctxt = &mut *xpath_ctxt;
927
928 // Save context state
929 let saved_node = ctxt.node;
930
931 // Set the current node as context
932 ctxt.node = node;
933
934 let mut result = true;
935
936 for predicate in predicates {
937 // Evaluate the predicate expression
938 // Get the document from the context or the node
939 let doc = if !ctxt.doc.is_null() {
940 ctxt.doc
941 } else if !node.is_null() {
942 (*node).doc
943 } else {
944 ptr::null_mut()
945 };
946 let mut xpath_ctx = crate::xml::xpath::context::XPathContext::new(doc);
947
948 // Copy relevant state from the C ABI context
949 if !saved_node.is_null() {
950 xpath_ctx.set_context_node(saved_node);
951 }
952
953 // Copy namespace mappings
954 if !ctxt.namespaces.is_null() && ctxt.nsNr > 0 {
955 let ns_slice = std::slice::from_raw_parts(ctxt.namespaces, ctxt.nsNr as usize);
956 for ns_ptr in ns_slice {
957 if !ns_ptr.is_null() {
958 let ns = &**ns_ptr;
959 let prefix = xmlstr_to_string(ns.prefix);
960 let href = xmlstr_to_string(ns.href);
961 xpath_ctx.register_namespace(&prefix, &href);
962 }
963 }
964 }
965
966 // Register the id() and key() extension functions
967 register_pattern_functions(&mut xpath_ctx);
968
969 let pred_result = crate::xml::xpath::eval::eval(&mut xpath_ctx, predicate);
970
971 match pred_result {
972 Ok(val) => {
973 // Predicate semantics: number n matches if n == context position,
974 // otherwise boolean conversion
975 let matches = match val {
976 XPathValue::Number(n) => {
977 // Number predicate: match if n == 1 (first node)
978 // For pattern predicates, position is always 1
979 (n - 1.0).abs() < f64::EPSILON
980 }
981 _ => val.as_boolean(),
982 };
983 if !matches {
984 result = false;
985 break;
986 }
987 }
988 Err(_) => {
989 result = false;
990 break;
991 }
992 }
993 }
994
995 // Restore context
996 ctxt.node = saved_node;
997
998 result
999}
1000
1001/// Register XSLT-specific functions needed for pattern evaluation (id(), key()).
1002fn register_pattern_functions(ctx: &mut crate::xml::xpath::context::XPathContext) {
1003 // Register id() function
1004 ctx.register_function("id", |_ctx, _args| {
1005 // Simple id() implementation: returns empty node-set for now
1006 // A full implementation would look up IDs in the document's DTD
1007 Ok(XPathValue::NodeSet(NodeSet::new()))
1008 });
1009
1010 // Register key() function
1011 ctx.register_function("key", |_ctx, _args| {
1012 // Simple key() implementation: returns empty node-set for now
1013 // A full implementation would look up keys in the stylesheet's key tables
1014 Ok(XPathValue::NodeSet(NodeSet::new()))
1015 });
1016}
1017
1018// ═══════════════════════════════════════════════════════════════════════════════
1019// Default Priority Computation
1020// ═══════════════════════════════════════════════════════════════════════════════
1021
1022/// Compute default priority for a match pattern.
1023///
1024/// XSLT 1.0 §5.5:
1025/// - 0.0 for simple name tests (child::para, para)
1026/// - -0.25 for node() test
1027/// - -0.5 for any name test (*) or namespace test (ns:*)
1028/// - +0.5 for attribute axis (@attr)
1029/// - +0.0 for other cases (compound patterns, id(), key())
1030///
1031/// # Parameters
1032///
1033/// * `pattern` — The pattern string (UTF-8, null-terminated `xmlChar*`).
1034///
1035/// # Returns
1036///
1037/// The default priority as a f64.
1038///
1039/// # Safety
1040///
1041/// `pattern` must be a valid null-terminated `xmlChar*` or null.
1042pub unsafe fn xsltDefaultPriority(pattern: *const xmlChar) -> f64 {
1043 if pattern.is_null() {
1044 return 0.5;
1045 }
1046
1047 let pattern_str = xmlstr_to_string(pattern);
1048 if pattern_str.is_empty() {
1049 return 0.5;
1050 }
1051
1052 compute_default_priority(&pattern_str)
1053}
1054
1055/// Internal: compute default priority from a pattern string.
1056fn compute_default_priority(pattern_str: &str) -> f64 {
1057 // Parse the pattern
1058 let expr = match parse_xpath(pattern_str) {
1059 Ok(e) => e,
1060 Err(_) => return 0.5, // Default for unparseable patterns
1061 };
1062
1063 compute_expr_priority(&expr)
1064}
1065
1066/// Compute the default priority of an expression.
1067fn compute_expr_priority(expr: &Expr) -> f64 {
1068 match expr {
1069 // Union patterns: use the highest priority of any branch
1070 Expr::Union(left, right) => {
1071 let left_p = compute_expr_priority(left);
1072 let right_p = compute_expr_priority(right);
1073 left_p.max(right_p)
1074 }
1075
1076 // Absolute path: analyze the inner expression
1077 Expr::AbsolutePath(inner) => compute_expr_priority(inner),
1078
1079 // Relative path: priority is based on the final step (rightmost)
1080 Expr::RelativePath(_, right) => compute_expr_priority(right),
1081
1082 // Single step: determine priority from the node test and axis
1083 Expr::Step(step) => compute_step_priority(step),
1084
1085 // Filter expression: priority based on the primary expression
1086 Expr::Filter(primary, _) => compute_expr_priority(primary),
1087
1088 // id() and key() functions: priority 0.0
1089 Expr::FunctionCall { name, .. } => {
1090 if name == "id" || name == "key" {
1091 0.0
1092 } else {
1093 // Bare node tests (node(), text(), comment(),
1094 // processing-instruction()) parse as function calls at the
1095 // top level; translate them to their step priorities.
1096 match name.as_str() {
1097 "node" => -0.25,
1098 "text" | "comment" | "processing-instruction" => 0.0,
1099 _ => 0.5,
1100 }
1101 }
1102 }
1103
1104 // Other expressions (literals, variables, etc.) — not typical patterns
1105 _ => 0.5,
1106 }
1107}
1108
1109/// Compute the default priority of a single step.
1110fn compute_step_priority(step: &Step) -> f64 {
1111 match &step.node_test {
1112 // node() test: -0.25
1113 NodeTest::Node => -0.25,
1114
1115 // text(), comment(), processing-instruction(): 0.0
1116 NodeTest::Text | NodeTest::Comment | NodeTest::ProcessingInstruction(_) => 0.0,
1117
1118 // Name test: 0.0 for a specific name on the child axis;
1119 // 0.5 on the attribute axis (@QName per XSLT 1.0 §5.5).
1120 NodeTest::NameTest(name_test) => match name_test {
1121 NameTest::LocalName(_) | NameTest::QName { .. } => {
1122 if step.axis == Axis::Attribute {
1123 0.5
1124 } else {
1125 0.0
1126 }
1127 }
1128 NameTest::Any => {
1129 // * in element context: -0.5
1130 // * in attribute context: +0.5
1131 if step.axis == Axis::Attribute {
1132 0.5
1133 } else {
1134 -0.5
1135 }
1136 }
1137 },
1138
1139 // * (Wildcard): -0.5 in element context, +0.5 in attribute context
1140 NodeTest::Wildcard => {
1141 if step.axis == Axis::Attribute {
1142 0.5
1143 } else {
1144 -0.5
1145 }
1146 }
1147
1148 // prefix:* namespace wildcard: -0.5
1149 NodeTest::NsWildcard(_) => {
1150 if step.axis == Axis::Attribute {
1151 0.5
1152 } else {
1153 -0.5
1154 }
1155 }
1156 }
1157}
1158
1159// ═══════════════════════════════════════════════════════════════════════════════
1160// Convenience Functions
1161// ═══════════════════════════════════════════════════════════════════════════════
1162
1163/// Check if a pattern string matches a single step (simple name test).
1164///
1165/// Returns true if the pattern is a simple name test like `para` or `foo:bar`,
1166/// without path separators, predicates, or union operators.
1167pub fn is_simple_name_pattern(pattern: &str) -> bool {
1168 let expr = match parse_xpath(pattern) {
1169 Ok(e) => e,
1170 Err(_) => return false,
1171 };
1172
1173 matches!(&expr, Expr::Step(Step {
1174 axis: Axis::Child,
1175 node_test: NodeTest::NameTest(name_test),
1176 predicates,
1177 }) if predicates.is_empty() && !matches!(name_test, NameTest::Any))
1178}
1179
1180/// Check if a pattern is a union pattern (contains `|`).
1181pub fn is_union_pattern(pattern: &str) -> bool {
1182 let expr = match parse_xpath(pattern) {
1183 Ok(e) => e,
1184 Err(_) => return false,
1185 };
1186
1187 matches!(&expr, Expr::Union(_, _))
1188}
1189
1190/// Get the names matched by a simple name-test pattern.
1191///
1192/// For a simple pattern like `para` or `foo | bar`, returns the list of
1193/// matched element names. Returns an empty vec for complex patterns.
1194pub fn get_pattern_matched_names(pattern: &str) -> Vec<String> {
1195 let expr = match parse_xpath(pattern) {
1196 Ok(e) => e,
1197 Err(_) => return vec![],
1198 };
1199
1200 let mut names = Vec::new();
1201 collect_matched_names(&expr, &mut names);
1202 names
1203}
1204
1205fn collect_matched_names(expr: &Expr, names: &mut Vec<String>) {
1206 match expr {
1207 Expr::Union(left, right) => {
1208 collect_matched_names(left, names);
1209 collect_matched_names(right, names);
1210 }
1211 Expr::Step(Step {
1212 node_test: NodeTest::NameTest(name_test),
1213 ..
1214 }) => match name_test {
1215 NameTest::LocalName(local) => names.push(local.clone()),
1216 NameTest::QName { prefix, local } => names.push(format!("{}:{}", prefix, local)),
1217 NameTest::Any => names.push("*".to_string()),
1218 },
1219 Expr::Step(Step {
1220 node_test: NodeTest::Wildcard,
1221 ..
1222 }) => {
1223 names.push("*".to_string());
1224 }
1225 Expr::Step(Step {
1226 node_test: NodeTest::NsWildcard(prefix),
1227 ..
1228 }) => {
1229 names.push(format!("{}:*", prefix));
1230 }
1231 _ => {}
1232 }
1233}
1234
1235// ═══════════════════════════════════════════════════════════════════════════════
1236// Tests
1237// ═══════════════════════════════════════════════════════════════════════════════
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 // ── Priority Tests ────────────────────────────────────────────────────
1244
1245 #[test]
1246 fn test_default_priority_name_test() {
1247 // Simple name test "para" → 0.0
1248 let priority = compute_default_priority("para");
1249 assert!(
1250 (priority - 0.0).abs() < f64::EPSILON,
1251 "Expected 0.0 for name test, got {}",
1252 priority
1253 );
1254 }
1255
1256 #[test]
1257 fn test_default_priority_qname() {
1258 // Qualified name "xslt:template" → 0.0
1259 let priority = compute_default_priority("xslt:template");
1260 assert!(
1261 (priority - 0.0).abs() < f64::EPSILON,
1262 "Expected 0.0 for QName, got {}",
1263 priority
1264 );
1265 }
1266
1267 #[test]
1268 fn test_default_priority_node_test() {
1269 // node() test → -0.25
1270 let priority = compute_default_priority("node()");
1271 assert!(
1272 (priority - (-0.25)).abs() < f64::EPSILON,
1273 "Expected -0.25 for node(), got {}",
1274 priority
1275 );
1276 }
1277
1278 #[test]
1279 fn test_default_priority_text_test() {
1280 // text() test → 0.0
1281 let priority = compute_default_priority("text()");
1282 assert!(
1283 (priority - 0.0).abs() < f64::EPSILON,
1284 "Expected 0.0 for text(), got {}",
1285 priority
1286 );
1287 }
1288
1289 #[test]
1290 fn test_default_priority_comment_test() {
1291 // comment() test → 0.0
1292 let priority = compute_default_priority("comment()");
1293 assert!(
1294 (priority - 0.0).abs() < f64::EPSILON,
1295 "Expected 0.0 for comment(), got {}",
1296 priority
1297 );
1298 }
1299
1300 #[test]
1301 fn test_default_priority_processing_instruction() {
1302 // processing-instruction() test → 0.0
1303 let priority = compute_default_priority("processing-instruction()");
1304 assert!(
1305 (priority - 0.0).abs() < f64::EPSILON,
1306 "Expected 0.0 for processing-instruction(), got {}",
1307 priority
1308 );
1309 }
1310
1311 #[test]
1312 fn test_default_priority_wildcard() {
1313 // * wildcard → -0.5
1314 let priority = compute_default_priority("*");
1315 assert!(
1316 (priority - (-0.5)).abs() < f64::EPSILON,
1317 "Expected -0.5 for *, got {}",
1318 priority
1319 );
1320 }
1321
1322 #[test]
1323 fn test_default_priority_ns_wildcard() {
1324 // ns:* wildcard → -0.5
1325 let priority = compute_default_priority("ns:*");
1326 assert!(
1327 (priority - (-0.5)).abs() < f64::EPSILON,
1328 "Expected -0.5 for ns:*, got {}",
1329 priority
1330 );
1331 }
1332
1333 #[test]
1334 fn test_default_priority_attribute() {
1335 // @attr → +0.5
1336 let priority = compute_default_priority("@attr");
1337 assert!(
1338 (priority - 0.5).abs() < f64::EPSILON,
1339 "Expected 0.5 for @attr, got {}",
1340 priority
1341 );
1342 }
1343
1344 #[test]
1345 fn test_default_priority_attribute_wildcard() {
1346 // @* → +0.5
1347 let priority = compute_default_priority("@*");
1348 assert!(
1349 (priority - 0.5).abs() < f64::EPSILON,
1350 "Expected 0.5 for @*, got {}",
1351 priority
1352 );
1353 }
1354
1355 #[test]
1356 fn test_default_priority_union() {
1357 // Union "para | *" → max(0.0, -0.5) = 0.0
1358 let priority = compute_default_priority("para | *");
1359 assert!(
1360 (priority - 0.0).abs() < f64::EPSILON,
1361 "Expected 0.0 for union, got {}",
1362 priority
1363 );
1364 }
1365
1366 #[test]
1367 fn test_default_priority_compound_path() {
1368 // Path "foo/bar" → priority of last step "bar" = 0.0
1369 let priority = compute_default_priority("foo/bar");
1370 assert!(
1371 (priority - 0.0).abs() < f64::EPSILON,
1372 "Expected 0.0 for foo/bar, got {}",
1373 priority
1374 );
1375 }
1376
1377 #[test]
1378 fn test_default_priority_empty() {
1379 // Empty pattern → 0.5
1380 let priority = compute_default_priority("");
1381 assert!(
1382 (priority - 0.5).abs() < f64::EPSILON,
1383 "Expected 0.5 for empty pattern, got {}",
1384 priority
1385 );
1386 }
1387
1388 // ── Pattern Matching Tests ────────────────────────────────────────────
1389
1390 /// Create a minimal element node for testing.
1391 unsafe fn create_test_node(name: &str, type_: c_int) -> *mut _xmlNode {
1392 let layout = std::alloc::Layout::new::<_xmlNode>();
1393 let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
1394 if ptr.is_null() {
1395 return ptr::null_mut();
1396 }
1397 let node = &mut *ptr;
1398 node.type_ = type_;
1399 // Allocate and copy the name
1400 let name_bytes = name.as_bytes();
1401 let name_buf = std::alloc::alloc_zeroed(
1402 std::alloc::Layout::array::<u8>(name_bytes.len() + 1).unwrap(),
1403 );
1404 if !name_buf.is_null() {
1405 std::ptr::copy_nonoverlapping(name_bytes.as_ptr(), name_buf, name_bytes.len());
1406 }
1407 node.name = name_buf as *mut xmlChar;
1408 ptr
1409 }
1410
1411 /// Free a test node.
1412 unsafe fn free_test_node(node: *mut _xmlNode) {
1413 if node.is_null() {
1414 return;
1415 }
1416 if !(*node).name.is_null() {
1417 let name = (*node).name;
1418 // Find length
1419 let len = crate::abi::exports_xml2::xmlStrlen(name) as usize;
1420 std::alloc::dealloc(
1421 name as *mut u8,
1422 std::alloc::Layout::array::<u8>(len + 1).unwrap(),
1423 );
1424 }
1425 let layout = std::alloc::Layout::new::<_xmlNode>();
1426 std::alloc::dealloc(node as *mut u8, layout);
1427 }
1428
1429 /// Verify `NodeTest` matching against a minimal element node.
1430 ///
1431 /// # Safety
1432 ///
1433 /// - `create_test_node` returns a valid `_xmlNode` whose `name` is a
1434 /// heap-allocated NUL-terminated string; `match_node_test` reads it
1435 /// while the node is alive.
1436 /// - The node is freed exactly once with `free_test_node`.
1437 #[test]
1438 fn test_node_test_matching_element() {
1439 unsafe {
1440 let node = create_test_node("para", 1); // XML_ELEMENT_NODE
1441 assert!(!node.is_null());
1442
1443 // Name test
1444 let name_test = NodeTest::NameTest(NameTest::LocalName("para".to_string()));
1445 assert!(match_node_test(node, &name_test));
1446
1447 // Wrong name
1448 let wrong_test = NodeTest::NameTest(NameTest::LocalName("foo".to_string()));
1449 assert!(!match_node_test(node, &wrong_test));
1450
1451 // Wildcard
1452 let wildcard = NodeTest::Wildcard;
1453 assert!(match_node_test(node, &wildcard));
1454
1455 // Node test
1456 let node_test = NodeTest::Node;
1457 assert!(match_node_test(node, &node_test));
1458
1459 // Text test should not match element
1460 let text_test = NodeTest::Text;
1461 assert!(!match_node_test(node, &text_test));
1462
1463 free_test_node(node);
1464 }
1465 }
1466
1467 /// Verify `NodeTest` matching against a minimal text node.
1468 ///
1469 /// # Safety
1470 ///
1471 /// - The node from `create_test_node` is a valid `_xmlNode` with a
1472 /// heap-allocated NUL-terminated `name`; `match_node_test` reads it
1473 /// while the node is alive.
1474 /// - The node is freed exactly once with `free_test_node`.
1475 #[test]
1476 fn test_node_test_matching_text() {
1477 unsafe {
1478 let node = create_test_node("", 3); // XML_TEXT_NODE
1479 assert!(!node.is_null());
1480
1481 let text_test = NodeTest::Text;
1482 assert!(match_node_test(node, &text_test));
1483
1484 let node_test = NodeTest::Node;
1485 assert!(match_node_test(node, &node_test));
1486
1487 let comment_test = NodeTest::Comment;
1488 assert!(!match_node_test(node, &comment_test));
1489
1490 let element_test = NodeTest::NameTest(NameTest::LocalName("para".to_string()));
1491 assert!(!match_node_test(node, &element_test));
1492
1493 free_test_node(node);
1494 }
1495 }
1496
1497 /// Compile a simple pattern and free the compiled result.
1498 ///
1499 /// # Safety
1500 ///
1501 /// - The pattern string is a valid NUL-terminated string; the compiled
1502 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1503 /// `xsltFreePattern`.
1504 #[test]
1505 fn test_compile_and_free_pattern() {
1506 unsafe {
1507 let pattern_str = c"para".as_ptr() as *const xmlChar;
1508 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1509 assert!(!compiled.is_null());
1510 xsltFreePattern(compiled);
1511 }
1512 }
1513
1514 /// Compile with a NULL pattern.
1515 ///
1516 /// # Safety
1517 ///
1518 /// - A NULL pattern is accepted by `xsltCompilePattern` and yields NULL
1519 /// without dereferencing.
1520 #[test]
1521 fn test_compile_null_pattern() {
1522 unsafe {
1523 let compiled = xsltCompilePattern(ptr::null(), ptr::null_mut());
1524 assert!(compiled.is_null());
1525 }
1526 }
1527
1528 /// Compile an empty pattern.
1529 ///
1530 /// # Safety
1531 ///
1532 /// - The empty string is a valid NUL-terminated string; `xsltCompilePattern`
1533 /// returns NULL for it.
1534 #[test]
1535 fn test_compile_empty_pattern() {
1536 unsafe {
1537 let pattern_str = c"".as_ptr() as *const xmlChar;
1538 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1539 assert!(compiled.is_null());
1540 }
1541 }
1542
1543 /// Free a NULL pattern.
1544 ///
1545 /// # Safety
1546 ///
1547 /// - `xsltFreePattern` accepts NULL and returns without freeing or
1548 /// dereferencing.
1549 #[test]
1550 fn test_free_null_pattern() {
1551 unsafe {
1552 xsltFreePattern(ptr::null_mut());
1553 // Should not crash
1554 }
1555 }
1556
1557 #[test]
1558 fn test_is_simple_name_pattern() {
1559 assert!(is_simple_name_pattern("para"));
1560 assert!(is_simple_name_pattern("foo:bar"));
1561 assert!(!is_simple_name_pattern("foo/bar"));
1562 assert!(!is_simple_name_pattern("para | foo"));
1563 assert!(!is_simple_name_pattern("*"));
1564 }
1565
1566 #[test]
1567 fn test_is_union_pattern() {
1568 assert!(is_union_pattern("para | foo"));
1569 assert!(is_union_pattern("para | foo | bar"));
1570 assert!(!is_union_pattern("para"));
1571 assert!(!is_union_pattern("foo/bar"));
1572 }
1573
1574 #[test]
1575 fn test_get_pattern_matched_names() {
1576 let names = get_pattern_matched_names("para");
1577 assert_eq!(names, vec!["para"]);
1578
1579 let names = get_pattern_matched_names("foo | bar");
1580 assert_eq!(names.len(), 2);
1581 assert!(names.contains(&"foo".to_string()));
1582 assert!(names.contains(&"bar".to_string()));
1583
1584 let names = get_pattern_matched_names("foo/bar");
1585 assert!(names.is_empty());
1586 }
1587
1588 /// Compile a union pattern and free the compiled result.
1589 ///
1590 /// # Safety
1591 ///
1592 /// - The pattern string is a valid NUL-terminated string; the compiled
1593 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1594 /// `xsltFreePattern`.
1595 #[test]
1596 fn test_compile_union_pattern() {
1597 unsafe {
1598 let pattern_str = c"para | foo".as_ptr() as *const xmlChar;
1599 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1600 assert!(!compiled.is_null());
1601 xsltFreePattern(compiled);
1602 }
1603 }
1604
1605 /// Compile a compound path pattern and free the compiled result.
1606 ///
1607 /// # Safety
1608 ///
1609 /// - The pattern string is a valid NUL-terminated string; the compiled
1610 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1611 /// `xsltFreePattern`.
1612 #[test]
1613 fn test_compile_compound_pattern() {
1614 unsafe {
1615 let pattern_str = c"foo/bar".as_ptr() as *const xmlChar;
1616 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1617 assert!(!compiled.is_null());
1618 xsltFreePattern(compiled);
1619 }
1620 }
1621
1622 /// Compile an absolute path pattern and free the compiled result.
1623 ///
1624 /// # Safety
1625 ///
1626 /// - The pattern string is a valid NUL-terminated string; the compiled
1627 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1628 /// `xsltFreePattern`.
1629 #[test]
1630 fn test_compile_absolute_pattern() {
1631 unsafe {
1632 let pattern_str = c"/foo/bar".as_ptr() as *const xmlChar;
1633 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1634 assert!(!compiled.is_null());
1635 xsltFreePattern(compiled);
1636 }
1637 }
1638
1639 /// Compile an attribute pattern and free the compiled result.
1640 ///
1641 /// # Safety
1642 ///
1643 /// - The pattern string is a valid NUL-terminated string; the compiled
1644 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1645 /// `xsltFreePattern`.
1646 #[test]
1647 fn test_compile_attribute_pattern() {
1648 unsafe {
1649 let pattern_str = c"@attr".as_ptr() as *const xmlChar;
1650 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1651 assert!(!compiled.is_null());
1652 xsltFreePattern(compiled);
1653 }
1654 }
1655
1656 /// Compile a wildcard pattern and free the compiled result.
1657 ///
1658 /// # Safety
1659 ///
1660 /// - The pattern string is a valid NUL-terminated string; the compiled
1661 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1662 /// `xsltFreePattern`.
1663 #[test]
1664 fn test_compile_wildcard_pattern() {
1665 unsafe {
1666 let pattern_str = c"*".as_ptr() as *const xmlChar;
1667 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1668 assert!(!compiled.is_null());
1669 xsltFreePattern(compiled);
1670 }
1671 }
1672
1673 /// Compile a namespace wildcard pattern and free the compiled result.
1674 ///
1675 /// # Safety
1676 ///
1677 /// - The pattern string is a valid NUL-terminated string; the compiled
1678 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1679 /// `xsltFreePattern`.
1680 #[test]
1681 fn test_compile_ns_wildcard_pattern() {
1682 unsafe {
1683 let pattern_str = c"ns:*".as_ptr() as *const xmlChar;
1684 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1685 assert!(!compiled.is_null());
1686 xsltFreePattern(compiled);
1687 }
1688 }
1689
1690 /// Compile a node-test pattern and free the compiled result.
1691 ///
1692 /// # Safety
1693 ///
1694 /// - The pattern string is a valid NUL-terminated string; the compiled
1695 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1696 /// `xsltFreePattern`.
1697 #[test]
1698 fn test_compile_node_test_pattern() {
1699 unsafe {
1700 let pattern_str = c"node()".as_ptr() as *const xmlChar;
1701 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1702 assert!(!compiled.is_null());
1703 xsltFreePattern(compiled);
1704 }
1705 }
1706
1707 /// Compile a text node-test pattern and free the compiled result.
1708 ///
1709 /// # Safety
1710 ///
1711 /// - The pattern string is a valid NUL-terminated string; the compiled
1712 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1713 /// `xsltFreePattern`.
1714 #[test]
1715 fn test_compile_text_pattern() {
1716 unsafe {
1717 let pattern_str = c"text()".as_ptr() as *const xmlChar;
1718 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1719 assert!(!compiled.is_null());
1720 xsltFreePattern(compiled);
1721 }
1722 }
1723
1724 /// Compile a comment node-test pattern and free the compiled result.
1725 ///
1726 /// # Safety
1727 ///
1728 /// - The pattern string is a valid NUL-terminated string; the compiled
1729 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1730 /// `xsltFreePattern`.
1731 #[test]
1732 fn test_compile_comment_pattern() {
1733 unsafe {
1734 let pattern_str = c"comment()".as_ptr() as *const xmlChar;
1735 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1736 assert!(!compiled.is_null());
1737 xsltFreePattern(compiled);
1738 }
1739 }
1740
1741 /// Compile a processing-instruction pattern and free the compiled
1742 /// result.
1743 ///
1744 /// # Safety
1745 ///
1746 /// - The pattern string is a valid NUL-terminated string; the compiled
1747 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1748 /// `xsltFreePattern`.
1749 #[test]
1750 fn test_compile_pi_pattern() {
1751 unsafe {
1752 let pattern_str = c"processing-instruction()".as_ptr() as *const xmlChar;
1753 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1754 assert!(!compiled.is_null());
1755 xsltFreePattern(compiled);
1756 }
1757 }
1758
1759 /// Compile a predicate pattern and free the compiled result.
1760 ///
1761 /// # Safety
1762 ///
1763 /// - The pattern string is a valid NUL-terminated string; the compiled
1764 /// pattern returned by `xsltCompilePattern` is freed exactly once with
1765 /// `xsltFreePattern`.
1766 #[test]
1767 fn test_compile_predicate_pattern() {
1768 unsafe {
1769 let pattern_str = c"para[1]".as_ptr() as *const xmlChar;
1770 let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1771 assert!(!compiled.is_null());
1772 xsltFreePattern(compiled);
1773 }
1774 }
1775
1776 #[test]
1777 fn test_decompose_union() {
1778 let expr = parse_xpath("a | b").unwrap();
1779 let patterns = decompose_pattern(&expr, "a | b");
1780 assert!(patterns.is_some());
1781 let patterns = patterns.unwrap();
1782 assert_eq!(patterns.len(), 2);
1783 assert_eq!(patterns[0].original, "a | b");
1784 assert_eq!(patterns[1].original, "a | b");
1785 }
1786
1787 #[test]
1788 fn test_decompose_single() {
1789 let expr = parse_xpath("para").unwrap();
1790 let patterns = decompose_pattern(&expr, "para");
1791 assert!(patterns.is_some());
1792 let patterns = patterns.unwrap();
1793 assert_eq!(patterns.len(), 1);
1794 }
1795
1796 #[test]
1797 fn test_collect_steps_simple() {
1798 let expr = parse_xpath("para").unwrap();
1799 let (steps, is_absolute) = collect_steps(&expr).unwrap();
1800 assert!(!is_absolute);
1801 assert_eq!(steps.len(), 1);
1802 if let PatternStepEntry::Step(step) = &steps[0] {
1803 assert_eq!(step.axis, Axis::Child);
1804 assert!(
1805 matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "para")
1806 );
1807 } else {
1808 panic!("Expected Step entry");
1809 }
1810 }
1811
1812 #[test]
1813 fn test_collect_steps_absolute() {
1814 let expr = parse_xpath("/foo/bar").unwrap();
1815 let (steps, is_absolute) = collect_steps(&expr).unwrap();
1816 assert!(is_absolute);
1817 assert_eq!(steps.len(), 2);
1818 }
1819
1820 #[test]
1821 fn test_collect_steps_attribute() {
1822 let expr = parse_xpath("@attr").unwrap();
1823 let (steps, is_absolute) = collect_steps(&expr).unwrap();
1824 assert!(!is_absolute);
1825 assert_eq!(steps.len(), 1);
1826 if let PatternStepEntry::Step(step) = &steps[0] {
1827 assert_eq!(step.axis, Axis::Attribute);
1828 } else {
1829 panic!("Expected Step entry");
1830 }
1831 }
1832
1833 #[test]
1834 fn test_collect_steps_compound() {
1835 let expr = parse_xpath("foo/bar").unwrap();
1836 let (steps, is_absolute) = collect_steps(&expr).unwrap();
1837 assert!(!is_absolute);
1838 assert_eq!(steps.len(), 2);
1839 // First step should be "bar" (rightmost)
1840 if let PatternStepEntry::Step(step) = &steps[0] {
1841 assert!(
1842 matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "bar")
1843 );
1844 } else {
1845 panic!("Expected Step entry for bar");
1846 }
1847 // Second step should be "foo"
1848 if let PatternStepEntry::Step(step) = &steps[1] {
1849 assert!(
1850 matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "foo")
1851 );
1852 } else {
1853 panic!("Expected Step entry for foo");
1854 }
1855 }
1856
1857 /// Verify `NameTest` matching against a minimal element node.
1858 ///
1859 /// # Safety
1860 ///
1861 /// - The node from `create_test_node` is a valid `_xmlNode` with a
1862 /// heap-allocated NUL-terminated `name`; `match_name_test` reads its
1863 /// `type_` and `name` while the node is alive.
1864 /// - The node is freed exactly once with `free_test_node`.
1865 #[test]
1866 fn test_match_name_test_local() {
1867 unsafe {
1868 let node = create_test_node("para", 1);
1869 assert!(!node.is_null());
1870
1871 assert!(match_name_test(
1872 node,
1873 &NameTest::LocalName("para".to_string())
1874 ));
1875 assert!(!match_name_test(
1876 node,
1877 &NameTest::LocalName("foo".to_string())
1878 ));
1879 assert!(match_name_test(node, &NameTest::Any));
1880
1881 free_test_node(node);
1882 }
1883 }
1884
1885 /// Verify wildcard node-test matching over element, text and comment
1886 /// nodes.
1887 ///
1888 /// # Safety
1889 ///
1890 /// - Each `create_test_node` result is a valid `_xmlNode` with a
1891 /// heap-allocated NUL-terminated `name`; each node is freed exactly
1892 /// once with `free_test_node`.
1893 #[test]
1894 fn test_match_node_test_wildcard() {
1895 unsafe {
1896 let element = create_test_node("para", 1);
1897 let text = create_test_node("", 3);
1898 let comment = create_test_node("", 8);
1899
1900 let wildcard = NodeTest::Wildcard;
1901 assert!(match_node_test(element, &wildcard));
1902 assert!(!match_node_test(text, &wildcard));
1903 assert!(!match_node_test(comment, &wildcard));
1904
1905 free_test_node(element);
1906 free_test_node(text);
1907 free_test_node(comment);
1908 }
1909 }
1910
1911 /// Verify namespace-wildcard matching on a node without a namespace.
1912 ///
1913 /// # Safety
1914 ///
1915 /// - The node from `create_test_node` is a valid `_xmlNode` with a
1916 /// heap-allocated NUL-terminated `name`; it is freed exactly once with
1917 /// `free_test_node`.
1918 #[test]
1919 fn test_match_node_test_ns_wildcard() {
1920 unsafe {
1921 let node = create_test_node("para", 1);
1922 // No namespace set — only empty prefix matches
1923 let ns_wildcard = NodeTest::NsWildcard("".to_string());
1924 assert!(match_node_test(node, &ns_wildcard));
1925
1926 let ns_wildcard = NodeTest::NsWildcard("foo".to_string());
1927 assert!(!match_node_test(node, &ns_wildcard));
1928
1929 free_test_node(node);
1930 }
1931 }
1932
1933 /// Verify default priorities through the C ABI entry point.
1934 ///
1935 /// # Safety
1936 ///
1937 /// - Each pattern string passed to `xsltDefaultPriority` is a valid
1938 /// NUL-terminated string.
1939 #[test]
1940 fn test_compute_priority_on_compiled_pattern() {
1941 unsafe {
1942 // Test through the C ABI function
1943 let pattern_str = c"para".as_ptr() as *const xmlChar;
1944 let priority = xsltDefaultPriority(pattern_str);
1945 assert!(
1946 (priority - 0.0).abs() < f64::EPSILON,
1947 "Expected 0.0 for 'para', got {}",
1948 priority
1949 );
1950
1951 let pattern_str = c"*".as_ptr() as *const xmlChar;
1952 let priority = xsltDefaultPriority(pattern_str);
1953 assert!(
1954 (priority - (-0.5)).abs() < f64::EPSILON,
1955 "Expected -0.5 for '*', got {}",
1956 priority
1957 );
1958
1959 let pattern_str = c"node()".as_ptr() as *const xmlChar;
1960 let priority = xsltDefaultPriority(pattern_str);
1961 assert!(
1962 (priority - (-0.25)).abs() < f64::EPSILON,
1963 "Expected -0.25 for 'node()', got {}",
1964 priority
1965 );
1966
1967 let pattern_str = c"@attr".as_ptr() as *const xmlChar;
1968 let priority = xsltDefaultPriority(pattern_str);
1969 assert!(
1970 (priority - 0.5).abs() < f64::EPSILON,
1971 "Expected 0.5 for '@attr', got {}",
1972 priority
1973 );
1974 }
1975 }
1976
1977 /// Verify the default priority of a NULL pattern.
1978 ///
1979 /// # Safety
1980 ///
1981 /// - `xsltDefaultPriority` accepts NULL and returns the default without
1982 /// dereferencing.
1983 #[test]
1984 fn test_compute_priority_null() {
1985 unsafe {
1986 let priority = xsltDefaultPriority(ptr::null());
1987 assert!(
1988 (priority - 0.5).abs() < f64::EPSILON,
1989 "Expected 0.5 for null pattern, got {}",
1990 priority
1991 );
1992 }
1993 }
1994
1995 /// Verify the default priority of an empty pattern.
1996 ///
1997 /// # Safety
1998 ///
1999 /// - The empty string is a valid NUL-terminated string passed to
2000 /// `xsltDefaultPriority`.
2001 #[test]
2002 fn test_compute_priority_empty() {
2003 unsafe {
2004 let pattern_str = c"".as_ptr() as *const xmlChar;
2005 let priority = xsltDefaultPriority(pattern_str);
2006 assert!(
2007 (priority - 0.5).abs() < f64::EPSILON,
2008 "Expected 0.5 for empty pattern, got {}",
2009 priority
2010 );
2011 }
2012 }
2013
2014 /// Verify `xsltTestPattern` with all-NULL arguments.
2015 ///
2016 /// # Safety
2017 ///
2018 /// - NULL context, pattern and node are accepted and yield 0 without
2019 /// dereferencing any of them.
2020 #[test]
2021 fn test_xslt_test_pattern_null_args() {
2022 unsafe {
2023 let result = xsltTestPattern(ptr::null_mut(), ptr::null_mut(), ptr::null_mut());
2024 assert_eq!(result, 0);
2025 }
2026 }
2027}