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