Skip to main content

libxml_rs/xml/xinclude/
mod.rs

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