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.
207///
208/// # Safety
209///
210/// - `doc` must be NULL or a valid pointer to a live `_xmlDoc` whose node tree
211///   stays alive for the duration of the call; it is forwarded to
212///   `eval_element_scheme`, which walks the tree through raw node pointers.
213unsafe fn try_eval_scheme(expr: &str, doc: *mut _xmlDoc) -> Option<Option<*mut _xmlNode>> {
214    let expr = expr.trim();
215
216    // Try to match `element(...)` scheme
217    if let Some(inner) = strip_scheme(expr, "element") {
218        return Some(unsafe { eval_element_scheme(inner, doc) });
219    }
220
221    // `xpointer(...)` scheme: the inner text is an XPath expression
222    // evaluated against the document (bug43364 uses
223    // `xpointer="xpointer(/root/a)"` includes).
224    if let Some(inner) = strip_scheme(expr, "xpointer") {
225        return Some(unsafe { eval_xpointer_scheme(inner, doc) });
226    }
227
228    // No known scheme matched; return None to let the caller fall back to
229    // shorthand pointer.
230    None
231}
232
233/// Evaluate an `xpointer(...)` scheme: run the inner XPath expression against
234/// the document and return the first selected node.
235///
236/// # Safety
237///
238/// - `doc` must be NULL or a valid `_xmlDoc`; the returned node is owned by
239///   the document (borrowed).
240unsafe fn eval_xpointer_scheme(inner: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
241    let mut ctx = crate::xml::xpath::context::XPathContext::new(doc);
242    ctx.set_context_node(doc as *mut crate::abi::structs::_xmlNode);
243    match crate::xml::xpath::evaluate_str(inner, &mut ctx) {
244        Some(crate::xml::xpath::types::XPathValue::NodeSet(ns)) => ns.first(),
245        _ => None,
246    }
247}
248
249/// Strip a scheme name and parentheses from the front of `expr`.
250///
251/// If `expr` starts with `scheme(` and ends with `)`, returns the inner
252/// content. Otherwise returns `None`.
253fn strip_scheme<'a>(expr: &'a str, scheme: &str) -> Option<&'a str> {
254    let expr = expr.trim();
255
256    let expected_prefix = format!("{}(", scheme);
257    if !expr.starts_with(&expected_prefix) {
258        return None;
259    }
260
261    let inner_start = expected_prefix.len();
262    if !expr.ends_with(')') {
263        return None;
264    }
265
266    let inner_end = expr.len() - 1;
267    if inner_end <= inner_start {
268        return Some("");
269    }
270
271    Some(&expr[inner_start..inner_end])
272}
273
274// ═══════════════════════════════════════════════════════════════════════════════
275// element() scheme
276// ═══════════════════════════════════════════════════════════════════════════════
277
278/// Evaluate an `element()` scheme pointer.
279///
280/// Syntax: `element(id)` or `element(id/N1/N2/...)`
281///
282/// * `element(id)` — select the element with the given ID.
283/// * `element(id/N)` — select the N-th child (1-indexed) of the element
284///   with the given ID.
285/// * `element(id/N1/N2/...)` — traverse deeper child levels.
286///
287/// # Safety
288///
289/// - `doc` must be NULL or a valid pointer to a live `_xmlDoc`; the lookup
290///   walks `(*doc).children` and the node `children`/`next` links via
291///   `find_element_by_id` and `nth_child_element`, so every visited node must
292///   belong to the live document. The returned node pointer is borrowed from
293///   `doc` and must not outlive it.
294unsafe fn eval_element_scheme(inner: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
295    let inner = inner.trim();
296    if inner.is_empty() {
297        return None;
298    }
299
300    // Split on '/'
301    let parts: Vec<&str> = inner.split('/').collect();
302    if parts.is_empty() {
303        return None;
304    }
305
306    let id = parts[0].trim();
307    if id.is_empty() {
308        return None;
309    }
310
311    // Find the element with this ID
312    let base = unsafe { find_element_by_id(id, doc) }?;
313
314    // If only ID was given, return the element directly
315    if parts.len() == 1 {
316        return Some(base);
317    }
318
319    // Otherwise traverse child indices: element(id/N1/N2/...)
320    let mut current = base;
321    for &part in &parts[1..] {
322        let index_str = part.trim();
323        let index: usize = match index_str.parse() {
324            Ok(n) if n >= 1 => n,
325            _ => return None,
326        };
327
328        // Get the N-th child element (1-indexed)
329        current = unsafe { nth_child_element(current, index) }?;
330    }
331
332    Some(current)
333}
334
335/// Get the N-th child element node (1-indexed) of `node`.
336///
337/// Only counts element nodes (XML_ELEMENT_NODE).
338///
339/// # Safety
340///
341/// - `node` must be NULL or a pointer to a valid, live `_xmlNode`; the
342///   function follows the `children` and `next` links inside the live tree and
343///   reads each visited node's `type_` field.
344unsafe fn nth_child_element(node: *mut _xmlNode, n: usize) -> Option<*mut _xmlNode> {
345    if node.is_null() {
346        return None;
347    }
348
349    let mut count = 0usize;
350    let mut child = unsafe { (*node).children };
351
352    while !child.is_null() {
353        let ty = unsafe { (*child).type_ };
354        if ty == XML_ELEMENT_NODE as std::os::raw::c_int {
355            count += 1;
356            if count == n {
357                return Some(child);
358            }
359        }
360        child = unsafe { (*child).next };
361    }
362
363    None
364}
365
366// ═══════════════════════════════════════════════════════════════════════════════
367// Shorthand pointer (bare name as ID)
368// ═══════════════════════════════════════════════════════════════════════════════
369
370/// Look up a bare name as an element ID (shorthand pointer).
371///
372/// Per the XPointer Framework, a shorthand pointer is treated as if it were
373/// `element(id)`.
374///
375/// # Safety
376///
377/// - `doc` must be NULL or a valid pointer to a live `_xmlDoc`; the lookup
378///   delegates to `find_element_by_id`, which walks the document tree through
379///   raw node pointers, so the document must stay alive for the call.
380unsafe fn shorthand_lookup(name: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
381    unsafe { find_element_by_id(name, doc) }
382}
383
384// ═══════════════════════════════════════════════════════════════════════════════
385// Element-by-ID lookup
386// ═══════════════════════════════════════════════════════════════════════════════
387
388/// Find an element by its ID attribute.
389///
390/// This function searches the document tree for an element whose `id`
391/// attribute (case-insensitive name match) has the given value.
392///
393/// It also checks the DTD-declared ID type (`_xmlAttr.atype ==
394/// XML_ATTRIBUTE_ID`) as a secondary identification mechanism.
395///
396/// # Parameters
397///
398/// * `id` — the ID value to search for.
399/// * `doc` — the document to search.
400///
401/// # Returns
402///
403/// The first matching element node, or `None`.
404///
405/// # Safety
406///
407/// - `doc` must be NULL or a valid pointer to a live `_xmlDoc`; the search
408///   dereferences `(*doc).children` and recurses through the node tree via
409///   `walk_for_id`, so every node visited must belong to the live document.
410unsafe fn find_element_by_id(id: &str, doc: *mut _xmlDoc) -> Option<*mut _xmlNode> {
411    if doc.is_null() || id.is_empty() {
412        return None;
413    }
414
415    // Walk the document tree searching for an element with a matching ID
416    // attribute.
417    let root = unsafe { (*doc).children };
418    if root.is_null() {
419        return None;
420    }
421
422    unsafe { walk_for_id(root, id) }
423}
424
425/// Recursively walk the tree looking for an element with the given ID.
426///
427/// # Safety
428///
429/// - `node` must be NULL or a pointer to a valid, live `_xmlNode` whose
430///   `children` and `next` links form the subtree to search; `id` is a
431///   borrowed `&str` that must stay valid for the whole walk.
432unsafe fn walk_for_id(node: *mut _xmlNode, id: &str) -> Option<*mut _xmlNode> {
433    if node.is_null() {
434        return None;
435    }
436
437    // Check if this node is an element with a matching ID attribute
438    let ty = unsafe { (*node).type_ };
439    if ty == XML_ELEMENT_NODE as std::os::raw::c_int && unsafe { element_has_id(node, id) } {
440        return Some(node);
441    }
442
443    // Recurse into children
444    let mut child = unsafe { (*node).children };
445    while !child.is_null() {
446        if let Some(found) = unsafe { walk_for_id(child, id) } {
447            return Some(found);
448        }
449        child = unsafe { (*child).next };
450    }
451
452    None
453}
454
455/// Check if an element node has an attribute whose ID value matches.
456///
457/// Checks:
458/// 1. If the attribute's `atype` is `XML_ATTRIBUTE_ID`, compare its value.
459/// 2. If the attribute's name is "id" (case-insensitive), compare its value.
460///
461/// # Safety
462///
463/// - `node` must be NULL or a pointer to a valid, live `_xmlNode`; the
464///   function walks `(*node).properties` through the `next` links and reads
465///   each attribute's `atype`, `name`, and value, so every visited attribute
466///   must belong to the live node.
467unsafe fn element_has_id(node: *mut _xmlNode, id: &str) -> bool {
468    if node.is_null() {
469        return false;
470    }
471
472    let mut prop = unsafe { (*node).properties };
473    while !prop.is_null() {
474        let attr = unsafe { &*prop };
475
476        // Check 1: DTD-declared ID type
477        if attr.atype == XML_ATTRIBUTE_ID as std::os::raw::c_int {
478            if let Some(val) = unsafe { get_attr_value(prop) } {
479                if val == id {
480                    return true;
481                }
482            }
483        }
484
485        // Check 2: attribute named "id" (case-insensitive)
486        if !attr.name.is_null() {
487            let name_str = unsafe { c_xmlchar_to_str(attr.name) };
488            if name_str.as_deref() == Some("id") || name_str.as_deref() == Some("ID") {
489                if let Some(val) = unsafe { get_attr_value(prop) } {
490                    if val == id {
491                        return true;
492                    }
493                }
494            }
495        }
496
497        prop = unsafe { (*prop).next };
498    }
499
500    false
501}
502
503/// Extract the string value of an attribute.
504unsafe fn get_attr_value(attr: *mut _xmlAttr) -> Option<String> {
505    if attr.is_null() {
506        return None;
507    }
508
509    let children = unsafe { (*attr).children };
510    if children.is_null() {
511        return None;
512    }
513
514    let text = unsafe { &*children };
515    if text.type_ == XML_TEXT_NODE as std::os::raw::c_int && !text.content.is_null() {
516        let val = unsafe { c_xmlchar_to_str(text.content) };
517        return val;
518    }
519
520    None
521}
522
523/// Convert a `*const xmlChar` (C string) to a Rust `String`.
524///
525/// SAFETY: `ptr` must point to a null-terminated sequence of bytes.
526unsafe fn c_xmlchar_to_str(ptr: *const crate::abi::types::xmlChar) -> Option<String> {
527    if ptr.is_null() {
528        return None;
529    }
530
531    // xmlChar is `c_uchar`; we reinterpret as `*const c_char` for CStr.
532    let c_str = unsafe { CStr::from_ptr(ptr as *const c_char) };
533    match c_str.to_str() {
534        Ok(s) => Some(s.to_string()),
535        Err(_) => None,
536    }
537}
538
539// ═══════════════════════════════════════════════════════════════════════════════
540// Tests
541// ═══════════════════════════════════════════════════════════════════════════════
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use crate::abi::allocator::xmlMallocZero;
547    use crate::abi::types::xmlElementType::*;
548    use std::mem;
549    use std::os::raw::c_int;
550    use std::ptr;
551
552    // ── Helper: create a minimal document tree for testing ────────────────
553
554    /// Create a minimal document with one element: `<root id="main">`.
555    ///
556    /// # Safety
557    ///
558    /// - The function dereferences the `xmlMallocZero` allocations it makes
559    ///   for `doc`, `root`, and the attribute and text nodes only after
560    ///   asserting they are non-NULL; the returned `doc` owns the whole tree,
561    ///   which the tests deliberately leak (never freed), so no use-after-free
562    ///   is possible.
563    unsafe fn create_simple_doc() -> *mut _xmlDoc {
564        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
565        assert!(!doc.is_null());
566
567        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
568        assert!(!root.is_null());
569
570        unsafe {
571            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
572            (*doc).doc = doc;
573            (*doc).children = root;
574
575            (*root).type_ = XML_ELEMENT_NODE as c_int;
576            (*root).name = string_to_xmlchar("root");
577            (*root).parent = doc as *mut _xmlNode;
578            (*root).doc = doc;
579            (*root).properties = ptr::null_mut();
580        }
581
582        // Add id="main" attribute
583        let attr = unsafe { add_id_attr(root, "id", "main") };
584        unsafe {
585            (*root).properties = attr;
586        }
587
588        doc
589    }
590
591    /// Create a more complex document tree:
592    /// ```
593    /// <root id="main">
594    ///   <child1 id="a"/>
595    ///   <child2 id="b">
596    ///     <grandchild id="c"/>
597    ///   </child2>
598    ///   <child3/>
599    /// </root>
600    /// ```
601    ///
602    /// # Safety
603    ///
604    /// - All nodes and attributes are `xmlMallocZero` allocations asserted
605    ///   non-NULL before being dereferenced and linked; the returned `doc`
606    ///   owns the whole tree, which the tests deliberately leak (never
607    ///   freed), so no use-after-free is possible.
608    unsafe fn create_complex_doc() -> *mut _xmlDoc {
609        let doc = xmlMallocZero(mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
610        assert!(!doc.is_null());
611
612        // root element
613        let root = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
614        assert!(!root.is_null());
615
616        unsafe {
617            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
618            (*doc).doc = doc;
619            (*doc).children = root;
620
621            (*root).type_ = XML_ELEMENT_NODE as c_int;
622            (*root).name = string_to_xmlchar("root");
623            (*root).parent = doc as *mut _xmlNode;
624            (*root).doc = doc;
625        }
626
627        let attr_root = unsafe { add_id_attr(root, "id", "main") };
628        unsafe { (*root).properties = attr_root };
629
630        // child1
631        let child1 = unsafe { append_child_element(root, "child1") };
632        let attr_c1 = unsafe { add_id_attr(child1, "id", "a") };
633        unsafe { (*child1).properties = attr_c1 };
634
635        // child2
636        let child2 = unsafe { append_child_element(root, "child2") };
637        let attr_c2 = unsafe { add_id_attr(child2, "id", "b") };
638        unsafe { (*child2).properties = attr_c2 };
639
640        // grandchild (child of child2)
641        let grandchild = unsafe { append_child_element(child2, "grandchild") };
642        let attr_gc = unsafe { add_id_attr(grandchild, "id", "c") };
643        unsafe { (*grandchild).properties = attr_gc };
644
645        // child3 (no ID)
646        let _child3 = unsafe { append_child_element(root, "child3") };
647
648        doc
649    }
650
651    unsafe fn string_to_xmlchar(s: &str) -> *const crate::abi::types::xmlChar {
652        let c_str = CString::new(s).unwrap();
653        c_str.into_raw() as *const crate::abi::types::xmlChar
654    }
655
656    /// Append a new element node as the last child of `parent`.
657    ///
658    /// # Safety
659    ///
660    /// - `parent` must be a non-NULL pointer to a valid, live `_xmlNode` whose
661    ///   `doc` field is readable; its `children` and `last` links are updated
662    ///   in place, and `name` must stay valid until it is copied by
663    ///   `string_to_xmlchar`.
664    unsafe fn append_child_element(parent: *mut _xmlNode, name: &str) -> *mut _xmlNode {
665        let node = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
666        assert!(!node.is_null());
667
668        unsafe {
669            (*node).type_ = XML_ELEMENT_NODE as c_int;
670            (*node).name = string_to_xmlchar(name);
671            (*node).parent = parent;
672            (*node).doc = (*parent).doc;
673            (*node).next = ptr::null_mut();
674            (*node).prev = (*parent).last;
675            (*node).properties = ptr::null_mut();
676
677            // Link into parent's child list
678            if (*parent).children.is_null() {
679                (*parent).children = node;
680                (*parent).last = node;
681            } else {
682                let last = (*parent).last;
683                if !last.is_null() {
684                    (*last).next = node;
685                }
686                (*parent).last = node;
687            }
688        }
689
690        node
691    }
692
693    /// Add an attribute node with a text-value child to an element.
694    ///
695    /// # Safety
696    ///
697    /// - `node` must be a non-NULL pointer to a valid, live `_xmlNode` whose
698    ///   `doc` field is readable; the attribute and its text child are fresh
699    ///   `xmlMallocZero` allocations asserted non-NULL, and `name` and `value`
700    ///   must stay valid until copied by `string_to_xmlchar`.
701    unsafe fn add_id_attr(node: *mut _xmlNode, name: &str, value: &str) -> *mut _xmlAttr {
702        let attr = xmlMallocZero(mem::size_of::<_xmlAttr>()) as *mut _xmlAttr;
703        assert!(!attr.is_null());
704
705        // Create text child for the attribute value
706        let text = xmlMallocZero(mem::size_of::<_xmlNode>()) as *mut _xmlNode;
707        assert!(!text.is_null());
708
709        unsafe {
710            (*attr).type_ = 2; // XML_ATTRIBUTE_NODE
711            (*attr).name = string_to_xmlchar(name);
712            (*attr).parent = node;
713            (*attr).doc = (*node).doc;
714            (*attr).children = text;
715            (*attr).last = text;
716            (*attr).atype = crate::abi::types::xmlAttributeType::XML_ATTRIBUTE_CDATA as c_int;
717            (*attr).next = ptr::null_mut();
718            (*attr).prev = ptr::null_mut();
719
720            (*text).type_ = XML_TEXT_NODE as c_int;
721            (*text).name = string_to_xmlchar("text");
722            (*text).content = string_to_xmlchar(value) as *mut crate::abi::types::xmlChar;
723            (*text).parent = attr as *mut _xmlNode;
724            (*text).doc = (*node).doc;
725            (*text).next = ptr::null_mut();
726            (*text).prev = ptr::null_mut();
727        }
728
729        attr
730    }
731
732    // ── Tests ────────────────────────────────────────────────────────────
733
734    macro_rules! c_name_eq {
735        ($node:expr, $expected:expr) => {
736            assert_eq!(
737                CStr::from_ptr((*$node).name as *const c_char)
738                    .to_str()
739                    .unwrap(),
740                $expected
741            );
742        };
743    }
744
745    #[test]
746    /// Tests that a bare name resolves as a shorthand ID pointer.
747    ///
748    /// # Safety
749    ///
750    /// - `doc` is built by `create_simple_doc`, which allocates and links the
751    ///   nodes; the resolved node is dereferenced by `c_name_eq` while `doc`
752    ///   is still live (the tree is leaked, never freed).
753    fn test_shorthand_pointer() {
754        unsafe {
755            let doc = create_simple_doc();
756            let result = xptr_eval("main", doc);
757            assert!(result.is_some());
758            c_name_eq!(result.unwrap(), "root");
759        }
760    }
761
762    #[test]
763    /// Tests that an unknown shorthand pointer resolves to nothing.
764    ///
765    /// # Safety
766    ///
767    /// - `doc` is built by `create_simple_doc`; `xptr_eval` walks the live
768    ///   tree and returns `None` without any dangling dereferences.
769    fn test_shorthand_pointer_not_found() {
770        unsafe {
771            let doc = create_simple_doc();
772            let result = xptr_eval("nonexistent", doc);
773            assert!(result.is_none());
774        }
775    }
776
777    #[test]
778    /// Tests basic `element(id)` scheme resolution.
779    ///
780    /// # Safety
781    ///
782    /// - `doc` is built by `create_complex_doc`; every node dereferenced by
783    ///   the lookup and by `c_name_eq` is part of that live, leaked tree.
784    fn test_element_scheme_basic() {
785        unsafe {
786            let doc = create_complex_doc();
787
788            let result = xptr_eval("element(main)", doc);
789            assert!(result.is_some());
790            c_name_eq!(result.unwrap(), "root");
791
792            let result = xptr_eval("element(a)", doc);
793            assert!(result.is_some());
794            c_name_eq!(result.unwrap(), "child1");
795
796            let result = xptr_eval("element(c)", doc);
797            assert!(result.is_some());
798            c_name_eq!(result.unwrap(), "grandchild");
799        }
800    }
801
802    #[test]
803    /// Tests `element(id/N)` child-sequence traversal.
804    ///
805    /// # Safety
806    ///
807    /// - `doc` is built by `create_complex_doc`; every node dereferenced by
808    ///   the child-axis walk and by `c_name_eq` belongs to that live, leaked
809    ///   tree.
810    fn test_element_scheme_with_child_sequence() {
811        unsafe {
812            let doc = create_complex_doc();
813
814            let result = xptr_eval("element(main/1)", doc);
815            assert!(result.is_some());
816            c_name_eq!(result.unwrap(), "child1");
817
818            let result = xptr_eval("element(main/2)", doc);
819            assert!(result.is_some());
820            c_name_eq!(result.unwrap(), "child2");
821
822            let result = xptr_eval("element(main/2/1)", doc);
823            assert!(result.is_some());
824            c_name_eq!(result.unwrap(), "grandchild");
825        }
826    }
827
828    #[test]
829    /// Tests that an out-of-range child index resolves to nothing.
830    ///
831    /// # Safety
832    ///
833    /// - `doc` is built by `create_complex_doc`; `xptr_eval` walks the live
834    ///   tree and returns `None` for the out-of-range index without dangling
835    ///   dereferences.
836    fn test_element_scheme_child_out_of_range() {
837        unsafe {
838            let doc = create_complex_doc();
839            let result = xptr_eval("element(main/99)", doc);
840            assert!(result.is_none());
841        }
842    }
843
844    #[test]
845    /// Tests that a zero child index is rejected by the `element` scheme.
846    ///
847    /// # Safety
848    ///
849    /// - `doc` is built by `create_complex_doc`; `xptr_eval` walks the live
850    ///   tree and returns `None` because zero is not a valid 1-indexed child
851    ///   position.
852    fn test_element_scheme_zero_index() {
853        unsafe {
854            let doc = create_complex_doc();
855            let result = xptr_eval("element(main/0)", doc);
856            assert!(result.is_none());
857        }
858    }
859
860    #[test]
861    /// Tests that an empty expression resolves to nothing.
862    ///
863    /// # Safety
864    ///
865    /// - `doc` is built by `create_simple_doc`; `xptr_eval` returns early for
866    ///   the empty expression, and the tree stays live for the whole test.
867    fn test_empty_expr() {
868        unsafe {
869            let doc = create_simple_doc();
870            let result = xptr_eval("", doc);
871            assert!(result.is_none());
872        }
873    }
874
875    #[test]
876    /// Tests that a NULL document resolves to nothing.
877    ///
878    /// # Safety
879    ///
880    /// - `xptr_eval` checks `doc` for NULL and returns early without
881    ///   dereferencing it.
882    fn test_null_doc() {
883        unsafe {
884            let result = xptr_eval("main", ptr::null_mut());
885            assert!(result.is_none());
886        }
887    }
888
889    #[test]
890    /// Tests the `xmlXPtrEval` C ABI entry point.
891    ///
892    /// # Safety
893    ///
894    /// - `c_expr` is a `CString` that stays alive for the call; `doc` is
895    ///   built by `create_simple_doc` and stays live while the returned node
896    ///   is dereferenced by `c_name_eq`.
897    fn test_xml_xptr_eval_c_abi() {
898        unsafe {
899            let doc = create_simple_doc();
900            let c_expr = CString::new("main").unwrap();
901            let node = xmlXPtrEval(c_expr.as_ptr(), doc);
902            assert!(!node.is_null());
903            c_name_eq!(node, "root");
904        }
905    }
906
907    #[test]
908    /// Tests that `xmlXPtrEval` returns NULL for a NULL expression.
909    ///
910    /// # Safety
911    ///
912    /// - `xmlXPtrEval` checks `expr` for NULL and returns early without
913    ///   dereferencing it; `doc` stays live for the whole test.
914    fn test_xml_xptr_eval_null_expr() {
915        unsafe {
916            let doc = create_simple_doc();
917            let node = xmlXPtrEval(ptr::null(), doc);
918            assert!(node.is_null());
919        }
920    }
921
922    #[test]
923    /// Tests that `xmlXPtrEval` returns NULL for a NULL document.
924    ///
925    /// # Safety
926    ///
927    /// - `c_expr` is a `CString` that stays alive for the call; `xmlXPtrEval`
928    ///   checks `doc` for NULL and returns early without dereferencing it.
929    fn test_xml_xptr_eval_null_doc() {
930        unsafe {
931            let c_expr = CString::new("main").unwrap();
932            let node = xmlXPtrEval(c_expr.as_ptr(), ptr::null_mut());
933            assert!(node.is_null());
934        }
935    }
936
937    #[test]
938    /// Tests the `xmlXPtrEvalNodeSet` C ABI entry point.
939    ///
940    /// # Safety
941    ///
942    /// - `c_expr` is a `CString` alive for the call; `doc` is built by
943    ///   `create_simple_doc` and stays live; the returned node set is asserted
944    ///   non-NULL before `(*ns).nodeNr` and `(*ns).nodeTab` are read, and the
945    ///   node taken from `(*ns).nodeTab` is dereferenced by `c_name_eq` while
946    ///   `doc` is live.
947    fn test_xml_xptr_eval_node_set() {
948        unsafe {
949            let doc = create_simple_doc();
950            let c_expr = CString::new("main").unwrap();
951            let ns = xmlXPtrEvalNodeSet(c_expr.as_ptr(), doc);
952            assert!(!ns.is_null());
953            assert_eq!((*ns).nodeNr, 1);
954            assert!(!(*ns).nodeTab.is_null());
955            let node = *(*ns).nodeTab;
956            c_name_eq!(node, "root");
957        }
958    }
959
960    #[test]
961    /// Tests that an unknown ID in the `element` scheme resolves to nothing.
962    ///
963    /// # Safety
964    ///
965    /// - `doc` is built by `create_complex_doc`; the lookup walks the live
966    ///   tree and returns `None` without dangling dereferences.
967    fn test_element_scheme_not_found() {
968        unsafe {
969            let doc = create_complex_doc();
970            let result = xptr_eval("element(nonexistent)", doc);
971            assert!(result.is_none());
972        }
973    }
974
975    #[test]
976    /// Tests that whitespace inside the `element` scheme is tolerated.
977    ///
978    /// # Safety
979    ///
980    /// - `doc` is built by `create_complex_doc`; the resolved node is
981    ///   dereferenced by `c_name_eq` while the tree is live.
982    fn test_element_scheme_extra_spaces() {
983        unsafe {
984            let doc = create_complex_doc();
985            let result = xptr_eval("element( main )", doc);
986            assert!(result.is_some());
987            c_name_eq!(result.unwrap(), "root");
988        }
989    }
990
991    #[test]
992    /// Tests that an element without an ID is not found by shorthand lookup.
993    ///
994    /// # Safety
995    ///
996    /// - `doc` is built by `create_complex_doc`; `xptr_eval` walks the live
997    ///   tree and returns `None` for the ID-less element.
998    fn test_child3_no_id() {
999        unsafe {
1000            let doc = create_complex_doc();
1001            let result = xptr_eval("child3", doc);
1002            assert!(result.is_none());
1003        }
1004    }
1005}