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}