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    pub fn sort(&mut self) {
143        self.nodes.sort_by(|a, b| {
144            // Document order: compare by pointer for now.
145            // A full implementation uses the document order algorithm.
146            // Pointer comparison gives us a consistent order per evaluation.
147            a.0.cmp(&b.0)
148        });
149        self.nodes.dedup();
150    }
151
152    /// Convert to raw C ABI node-set.
153    ///
154    /// SAFETY: The returned pointer must be freed with xmlXPathFreeNodeSet
155    /// or the owning XPath object must be freed.
156    pub unsafe fn to_raw(&self) -> *mut crate::abi::structs::_xmlNodeSet {
157        let node_max = self.nodes.len();
158        let node_tab = if node_max > 0 {
159            let ptr =
160                crate::abi::allocator::xmlMalloc(node_max * std::mem::size_of::<*mut _xmlNode>())
161                    as *mut *mut _xmlNode;
162            if ptr.is_null() {
163                return ptr::null_mut();
164            }
165            for (i, node) in self.nodes.iter().enumerate() {
166                ptr::write(ptr.add(i), node.0);
167            }
168            ptr
169        } else {
170            ptr::null_mut()
171        };
172
173        let raw = crate::abi::allocator::xmlMalloc(std::mem::size_of::<
174            crate::abi::structs::_xmlNodeSet,
175        >()) as *mut crate::abi::structs::_xmlNodeSet;
176        if raw.is_null() {
177            if !node_tab.is_null() {
178                crate::abi::allocator::xmlFree(node_tab as *mut _);
179            }
180            return ptr::null_mut();
181        }
182        ptr::write(
183            raw,
184            crate::abi::structs::_xmlNodeSet {
185                nodeNr: node_max as std::os::raw::c_int,
186                nodeMax: node_max as std::os::raw::c_int,
187                nodeTab: node_tab,
188            },
189        );
190        raw
191    }
192}
193
194impl Default for NodeSet {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200/// XPath runtime value.
201#[derive(Debug, Clone)]
202pub enum XPathValue {
203    NodeSet(NodeSet),
204    String(String),
205    Number(f64),
206    Boolean(bool),
207}
208
209impl XPathValue {
210    /// Get the XPath type of this value.
211    pub fn xpath_type(&self) -> XPathType {
212        match self {
213            XPathValue::NodeSet(_) => XPathType::NodeSet,
214            XPathValue::String(_) => XPathType::String,
215            XPathValue::Number(_) => XPathType::Number,
216            XPathValue::Boolean(_) => XPathType::Boolean,
217        }
218    }
219
220    /// Convert to boolean (XPath 1.0 §3.4).
221    pub fn as_boolean(&self) -> bool {
222        match self {
223            XPathValue::NodeSet(ns) => !ns.is_empty(),
224            XPathValue::String(s) => !s.is_empty(),
225            XPathValue::Number(n) => *n != 0.0 && !n.is_nan(),
226            XPathValue::Boolean(b) => *b,
227        }
228    }
229
230    /// Convert to number (XPath 1.0 §3.5).
231    pub fn as_number(&self) -> f64 {
232        match self {
233            XPathValue::NodeSet(ns) => {
234                // Convert string value of first node to number
235                if let Some(node) = ns.first() {
236                    let s = node_string_value(node);
237                    string_to_number(&s)
238                } else {
239                    f64::NAN
240                }
241            }
242            XPathValue::String(s) => string_to_number(s),
243            XPathValue::Number(n) => *n,
244            XPathValue::Boolean(true) => 1.0,
245            XPathValue::Boolean(false) => 0.0,
246        }
247    }
248
249    /// Convert to string (XPath 1.0 §3.6).
250    pub fn as_string(&self) -> String {
251        match self {
252            XPathValue::NodeSet(ns) => {
253                if let Some(node) = ns.first() {
254                    node_string_value(node)
255                } else {
256                    String::new()
257                }
258            }
259            XPathValue::String(s) => s.clone(),
260            XPathValue::Number(n) => number_to_string(*n),
261            XPathValue::Boolean(true) => "true".to_string(),
262            XPathValue::Boolean(false) => "false".to_string(),
263        }
264    }
265
266    /// Get node-set reference (panics if not a node-set).
267    pub fn as_node_set(&self) -> &NodeSet {
268        match self {
269            XPathValue::NodeSet(ns) => ns,
270            _ => panic!("XPathValue is not a node-set"),
271        }
272    }
273
274    /// Get mutable node-set reference.
275    pub fn as_node_set_mut(&mut self) -> &mut NodeSet {
276        match self {
277            XPathValue::NodeSet(ns) => ns,
278            _ => panic!("XPathValue is not a node-set"),
279        }
280    }
281}
282
283// ═══════════════════════════════════════════════════════════════════════════════
284// String value of a node
285// ═══════════════════════════════════════════════════════════════════════════════
286
287/// Get the string value of a node (XPath 1.0 §5.1).
288///
289/// For element/root nodes: concatenation of all descendant text nodes.
290/// For text nodes: the text content.
291/// For attribute nodes: the attribute value.
292/// For namespace nodes: the namespace URI.
293/// For comment/PI nodes: the content.
294pub fn node_string_value(node: *mut _xmlNode) -> String {
295    if node.is_null() {
296        return String::new();
297    }
298
299    unsafe {
300        let node_ref = &*node;
301        match node_ref.type_ {
302            1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19
303            | 20 => {}
304            _ => return String::new(),
305        }
306
307        // Element node (type 1): concatenate text descendants
308        if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 13 {
309            // Root / element / attribute: collect text content
310            if node_ref.type_ == 13 {
311                // Attribute
312                if !node_ref.content.is_null() {
313                    return crate::xml::string::xmlstr_to_string(node_ref.content);
314                }
315                return String::new();
316            }
317            // Element or document node: concatenate text descendants
318            let mut result = String::new();
319            collect_text(&mut result, node);
320            return result;
321        }
322
323        // Text / CDATA
324        if node_ref.type_ == 3 || node_ref.type_ == 4 {
325            if !node_ref.content.is_null() {
326                return crate::xml::string::xmlstr_to_string(node_ref.content);
327            }
328            return String::new();
329        }
330
331        // Comment / PI
332        if node_ref.type_ == 7 {
333            // PI: content
334            if !node_ref.content.is_null() {
335                return crate::xml::string::xmlstr_to_string(node_ref.content);
336            }
337            return String::new();
338        }
339
340        String::new()
341    }
342}
343
344/// Recursively collect text content from element/document nodes.
345unsafe fn collect_text(result: &mut String, node: *mut _xmlNode) {
346    if node.is_null() {
347        return;
348    }
349    let node_ref = &*node;
350
351    // If this is a text or CDATA node, append its content
352    if node_ref.type_ == 3 || node_ref.type_ == 4 {
353        if !node_ref.content.is_null() {
354            result.push_str(&crate::xml::string::xmlstr_to_string(node_ref.content));
355        }
356        return;
357    }
358
359    // For element/document nodes, recurse into children
360    if node_ref.type_ == 1 || node_ref.type_ == 9 || node_ref.type_ == 19 {
361        let mut child = node_ref.children;
362        while !child.is_null() {
363            collect_text(result, child);
364            child = (*child).next;
365        }
366    }
367}
368
369// ═══════════════════════════════════════════════════════════════════════════════
370// Number <-> String conversions
371// ═══════════════════════════════════════════════════════════════════════════════
372
373/// Convert a string to a number (XPath 1.0 §4.7.1).
374pub fn string_to_number(s: &str) -> f64 {
375    let s = s.trim();
376    if s.is_empty() {
377        return f64::NAN;
378    }
379    // Handle IEEE special values
380    match s {
381        "NaN" => return f64::NAN,
382        "Infinity" | "INF" => return f64::INFINITY,
383        "-Infinity" | "-INF" => return f64::NEG_INFINITY,
384        _ => {}
385    }
386    // Try parsing as a number
387    if let Ok(n) = s.parse::<f64>() {
388        n
389    } else {
390        f64::NAN
391    }
392}
393
394/// Convert a number to a string (XPath 1.0 §4.7.2).
395///
396/// UPSTREAM-PARITY:
397/// - NaN → "NaN"
398/// - +0 → "0"
399/// - -0 → "0" (XPath says negative zero stringifies as "0")
400/// - Infinity → "Infinity"
401/// - -Infinity → "-Infinity"
402/// - Integer → no decimal point: "42"
403/// - Non-integer → at least one digit after decimal: "3.14"
404pub fn number_to_string(n: f64) -> String {
405    if n.is_nan() {
406        return "NaN".to_string();
407    }
408    if n.is_infinite() {
409        if n.is_sign_negative() {
410            return "-Infinity".to_string();
411        }
412        return "Infinity".to_string();
413    }
414    if n == 0.0 {
415        return "0".to_string();
416    }
417
418    // For integers, no decimal point
419    if n.fract() == 0.0 && n.is_finite() {
420        // Check if it's within safe integer range
421        if n.abs() < 1e16 {
422            return format!("{:.0}", n);
423        }
424    }
425
426    // Format with minimal decimal places
427    let s = format!("{:.15}", n);
428    // Trim trailing zeros
429    let trimmed = s.trim_end_matches('0');
430    // Ensure at least one digit after decimal
431    if trimmed.ends_with('.') {
432        format!("{}0", trimmed)
433    } else {
434        trimmed.to_string()
435    }
436}
437
438// ═══════════════════════════════════════════════════════════════════════════════
439// Node comparison for document order
440// ═══════════════════════════════════════════════════════════════════════════════
441
442/// Compare two nodes in document order.
443///
444/// Returns:
445/// - `Ordering::Less` if `a` comes before `b` in document order
446/// - `Ordering::Greater` if `a` comes after `b`
447/// - `Ordering::Equal` if `a == b`
448///
449/// UPSTREAM-PARITY: Uses the `xmlXPathCmpNodes` algorithm.
450pub unsafe fn compare_document_order(a: *mut _xmlNode, b: *mut _xmlNode) -> Ordering {
451    if a.is_null() && b.is_null() {
452        return Ordering::Equal;
453    }
454    if a.is_null() {
455        return Ordering::Less;
456    }
457    if b.is_null() {
458        return Ordering::Greater;
459    }
460    if a == b {
461        return Ordering::Equal;
462    }
463
464    // Find depths of both nodes
465    let depth_a = node_depth(a);
466    let depth_b = node_depth(b);
467
468    // If one is an ancestor of the other, the ancestor comes first
469    if depth_a < depth_b {
470        let mut n = b;
471        for _ in 0..(depth_b - depth_a) {
472            n = (*n).parent;
473            if n.is_null() {
474                break;
475            }
476        }
477        if n == a {
478            return Ordering::Less;
479        }
480    } else if depth_b < depth_a {
481        let mut n = a;
482        for _ in 0..(depth_a - depth_b) {
483            n = (*n).parent;
484            if n.is_null() {
485                break;
486            }
487        }
488        if n == b {
489            return Ordering::Greater;
490        }
491    }
492
493    // Find the common ancestor and the first differing child
494    let mut parent_a = a;
495    let mut parent_b = b;
496
497    // Move both up to the same depth
498    let mut d_a = depth_a;
499    let mut d_b = depth_b;
500    while d_a > d_b {
501        parent_a = (*parent_a).parent;
502        d_a -= 1;
503    }
504    while d_b > d_a {
505        parent_b = (*parent_b).parent;
506        d_b -= 1;
507    }
508
509    // Move both up until they share the same parent
510    while (*parent_a).parent != (*parent_b).parent {
511        parent_a = (*parent_a).parent;
512        parent_b = (*parent_b).parent;
513        if parent_a.is_null() || parent_b.is_null() {
514            // Fallback: compare by pointer
515            return a.cmp(&b);
516        }
517    }
518
519    // Now parent_a and parent_b are siblings. Find which comes first.
520    let mut n = (*parent_a).parent;
521    if n.is_null() {
522        return a.cmp(&b);
523    }
524    let mut child = (*n).children;
525    while !child.is_null() {
526        if child == parent_a {
527            return Ordering::Less;
528        }
529        if child == parent_b {
530            return Ordering::Greater;
531        }
532        child = (*child).next;
533    }
534
535    // Fallback
536    a.cmp(&b)
537}
538
539/// Compute the depth of a node (root = 0).
540unsafe fn node_depth(node: *mut _xmlNode) -> usize {
541    let mut depth = 0;
542    let mut n = node;
543    while !(*n).parent.is_null() {
544        depth += 1;
545        n = (*n).parent;
546    }
547    depth
548}
549
550// ═══════════════════════════════════════════════════════════════════════════════
551// Tests
552// ═══════════════════════════════════════════════════════════════════════════════
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_string_to_number() {
560        assert!(string_to_number("").is_nan());
561        assert!(string_to_number("NaN").is_nan());
562        assert_eq!(string_to_number("42"), 42.0);
563        assert_eq!(string_to_number("-42"), -42.0);
564        assert_eq!(string_to_number("3.14"), 3.14);
565        assert_eq!(string_to_number("  42  "), 42.0);
566        assert!(string_to_number("true").is_nan());
567        assert!(string_to_number("false").is_nan());
568        assert_eq!(string_to_number("0"), 0.0);
569    }
570
571    #[test]
572    fn test_number_to_string() {
573        assert_eq!(number_to_string(f64::NAN), "NaN");
574        assert_eq!(number_to_string(f64::INFINITY), "Infinity");
575        assert_eq!(number_to_string(f64::NEG_INFINITY), "-Infinity");
576        assert_eq!(number_to_string(0.0), "0");
577        assert_eq!(number_to_string(-0.0), "0");
578        assert_eq!(number_to_string(42.0), "42");
579        assert_eq!(number_to_string(3.14), "3.14");
580    }
581
582    #[test]
583    fn test_value_conversions() {
584        let v = XPathValue::Number(42.0);
585        assert_eq!(v.as_number(), 42.0);
586        assert_eq!(v.as_string(), "42");
587        assert_eq!(v.as_boolean(), true);
588
589        let v = XPathValue::Number(0.0);
590        assert_eq!(v.as_boolean(), false);
591
592        let v = XPathValue::Number(f64::NAN);
593        assert_eq!(v.as_boolean(), false);
594
595        let v = XPathValue::String("hello".into());
596        assert_eq!(v.as_string(), "hello");
597        assert_eq!(v.as_boolean(), true);
598
599        let v = XPathValue::String("".into());
600        assert_eq!(v.as_boolean(), false);
601
602        let v = XPathValue::Boolean(true);
603        assert_eq!(v.as_number(), 1.0);
604        assert_eq!(v.as_string(), "true");
605
606        let v = XPathValue::Boolean(false);
607        assert_eq!(v.as_number(), 0.0);
608        assert_eq!(v.as_string(), "false");
609    }
610
611    #[test]
612    fn test_node_set() {
613        let mut ns = NodeSet::new();
614        assert!(ns.is_empty());
615        assert_eq!(ns.len(), 0);
616    }
617}