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    /// Create a simple XML document from a string.
1025    #[allow(dead_code)]
1026    unsafe fn create_doc_from_xml(xml: &[u8]) -> *mut _xmlDoc {
1027        let doc = unsafe {
1028            crate::abi::exports_xml2::xmlReadMemory(
1029                xml.as_ptr() as *const c_char,
1030                xml.len() as c_int,
1031                ptr::null(),
1032                ptr::null(),
1033                0,
1034            )
1035        };
1036        if doc.is_null() {
1037            return ptr::null_mut();
1038        }
1039        doc
1040    }
1041
1042    /// Create a simple document with one root element.
1043    unsafe fn create_simple_doc() -> *mut _xmlDoc {
1044        let doc = tree::new_doc(ptr::null());
1045        assert!(!doc.is_null(), "Failed to create doc");
1046
1047        let root = tree::new_child(
1048            doc as *mut _xmlNode,
1049            ptr::null_mut(),
1050            c"root".as_ptr() as *const xmlChar,
1051        );
1052        assert!(!root.is_null(), "Failed to create root");
1053
1054        doc
1055    }
1056
1057    /// Create a namespace on a node.
1058    unsafe fn create_ns(
1059        node: *mut _xmlNode,
1060        prefix: *const xmlChar,
1061        href: *const xmlChar,
1062    ) -> *mut _xmlNs {
1063        tree::new_ns(node, href, prefix)
1064    }
1065
1066    /// Create a doc with a root and an XInclude namespace.
1067    unsafe fn create_doc_with_xinclude_ns() -> (*mut _xmlDoc, *mut _xmlNode) {
1068        let doc = tree::new_doc(ptr::null());
1069        assert!(!doc.is_null());
1070        let root = tree::new_child(
1071            doc as *mut _xmlNode,
1072            ptr::null_mut(),
1073            c"root".as_ptr() as *const xmlChar,
1074        );
1075        assert!(!root.is_null());
1076        create_ns(
1077            root,
1078            c"xi".as_ptr() as *const xmlChar,
1079            XINCLUDE_NS.as_ptr() as *const xmlChar,
1080        );
1081        (doc, root)
1082    }
1083
1084    /// Create an xi:include child element with optional attributes.
1085    unsafe fn create_include_child(
1086        parent: *mut _xmlNode,
1087        href: Option<&[u8]>,
1088        parse: Option<&[u8]>,
1089    ) -> *mut _xmlNode {
1090        let ns = create_ns(
1091            parent,
1092            c"xi".as_ptr() as *const xmlChar,
1093            XINCLUDE_NS.as_ptr() as *const xmlChar,
1094        );
1095        let elem = tree::new_child(parent, ns, c"include".as_ptr() as *const xmlChar);
1096        if let Some(h) = href {
1097            let h_str = crate::xml::string::bytes_to_xmlstr(h);
1098            tree::set_prop(elem, ATTR_HREF.as_ptr() as *const xmlChar, h_str);
1099            allocator::xmlFreeImpl(h_str as *mut c_void);
1100        }
1101        if let Some(p) = parse {
1102            let p_str = crate::xml::string::bytes_to_xmlstr(p);
1103            tree::set_prop(elem, ATTR_PARSE.as_ptr() as *const xmlChar, p_str);
1104            allocator::xmlFreeImpl(p_str as *mut c_void);
1105        }
1106        elem
1107    }
1108
1109    /// Create an xi:fallback child element.
1110    unsafe fn create_fallback_child(parent: *mut _xmlNode) -> *mut _xmlNode {
1111        let ns = create_ns(
1112            parent,
1113            c"xi".as_ptr() as *const xmlChar,
1114            XINCLUDE_NS.as_ptr() as *const xmlChar,
1115        );
1116        tree::new_child(parent, ns, c"fallback".as_ptr() as *const xmlChar)
1117    }
1118    #[allow(dead_code)]
1119    /// Find the first element by name in the document.
1120    unsafe fn find_element(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
1121        if doc.is_null() {
1122            return ptr::null_mut();
1123        }
1124        let mut child = unsafe { (*doc).children };
1125        while !child.is_null() {
1126            let result = unsafe { find_element_recursive(child, name) };
1127            if !result.is_null() {
1128                return result;
1129            }
1130            child = unsafe { (*child).next };
1131        }
1132        ptr::null_mut()
1133    }
1134
1135    #[allow(dead_code)]
1136    unsafe fn find_element_recursive(node: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlNode {
1137        if node.is_null() {
1138            return ptr::null_mut();
1139        }
1140        let n = unsafe { &*node };
1141        if n.type_ == XML_ELEMENT_NODE as c_int
1142            && !n.name.is_null()
1143            && unsafe { xml_str_equal(n.name, name) }
1144        {
1145            return node;
1146        }
1147        let mut child = n.children;
1148        while !child.is_null() {
1149            let result = unsafe { find_element_recursive(child, name) };
1150            if !result.is_null() {
1151                return result;
1152            }
1153            child = unsafe { (*child).next };
1154        }
1155        ptr::null_mut()
1156    }
1157
1158    /// Count elements with a given name in the document.
1159    unsafe fn count_elements(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
1160        if doc.is_null() {
1161            return 0;
1162        }
1163        let mut count: c_int = 0;
1164        let mut child = unsafe { (*doc).children };
1165        while !child.is_null() {
1166            count += unsafe { count_elements_recursive(child, name) };
1167            child = unsafe { (*child).next };
1168        }
1169        count
1170    }
1171
1172    unsafe fn count_elements_recursive(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
1173        if node.is_null() {
1174            return 0;
1175        }
1176        let mut count: c_int = 0;
1177        let n = unsafe { &*node };
1178        if n.type_ == XML_ELEMENT_NODE as c_int
1179            && !n.name.is_null()
1180            && unsafe { xml_str_equal(n.name, name) }
1181        {
1182            count += 1;
1183        }
1184        let mut child = n.children;
1185        while !child.is_null() {
1186            count += unsafe { count_elements_recursive(child, name) };
1187            child = unsafe { (*child).next };
1188        }
1189        count
1190    }
1191
1192    // ═══════════════════════════════════════════════════════════════════════════
1193    // Tests
1194    // ═══════════════════════════════════════════════════════════════════════════
1195
1196    #[test]
1197    fn test_is_xinclude_element() {
1198        unsafe {
1199            let doc = create_simple_doc();
1200            assert!(!doc.is_null());
1201            let root = (*doc).children;
1202            assert!(!root.is_null());
1203            assert!(!is_xinclude_element(root));
1204            assert!(!is_xinclude_element(ptr::null_mut()));
1205            tree::free_doc(doc);
1206        }
1207    }
1208
1209    #[test]
1210    fn test_xinclude_namespace_detection() {
1211        unsafe {
1212            let (doc, root) = create_doc_with_xinclude_ns();
1213            let include = create_include_child(root, Some(b"test.xml"), None);
1214            assert!(!include.is_null());
1215            assert!(is_xinclude_element(include), "Should detect xi:include");
1216
1217            // A regular child should not be detected as xinclude.
1218            let regular =
1219                tree::new_child(root, ptr::null_mut(), c"regular".as_ptr() as *const xmlChar);
1220            assert!(!regular.is_null());
1221            assert!(!is_xinclude_element(regular), "Regular elem not xinclude");
1222
1223            tree::free_doc(doc);
1224        }
1225    }
1226
1227    #[test]
1228    fn test_find_fallback_child() {
1229        unsafe {
1230            let (doc, root) = create_doc_with_xinclude_ns();
1231            let include = create_include_child(root, None, None);
1232            assert!(!include.is_null());
1233            let fallback = create_fallback_child(include);
1234            assert!(!fallback.is_null());
1235
1236            let found = find_fallback_child(include);
1237            assert!(!found.is_null(), "Should find fallback child");
1238
1239            let no_fallback = find_fallback_child(root);
1240            assert!(no_fallback.is_null(), "Root should not have fallback");
1241
1242            tree::free_doc(doc);
1243        }
1244    }
1245
1246    #[test]
1247    fn test_xml_str_equal() {
1248        unsafe {
1249            assert!(xml_str_equal(
1250                c"hello".as_ptr() as *const xmlChar,
1251                c"hello".as_ptr() as *const xmlChar,
1252            ));
1253            assert!(!xml_str_equal(
1254                c"hello".as_ptr() as *const xmlChar,
1255                c"world".as_ptr() as *const xmlChar,
1256            ));
1257            assert!(!xml_str_equal(
1258                ptr::null(),
1259                c"hello".as_ptr() as *const xmlChar
1260            ));
1261            assert!(!xml_str_equal(
1262                c"hello".as_ptr() as *const xmlChar,
1263                ptr::null()
1264            ));
1265            assert!(xml_str_equal(ptr::null(), ptr::null()));
1266        }
1267    }
1268
1269    #[test]
1270    fn test_xinclude_process_null_doc() {
1271        unsafe {
1272            assert_eq!(xinclude_process(ptr::null_mut()), XINCLUDE_FAILURE);
1273        }
1274    }
1275
1276    #[test]
1277    fn test_xinclude_process_no_includes() {
1278        unsafe {
1279            let doc = create_simple_doc();
1280            assert_eq!(xinclude_process(doc), 0);
1281            tree::free_doc(doc);
1282        }
1283    }
1284
1285    #[test]
1286    fn test_xinclude_process_with_includes() {
1287        unsafe {
1288            // Create doc with xi:include that references a nonexistent file.
1289            let (doc, root) = create_doc_with_xinclude_ns();
1290            create_include_child(root, Some(b"nonexistent.xml"), None);
1291            let result = xinclude_process(doc);
1292            assert!(result >= 0, "Should handle missing files: {}", result);
1293            tree::free_doc(doc);
1294        }
1295    }
1296
1297    #[test]
1298    fn test_xinclude_fallback_content() {
1299        unsafe {
1300            let (doc, root) = create_doc_with_xinclude_ns();
1301            let include = create_include_child(root, Some(b"nonexistent.xml"), None);
1302            let fb = create_fallback_child(include);
1303            // Add a child to fallback
1304            let fb_child = tree::new_child(
1305                fb,
1306                ptr::null_mut(),
1307                c"fallback-elem".as_ptr() as *const xmlChar,
1308            );
1309            assert!(!fb_child.is_null());
1310
1311            let before = count_elements(doc, c"fallback-elem".as_ptr() as *const xmlChar);
1312            assert!(before > 0, "Should have fallback-elem before processing");
1313
1314            let result = xinclude_process(doc);
1315            assert!(result >= 0, "Should handle fallback: {}", result);
1316            tree::free_doc(doc);
1317        }
1318    }
1319
1320    #[test]
1321    fn test_xinclude_circular_reference_detection() {
1322        unsafe {
1323            let (doc, root) = create_doc_with_xinclude_ns();
1324            create_include_child(root, Some(b"self-ref.xml"), None);
1325            let result = xinclude_process(doc);
1326            assert!(result >= 0, "Circular ref should not crash: {}", result);
1327            tree::free_doc(doc);
1328        }
1329    }
1330
1331    #[test]
1332    fn test_xinclude_parse_attribute_detection() {
1333        unsafe {
1334            let (doc, root) = create_doc_with_xinclude_ns();
1335            create_include_child(root, Some(b"test.xml"), Some(b"xml"));
1336            create_include_child(root, Some(b"test.txt"), Some(b"text"));
1337            create_include_child(root, Some(b"default.xml"), None);
1338
1339            // Count include elements by iterating children.
1340            let mut count = 0;
1341            let mut child = (*root).children;
1342            while !child.is_null() {
1343                if is_xinclude_element(child) {
1344                    count += 1;
1345                }
1346                child = (*child).next;
1347            }
1348            assert_eq!(count, 3, "Should have 3 include elements");
1349            tree::free_doc(doc);
1350        }
1351    }
1352
1353    #[test]
1354    fn test_xinclude_process_functions() {
1355        unsafe {
1356            let doc = create_simple_doc();
1357            let r1 = xinclude_process(doc);
1358            assert!(r1 >= 0);
1359            let r2 = xinclude_process_flags(doc, 0);
1360            assert!(r2 >= 0);
1361            tree::free_doc(doc);
1362        }
1363    }
1364
1365    #[test]
1366    fn test_xinclude_process_with_empty_href() {
1367        unsafe {
1368            let (doc, root) = create_doc_with_xinclude_ns();
1369            let include = create_include_child(root, None, None);
1370            create_fallback_child(include);
1371            let result = xinclude_process(doc);
1372            assert!(result >= 0, "Empty href with fallback: {}", result);
1373            tree::free_doc(doc);
1374        }
1375    }
1376
1377    #[test]
1378    fn test_set_doc_recursive() {
1379        unsafe {
1380            let doc = tree::new_doc(ptr::null());
1381            assert!(!doc.is_null());
1382            let parent = tree::new_child(
1383                doc as *mut _xmlNode,
1384                ptr::null_mut(),
1385                c"parent".as_ptr() as *const xmlChar,
1386            );
1387            assert!(!parent.is_null());
1388            let detached = tree::new_node(ptr::null_mut(), c"detached".as_ptr() as *const xmlChar);
1389            assert!(!detached.is_null());
1390            assert!((*detached).doc.is_null());
1391            set_doc_recursive(detached, doc);
1392            assert_eq!((*detached).doc, doc);
1393            tree::free_node(detached);
1394            tree::free_doc(doc);
1395        }
1396    }
1397
1398    #[test]
1399    fn test_find_root_element() {
1400        unsafe {
1401            let doc = create_simple_doc();
1402            let root = find_root_element(doc);
1403            assert!(!root.is_null());
1404            assert_eq!((*root).type_, XML_ELEMENT_NODE as c_int);
1405            tree::free_doc(doc);
1406        }
1407    }
1408
1409    #[test]
1410    fn test_xinclude_xpointer_attribute() {
1411        unsafe {
1412            let (doc, root) = create_doc_with_xinclude_ns();
1413            let include = create_include_child(root, Some(b"test.xml"), None);
1414            let xptr_val = crate::xml::string::bytes_to_xmlstr(b"xpointer(//target)");
1415            tree::set_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar, xptr_val);
1416            allocator::xmlFreeImpl(xptr_val as *mut c_void);
1417
1418            let xptr = tree::get_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar);
1419            assert!(!xptr.is_null(), "Should have xpointer attribute");
1420            assert_eq!(xmlstr_to_bytes(xptr), b"xpointer(//target)");
1421            allocator::xmlFreeImpl(xptr as *mut c_void);
1422
1423            tree::free_doc(doc);
1424        }
1425    }
1426
1427    #[test]
1428    fn test_xinclude_accept_attributes() {
1429        unsafe {
1430            let (doc, root) = create_doc_with_xinclude_ns();
1431            let include = create_include_child(root, Some(b"data.xml"), None);
1432
1433            let accept_val = crate::xml::string::bytes_to_xmlstr(b"application/xml");
1434            tree::set_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar, accept_val);
1435            allocator::xmlFreeImpl(accept_val as *mut c_void);
1436
1437            let lang_val = crate::xml::string::bytes_to_xmlstr(b"en");
1438            tree::set_prop(
1439                include,
1440                ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar,
1441                lang_val,
1442            );
1443            allocator::xmlFreeImpl(lang_val as *mut c_void);
1444
1445            let accept = tree::get_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar);
1446            assert!(!accept.is_null());
1447            assert_eq!(xmlstr_to_bytes(accept), b"application/xml");
1448            allocator::xmlFreeImpl(accept as *mut c_void);
1449
1450            let lang = tree::get_prop(include, ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar);
1451            assert!(!lang.is_null());
1452            assert_eq!(xmlstr_to_bytes(lang), b"en");
1453            allocator::xmlFreeImpl(lang as *mut c_void);
1454
1455            tree::free_doc(doc);
1456        }
1457    }
1458
1459    #[test]
1460    fn test_xinclude_encoding_attribute() {
1461        unsafe {
1462            let (doc, root) = create_doc_with_xinclude_ns();
1463            let include = create_include_child(root, Some(b"data.txt"), Some(b"text"));
1464
1465            let enc_val = crate::xml::string::bytes_to_xmlstr(b"UTF-8");
1466            tree::set_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar, enc_val);
1467            allocator::xmlFreeImpl(enc_val as *mut c_void);
1468
1469            let encoding = tree::get_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar);
1470            assert!(!encoding.is_null());
1471            assert_eq!(xmlstr_to_bytes(encoding), b"UTF-8");
1472            allocator::xmlFreeImpl(encoding as *mut c_void);
1473
1474            tree::free_doc(doc);
1475        }
1476    }
1477
1478    #[test]
1479    fn test_xinclude_process_flags_equivalence() {
1480        unsafe {
1481            let doc = create_simple_doc();
1482            let r1 = xinclude_process(doc);
1483            let r2 = xinclude_process_flags(doc, 0);
1484            assert_eq!(r1, r2);
1485            tree::free_doc(doc);
1486        }
1487    }
1488
1489    #[test]
1490    fn test_xinclude_process_flags_noxincnode() {
1491        unsafe {
1492            let doc = create_simple_doc();
1493            let result = xinclude_process_flags(doc, XML_PARSE_NOXINCNODE);
1494            assert_eq!(result, 0);
1495            tree::free_doc(doc);
1496        }
1497    }
1498
1499    #[test]
1500    #[ignore = "pre-existing tree module cleanup bug with modified trees"]
1501    fn test_complex_nested_includes_structure() {
1502        unsafe {
1503            // Build a doc with a complex structure including xi:include elements.
1504            let doc = tree::new_doc(ptr::null());
1505            assert!(!doc.is_null());
1506            let root = tree::new_child(
1507                doc as *mut _xmlNode,
1508                ptr::null_mut(),
1509                c"root".as_ptr() as *const xmlChar,
1510            );
1511            assert!(!root.is_null());
1512            create_ns(
1513                root,
1514                c"xi".as_ptr() as *const xmlChar,
1515                XINCLUDE_NS.as_ptr() as *const xmlChar,
1516            );
1517
1518            create_include_child(root, Some(b"nonexistent1.xml"), None);
1519
1520            let inc2 = create_include_child(root, Some(b"nonexistent2.xml"), None);
1521            let fb2 = create_fallback_child(inc2);
1522            tree::new_child(
1523                fb2,
1524                ptr::null_mut(),
1525                c"fallback-content".as_ptr() as *const xmlChar,
1526            );
1527
1528            create_include_child(root, Some(b"nonexistent3.txt"), Some(b"text"));
1529
1530            let result = xinclude_process(doc);
1531            assert!(result >= 0, "Complex structure: {}", result);
1532        }
1533    }
1534
1535    #[test]
1536    fn test_xinclude_process_xml_memory_cleanup() {
1537        unsafe {
1538            let doc = create_simple_doc();
1539            assert!(xinclude_process(doc) >= 0);
1540            tree::free_doc(doc);
1541        }
1542    }
1543
1544    #[test]
1545    fn test_mark_doc_xinclude_processed() {
1546        unsafe {
1547            let doc = create_simple_doc();
1548            assert_eq!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1549            mark_doc_xinclude_processed(doc);
1550            assert_ne!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1551            tree::free_doc(doc);
1552        }
1553    }
1554
1555    #[test]
1556    fn test_xinclude_xinclude_start_end_nodes() {
1557        unsafe {
1558            let doc = create_simple_doc();
1559            let root = find_root_element(doc);
1560            assert!(!root.is_null());
1561
1562            // Create a sentinel XML_XINCLUDE_START node attached to root.
1563            let sentinel =
1564                tree::new_node(ptr::null_mut(), c"XIncludeStart".as_ptr() as *const xmlChar);
1565            assert!(!sentinel.is_null());
1566            (*sentinel).type_ = XML_XINCLUDE_START as c_int;
1567            (*sentinel).doc = doc;
1568            // Link as next sibling of root's children (simple linking).
1569            let first_child = (*root).children;
1570            if !first_child.is_null() {
1571                // Insert sentinel after first child
1572                (*sentinel).parent = root;
1573                (*sentinel).prev = first_child;
1574                (*sentinel).next = (*first_child).next;
1575                if !(*first_child).next.is_null() {
1576                    (*(*first_child).next).prev = sentinel;
1577                }
1578                (*first_child).next = sentinel;
1579                if (*root).last == first_child {
1580                    (*root).last = sentinel;
1581                }
1582            }
1583
1584            let mut visited = Vec::new();
1585            let count = { process_node_tree(root, doc, &mut visited) };
1586            assert_eq!(count, 0, "Should not process sentinel nodes");
1587
1588            // Unlink sentinel before freeing
1589            if !(*sentinel).prev.is_null() {
1590                (*(*sentinel).prev).next = (*sentinel).next;
1591            }
1592            if !(*sentinel).next.is_null() {
1593                (*(*sentinel).next).prev = (*sentinel).prev;
1594            }
1595            (*sentinel).prev = ptr::null_mut();
1596            (*sentinel).next = ptr::null_mut();
1597            (*sentinel).parent = ptr::null_mut();
1598
1599            tree::free_node(sentinel);
1600            tree::free_doc(doc);
1601        }
1602    }
1603}