Skip to main content

libxml_rs/xml/xpath/
types.rs

1//! XPath 1.0 Runtime Types (§25).
2//!
3//! Internal Rust representation of XPath values: node-sets, strings,
4//! numbers, booleans, and conversions between them.
5//!
6//! # UPSTREAM-PARITY
7//!
8//! XPath 1.0 type system with exact IEEE 754 floating-point semantics:
9//! NaN, infinity, negative zero, rounding behavior.
10//!
11//! # Courts
12//!
13//! XPATH-TYPES-*
14//!
15//! # Upstream contract
16//!
17//! Mirrors the value model of upstream `xpath.c` / `xpathInternals.h`
18//! (`SRC-LIBXML2-2.15.0-XPATH-C`, parity target libxml2 2.15.3 oracle):
19//! `xmlXPathObject` types (XPATH_NODESET, XPATH_BOOLEAN, XPATH_NUMBER,
20//! XPATH_STRING, XPATH_POINT/RANGE/LOCATIONSET, XPATH_USERS,
21//! XPATH_XSLT_TREE) and the string/number conversion functions
22//! `xmlXPathCastToString`, `xmlXPathStringEvalNumber` and
23//! `xmlXPathCastNumberToString`.
24//!
25//! # Conceptual behavior
26//!
27//! Defines the runtime value types and their conversions. Node-sets are
28//! ordered, deduplicated collections of borrowed document nodes;
29//! `node_string_value` computes the XPath string-value of a node
30//! (R-000114 fixed the empty attribute string-value); `string_bytes_to_
31//! number` / `number_to_string` are faithful ports of the R-000166
32//! number conversion with the 1e9/1e-5 scientific threshold and
33//! DBL_DIG=15 fraction digits.
34//!
35//! # Ownership & safety invariants
36//!
37//! `XPathNode` holds a raw `*mut _xmlNode` that is borrowed from the
38//! document — the tree must outlive evaluation (SAFETY note on the
39//! struct). Values own their storage (String/NodeSet) and are freed by
40//! drop; nothing here allocates through the C allocator except the
41//! exports bridge.
42//!
43//! # Historical quirks & epochs
44//!
45//! The conversion rules track the 2.15.3 oracle epoch: the E-001
46//! newline-separated node-set dump (commit da35eeae, 2.9.10) is the
47//! output epoch the XPath CLI surfaces target, and number formatting
48//! (R-000166, 967/967 number() corpus) is fixed since the same era.
49//!
50//! # Deliberate oddities
51//!
52//! `-0.0` serializes as `0`, NaN as `NaN`, infinities as `Infinity`/
53//! `-Infinity`, and integral values take the integer shortcut — the
54//! upstream xmlXPathFormatNumber quirks reproduced instead of Rust
55//! Display.
56//!
57//! # Proving courts
58//!
59//! XPATH-TYPES-* differential probes and the 967/967 number() corpus
60//! compare conversions byte-identical against the oracle; cargo test
61//! runs the conversion unit suites (incl. test_number_to_string).
62//!
63//! # Tempting simplifications that would break parity
64//!
65//! Do not switch conversions to Rust std float formatting or parsing:
66//! the oracle digit accumulation (MAX_FRAC=20), exponent underflow
67//! (5e-324 → 0), threshold selection and exponent padding are observable
68//! (R-000166). Do not make node-sets own the tree: callers free the
69//! document independently of the XPath object.
70
71use crate::abi::structs::_xmlNode;
72use crate::abi::types::xmlElementType;
73use std::cmp::Ordering;
74use std::collections::HashSet;
75use std::ffi::c_int;
76use std::ptr;
77
78// ═══════════════════════════════════════════════════════════════════════════════
79// XPath Value Types
80// ═══════════════════════════════════════════════════════════════════════════════
81
82/// XPath type.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum XPathType {
85    /// A node-set: an ordered, deduplicated collection of document nodes
86    NodeSet,
87    /// A string
88    String,
89    /// A number (IEEE 754 double)
90    Number,
91    /// A boolean
92    Boolean,
93    /// A single point in the tree: a node plus a position within it
94    Point,
95    /// A range of nodes in the tree
96    Range,
97    /// A set of points and ranges
98    LocationSet,
99    /// A user-defined value type
100    Users,
101    /// An XSLT tree fragment (result tree fragment)
102    XsltTree,
103    /// No type assigned yet (uninitialized value)
104    Undefined,
105}
106
107/// A node in a node-set, identified by pointer.
108///
109/// We use raw pointers because:
110/// 1. The tree is owned by the document, not by XPath.
111/// 2. The C ABI exposes node pointers that callers manipulate.
112/// 3. Multiple XPath evaluations may reference the same tree.
113///
114/// SAFETY: Node pointers must remain valid for the duration of evaluation.
115#[derive(Debug, Clone, Copy)]
116pub struct XPathNode(pub *mut _xmlNode);
117
118impl PartialEq for XPathNode {
119    fn eq(&self, other: &Self) -> bool {
120        self.0 == other.0
121    }
122}
123
124impl Eq for XPathNode {}
125
126impl PartialOrd for XPathNode {
127    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128        Some(self.cmp(other))
129    }
130}
131
132impl Ord for XPathNode {
133    fn cmp(&self, other: &Self) -> Ordering {
134        // Compare by pointer value for document order
135        // In a full implementation, this would use the document order algorithm
136        self.0.cmp(&other.0)
137    }
138}
139
140impl std::hash::Hash for XPathNode {
141    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
142        self.0.hash(state);
143    }
144}
145
146/// An XPath node-set.
147///
148/// Internally stored as a sorted, deduplicated Vec of node pointers
149/// in document order.
150#[derive(Debug, Clone)]
151pub struct NodeSet {
152    nodes: Vec<XPathNode>,
153}
154
155impl NodeSet {
156    /// Create an empty node-set.
157    pub const fn new() -> Self {
158        Self { nodes: Vec::new() }
159    }
160
161    /// Create a node-set containing exactly one node.
162    pub fn singleton(node: *mut _xmlNode) -> Self {
163        Self {
164            nodes: vec![XPathNode(node)],
165        }
166    }
167
168    /// Return `true` if the node-set contains no nodes.
169    pub const fn is_empty(&self) -> bool {
170        self.nodes.is_empty()
171    }
172
173    /// Return the number of nodes in the node-set.
174    pub const fn len(&self) -> usize {
175        self.nodes.len()
176    }
177
178    /// Iterate over the nodes in document order.
179    pub fn iter(&self) -> impl Iterator<Item = *mut _xmlNode> + '_ {
180        self.nodes.iter().map(|n| n.0)
181    }
182
183    /// Return the node at `index` in document order, or `None` if out of bounds.
184    pub fn get(&self, index: usize) -> Option<*mut _xmlNode> {
185        self.nodes.get(index).map(|n| n.0)
186    }
187
188    /// Return the first node in document order.
189    pub fn first(&self) -> Option<*mut _xmlNode> {
190        self.nodes.first().map(|n| n.0)
191    }
192
193    /// Return the last node in document order.
194    pub fn last(&self) -> Option<*mut _xmlNode> {
195        self.nodes.last().map(|n| n.0)
196    }
197
198    /// Return `true` if the given node is in the node-set.
199    pub fn contains(&self, node: *mut _xmlNode) -> bool {
200        self.nodes.iter().any(|n| n.0 == node)
201    }
202
203    /// Append a node to the set.
204    ///
205    /// This is O(1) and does NOT deduplicate or sort. Callers that need the
206    /// document-order/unique invariant (axis results are already unique and
207    /// ordered, but unions and relative-path concatenations are not) call
208    /// [`sort`](Self::sort) once at the boundary. Deferring the sort avoids
209    /// the O(n² log n) node-set construction cost — the Phase 15.1 XPath perf
210    /// cliff (`count(//item)` was ~1000× slower than the oracle because every
211    /// push re-sorted the growing set).
212    pub fn push(&mut self, node: *mut _xmlNode) {
213        self.nodes.push(XPathNode(node));
214    }
215
216    /// Extend with another node-set.
217    pub fn extend(&mut self, other: &NodeSet) {
218        for node in other.iter() {
219            self.push(node);
220        }
221    }
222
223    /// Sort nodes in document order.
224    ///
225    /// # UPSTREAM-PARITY
226    ///
227    /// XPath node-sets are always in document order (XPath 1.0 §3.3).
228    /// libxml2 maintains this via its node-set insertion/merge logic plus
229    /// the document-order comparator (xmlXPathNodeSetSort). Sorting by
230    /// pointer address is NOT document order and breaks downstream ordering
231    /// guarantees; the oracle-observed symptom is rotated results on the
232    /// second of two transforms in one process.
233    pub fn sort(&mut self) {
234        // Dedup by pointer first (matching the old per-push membership check:
235        // first occurrence wins), then order by document order. `retain` keeps
236        // the first occurrence and drops subsequent duplicates before the sort,
237        // so dedup is independent of the comparator's namespace-node Equal
238        // quirk (which would otherwise fail to make identical namespace copies
239        // adjacent for `dedup()`).
240        let mut seen: HashSet<usize> = HashSet::with_capacity(self.nodes.len());
241        self.nodes.retain(|n| seen.insert(n.0 as usize));
242        self.nodes
243            .sort_by(|a, b| unsafe { compare_document_order(a.0, b.0) });
244    }
245
246    /// Convert to raw C ABI node-set.
247    ///
248    /// SAFETY: The returned pointer must be freed with xmlXPathFreeNodeSet
249    /// or the owning XPath object must be freed.
250    ///
251    /// # SAFETY
252    ///
253    /// The function touches crate-global state only; it is safe
254    /// as long as the caller respects the library's global
255    /// initialization/cleanup ordering (xmlInitParser before use,
256    /// xmlCleanupParser only after all users are done).
257    ///
258    /// Violating the global lifecycle ordering, or calling this after
259    /// teardown or from a signal handler, is undefined behavior.
260    pub unsafe fn to_raw(&self) -> *mut crate::abi::structs::_xmlNodeSet {
261        let node_max = self.nodes.len();
262        let node_tab = if node_max > 0 {
263            let ptr = crate::abi::allocator::xmlMallocImpl(
264                node_max * std::mem::size_of::<*mut _xmlNode>(),
265            ) as *mut *mut _xmlNode;
266            if ptr.is_null() {
267                return ptr::null_mut();
268            }
269            for (i, node) in self.nodes.iter().enumerate() {
270                ptr::write(ptr.add(i), node.0);
271            }
272            ptr
273        } else {
274            ptr::null_mut()
275        };
276
277        let raw = crate::abi::allocator::xmlMallocImpl(std::mem::size_of::<
278            crate::abi::structs::_xmlNodeSet,
279        >()) as *mut crate::abi::structs::_xmlNodeSet;
280        if raw.is_null() {
281            if !node_tab.is_null() {
282                crate::abi::allocator::xmlFreeImpl(node_tab as *mut _);
283            }
284            return ptr::null_mut();
285        }
286        ptr::write(
287            raw,
288            crate::abi::structs::_xmlNodeSet {
289                nodeNr: node_max as std::os::raw::c_int,
290                nodeMax: node_max as std::os::raw::c_int,
291                nodeTab: node_tab,
292            },
293        );
294        raw
295    }
296}
297
298impl Default for NodeSet {
299    fn default() -> Self {
300        Self::new()
301    }
302}
303
304/// XPath runtime value.
305#[derive(Debug, Clone)]
306pub enum XPathValue {
307    /// A node-set value
308    NodeSet(NodeSet),
309    /// A string value
310    String(String),
311    /// A number value (IEEE 754 double)
312    Number(f64),
313    /// A boolean value
314    Boolean(bool),
315}
316
317impl XPathValue {
318    /// Get the XPath type of this value.
319    pub const fn xpath_type(&self) -> XPathType {
320        match self {
321            XPathValue::NodeSet(_) => XPathType::NodeSet,
322            XPathValue::String(_) => XPathType::String,
323            XPathValue::Number(_) => XPathType::Number,
324            XPathValue::Boolean(_) => XPathType::Boolean,
325        }
326    }
327
328    /// Convert to boolean (XPath 1.0 §3.4).
329    pub fn as_boolean(&self) -> bool {
330        match self {
331            XPathValue::NodeSet(ns) => !ns.is_empty(),
332            XPathValue::String(s) => !s.is_empty(),
333            XPathValue::Number(n) => *n != 0.0 && !n.is_nan(),
334            XPathValue::Boolean(b) => *b,
335        }
336    }
337
338    /// Convert to number (XPath 1.0 §3.5).
339    pub fn as_number(&self) -> f64 {
340        match self {
341            XPathValue::NodeSet(ns) => {
342                // Convert string value of first node to number
343                if let Some(node) = ns.first() {
344                    let s = node_string_value(node);
345                    string_to_number(&s)
346                } else {
347                    f64::NAN
348                }
349            }
350            XPathValue::String(s) => string_to_number(s),
351            XPathValue::Number(n) => *n,
352            XPathValue::Boolean(true) => 1.0,
353            XPathValue::Boolean(false) => 0.0,
354        }
355    }
356
357    /// Convert to string (XPath 1.0 §3.6).
358    pub fn as_string(&self) -> String {
359        match self {
360            XPathValue::NodeSet(ns) => {
361                if let Some(node) = ns.first() {
362                    node_string_value(node)
363                } else {
364                    String::new()
365                }
366            }
367            XPathValue::String(s) => s.clone(),
368            XPathValue::Number(n) => number_to_string(*n),
369            XPathValue::Boolean(true) => "true".to_string(),
370            XPathValue::Boolean(false) => "false".to_string(),
371        }
372    }
373
374    /// Get node-set reference (panics if not a node-set).
375    pub fn as_node_set(&self) -> &NodeSet {
376        match self {
377            XPathValue::NodeSet(ns) => ns,
378            _ => panic!("XPathValue is not a node-set"),
379        }
380    }
381
382    /// Get mutable node-set reference.
383    pub fn as_node_set_mut(&mut self) -> &mut NodeSet {
384        match self {
385            XPathValue::NodeSet(ns) => ns,
386            _ => panic!("XPathValue is not a node-set"),
387        }
388    }
389}
390
391// ═══════════════════════════════════════════════════════════════════════════════
392// String value of a node
393// ═══════════════════════════════════════════════════════════════════════════════
394
395/// Get the string value of a node (XPath 1.0 §5.1).
396///
397/// For element/root nodes: concatenation of all descendant text nodes.
398/// For text nodes: the text content.
399/// For attribute nodes: the attribute value.
400/// For namespace nodes: the namespace URI.
401/// For comment/PI nodes: the content.
402///
403/// # Safety
404///
405/// - `node` must be NULL or a valid `_xmlNode` that stays alive for the
406///   call; its `children` chain (recursed by `collect_text`) and every
407///   `content`/`name` pointer must be NULL or valid NUL-terminated
408///   strings, and the reachable subtree must be acyclic.
409pub fn node_string_value(node: *mut _xmlNode) -> String {
410    if node.is_null() {
411        return String::new();
412    }
413
414    unsafe {
415        let node_ref = &*node;
416        match node_ref.type_ {
417            1..=20 => {}
418            _ => return String::new(),
419        }
420
421        // Element / document / HTML document: concatenate text descendants
422        if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 13 {
423            let mut result = String::new();
424            collect_text(&mut result, node);
425            return result;
426        }
427
428        // Attribute node (type 2): the value is stored as the first text
429        // child of the attribute node (tree::set_prop layout, matching
430        // libxml2's xmlAttr->children). NOTE: type 13 is
431        // XML_HTML_DOCUMENT_NODE, not attribute.
432        if node_ref.type_ == 2 {
433            if !node_ref.children.is_null() {
434                let child = &*node_ref.children;
435                if (child.type_ == 3 || child.type_ == 4) && !child.content.is_null() {
436                    return crate::xml::string::xmlstr_to_string(child.content);
437                }
438            }
439            return String::new();
440        }
441
442        // Text / CDATA
443        if node_ref.type_ == 3 || node_ref.type_ == 4 {
444            if !node_ref.content.is_null() {
445                return crate::xml::string::xmlstr_to_string(node_ref.content);
446            }
447            return String::new();
448        }
449
450        // Comment / PI
451        if node_ref.type_ == 7 {
452            // PI: content
453            if !node_ref.content.is_null() {
454                return crate::xml::string::xmlstr_to_string(node_ref.content);
455            }
456            return String::new();
457        }
458
459        String::new()
460    }
461}
462
463/// Recursively collect text content from element/document nodes.
464unsafe fn collect_text(result: &mut String, node: *mut _xmlNode) {
465    if node.is_null() {
466        return;
467    }
468    let node_ref = &*node;
469
470    // If this is a text or CDATA node, append its content
471    if node_ref.type_ == 3 || node_ref.type_ == 4 {
472        if !node_ref.content.is_null() {
473            result.push_str(&crate::xml::string::xmlstr_to_string(node_ref.content));
474        }
475        return;
476    }
477
478    // For element/document nodes, recurse into children
479    if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 19 {
480        let mut child = node_ref.children;
481        while !child.is_null() {
482            collect_text(result, child);
483            child = (*child).next;
484        }
485    }
486}
487
488// ═══════════════════════════════════════════════════════════════════════════════
489// Number <-> String conversions
490// ═══════════════════════════════════════════════════════════════════════════════
491
492/// Port of upstream xpath.c `xmlXPathStringEvalNumber` (R-000166): the
493/// oracle accumulates digits directly (`ret = ret * 10 + d`), caps the
494/// fraction at MAX_FRAC=20 digits after any leading zeros, applies the
495/// exponent with `pow(10.0, exp)` (underflowing to 0 below the smallest
496/// subnormal, e.g. `5e-324`), accepts XML whitespace around the number, and
497/// returns NaN for anything else — including a leading '+'.
498pub fn string_bytes_to_number(bytes: &[u8]) -> f64 {
499    let len = bytes.len();
500    let mut cur = 0usize;
501    // Skip leading XML whitespace.
502    while cur < len && matches!(bytes[cur], b' ' | b'\t' | b'\n' | b'\r') {
503        cur += 1;
504    }
505    let mut isneg = false;
506    if cur < len && bytes[cur] == b'-' {
507        isneg = true;
508        cur += 1;
509    }
510    if cur >= len || (bytes[cur] != b'.' && !bytes[cur].is_ascii_digit()) {
511        return f64::NAN;
512    }
513
514    let mut ret = 0.0f64;
515    let mut ok = false;
516    while cur < len && bytes[cur].is_ascii_digit() {
517        ret = ret * 10.0 + (bytes[cur] - b'0') as f64;
518        ok = true;
519        cur += 1;
520    }
521
522    let mut frac: i32 = 0;
523    if cur < len && bytes[cur] == b'.' {
524        cur += 1;
525        if (cur >= len || !bytes[cur].is_ascii_digit()) && !ok {
526            return f64::NAN;
527        }
528        while cur < len && bytes[cur] == b'0' {
529            frac += 1;
530            cur += 1;
531        }
532        let max = frac + 20; // MAX_FRAC
533        let mut fraction = 0.0f64;
534        while cur < len && bytes[cur].is_ascii_digit() && frac < max {
535            let v = (bytes[cur] - b'0') as f64;
536            fraction = fraction * 10.0 + v;
537            frac += 1;
538            cur += 1;
539        }
540        fraction /= 10f64.powf(frac as f64);
541        ret += fraction;
542        while cur < len && bytes[cur].is_ascii_digit() {
543            cur += 1;
544        }
545    }
546
547    let mut exponent: i32 = 0;
548    let mut is_exponent_negative = false;
549    if cur < len && (bytes[cur] == b'e' || bytes[cur] == b'E') {
550        cur += 1;
551        if cur < len && bytes[cur] == b'-' {
552            is_exponent_negative = true;
553            cur += 1;
554        } else if cur < len && bytes[cur] == b'+' {
555            cur += 1;
556        }
557        while cur < len && bytes[cur].is_ascii_digit() {
558            if exponent < 1000000 {
559                exponent = exponent * 10 + (bytes[cur] - b'0') as i32;
560            }
561            cur += 1;
562        }
563    }
564    while cur < len && matches!(bytes[cur], b' ' | b'\t' | b'\n' | b'\r') {
565        cur += 1;
566    }
567    if cur != len {
568        return f64::NAN;
569    }
570    if isneg {
571        ret = -ret;
572    }
573    if is_exponent_negative {
574        exponent = -exponent;
575    }
576    ret *= 10f64.powf(exponent as f64);
577    ret
578}
579
580/// Convert a string to a number (XPath 1.0 §4.7.1) — upstream
581/// `xmlXPathStringEvalNumber` semantics.
582pub fn string_to_number(s: &str) -> f64 {
583    string_bytes_to_number(s.as_bytes())
584}
585
586/// Convert a number to a string (XPath 1.0 §4.7.2) — a faithful port of
587/// upstream `xmlXPathCastNumberToString` / `xmlXPathFormatNumber` (xpath.c,
588/// R-000166): the integer shortcut, the 1e9/1e-5 scientific threshold, and
589/// the DBL_DIG=15 fraction-digit computation reproduce the oracle's exact
590/// digits, including exponent formatting (`e+20`, `e-05`) and
591/// trailing-zero trimming.
592pub fn number_to_string(n: f64) -> String {
593    if n.is_nan() {
594        return "NaN".to_string();
595    }
596    if n.is_infinite() {
597        return if n > 0.0 {
598            "Infinity".to_string()
599        } else {
600            "-Infinity".to_string()
601        };
602    }
603    if n == 0.0 {
604        // Both +0 and -0 serialize as "0" per XPath 1.0.
605        return "0".to_string();
606    }
607    // Upstream integer shortcut (xmlXPathFormatNumber): integral values
608    // within the int range print as plain decimal.
609    if n > i32::MIN as f64 && n < i32::MAX as f64 && n == (n as i32) as f64 {
610        return format!("{}", n as i32);
611    }
612
613    let absolute_value = n.abs();
614    let s = if ((absolute_value > 1e9) || (absolute_value < 1e-5)) && absolute_value != 0.0 {
615        // Scientific notation: "%*.*e" with 14 fraction digits, then trim
616        // trailing zeros before the exponent (work[size] == 'e' scan).
617        let raw = format!("{:.14e}", n);
618        let e_pos = raw.find('e').expect("exponent format contains 'e'");
619        let mantissa = &raw[..e_pos];
620        let exponent = &raw[e_pos + 1..];
621        let mut mantissa = mantissa.to_string();
622        while mantissa.ends_with('0') {
623            mantissa.pop();
624        }
625        if mantissa.ends_with('.') {
626            mantissa.pop();
627        }
628        // C's %e pads the exponent to at least two digits and always
629        // includes the sign: "e+20", "e-05", "e+100".
630        let (sign, digits) = if let Some(rest) = exponent.strip_prefix('-') {
631            ("-", rest)
632        } else {
633            ("+", exponent)
634        };
635        let digits = if digits.len() < 2 {
636            format!("0{}", digits)
637        } else {
638            digits.to_string()
639        };
640        format!("{}e{}{}", mantissa, sign, digits)
641    } else {
642        // Regular notation: fraction digits depend on the integer place.
643        let integer_place = absolute_value.log10() as i32;
644        let fraction_place = if integer_place > 0 {
645            15 - integer_place - 1
646        } else {
647            15 - integer_place
648        };
649        let mut s = format!("{:.*}", fraction_place as usize, n);
650        // Trim fractional trailing zeros (and a trailing dot).
651        if s.contains('.') {
652            while s.ends_with('0') {
653                s.pop();
654            }
655            if s.ends_with('.') {
656                s.pop();
657            }
658        }
659        s
660    };
661    if s == "-0" {
662        return "0".to_string();
663    }
664    s
665}
666
667// ═══════════════════════════════════════════════════════════════════════════════
668// Node comparison for document order
669// ═══════════════════════════════════════════════════════════════════════════════
670
671/// Compare two nodes in document order.
672///
673/// Returns:
674/// - `Ordering::Less` if `a` comes before `b` in document order
675/// - `Ordering::Greater` if `a` comes after `b`
676/// - `Ordering::Equal` if `a == b`
677///
678/// UPSTREAM-PARITY: Uses the `xmlXPathCmpNodes` algorithm.
679///
680/// # SAFETY
681///
682/// - `a`, `b` must be valid pointers (or NULL
683///   where the upstream C contract allows), obtained from the
684///   matching constructor/owner and not yet freed; the callee may
685///   take or keep ownership exactly as the C API specifies.
686///
687/// The caller must not race this call with concurrent mutation of the
688/// same objects from other threads (per-object state is not internally
689/// synchronized). Violating any of the above is undefined behavior.
690///
691/// Exercised by the C-API differential courts
692/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
693/// courts; those pass byte-for-byte against the upstream oracle.
694pub unsafe fn compare_document_order(a: *mut _xmlNode, b: *mut _xmlNode) -> Ordering {
695    if a.is_null() && b.is_null() {
696        return Ordering::Equal;
697    }
698    if a.is_null() {
699        return Ordering::Less;
700    }
701    if b.is_null() {
702        return Ordering::Greater;
703    }
704    if a == b {
705        return Ordering::Equal;
706    }
707
708    // UPSTREAM-PARITY (xpath.c xmlXPathCmpNodes): a namespace node in a
709    // node-set is an independent `_xmlNs` copy — its fields are NOT tree
710    // links (the xmlXPathNodeSetDupNs copy has no parent/children), so any
711    // tree walk on it is invalid. Upstream returns 1 for every comparison
712    // involving a namespace node; reporting Equal keeps a stable sort in the
713    // axis emission order (the php-visible contract for `//namespace::*`,
714    // which lists each element's namespace nodes xml-first then reverse
715    // declaration order) and never dereferences the copy's non-node fields.
716    let ta = unsafe { (*a).type_ };
717    let tb = unsafe { (*b).type_ };
718    if ta == xmlElementType::XML_NAMESPACE_DECL as c_int
719        || tb == xmlElementType::XML_NAMESPACE_DECL as c_int
720    {
721        return Ordering::Equal;
722    }
723
724    // Find depths of both nodes
725    let depth_a = node_depth(a);
726    let depth_b = node_depth(b);
727
728    // If one is an ancestor of the other, the ancestor comes first
729    if depth_a < depth_b {
730        let mut n = b;
731        for _ in 0..(depth_b - depth_a) {
732            n = (*n).parent;
733            if n.is_null() {
734                break;
735            }
736        }
737        if n == a {
738            return Ordering::Less;
739        }
740    } else if depth_b < depth_a {
741        let mut n = a;
742        for _ in 0..(depth_a - depth_b) {
743            n = (*n).parent;
744            if n.is_null() {
745                break;
746            }
747        }
748        if n == b {
749            return Ordering::Greater;
750        }
751    }
752
753    // Find the common ancestor and the first differing child
754    let mut parent_a = a;
755    let mut parent_b = b;
756
757    // Move both up to the same depth
758    let mut d_a = depth_a;
759    let mut d_b = depth_b;
760    while d_a > d_b {
761        parent_a = (*parent_a).parent;
762        d_a -= 1;
763    }
764    while d_b > d_a {
765        parent_b = (*parent_b).parent;
766        d_b -= 1;
767    }
768
769    // Move both up until they share the same parent
770    while (*parent_a).parent != (*parent_b).parent {
771        parent_a = (*parent_a).parent;
772        parent_b = (*parent_b).parent;
773        if parent_a.is_null() || parent_b.is_null() {
774            // Fallback: compare by pointer
775            return a.cmp(&b);
776        }
777    }
778
779    // Now parent_a and parent_b are siblings. Find which comes first.
780    let n = (*parent_a).parent;
781    if n.is_null() {
782        return a.cmp(&b);
783    }
784    // UPSTREAM-PARITY (xpath.c xmlXPathCmpNodes): in document order an
785    // ELEMENT's attribute nodes precede its child nodes (so a `node()|@*`
786    // union sorts the attributes first — the XSLT identity copy depends on
787    // copying attributes onto a still childless result element). The
788    // children-list walk below cannot order attributes (they are not in the
789    // list), so resolve attribute siblings explicitly first.
790    let attr_a = (*parent_a).type_ == xmlElementType::XML_ATTRIBUTE_NODE as c_int;
791    let attr_b = (*parent_b).type_ == xmlElementType::XML_ATTRIBUTE_NODE as c_int;
792    if attr_a || attr_b {
793        if attr_a && !attr_b {
794            return Ordering::Less;
795        }
796        if !attr_a && attr_b {
797            return Ordering::Greater;
798        }
799        // Both are attributes of the same element: order by their position in
800        // the properties list.
801        let mut prop = (*n).properties;
802        while !prop.is_null() {
803            if prop as usize == parent_a as usize {
804                return Ordering::Less;
805            }
806            if prop as usize == parent_b as usize {
807                return Ordering::Greater;
808            }
809            prop = (*prop).next;
810        }
811        return a.cmp(&b);
812    }
813    let mut child = (*n).children;
814    while !child.is_null() {
815        if child == parent_a {
816            return Ordering::Less;
817        }
818        if child == parent_b {
819            return Ordering::Greater;
820        }
821        child = (*child).next;
822    }
823
824    // Fallback
825    a.cmp(&b)
826}
827
828/// Compute the depth of a node (root = 0).
829unsafe fn node_depth(node: *mut _xmlNode) -> usize {
830    let mut depth = 0;
831    let mut n = node;
832    while !(*n).parent.is_null() {
833        depth += 1;
834        n = (*n).parent;
835    }
836    depth
837}
838
839// ═══════════════════════════════════════════════════════════════════════════════
840// Tests
841// ═══════════════════════════════════════════════════════════════════════════════
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846    #[allow(clippy::approx_constant)]
847    #[test]
848    fn test_string_to_number() {
849        assert!(string_to_number("").is_nan());
850        assert!(string_to_number("NaN").is_nan());
851        assert_eq!(string_to_number("42"), 42.0);
852        assert_eq!(string_to_number("-42"), -42.0);
853        assert_eq!(string_to_number("3.14"), 3.14);
854        assert_eq!(string_to_number("  42  "), 42.0);
855        assert!(string_to_number("true").is_nan());
856        assert!(string_to_number("false").is_nan());
857        assert_eq!(string_to_number("0"), 0.0);
858    }
859    #[allow(clippy::approx_constant)]
860    #[test]
861    fn test_number_to_string() {
862        assert_eq!(number_to_string(f64::NAN), "NaN");
863        assert_eq!(number_to_string(f64::INFINITY), "Infinity");
864        assert_eq!(number_to_string(f64::NEG_INFINITY), "-Infinity");
865        assert_eq!(number_to_string(0.0), "0");
866        assert_eq!(number_to_string(-0.0), "0");
867        assert_eq!(number_to_string(42.0), "42");
868        assert_eq!(number_to_string(3.14), "3.14");
869    }
870
871    #[test]
872    fn test_value_conversions() {
873        let v = XPathValue::Number(42.0);
874        assert_eq!(v.as_number(), 42.0);
875        assert_eq!(v.as_string(), "42");
876        assert!(v.as_boolean());
877
878        let v = XPathValue::Number(0.0);
879        assert!(!v.as_boolean());
880
881        let v = XPathValue::Number(f64::NAN);
882        assert!(!v.as_boolean());
883
884        let v = XPathValue::String("hello".into());
885        assert_eq!(v.as_string(), "hello");
886        assert!(v.as_boolean());
887
888        let v = XPathValue::String("".into());
889        assert!(!v.as_boolean());
890
891        let v = XPathValue::Boolean(true);
892        assert_eq!(v.as_number(), 1.0);
893        assert_eq!(v.as_string(), "true");
894
895        let v = XPathValue::Boolean(false);
896        assert_eq!(v.as_number(), 0.0);
897        assert_eq!(v.as_string(), "false");
898    }
899
900    #[test]
901    fn test_node_set() {
902        let ns = NodeSet::new();
903        assert!(ns.is_empty());
904        assert_eq!(ns.len(), 0);
905    }
906}