Skip to main content

libxml_rs/xml/xpointer/
mod.rs

1//! XPointer implementation (§26, §85 Phase 5).
2//!
3//! XML Pointer Language (XPointer) v1.0 support based on the
4//! [XPointer Framework](https://www.w3.org/TR/xptr-framework/) and
5//! [element() Scheme](https://www.w3.org/TR/xptr-element/) W3C Recommendations.
6//!
7//! This module provides:
8//!
9//! - **Shorthand pointers** — bare names treated as element IDs
10//! - **`element()` scheme** — `element(id)` or `element(id/N/M/…)` for
11//!   child-axis traversal
12//! - **`xmlXPtrEval` C ABI** — for interop with libxml2 consumers
13//!
14//! The caller is responsible for stripping the `#` from the URI fragment;
15//! this module receives only the fragment content.
16
17use crate::abi::structs::{_xmlAttr, _xmlDoc, _xmlNode};
18use crate::abi::types::xmlAttributeType::XML_ATTRIBUTE_ID;
19use crate::abi::types::xmlElementType::{XML_ELEMENT_NODE, XML_TEXT_NODE};
20use crate::xml::xpath::context::XPathContext;
21use crate::xml::xpath::types::NodeSet;
22use std::ffi::CStr;
23
24#[cfg(test)]
25use std::ffi::CString;
26use std::os::raw::c_char;
27use std::ptr;
28
29// ═══════════════════════════════════════════════════════════════════════════════
30// Public API
31// ═══════════════════════════════════════════════════════════════════════════════
32
33/// Evaluate an XPointer expression and return the pointed-to node.
34///
35/// Supports:
36/// - **Shorthand pointers** — bare name treated as an element ID.
37/// - **`element()` scheme** — `element(id)` selects the element with that ID;
38///   `element(id/N)` selects the N-th child (1-indexed) of that element, etc.
39///
40/// Returns `None` if the pointer does not resolve to a node.
41///
42/// # Parameters
43///
44/// * `expr` — the XPointer expression (without the leading `#`).
45/// * `doc` — pointer to the XML document to search in.
46///
47/// # Safety
48///
49/// `doc` must be a valid, non-null pointer to a fully parsed `_xmlDoc`.
50pub unsafe fn xptr_eval(expr: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
51    if doc.is_null() {
52        return None;
53    }
54
55    let expr = expr.trim();
56
57    if expr.is_empty() {
58        return None;
59    }
60
61    // Try to parse as a scheme-based pointer: scheme(data)
62    if let Some(result) = try_eval_scheme(expr, doc) {
63        return result;
64    }
65
66    // Fall back to shorthand pointer (bare name as ID).
67    shorthand_lookup(expr, doc)
68}
69
70/// Evaluate an XPointer using the full XPath/XPointer context.
71///
72/// This is a convenience wrapper that creates a temporary XPath context
73/// and delegates to [`xptr_eval`].
74///
75/// # Safety
76///
77/// `doc` must be a valid, non-null pointer to a fully parsed `_xmlDoc`.
78pub unsafe fn xptr_eval_with_context(
79    expr: &str,
80    doc: *mut _xmlDoc,
81    _context: Option<&mut XPathContext>,
82) -> Option<*mut _xmlNode> {
83    xptr_eval(expr, doc)
84}
85
86// ═══════════════════════════════════════════════════════════════════════════════
87// C ABI
88// ═══════════════════════════════════════════════════════════════════════════════
89
90/// C ABI entry point for XPointer evaluation.
91///
92/// Corresponds to `xmlXPtrEval` in libxml2.
93///
94/// # Safety
95///
96/// * `expr` must be a valid null-terminated C string.
97/// * `doc` must be a valid pointer to `_xmlDoc` or NULL.
98///
99/// Returns a pointer to the selected `_xmlNode`, or NULL if the pointer
100/// does not resolve.
101pub unsafe extern "C" fn xmlXPtrEval(expr: *const c_char, doc: *mut _xmlDoc) -> *mut _xmlNode {
102    if expr.is_null() || doc.is_null() {
103        return ptr::null_mut();
104    }
105
106    let expr_str = match unsafe { CStr::from_ptr(expr) }.to_str() {
107        Ok(s) => s,
108        Err(_) => return ptr::null_mut(),
109    };
110
111    match unsafe { xptr_eval(expr_str, doc) } {
112        Some(node) => node,
113        None => ptr::null_mut(),
114    }
115}
116
117/// Evaluate an XPointer expression and return a node-set.
118///
119/// Corresponds to `xmlXPtrEval` returning a node-set in some libxml2 APIs.
120///
121/// # Safety
122///
123/// * `expr` must be a valid null-terminated C string.
124/// * `doc` must be a valid pointer to `_xmlDoc` or NULL.
125#[no_mangle]
126pub unsafe extern "C" fn xmlXPtrEvalNodeSet(
127    expr: *const c_char,
128    doc: *mut _xmlDoc,
129) -> *mut crate::abi::structs::_xmlNodeSet {
130    if expr.is_null() || doc.is_null() {
131        return ptr::null_mut();
132    }
133
134    let expr_str = match unsafe { CStr::from_ptr(expr) }.to_str() {
135        Ok(s) => s,
136        Err(_) => return ptr::null_mut(),
137    };
138
139    let node = unsafe { xptr_eval(expr_str, doc) };
140
141    let mut ns = NodeSet::new();
142    if let Some(n) = node {
143        ns.push(n);
144    }
145
146    unsafe { ns.to_raw() }
147}
148
149// ═══════════════════════════════════════════════════════════════════════════════
150// Scheme-based pointer evaluation
151// ═══════════════════════════════════════════════════════════════════════════════
152
153/// Try to evaluate `expr` as a scheme-based pointer (`scheme(data)`).
154///
155/// Returns `None` if the expression does not match a known scheme pattern.
156unsafe fn try_eval_scheme(expr: &str, doc: *mut _xmlDoc) -> Option<Option<*mut _xmlNode>> {
157    let expr = expr.trim();
158
159    // Try to match `element(...)` scheme
160    if let Some(inner) = strip_scheme(expr, "element") {
161        return Some(unsafe { eval_element_scheme(inner, doc) });
162    }
163
164    // No known scheme matched; return None to let the caller fall back to
165    // shorthand pointer.
166    None
167}
168
169/// Strip a scheme name and parentheses from the front of `expr`.
170///
171/// If `expr` starts with `scheme(` and ends with `)`, returns the inner
172/// content. Otherwise returns `None`.
173fn strip_scheme<'a>(expr: &'a str, scheme: &str) -> Option<&'a str> {
174    let expr = expr.trim();
175
176    let expected_prefix = format!("{}(", scheme);
177    if !expr.starts_with(&expected_prefix) {
178        return None;
179    }
180
181    let inner_start = expected_prefix.len();
182    if !expr.ends_with(')') {
183        return None;
184    }
185
186    let inner_end = expr.len() - 1;
187    if inner_end <= inner_start {
188        return Some("");
189    }
190
191    Some(&expr[inner_start..inner_end])
192}
193
194// ═══════════════════════════════════════════════════════════════════════════════
195// element() scheme
196// ═══════════════════════════════════════════════════════════════════════════════
197
198/// Evaluate an `element()` scheme pointer.
199///
200/// Syntax: `element(id)` or `element(id/N1/N2/...)`
201///
202/// * `element(id)` — select the element with the given ID.
203/// * `element(id/N)` — select the N-th child (1-indexed) of the element
204///   with the given ID.
205/// * `element(id/N1/N2/...)` — traverse deeper child levels.
206unsafe fn eval_element_scheme(inner: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
207    let inner = inner.trim();
208    if inner.is_empty() {
209        return None;
210    }
211
212    // Split on '/'
213    let parts: Vec<&str> = inner.split('/').collect();
214    if parts.is_empty() {
215        return None;
216    }
217
218    let id = parts[0].trim();
219    if id.is_empty() {
220        return None;
221    }
222
223    // Find the element with this ID
224    let base = unsafe { find_element_by_id(id, doc) }?;
225
226    // If only ID was given, return the element directly
227    if parts.len() == 1 {
228        return Some(base);
229    }
230
231    // Otherwise traverse child indices: element(id/N1/N2/...)
232    let mut current = base;
233    for &part in &parts[1..] {
234        let index_str = part.trim();
235        let index: usize = match index_str.parse() {
236            Ok(n) if n >= 1 => n,
237            _ => return None,
238        };
239
240        // Get the N-th child element (1-indexed)
241        current = unsafe { nth_child_element(current, index) }?;
242    }
243
244    Some(current)
245}
246
247/// Get the N-th child element node (1-indexed) of `node`.
248///
249/// Only counts element nodes (XML_ELEMENT_NODE).
250unsafe fn nth_child_element(node: *mut _xmlNode, n: usize) -> Option<*mut _xmlNode> {
251    if node.is_null() {
252        return None;
253    }
254
255    let mut count = 0usize;
256    let mut child = unsafe { (*node).children };
257
258    while !child.is_null() {
259        let ty = unsafe { (*child).type_ };
260        if ty == XML_ELEMENT_NODE as std::os::raw::c_int {
261            count += 1;
262            if count == n {
263                return Some(child);
264            }
265        }
266        child = unsafe { (*child).next };
267    }
268
269    None
270}
271
272// ═══════════════════════════════════════════════════════════════════════════════
273// Shorthand pointer (bare name as ID)
274// ═══════════════════════════════════════════════════════════════════════════════
275
276/// Look up a bare name as an element ID (shorthand pointer).
277///
278/// Per the XPointer Framework, a shorthand pointer is treated as if it were
279/// `element(id)`.
280unsafe fn shorthand_lookup(name: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
281    unsafe { find_element_by_id(name, doc) }
282}
283
284// ═══════════════════════════════════════════════════════════════════════════════
285// Element-by-ID lookup
286// ═══════════════════════════════════════════════════════════════════════════════
287
288/// Find an element by its ID attribute.
289///
290/// This function searches the document tree for an element whose `id`
291/// attribute (case-insensitive name match) has the given value.
292///
293/// It also checks the DTD-declared ID type (`_xmlAttr.atype ==
294/// XML_ATTRIBUTE_ID`) as a secondary identification mechanism.
295///
296/// # Parameters
297///
298/// * `id` — the ID value to search for.
299/// * `doc` — the document to search.
300///
301/// # Returns
302///
303/// The first matching element node, or `None`.
304unsafe fn find_element_by_id(id: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
305    if doc.is_null() || id.is_empty() {
306        return None;
307    }
308
309    // Walk the document tree searching for an element with a matching ID
310    // attribute.
311    let root = unsafe { (*doc).children };
312    if root.is_null() {
313        return None;
314    }
315
316    unsafe { walk_for_id(root, id) }
317}
318
319/// Recursively walk the tree looking for an element with the given ID.
320unsafe fn walk_for_id(node: *mut _xmlNode, id: &str) -> Option<*mut _xmlNode> {
321    if node.is_null() {
322        return None;
323    }
324
325    // Check if this node is an element with a matching ID attribute
326    let ty = unsafe { (*node).type_ };
327    if ty == XML_ELEMENT_NODE as std::os::raw::c_int && unsafe { element_has_id(node, id) } {
328        return Some(node);
329    }
330
331    // Recurse into children
332    let mut child = unsafe { (*node).children };
333    while !child.is_null() {
334        if let Some(found) = unsafe { walk_for_id(child, id) } {
335            return Some(found);
336        }
337        child = unsafe { (*child).next };
338    }
339
340    None
341}
342
343/// Check if an element node has an attribute whose ID value matches.
344///
345/// Checks:
346/// 1. If the attribute's `atype` is `XML_ATTRIBUTE_ID`, compare its value.
347/// 2. If the attribute's name is "id" (case-insensitive), compare its value.
348unsafe fn element_has_id(node: *mut _xmlNode, id: &str) -> bool {
349    if node.is_null() {
350        return false;
351    }
352
353    let mut prop = unsafe { (*node).properties };
354    while !prop.is_null() {
355        let attr = unsafe { &*prop };
356
357        // Check 1: DTD-declared ID type
358        if attr.atype == XML_ATTRIBUTE_ID as std::os::raw::c_int {
359            if let Some(val) = unsafe { get_attr_value(prop) } {
360                if val == id {
361                    return true;
362                }
363            }
364        }
365
366        // Check 2: attribute named "id" (case-insensitive)
367        if !attr.name.is_null() {
368            let name_str = unsafe { c_xmlchar_to_str(attr.name) };
369            if name_str.as_deref() == Some("id") || name_str.as_deref() == Some("ID") {
370                if let Some(val) = unsafe { get_attr_value(prop) } {
371                    if val == id {
372                        return true;
373                    }
374                }
375            }
376        }
377
378        prop = unsafe { (*prop).next };
379    }
380
381    false
382}
383
384/// Extract the string value of an attribute.
385unsafe fn get_attr_value(attr: *mut _xmlAttr) -> Option<String> {
386    if attr.is_null() {
387        return None;
388    }
389
390    let children = unsafe { (*attr).children };
391    if children.is_null() {
392        return None;
393    }
394
395    let text = unsafe { &*children };
396    if text.type_ == XML_TEXT_NODE as std::os::raw::c_int && !text.content.is_null() {
397        let val = unsafe { c_xmlchar_to_str(text.content) };
398        return val;
399    }
400
401    None
402}
403
404/// Convert a `*const xmlChar` (C string) to a Rust `String`.
405///
406/// SAFETY: `ptr` must point to a null-terminated sequence of bytes.
407unsafe fn c_xmlchar_to_str(ptr: *const crate::abi::types::xmlChar) -> Option<String> {
408    if ptr.is_null() {
409        return None;
410    }
411
412    // xmlChar is `c_uchar`; we reinterpret as `*const c_char` for CStr.
413    let c_str = unsafe { CStr::from_ptr(ptr as *const c_char) };
414    match c_str.to_str() {
415        Ok(s) => Some(s.to_string()),
416        Err(_) => None,
417    }
418}
419
420// ═══════════════════════════════════════════════════════════════════════════════
421// Tests
422// ═══════════════════════════════════════════════════════════════════════════════
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use crate::abi::allocator::xmlMallocZero;
428    use crate::abi::types::xmlElementType::*;
429    use std::mem;
430    use std::os::raw::c_int;
431    use std::ptr;
432
433    // ── Helper: create a minimal document tree for testing ────────────────
434
435    /// Create a minimal document with one element: `<root id="main">`.
436    unsafe fn create_simple_doc() -> *mut _xmlDoc {
437        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
438        assert!(!doc.is_null());
439
440        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
441        assert!(!root.is_null());
442
443        unsafe {
444            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
445            (*doc).doc = doc;
446            (*doc).children = root;
447
448            (*root).type_ = XML_ELEMENT_NODE as c_int;
449            (*root).name = string_to_xmlchar("root");
450            (*root).parent = doc as *mut _xmlNode;
451            (*root).doc = doc;
452            (*root).properties = ptr::null_mut();
453        }
454
455        // Add id="main" attribute
456        let attr = unsafe { add_id_attr(root, "id", "main") };
457        unsafe {
458            (*root).properties = attr;
459        }
460
461        doc
462    }
463
464    /// Create a more complex document tree:
465    /// ```
466    /// <root id="main">
467    ///   <child1 id="a"/>
468    ///   <child2 id="b">
469    ///     <grandchild id="c"/>
470    ///   </child2>
471    ///   <child3/>
472    /// </root>
473    /// ```
474    unsafe fn create_complex_doc() -> *mut _xmlDoc {
475        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
476        assert!(!doc.is_null());
477
478        // root element
479        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
480        assert!(!root.is_null());
481
482        unsafe {
483            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
484            (*doc).doc = doc;
485            (*doc).children = root;
486
487            (*root).type_ = XML_ELEMENT_NODE as c_int;
488            (*root).name = string_to_xmlchar("root");
489            (*root).parent = doc as *mut _xmlNode;
490            (*root).doc = doc;
491        }
492
493        let attr_root = unsafe { add_id_attr(root, "id", "main") };
494        unsafe { (*root).properties = attr_root };
495
496        // child1
497        let child1 = unsafe { append_child_element(root, "child1") };
498        let attr_c1 = unsafe { add_id_attr(child1, "id", "a") };
499        unsafe { (*child1).properties = attr_c1 };
500
501        // child2
502        let child2 = unsafe { append_child_element(root, "child2") };
503        let attr_c2 = unsafe { add_id_attr(child2, "id", "b") };
504        unsafe { (*child2).properties = attr_c2 };
505
506        // grandchild (child of child2)
507        let grandchild = unsafe { append_child_element(child2, "grandchild") };
508        let attr_gc = unsafe { add_id_attr(grandchild, "id", "c") };
509        unsafe { (*grandchild).properties = attr_gc };
510
511        // child3 (no ID)
512        let _child3 = unsafe { append_child_element(root, "child3") };
513
514        doc
515    }
516
517    unsafe fn string_to_xmlchar(s: &str) -> *const crate::abi::types::xmlChar {
518        let c_str = CString::new(s).unwrap();
519        c_str.into_raw() as *const crate::abi::types::xmlChar
520    }
521
522    unsafe fn append_child_element(parent: *mut _xmlNode, name: &str) -> *mut _xmlNode {
523        let node = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
524        assert!(!node.is_null());
525
526        unsafe {
527            (*node).type_ = XML_ELEMENT_NODE as c_int;
528            (*node).name = string_to_xmlchar(name);
529            (*node).parent = parent;
530            (*node).doc = (*parent).doc;
531            (*node).next = ptr::null_mut();
532            (*node).prev = (*parent).last;
533            (*node).properties = ptr::null_mut();
534
535            // Link into parent's child list
536            if (*parent).children.is_null() {
537                (*parent).children = node;
538                (*parent).last = node;
539            } else {
540                let last = (*parent).last;
541                if !last.is_null() {
542                    (*last).next = node;
543                }
544                (*parent).last = node;
545            }
546        }
547
548        node
549    }
550
551    unsafe fn add_id_attr(node: *mut _xmlNode, name: &str, value: &str) -> *mut _xmlAttr {
552        let attr = xmlMallocZero(mem::size_of::<_xmlAttr>()) as *mut _xmlAttr;
553        assert!(!attr.is_null());
554
555        // Create text child for the attribute value
556        let text = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
557        assert!(!text.is_null());
558
559        unsafe {
560            (*attr).type_ = 2; // XML_ATTRIBUTE_NODE
561            (*attr).name = string_to_xmlchar(name);
562            (*attr).parent = node;
563            (*attr).doc = (*node).doc;
564            (*attr).children = text;
565            (*attr).last = text;
566            (*attr).atype = crate::abi::types::xmlAttributeType::XML_ATTRIBUTE_CDATA as c_int;
567            (*attr).next = ptr::null_mut();
568            (*attr).prev = ptr::null_mut();
569
570            (*text).type_ = XML_TEXT_NODE as c_int;
571            (*text).name = string_to_xmlchar("text");
572            (*text).content = string_to_xmlchar(value) as *mut crate::abi::types::xmlChar;
573            (*text).parent = attr as *mut _xmlNode;
574            (*text).doc = (*node).doc;
575            (*text).next = ptr::null_mut();
576            (*text).prev = ptr::null_mut();
577        }
578
579        attr
580    }
581
582    // ── Tests ────────────────────────────────────────────────────────────
583
584    macro_rules! c_name_eq {
585        ($node:expr, $expected:expr) => {
586            assert_eq!(
587                CStr::from_ptr((*$node).name as *const c_char)
588                    .to_str()
589                    .unwrap(),
590                $expected
591            );
592        };
593    }
594
595    #[test]
596    fn test_shorthand_pointer() {
597        unsafe {
598            let doc = create_simple_doc();
599            let result = xptr_eval("main", doc);
600            assert!(result.is_some());
601            c_name_eq!(result.unwrap(), "root");
602        }
603    }
604
605    #[test]
606    fn test_shorthand_pointer_not_found() {
607        unsafe {
608            let doc = create_simple_doc();
609            let result = xptr_eval("nonexistent", doc);
610            assert!(result.is_none());
611        }
612    }
613
614    #[test]
615    fn test_element_scheme_basic() {
616        unsafe {
617            let doc = create_complex_doc();
618
619            let result = xptr_eval("element(main)", doc);
620            assert!(result.is_some());
621            c_name_eq!(result.unwrap(), "root");
622
623            let result = xptr_eval("element(a)", doc);
624            assert!(result.is_some());
625            c_name_eq!(result.unwrap(), "child1");
626
627            let result = xptr_eval("element(c)", doc);
628            assert!(result.is_some());
629            c_name_eq!(result.unwrap(), "grandchild");
630        }
631    }
632
633    #[test]
634    fn test_element_scheme_with_child_sequence() {
635        unsafe {
636            let doc = create_complex_doc();
637
638            let result = xptr_eval("element(main/1)", doc);
639            assert!(result.is_some());
640            c_name_eq!(result.unwrap(), "child1");
641
642            let result = xptr_eval("element(main/2)", doc);
643            assert!(result.is_some());
644            c_name_eq!(result.unwrap(), "child2");
645
646            let result = xptr_eval("element(main/2/1)", doc);
647            assert!(result.is_some());
648            c_name_eq!(result.unwrap(), "grandchild");
649        }
650    }
651
652    #[test]
653    fn test_element_scheme_child_out_of_range() {
654        unsafe {
655            let doc = create_complex_doc();
656            let result = xptr_eval("element(main/99)", doc);
657            assert!(result.is_none());
658        }
659    }
660
661    #[test]
662    fn test_element_scheme_zero_index() {
663        unsafe {
664            let doc = create_complex_doc();
665            let result = xptr_eval("element(main/0)", doc);
666            assert!(result.is_none());
667        }
668    }
669
670    #[test]
671    fn test_empty_expr() {
672        unsafe {
673            let doc = create_simple_doc();
674            let result = xptr_eval("", doc);
675            assert!(result.is_none());
676        }
677    }
678
679    #[test]
680    fn test_null_doc() {
681        unsafe {
682            let result = xptr_eval("main", ptr::null_mut());
683            assert!(result.is_none());
684        }
685    }
686
687    #[test]
688    fn test_xml_xptr_eval_c_abi() {
689        unsafe {
690            let doc = create_simple_doc();
691            let c_expr = CString::new("main").unwrap();
692            let node = xmlXPtrEval(c_expr.as_ptr(), doc);
693            assert!(!node.is_null());
694            c_name_eq!(node, "root");
695        }
696    }
697
698    #[test]
699    fn test_xml_xptr_eval_null_expr() {
700        unsafe {
701            let doc = create_simple_doc();
702            let node = xmlXPtrEval(ptr::null(), doc);
703            assert!(node.is_null());
704        }
705    }
706
707    #[test]
708    fn test_xml_xptr_eval_null_doc() {
709        unsafe {
710            let c_expr = CString::new("main").unwrap();
711            let node = xmlXPtrEval(c_expr.as_ptr(), ptr::null_mut());
712            assert!(node.is_null());
713        }
714    }
715
716    #[test]
717    fn test_xml_xptr_eval_node_set() {
718        unsafe {
719            let doc = create_simple_doc();
720            let c_expr = CString::new("main").unwrap();
721            let ns = xmlXPtrEvalNodeSet(c_expr.as_ptr(), doc);
722            assert!(!ns.is_null());
723            assert_eq!((*ns).nodeNr, 1);
724            assert!(!(*ns).nodeTab.is_null());
725            let node = *(*ns).nodeTab;
726            c_name_eq!(node, "root");
727        }
728    }
729
730    #[test]
731    fn test_element_scheme_not_found() {
732        unsafe {
733            let doc = create_complex_doc();
734            let result = xptr_eval("element(nonexistent)", doc);
735            assert!(result.is_none());
736        }
737    }
738
739    #[test]
740    fn test_element_scheme_extra_spaces() {
741        unsafe {
742            let doc = create_complex_doc();
743            let result = xptr_eval("element( main )", doc);
744            assert!(result.is_some());
745            c_name_eq!(result.unwrap(), "root");
746        }
747    }
748
749    #[test]
750    fn test_child3_no_id() {
751        unsafe {
752            let doc = create_complex_doc();
753            let result = xptr_eval("child3", doc);
754            assert!(result.is_none());
755        }
756    }
757}