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 {
328        if unsafe { element_has_id(node, id) } {
329            return Some(node);
330        }
331    }
332
333    // Recurse into children
334    let mut child = unsafe { (*node).children };
335    while !child.is_null() {
336        if let Some(found) = unsafe { walk_for_id(child, id) } {
337            return Some(found);
338        }
339        child = unsafe { (*child).next };
340    }
341
342    None
343}
344
345/// Check if an element node has an attribute whose ID value matches.
346///
347/// Checks:
348/// 1. If the attribute's `atype` is `XML_ATTRIBUTE_ID`, compare its value.
349/// 2. If the attribute's name is "id" (case-insensitive), compare its value.
350unsafe fn element_has_id(node: *mut _xmlNode, id: &str) -> bool {
351    if node.is_null() {
352        return false;
353    }
354
355    let mut prop = unsafe { (*node).properties };
356    while !prop.is_null() {
357        let attr = unsafe { &*prop };
358
359        // Check 1: DTD-declared ID type
360        if attr.atype == XML_ATTRIBUTE_ID as std::os::raw::c_int {
361            if let Some(val) = unsafe { get_attr_value(prop) } {
362                if val == id {
363                    return true;
364                }
365            }
366        }
367
368        // Check 2: attribute named "id" (case-insensitive)
369        if !attr.name.is_null() {
370            let name_str = unsafe { c_xmlchar_to_str(attr.name) };
371            if name_str.as_deref() == Some("id") || name_str.as_deref() == Some("ID") {
372                if let Some(val) = unsafe { get_attr_value(prop) } {
373                    if val == id {
374                        return true;
375                    }
376                }
377            }
378        }
379
380        prop = unsafe { (*prop).next };
381    }
382
383    false
384}
385
386/// Extract the string value of an attribute.
387unsafe fn get_attr_value(attr: *mut _xmlAttr) -> Option<String> {
388    if attr.is_null() {
389        return None;
390    }
391
392    let children = unsafe { (*attr).children };
393    if children.is_null() {
394        return None;
395    }
396
397    let text = unsafe { &*children };
398    if text.type_ == XML_TEXT_NODE as std::os::raw::c_int && !text.content.is_null() {
399        let val = unsafe { c_xmlchar_to_str(text.content) };
400        return val;
401    }
402
403    None
404}
405
406/// Convert a `*const xmlChar` (C string) to a Rust `String`.
407///
408/// SAFETY: `ptr` must point to a null-terminated sequence of bytes.
409unsafe fn c_xmlchar_to_str(ptr: *const crate::abi::types::xmlChar) -> Option<String> {
410    if ptr.is_null() {
411        return None;
412    }
413
414    // xmlChar is `c_uchar`; we reinterpret as `*const c_char` for CStr.
415    let c_str = unsafe { CStr::from_ptr(ptr as *const c_char) };
416    match c_str.to_str() {
417        Ok(s) => Some(s.to_string()),
418        Err(_) => None,
419    }
420}
421
422// ═══════════════════════════════════════════════════════════════════════════════
423// Tests
424// ═══════════════════════════════════════════════════════════════════════════════
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::abi::allocator::xmlMallocZero;
430    use crate::abi::types::xmlElementType::*;
431    use std::mem;
432    use std::os::raw::c_int;
433    use std::ptr;
434
435    // ── Helper: create a minimal document tree for testing ────────────────
436
437    /// Create a minimal document with one element: `<root id="main">`.
438    unsafe fn create_simple_doc() -> *mut _xmlDoc {
439        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
440        assert!(!doc.is_null());
441
442        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
443        assert!(!root.is_null());
444
445        unsafe {
446            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
447            (*doc).doc = doc;
448            (*doc).children = root;
449
450            (*root).type_ = XML_ELEMENT_NODE as c_int;
451            (*root).name = string_to_xmlchar("root");
452            (*root).parent = doc as *mut _xmlNode;
453            (*root).doc = doc;
454            (*root).properties = ptr::null_mut();
455        }
456
457        // Add id="main" attribute
458        let attr = unsafe { add_id_attr(root, "id", "main") };
459        unsafe {
460            (*root).properties = attr;
461        }
462
463        doc
464    }
465
466    /// Create a more complex document tree:
467    /// ```
468    /// <root id="main">
469    ///   <child1 id="a"/>
470    ///   <child2 id="b">
471    ///     <grandchild id="c"/>
472    ///   </child2>
473    ///   <child3/>
474    /// </root>
475    /// ```
476    unsafe fn create_complex_doc() -> *mut _xmlDoc {
477        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
478        assert!(!doc.is_null());
479
480        // root element
481        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
482        assert!(!root.is_null());
483
484        unsafe {
485            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
486            (*doc).doc = doc;
487            (*doc).children = root;
488
489            (*root).type_ = XML_ELEMENT_NODE as c_int;
490            (*root).name = string_to_xmlchar("root");
491            (*root).parent = doc as *mut _xmlNode;
492            (*root).doc = doc;
493        }
494
495        let attr_root = unsafe { add_id_attr(root, "id", "main") };
496        unsafe { (*root).properties = attr_root };
497
498        // child1
499        let child1 = unsafe { append_child_element(root, "child1") };
500        let attr_c1 = unsafe { add_id_attr(child1, "id", "a") };
501        unsafe { (*child1).properties = attr_c1 };
502
503        // child2
504        let child2 = unsafe { append_child_element(root, "child2") };
505        let attr_c2 = unsafe { add_id_attr(child2, "id", "b") };
506        unsafe { (*child2).properties = attr_c2 };
507
508        // grandchild (child of child2)
509        let grandchild = unsafe { append_child_element(child2, "grandchild") };
510        let attr_gc = unsafe { add_id_attr(grandchild, "id", "c") };
511        unsafe { (*grandchild).properties = attr_gc };
512
513        // child3 (no ID)
514        let _child3 = unsafe { append_child_element(root, "child3") };
515
516        doc
517    }
518
519    unsafe fn string_to_xmlchar(s: &str) -> *const crate::abi::types::xmlChar {
520        let c_str = CString::new(s).unwrap();
521        c_str.into_raw() as *const crate::abi::types::xmlChar
522    }
523
524    unsafe fn append_child_element(parent: *mut _xmlNode, name: &str) -> *mut _xmlNode {
525        let node = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
526        assert!(!node.is_null());
527
528        unsafe {
529            (*node).type_ = XML_ELEMENT_NODE as c_int;
530            (*node).name = string_to_xmlchar(name);
531            (*node).parent = parent;
532            (*node).doc = (*parent).doc;
533            (*node).next = ptr::null_mut();
534            (*node).prev = (*parent).last;
535            (*node).properties = ptr::null_mut();
536
537            // Link into parent's child list
538            if (*parent).children.is_null() {
539                (*parent).children = node;
540                (*parent).last = node;
541            } else {
542                let last = (*parent).last;
543                if !last.is_null() {
544                    (*last).next = node;
545                }
546                (*parent).last = node;
547            }
548        }
549
550        node
551    }
552
553    unsafe fn add_id_attr(node: *mut _xmlNode, name: &str, value: &str) -> *mut _xmlAttr {
554        let attr = xmlMallocZero(mem::size_of::<_xmlAttr>()) as *mut _xmlAttr;
555        assert!(!attr.is_null());
556
557        // Create text child for the attribute value
558        let text = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
559        assert!(!text.is_null());
560
561        unsafe {
562            (*attr).type_ = 2; // XML_ATTRIBUTE_NODE
563            (*attr).name = string_to_xmlchar(name);
564            (*attr).parent = node;
565            (*attr).doc = (*node).doc;
566            (*attr).children = text;
567            (*attr).last = text;
568            (*attr).atype = crate::abi::types::xmlAttributeType::XML_ATTRIBUTE_CDATA as c_int;
569            (*attr).next = ptr::null_mut();
570            (*attr).prev = ptr::null_mut();
571
572            (*text).type_ = XML_TEXT_NODE as c_int;
573            (*text).name = string_to_xmlchar("text");
574            (*text).content = string_to_xmlchar(value) as *mut crate::abi::types::xmlChar;
575            (*text).parent = attr as *mut _xmlNode;
576            (*text).doc = (*node).doc;
577            (*text).next = ptr::null_mut();
578            (*text).prev = ptr::null_mut();
579        }
580
581        attr
582    }
583
584    // ── Tests ────────────────────────────────────────────────────────────
585
586    macro_rules! c_name_eq {
587        ($node:expr, $expected:expr) => {
588            assert_eq!(
589                CStr::from_ptr((*$node).name as *const c_char)
590                    .to_str()
591                    .unwrap(),
592                $expected
593            );
594        };
595    }
596
597    #[test]
598    fn test_shorthand_pointer() {
599        unsafe {
600            let doc = create_simple_doc();
601            let result = xptr_eval("main", doc);
602            assert!(result.is_some());
603            c_name_eq!(result.unwrap(), "root");
604        }
605    }
606
607    #[test]
608    fn test_shorthand_pointer_not_found() {
609        unsafe {
610            let doc = create_simple_doc();
611            let result = xptr_eval("nonexistent", doc);
612            assert!(result.is_none());
613        }
614    }
615
616    #[test]
617    fn test_element_scheme_basic() {
618        unsafe {
619            let doc = create_complex_doc();
620
621            let result = xptr_eval("element(main)", doc);
622            assert!(result.is_some());
623            c_name_eq!(result.unwrap(), "root");
624
625            let result = xptr_eval("element(a)", doc);
626            assert!(result.is_some());
627            c_name_eq!(result.unwrap(), "child1");
628
629            let result = xptr_eval("element(c)", doc);
630            assert!(result.is_some());
631            c_name_eq!(result.unwrap(), "grandchild");
632        }
633    }
634
635    #[test]
636    fn test_element_scheme_with_child_sequence() {
637        unsafe {
638            let doc = create_complex_doc();
639
640            let result = xptr_eval("element(main/1)", doc);
641            assert!(result.is_some());
642            c_name_eq!(result.unwrap(), "child1");
643
644            let result = xptr_eval("element(main/2)", doc);
645            assert!(result.is_some());
646            c_name_eq!(result.unwrap(), "child2");
647
648            let result = xptr_eval("element(main/2/1)", doc);
649            assert!(result.is_some());
650            c_name_eq!(result.unwrap(), "grandchild");
651        }
652    }
653
654    #[test]
655    fn test_element_scheme_child_out_of_range() {
656        unsafe {
657            let doc = create_complex_doc();
658            let result = xptr_eval("element(main/99)", doc);
659            assert!(result.is_none());
660        }
661    }
662
663    #[test]
664    fn test_element_scheme_zero_index() {
665        unsafe {
666            let doc = create_complex_doc();
667            let result = xptr_eval("element(main/0)", doc);
668            assert!(result.is_none());
669        }
670    }
671
672    #[test]
673    fn test_empty_expr() {
674        unsafe {
675            let doc = create_simple_doc();
676            let result = xptr_eval("", doc);
677            assert!(result.is_none());
678        }
679    }
680
681    #[test]
682    fn test_null_doc() {
683        unsafe {
684            let result = xptr_eval("main", ptr::null_mut());
685            assert!(result.is_none());
686        }
687    }
688
689    #[test]
690    fn test_xml_xptr_eval_c_abi() {
691        unsafe {
692            let doc = create_simple_doc();
693            let c_expr = CString::new("main").unwrap();
694            let node = xmlXPtrEval(c_expr.as_ptr(), doc);
695            assert!(!node.is_null());
696            c_name_eq!(node, "root");
697        }
698    }
699
700    #[test]
701    fn test_xml_xptr_eval_null_expr() {
702        unsafe {
703            let doc = create_simple_doc();
704            let node = xmlXPtrEval(ptr::null(), doc);
705            assert!(node.is_null());
706        }
707    }
708
709    #[test]
710    fn test_xml_xptr_eval_null_doc() {
711        unsafe {
712            let c_expr = CString::new("main").unwrap();
713            let node = xmlXPtrEval(c_expr.as_ptr(), ptr::null_mut());
714            assert!(node.is_null());
715        }
716    }
717
718    #[test]
719    fn test_xml_xptr_eval_node_set() {
720        unsafe {
721            let doc = create_simple_doc();
722            let c_expr = CString::new("main").unwrap();
723            let ns = xmlXPtrEvalNodeSet(c_expr.as_ptr(), doc);
724            assert!(!ns.is_null());
725            assert_eq!((*ns).nodeNr, 1);
726            assert!(!(*ns).nodeTab.is_null());
727            let node = *(*ns).nodeTab;
728            c_name_eq!(node, "root");
729        }
730    }
731
732    #[test]
733    fn test_element_scheme_not_found() {
734        unsafe {
735            let doc = create_complex_doc();
736            let result = xptr_eval("element(nonexistent)", doc);
737            assert!(result.is_none());
738        }
739    }
740
741    #[test]
742    fn test_element_scheme_extra_spaces() {
743        unsafe {
744            let doc = create_complex_doc();
745            let result = xptr_eval("element( main )", doc);
746            assert!(result.is_some());
747            c_name_eq!(result.unwrap(), "root");
748        }
749    }
750
751    #[test]
752    fn test_child3_no_id() {
753        unsafe {
754            let doc = create_complex_doc();
755            let result = xptr_eval("child3", doc);
756            assert!(result.is_none());
757        }
758    }
759}