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}