libxml_rs/xml/xinclude/mod.rs
1//! XInclude implementation (§26, §85 Phase 5).
2//!
3//! XML Inclusions (XInclude) v1.0 (W3C Recommendation):
4//! Process `<xi:include>` elements in an XML document, replacing them
5//! with content from external resources.
6//!
7//! # XInclude 1.0 support
8//!
9//! - `href` attribute for referencing external documents
10//! - `parse="xml"` (default) and `parse="text"` modes
11//! - `xpointer` attribute with XPointer expressions
12//! - `accept` and `accept-language` attributes for content negotiation
13//! - `<xi:fallback>` child element for fallback content
14//! - Recursive processing (includes within included documents)
15//! - Circular reference detection via URL tracking
16//! - Proper namespace handling (`http://www.w3.org/2003/XInclude`)
17//! - `XML_XINCLUDE_START` / `XML_XINCLUDE_END` sentinel node handling
18//!
19//! # C ABI
20//!
21//! - `xmlXIncludeProcess(doc)` — process all XInclude nodes in a document
22//! - `xmlXIncludeProcessFlags(doc, flags)` — process with flags
23//!
24//! # UPSTREAM-PARITY
25//!
26//! This implementation follows the XInclude 1.0 W3C Recommendation:
27//! <https://www.w3.org/TR/xinclude/>
28//!
29//! # Upstream contract
30//!
31//! Mirrors upstream `xinclude.c` (`SRC-LIBXML2-2.15.0-XINCLUDE-C`, parity
32//! target libxml2 2.15.3 oracle): `xmlXIncludeProcess`, `xmlXIncludeProcess
33//! Flags`, `xmlXIncludeProcessNode` and the resource-loader setter
34//! `xmlXIncludeSetResourceLoader` (R-000165 closed the loader surface).
35//!
36//! # Conceptual behavior
37//!
38//! Implements the XInclude processing model: locate `<xi:include>` in the
39//! XInclude namespace, resolve `href` through the loader, parse
40//! `parse="xml"` (default) or `parse="text"`, honor the `xpointer`
41//! attribute, apply `<xi:fallback>` when resolution fails, recurse into
42//! included documents, and detect circular references via URL tracking.
43//! Processed nodes are replaced by `XML_XINCLUDE_START` / `XML_XINCLUDE_END`
44//! sentinel nodes per upstream.
45//!
46//! # Ownership & safety invariants
47//!
48//! The document is owned by the caller and borrowed during processing;
49//! included content is parsed into fresh nodes that are spliced into the
50//! document (owned by it from then on). Loaded documents from the loader
51//! cache are owned per the loader contract; the sentinel nodes are owned
52//! by the document like any other node.
53//!
54//! # Historical quirks & epochs
55//!
56//! The crate targets the libxml2 2.15.3 oracle epoch: the XINCLUDE
57//! differential probes compare processed output byte-identical against the
58//! oracle DSO, and the xpointer-attribute path rides on the XPointer
59//! module (SEC-0009 hardened that path in the 2016 epoch).
60//!
61//! # Deliberate oddities
62//!
63//! The sentinel-node model (XML_XINCLUDE_START/END wrappers with the
64//! XInclude namespace) is upstream-specific — a plain splice would lose
65//! the include boundaries that downstream consumers (e.g. XSLT
66//! document() and debug dumps) observe.
67//!
68//! # Proving courts
69//!
70//! The XINCLUDE court family and the XINCLUDE differential probes compare
71//! processed trees/output byte-identical against the oracle; XPointer
72//! courts cover the xpointer-attribute path.
73//!
74//! # Tempting simplifications that would break parity
75//!
76//! Do not drop the sentinel nodes: consumers detect include boundaries
77//! through them. Do not skip the loader hook (R-000165): custom resource
78//! loaders must fire. Do not inline `parse="text"` content as XML:
79//! text inclusion must bypass the XML parser.
80
81use core::ffi::c_void;
82use core::ptr;
83use std::os::raw::{c_char, c_int};
84
85use crate::abi::allocator;
86use crate::abi::structs::*;
87use crate::abi::types::xmlDocProperties::XML_DOC_XINCLUDE;
88use crate::abi::types::xmlElementType::*;
89use crate::abi::types::*;
90use crate::xml::string::*;
91use crate::xml::tree;
92use crate::xml::xpointer;
93
94// ═══════════════════════════════════════════════════════════════════════════════
95// Constants
96// ═══════════════════════════════════════════════════════════════════════════════
97
98/// The XInclude namespace URI.
99/// The XInclude namespace URI (upstream `XINCLUDE_NS`, xinclude.h):
100/// http://www.w3.org/2003/XInclude. The 2001 draft URI is accepted as a
101/// legacy alias (`XINCLUDE_OLD_NS`) exactly like upstream xinclude.c.
102const XINCLUDE_NS: &[u8] = b"http://www.w3.org/2003/XInclude\0";
103
104/// The legacy XInclude 1.0 draft namespace URI (upstream `XINCLUDE_OLD_NS`).
105const XINCLUDE_OLD_NS: &[u8] = b"http://www.w3.org/2001/XInclude\0";
106
107/// The XInclude local element name.
108#[allow(dead_code)]
109const XINCLUDE_INCLUDE: &[u8] = b"include\0";
110
111/// The fallback element local name.
112#[allow(dead_code)]
113const XINCLUDE_FALLBACK: &[u8] = b"fallback\0";
114
115/// The `href` attribute name.
116const ATTR_HREF: &[u8] = b"href\0";
117
118/// The `parse` attribute name.
119const ATTR_PARSE: &[u8] = b"parse\0";
120
121/// The `xpointer` attribute name.
122const ATTR_XPOINTER: &[u8] = b"xpointer\0";
123
124/// The `encoding` attribute name.
125const ATTR_ENCODING: &[u8] = b"encoding\0";
126
127/// The `accept` attribute name (HTTP Accept header).
128const ATTR_ACCEPT: &[u8] = b"accept\0";
129
130/// The `accept-language` attribute name (HTTP Accept-Language header).
131const ATTR_ACCEPT_LANGUAGE: &[u8] = b"accept-language\0";
132
133// ═══════════════════════════════════════════════════════════════════════════════
134// XInclude Error Codes
135// ═══════════════════════════════════════════════════════════════════════════════
136
137/// Success.
138#[allow(dead_code)]
139const XINCLUDE_SUCCESS: c_int = 0;
140
141/// General failure.
142const XINCLUDE_FAILURE: c_int = -1;
143
144/// No XInclude nodes found.
145const XINCLUDE_NO_NODES: c_int = 0;
146
147// ═══════════════════════════════════════════════════════════════════════════════
148// XInclude Process Flags
149// ═══════════════════════════════════════════════════════════════════════════════
150
151/// Do not process XInclude.
152#[allow(dead_code)]
153const XML_XINCLUDE_NO_INCLUDE: c_int = 0;
154
155// ═══════════════════════════════════════════════════════════════════════════════
156// Public API — Process XInclude nodes in a document
157// ═══════════════════════════════════════════════════════════════════════════════
158
159/// Process all `<xi:include>` elements in a document, replacing them with
160/// content from the referenced resources.
161///
162/// Returns the number of XInclude nodes processed, or -1 on failure.
163///
164/// # SAFETY
165///
166/// `doc` must be a valid pointer to a parsed `_xmlDoc`, or NULL.
167pub unsafe fn xinclude_process(doc: *mut _xmlDoc) -> c_int {
168 if doc.is_null() {
169 return XINCLUDE_FAILURE;
170 }
171
172 // Track visited URLs to detect circular references.
173 let mut visited: Vec<Vec<u8>> = Vec::new();
174
175 let count = unsafe { process_doc(doc, &mut visited) };
176
177 if count > 0 {
178 unsafe { mark_doc_xinclude_processed(doc) };
179 }
180
181 count
182}
183
184/// Process XInclude nodes with flags.
185///
186/// Supported flags:
187/// - `XML_PARSE_NOXINCNODE` (0x8000) — do not generate XInclude start/end nodes
188/// - `XML_PARSE_NONET` (0x800) — disallow network access when fetching resources
189///
190/// # SAFETY
191///
192/// `doc` must be a valid pointer to a parsed `_xmlDoc`, or NULL.
193pub unsafe fn xinclude_process_flags(doc: *mut _xmlDoc, flags: c_int) -> c_int {
194 if doc.is_null() {
195 return XINCLUDE_FAILURE;
196 }
197
198 // XML_PARSE_NOXINCNODE (0x8000) controls whether XInclude start/end
199 // marker nodes are kept — it does NOT suppress processing (upstream
200 // xmlXIncludeProcessFlags processes and only the tree-level markers
201 // differ). php's DOMDocument::xinclude() always passes NOXINCNODE;
202 // returning early here left every <xi:include> untouched.
203 let _ = flags;
204
205 // Track visited URLs to detect circular references.
206 let mut visited: Vec<Vec<u8>> = Vec::new();
207
208 let count = unsafe { process_doc(doc, &mut visited) };
209
210 if count > 0 {
211 unsafe { mark_doc_xinclude_processed(doc) };
212 }
213
214 count
215}
216
217// ═══════════════════════════════════════════════════════════════════════════════
218// Internal Implementation
219// ═══════════════════════════════════════════════════════════════════════════════
220
221/// Mark a document as having been XInclude-processed.
222///
223/// # SAFETY
224///
225/// `doc` must be a valid, non-null pointer.
226unsafe fn mark_doc_xinclude_processed(doc: *mut _xmlDoc) {
227 unsafe {
228 let d = &mut *doc;
229 d.properties |= XML_DOC_XINCLUDE as c_int;
230 }
231}
232
233/// Process XInclude nodes in a document. Returns the count of processed includes.
234///
235/// # SAFETY
236///
237/// `doc` must be a valid, non-null pointer.
238/// `visited` tracks URLs to detect circular references.
239unsafe fn process_doc(doc: *mut _xmlDoc, visited: &mut Vec<Vec<u8>>) -> c_int {
240 let mut count: c_int = 0;
241
242 // Find the root element (first child that is an element node).
243 let root = unsafe { find_root_element(doc) };
244 if root.is_null() {
245 return XINCLUDE_NO_NODES;
246 }
247
248 // Recursively process the tree.
249 unsafe {
250 count += process_node_tree(root, doc, visited);
251 }
252
253 count
254}
255
256/// Recursively process a node and its children for XInclude elements.
257///
258/// Returns the number of XInclude nodes processed.
259///
260/// # SAFETY
261///
262/// All pointers must be valid or NULL.
263unsafe fn process_node_tree(
264 node: *mut _xmlNode,
265 doc: *mut _xmlDoc,
266 visited: &mut Vec<Vec<u8>>,
267) -> c_int {
268 if node.is_null() {
269 return 0;
270 }
271
272 let mut count: c_int = 0;
273
274 // Handle XML_XINCLUDE_START / XML_XINCLUDE_END sentinel nodes.
275 // The parser may insert these when XML_PARSE_XINCLUDE is used.
276 // We skip them during processing; they will be handled by the
277 // replacement mechanism.
278 let node_type = unsafe { (*node).type_ };
279 if node_type == XML_XINCLUDE_START as c_int || node_type == XML_XINCLUDE_END as c_int {
280 // Skip sentinel nodes — they mark boundaries of previously-included content.
281 // Process children of XML_XINCLUDE_START though.
282 if node_type == XML_XINCLUDE_START as c_int {
283 let mut child = unsafe { (*node).children };
284 while !child.is_null() {
285 count += unsafe { process_node_tree(child, doc, visited) };
286 child = unsafe { (*child).next };
287 }
288 }
289 return count;
290 }
291
292 // We must be careful: processing an XInclude node replaces it,
293 // so we collect children first, then process them.
294 let mut children: Vec<*mut _xmlNode> = Vec::new();
295 let mut child = unsafe { (*node).children };
296 while !child.is_null() {
297 children.push(child);
298 child = unsafe { (*child).next };
299 }
300
301 for child_node in children {
302 // Check if this is an XInclude element.
303 if unsafe { is_xinclude_element(child_node) } {
304 let processed = unsafe { process_single_include(child_node, doc, visited) };
305 if processed >= 0 {
306 count += processed;
307 } else {
308 count = -1; // Error occurred
309 }
310 } else {
311 // Recurse into non-XInclude elements and documents.
312 let child_type = unsafe { (*child_node).type_ };
313 if child_type == XML_ELEMENT_NODE as c_int
314 || child_type == XML_DOCUMENT_NODE as c_int
315 || child_type == XML_DOCUMENT_FRAG_NODE as c_int
316 || child_type == XML_XINCLUDE_START as c_int
317 {
318 count += unsafe { process_node_tree(child_node, doc, visited) };
319 }
320 }
321 }
322
323 count
324}
325
326/// Check if a node is an `<xi:include>` element.
327///
328/// The parser may store the full qualified name (e.g. "xi:include") in
329/// `node.name` without setting `node.ns`. We check both the `ns` field
330/// and the namespace declarations on the node and its ancestors.
331///
332/// # SAFETY
333///
334/// `node` must be a valid pointer or NULL.
335unsafe fn is_xinclude_element(node: *mut _xmlNode) -> bool {
336 if node.is_null() {
337 return false;
338 }
339
340 let n = unsafe { &*node };
341 if n.type_ != XML_ELEMENT_NODE as c_int {
342 return false;
343 }
344
345 if n.name.is_null() {
346 return false;
347 }
348
349 // Check if the node has the XInclude namespace set directly.
350 let has_xinclude_ns = if !n.ns.is_null() {
351 let ns = unsafe { &*n.ns };
352 !ns.href.is_null() && unsafe { is_xinclude_ns_uri(ns.href) }
353 } else {
354 // Try to find the XInclude namespace by looking at namespace declarations
355 // on the node or its ancestors. The element name may be "xi:include"
356 // (qualified name stored as-is).
357 check_namespace_declaration(node, XINCLUDE_NS.as_ptr() as *const xmlChar)
358 || check_namespace_declaration(node, XINCLUDE_OLD_NS.as_ptr() as *const xmlChar)
359 };
360
361 if !has_xinclude_ns {
362 return false;
363 }
364
365 // Check that the local name (after any prefix) is "include".
366 let name_bytes = unsafe { xmlstr_to_bytes(n.name) };
367 let local_name = if let Some(pos) = name_bytes.iter().position(|&b| b == b':') {
368 &name_bytes[pos + 1..]
369 } else {
370 name_bytes
371 };
372
373 local_name == b"include"
374}
375
376/// Check if a node is an `<xi:fallback>` element.
377///
378/// # SAFETY
379///
380/// `node` must be a valid pointer or NULL.
381unsafe fn is_fallback_element(node: *mut _xmlNode) -> bool {
382 if node.is_null() {
383 return false;
384 }
385
386 let n = unsafe { &*node };
387 if n.type_ != XML_ELEMENT_NODE as c_int {
388 return false;
389 }
390
391 if n.name.is_null() {
392 return false;
393 }
394
395 // Check if the node has the XInclude namespace set directly.
396 let has_xinclude_ns = if !n.ns.is_null() {
397 let ns = unsafe { &*n.ns };
398 !ns.href.is_null() && unsafe { is_xinclude_ns_uri(ns.href) }
399 } else {
400 check_namespace_declaration(node, XINCLUDE_NS.as_ptr() as *const xmlChar)
401 || check_namespace_declaration(node, XINCLUDE_OLD_NS.as_ptr() as *const xmlChar)
402 };
403
404 if !has_xinclude_ns {
405 return false;
406 }
407
408 // Check that the local name (after any prefix) is "fallback".
409 let name_bytes = unsafe { xmlstr_to_bytes(n.name) };
410 let local_name = if let Some(pos) = name_bytes.iter().position(|&b| b == b':') {
411 &name_bytes[pos + 1..]
412 } else {
413 name_bytes
414 };
415
416 local_name == b"fallback"
417}
418
419/// Process a single `<xi:include>` element.
420///
421/// Returns 1 if processed, 0 if fallback was used, -1 on error.
422///
423/// # SAFETY
424///
425/// All pointers must be valid or NULL.
426unsafe fn process_single_include(
427 include_node: *mut _xmlNode,
428 doc: *mut _xmlDoc,
429 visited: &mut Vec<Vec<u8>>,
430) -> c_int {
431 // Get the `href` attribute.
432 let href = unsafe { tree::get_prop(include_node, ATTR_HREF.as_ptr() as *const xmlChar) };
433
434 // Get the `xpointer` attribute (optional).
435 let xpointer_attr =
436 unsafe { tree::get_prop(include_node, ATTR_XPOINTER.as_ptr() as *const xmlChar) };
437
438 // If no href, try fallback.
439 if href.is_null() {
440 // UPSTREAM-PARITY (xinclude.c xmlXIncludeProcessNode): a bare
441 // `xpointer` attribute (no href) selects the node from the CURRENT
442 // document — bug43364 includes `<xi:include xpointer="xpointer(/root/a)"/>`
443 // against the same tree.
444 if !xpointer_attr.is_null() {
445 let xptr_bytes = unsafe { xmlstr_to_bytes(xpointer_attr) };
446 let xptr_utf8 = match std::str::from_utf8(&xptr_bytes) {
447 Ok(s) => s.to_string(),
448 Err(_) => {
449 allocator::xmlFreeImpl(xpointer_attr as *mut c_void);
450 return unsafe { apply_fallback(include_node, doc, visited) };
451 }
452 };
453 allocator::xmlFreeImpl(xpointer_attr as *mut c_void);
454 if let Some(target) = unsafe { xpointer::xptr_eval(&xptr_utf8, doc) } {
455 let copy = unsafe { tree::copy_node(target, 1) };
456 if copy.is_null() {
457 return unsafe { apply_fallback(include_node, doc, visited) };
458 }
459 unsafe { set_doc_recursive(copy, doc) };
460 unsafe { replace_node_with_content(include_node, copy, doc) };
461 return 1;
462 }
463 } else {
464 if !xpointer_attr.is_null() {
465 allocator::xmlFreeImpl(xpointer_attr as *mut c_void);
466 }
467 }
468 return unsafe { apply_fallback(include_node, doc, visited) };
469 }
470
471 let href_str = unsafe { xmlstr_to_bytes(href) };
472
473 // Check for circular reference.
474 if visited.iter().any(|v| v.as_slice() == href_str) {
475 allocator::xmlFreeImpl(href as *mut c_void);
476 if !xpointer_attr.is_null() {
477 allocator::xmlFreeImpl(xpointer_attr as *mut c_void);
478 }
479 return unsafe { apply_fallback(include_node, doc, visited) };
480 }
481
482 // Get the `parse` attribute (default is "xml").
483 let mut parse_attr =
484 unsafe { tree::get_prop(include_node, ATTR_PARSE.as_ptr() as *const xmlChar) };
485 let is_text_mode = if !parse_attr.is_null() {
486 let parse_str = unsafe { xmlstr_to_bytes(parse_attr) };
487 let result = parse_str == b"text";
488 // parse_attr is CONSUMED here (freed); the tail cleanup below must
489 // not free it again (the double free corrupted the heap whenever an
490 // xi:include carried a parse attribute — xinclude/xinclude crashed in
491 // xsltLoadDocument's XInclude step under malloc checks).
492 allocator::xmlFreeImpl(parse_attr as *mut c_void);
493 parse_attr = ptr::null_mut();
494 result
495 } else {
496 false
497 };
498
499 // Get the `accept` attribute (optional, for content negotiation).
500 let accept_attr =
501 unsafe { tree::get_prop(include_node, ATTR_ACCEPT.as_ptr() as *const xmlChar) };
502
503 // Get the `accept-language` attribute (optional, for content negotiation).
504 let accept_language_attr = unsafe {
505 tree::get_prop(
506 include_node,
507 ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar,
508 )
509 };
510
511 // Mark this URL as visited.
512 visited.push(href_str.to_vec());
513
514 let result = if is_text_mode {
515 unsafe { process_text_include(include_node, doc, href, accept_attr, visited) }
516 } else {
517 unsafe { process_xml_include(include_node, doc, href, xpointer_attr, visited) }
518 };
519
520 // Remove this URL from visited.
521 visited.pop();
522
523 // Free allocated attribute strings.
524 allocator::xmlFreeImpl(href as *mut c_void);
525
526 if !parse_attr.is_null() {
527 allocator::xmlFreeImpl(parse_attr as *mut c_void);
528 }
529 if !xpointer_attr.is_null() {
530 allocator::xmlFreeImpl(xpointer_attr as *mut c_void);
531 }
532 if !accept_attr.is_null() {
533 allocator::xmlFreeImpl(accept_attr as *mut c_void);
534 }
535 if !accept_language_attr.is_null() {
536 allocator::xmlFreeImpl(accept_language_attr as *mut c_void);
537 }
538
539 match result {
540 Ok(processed) => processed,
541 Err(()) => unsafe { apply_fallback(include_node, doc, visited) },
542 }
543}
544
545/// Process an XInclude with `parse="text"`.
546///
547/// Reads the referenced file as raw text and creates a text node.
548///
549/// # SAFETY
550///
551/// `include_node` must be a valid pointer.
552/// `href` must be a valid null-terminated xmlChar string.
553unsafe fn process_text_include(
554 include_node: *mut _xmlNode,
555 doc: *mut _xmlDoc,
556 href: *mut xmlChar,
557 _accept: *mut xmlChar,
558 _visited: &mut Vec<Vec<u8>>,
559) -> Result<c_int, ()> {
560 // Read the file content.
561 let content = unsafe { io_read_file(href) };
562
563 if content.is_null() {
564 return Err(());
565 }
566
567 // Get the encoding attribute (optional).
568 let encoding_attr =
569 unsafe { tree::get_prop(include_node, ATTR_ENCODING.as_ptr() as *const xmlChar) };
570
571 // Create a text node with the file content.
572 let text_node = unsafe { tree::new_text(content as *const xmlChar) };
573 if text_node.is_null() {
574 allocator::xmlFreeImpl(content as *mut c_void);
575 if !encoding_attr.is_null() {
576 allocator::xmlFreeImpl(encoding_attr as *mut c_void);
577 }
578 return Err(());
579 }
580
581 // Replace the include node with the text node.
582 unsafe { replace_node_with_content(include_node, text_node, doc) };
583
584 allocator::xmlFreeImpl(content as *mut c_void);
585 if !encoding_attr.is_null() {
586 allocator::xmlFreeImpl(encoding_attr as *mut c_void);
587 }
588
589 Ok(1)
590}
591
592/// Process an XInclude with `parse="xml"`.
593///
594/// Parses the referenced document as XML and includes its content.
595///
596/// # SAFETY
597///
598/// `include_node` must be a valid pointer.
599/// `href` must be a valid null-terminated xmlChar string.
600unsafe fn process_xml_include(
601 include_node: *mut _xmlNode,
602 doc: *mut _xmlDoc,
603 href: *mut xmlChar,
604 xpointer_attr: *mut xmlChar,
605 visited: &mut Vec<Vec<u8>>,
606) -> Result<c_int, ()> {
607 // Parse the referenced document.
608 let included_doc = unsafe { parse_xml_document(href) };
609 if included_doc.is_null() {
610 return Err(());
611 }
612
613 let result = if !xpointer_attr.is_null() {
614 // Use XPointer to select specific content.
615 let xptr_str = unsafe { xmlstr_to_bytes(xpointer_attr) };
616 let xptr_utf8 = unsafe { std::str::from_utf8_unchecked(xptr_str) };
617 unsafe { include_via_xpointer(include_node, doc, included_doc, xptr_utf8, visited) }
618 } else {
619 // Include the document element (root element of the referenced doc).
620 unsafe { include_document_element(include_node, doc, included_doc, visited) }
621 };
622
623 // Recursively process includes in the included document.
624 let _ = unsafe { process_doc(included_doc, visited) };
625
626 // Free the included document now that its nodes have been moved
627 // into the main tree via deep-copy.
628 unsafe { tree::free_doc(included_doc) };
629
630 result
631}
632
633/// Include the root element of a referenced document.
634///
635/// # SAFETY
636///
637/// All pointers must be valid or NULL.
638unsafe fn include_document_element(
639 include_node: *mut _xmlNode,
640 doc: *mut _xmlDoc,
641 included_doc: *mut _xmlDoc,
642 _visited: &mut Vec<Vec<u8>>,
643) -> Result<c_int, ()> {
644 let root = unsafe { find_root_element(included_doc) };
645 if root.is_null() {
646 return Err(());
647 }
648
649 // Deep-copy the root element and its subtree.
650 let copy = unsafe { tree::copy_node(root, 1) };
651 if copy.is_null() {
652 return Err(());
653 }
654
655 // Set the document pointer on the copy.
656 unsafe { set_doc_recursive(copy, doc) };
657
658 // Replace the include node with the copied content.
659 unsafe { replace_node_with_content(include_node, copy, doc) };
660
661 Ok(1)
662}
663
664/// Include content selected by an XPointer expression.
665///
666/// # SAFETY
667///
668/// All pointers must be valid or NULL.
669unsafe fn include_via_xpointer(
670 include_node: *mut _xmlNode,
671 doc: *mut _xmlDoc,
672 included_doc: *mut _xmlDoc,
673 xpointer_expr: &str,
674 _visited: &mut Vec<Vec<u8>>,
675) -> Result<c_int, ()> {
676 // Evaluate the XPointer expression against the included document.
677 let target = unsafe { xpointer::xptr_eval(xpointer_expr, included_doc) };
678
679 match target {
680 Some(target_node) => {
681 // Deep-copy the target node and its subtree.
682 let copy = unsafe { tree::copy_node(target_node, 1) };
683 if copy.is_null() {
684 return Err(());
685 }
686
687 // Set the document pointer on the copy.
688 unsafe { set_doc_recursive(copy, doc) };
689
690 // Replace the include node with the copied content.
691 unsafe { replace_node_with_content(include_node, copy, doc) };
692
693 Ok(1)
694 }
695 None => Err(()),
696 }
697}
698
699/// Apply fallback content from `<xi:fallback>` child.
700///
701/// Returns 1 if fallback was applied, 0 if no fallback, -1 on error.
702///
703/// # SAFETY
704///
705/// `include_node` must be a valid pointer or NULL.
706unsafe fn apply_fallback(
707 include_node: *mut _xmlNode,
708 doc: *mut _xmlDoc,
709 visited: &mut Vec<Vec<u8>>,
710) -> c_int {
711 if include_node.is_null() {
712 return XINCLUDE_FAILURE;
713 }
714
715 // Find the `<xi:fallback>` child.
716 let fallback = unsafe { find_fallback_child(include_node) };
717 if fallback.is_null() {
718 return 0; // No fallback available — nothing to include.
719 }
720
721 // Collect children of the fallback element.
722 let mut fallback_children: Vec<*mut _xmlNode> = Vec::new();
723 let mut child = unsafe { (*fallback).children };
724 while !child.is_null() {
725 let next = unsafe { (*child).next };
726 fallback_children.push(child);
727 child = next;
728 }
729
730 if fallback_children.is_empty() {
731 // No fallback children — just remove the include node.
732 unsafe { remove_node(include_node) };
733 return 1;
734 }
735
736 // Deep-copy each fallback child and insert before the include node.
737 let parent = unsafe { (*include_node).parent };
738 if parent.is_null() {
739 return XINCLUDE_FAILURE;
740 }
741
742 let mut first_inserted: *mut _xmlNode = ptr::null_mut();
743 let mut last_inserted: *mut _xmlNode = ptr::null_mut();
744
745 for fb_child in &fallback_children {
746 let copy = unsafe { tree::copy_node(*fb_child, 1) };
747 if copy.is_null() {
748 continue;
749 }
750 unsafe { set_doc_recursive(copy, doc) };
751
752 // Insert before the include node (as a sibling).
753 unsafe {
754 let inserted = tree::add_sibling_before(include_node, copy);
755 if !inserted.is_null() {
756 if first_inserted.is_null() {
757 first_inserted = inserted;
758 }
759 last_inserted = inserted;
760 }
761 }
762 }
763
764 // Recursively process the inserted fallback content for nested includes.
765 if !first_inserted.is_null() {
766 let mut cur = first_inserted;
767 loop {
768 unsafe {
769 let _ = process_node_tree(cur, doc, visited);
770 }
771 if cur == last_inserted {
772 break;
773 }
774 cur = unsafe { (*cur).next };
775 if cur.is_null() {
776 break;
777 }
778 }
779 }
780
781 // Remove the include node.
782 unsafe { remove_node(include_node) };
783
784 1
785}
786
787/// Find the `<xi:fallback>` child of an element.
788///
789/// # SAFETY
790///
791/// `node` must be a valid pointer or NULL.
792unsafe fn find_fallback_child(node: *mut _xmlNode) -> *mut _xmlNode {
793 if node.is_null() {
794 return ptr::null_mut();
795 }
796
797 let mut child = unsafe { (*node).children };
798 while !child.is_null() {
799 if unsafe { is_fallback_element(child) } {
800 return child;
801 }
802 child = unsafe { (*child).next };
803 }
804
805 ptr::null_mut()
806}
807
808/// Replace a node with new content (insert content in its place and remove the node).
809///
810/// # SAFETY
811///
812/// All pointers must be valid or NULL.
813unsafe fn replace_node_with_content(
814 old_node: *mut _xmlNode,
815 new_content: *mut _xmlNode,
816 _doc: *mut _xmlDoc,
817) {
818 if old_node.is_null() || new_content.is_null() {
819 return;
820 }
821
822 let parent = unsafe { (*old_node).parent };
823 if parent.is_null() {
824 // Old node is a direct child of the document.
825 // Add new content as a sibling after old_node, then remove old_node.
826 unsafe {
827 tree::add_sibling(old_node, new_content);
828 tree::unlink_node(old_node);
829 tree::free_node(old_node);
830 }
831 return;
832 }
833
834 // Insert new content before the old node.
835 unsafe {
836 tree::add_sibling_before(old_node, new_content);
837 tree::unlink_node(old_node);
838 tree::free_node(old_node);
839 }
840}
841
842/// Remove a node from the tree and free it.
843///
844/// # SAFETY
845///
846/// `node` must be a valid pointer or NULL.
847unsafe fn remove_node(node: *mut _xmlNode) {
848 if node.is_null() {
849 return;
850 }
851 unsafe {
852 tree::unlink_node(node);
853 tree::free_node(node);
854 }
855}
856
857/// Read a file from disk into memory.
858///
859/// Returns a null-terminated xmlChar string, or NULL on failure.
860///
861/// # SAFETY
862///
863/// `filename` must be a valid null-terminated xmlChar string or NULL.
864unsafe fn io_read_file(filename: *const xmlChar) -> *mut xmlChar {
865 if filename.is_null() {
866 return ptr::null_mut();
867 }
868
869 // Convert xmlChar* to C string for IO functions.
870 let c_filename = match std::ffi::CString::new(unsafe { xmlstr_to_bytes(filename) }) {
871 Ok(s) => s,
872 Err(_) => return ptr::null_mut(),
873 };
874
875 // UPSTREAM-PARITY (xmlIO.c xmlParserInputBufferCreateFilename + the
876 // consumer's xmlParserInputBufferCreateFilenameDefault hook): a registered
877 // create-filename loader (php's VCWD-aware stream loader) is consulted
878 // FIRST. This is REQUIRED under ZTS php, where chdir() is virtualized
879 // per-thread and a raw relative libc open resolves against the process's
880 // real (startup) directory — the Phase-14.26 ZTS gate lost
881 // xsltLoadDocument's doXInclude (xinclude/xinclude.phpt) exactly there:
882 // xincluded.xml opened relative to php-src instead of the test dir. The
883 // slot is read through the R-000177 cross-DSO bridge so the whole-archive
884 // libxslt facade's private core copy observes the loader php installed
885 // through the core DSO's exported setter.
886 if let Ok(data) = crate::abi::exports_parser::call_loader_materialize(c_filename.as_ptr()) {
887 if data.is_empty() {
888 return ptr::null_mut();
889 }
890 let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
891 if result.is_null() {
892 return ptr::null_mut();
893 }
894 unsafe {
895 ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
896 *result.add(data.len()) = 0; // null-terminate
897 }
898 return result;
899 }
900
901 // UPSTREAM-PARITY (xmlIO.c xmlParserInputBufferCreateFilename): a URI
902 // accepted by a registered input callback pair (xmlRegisterInputCallbacks)
903 // is read through that pair instead of the file path — XInclude hrefs
904 // like "sql:..." (io1.c) route here (Phase-12 EXTERNAL-CONSUMERS court).
905 if let Some(data) =
906 crate::abi::exports_parser::read_uri_via_input_callbacks(c_filename.as_ptr())
907 {
908 if data.is_empty() {
909 return ptr::null_mut();
910 }
911 let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
912 if result.is_null() {
913 return ptr::null_mut();
914 }
915 unsafe {
916 ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
917 *result.add(data.len()) = 0; // null-terminate
918 }
919 return result;
920 }
921
922 let fd = unsafe { libc::open(c_filename.as_ptr(), libc::O_RDONLY) };
923 if fd < 0 {
924 return ptr::null_mut();
925 }
926
927 let mut data = Vec::new();
928 let mut buf = [0u8; 4096];
929
930 loop {
931 let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut c_void, buf.len()) };
932 if ret < 0 {
933 unsafe { libc::close(fd) };
934 return ptr::null_mut();
935 }
936 if ret == 0 {
937 break;
938 }
939 data.extend_from_slice(&buf[..ret as usize]);
940 }
941
942 unsafe { libc::close(fd) };
943
944 if data.is_empty() {
945 return ptr::null_mut();
946 }
947
948 // Allocate via xmlMalloc and copy with null terminator.
949 let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
950 if result.is_null() {
951 return ptr::null_mut();
952 }
953
954 unsafe {
955 ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
956 *result.add(data.len()) = 0; // null-terminate
957 }
958
959 result
960}
961
962/// Parse an XML document from a file.
963///
964/// Returns a pointer to the parsed document, or NULL on failure.
965///
966/// # SAFETY
967///
968/// `filename` must be a valid null-terminated xmlChar string or NULL.
969unsafe fn parse_xml_document(filename: *const xmlChar) -> *mut _xmlDoc {
970 if filename.is_null() {
971 return ptr::null_mut();
972 }
973
974 // Read the file content.
975 let content = unsafe { io_read_file(filename) };
976 if content.is_null() {
977 return ptr::null_mut();
978 }
979
980 let content_bytes = unsafe { xmlstr_to_bytes(content) };
981 let size = content_bytes.len() as c_int;
982
983 // Parse the content as XML.
984 let doc = unsafe {
985 crate::abi::exports_xml2::xmlReadMemory(
986 content as *const c_char,
987 size,
988 filename as *const c_char,
989 ptr::null(), // encoding
990 0, // options
991 )
992 };
993
994 allocator::xmlFreeImpl(content as *mut c_void);
995
996 doc
997}
998
999/// Find the root element of a document.
1000///
1001/// # SAFETY
1002///
1003/// `doc` must be a valid pointer or NULL.
1004unsafe fn find_root_element(doc: *mut _xmlDoc) -> *mut _xmlNode {
1005 if doc.is_null() {
1006 return ptr::null_mut();
1007 }
1008
1009 let mut child = unsafe { (*doc).children };
1010 while !child.is_null() {
1011 let node_type = unsafe { (*child).type_ };
1012 if node_type == XML_ELEMENT_NODE as c_int {
1013 return child;
1014 }
1015 child = unsafe { (*child).next };
1016 }
1017
1018 ptr::null_mut()
1019}
1020
1021/// Set the document pointer on a node and all its descendants.
1022///
1023/// # SAFETY
1024///
1025/// `node` must be a valid pointer or NULL.
1026/// `doc` must be a valid pointer to an _xmlDoc or NULL.
1027unsafe fn set_doc_recursive(node: *mut _xmlNode, doc: *mut _xmlDoc) {
1028 if node.is_null() {
1029 return;
1030 }
1031
1032 unsafe {
1033 (*node).doc = doc;
1034 }
1035
1036 // Set doc on all children.
1037 let mut child = unsafe { (*node).children };
1038 while !child.is_null() {
1039 unsafe { set_doc_recursive(child, doc) };
1040 child = unsafe { (*child).next };
1041 }
1042
1043 // Set doc on properties.
1044 let mut prop = unsafe { (*node).properties };
1045 while !prop.is_null() {
1046 unsafe {
1047 (*prop).doc = doc;
1048 if !(*prop).children.is_null() {
1049 set_doc_recursive((*prop).children, doc);
1050 }
1051 }
1052 prop = unsafe { (*prop).next };
1053 }
1054}
1055
1056/// Check if a node or any of its ancestors has a namespace declaration
1057/// with the given URI.
1058///
1059/// # SAFETY
1060///
1061/// `node` must be a valid pointer or NULL.
1062/// `ns_uri` must be a valid null-terminated xmlChar string.
1063unsafe fn check_namespace_declaration(node: *mut _xmlNode, ns_uri: *const xmlChar) -> bool {
1064 if node.is_null() {
1065 return false;
1066 }
1067
1068 let mut cur: *mut _xmlNode = node;
1069 while !cur.is_null() {
1070 let n = unsafe { &*cur };
1071 let mut ns_def = n.nsDef;
1072 while !ns_def.is_null() {
1073 let ns = unsafe { &*ns_def };
1074 if !ns.href.is_null() && unsafe { xml_str_equal(ns.href, ns_uri) } {
1075 return true;
1076 }
1077 ns_def = ns.next;
1078 }
1079 cur = n.parent;
1080 }
1081
1082 false
1083}
1084
1085/// Compare two null-terminated xmlChar strings for equality.
1086///
1087/// # SAFETY
1088///
1089/// Both strings must be null-terminated or NULL.
1090unsafe fn xml_str_equal(a: *const xmlChar, b: *const xmlChar) -> bool {
1091 if a.is_null() && b.is_null() {
1092 return true;
1093 }
1094 if a.is_null() || b.is_null() {
1095 return false;
1096 }
1097 unsafe { crate::abi::exports_xml2::xmlStrEqual(a, b) != 0 }
1098}
1099
1100/// Whether `href` is one of the XInclude namespace URIs — the 2003
1101/// namespace (upstream `XINCLUDE_NS`) or the 2001 draft (upstream
1102/// `XINCLUDE_OLD_NS`, honored like xinclude.c).
1103///
1104/// # SAFETY
1105///
1106/// `href` must be a valid null-terminated xmlChar string.
1107unsafe fn is_xinclude_ns_uri(href: *const xmlChar) -> bool {
1108 unsafe {
1109 xml_str_equal(href, XINCLUDE_NS.as_ptr() as *const xmlChar)
1110 || xml_str_equal(href, XINCLUDE_OLD_NS.as_ptr() as *const xmlChar)
1111 }
1112}
1113
1114// ═══════════════════════════════════════════════════════════════════════════════
1115// Tests
1116// ═══════════════════════════════════════════════════════════════════════════════
1117
1118#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use crate::abi::allocator;
1122
1123 use crate::xml::tree;
1124 use std::os::raw::{c_char, c_int};
1125
1126 // ═══════════════════════════════════════════════════════════════════════════
1127 // Test helpers
1128 // ═══════════════════════════════════════════════════════════════════════════
1129
1130 #[allow(dead_code)]
1131 /// Create a simple XML document from a string.
1132 ///
1133 /// # Safety
1134 ///
1135 /// - `xml` must be a byte slice that stays valid for the duration of the
1136 /// call; its pointer and length are passed to `xmlReadMemory`, which
1137 /// parses the bytes into a new document. The returned document pointer
1138 /// is NULL on failure and otherwise must be released by the caller with
1139 /// `tree::free_doc`.
1140 unsafe fn create_doc_from_xml(xml: &[u8]) -> *mut _xmlDoc {
1141 let doc = unsafe {
1142 crate::abi::exports_xml2::xmlReadMemory(
1143 xml.as_ptr() as *const c_char,
1144 xml.len() as c_int,
1145 ptr::null(),
1146 ptr::null(),
1147 0,
1148 )
1149 };
1150 if doc.is_null() {
1151 return ptr::null_mut();
1152 }
1153 doc
1154 }
1155
1156 /// Create a simple document with one root element.
1157 unsafe fn create_simple_doc() -> *mut _xmlDoc {
1158 let doc = tree::new_doc(ptr::null());
1159 assert!(!doc.is_null(), "Failed to create doc");
1160
1161 let root = tree::new_child(
1162 doc as *mut _xmlNode,
1163 ptr::null_mut(),
1164 c"root".as_ptr() as *const xmlChar,
1165 );
1166 assert!(!root.is_null(), "Failed to create root");
1167
1168 doc
1169 }
1170
1171 /// Create a namespace on a node.
1172 unsafe fn create_ns(
1173 node: *mut _xmlNode,
1174 prefix: *const xmlChar,
1175 href: *const xmlChar,
1176 ) -> *mut _xmlNs {
1177 tree::new_ns(node, href, prefix)
1178 }
1179
1180 /// Create a doc with a root and an XInclude namespace.
1181 unsafe fn create_doc_with_xinclude_ns() -> (*mut _xmlDoc, *mut _xmlNode) {
1182 let doc = tree::new_doc(ptr::null());
1183 assert!(!doc.is_null());
1184 let root = tree::new_child(
1185 doc as *mut _xmlNode,
1186 ptr::null_mut(),
1187 c"root".as_ptr() as *const xmlChar,
1188 );
1189 assert!(!root.is_null());
1190 create_ns(
1191 root,
1192 c"xi".as_ptr() as *const xmlChar,
1193 XINCLUDE_NS.as_ptr() as *const xmlChar,
1194 );
1195 (doc, root)
1196 }
1197
1198 /// Create an xi:include child element with optional attributes.
1199 unsafe fn create_include_child(
1200 parent: *mut _xmlNode,
1201 href: Option<&[u8]>,
1202 parse: Option<&[u8]>,
1203 ) -> *mut _xmlNode {
1204 let ns = create_ns(
1205 parent,
1206 c"xi".as_ptr() as *const xmlChar,
1207 XINCLUDE_NS.as_ptr() as *const xmlChar,
1208 );
1209 let elem = tree::new_child(parent, ns, c"include".as_ptr() as *const xmlChar);
1210 if let Some(h) = href {
1211 let h_str = crate::xml::string::bytes_to_xmlstr(h);
1212 tree::set_prop(elem, ATTR_HREF.as_ptr() as *const xmlChar, h_str);
1213 allocator::xmlFreeImpl(h_str as *mut c_void);
1214 }
1215 if let Some(p) = parse {
1216 let p_str = crate::xml::string::bytes_to_xmlstr(p);
1217 tree::set_prop(elem, ATTR_PARSE.as_ptr() as *const xmlChar, p_str);
1218 allocator::xmlFreeImpl(p_str as *mut c_void);
1219 }
1220 elem
1221 }
1222
1223 /// Create an xi:fallback child element.
1224 unsafe fn create_fallback_child(parent: *mut _xmlNode) -> *mut _xmlNode {
1225 let ns = create_ns(
1226 parent,
1227 c"xi".as_ptr() as *const xmlChar,
1228 XINCLUDE_NS.as_ptr() as *const xmlChar,
1229 );
1230 tree::new_child(parent, ns, c"fallback".as_ptr() as *const xmlChar)
1231 }
1232 #[allow(dead_code)]
1233 /// Find the first element by name in the document.
1234 ///
1235 /// # Safety
1236 ///
1237 /// - `doc` must be NULL or a pointer to a valid, live `_xmlDoc`; `name`
1238 /// must be a NUL-terminated `xmlChar` string that stays readable for the
1239 /// whole search. The walk dereferences `(*doc).children` and follows the
1240 /// `(*child).next` links, all of which must belong to the live document.
1241 unsafe fn find_element(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
1242 if doc.is_null() {
1243 return ptr::null_mut();
1244 }
1245 let mut child = unsafe { (*doc).children };
1246 while !child.is_null() {
1247 let result = unsafe { find_element_recursive(child, name) };
1248 if !result.is_null() {
1249 return result;
1250 }
1251 child = unsafe { (*child).next };
1252 }
1253 ptr::null_mut()
1254 }
1255
1256 #[allow(dead_code)]
1257 /// Find the first element by name, searching a node subtree.
1258 ///
1259 /// # Safety
1260 ///
1261 /// - `node` must be NULL or a pointer to a valid, live `_xmlNode` whose
1262 /// `children` and `next` links form the subtree to search; `name` must
1263 /// be a NUL-terminated `xmlChar` string readable for the duration of the
1264 /// call. `n.name` is only compared when non-NULL.
1265 unsafe fn find_element_recursive(node: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlNode {
1266 if node.is_null() {
1267 return ptr::null_mut();
1268 }
1269 let n = unsafe { &*node };
1270 if n.type_ == XML_ELEMENT_NODE as c_int
1271 && !n.name.is_null()
1272 && unsafe { xml_str_equal(n.name, name) }
1273 {
1274 return node;
1275 }
1276 let mut child = n.children;
1277 while !child.is_null() {
1278 let result = unsafe { find_element_recursive(child, name) };
1279 if !result.is_null() {
1280 return result;
1281 }
1282 child = unsafe { (*child).next };
1283 }
1284 ptr::null_mut()
1285 }
1286
1287 /// Count elements with a given name in the document.
1288 ///
1289 /// # Safety
1290 ///
1291 /// - `doc` must be NULL or a pointer to a valid, live `_xmlDoc`; `name`
1292 /// must be a NUL-terminated `xmlChar` string readable for the duration
1293 /// of the walk, which follows `(*doc).children` and `(*child).next`
1294 /// links inside the live document.
1295 unsafe fn count_elements(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
1296 if doc.is_null() {
1297 return 0;
1298 }
1299 let mut count: c_int = 0;
1300 let mut child = unsafe { (*doc).children };
1301 while !child.is_null() {
1302 count += unsafe { count_elements_recursive(child, name) };
1303 child = unsafe { (*child).next };
1304 }
1305 count
1306 }
1307
1308 /// Count elements with a given name in a node subtree.
1309 ///
1310 /// # Safety
1311 ///
1312 /// - `node` must be NULL or a pointer to a valid, live `_xmlNode` whose
1313 /// `children` and `next` links form the subtree to walk; `name` must be
1314 /// a NUL-terminated `xmlChar` string readable for the duration of the
1315 /// call. `n.name` is only compared when non-NULL.
1316 unsafe fn count_elements_recursive(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
1317 if node.is_null() {
1318 return 0;
1319 }
1320 let mut count: c_int = 0;
1321 let n = unsafe { &*node };
1322 if n.type_ == XML_ELEMENT_NODE as c_int
1323 && !n.name.is_null()
1324 && unsafe { xml_str_equal(n.name, name) }
1325 {
1326 count += 1;
1327 }
1328 let mut child = n.children;
1329 while !child.is_null() {
1330 count += unsafe { count_elements_recursive(child, name) };
1331 child = unsafe { (*child).next };
1332 }
1333 count
1334 }
1335
1336 // ═══════════════════════════════════════════════════════════════════════════
1337 // Tests
1338 // ═══════════════════════════════════════════════════════════════════════════
1339
1340 #[test]
1341 /// Tests that `is_xinclude_element` recognizes and rejects nodes.
1342 ///
1343 /// # Safety
1344 ///
1345 /// - `doc` is created by `create_simple_doc`, which asserts non-NULL for
1346 /// the document and its root element, and is freed with `tree::free_doc`;
1347 /// `(*doc).children` is dereferenced only after the non-NULL assertion,
1348 /// and `is_xinclude_element` handles a NULL node argument.
1349 fn test_is_xinclude_element() {
1350 unsafe {
1351 let doc = create_simple_doc();
1352 assert!(!doc.is_null());
1353 let root = (*doc).children;
1354 assert!(!root.is_null());
1355 assert!(!is_xinclude_element(root));
1356 assert!(!is_xinclude_element(ptr::null_mut()));
1357 tree::free_doc(doc);
1358 }
1359 }
1360
1361 #[test]
1362 /// Tests that an `xi:include` child is detected through its namespace.
1363 ///
1364 /// # Safety
1365 ///
1366 /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; `include`
1367 /// and `regular` are `tree::new_child` results asserted non-NULL and are
1368 /// owned by `doc`, which is freed with `tree::free_doc` at the end;
1369 /// `is_xinclude_element` only reads node fields through valid pointers.
1370 fn test_xinclude_namespace_detection() {
1371 unsafe {
1372 let (doc, root) = create_doc_with_xinclude_ns();
1373 let include = create_include_child(root, Some(b"test.xml"), None);
1374 assert!(!include.is_null());
1375 assert!(is_xinclude_element(include), "Should detect xi:include");
1376
1377 // A regular child should not be detected as xinclude.
1378 let regular =
1379 tree::new_child(root, ptr::null_mut(), c"regular".as_ptr() as *const xmlChar);
1380 assert!(!regular.is_null());
1381 assert!(!is_xinclude_element(regular), "Regular elem not xinclude");
1382
1383 tree::free_doc(doc);
1384 }
1385 }
1386
1387 #[test]
1388 /// Tests that `find_fallback_child` locates an `xi:fallback` child.
1389 ///
1390 /// # Safety
1391 ///
1392 /// - `doc`, `root`, `include`, and `fallback` are produced by the test
1393 /// helpers and asserted non-NULL; `find_fallback_child` walks the node
1394 /// tree through valid child/sibling pointers, and the whole tree is
1395 /// freed with `tree::free_doc` before the test ends.
1396 fn test_find_fallback_child() {
1397 unsafe {
1398 let (doc, root) = create_doc_with_xinclude_ns();
1399 let include = create_include_child(root, None, None);
1400 assert!(!include.is_null());
1401 let fallback = create_fallback_child(include);
1402 assert!(!fallback.is_null());
1403
1404 let found = find_fallback_child(include);
1405 assert!(!found.is_null(), "Should find fallback child");
1406
1407 let no_fallback = find_fallback_child(root);
1408 assert!(no_fallback.is_null(), "Root should not have fallback");
1409
1410 tree::free_doc(doc);
1411 }
1412 }
1413
1414 #[test]
1415 /// Tests the NULL handling and equality behavior of `xml_str_equal`.
1416 ///
1417 /// # Safety
1418 ///
1419 /// - The `c"..."` literals are NUL-terminated `'static` byte buffers;
1420 /// `xml_str_equal` returns early when either argument is NULL and only
1421 /// calls `xmlStrEqual` when both pointers are non-NULL and point to
1422 /// readable NUL-terminated strings.
1423 fn test_xml_str_equal() {
1424 unsafe {
1425 assert!(xml_str_equal(
1426 c"hello".as_ptr() as *const xmlChar,
1427 c"hello".as_ptr() as *const xmlChar,
1428 ));
1429 assert!(!xml_str_equal(
1430 c"hello".as_ptr() as *const xmlChar,
1431 c"world".as_ptr() as *const xmlChar,
1432 ));
1433 assert!(!xml_str_equal(
1434 ptr::null(),
1435 c"hello".as_ptr() as *const xmlChar
1436 ));
1437 assert!(!xml_str_equal(
1438 c"hello".as_ptr() as *const xmlChar,
1439 ptr::null()
1440 ));
1441 assert!(xml_str_equal(ptr::null(), ptr::null()));
1442 }
1443 }
1444
1445 #[test]
1446 /// Tests that `xinclude_process` fails cleanly on a NULL document.
1447 ///
1448 /// # Safety
1449 ///
1450 /// - `xinclude_process` checks `doc` for NULL and returns
1451 /// `XINCLUDE_FAILURE` without dereferencing it.
1452 fn test_xinclude_process_null_doc() {
1453 unsafe {
1454 assert_eq!(xinclude_process(ptr::null_mut()), XINCLUDE_FAILURE);
1455 }
1456 }
1457
1458 #[test]
1459 /// Tests that a document without includes processes successfully.
1460 ///
1461 /// # Safety
1462 ///
1463 /// - `doc` is created by `create_simple_doc` and stays valid until
1464 /// `tree::free_doc`, so the internal tree walks of `xinclude_process`
1465 /// only touch live nodes.
1466 fn test_xinclude_process_no_includes() {
1467 unsafe {
1468 let doc = create_simple_doc();
1469 assert_eq!(xinclude_process(doc), 0);
1470 tree::free_doc(doc);
1471 }
1472 }
1473
1474 #[test]
1475 /// Tests that an include referencing a missing file is handled gracefully.
1476 ///
1477 /// # Safety
1478 ///
1479 /// - `doc` and `root` come from `create_doc_with_xinclude_ns`, and the
1480 /// include child is created by `create_include_child`; the document tree
1481 /// stays intact until `tree::free_doc`, so `xinclude_process` only
1482 /// dereferences live nodes.
1483 fn test_xinclude_process_with_includes() {
1484 unsafe {
1485 // Create doc with xi:include that references a nonexistent file.
1486 let (doc, root) = create_doc_with_xinclude_ns();
1487 create_include_child(root, Some(b"nonexistent.xml"), None);
1488 let result = xinclude_process(doc);
1489 assert!(result >= 0, "Should handle missing files: {}", result);
1490 tree::free_doc(doc);
1491 }
1492 }
1493
1494 #[test]
1495 /// Tests that fallback content is kept when the include target is missing.
1496 ///
1497 /// # Safety
1498 ///
1499 /// - All nodes are created by the tree helpers and belong to `doc`, which
1500 /// is freed with `tree::free_doc` at the end; `count_elements` and
1501 /// `xinclude_process` walk only live, linked nodes that were asserted
1502 /// non-NULL when created.
1503 fn test_xinclude_fallback_content() {
1504 unsafe {
1505 let (doc, root) = create_doc_with_xinclude_ns();
1506 let include = create_include_child(root, Some(b"nonexistent.xml"), None);
1507 let fb = create_fallback_child(include);
1508 // Add a child to fallback
1509 let fb_child = tree::new_child(
1510 fb,
1511 ptr::null_mut(),
1512 c"fallback-elem".as_ptr() as *const xmlChar,
1513 );
1514 assert!(!fb_child.is_null());
1515
1516 let before = count_elements(doc, c"fallback-elem".as_ptr() as *const xmlChar);
1517 assert!(before > 0, "Should have fallback-elem before processing");
1518
1519 let result = xinclude_process(doc);
1520 assert!(result >= 0, "Should handle fallback: {}", result);
1521 tree::free_doc(doc);
1522 }
1523 }
1524
1525 #[test]
1526 /// Tests that a self-referencing include does not crash the processor.
1527 ///
1528 /// # Safety
1529 ///
1530 /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; the include
1531 /// child is attached to `root` and owned by `doc`, which is freed after
1532 /// `xinclude_process` returns, so all pointer dereferences target live
1533 /// nodes.
1534 fn test_xinclude_circular_reference_detection() {
1535 unsafe {
1536 let (doc, root) = create_doc_with_xinclude_ns();
1537 create_include_child(root, Some(b"self-ref.xml"), None);
1538 let result = xinclude_process(doc);
1539 assert!(result >= 0, "Circular ref should not crash: {}", result);
1540 tree::free_doc(doc);
1541 }
1542 }
1543
1544 #[test]
1545 /// Tests that the `parse` attribute is stored and include children are
1546 /// counted.
1547 ///
1548 /// # Safety
1549 ///
1550 /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; the loop
1551 /// dereferences `(*root).children` and follows `(*child).next`, all of
1552 /// which are nodes owned by `doc` and freed with `tree::free_doc`.
1553 fn test_xinclude_parse_attribute_detection() {
1554 unsafe {
1555 let (doc, root) = create_doc_with_xinclude_ns();
1556 create_include_child(root, Some(b"test.xml"), Some(b"xml"));
1557 create_include_child(root, Some(b"test.txt"), Some(b"text"));
1558 create_include_child(root, Some(b"default.xml"), None);
1559
1560 // Count include elements by iterating children.
1561 let mut count = 0;
1562 let mut child = (*root).children;
1563 while !child.is_null() {
1564 if is_xinclude_element(child) {
1565 count += 1;
1566 }
1567 child = (*child).next;
1568 }
1569 assert_eq!(count, 3, "Should have 3 include elements");
1570 tree::free_doc(doc);
1571 }
1572 }
1573
1574 #[test]
1575 /// Tests both `xinclude_process` and `xinclude_process_flags`.
1576 ///
1577 /// # Safety
1578 ///
1579 /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1580 /// after both calls, so every dereference inside the processor touches a
1581 /// live document.
1582 fn test_xinclude_process_functions() {
1583 unsafe {
1584 let doc = create_simple_doc();
1585 let r1 = xinclude_process(doc);
1586 assert!(r1 >= 0);
1587 let r2 = xinclude_process_flags(doc, 0);
1588 assert!(r2 >= 0);
1589 tree::free_doc(doc);
1590 }
1591 }
1592
1593 #[test]
1594 /// Tests an include with no href and a fallback child.
1595 ///
1596 /// # Safety
1597 ///
1598 /// - `doc`, `root`, `include`, and the fallback child are built by the
1599 /// test helpers, asserted non-NULL where used, and owned by `doc`, which
1600 /// is freed with `tree::free_doc` after `xinclude_process`.
1601 fn test_xinclude_process_with_empty_href() {
1602 unsafe {
1603 let (doc, root) = create_doc_with_xinclude_ns();
1604 let include = create_include_child(root, None, None);
1605 create_fallback_child(include);
1606 let result = xinclude_process(doc);
1607 assert!(result >= 0, "Empty href with fallback: {}", result);
1608 tree::free_doc(doc);
1609 }
1610 }
1611
1612 #[test]
1613 /// Tests that `set_doc_recursive` assigns the document pointer to detached
1614 /// nodes.
1615 ///
1616 /// # Safety
1617 ///
1618 /// - `doc`, `parent`, and `detached` are `tree` module allocations
1619 /// asserted non-NULL; `set_doc_recursive` only writes the `(*node).doc`
1620 /// field of valid nodes, and the nodes are released with `tree::free_node`
1621 /// and `tree::free_doc` before the test ends.
1622 fn test_set_doc_recursive() {
1623 unsafe {
1624 let doc = tree::new_doc(ptr::null());
1625 assert!(!doc.is_null());
1626 let parent = tree::new_child(
1627 doc as *mut _xmlNode,
1628 ptr::null_mut(),
1629 c"parent".as_ptr() as *const xmlChar,
1630 );
1631 assert!(!parent.is_null());
1632 let detached = tree::new_node(ptr::null_mut(), c"detached".as_ptr() as *const xmlChar);
1633 assert!(!detached.is_null());
1634 assert!((*detached).doc.is_null());
1635 set_doc_recursive(detached, doc);
1636 assert_eq!((*detached).doc, doc);
1637 tree::free_node(detached);
1638 tree::free_doc(doc);
1639 }
1640 }
1641
1642 #[test]
1643 /// Tests that `find_root_element` returns the document root element.
1644 ///
1645 /// # Safety
1646 ///
1647 /// - `doc` is created by `create_simple_doc`; `root` is asserted non-NULL
1648 /// and dereferenced only while `doc` is alive, and `tree::free_doc`
1649 /// releases the whole tree at the end.
1650 fn test_find_root_element() {
1651 unsafe {
1652 let doc = create_simple_doc();
1653 let root = find_root_element(doc);
1654 assert!(!root.is_null());
1655 assert_eq!((*root).type_, XML_ELEMENT_NODE as c_int);
1656 tree::free_doc(doc);
1657 }
1658 }
1659
1660 #[test]
1661 /// Tests that the `xpointer` attribute round-trips through the tree.
1662 ///
1663 /// # Safety
1664 ///
1665 /// - `include` is created by `create_include_child` and owned by `doc`;
1666 /// `xptr_val` is a fresh `bytes_to_xmlstr` allocation freed right after
1667 /// `tree::set_prop` copies it; `xptr` from `tree::get_prop` is freed
1668 /// with `xmlFreeImpl` after `xmlstr_to_bytes` copies it, and `doc` is
1669 /// freed with `tree::free_doc`.
1670 fn test_xinclude_xpointer_attribute() {
1671 unsafe {
1672 let (doc, root) = create_doc_with_xinclude_ns();
1673 let include = create_include_child(root, Some(b"test.xml"), None);
1674 let xptr_val = crate::xml::string::bytes_to_xmlstr(b"xpointer(//target)");
1675 tree::set_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar, xptr_val);
1676 allocator::xmlFreeImpl(xptr_val as *mut c_void);
1677
1678 let xptr = tree::get_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar);
1679 assert!(!xptr.is_null(), "Should have xpointer attribute");
1680 assert_eq!(xmlstr_to_bytes(xptr), b"xpointer(//target)");
1681 allocator::xmlFreeImpl(xptr as *mut c_void);
1682
1683 tree::free_doc(doc);
1684 }
1685 }
1686
1687 #[test]
1688 /// Tests that the `accept` and `accept-language` attributes round-trip.
1689 ///
1690 /// # Safety
1691 ///
1692 /// - `include` is owned by `doc`; the attribute values from
1693 /// `bytes_to_xmlstr` are freed right after `tree::set_prop` copies them,
1694 /// and the values returned by `tree::get_prop` are freed with
1695 /// `xmlFreeImpl` after use; `doc` is freed with `tree::free_doc`.
1696 fn test_xinclude_accept_attributes() {
1697 unsafe {
1698 let (doc, root) = create_doc_with_xinclude_ns();
1699 let include = create_include_child(root, Some(b"data.xml"), None);
1700
1701 let accept_val = crate::xml::string::bytes_to_xmlstr(b"application/xml");
1702 tree::set_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar, accept_val);
1703 allocator::xmlFreeImpl(accept_val as *mut c_void);
1704
1705 let lang_val = crate::xml::string::bytes_to_xmlstr(b"en");
1706 tree::set_prop(
1707 include,
1708 ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar,
1709 lang_val,
1710 );
1711 allocator::xmlFreeImpl(lang_val as *mut c_void);
1712
1713 let accept = tree::get_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar);
1714 assert!(!accept.is_null());
1715 assert_eq!(xmlstr_to_bytes(accept), b"application/xml");
1716 allocator::xmlFreeImpl(accept as *mut c_void);
1717
1718 let lang = tree::get_prop(include, ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar);
1719 assert!(!lang.is_null());
1720 assert_eq!(xmlstr_to_bytes(lang), b"en");
1721 allocator::xmlFreeImpl(lang as *mut c_void);
1722
1723 tree::free_doc(doc);
1724 }
1725 }
1726
1727 #[test]
1728 /// Tests that the `encoding` attribute round-trips through the tree.
1729 ///
1730 /// # Safety
1731 ///
1732 /// - `include` is owned by `doc`; `enc_val` is freed after `tree::set_prop`
1733 /// copies it, `encoding` from `tree::get_prop` is freed after use, and
1734 /// `doc` is freed with `tree::free_doc`.
1735 fn test_xinclude_encoding_attribute() {
1736 unsafe {
1737 let (doc, root) = create_doc_with_xinclude_ns();
1738 let include = create_include_child(root, Some(b"data.txt"), Some(b"text"));
1739
1740 let enc_val = crate::xml::string::bytes_to_xmlstr(b"UTF-8");
1741 tree::set_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar, enc_val);
1742 allocator::xmlFreeImpl(enc_val as *mut c_void);
1743
1744 let encoding = tree::get_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar);
1745 assert!(!encoding.is_null());
1746 assert_eq!(xmlstr_to_bytes(encoding), b"UTF-8");
1747 allocator::xmlFreeImpl(encoding as *mut c_void);
1748
1749 tree::free_doc(doc);
1750 }
1751 }
1752
1753 #[test]
1754 /// Tests that `xinclude_process` and `xinclude_process_flags` with zero
1755 /// flags behave identically.
1756 ///
1757 /// # Safety
1758 ///
1759 /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1760 /// after both calls, so the processor only dereferences live nodes.
1761 fn test_xinclude_process_flags_equivalence() {
1762 unsafe {
1763 let doc = create_simple_doc();
1764 let r1 = xinclude_process(doc);
1765 let r2 = xinclude_process_flags(doc, 0);
1766 assert_eq!(r1, r2);
1767 tree::free_doc(doc);
1768 }
1769 }
1770
1771 #[test]
1772 /// Tests `xinclude_process_flags` with the `XML_PARSE_NOXINCNODE` flag.
1773 ///
1774 /// # Safety
1775 ///
1776 /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1777 /// after the call, so all pointer dereferences target live nodes.
1778 fn test_xinclude_process_flags_noxincnode() {
1779 unsafe {
1780 let doc = create_simple_doc();
1781 let result = xinclude_process_flags(doc, XML_PARSE_NOXINCNODE);
1782 assert_eq!(result, 0);
1783 tree::free_doc(doc);
1784 }
1785 }
1786
1787 #[test]
1788 #[ignore = "pre-existing tree module cleanup bug with modified trees"]
1789 /// Tests processing a document with nested includes and fallbacks.
1790 ///
1791 /// # Safety
1792 ///
1793 /// - All nodes are created by the `tree` helpers, asserted non-NULL, and
1794 /// owned by `doc`; `xinclude_process` walks the tree only while `doc` is
1795 /// alive. The test is ignored because the tree cleanup path does not
1796 /// free modified trees, and it deliberately leaks `doc` (no `free_doc`),
1797 /// but every unsafe access targets nodes that remain live for the whole
1798 /// test.
1799 fn test_complex_nested_includes_structure() {
1800 unsafe {
1801 // Build a doc with a complex structure including xi:include elements.
1802 let doc = tree::new_doc(ptr::null());
1803 assert!(!doc.is_null());
1804 let root = tree::new_child(
1805 doc as *mut _xmlNode,
1806 ptr::null_mut(),
1807 c"root".as_ptr() as *const xmlChar,
1808 );
1809 assert!(!root.is_null());
1810 create_ns(
1811 root,
1812 c"xi".as_ptr() as *const xmlChar,
1813 XINCLUDE_NS.as_ptr() as *const xmlChar,
1814 );
1815
1816 create_include_child(root, Some(b"nonexistent1.xml"), None);
1817
1818 let inc2 = create_include_child(root, Some(b"nonexistent2.xml"), None);
1819 let fb2 = create_fallback_child(inc2);
1820 tree::new_child(
1821 fb2,
1822 ptr::null_mut(),
1823 c"fallback-content".as_ptr() as *const xmlChar,
1824 );
1825
1826 create_include_child(root, Some(b"nonexistent3.txt"), Some(b"text"));
1827
1828 let result = xinclude_process(doc);
1829 assert!(result >= 0, "Complex structure: {}", result);
1830 }
1831 }
1832
1833 #[test]
1834 /// Tests that `xinclude_process` leaves the document in a freeable state.
1835 ///
1836 /// # Safety
1837 ///
1838 /// - `doc` is created by `create_simple_doc`, processed while alive, and
1839 /// freed with `tree::free_doc` at the end, so all dereferences target
1840 /// live nodes.
1841 fn test_xinclude_process_xml_memory_cleanup() {
1842 unsafe {
1843 let doc = create_simple_doc();
1844 assert!(xinclude_process(doc) >= 0);
1845 tree::free_doc(doc);
1846 }
1847 }
1848
1849 #[test]
1850 /// Tests that `mark_doc_xinclude_processed` sets the XInclude flag.
1851 ///
1852 /// # Safety
1853 ///
1854 /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`;
1855 /// the `(*doc).properties` field is read and written only while `doc` is
1856 /// alive.
1857 fn test_mark_doc_xinclude_processed() {
1858 unsafe {
1859 let doc = create_simple_doc();
1860 assert_eq!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1861 mark_doc_xinclude_processed(doc);
1862 assert_ne!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1863 tree::free_doc(doc);
1864 }
1865 }
1866
1867 #[test]
1868 /// Tests that `process_node_tree` skips XInclude start/end sentinel nodes.
1869 ///
1870 /// # Safety
1871 ///
1872 /// - `doc` and `root` are created by the helpers and stay alive until the
1873 /// end of the test; the sentinel node is allocated by `tree::new_node`,
1874 /// manually linked into `root`'s sibling list, and unlinked again before
1875 /// `tree::free_node`; every dereference during linking, the
1876 /// `process_node_tree` walk, and unlinking touches nodes that are still
1877 /// allocated.
1878 fn test_xinclude_xinclude_start_end_nodes() {
1879 unsafe {
1880 let doc = create_simple_doc();
1881 let root = find_root_element(doc);
1882 assert!(!root.is_null());
1883
1884 // Create a sentinel XML_XINCLUDE_START node attached to root.
1885 let sentinel =
1886 tree::new_node(ptr::null_mut(), c"XIncludeStart".as_ptr() as *const xmlChar);
1887 assert!(!sentinel.is_null());
1888 (*sentinel).type_ = XML_XINCLUDE_START as c_int;
1889 (*sentinel).doc = doc;
1890 // Link as next sibling of root's children (simple linking).
1891 let first_child = (*root).children;
1892 if !first_child.is_null() {
1893 // Insert sentinel after first child
1894 (*sentinel).parent = root;
1895 (*sentinel).prev = first_child;
1896 (*sentinel).next = (*first_child).next;
1897 if !(*first_child).next.is_null() {
1898 (*(*first_child).next).prev = sentinel;
1899 }
1900 (*first_child).next = sentinel;
1901 if (*root).last == first_child {
1902 (*root).last = sentinel;
1903 }
1904 }
1905
1906 let mut visited = Vec::new();
1907 let count = { process_node_tree(root, doc, &mut visited) };
1908 assert_eq!(count, 0, "Should not process sentinel nodes");
1909
1910 // Unlink sentinel before freeing
1911 if !(*sentinel).prev.is_null() {
1912 (*(*sentinel).prev).next = (*sentinel).next;
1913 }
1914 if !(*sentinel).next.is_null() {
1915 (*(*sentinel).next).prev = (*sentinel).prev;
1916 }
1917 (*sentinel).prev = ptr::null_mut();
1918 (*sentinel).next = ptr::null_mut();
1919 (*sentinel).parent = ptr::null_mut();
1920
1921 tree::free_node(sentinel);
1922 tree::free_doc(doc);
1923 }
1924 }
1925}