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