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