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