Skip to main content

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