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 + the
876    // consumer's xmlParserInputBufferCreateFilenameDefault hook): a registered
877    // create-filename loader (php's VCWD-aware stream loader) is consulted
878    // FIRST. This is REQUIRED under ZTS php, where chdir() is virtualized
879    // per-thread and a raw relative libc open resolves against the process's
880    // real (startup) directory — the Phase-14.26 ZTS gate lost
881    // xsltLoadDocument's doXInclude (xinclude/xinclude.phpt) exactly there:
882    // xincluded.xml opened relative to php-src instead of the test dir. The
883    // slot is read through the R-000177 cross-DSO bridge so the whole-archive
884    // libxslt facade's private core copy observes the loader php installed
885    // through the core DSO's exported setter.
886    if let Ok(data) = crate::abi::exports_parser::call_loader_materialize(c_filename.as_ptr()) {
887        if data.is_empty() {
888            return ptr::null_mut();
889        }
890        let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
891        if result.is_null() {
892            return ptr::null_mut();
893        }
894        unsafe {
895            ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
896            *result.add(data.len()) = 0; // null-terminate
897        }
898        return result;
899    }
900
901    // UPSTREAM-PARITY (xmlIO.c xmlParserInputBufferCreateFilename): a URI
902    // accepted by a registered input callback pair (xmlRegisterInputCallbacks)
903    // is read through that pair instead of the file path — XInclude hrefs
904    // like "sql:..." (io1.c) route here (Phase-12 EXTERNAL-CONSUMERS court).
905    if let Some(data) =
906        crate::abi::exports_parser::read_uri_via_input_callbacks(c_filename.as_ptr())
907    {
908        if data.is_empty() {
909            return ptr::null_mut();
910        }
911        let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
912        if result.is_null() {
913            return ptr::null_mut();
914        }
915        unsafe {
916            ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
917            *result.add(data.len()) = 0; // null-terminate
918        }
919        return result;
920    }
921
922    let fd = unsafe { libc::open(c_filename.as_ptr(), libc::O_RDONLY) };
923    if fd < 0 {
924        return ptr::null_mut();
925    }
926
927    let mut data = Vec::new();
928    let mut buf = [0u8; 4096];
929
930    loop {
931        let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut c_void, buf.len()) };
932        if ret < 0 {
933            unsafe { libc::close(fd) };
934            return ptr::null_mut();
935        }
936        if ret == 0 {
937            break;
938        }
939        data.extend_from_slice(&buf[..ret as usize]);
940    }
941
942    unsafe { libc::close(fd) };
943
944    if data.is_empty() {
945        return ptr::null_mut();
946    }
947
948    // Allocate via xmlMalloc and copy with null terminator.
949    let result = unsafe { allocator::xmlMallocImpl(data.len() + 1) as *mut xmlChar };
950    if result.is_null() {
951        return ptr::null_mut();
952    }
953
954    unsafe {
955        ptr::copy_nonoverlapping(data.as_ptr(), result, data.len());
956        *result.add(data.len()) = 0; // null-terminate
957    }
958
959    result
960}
961
962/// Parse an XML document from a file.
963///
964/// Returns a pointer to the parsed document, or NULL on failure.
965///
966/// # SAFETY
967///
968/// `filename` must be a valid null-terminated xmlChar string or NULL.
969unsafe fn parse_xml_document(filename: *const xmlChar) -> *mut _xmlDoc {
970    if filename.is_null() {
971        return ptr::null_mut();
972    }
973
974    // Read the file content.
975    let content = unsafe { io_read_file(filename) };
976    if content.is_null() {
977        return ptr::null_mut();
978    }
979
980    let content_bytes = unsafe { xmlstr_to_bytes(content) };
981    let size = content_bytes.len() as c_int;
982
983    // Parse the content as XML.
984    let doc = unsafe {
985        crate::abi::exports_xml2::xmlReadMemory(
986            content as *const c_char,
987            size,
988            filename as *const c_char,
989            ptr::null(), // encoding
990            0,           // options
991        )
992    };
993
994    allocator::xmlFreeImpl(content as *mut c_void);
995
996    doc
997}
998
999/// Find the root element of a document.
1000///
1001/// # SAFETY
1002///
1003/// `doc` must be a valid pointer or NULL.
1004unsafe fn find_root_element(doc: *mut _xmlDoc) -> *mut _xmlNode {
1005    if doc.is_null() {
1006        return ptr::null_mut();
1007    }
1008
1009    let mut child = unsafe { (*doc).children };
1010    while !child.is_null() {
1011        let node_type = unsafe { (*child).type_ };
1012        if node_type == XML_ELEMENT_NODE as c_int {
1013            return child;
1014        }
1015        child = unsafe { (*child).next };
1016    }
1017
1018    ptr::null_mut()
1019}
1020
1021/// Set the document pointer on a node and all its descendants.
1022///
1023/// # SAFETY
1024///
1025/// `node` must be a valid pointer or NULL.
1026/// `doc` must be a valid pointer to an _xmlDoc or NULL.
1027unsafe fn set_doc_recursive(node: *mut _xmlNode, doc: *mut _xmlDoc) {
1028    if node.is_null() {
1029        return;
1030    }
1031
1032    unsafe {
1033        (*node).doc = doc;
1034    }
1035
1036    // Set doc on all children.
1037    let mut child = unsafe { (*node).children };
1038    while !child.is_null() {
1039        unsafe { set_doc_recursive(child, doc) };
1040        child = unsafe { (*child).next };
1041    }
1042
1043    // Set doc on properties.
1044    let mut prop = unsafe { (*node).properties };
1045    while !prop.is_null() {
1046        unsafe {
1047            (*prop).doc = doc;
1048            if !(*prop).children.is_null() {
1049                set_doc_recursive((*prop).children, doc);
1050            }
1051        }
1052        prop = unsafe { (*prop).next };
1053    }
1054}
1055
1056/// Check if a node or any of its ancestors has a namespace declaration
1057/// with the given URI.
1058///
1059/// # SAFETY
1060///
1061/// `node` must be a valid pointer or NULL.
1062/// `ns_uri` must be a valid null-terminated xmlChar string.
1063unsafe fn check_namespace_declaration(node: *mut _xmlNode, ns_uri: *const xmlChar) -> bool {
1064    if node.is_null() {
1065        return false;
1066    }
1067
1068    let mut cur: *mut _xmlNode = node;
1069    while !cur.is_null() {
1070        let n = unsafe { &*cur };
1071        let mut ns_def = n.nsDef;
1072        while !ns_def.is_null() {
1073            let ns = unsafe { &*ns_def };
1074            if !ns.href.is_null() && unsafe { xml_str_equal(ns.href, ns_uri) } {
1075                return true;
1076            }
1077            ns_def = ns.next;
1078        }
1079        cur = n.parent;
1080    }
1081
1082    false
1083}
1084
1085/// Compare two null-terminated xmlChar strings for equality.
1086///
1087/// # SAFETY
1088///
1089/// Both strings must be null-terminated or NULL.
1090unsafe fn xml_str_equal(a: *const xmlChar, b: *const xmlChar) -> bool {
1091    if a.is_null() && b.is_null() {
1092        return true;
1093    }
1094    if a.is_null() || b.is_null() {
1095        return false;
1096    }
1097    unsafe { crate::abi::exports_xml2::xmlStrEqual(a, b) != 0 }
1098}
1099
1100/// Whether `href` is one of the XInclude namespace URIs — the 2003
1101/// namespace (upstream `XINCLUDE_NS`) or the 2001 draft (upstream
1102/// `XINCLUDE_OLD_NS`, honored like xinclude.c).
1103///
1104/// # SAFETY
1105///
1106/// `href` must be a valid null-terminated xmlChar string.
1107unsafe fn is_xinclude_ns_uri(href: *const xmlChar) -> bool {
1108    unsafe {
1109        xml_str_equal(href, XINCLUDE_NS.as_ptr() as *const xmlChar)
1110            || xml_str_equal(href, XINCLUDE_OLD_NS.as_ptr() as *const xmlChar)
1111    }
1112}
1113
1114// ═══════════════════════════════════════════════════════════════════════════════
1115// Tests
1116// ═══════════════════════════════════════════════════════════════════════════════
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::abi::allocator;
1122
1123    use crate::xml::tree;
1124    use std::os::raw::{c_char, c_int};
1125
1126    // ═══════════════════════════════════════════════════════════════════════════
1127    // Test helpers
1128    // ═══════════════════════════════════════════════════════════════════════════
1129
1130    #[allow(dead_code)]
1131    /// Create a simple XML document from a string.
1132    ///
1133    /// # Safety
1134    ///
1135    /// - `xml` must be a byte slice that stays valid for the duration of the
1136    ///   call; its pointer and length are passed to `xmlReadMemory`, which
1137    ///   parses the bytes into a new document. The returned document pointer
1138    ///   is NULL on failure and otherwise must be released by the caller with
1139    ///   `tree::free_doc`.
1140    unsafe fn create_doc_from_xml(xml: &[u8]) -> *mut _xmlDoc {
1141        let doc = unsafe {
1142            crate::abi::exports_xml2::xmlReadMemory(
1143                xml.as_ptr() as *const c_char,
1144                xml.len() as c_int,
1145                ptr::null(),
1146                ptr::null(),
1147                0,
1148            )
1149        };
1150        if doc.is_null() {
1151            return ptr::null_mut();
1152        }
1153        doc
1154    }
1155
1156    /// Create a simple document with one root element.
1157    unsafe fn create_simple_doc() -> *mut _xmlDoc {
1158        let doc = tree::new_doc(ptr::null());
1159        assert!(!doc.is_null(), "Failed to create doc");
1160
1161        let root = tree::new_child(
1162            doc as *mut _xmlNode,
1163            ptr::null_mut(),
1164            c"root".as_ptr() as *const xmlChar,
1165        );
1166        assert!(!root.is_null(), "Failed to create root");
1167
1168        doc
1169    }
1170
1171    /// Create a namespace on a node.
1172    unsafe fn create_ns(
1173        node: *mut _xmlNode,
1174        prefix: *const xmlChar,
1175        href: *const xmlChar,
1176    ) -> *mut _xmlNs {
1177        tree::new_ns(node, href, prefix)
1178    }
1179
1180    /// Create a doc with a root and an XInclude namespace.
1181    unsafe fn create_doc_with_xinclude_ns() -> (*mut _xmlDoc, *mut _xmlNode) {
1182        let doc = tree::new_doc(ptr::null());
1183        assert!(!doc.is_null());
1184        let root = tree::new_child(
1185            doc as *mut _xmlNode,
1186            ptr::null_mut(),
1187            c"root".as_ptr() as *const xmlChar,
1188        );
1189        assert!(!root.is_null());
1190        create_ns(
1191            root,
1192            c"xi".as_ptr() as *const xmlChar,
1193            XINCLUDE_NS.as_ptr() as *const xmlChar,
1194        );
1195        (doc, root)
1196    }
1197
1198    /// Create an xi:include child element with optional attributes.
1199    unsafe fn create_include_child(
1200        parent: *mut _xmlNode,
1201        href: Option<&[u8]>,
1202        parse: Option<&[u8]>,
1203    ) -> *mut _xmlNode {
1204        let ns = create_ns(
1205            parent,
1206            c"xi".as_ptr() as *const xmlChar,
1207            XINCLUDE_NS.as_ptr() as *const xmlChar,
1208        );
1209        let elem = tree::new_child(parent, ns, c"include".as_ptr() as *const xmlChar);
1210        if let Some(h) = href {
1211            let h_str = crate::xml::string::bytes_to_xmlstr(h);
1212            tree::set_prop(elem, ATTR_HREF.as_ptr() as *const xmlChar, h_str);
1213            allocator::xmlFreeImpl(h_str as *mut c_void);
1214        }
1215        if let Some(p) = parse {
1216            let p_str = crate::xml::string::bytes_to_xmlstr(p);
1217            tree::set_prop(elem, ATTR_PARSE.as_ptr() as *const xmlChar, p_str);
1218            allocator::xmlFreeImpl(p_str as *mut c_void);
1219        }
1220        elem
1221    }
1222
1223    /// Create an xi:fallback child element.
1224    unsafe fn create_fallback_child(parent: *mut _xmlNode) -> *mut _xmlNode {
1225        let ns = create_ns(
1226            parent,
1227            c"xi".as_ptr() as *const xmlChar,
1228            XINCLUDE_NS.as_ptr() as *const xmlChar,
1229        );
1230        tree::new_child(parent, ns, c"fallback".as_ptr() as *const xmlChar)
1231    }
1232    #[allow(dead_code)]
1233    /// Find the first element by name in the document.
1234    ///
1235    /// # Safety
1236    ///
1237    /// - `doc` must be NULL or a pointer to a valid, live `_xmlDoc`; `name`
1238    ///   must be a NUL-terminated `xmlChar` string that stays readable for the
1239    ///   whole search. The walk dereferences `(*doc).children` and follows the
1240    ///   `(*child).next` links, all of which must belong to the live document.
1241    unsafe fn find_element(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
1242        if doc.is_null() {
1243            return ptr::null_mut();
1244        }
1245        let mut child = unsafe { (*doc).children };
1246        while !child.is_null() {
1247            let result = unsafe { find_element_recursive(child, name) };
1248            if !result.is_null() {
1249                return result;
1250            }
1251            child = unsafe { (*child).next };
1252        }
1253        ptr::null_mut()
1254    }
1255
1256    #[allow(dead_code)]
1257    /// Find the first element by name, searching a node subtree.
1258    ///
1259    /// # Safety
1260    ///
1261    /// - `node` must be NULL or a pointer to a valid, live `_xmlNode` whose
1262    ///   `children` and `next` links form the subtree to search; `name` must
1263    ///   be a NUL-terminated `xmlChar` string readable for the duration of the
1264    ///   call. `n.name` is only compared when non-NULL.
1265    unsafe fn find_element_recursive(node: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlNode {
1266        if node.is_null() {
1267            return ptr::null_mut();
1268        }
1269        let n = unsafe { &*node };
1270        if n.type_ == XML_ELEMENT_NODE as c_int
1271            && !n.name.is_null()
1272            && unsafe { xml_str_equal(n.name, name) }
1273        {
1274            return node;
1275        }
1276        let mut child = n.children;
1277        while !child.is_null() {
1278            let result = unsafe { find_element_recursive(child, name) };
1279            if !result.is_null() {
1280                return result;
1281            }
1282            child = unsafe { (*child).next };
1283        }
1284        ptr::null_mut()
1285    }
1286
1287    /// Count elements with a given name in the document.
1288    ///
1289    /// # Safety
1290    ///
1291    /// - `doc` must be NULL or a pointer to a valid, live `_xmlDoc`; `name`
1292    ///   must be a NUL-terminated `xmlChar` string readable for the duration
1293    ///   of the walk, which follows `(*doc).children` and `(*child).next`
1294    ///   links inside the live document.
1295    unsafe fn count_elements(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
1296        if doc.is_null() {
1297            return 0;
1298        }
1299        let mut count: c_int = 0;
1300        let mut child = unsafe { (*doc).children };
1301        while !child.is_null() {
1302            count += unsafe { count_elements_recursive(child, name) };
1303            child = unsafe { (*child).next };
1304        }
1305        count
1306    }
1307
1308    /// Count elements with a given name in a node subtree.
1309    ///
1310    /// # Safety
1311    ///
1312    /// - `node` must be NULL or a pointer to a valid, live `_xmlNode` whose
1313    ///   `children` and `next` links form the subtree to walk; `name` must be
1314    ///   a NUL-terminated `xmlChar` string readable for the duration of the
1315    ///   call. `n.name` is only compared when non-NULL.
1316    unsafe fn count_elements_recursive(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
1317        if node.is_null() {
1318            return 0;
1319        }
1320        let mut count: c_int = 0;
1321        let n = unsafe { &*node };
1322        if n.type_ == XML_ELEMENT_NODE as c_int
1323            && !n.name.is_null()
1324            && unsafe { xml_str_equal(n.name, name) }
1325        {
1326            count += 1;
1327        }
1328        let mut child = n.children;
1329        while !child.is_null() {
1330            count += unsafe { count_elements_recursive(child, name) };
1331            child = unsafe { (*child).next };
1332        }
1333        count
1334    }
1335
1336    // ═══════════════════════════════════════════════════════════════════════════
1337    // Tests
1338    // ═══════════════════════════════════════════════════════════════════════════
1339
1340    #[test]
1341    /// Tests that `is_xinclude_element` recognizes and rejects nodes.
1342    ///
1343    /// # Safety
1344    ///
1345    /// - `doc` is created by `create_simple_doc`, which asserts non-NULL for
1346    ///   the document and its root element, and is freed with `tree::free_doc`;
1347    ///   `(*doc).children` is dereferenced only after the non-NULL assertion,
1348    ///   and `is_xinclude_element` handles a NULL node argument.
1349    fn test_is_xinclude_element() {
1350        unsafe {
1351            let doc = create_simple_doc();
1352            assert!(!doc.is_null());
1353            let root = (*doc).children;
1354            assert!(!root.is_null());
1355            assert!(!is_xinclude_element(root));
1356            assert!(!is_xinclude_element(ptr::null_mut()));
1357            tree::free_doc(doc);
1358        }
1359    }
1360
1361    #[test]
1362    /// Tests that an `xi:include` child is detected through its namespace.
1363    ///
1364    /// # Safety
1365    ///
1366    /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; `include`
1367    ///   and `regular` are `tree::new_child` results asserted non-NULL and are
1368    ///   owned by `doc`, which is freed with `tree::free_doc` at the end;
1369    ///   `is_xinclude_element` only reads node fields through valid pointers.
1370    fn test_xinclude_namespace_detection() {
1371        unsafe {
1372            let (doc, root) = create_doc_with_xinclude_ns();
1373            let include = create_include_child(root, Some(b"test.xml"), None);
1374            assert!(!include.is_null());
1375            assert!(is_xinclude_element(include), "Should detect xi:include");
1376
1377            // A regular child should not be detected as xinclude.
1378            let regular =
1379                tree::new_child(root, ptr::null_mut(), c"regular".as_ptr() as *const xmlChar);
1380            assert!(!regular.is_null());
1381            assert!(!is_xinclude_element(regular), "Regular elem not xinclude");
1382
1383            tree::free_doc(doc);
1384        }
1385    }
1386
1387    #[test]
1388    /// Tests that `find_fallback_child` locates an `xi:fallback` child.
1389    ///
1390    /// # Safety
1391    ///
1392    /// - `doc`, `root`, `include`, and `fallback` are produced by the test
1393    ///   helpers and asserted non-NULL; `find_fallback_child` walks the node
1394    ///   tree through valid child/sibling pointers, and the whole tree is
1395    ///   freed with `tree::free_doc` before the test ends.
1396    fn test_find_fallback_child() {
1397        unsafe {
1398            let (doc, root) = create_doc_with_xinclude_ns();
1399            let include = create_include_child(root, None, None);
1400            assert!(!include.is_null());
1401            let fallback = create_fallback_child(include);
1402            assert!(!fallback.is_null());
1403
1404            let found = find_fallback_child(include);
1405            assert!(!found.is_null(), "Should find fallback child");
1406
1407            let no_fallback = find_fallback_child(root);
1408            assert!(no_fallback.is_null(), "Root should not have fallback");
1409
1410            tree::free_doc(doc);
1411        }
1412    }
1413
1414    #[test]
1415    /// Tests the NULL handling and equality behavior of `xml_str_equal`.
1416    ///
1417    /// # Safety
1418    ///
1419    /// - The `c"..."` literals are NUL-terminated `'static` byte buffers;
1420    ///   `xml_str_equal` returns early when either argument is NULL and only
1421    ///   calls `xmlStrEqual` when both pointers are non-NULL and point to
1422    ///   readable NUL-terminated strings.
1423    fn test_xml_str_equal() {
1424        unsafe {
1425            assert!(xml_str_equal(
1426                c"hello".as_ptr() as *const xmlChar,
1427                c"hello".as_ptr() as *const xmlChar,
1428            ));
1429            assert!(!xml_str_equal(
1430                c"hello".as_ptr() as *const xmlChar,
1431                c"world".as_ptr() as *const xmlChar,
1432            ));
1433            assert!(!xml_str_equal(
1434                ptr::null(),
1435                c"hello".as_ptr() as *const xmlChar
1436            ));
1437            assert!(!xml_str_equal(
1438                c"hello".as_ptr() as *const xmlChar,
1439                ptr::null()
1440            ));
1441            assert!(xml_str_equal(ptr::null(), ptr::null()));
1442        }
1443    }
1444
1445    #[test]
1446    /// Tests that `xinclude_process` fails cleanly on a NULL document.
1447    ///
1448    /// # Safety
1449    ///
1450    /// - `xinclude_process` checks `doc` for NULL and returns
1451    ///   `XINCLUDE_FAILURE` without dereferencing it.
1452    fn test_xinclude_process_null_doc() {
1453        unsafe {
1454            assert_eq!(xinclude_process(ptr::null_mut()), XINCLUDE_FAILURE);
1455        }
1456    }
1457
1458    #[test]
1459    /// Tests that a document without includes processes successfully.
1460    ///
1461    /// # Safety
1462    ///
1463    /// - `doc` is created by `create_simple_doc` and stays valid until
1464    ///   `tree::free_doc`, so the internal tree walks of `xinclude_process`
1465    ///   only touch live nodes.
1466    fn test_xinclude_process_no_includes() {
1467        unsafe {
1468            let doc = create_simple_doc();
1469            assert_eq!(xinclude_process(doc), 0);
1470            tree::free_doc(doc);
1471        }
1472    }
1473
1474    #[test]
1475    /// Tests that an include referencing a missing file is handled gracefully.
1476    ///
1477    /// # Safety
1478    ///
1479    /// - `doc` and `root` come from `create_doc_with_xinclude_ns`, and the
1480    ///   include child is created by `create_include_child`; the document tree
1481    ///   stays intact until `tree::free_doc`, so `xinclude_process` only
1482    ///   dereferences live nodes.
1483    fn test_xinclude_process_with_includes() {
1484        unsafe {
1485            // Create doc with xi:include that references a nonexistent file.
1486            let (doc, root) = create_doc_with_xinclude_ns();
1487            create_include_child(root, Some(b"nonexistent.xml"), None);
1488            let result = xinclude_process(doc);
1489            assert!(result >= 0, "Should handle missing files: {}", result);
1490            tree::free_doc(doc);
1491        }
1492    }
1493
1494    #[test]
1495    /// Tests that fallback content is kept when the include target is missing.
1496    ///
1497    /// # Safety
1498    ///
1499    /// - All nodes are created by the tree helpers and belong to `doc`, which
1500    ///   is freed with `tree::free_doc` at the end; `count_elements` and
1501    ///   `xinclude_process` walk only live, linked nodes that were asserted
1502    ///   non-NULL when created.
1503    fn test_xinclude_fallback_content() {
1504        unsafe {
1505            let (doc, root) = create_doc_with_xinclude_ns();
1506            let include = create_include_child(root, Some(b"nonexistent.xml"), None);
1507            let fb = create_fallback_child(include);
1508            // Add a child to fallback
1509            let fb_child = tree::new_child(
1510                fb,
1511                ptr::null_mut(),
1512                c"fallback-elem".as_ptr() as *const xmlChar,
1513            );
1514            assert!(!fb_child.is_null());
1515
1516            let before = count_elements(doc, c"fallback-elem".as_ptr() as *const xmlChar);
1517            assert!(before > 0, "Should have fallback-elem before processing");
1518
1519            let result = xinclude_process(doc);
1520            assert!(result >= 0, "Should handle fallback: {}", result);
1521            tree::free_doc(doc);
1522        }
1523    }
1524
1525    #[test]
1526    /// Tests that a self-referencing include does not crash the processor.
1527    ///
1528    /// # Safety
1529    ///
1530    /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; the include
1531    ///   child is attached to `root` and owned by `doc`, which is freed after
1532    ///   `xinclude_process` returns, so all pointer dereferences target live
1533    ///   nodes.
1534    fn test_xinclude_circular_reference_detection() {
1535        unsafe {
1536            let (doc, root) = create_doc_with_xinclude_ns();
1537            create_include_child(root, Some(b"self-ref.xml"), None);
1538            let result = xinclude_process(doc);
1539            assert!(result >= 0, "Circular ref should not crash: {}", result);
1540            tree::free_doc(doc);
1541        }
1542    }
1543
1544    #[test]
1545    /// Tests that the `parse` attribute is stored and include children are
1546    /// counted.
1547    ///
1548    /// # Safety
1549    ///
1550    /// - `doc` and `root` come from `create_doc_with_xinclude_ns`; the loop
1551    ///   dereferences `(*root).children` and follows `(*child).next`, all of
1552    ///   which are nodes owned by `doc` and freed with `tree::free_doc`.
1553    fn test_xinclude_parse_attribute_detection() {
1554        unsafe {
1555            let (doc, root) = create_doc_with_xinclude_ns();
1556            create_include_child(root, Some(b"test.xml"), Some(b"xml"));
1557            create_include_child(root, Some(b"test.txt"), Some(b"text"));
1558            create_include_child(root, Some(b"default.xml"), None);
1559
1560            // Count include elements by iterating children.
1561            let mut count = 0;
1562            let mut child = (*root).children;
1563            while !child.is_null() {
1564                if is_xinclude_element(child) {
1565                    count += 1;
1566                }
1567                child = (*child).next;
1568            }
1569            assert_eq!(count, 3, "Should have 3 include elements");
1570            tree::free_doc(doc);
1571        }
1572    }
1573
1574    #[test]
1575    /// Tests both `xinclude_process` and `xinclude_process_flags`.
1576    ///
1577    /// # Safety
1578    ///
1579    /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1580    ///   after both calls, so every dereference inside the processor touches a
1581    ///   live document.
1582    fn test_xinclude_process_functions() {
1583        unsafe {
1584            let doc = create_simple_doc();
1585            let r1 = xinclude_process(doc);
1586            assert!(r1 >= 0);
1587            let r2 = xinclude_process_flags(doc, 0);
1588            assert!(r2 >= 0);
1589            tree::free_doc(doc);
1590        }
1591    }
1592
1593    #[test]
1594    /// Tests an include with no href and a fallback child.
1595    ///
1596    /// # Safety
1597    ///
1598    /// - `doc`, `root`, `include`, and the fallback child are built by the
1599    ///   test helpers, asserted non-NULL where used, and owned by `doc`, which
1600    ///   is freed with `tree::free_doc` after `xinclude_process`.
1601    fn test_xinclude_process_with_empty_href() {
1602        unsafe {
1603            let (doc, root) = create_doc_with_xinclude_ns();
1604            let include = create_include_child(root, None, None);
1605            create_fallback_child(include);
1606            let result = xinclude_process(doc);
1607            assert!(result >= 0, "Empty href with fallback: {}", result);
1608            tree::free_doc(doc);
1609        }
1610    }
1611
1612    #[test]
1613    /// Tests that `set_doc_recursive` assigns the document pointer to detached
1614    /// nodes.
1615    ///
1616    /// # Safety
1617    ///
1618    /// - `doc`, `parent`, and `detached` are `tree` module allocations
1619    ///   asserted non-NULL; `set_doc_recursive` only writes the `(*node).doc`
1620    ///   field of valid nodes, and the nodes are released with `tree::free_node`
1621    ///   and `tree::free_doc` before the test ends.
1622    fn test_set_doc_recursive() {
1623        unsafe {
1624            let doc = tree::new_doc(ptr::null());
1625            assert!(!doc.is_null());
1626            let parent = tree::new_child(
1627                doc as *mut _xmlNode,
1628                ptr::null_mut(),
1629                c"parent".as_ptr() as *const xmlChar,
1630            );
1631            assert!(!parent.is_null());
1632            let detached = tree::new_node(ptr::null_mut(), c"detached".as_ptr() as *const xmlChar);
1633            assert!(!detached.is_null());
1634            assert!((*detached).doc.is_null());
1635            set_doc_recursive(detached, doc);
1636            assert_eq!((*detached).doc, doc);
1637            tree::free_node(detached);
1638            tree::free_doc(doc);
1639        }
1640    }
1641
1642    #[test]
1643    /// Tests that `find_root_element` returns the document root element.
1644    ///
1645    /// # Safety
1646    ///
1647    /// - `doc` is created by `create_simple_doc`; `root` is asserted non-NULL
1648    ///   and dereferenced only while `doc` is alive, and `tree::free_doc`
1649    ///   releases the whole tree at the end.
1650    fn test_find_root_element() {
1651        unsafe {
1652            let doc = create_simple_doc();
1653            let root = find_root_element(doc);
1654            assert!(!root.is_null());
1655            assert_eq!((*root).type_, XML_ELEMENT_NODE as c_int);
1656            tree::free_doc(doc);
1657        }
1658    }
1659
1660    #[test]
1661    /// Tests that the `xpointer` attribute round-trips through the tree.
1662    ///
1663    /// # Safety
1664    ///
1665    /// - `include` is created by `create_include_child` and owned by `doc`;
1666    ///   `xptr_val` is a fresh `bytes_to_xmlstr` allocation freed right after
1667    ///   `tree::set_prop` copies it; `xptr` from `tree::get_prop` is freed
1668    ///   with `xmlFreeImpl` after `xmlstr_to_bytes` copies it, and `doc` is
1669    ///   freed with `tree::free_doc`.
1670    fn test_xinclude_xpointer_attribute() {
1671        unsafe {
1672            let (doc, root) = create_doc_with_xinclude_ns();
1673            let include = create_include_child(root, Some(b"test.xml"), None);
1674            let xptr_val = crate::xml::string::bytes_to_xmlstr(b"xpointer(//target)");
1675            tree::set_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar, xptr_val);
1676            allocator::xmlFreeImpl(xptr_val as *mut c_void);
1677
1678            let xptr = tree::get_prop(include, ATTR_XPOINTER.as_ptr() as *const xmlChar);
1679            assert!(!xptr.is_null(), "Should have xpointer attribute");
1680            assert_eq!(xmlstr_to_bytes(xptr), b"xpointer(//target)");
1681            allocator::xmlFreeImpl(xptr as *mut c_void);
1682
1683            tree::free_doc(doc);
1684        }
1685    }
1686
1687    #[test]
1688    /// Tests that the `accept` and `accept-language` attributes round-trip.
1689    ///
1690    /// # Safety
1691    ///
1692    /// - `include` is owned by `doc`; the attribute values from
1693    ///   `bytes_to_xmlstr` are freed right after `tree::set_prop` copies them,
1694    ///   and the values returned by `tree::get_prop` are freed with
1695    ///   `xmlFreeImpl` after use; `doc` is freed with `tree::free_doc`.
1696    fn test_xinclude_accept_attributes() {
1697        unsafe {
1698            let (doc, root) = create_doc_with_xinclude_ns();
1699            let include = create_include_child(root, Some(b"data.xml"), None);
1700
1701            let accept_val = crate::xml::string::bytes_to_xmlstr(b"application/xml");
1702            tree::set_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar, accept_val);
1703            allocator::xmlFreeImpl(accept_val as *mut c_void);
1704
1705            let lang_val = crate::xml::string::bytes_to_xmlstr(b"en");
1706            tree::set_prop(
1707                include,
1708                ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar,
1709                lang_val,
1710            );
1711            allocator::xmlFreeImpl(lang_val as *mut c_void);
1712
1713            let accept = tree::get_prop(include, ATTR_ACCEPT.as_ptr() as *const xmlChar);
1714            assert!(!accept.is_null());
1715            assert_eq!(xmlstr_to_bytes(accept), b"application/xml");
1716            allocator::xmlFreeImpl(accept as *mut c_void);
1717
1718            let lang = tree::get_prop(include, ATTR_ACCEPT_LANGUAGE.as_ptr() as *const xmlChar);
1719            assert!(!lang.is_null());
1720            assert_eq!(xmlstr_to_bytes(lang), b"en");
1721            allocator::xmlFreeImpl(lang as *mut c_void);
1722
1723            tree::free_doc(doc);
1724        }
1725    }
1726
1727    #[test]
1728    /// Tests that the `encoding` attribute round-trips through the tree.
1729    ///
1730    /// # Safety
1731    ///
1732    /// - `include` is owned by `doc`; `enc_val` is freed after `tree::set_prop`
1733    ///   copies it, `encoding` from `tree::get_prop` is freed after use, and
1734    ///   `doc` is freed with `tree::free_doc`.
1735    fn test_xinclude_encoding_attribute() {
1736        unsafe {
1737            let (doc, root) = create_doc_with_xinclude_ns();
1738            let include = create_include_child(root, Some(b"data.txt"), Some(b"text"));
1739
1740            let enc_val = crate::xml::string::bytes_to_xmlstr(b"UTF-8");
1741            tree::set_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar, enc_val);
1742            allocator::xmlFreeImpl(enc_val as *mut c_void);
1743
1744            let encoding = tree::get_prop(include, ATTR_ENCODING.as_ptr() as *const xmlChar);
1745            assert!(!encoding.is_null());
1746            assert_eq!(xmlstr_to_bytes(encoding), b"UTF-8");
1747            allocator::xmlFreeImpl(encoding as *mut c_void);
1748
1749            tree::free_doc(doc);
1750        }
1751    }
1752
1753    #[test]
1754    /// Tests that `xinclude_process` and `xinclude_process_flags` with zero
1755    /// flags behave identically.
1756    ///
1757    /// # Safety
1758    ///
1759    /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1760    ///   after both calls, so the processor only dereferences live nodes.
1761    fn test_xinclude_process_flags_equivalence() {
1762        unsafe {
1763            let doc = create_simple_doc();
1764            let r1 = xinclude_process(doc);
1765            let r2 = xinclude_process_flags(doc, 0);
1766            assert_eq!(r1, r2);
1767            tree::free_doc(doc);
1768        }
1769    }
1770
1771    #[test]
1772    /// Tests `xinclude_process_flags` with the `XML_PARSE_NOXINCNODE` flag.
1773    ///
1774    /// # Safety
1775    ///
1776    /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`
1777    ///   after the call, so all pointer dereferences target live nodes.
1778    fn test_xinclude_process_flags_noxincnode() {
1779        unsafe {
1780            let doc = create_simple_doc();
1781            let result = xinclude_process_flags(doc, XML_PARSE_NOXINCNODE);
1782            assert_eq!(result, 0);
1783            tree::free_doc(doc);
1784        }
1785    }
1786
1787    #[test]
1788    #[ignore = "pre-existing tree module cleanup bug with modified trees"]
1789    /// Tests processing a document with nested includes and fallbacks.
1790    ///
1791    /// # Safety
1792    ///
1793    /// - All nodes are created by the `tree` helpers, asserted non-NULL, and
1794    ///   owned by `doc`; `xinclude_process` walks the tree only while `doc` is
1795    ///   alive. The test is ignored because the tree cleanup path does not
1796    ///   free modified trees, and it deliberately leaks `doc` (no `free_doc`),
1797    ///   but every unsafe access targets nodes that remain live for the whole
1798    ///   test.
1799    fn test_complex_nested_includes_structure() {
1800        unsafe {
1801            // Build a doc with a complex structure including xi:include elements.
1802            let doc = tree::new_doc(ptr::null());
1803            assert!(!doc.is_null());
1804            let root = tree::new_child(
1805                doc as *mut _xmlNode,
1806                ptr::null_mut(),
1807                c"root".as_ptr() as *const xmlChar,
1808            );
1809            assert!(!root.is_null());
1810            create_ns(
1811                root,
1812                c"xi".as_ptr() as *const xmlChar,
1813                XINCLUDE_NS.as_ptr() as *const xmlChar,
1814            );
1815
1816            create_include_child(root, Some(b"nonexistent1.xml"), None);
1817
1818            let inc2 = create_include_child(root, Some(b"nonexistent2.xml"), None);
1819            let fb2 = create_fallback_child(inc2);
1820            tree::new_child(
1821                fb2,
1822                ptr::null_mut(),
1823                c"fallback-content".as_ptr() as *const xmlChar,
1824            );
1825
1826            create_include_child(root, Some(b"nonexistent3.txt"), Some(b"text"));
1827
1828            let result = xinclude_process(doc);
1829            assert!(result >= 0, "Complex structure: {}", result);
1830        }
1831    }
1832
1833    #[test]
1834    /// Tests that `xinclude_process` leaves the document in a freeable state.
1835    ///
1836    /// # Safety
1837    ///
1838    /// - `doc` is created by `create_simple_doc`, processed while alive, and
1839    ///   freed with `tree::free_doc` at the end, so all dereferences target
1840    ///   live nodes.
1841    fn test_xinclude_process_xml_memory_cleanup() {
1842        unsafe {
1843            let doc = create_simple_doc();
1844            assert!(xinclude_process(doc) >= 0);
1845            tree::free_doc(doc);
1846        }
1847    }
1848
1849    #[test]
1850    /// Tests that `mark_doc_xinclude_processed` sets the XInclude flag.
1851    ///
1852    /// # Safety
1853    ///
1854    /// - `doc` is created by `create_simple_doc` and freed with `tree::free_doc`;
1855    ///   the `(*doc).properties` field is read and written only while `doc` is
1856    ///   alive.
1857    fn test_mark_doc_xinclude_processed() {
1858        unsafe {
1859            let doc = create_simple_doc();
1860            assert_eq!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1861            mark_doc_xinclude_processed(doc);
1862            assert_ne!((*doc).properties & XML_DOC_XINCLUDE as c_int, 0);
1863            tree::free_doc(doc);
1864        }
1865    }
1866
1867    #[test]
1868    /// Tests that `process_node_tree` skips XInclude start/end sentinel nodes.
1869    ///
1870    /// # Safety
1871    ///
1872    /// - `doc` and `root` are created by the helpers and stay alive until the
1873    ///   end of the test; the sentinel node is allocated by `tree::new_node`,
1874    ///   manually linked into `root`'s sibling list, and unlinked again before
1875    ///   `tree::free_node`; every dereference during linking, the
1876    ///   `process_node_tree` walk, and unlinking touches nodes that are still
1877    ///   allocated.
1878    fn test_xinclude_xinclude_start_end_nodes() {
1879        unsafe {
1880            let doc = create_simple_doc();
1881            let root = find_root_element(doc);
1882            assert!(!root.is_null());
1883
1884            // Create a sentinel XML_XINCLUDE_START node attached to root.
1885            let sentinel =
1886                tree::new_node(ptr::null_mut(), c"XIncludeStart".as_ptr() as *const xmlChar);
1887            assert!(!sentinel.is_null());
1888            (*sentinel).type_ = XML_XINCLUDE_START as c_int;
1889            (*sentinel).doc = doc;
1890            // Link as next sibling of root's children (simple linking).
1891            let first_child = (*root).children;
1892            if !first_child.is_null() {
1893                // Insert sentinel after first child
1894                (*sentinel).parent = root;
1895                (*sentinel).prev = first_child;
1896                (*sentinel).next = (*first_child).next;
1897                if !(*first_child).next.is_null() {
1898                    (*(*first_child).next).prev = sentinel;
1899                }
1900                (*first_child).next = sentinel;
1901                if (*root).last == first_child {
1902                    (*root).last = sentinel;
1903                }
1904            }
1905
1906            let mut visited = Vec::new();
1907            let count = { process_node_tree(root, doc, &mut visited) };
1908            assert_eq!(count, 0, "Should not process sentinel nodes");
1909
1910            // Unlink sentinel before freeing
1911            if !(*sentinel).prev.is_null() {
1912                (*(*sentinel).prev).next = (*sentinel).next;
1913            }
1914            if !(*sentinel).next.is_null() {
1915                (*(*sentinel).next).prev = (*sentinel).prev;
1916            }
1917            (*sentinel).prev = ptr::null_mut();
1918            (*sentinel).next = ptr::null_mut();
1919            (*sentinel).parent = ptr::null_mut();
1920
1921            tree::free_node(sentinel);
1922            tree::free_doc(doc);
1923        }
1924    }
1925}