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