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
15use crate::abi::structs::_xmlNode;
16use crate::abi::types::xmlChar;
17use std::cmp::Ordering;
18use std::ptr;
19
20// ═══════════════════════════════════════════════════════════════════════════════
21// XPath Value Types
22// ═══════════════════════════════════════════════════════════════════════════════
23
24/// XPath type.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum XPathType {
27    NodeSet,
28    String,
29    Number,
30    Boolean,
31    Point,
32    Range,
33    LocationSet,
34    Users,
35    XsltTree,
36    Undefined,
37}
38
39/// A node in a node-set, identified by pointer.
40///
41/// We use raw pointers because:
42/// 1. The tree is owned by the document, not by XPath.
43/// 2. The C ABI exposes node pointers that callers manipulate.
44/// 3. Multiple XPath evaluations may reference the same tree.
45///
46/// SAFETY: Node pointers must remain valid for the duration of evaluation.
47#[derive(Debug, Clone, Copy)]
48pub struct XPathNode(pub *mut _xmlNode);
49
50impl PartialEq for XPathNode {
51    fn eq(&self, other: &Self) -> bool {
52        self.0 == other.0
53    }
54}
55
56impl Eq for XPathNode {}
57
58impl PartialOrd for XPathNode {
59    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
60        Some(self.cmp(other))
61    }
62}
63
64impl Ord for XPathNode {
65    fn cmp(&self, other: &Self) -> Ordering {
66        // Compare by pointer value for document order
67        // In a full implementation, this would use the document order algorithm
68        self.0.cmp(&other.0)
69    }
70}
71
72impl std::hash::Hash for XPathNode {
73    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
74        self.0.hash(state);
75    }
76}
77
78/// An XPath node-set.
79///
80/// Internally stored as a sorted, deduplicated Vec of node pointers
81/// in document order.
82#[derive(Debug, Clone)]
83pub struct NodeSet {
84    nodes: Vec<XPathNode>,
85}
86
87impl NodeSet {
88    pub fn new() -> Self {
89        Self { nodes: Vec::new() }
90    }
91
92    pub fn singleton(node: *mut _xmlNode) -> Self {
93        Self {
94            nodes: vec![XPathNode(node)],
95        }
96    }
97
98    pub fn is_empty(&self) -> bool {
99        self.nodes.is_empty()
100    }
101
102    pub fn len(&self) -> usize {
103        self.nodes.len()
104    }
105
106    pub fn iter(&self) -> impl Iterator<Item = *mut _xmlNode> + '_ {
107        self.nodes.iter().map(|n| n.0)
108    }
109
110    pub fn get(&self, index: usize) -> Option<*mut _xmlNode> {
111        self.nodes.get(index).map(|n| n.0)
112    }
113
114    pub fn first(&self) -> Option<*mut _xmlNode> {
115        self.nodes.first().map(|n| n.0)
116    }
117
118    pub fn last(&self) -> Option<*mut _xmlNode> {
119        self.nodes.last().map(|n| n.0)
120    }
121
122    pub fn contains(&self, node: *mut _xmlNode) -> bool {
123        self.nodes.iter().any(|n| n.0 == node)
124    }
125
126    /// Add a node to the set, maintaining document order and uniqueness.
127    pub fn push(&mut self, node: *mut _xmlNode) {
128        if !self.nodes.iter().any(|n| n.0 == node) {
129            self.nodes.push(XPathNode(node));
130            self.sort();
131        }
132    }
133
134    /// Extend with another node-set.
135    pub fn extend(&mut self, other: &NodeSet) {
136        for node in other.iter() {
137            self.push(node);
138        }
139    }
140
141    /// Sort nodes in document order.
142    ///
143    /// # UPSTREAM-PARITY
144    ///
145    /// XPath node-sets are always in document order (XPath 1.0 §3.3).
146    /// libxml2 maintains this via its node-set insertion/merge logic plus
147    /// the document-order comparator (xmlXPathNodeSetSort). Sorting by
148    /// pointer address is NOT document order and breaks downstream ordering
149    /// guarantees; the oracle-observed symptom is rotated results on the
150    /// second of two transforms in one process.
151    pub fn sort(&mut self) {
152        self.nodes
153            .sort_by(|a, b| unsafe { compare_document_order(a.0, b.0) });
154        self.nodes.dedup();
155    }
156
157    /// Convert to raw C ABI node-set.
158    ///
159    /// SAFETY: The returned pointer must be freed with xmlXPathFreeNodeSet
160    /// or the owning XPath object must be freed.
161    pub unsafe fn to_raw(&self) -> *mut crate::abi::structs::_xmlNodeSet {
162        let node_max = self.nodes.len();
163        let node_tab = if node_max > 0 {
164            let ptr = crate::abi::allocator::xmlMallocImpl(
165                node_max * std::mem::size_of::<*mut _xmlNode>(),
166            ) as *mut *mut _xmlNode;
167            if ptr.is_null() {
168                return ptr::null_mut();
169            }
170            for (i, node) in self.nodes.iter().enumerate() {
171                ptr::write(ptr.add(i), node.0);
172            }
173            ptr
174        } else {
175            ptr::null_mut()
176        };
177
178        let raw = crate::abi::allocator::xmlMallocImpl(std::mem::size_of::<
179            crate::abi::structs::_xmlNodeSet,
180        >()) as *mut crate::abi::structs::_xmlNodeSet;
181        if raw.is_null() {
182            if !node_tab.is_null() {
183                crate::abi::allocator::xmlFreeImpl(node_tab as *mut _);
184            }
185            return ptr::null_mut();
186        }
187        ptr::write(
188            raw,
189            crate::abi::structs::_xmlNodeSet {
190                nodeNr: node_max as std::os::raw::c_int,
191                nodeMax: node_max as std::os::raw::c_int,
192                nodeTab: node_tab,
193            },
194        );
195        raw
196    }
197}
198
199impl Default for NodeSet {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205/// XPath runtime value.
206#[derive(Debug, Clone)]
207pub enum XPathValue {
208    NodeSet(NodeSet),
209    String(String),
210    Number(f64),
211    Boolean(bool),
212}
213
214impl XPathValue {
215    /// Get the XPath type of this value.
216    pub fn xpath_type(&self) -> XPathType {
217        match self {
218            XPathValue::NodeSet(_) => XPathType::NodeSet,
219            XPathValue::String(_) => XPathType::String,
220            XPathValue::Number(_) => XPathType::Number,
221            XPathValue::Boolean(_) => XPathType::Boolean,
222        }
223    }
224
225    /// Convert to boolean (XPath 1.0 §3.4).
226    pub fn as_boolean(&self) -> bool {
227        match self {
228            XPathValue::NodeSet(ns) => !ns.is_empty(),
229            XPathValue::String(s) => !s.is_empty(),
230            XPathValue::Number(n) => *n != 0.0 && !n.is_nan(),
231            XPathValue::Boolean(b) => *b,
232        }
233    }
234
235    /// Convert to number (XPath 1.0 §3.5).
236    pub fn as_number(&self) -> f64 {
237        match self {
238            XPathValue::NodeSet(ns) => {
239                // Convert string value of first node to number
240                if let Some(node) = ns.first() {
241                    let s = node_string_value(node);
242                    string_to_number(&s)
243                } else {
244                    f64::NAN
245                }
246            }
247            XPathValue::String(s) => string_to_number(s),
248            XPathValue::Number(n) => *n,
249            XPathValue::Boolean(true) => 1.0,
250            XPathValue::Boolean(false) => 0.0,
251        }
252    }
253
254    /// Convert to string (XPath 1.0 §3.6).
255    pub fn as_string(&self) -> String {
256        match self {
257            XPathValue::NodeSet(ns) => {
258                if let Some(node) = ns.first() {
259                    node_string_value(node)
260                } else {
261                    String::new()
262                }
263            }
264            XPathValue::String(s) => s.clone(),
265            XPathValue::Number(n) => number_to_string(*n),
266            XPathValue::Boolean(true) => "true".to_string(),
267            XPathValue::Boolean(false) => "false".to_string(),
268        }
269    }
270
271    /// Get node-set reference (panics if not a node-set).
272    pub fn as_node_set(&self) -> &NodeSet {
273        match self {
274            XPathValue::NodeSet(ns) => ns,
275            _ => panic!("XPathValue is not a node-set"),
276        }
277    }
278
279    /// Get mutable node-set reference.
280    pub fn as_node_set_mut(&mut self) -> &mut NodeSet {
281        match self {
282            XPathValue::NodeSet(ns) => ns,
283            _ => panic!("XPathValue is not a node-set"),
284        }
285    }
286}
287
288// ═══════════════════════════════════════════════════════════════════════════════
289// String value of a node
290// ═══════════════════════════════════════════════════════════════════════════════
291
292/// Get the string value of a node (XPath 1.0 §5.1).
293///
294/// For element/root nodes: concatenation of all descendant text nodes.
295/// For text nodes: the text content.
296/// For attribute nodes: the attribute value.
297/// For namespace nodes: the namespace URI.
298/// For comment/PI nodes: the content.
299pub fn node_string_value(node: *mut _xmlNode) -> String {
300    if node.is_null() {
301        return String::new();
302    }
303
304    unsafe {
305        let node_ref = &*node;
306        match node_ref.type_ {
307            1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19
308            | 20 => {}
309            _ => return String::new(),
310        }
311
312        // Element / document / HTML document: concatenate text descendants
313        if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 13 {
314            let mut result = String::new();
315            collect_text(&mut result, node);
316            return result;
317        }
318
319        // Attribute node (type 2): the value is stored as the first text
320        // child of the attribute node (tree::set_prop layout, matching
321        // libxml2's xmlAttr->children). NOTE: type 13 is
322        // XML_HTML_DOCUMENT_NODE, not attribute.
323        if node_ref.type_ == 2 {
324            if !node_ref.children.is_null() {
325                let child = &*node_ref.children;
326                if (child.type_ == 3 || child.type_ == 4) && !child.content.is_null() {
327                    return crate::xml::string::xmlstr_to_string(child.content);
328                }
329            }
330            return String::new();
331        }
332
333        // Text / CDATA
334        if node_ref.type_ == 3 || node_ref.type_ == 4 {
335            if !node_ref.content.is_null() {
336                return crate::xml::string::xmlstr_to_string(node_ref.content);
337            }
338            return String::new();
339        }
340
341        // Comment / PI
342        if node_ref.type_ == 7 {
343            // PI: content
344            if !node_ref.content.is_null() {
345                return crate::xml::string::xmlstr_to_string(node_ref.content);
346            }
347            return String::new();
348        }
349
350        String::new()
351    }
352}
353
354/// Recursively collect text content from element/document nodes.
355unsafe fn collect_text(result: &mut String, node: *mut _xmlNode) {
356    if node.is_null() {
357        return;
358    }
359    let node_ref = &*node;
360
361    // If this is a text or CDATA node, append its content
362    if node_ref.type_ == 3 || node_ref.type_ == 4 {
363        if !node_ref.content.is_null() {
364            result.push_str(&crate::xml::string::xmlstr_to_string(node_ref.content));
365        }
366        return;
367    }
368
369    // For element/document nodes, recurse into children
370    if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 19 {
371        let mut child = node_ref.children;
372        while !child.is_null() {
373            collect_text(result, child);
374            child = (*child).next;
375        }
376    }
377}
378
379// ═══════════════════════════════════════════════════════════════════════════════
380// Number <-> String conversions
381// ═══════════════════════════════════════════════════════════════════════════════
382
383/// Port of upstream xpath.c `xmlXPathStringEvalNumber` (R-000166): the
384/// oracle accumulates digits directly (`ret = ret * 10 + d`), caps the
385/// fraction at MAX_FRAC=20 digits after any leading zeros, applies the
386/// exponent with `pow(10.0, exp)` (underflowing to 0 below the smallest
387/// subnormal, e.g. `5e-324`), accepts XML whitespace around the number, and
388/// returns NaN for anything else — including a leading '+'.
389pub fn string_bytes_to_number(bytes: &[u8]) -> f64 {
390    let len = bytes.len();
391    let mut cur = 0usize;
392    // Skip leading XML whitespace.
393    while cur < len && matches!(bytes[cur], b' ' | b'\t' | b'\n' | b'\r') {
394        cur += 1;
395    }
396    let mut isneg = false;
397    if cur < len && bytes[cur] == b'-' {
398        isneg = true;
399        cur += 1;
400    }
401    if cur >= len || (bytes[cur] != b'.' && !bytes[cur].is_ascii_digit()) {
402        return f64::NAN;
403    }
404
405    let mut ret = 0.0f64;
406    let mut ok = false;
407    while cur < len && bytes[cur].is_ascii_digit() {
408        ret = ret * 10.0 + (bytes[cur] - b'0') as f64;
409        ok = true;
410        cur += 1;
411    }
412
413    let mut frac: i32 = 0;
414    if cur < len && bytes[cur] == b'.' {
415        cur += 1;
416        if (cur >= len || !bytes[cur].is_ascii_digit()) && !ok {
417            return f64::NAN;
418        }
419        while cur < len && bytes[cur] == b'0' {
420            frac += 1;
421            cur += 1;
422        }
423        let max = frac + 20; // MAX_FRAC
424        let mut fraction = 0.0f64;
425        while cur < len && bytes[cur].is_ascii_digit() && frac < max {
426            let v = (bytes[cur] - b'0') as f64;
427            fraction = fraction * 10.0 + v;
428            frac += 1;
429            cur += 1;
430        }
431        fraction /= 10f64.powf(frac as f64);
432        ret += fraction;
433        while cur < len && bytes[cur].is_ascii_digit() {
434            cur += 1;
435        }
436    }
437
438    let mut exponent: i32 = 0;
439    let mut is_exponent_negative = false;
440    if cur < len && (bytes[cur] == b'e' || bytes[cur] == b'E') {
441        cur += 1;
442        if cur < len && bytes[cur] == b'-' {
443            is_exponent_negative = true;
444            cur += 1;
445        } else if cur < len && bytes[cur] == b'+' {
446            cur += 1;
447        }
448        while cur < len && bytes[cur].is_ascii_digit() {
449            if exponent < 1000000 {
450                exponent = exponent * 10 + (bytes[cur] - b'0') as i32;
451            }
452            cur += 1;
453        }
454    }
455    while cur < len && matches!(bytes[cur], b' ' | b'\t' | b'\n' | b'\r') {
456        cur += 1;
457    }
458    if cur != len {
459        return f64::NAN;
460    }
461    if isneg {
462        ret = -ret;
463    }
464    if is_exponent_negative {
465        exponent = -exponent;
466    }
467    ret *= 10f64.powf(exponent as f64);
468    ret
469}
470
471/// Convert a string to a number (XPath 1.0 §4.7.1) — upstream
472/// `xmlXPathStringEvalNumber` semantics.
473pub fn string_to_number(s: &str) -> f64 {
474    string_bytes_to_number(s.as_bytes())
475}
476
477/// Convert a number to a string (XPath 1.0 §4.7.2) — a faithful port of
478/// upstream `xmlXPathCastNumberToString` / `xmlXPathFormatNumber` (xpath.c,
479/// R-000166): the integer shortcut, the 1e9/1e-5 scientific threshold, and
480/// the DBL_DIG=15 fraction-digit computation reproduce the oracle's exact
481/// digits, including exponent formatting (`e+20`, `e-05`) and
482/// trailing-zero trimming.
483pub fn number_to_string(n: f64) -> String {
484    if n.is_nan() {
485        return "NaN".to_string();
486    }
487    if n.is_infinite() {
488        return if n > 0.0 {
489            "Infinity".to_string()
490        } else {
491            "-Infinity".to_string()
492        };
493    }
494    if n == 0.0 {
495        // Both +0 and -0 serialize as "0" per XPath 1.0.
496        return "0".to_string();
497    }
498    // Upstream integer shortcut (xmlXPathFormatNumber): integral values
499    // within the int range print as plain decimal.
500    if n > i32::MIN as f64 && n < i32::MAX as f64 && n == (n as i32) as f64 {
501        return format!("{}", n as i32);
502    }
503
504    let absolute_value = n.abs();
505    let s = if ((absolute_value > 1e9) || (absolute_value < 1e-5)) && absolute_value != 0.0 {
506        // Scientific notation: "%*.*e" with 14 fraction digits, then trim
507        // trailing zeros before the exponent (work[size] == 'e' scan).
508        let raw = format!("{:.14e}", n);
509        let e_pos = raw.find('e').expect("exponent format contains 'e'");
510        let mantissa = &raw[..e_pos];
511        let exponent = &raw[e_pos + 1..];
512        let mut mantissa = mantissa.to_string();
513        while mantissa.ends_with('0') {
514            mantissa.pop();
515        }
516        if mantissa.ends_with('.') {
517            mantissa.pop();
518        }
519        // C's %e pads the exponent to at least two digits and always
520        // includes the sign: "e+20", "e-05", "e+100".
521        let (sign, digits) = if let Some(rest) = exponent.strip_prefix('-') {
522            ("-", rest)
523        } else {
524            ("+", exponent)
525        };
526        let digits = if digits.len() < 2 {
527            format!("0{}", digits)
528        } else {
529            digits.to_string()
530        };
531        format!("{}e{}{}", mantissa, sign, digits)
532    } else {
533        // Regular notation: fraction digits depend on the integer place.
534        let integer_place = absolute_value.log10() as i32;
535        let fraction_place = if integer_place > 0 {
536            15 - integer_place - 1
537        } else {
538            15 - integer_place
539        };
540        let mut s = format!("{:.*}", fraction_place as usize, n);
541        // Trim fractional trailing zeros (and a trailing dot).
542        if s.contains('.') {
543            while s.ends_with('0') {
544                s.pop();
545            }
546            if s.ends_with('.') {
547                s.pop();
548            }
549        }
550        s
551    };
552    if s == "-0" {
553        return "0".to_string();
554    }
555    s
556}
557
558// ═══════════════════════════════════════════════════════════════════════════════
559// Node comparison for document order
560// ═══════════════════════════════════════════════════════════════════════════════
561
562/// Compare two nodes in document order.
563///
564/// Returns:
565/// - `Ordering::Less` if `a` comes before `b` in document order
566/// - `Ordering::Greater` if `a` comes after `b`
567/// - `Ordering::Equal` if `a == b`
568///
569/// UPSTREAM-PARITY: Uses the `xmlXPathCmpNodes` algorithm.
570pub unsafe fn compare_document_order(a: *mut _xmlNode, b: *mut _xmlNode) -> Ordering {
571    if a.is_null() && b.is_null() {
572        return Ordering::Equal;
573    }
574    if a.is_null() {
575        return Ordering::Less;
576    }
577    if b.is_null() {
578        return Ordering::Greater;
579    }
580    if a == b {
581        return Ordering::Equal;
582    }
583
584    // Find depths of both nodes
585    let depth_a = node_depth(a);
586    let depth_b = node_depth(b);
587
588    // If one is an ancestor of the other, the ancestor comes first
589    if depth_a < depth_b {
590        let mut n = b;
591        for _ in 0..(depth_b - depth_a) {
592            n = (*n).parent;
593            if n.is_null() {
594                break;
595            }
596        }
597        if n == a {
598            return Ordering::Less;
599        }
600    } else if depth_b < depth_a {
601        let mut n = a;
602        for _ in 0..(depth_a - depth_b) {
603            n = (*n).parent;
604            if n.is_null() {
605                break;
606            }
607        }
608        if n == b {
609            return Ordering::Greater;
610        }
611    }
612
613    // Find the common ancestor and the first differing child
614    let mut parent_a = a;
615    let mut parent_b = b;
616
617    // Move both up to the same depth
618    let mut d_a = depth_a;
619    let mut d_b = depth_b;
620    while d_a > d_b {
621        parent_a = (*parent_a).parent;
622        d_a -= 1;
623    }
624    while d_b > d_a {
625        parent_b = (*parent_b).parent;
626        d_b -= 1;
627    }
628
629    // Move both up until they share the same parent
630    while (*parent_a).parent != (*parent_b).parent {
631        parent_a = (*parent_a).parent;
632        parent_b = (*parent_b).parent;
633        if parent_a.is_null() || parent_b.is_null() {
634            // Fallback: compare by pointer
635            return a.cmp(&b);
636        }
637    }
638
639    // Now parent_a and parent_b are siblings. Find which comes first.
640    let mut n = (*parent_a).parent;
641    if n.is_null() {
642        return a.cmp(&b);
643    }
644    let mut child = (*n).children;
645    while !child.is_null() {
646        if child == parent_a {
647            return Ordering::Less;
648        }
649        if child == parent_b {
650            return Ordering::Greater;
651        }
652        child = (*child).next;
653    }
654
655    // Fallback
656    a.cmp(&b)
657}
658
659/// Compute the depth of a node (root = 0).
660unsafe fn node_depth(node: *mut _xmlNode) -> usize {
661    let mut depth = 0;
662    let mut n = node;
663    while !(*n).parent.is_null() {
664        depth += 1;
665        n = (*n).parent;
666    }
667    depth
668}
669
670// ═══════════════════════════════════════════════════════════════════════════════
671// Tests
672// ═══════════════════════════════════════════════════════════════════════════════
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    #[test]
679    fn test_string_to_number() {
680        assert!(string_to_number("").is_nan());
681        assert!(string_to_number("NaN").is_nan());
682        assert_eq!(string_to_number("42"), 42.0);
683        assert_eq!(string_to_number("-42"), -42.0);
684        assert_eq!(string_to_number("3.14"), 3.14);
685        assert_eq!(string_to_number("  42  "), 42.0);
686        assert!(string_to_number("true").is_nan());
687        assert!(string_to_number("false").is_nan());
688        assert_eq!(string_to_number("0"), 0.0);
689    }
690
691    #[test]
692    fn test_number_to_string() {
693        assert_eq!(number_to_string(f64::NAN), "NaN");
694        assert_eq!(number_to_string(f64::INFINITY), "Infinity");
695        assert_eq!(number_to_string(f64::NEG_INFINITY), "-Infinity");
696        assert_eq!(number_to_string(0.0), "0");
697        assert_eq!(number_to_string(-0.0), "0");
698        assert_eq!(number_to_string(42.0), "42");
699        assert_eq!(number_to_string(3.14), "3.14");
700    }
701
702    #[test]
703    fn test_value_conversions() {
704        let v = XPathValue::Number(42.0);
705        assert_eq!(v.as_number(), 42.0);
706        assert_eq!(v.as_string(), "42");
707        assert_eq!(v.as_boolean(), true);
708
709        let v = XPathValue::Number(0.0);
710        assert_eq!(v.as_boolean(), false);
711
712        let v = XPathValue::Number(f64::NAN);
713        assert_eq!(v.as_boolean(), false);
714
715        let v = XPathValue::String("hello".into());
716        assert_eq!(v.as_string(), "hello");
717        assert_eq!(v.as_boolean(), true);
718
719        let v = XPathValue::String("".into());
720        assert_eq!(v.as_boolean(), false);
721
722        let v = XPathValue::Boolean(true);
723        assert_eq!(v.as_number(), 1.0);
724        assert_eq!(v.as_string(), "true");
725
726        let v = XPathValue::Boolean(false);
727        assert_eq!(v.as_number(), 0.0);
728        assert_eq!(v.as_string(), "false");
729    }
730
731    #[test]
732    fn test_node_set() {
733        let mut ns = NodeSet::new();
734        assert!(ns.is_empty());
735        assert_eq!(ns.len(), 0);
736    }
737}