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