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