Skip to main content

libxml_rs/xml/tree/
mod.rs

1//! XML tree construction and manipulation (§17, §18, §85 Phase 1).
2//!
3//! Complete tree construction/manipulation, namespaces, attributes,
4//! dictionaries, entity structures, document ownership, copying, linking,
5//! and freeing.
6//!
7//! # UPSTREAM-PARITY
8//!
9//! The libxml2 tree is an observable data structure. The pointer topology
10//! (parent, children, last, next, prev, doc, ns, properties, nsDef) is
11//! part of the compatibility contract and must be court-tested.
12//!
13//! Key invariants (matching upstream):
14//!
15//! - `node->doc` points to the owning document (or NULL if not owned)
16//! - `node->parent` points to the parent element (or NULL for root)
17//! - `node->children` points to the first child
18//! - `node->last` points to the last child
19//! - `node->next` / `node->prev` form a doubly-linked list of siblings
20//! - `node->properties` points to the first attribute (for elements)
21//! - `node->nsDef` points to the first namespace declaration (for elements)
22//! - `doc->children` points to the root element
23//! - `doc->doc` points to itself (self-reference)
24//!
25//! # Ownership model
26//!
27//! Documents own all their nodes. When a document is freed, all nodes
28//! are freed. Nodes can be moved between documents via unlinking and
29//! re-adding.
30//!
31//! # Phase 1 status
32//!
33//! Complete — all tree operations are implemented.
34//! Future phases may add more edge-case handling for historical quirks.
35//!
36//! # Upstream contract
37//!
38//! Mirrors upstream tree.c and buf.c (SRC-LIBXML2-2.15.0, oracle tree
39//! `oracle/historical/src/libxml2-2.15.0/`). The tree is an observable data
40//! structure: pointer topology (parent, children, last, next, prev, doc, ns,
41//! properties, nsDef) is part of the compatibility contract and must be
42//! court-tested. Parity target: the system libxml2 2.15.3 oracle.
43//!
44//! # Conceptual behavior
45//!
46//! Complete tree construction/manipulation: namespaces, attributes,
47//! dictionaries, entity structures, document ownership, copying, linking and
48//! freeing. Nodes are C-layout mirrors (tree.h); copy/link/free semantics
49//! follow xmlCopyNode / xmlAddChild / xmlFreeNodeList / xmlFreeDoc.
50//!
51//! # Ownership & safety invariants
52//!
53//! Documents own all their nodes; freeing the document frees the subtree.
54//! node->parent, node->doc, node->ns, node->next/prev are borrowed pointers —
55//! never freed by the reader. Allocator domain: xmlMalloc, freed with xmlFree
56//! (atlas/OWNERSHIP_ATLAS.md). SAFETY: the Rust mirrors enforce layout exactly
57//! (`#[repr(C)]`); `_xmlElement` is 104 bytes upstream and must stay that size
58//! (R-000139: a 56-byte mirror under-allocated every element declaration).
59//!
60//! # Historical quirks & epochs
61//!
62//! QUIRK-0002 / LORE-0006: namespace nodes have no parent — a long-standing
63//! divergence upstream was aware of since the c14n fix commit 044fc6b7
64//! (2002). E-004: entity-content text nodes became TEXT compact at 2.13.0
65//! (commit 8d04f0ee). The 11.1-N structural alignment (R-000164) pinned
66//! doc->children DTD placement, CDATA node names, standalone=-2 and the
67//! attribute hash (name,prefix,elem) key order.
68//!
69//! # Deliberate oddities
70//!
71//! Deliberate oddities preserved for parity: an xmlns= declaration with an
72//! empty value yields href pointing at an empty string (not NULL), parsed
73//! attributes keep atype=0, the DTD node joins doc->children before the first
74//! element, and xmlGetLineNo returns long with the upstream -1 walk for
75//! non-element nodes (all R-000164).
76//!
77//! # Proving courts
78//!
79//! OWNERSHIP and TREE-STRUCTURE court families; TREE-001 (27-block structural
80//! fingerprint of 20 corpus docs x 8 option variants, byte-identical), ASan
81//! full-suite runs, and `cargo test --lib` (1135+ tests). Receipts under
82//! courts/receipts/phase-11.
83//!
84//! # Tempting simplifications that would break parity
85//!
86//! A tempting simplification is a nicer Rust node type instead of the exact
87//! `_xmlNode` / `_xmlElement` mirrors — it would break the C ABI layout
88//! (R-000139 class) and every C consumer reading fields at upstream offsets.
89//! Do not auto-maintain parent pointers for namespace nodes (QUIRK-0002); do
90//! not drop the last/next/prev links — TREE-001 fingerprints them.
91//!
92//! # Safety
93//!
94//! - The unsafe entry points in this module accept raw pointers that must be
95//!   valid, correctly typed, and live for the duration of the call: `_xmlDoc`,
96//!   `_xmlNode`, `_xmlAttr`, `_xmlNs`, `_xmlDtd`, `_xmlBuffer`, and
97//!   NUL-terminated `xmlChar` strings. NULL is permitted only where an
98//!   individual function's contract explicitly allows it.
99//! - Tree links (`parent`, `children`, `last`, `next`, `prev`, `properties`,
100//!   `nsDef`) must form a consistent, live tree; documents own their node
101//!   subtrees, so callers must not free a node that still belongs to a live
102//!   document.
103
104use core::ffi::c_void;
105use core::ptr;
106use std::os::raw::{c_char, c_int, c_long, c_ulong};
107
108use crate::abi::allocator;
109use crate::abi::constants::*;
110use crate::abi::structs::*;
111use crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8;
112use crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT;
113use crate::abi::types::xmlElementType::*;
114use crate::abi::types::*;
115use crate::xml::io;
116
117// ═══════════════════════════════════════════════════════════════════════════════
118// String Helpers
119// ═══════════════════════════════════════════════════════════════════════════════
120
121/// Duplicate an xmlChar string using xmlMalloc.
122///
123/// # SAFETY
124///
125/// - `str` must be a valid null-terminated xmlChar* or NULL.
126unsafe fn dup_xml_str(str: *const xmlChar) -> *mut xmlChar {
127    if str.is_null() {
128        return ptr::null_mut();
129    }
130    let len = unsafe { crate::abi::exports_xml2::xmlStrlen(str) as usize };
131    if len == 0 {
132        // Return a pointer to a null byte
133        let buf = unsafe { allocator::xmlMallocImpl(1) as *mut xmlChar };
134        if !buf.is_null() {
135            unsafe { *buf = 0 };
136        }
137        return buf;
138    }
139    let buf = unsafe { allocator::xmlMallocImpl(len + 1) as *mut xmlChar };
140    if !buf.is_null() {
141        unsafe {
142            ptr::copy_nonoverlapping(str, buf, len + 1);
143        }
144    }
145    buf
146}
147
148/// Copy an xmlChar string into an already-allocated buffer, or return NULL.
149#[allow(dead_code)]
150unsafe fn copy_xml_str_content(dest: *mut xmlChar, src: *const xmlChar, max_len: usize) -> bool {
151    if src.is_null() || dest.is_null() || max_len == 0 {
152        return false;
153    }
154    let len = unsafe { crate::abi::exports_xml2::xmlStrlen(src) as usize };
155    if len >= max_len {
156        return false;
157    }
158    unsafe {
159        ptr::copy_nonoverlapping(src, dest, len);
160        *dest.add(len) = 0;
161    }
162    true
163}
164
165/// Get the length of a null-terminated xmlChar string.
166///
167/// # SAFETY
168///
169///
170/// - `str` must point to valid NUL-terminated
171///   strings (or NULL where the C contract allows) for the lifetime
172///   of the call.
173///
174/// The caller must not race this call with concurrent mutation of the
175/// same objects from other threads (per-object state is not internally
176/// synchronized). Violating any of the above is undefined behavior.
177///
178/// Exercised by the C-API differential courts
179/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
180/// courts; those pass byte-for-byte against the upstream oracle.
181pub const unsafe fn xml_strlen(str: *const xmlChar) -> c_int {
182    if str.is_null() {
183        return 0;
184    }
185    let mut len: c_int = 0;
186    while unsafe { *str.add(len as usize) != 0 } {
187        len += 1;
188    }
189    len
190}
191
192// ═══════════════════════════════════════════════════════════════════════════════
193// Document Operations
194// ═══════════════════════════════════════════════════════════════════════════════
195
196/// Create a new XML document.
197///
198/// # UPSTREAM-PARITY
199///
200/// ```c
201/// xmlDocPtr xmlNewDoc(const xmlChar *version);
202/// ```
203///
204/// Creates a new document with the given version string (or "1.0" if NULL).
205/// The document is initialized with:
206/// - type = XML_DOCUMENT_NODE
207/// - standalone = -1 (unknown)
208/// - doc->doc = self (self-reference)
209/// - properties = XML_DOC_WELLFORMED
210///
211/// # SAFETY
212///
213/// - `version` must be a valid null-terminated string or NULL.
214pub unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
215    // SAFETY: Allocate zero-initialized memory for the document.
216    let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
217    if doc.is_null() {
218        return ptr::null_mut();
219    }
220
221    unsafe {
222        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
223        (*doc).standalone = -1; // unknown
224        (*doc).doc = doc; // self-reference
225        (*doc).properties = XML_DOC_USERBUILT as c_int;
226        (*doc).compression = -1; // not initialized (upstream xmlNewDoc)
227        (*doc).charset = XML_CHAR_ENCODING_UTF8 as c_int;
228
229        // Set version
230        let ver = if version.is_null() {
231            XML_DEFAULT_VERSION.as_ptr() as *const xmlChar
232        } else {
233            version
234        };
235        (*doc).version = dup_xml_str(ver);
236    }
237
238    // UPSTREAM-PARITY (tree.c xmlNewDoc): the document node is registered.
239    crate::abi::data_globals::register_node_hook(doc as *mut _xmlNode);
240
241    doc
242}
243
244/// Free a document and all its contents.
245///
246/// # UPSTREAM-PARITY
247///
248/// ```c
249/// void xmlFreeDoc(xmlDocPtr doc);
250/// ```
251///
252/// Frees the document, its DTDs, and all nodes in the tree.
253///
254/// # SAFETY
255///
256/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
257pub unsafe fn free_doc(doc: *mut _xmlDoc) {
258    if doc.is_null() {
259        return;
260    }
261
262    // UPSTREAM-PARITY (tree.c xmlFreeDoc): the deregister hook fires before
263    // the document is torn down.
264    crate::abi::data_globals::deregister_node_hook(doc as *mut _xmlNode);
265
266    let d = unsafe { &mut *doc };
267
268    // UPSTREAM-PARITY (tree.c xmlFreeDoc): the subset DTD nodes are unlinked
269    // from the child list before the tree is freed (they may be part of
270    // doc->children after a parse).
271    let dict = d.dict;
272    let mut ext_subset = d.extSubset;
273    let int_subset = d.intSubset;
274    if !ext_subset.is_null() && ext_subset == int_subset {
275        ext_subset = ptr::null_mut();
276    }
277    if !ext_subset.is_null() {
278        unlink_node_internal(ext_subset as *mut _xmlNode, d as *mut _xmlDoc);
279        d.extSubset = ptr::null_mut();
280        free_dtd(ext_subset);
281    }
282    if !int_subset.is_null() {
283        unlink_node_internal(int_subset as *mut _xmlNode, d as *mut _xmlDoc);
284        d.intSubset = ptr::null_mut();
285        free_dtd(int_subset);
286    }
287
288    // Free the tree
289    if !d.children.is_null() {
290        free_node_list(d.children);
291    }
292
293    // Free oldNs list
294    if !d.oldNs.is_null() {
295        free_ns_list(d.oldNs);
296    }
297
298    // Free strings
299    if !d.version.is_null() {
300        allocator::xmlFreeImpl(d.version as *mut c_void);
301    }
302    if !d.encoding.is_null() {
303        allocator::xmlFreeImpl(d.encoding as *mut c_void);
304    }
305    if !d.URL.is_null() {
306        allocator::xmlFreeImpl(d.URL as *mut c_void);
307    }
308
309    // Free the document itself
310    allocator::xmlFreeImpl(doc as *mut c_void);
311
312    // UPSTREAM-PARITY: the document holds a reference on its dictionary.
313    if !dict.is_null() {
314        crate::abi::exports_xml2::xmlDictFree(dict);
315    }
316}
317
318/// Unlink a node from its parent's child list without freeing it
319/// (upstream xmlUnlinkNodeInternal semantics; doc is used for ID/ref
320/// bookkeeping which the candidate does not maintain on unlink).
321unsafe fn unlink_node_internal(node: *mut _xmlNode, _doc: *mut _xmlDoc) {
322    if node.is_null() {
323        return;
324    }
325    let parent = (*node).parent;
326    if parent.is_null() {
327        return;
328    }
329    if (*node).prev.is_null() {
330        (*parent).children = (*node).next;
331    } else {
332        (*(*node).prev).next = (*node).next;
333    }
334    if (*node).next.is_null() {
335        (*parent).last = (*node).prev;
336    } else {
337        (*(*node).next).prev = (*node).prev;
338    }
339    (*node).next = ptr::null_mut();
340    (*node).prev = ptr::null_mut();
341    (*node).parent = ptr::null_mut();
342}
343
344/// Copy a document (deep copy by default).
345///
346/// # UPSTREAM-PARITY
347///
348/// ```c
349/// xmlDocPtr xmlCopyDoc(xmlDocPtr doc, int recursive);
350/// ```
351///
352/// If `recursive` is 1, the entire tree is copied.
353/// If `recursive` is 0, only the document structure is copied (no children).
354///
355/// # SAFETY
356///
357/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
358pub unsafe fn copy_doc(doc: *const _xmlDoc, recursive: c_int) -> *mut _xmlDoc {
359    if doc.is_null() {
360        return ptr::null_mut();
361    }
362
363    let d = unsafe { &*doc };
364
365    let new_doc = new_doc(d.version);
366    if new_doc.is_null() {
367        return ptr::null_mut();
368    }
369
370    unsafe {
371        (*new_doc).type_ = d.type_;
372        (*new_doc).standalone = d.standalone;
373        (*new_doc).encoding = dup_xml_str(d.encoding);
374        (*new_doc).URL = dup_xml_str(d.URL);
375        (*new_doc).charset = d.charset;
376        (*new_doc).properties = d.properties;
377
378        // UPSTREAM-PARITY: xmlCopyDoc copies the document's children, which
379        // include the DTD node (upstream keeps it as the first child); ours
380        // stores the internal subset on doc->intSubset, so copy it there to
381        // reproduce --copy output.
382        if !d.intSubset.is_null() {
383            let dtd_copy = crate::xml::dtd::copy_dtd(d.intSubset);
384            if !dtd_copy.is_null() {
385                (*new_doc).intSubset = dtd_copy;
386            }
387        }
388
389        if recursive != 0 && !d.children.is_null() {
390            (*new_doc).children = copy_node_list(d.children, recursive);
391            if !(*new_doc).children.is_null() {
392                (*(*new_doc).children).parent = ptr::null_mut(); // root element parent is NULL
393                (*(*new_doc).children).doc = new_doc;
394                // Update doc for all descendants
395                propagate_doc((*new_doc).children, new_doc);
396            }
397        }
398    }
399
400    new_doc
401}
402
403/// Set the root element of a document.
404///
405/// # UPSTREAM-PARITY
406///
407/// ```c
408/// xmlNodePtr xmlDocSetRootElement(xmlDocPtr doc, xmlNodePtr root);
409/// ```
410///
411/// If the document already has a root element, the old root is returned.
412/// The new root is added as a child of the document.
413///
414/// # SAFETY
415///
416/// - `doc` must be a valid pointer to an _xmlDoc.
417/// - `root` must be a valid pointer to an _xmlNode, or NULL.
418pub unsafe fn doc_set_root_element(doc: *mut _xmlDoc, root: *mut _xmlNode) -> *mut _xmlNode {
419    if doc.is_null() {
420        return ptr::null_mut();
421    }
422
423    let d = unsafe { &mut *doc };
424
425    let old_root = doc_get_root_element(doc);
426
427    if !root.is_null() {
428        unsafe {
429            (*root).parent = ptr::null_mut();
430            (*root).doc = doc;
431        }
432        d.children = root;
433        d.last = root;
434        unsafe {
435            (*root).prev = ptr::null_mut();
436            (*root).next = ptr::null_mut();
437        }
438    } else {
439        d.children = ptr::null_mut();
440        d.last = ptr::null_mut();
441    }
442
443    old_root
444}
445
446/// Get the root element of a document.
447///
448/// # UPSTREAM-PARITY
449///
450/// ```c
451/// xmlNodePtr xmlDocGetRootElement(xmlDocPtr doc);
452/// ```
453///
454/// Returns the root element, or NULL if the document has no root element.
455/// Skips non-element nodes (like PIs, comments) at the document level.
456///
457/// # Safety
458///
459/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL; when non-NULL it
460///   is dereferenced and its `children` chain is walked.
461pub fn doc_get_root_element(doc: *mut _xmlDoc) -> *mut _xmlNode {
462    if doc.is_null() {
463        return ptr::null_mut();
464    }
465
466    let d = unsafe { &*doc };
467    let mut cur = d.children;
468
469    while !cur.is_null() {
470        let node = unsafe { &*cur };
471        if node.type_ == XML_ELEMENT_NODE as c_int {
472            return cur;
473        }
474        cur = node.next;
475    }
476
477    ptr::null_mut()
478}
479
480/// Get the line number of a node.
481///
482/// # UPSTREAM-PARITY
483///
484/// ```c
485/// long xmlGetLineNo(xmlNodePtr node);
486/// ```
487///
488/// Returns the line number, or 0 if not available.
489///
490/// # Safety
491///
492/// - `node` must be NULL or a valid pointer to an `_xmlNode`; it is forwarded
493///   to `get_line_no_internal`, which walks the tree links.
494pub fn get_line_no(node: *const _xmlNode) -> c_long {
495    unsafe { get_line_no_internal(node, 0) }
496}
497
498/// UPSTREAM-PARITY (tree.c xmlGetLineNoInternal): element/text/comment/PI
499/// nodes report their stored line; other node types (DTD nodes, entity
500/// references, declarations, ...) walk to the nearest previous or ancestor
501/// element-ish node and return -1 when none exists.
502///
503/// # Safety
504///
505/// - `node` must be NULL or a valid pointer to an `_xmlNode` in a consistent
506///   tree: the `children`, `next`, `prev`, and `parent` links it follows must
507///   themselves point to valid `_xmlNode` structs.
508/// - `depth` bounds the recursion; callers start at 0.
509unsafe fn get_line_no_internal(node: *const _xmlNode, depth: c_int) -> c_long {
510    if depth >= 5 {
511        return -1;
512    }
513    if node.is_null() {
514        return -1;
515    }
516    let n = unsafe { &*node };
517    if n.type_ == XML_ELEMENT_NODE as c_int
518        || n.type_ == XML_TEXT_NODE as c_int
519        || n.type_ == XML_COMMENT_NODE as c_int
520        || n.type_ == XML_PI_NODE as c_int
521    {
522        if n.line == 65535 {
523            if n.type_ == XML_ELEMENT_NODE as c_int && !n.children.is_null() {
524                let r = unsafe { get_line_no_internal(n.children, depth + 1) };
525                if r != -1 {
526                    return r;
527                }
528            }
529            if !n.next.is_null() {
530                let r = unsafe { get_line_no_internal(n.next, depth + 1) };
531                if r != -1 {
532                    return r;
533                }
534            }
535            if !n.prev.is_null() {
536                let r = unsafe { get_line_no_internal(n.prev, depth + 1) };
537                if r != -1 {
538                    return r;
539                }
540            }
541        }
542        n.line as c_long
543    } else if !n.prev.is_null()
544        && (unsafe { (*n.prev).type_ } == XML_ELEMENT_NODE as c_int
545            || unsafe { (*n.prev).type_ } == XML_TEXT_NODE as c_int
546            || unsafe { (*n.prev).type_ } == XML_COMMENT_NODE as c_int
547            || unsafe { (*n.prev).type_ } == XML_PI_NODE as c_int)
548    {
549        unsafe { get_line_no_internal(n.prev, depth + 1) }
550    } else if !n.parent.is_null() && unsafe { (*n.parent).type_ } == XML_ELEMENT_NODE as c_int {
551        unsafe { get_line_no_internal(n.parent, depth + 1) }
552    } else {
553        -1
554    }
555}
556
557/// Get the content of a node, recursively concatenating child text.
558///
559/// # UPSTREAM-PARITY
560///
561/// ```c
562/// xmlChar *xmlNodeGetContent(const xmlNode *cur);
563/// ```
564///
565/// Oracle behavior (tree.c `xmlNodeGetContent`):
566/// - For text/CDATA nodes: returns the content directly.
567/// - For element nodes: recursively concatenates the string values of
568///   children (text and CDATA; entity references are expanded via their
569///   content when available).
570/// - For attribute nodes: returns the attribute value (first child).
571/// - For comments/PIs: returns the content field.
572/// - For documents: returns content of the root element.
573/// - Returns NULL on error, empty string for empty nodes.
574///
575/// Returns a newly allocated string; caller frees with `xmlFree`.
576///
577/// # SAFETY
578///
579/// - `node` must be valid pointers (or NULL
580///   where the upstream C contract allows), obtained from the
581///   matching constructor/owner and not yet freed; the callee may
582///   take or keep ownership exactly as the C API specifies.
583///
584/// The caller must not race this call with concurrent mutation of the
585/// same objects from other threads (per-object state is not internally
586/// synchronized). Violating any of the above is undefined behavior.
587///
588/// Exercised by the C-API differential courts
589/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
590/// courts; those pass byte-for-byte against the upstream oracle.
591pub unsafe fn node_get_content(node: *mut _xmlNode) -> *mut xmlChar {
592    if node.is_null() {
593        return ptr::null_mut();
594    }
595    let typ = (*node).type_;
596    let mut result: Vec<u8> = Vec::new();
597    match typ {
598        t if t == XML_TEXT_NODE as c_int
599            || t == XML_CDATA_SECTION_NODE as c_int
600            || t == XML_COMMENT_NODE as c_int
601            || t == XML_PI_NODE as c_int =>
602        {
603            if !(*node).content.is_null() {
604                let len = crate::abi::exports_xml2::xmlStrlen((*node).content);
605                result
606                    .extend_from_slice(core::slice::from_raw_parts((*node).content, len as usize));
607            } else if t == XML_TEXT_NODE as c_int && !(*node).children.is_null() {
608                // Non-compact text node (entity merge): content lives in the
609                // child text nodes.
610                let mut child = (*node).children;
611                while !child.is_null() {
612                    if !(*child).content.is_null() {
613                        let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
614                        result.extend_from_slice(core::slice::from_raw_parts(
615                            (*child).content,
616                            len as usize,
617                        ));
618                    }
619                    child = (*child).next;
620                }
621            }
622        }
623        t if t == XML_ATTRIBUTE_NODE as c_int => {
624            // Attribute: content is the value (first text child).
625            if !(*node).children.is_null() {
626                let child = (*node).children;
627                if !(*child).content.is_null() {
628                    let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
629                    result.extend_from_slice(core::slice::from_raw_parts(
630                        (*child).content,
631                        len as usize,
632                    ));
633                }
634            }
635        }
636        t if t == XML_ENTITY_REF_NODE as c_int => {
637            // Entity reference: expand via entity content.
638            let name = (*node).name;
639            if !name.is_null() && !(*node).doc.is_null() {
640                let ent = crate::xml::tree::get_doc_entity((*node).doc, name);
641                if !ent.is_null() && !(*ent).content.is_null() {
642                    let len = crate::abi::exports_xml2::xmlStrlen((*ent).content);
643                    result.extend_from_slice(core::slice::from_raw_parts(
644                        (*ent).content,
645                        len as usize,
646                    ));
647                }
648            }
649        }
650        t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
651            let root = doc_get_root_element(node as *mut _xmlDoc);
652            if !root.is_null() {
653                let sub = node_get_content(root);
654                if !sub.is_null() {
655                    let len = crate::abi::exports_xml2::xmlStrlen(sub);
656                    result.extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
657                    allocator::xmlFreeImpl(sub as *mut c_void);
658                }
659            }
660        }
661        _ => {
662            // Element and everything else: concatenate descendant text
663            // content (XPath 1.0 string-value semantics — §4.2 / tree.c
664            // xmlNodeGetContent, which walks the full subtree, not just
665            // direct text children).
666            let mut child = (*node).children;
667            while !child.is_null() {
668                let ctype = (*child).type_;
669                if ctype == XML_TEXT_NODE as c_int || ctype == XML_CDATA_SECTION_NODE as c_int {
670                    if !(*child).content.is_null() {
671                        let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
672                        result.extend_from_slice(core::slice::from_raw_parts(
673                            (*child).content,
674                            len as usize,
675                        ));
676                    }
677                } else if ctype == XML_ENTITY_REF_NODE as c_int
678                    || ctype == XML_ELEMENT_NODE as c_int
679                {
680                    let sub = node_get_content(child);
681                    if !sub.is_null() {
682                        let len = crate::abi::exports_xml2::xmlStrlen(sub);
683                        result.extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
684                        allocator::xmlFreeImpl(sub as *mut c_void);
685                    }
686                }
687                child = (*child).next;
688            }
689        }
690    }
691    // Allocate the C string.
692    let buf = allocator::xmlMallocImpl(result.len() + 1) as *mut xmlChar;
693    if buf.is_null() {
694        return ptr::null_mut();
695    }
696    if !result.is_empty() {
697        ptr::copy_nonoverlapping(result.as_ptr(), buf, result.len());
698    }
699    *buf.add(result.len()) = 0;
700    buf
701}
702
703// ═══════════════════════════════════════════════════════════════════════════════
704// Node Operations
705// ═══════════════════════════════════════════════════════════════════════════════
706
707/// Create a new XML node.
708///
709/// # UPSTREAM-PARITY
710///
711/// ```c
712/// xmlNodePtr xmlNewNode(xmlNsPtr ns, const xmlChar *name);
713/// ```
714///
715/// Creates a new element node with the given name and namespace.
716///
717/// # SAFETY
718///
719/// - `ns` may be NULL.
720/// - `name` must be a valid null-terminated string or NULL.
721pub unsafe fn new_node(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
722    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
723    if node.is_null() {
724        return ptr::null_mut();
725    }
726
727    unsafe {
728        (*node).type_ = XML_ELEMENT_NODE as c_int;
729        (*node).name = dup_xml_str(name);
730        (*node).ns = ns;
731        (*node).line = 0;
732        (*node).extra = 0;
733
734        if !ns.is_null() {
735            (*ns).context = node as *mut _xmlDoc;
736        }
737    }
738
739    // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
740    // node is fully initialised.
741    crate::abi::data_globals::register_node_hook(node);
742
743    node
744}
745
746/// Free a single node (without freeing children).
747///
748/// # UPSTREAM-PARITY
749///
750/// ```c
751/// void xmlFreeNode(xmlNodePtr node);
752/// ```
753///
754/// Frees a node and its properties/namespaces, but NOT its children.
755/// Children must be freed separately or reattached.
756///
757/// # SAFETY
758///
759/// - `node` must be a valid pointer to an _xmlNode, or NULL.
760pub unsafe fn free_node(node: *mut _xmlNode) {
761    if node.is_null() {
762        return;
763    }
764
765    let n = unsafe { &mut *node };
766
767    // UPSTREAM-PARITY (tree.c xmlFreeNode): declaration nodes and namespace
768    // declarations are routed to their dedicated free functions (their
769    // struct layouts diverge from _xmlNode).
770    if n.type_ == XML_DTD_NODE as c_int {
771        free_dtd(node as *mut _xmlDtd);
772        return;
773    } else if n.type_ == XML_NAMESPACE_DECL as c_int {
774        free_ns(node as *mut _xmlNs);
775        return;
776    } else if n.type_ == XML_ATTRIBUTE_NODE as c_int {
777        free_prop(node as *mut _xmlAttr);
778        return;
779    } else if n.type_ == XML_ELEMENT_DECL as c_int {
780        crate::xml::dtd::free_element(node as *mut _xmlElement);
781        return;
782    } else if n.type_ == XML_ATTRIBUTE_DECL as c_int {
783        crate::xml::dtd::free_attribute(node as *mut _xmlAttribute);
784        return;
785    } else if n.type_ == XML_ENTITY_DECL as c_int {
786        crate::xml::entities::free_entity(node as *mut _xmlEntity);
787        return;
788    }
789
790    // UPSTREAM-PARITY (tree.c xmlFreeNode): the deregister hook fires before
791    // the node is torn down.
792    crate::abi::data_globals::deregister_node_hook(node);
793
794    // Free properties and namespace declarations. Only element nodes carry
795    // them; compact text nodes store their inline content at the address of
796    // the `properties` field (and the following `nsDef` field), so touching
797    // these for other node types would read text bytes as pointers.
798    let is_element = n.type_ == XML_ELEMENT_NODE as c_int;
799    if is_element && !n.properties.is_null() {
800        free_prop_list(n.properties);
801    }
802
803    if is_element && !n.nsDef.is_null() {
804        free_ns_list(n.nsDef);
805    }
806
807    // Free the name
808    if !n.name.is_null() {
809        allocator::xmlFreeImpl(n.name as *mut c_void);
810    }
811
812    // Free content (for text/CDATA nodes). Compact text content lives inside
813    // the node struct (at the `properties` field address) and must not be
814    // freed separately. UPSTREAM-PARITY: entity-reference content is shared
815    // with the entity declaration and must not be freed here.
816    if !n.content.is_null() {
817        let node_type = n.type_;
818        if node_type == XML_TEXT_NODE as c_int
819            || node_type == XML_CDATA_SECTION_NODE as c_int
820            || node_type == XML_COMMENT_NODE as c_int
821            || node_type == XML_PI_NODE as c_int
822        {
823            let inline_addr = std::ptr::addr_of_mut!((*node).properties) as *const c_void;
824            if n.content as *const c_void != inline_addr {
825                allocator::xmlFreeImpl(n.content as *mut c_void);
826            }
827        }
828    }
829
830    allocator::xmlFreeImpl(node as *mut c_void);
831}
832
833/// Free a linked list of nodes.
834///
835/// Frees all nodes in the list and their children recursively.
836///
837/// # UPSTREAM-PARITY
838///
839/// ```c
840/// void xmlFreeNodeList(xmlNodePtr node);
841/// ```
842///
843/// Does NOT descend into `XML_ENTITY_REF_NODE` children (their child list
844/// points at the shared entity declaration, owned by the DTD) nor free
845/// `XML_DTD_NODE` children (a DTD node in the list is unlinked, not freed —
846/// xmlFreeDtd owns the subset teardown).
847///
848/// # SAFETY
849///
850/// - `node` must be a valid pointer to an _xmlNode, or NULL.
851pub unsafe fn free_node_list(node: *mut _xmlNode) {
852    let mut cur = node;
853    while !cur.is_null() {
854        let next = unsafe { (*cur).next };
855        let t = unsafe { (*cur).type_ };
856
857        if t == XML_DTD_NODE as c_int {
858            // UPSTREAM-PARITY: DTD nodes are unlinked but not freed here.
859            unsafe {
860                (*cur).prev = ptr::null_mut();
861                (*cur).next = ptr::null_mut();
862            }
863            cur = next;
864            continue;
865        }
866
867        // Free children recursively (entity-ref children are shared with the
868        // entity declaration and are owned by the DTD).
869        if t != XML_ENTITY_REF_NODE as c_int && !unsafe { (*cur).children }.is_null() {
870            free_node_list(unsafe { (*cur).children });
871        }
872
873        free_node(cur);
874        cur = next;
875    }
876}
877
878/// Free a linked list of properties.
879///
880/// # SAFETY
881///
882/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
883unsafe fn free_prop_list(prop: *mut _xmlAttr) {
884    let mut cur = prop;
885    while !cur.is_null() {
886        let next = unsafe { (*cur).next };
887        free_prop(cur);
888        cur = next;
889    }
890}
891
892/// Free a single attribute (upstream `xmlFreeProp`).
893///
894/// # SAFETY
895///
896/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
897unsafe fn free_prop(prop: *mut _xmlAttr) {
898    if prop.is_null() {
899        return;
900    }
901
902    // Free children (text nodes with value)
903    if !unsafe { (*prop).children }.is_null() {
904        free_node_list(unsafe { (*prop).children });
905    }
906
907    // Free name
908    if !unsafe { (*prop).name }.is_null() {
909        allocator::xmlFreeImpl(unsafe { (*prop).name } as *mut c_void);
910    }
911
912    allocator::xmlFreeImpl(prop as *mut c_void);
913}
914
915/// Free a linked list of namespace declarations.
916///
917/// # SAFETY
918///
919/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
920unsafe fn free_ns_list(ns: *mut _xmlNs) {
921    let mut cur = ns;
922    while !cur.is_null() {
923        let next = unsafe { (*cur).next };
924        free_ns(cur);
925        cur = next;
926    }
927}
928
929/// Free a single namespace declaration (upstream `xmlFreeNs`).
930///
931/// # SAFETY
932///
933/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
934unsafe fn free_ns(ns: *mut _xmlNs) {
935    if ns.is_null() {
936        return;
937    }
938
939    // Free href and prefix
940    if !unsafe { (*ns).href }.is_null() {
941        allocator::xmlFreeImpl(unsafe { (*ns).href } as *mut c_void);
942    }
943    if !unsafe { (*ns).prefix }.is_null() {
944        allocator::xmlFreeImpl(unsafe { (*ns).prefix } as *mut c_void);
945    }
946
947    allocator::xmlFreeImpl(ns as *mut c_void);
948}
949
950/// Copy a node (shallow or deep).
951///
952/// # UPSTREAM-PARITY
953///
954/// ```c
955/// xmlNodePtr xmlCopyNode(xmlNodePtr node, int recursive);
956/// ```
957///
958/// If `recursive` is 1, children are also copied.
959/// Returns the new node, or NULL on failure.
960///
961/// # SAFETY
962///
963/// - `node` must be a valid pointer to an _xmlNode, or NULL.
964pub unsafe fn copy_node(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
965    if node.is_null() {
966        return ptr::null_mut();
967    }
968
969    let n = unsafe { &*node };
970
971    let new_node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
972    if new_node.is_null() {
973        return ptr::null_mut();
974    }
975
976    unsafe {
977        (*new_node).type_ = n.type_;
978        (*new_node).name = dup_xml_str(n.name);
979        // UPSTREAM-PARITY (tree.c xmlStaticCopyNode): the line number is
980        // copied for element nodes only; text/CDATA/comment/PI copies keep
981        // line 0.
982        if n.type_ == XML_ELEMENT_NODE as c_int {
983            (*new_node).line = n.line;
984        }
985        (*new_node).extra = n.extra;
986        (*new_node).psvi = n.psvi;
987        (*new_node)._private = n._private;
988
989        // Copy namespace pointer (NOT the ns declaration — just the reference)
990        (*new_node).ns = n.ns;
991
992        // Copy namespace declarations (element nodes only; compact text nodes
993        // store inline content over the `properties`/`nsDef` fields).
994        let is_element = n.type_ == XML_ELEMENT_NODE as c_int;
995        if is_element && !n.nsDef.is_null() {
996            (*new_node).nsDef = copy_ns_list(n.nsDef);
997        }
998
999        // Copy content for text/CDATA/comment/PI nodes
1000        let node_type = n.type_;
1001        if (node_type == XML_TEXT_NODE as c_int
1002            || node_type == XML_CDATA_SECTION_NODE as c_int
1003            || node_type == XML_COMMENT_NODE as c_int
1004            || node_type == XML_PI_NODE as c_int)
1005            && !n.content.is_null()
1006        {
1007            (*new_node).content = dup_xml_str(n.content);
1008        }
1009
1010        // Copy properties (element nodes only).
1011        if is_element && !n.properties.is_null() {
1012            (*new_node).properties = copy_prop_list(n.properties);
1013            // Update doc links on properties
1014            let mut prop = (*new_node).properties;
1015            while !prop.is_null() {
1016                (*prop).parent = new_node;
1017                if !(*prop).children.is_null() {
1018                    propagate_doc((*prop).children, (*new_node).doc);
1019                }
1020                prop = (*prop).next;
1021            }
1022        }
1023
1024        // Copy children if recursive (each copied child gets its parent and
1025        // document pointers; `last` follows the upstream link order).
1026        if recursive != 0 && !n.children.is_null() {
1027            (*new_node).children = copy_node_list(n.children, recursive);
1028            if !(*new_node).children.is_null() {
1029                let mut child = (*new_node).children;
1030                let mut last_child = child;
1031                while !child.is_null() {
1032                    (*child).parent = new_node;
1033                    (*child).doc = (*new_node).doc;
1034                    propagate_doc(child, (*new_node).doc);
1035                    if (*child).next.is_null() {
1036                        last_child = child;
1037                    }
1038                    child = (*child).next;
1039                }
1040                (*new_node).last = last_child;
1041            }
1042        }
1043    }
1044
1045    new_node
1046}
1047
1048/// Copy a linked list of nodes.
1049///
1050/// Returns the first node of the new list, or NULL on failure.
1051///
1052/// # Safety
1053///
1054/// - `node` must be NULL or a valid pointer to an `_xmlNode`; when non-NULL it
1055///   is copied with `copy_node` and its `next` chain is walked, so every node
1056///   reachable through `next` must be valid and alive.
1057/// - `recursive` is forwarded to `copy_node` and selects deep versus shallow
1058///   copy; deep copies require valid `children` subtrees.
1059unsafe fn copy_node_list(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
1060    if node.is_null() {
1061        return ptr::null_mut();
1062    }
1063
1064    let n = unsafe { &*node };
1065    let new_node = copy_node(node, recursive);
1066    if new_node.is_null() {
1067        return ptr::null_mut();
1068    }
1069
1070    let mut prev = new_node;
1071    let mut cur = n.next;
1072
1073    while !cur.is_null() {
1074        let new_cur = copy_node(cur, recursive);
1075        if new_cur.is_null() {
1076            break;
1077        }
1078        unsafe {
1079            (*prev).next = new_cur;
1080            (*new_cur).prev = prev;
1081        }
1082        prev = new_cur;
1083        cur = unsafe { (*cur).next };
1084    }
1085
1086    new_node
1087}
1088
1089/// Copy a linked list of namespace declarations.
1090///
1091/// # Safety
1092///
1093/// - `ns` must be NULL or a valid pointer to an `_xmlNs`; when non-NULL its
1094///   fields are read and its `next` chain is walked, so every reachable
1095///   `_xmlNs` must be valid and alive.
1096/// - `href` and `prefix` of each entry may be NULL or NUL-terminated `xmlChar`
1097///   strings; `dup_xml_str` reads them as C strings.
1098unsafe fn copy_ns_list(ns: *const _xmlNs) -> *mut _xmlNs {
1099    if ns.is_null() {
1100        return ptr::null_mut();
1101    }
1102
1103    let n = unsafe { &*ns };
1104    let new_ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1105    if new_ns.is_null() {
1106        return ptr::null_mut();
1107    }
1108
1109    unsafe {
1110        (*new_ns).type_ = n.type_;
1111        (*new_ns).href = dup_xml_str(n.href);
1112        (*new_ns).prefix = dup_xml_str(n.prefix);
1113        (*new_ns)._private = n._private;
1114    }
1115
1116    let mut prev = new_ns;
1117    let mut cur = n.next;
1118
1119    while !cur.is_null() {
1120        let c = unsafe { &*cur };
1121        let new_cur = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1122        if new_cur.is_null() {
1123            break;
1124        }
1125        unsafe {
1126            (*new_cur).type_ = c.type_;
1127            (*new_cur).href = dup_xml_str(c.href);
1128            (*new_cur).prefix = dup_xml_str(c.prefix);
1129            (*new_cur)._private = c._private;
1130            (*prev).next = new_cur;
1131        }
1132        prev = new_cur;
1133        cur = c.next;
1134    }
1135
1136    new_ns
1137}
1138
1139/// Copy a linked list of properties.
1140///
1141/// # Safety
1142///
1143/// - `prop` must be NULL or a valid pointer to an `_xmlAttr`; its fields are
1144///   read and its `next` chain is walked, so every reachable `_xmlAttr` must
1145///   be valid and alive.
1146/// - `children` of each attribute must be NULL or a valid node list; it is
1147///   copied recursively via `copy_node_list`. `name` may be NULL or a
1148///   NUL-terminated `xmlChar` string read by `dup_xml_str`.
1149unsafe fn copy_prop_list(prop: *const _xmlAttr) -> *mut _xmlAttr {
1150    if prop.is_null() {
1151        return ptr::null_mut();
1152    }
1153
1154    let p = unsafe { &*prop };
1155    let new_prop = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1156    if new_prop.is_null() {
1157        return ptr::null_mut();
1158    }
1159
1160    unsafe {
1161        (*new_prop).type_ = p.type_;
1162        (*new_prop).name = dup_xml_str(p.name);
1163        (*new_prop).ns = p.ns;
1164        (*new_prop).atype = p.atype;
1165
1166        // Copy children (text value nodes)
1167        if !p.children.is_null() {
1168            (*new_prop).children = copy_node_list(p.children, 1);
1169            if !(*new_prop).children.is_null() {
1170                (*(*new_prop).children).parent = new_prop as *mut _xmlNode;
1171            }
1172        }
1173    }
1174
1175    let mut prev = new_prop;
1176    let mut cur = p.next;
1177
1178    while !cur.is_null() {
1179        let c = unsafe { &*cur };
1180        let new_cur = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1181        if new_cur.is_null() {
1182            break;
1183        }
1184        unsafe {
1185            (*new_cur).type_ = c.type_;
1186            (*new_cur).name = dup_xml_str(c.name);
1187            (*new_cur).ns = c.ns;
1188            (*new_cur).atype = c.atype;
1189
1190            if !c.children.is_null() {
1191                (*new_cur).children = copy_node_list(c.children, 1);
1192                if !(*new_cur).children.is_null() {
1193                    (*(*new_cur).children).parent = new_cur as *mut _xmlNode;
1194                }
1195            }
1196
1197            (*prev).next = new_cur;
1198        }
1199        prev = new_cur;
1200        cur = c.next;
1201    }
1202
1203    new_prop
1204}
1205
1206/// Propagate the document pointer to all descendants of a node.
1207///
1208/// # Safety
1209///
1210/// - `node` must be NULL or a valid pointer to an `_xmlNode`; the function
1211///   walks the whole subtree through `properties`, `children`, and `next`
1212///   links, so every reachable node and attribute must be a valid, live
1213///   struct.
1214/// - `doc` may be NULL or a valid pointer to an `_xmlDoc`; it is only stored
1215///   into `doc` fields, never dereferenced.
1216unsafe fn propagate_doc(node: *mut _xmlNode, doc: *mut _xmlDoc) {
1217    let mut cur = node;
1218    while !cur.is_null() {
1219        unsafe {
1220            (*cur).doc = doc;
1221
1222            // Propagate to properties (element nodes only; other node types
1223            // never carry properties, and compact text nodes store inline
1224            // content at the `properties` field address).
1225            if (*cur).type_ == XML_ELEMENT_NODE as c_int {
1226                let mut prop = (*cur).properties;
1227                while !prop.is_null() {
1228                    (*prop).doc = doc;
1229                    if !(*prop).children.is_null() {
1230                        propagate_doc((*prop).children, doc);
1231                    }
1232                    prop = (*prop).next;
1233                }
1234            }
1235
1236            // Recurse into children
1237            if !(*cur).children.is_null() {
1238                propagate_doc((*cur).children, doc);
1239            }
1240        }
1241        cur = unsafe { (*cur).next };
1242    }
1243}
1244
1245/// Unlink a node from its parent/siblings.
1246///
1247/// # UPSTREAM-PARITY
1248///
1249/// ```c
1250/// void xmlUnlinkNode(xmlNodePtr node);
1251/// ```
1252///
1253/// Removes the node from its parent's child list and sibling list.
1254/// The node's parent, prev, and next pointers are cleared.
1255/// The node is NOT freed — the caller is responsible for freeing it.
1256///
1257/// # SAFETY
1258///
1259/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1260pub unsafe fn unlink_node(node: *mut _xmlNode) {
1261    if node.is_null() {
1262        return;
1263    }
1264
1265    let n = unsafe { &mut *node };
1266
1267    // Fix up prev/next chain
1268    let prev = n.prev;
1269    let next = n.next;
1270
1271    if !prev.is_null() {
1272        unsafe { (*prev).next = next };
1273    }
1274    if !next.is_null() {
1275        unsafe { (*next).prev = prev };
1276    }
1277
1278    // Fix up parent's children/last pointers
1279    let parent = n.parent;
1280    if !parent.is_null() {
1281        if unsafe { (*parent).children } == node {
1282            unsafe { (*parent).children = next };
1283        }
1284        if unsafe { (*parent).last } == node {
1285            unsafe { (*parent).last = prev };
1286        }
1287    }
1288
1289    // Also fix up doc-level children/last if node is a direct doc child
1290    let doc = n.doc;
1291    if !doc.is_null() && !parent.is_null() {
1292        // Already handled above
1293    }
1294    if !doc.is_null() && parent.is_null() {
1295        // Node is a direct child of the document
1296        if unsafe { (*doc).children } == node {
1297            unsafe { (*doc).children = next };
1298        }
1299        if unsafe { (*doc).last } == node {
1300            unsafe { (*doc).last = prev };
1301        }
1302    }
1303
1304    // Clear the node's links
1305    n.parent = ptr::null_mut();
1306    n.prev = ptr::null_mut();
1307    n.next = ptr::null_mut();
1308}
1309
1310/// Add a child node to a parent.
1311///
1312/// # UPSTREAM-PARITY
1313///
1314/// ```c
1315/// xmlNodePtr xmlAddChild(xmlNodePtr parent, xmlNodePtr cur);
1316/// ```
1317///
1318/// Adds `cur` as the last child of `parent`.
1319/// Returns the child, or NULL on failure.
1320///
1321/// # SAFETY
1322///
1323/// - `parent` must be a valid pointer to an _xmlNode.
1324/// - `cur` must be a valid pointer to an _xmlNode.
1325pub unsafe fn add_child(parent: *mut _xmlNode, cur: *mut _xmlNode) -> *mut _xmlNode {
1326    if parent.is_null() || cur.is_null() {
1327        return ptr::null_mut();
1328    }
1329
1330    let p = unsafe { &mut *parent };
1331    let c = unsafe { &mut *cur };
1332
1333    // If cur is already linked, unlink it first
1334    if !c.parent.is_null() || !c.prev.is_null() || !c.next.is_null() {
1335        unlink_node(cur);
1336    }
1337
1338    // Update parent/child links
1339    c.parent = parent;
1340
1341    if p.children.is_null() {
1342        // First child
1343        p.children = cur;
1344        p.last = cur;
1345        c.prev = ptr::null_mut();
1346        c.next = ptr::null_mut();
1347    } else {
1348        // Append to end
1349        c.prev = p.last;
1350        c.next = ptr::null_mut();
1351        if !p.last.is_null() {
1352            unsafe { (*p.last).next = cur };
1353        }
1354        p.last = cur;
1355    }
1356
1357    // Update doc
1358    let doc = if !p.doc.is_null() {
1359        p.doc
1360    } else {
1361        ptr::null_mut()
1362    };
1363    if !doc.is_null() && c.doc != doc {
1364        propagate_doc(cur, doc);
1365    }
1366
1367    cur
1368}
1369
1370/// Add a sibling node after another.
1371///
1372/// # UPSTREAM-PARITY
1373///
1374/// ```c
1375/// xmlNodePtr xmlAddSibling(xmlNodePtr cur, xmlNodePtr elem);
1376/// ```
1377///
1378/// Adds `elem` as the next sibling of `cur`.
1379/// Returns `elem`, or NULL on failure.
1380///
1381/// # SAFETY
1382///
1383/// - `cur` must be a valid pointer to an _xmlNode.
1384/// - `elem` must be a valid pointer to an _xmlNode.
1385pub unsafe fn add_sibling(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
1386    if cur.is_null() || elem.is_null() {
1387        return ptr::null_mut();
1388    }
1389
1390    let c = unsafe { &mut *cur };
1391
1392    // If elem is already linked, unlink it first
1393    let e = unsafe { &mut *elem };
1394    if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
1395        unlink_node(elem);
1396    }
1397
1398    // Set parent
1399    e.parent = c.parent;
1400
1401    // Link elem after cur
1402    e.prev = cur;
1403    e.next = c.next;
1404
1405    if !c.next.is_null() {
1406        unsafe { (*c.next).prev = elem };
1407    }
1408    c.next = elem;
1409
1410    // Update parent's last if needed
1411    let parent = c.parent;
1412    if !parent.is_null() && unsafe { (*parent).last } == cur {
1413        unsafe { (*parent).last = elem };
1414    }
1415
1416    // Update doc
1417    if !c.doc.is_null() && e.doc != c.doc {
1418        propagate_doc(elem, c.doc);
1419    }
1420
1421    elem
1422}
1423
1424/// Add a sibling node before another.
1425///
1426/// # UPSTREAM-PARITY
1427///
1428/// ```c
1429/// xmlNodePtr xmlAddPrevSibling(xmlNodePtr cur, xmlNodePtr elem);
1430/// ```
1431///
1432/// Adds `elem` as the previous sibling of `cur`.
1433/// Returns `elem`, or NULL on failure.
1434///
1435/// # SAFETY
1436///
1437/// - `cur` must be a valid pointer to an _xmlNode.
1438/// - `elem` must be a valid pointer to an _xmlNode.
1439pub unsafe fn add_sibling_before(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
1440    if cur.is_null() || elem.is_null() {
1441        return ptr::null_mut();
1442    }
1443
1444    let c = unsafe { &mut *cur };
1445
1446    // If elem is already linked, unlink it first
1447    let e = unsafe { &mut *elem };
1448    if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
1449        unlink_node(elem);
1450    }
1451
1452    // Set parent
1453    e.parent = c.parent;
1454
1455    // Link elem before cur
1456    e.prev = c.prev;
1457    e.next = cur;
1458
1459    if !c.prev.is_null() {
1460        unsafe { (*c.prev).next = elem };
1461    }
1462    c.prev = elem;
1463
1464    // Update parent's first if needed
1465    let parent = c.parent;
1466    if !parent.is_null() && unsafe { (*parent).children } == cur {
1467        unsafe { (*parent).children = elem };
1468    }
1469
1470    // Update doc-level children if node is a direct doc child
1471    let doc = c.doc;
1472    if !doc.is_null() && parent.is_null() && unsafe { (*doc).children } == cur {
1473        unsafe { (*doc).children = elem };
1474    }
1475
1476    // Update doc
1477    if !c.doc.is_null() && e.doc != c.doc {
1478        propagate_doc(elem, c.doc);
1479    }
1480
1481    elem
1482}
1483
1484/// Create a new child element.
1485///
1486/// # UPSTREAM-PARITY
1487///
1488/// ```c
1489/// xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns, const xmlChar *name);
1490/// ```
1491///
1492/// Creates a new element and adds it as the last child of `parent`.
1493///
1494/// # SAFETY
1495///
1496/// - `parent` must be a valid pointer to an _xmlNode, or NULL.
1497/// - `name` must be a valid null-terminated string or NULL.
1498pub unsafe fn new_child(
1499    parent: *mut _xmlNode,
1500    ns: *mut _xmlNs,
1501    name: *const xmlChar,
1502) -> *mut _xmlNode {
1503    let node = new_node(ns, name);
1504    if node.is_null() {
1505        return ptr::null_mut();
1506    }
1507
1508    if !parent.is_null() {
1509        add_child(parent, node);
1510    }
1511
1512    node
1513}
1514
1515// ═══════════════════════════════════════════════════════════════════════════════
1516// Text / Content Nodes
1517// ═══════════════════════════════════════════════════════════════════════════════
1518
1519/// Create a new text node.
1520///
1521/// # UPSTREAM-PARITY
1522///
1523/// ```c
1524/// xmlNodePtr xmlNewText(const xmlChar *content);
1525/// ```
1526///
1527/// Creates a text node with the given content.
1528/// If content is NULL, creates an empty text node.
1529///
1530/// # SAFETY
1531///
1532/// - `content` must be a valid null-terminated string or NULL.
1533pub unsafe fn new_text(content: *const xmlChar) -> *mut _xmlNode {
1534    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1535    if node.is_null() {
1536        return ptr::null_mut();
1537    }
1538
1539    unsafe {
1540        (*node).type_ = XML_TEXT_NODE as c_int;
1541        (*node).name = dup_xml_str(b"text\0" as *const u8 as *const xmlChar);
1542        (*node).content = if content.is_null() {
1543            let empty = allocator::xmlMallocImpl(1) as *mut xmlChar;
1544            if !empty.is_null() {
1545                *empty = 0;
1546            }
1547            empty
1548        } else {
1549            dup_xml_str(content)
1550        };
1551        (*node).line = 0;
1552    }
1553
1554    // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
1555    // node is fully initialised.
1556    crate::abi::data_globals::register_node_hook(node);
1557
1558    node
1559}
1560
1561/// Create a new comment node.
1562///
1563/// # UPSTREAM-PARITY
1564///
1565/// ```c
1566/// xmlNodePtr xmlNewComment(const xmlChar *content);
1567/// ```
1568///
1569/// Creates a comment node with the given content.
1570///
1571/// # SAFETY
1572///
1573/// - `content` must be a valid null-terminated string or NULL.
1574pub unsafe fn new_comment(content: *const xmlChar) -> *mut _xmlNode {
1575    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1576    if node.is_null() {
1577        return ptr::null_mut();
1578    }
1579
1580    unsafe {
1581        (*node).type_ = XML_COMMENT_NODE as c_int;
1582        (*node).name = dup_xml_str(b"comment\0" as *const u8 as *const xmlChar);
1583        (*node).content = dup_xml_str(content);
1584        (*node).line = 0;
1585    }
1586
1587    // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
1588    // node is fully initialised.
1589    crate::abi::data_globals::register_node_hook(node);
1590
1591    node
1592}
1593
1594/// Create a new processing instruction node.
1595///
1596/// # UPSTREAM-PARITY
1597///
1598/// ```c
1599/// xmlNodePtr xmlNewPI(const xmlChar *name, const xmlChar *content);
1600/// ```
1601///
1602/// Creates a PI node with the given target name and content.
1603///
1604/// # SAFETY
1605///
1606/// - `name` must be a valid null-terminated string.
1607/// - `content` must be a valid null-terminated string or NULL.
1608pub unsafe fn new_pi(name: *const xmlChar, content: *const xmlChar) -> *mut _xmlNode {
1609    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1610    if node.is_null() {
1611        return ptr::null_mut();
1612    }
1613
1614    unsafe {
1615        (*node).type_ = XML_PI_NODE as c_int;
1616        (*node).name = dup_xml_str(name);
1617        (*node).content = dup_xml_str(content);
1618        (*node).line = 0;
1619    }
1620
1621    // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
1622    // node is fully initialised.
1623    crate::abi::data_globals::register_node_hook(node);
1624
1625    node
1626}
1627
1628/// Create a new CDATA section node.
1629///
1630/// # UPSTREAM-PARITY
1631///
1632/// ```c
1633/// xmlNodePtr xmlNewCDataBlock(xmlDocPtr doc, const xmlChar *content, int len);
1634/// ```
1635///
1636/// Creates a CDATA section node with the given content.
1637///
1638/// # SAFETY
1639///
1640/// - `doc` may be NULL.
1641/// - `content` must be a valid pointer to a buffer of at least `len` bytes,
1642///   or NULL.
1643pub unsafe fn new_cdata_block(
1644    doc: *mut _xmlDoc,
1645    content: *const xmlChar,
1646    len: c_int,
1647) -> *mut _xmlNode {
1648    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1649    if node.is_null() {
1650        return ptr::null_mut();
1651    }
1652
1653    unsafe {
1654        (*node).type_ = XML_CDATA_SECTION_NODE as c_int;
1655        // UPSTREAM-PARITY (tree.c xmlNewCDataBlock): the name field is left
1656        // NULL (zero-initialised).
1657        (*node).doc = doc;
1658
1659        if !content.is_null() && len > 0 {
1660            (*node).content = allocator::xmlMallocImpl((len + 1) as usize) as *mut xmlChar;
1661            if !(*node).content.is_null() {
1662                ptr::copy_nonoverlapping(content, (*node).content, len as usize);
1663                *((*node).content.add(len as usize)) = 0;
1664            }
1665        } else {
1666            let empty = allocator::xmlMallocImpl(1) as *mut xmlChar;
1667            if !empty.is_null() {
1668                *empty = 0;
1669            }
1670            (*node).content = empty;
1671        }
1672
1673        (*node).line = 0;
1674    }
1675
1676    node
1677}
1678
1679// ═══════════════════════════════════════════════════════════════════════════════
1680// Namespace Operations
1681// ═══════════════════════════════════════════════════════════════════════════════
1682
1683/// Create a new namespace declaration.
1684///
1685/// # UPSTREAM-PARITY
1686///
1687/// ```c
1688/// xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
1689/// ```
1690///
1691/// Creates a new namespace declaration on the given node.
1692/// The namespace is added to the node's nsDef list.
1693///
1694/// If `href` is NULL, the namespace is a default namespace undeclaration.
1695/// If `prefix` is NULL, this is the default namespace (xmlns="...").
1696///
1697/// # SAFETY
1698///
1699/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1700/// - `href` must be a valid null-terminated string or NULL.
1701/// - `prefix` must be a valid null-terminated string or NULL.
1702pub unsafe fn new_ns(
1703    node: *mut _xmlNode,
1704    href: *const xmlChar,
1705    prefix: *const xmlChar,
1706) -> *mut _xmlNs {
1707    let ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1708    if ns.is_null() {
1709        return ptr::null_mut();
1710    }
1711
1712    unsafe {
1713        (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
1714        (*ns).href = dup_xml_str(href);
1715        (*ns).prefix = dup_xml_str(prefix);
1716        (*ns).context = node as *mut _xmlDoc;
1717
1718        // Add to node's nsDef list
1719        if !node.is_null() {
1720            let n = &mut *node;
1721            if n.nsDef.is_null() {
1722                n.nsDef = ns;
1723            } else {
1724                // Append to end
1725                let mut last = n.nsDef;
1726                while !(*last).next.is_null() {
1727                    last = (*last).next;
1728                }
1729                (*last).next = ns;
1730            }
1731        }
1732    }
1733
1734    ns
1735}
1736
1737/// Set the namespace of a node.
1738///
1739/// # UPSTREAM-PARITY
1740///
1741/// ```c
1742/// void xmlSetNs(xmlNodePtr node, xmlNsPtr ns);
1743/// ```
1744///
1745/// # SAFETY
1746///
1747/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1748/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
1749pub unsafe fn set_ns(node: *mut _xmlNode, ns: *mut _xmlNs) {
1750    if node.is_null() {
1751        return;
1752    }
1753    unsafe {
1754        (*node).ns = ns;
1755    }
1756}
1757
1758/// Get a list of namespaces in scope for a node.
1759///
1760/// # UPSTREAM-PARITY
1761///
1762/// ```c
1763/// xmlNsPtr *xmlGetNsList(xmlDocPtr doc, xmlNodePtr node);
1764/// ```
1765///
1766/// Returns a NULL-terminated array of namespace pointers in scope,
1767/// or NULL on failure.
1768///
1769/// # SAFETY
1770///
1771/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1772/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1773pub unsafe fn get_ns_list(_doc: *mut _xmlDoc, node: *mut _xmlNode) -> *mut *mut _xmlNs {
1774    // Phase 1: basic implementation
1775    // A more complete implementation would walk the node's ancestors
1776    // and collect all in-scope namespaces.
1777    if node.is_null() {
1778        return ptr::null_mut();
1779    }
1780
1781    // Collect namespaces from this node and ancestors
1782    let mut ns_ptrs: Vec<*mut _xmlNs> = Vec::new();
1783    let mut cur = node;
1784
1785    while !cur.is_null() {
1786        let n = unsafe { &*cur };
1787        let mut ns_def = n.nsDef;
1788        while !ns_def.is_null() {
1789            // Avoid duplicates
1790            let ns = unsafe { &*ns_def };
1791            let mut found = false;
1792            for &existing in &ns_ptrs {
1793                if existing == ns_def {
1794                    found = true;
1795                    break;
1796                }
1797                let e = unsafe { &*existing };
1798                if !ns.href.is_null() && !e.href.is_null() {
1799                    let href_match =
1800                        unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, e.href) != 0 };
1801                    if href_match {
1802                        if ns.prefix.is_null() && e.prefix.is_null() {
1803                            found = true;
1804                            break;
1805                        }
1806                        if !ns.prefix.is_null() && !e.prefix.is_null() {
1807                            let prefix_match = unsafe {
1808                                crate::abi::exports_xml2::xmlStrEqual(ns.prefix, e.prefix) != 0
1809                            };
1810                            if prefix_match {
1811                                found = true;
1812                                break;
1813                            }
1814                        }
1815                    }
1816                }
1817            }
1818            if !found {
1819                ns_ptrs.push(ns_def);
1820            }
1821            ns_def = unsafe { (*ns_def).next };
1822        }
1823        cur = n.parent;
1824    }
1825
1826    if ns_ptrs.is_empty() {
1827        return ptr::null_mut();
1828    }
1829
1830    // Allocate NULL-terminated array
1831    let arr = allocator::xmlMallocImpl((ns_ptrs.len() + 1) * size_of::<*mut _xmlNs>())
1832        as *mut *mut _xmlNs;
1833    if arr.is_null() {
1834        return ptr::null_mut();
1835    }
1836
1837    for (i, ns) in ns_ptrs.iter().enumerate() {
1838        unsafe { *arr.add(i) = *ns };
1839    }
1840    unsafe { *arr.add(ns_ptrs.len()) = ptr::null_mut() };
1841
1842    arr
1843}
1844
1845/// Search for a namespace by prefix.
1846///
1847/// # UPSTREAM-PARITY
1848///
1849/// ```c
1850/// xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace);
1851/// ```
1852///
1853/// Searches for a namespace declaration with the given prefix.
1854/// If `nameSpace` is NULL, searches for the default namespace.
1855///
1856/// # SAFETY
1857///
1858/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1859/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1860/// - `nameSpace` must be a valid null-terminated string or NULL.
1861pub unsafe fn search_ns(
1862    _doc: *mut _xmlDoc,
1863    node: *mut _xmlNode,
1864    name_space: *const xmlChar,
1865) -> *mut _xmlNs {
1866    if node.is_null() {
1867        return ptr::null_mut();
1868    }
1869
1870    let mut cur = node;
1871    while !cur.is_null() {
1872        let n = unsafe { &*cur };
1873        let mut ns_def = n.nsDef;
1874        while !ns_def.is_null() {
1875            let ns = unsafe { &*ns_def };
1876            let match_prefix = if name_space.is_null() {
1877                // Default namespace: prefix should be NULL
1878                ns.prefix.is_null()
1879            } else {
1880                !ns.prefix.is_null()
1881                    && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, name_space) != 0 }
1882            };
1883            if match_prefix {
1884                return ns_def;
1885            }
1886            ns_def = unsafe { (*ns_def).next };
1887        }
1888        cur = n.parent;
1889    }
1890
1891    ptr::null_mut()
1892}
1893
1894/// Search for a namespace by href (URI).
1895///
1896/// # UPSTREAM-PARITY
1897///
1898/// ```c
1899/// xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar *href);
1900/// ```
1901///
1902/// Searches for a namespace declaration with the given URI.
1903///
1904/// # SAFETY
1905///
1906/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1907/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1908/// - `href` must be a valid null-terminated string or NULL.
1909pub unsafe fn search_ns_by_href(
1910    _doc: *mut _xmlDoc,
1911    node: *mut _xmlNode,
1912    href: *const xmlChar,
1913) -> *mut _xmlNs {
1914    if node.is_null() || href.is_null() {
1915        return ptr::null_mut();
1916    }
1917
1918    let mut cur = node;
1919    while !cur.is_null() {
1920        let n = unsafe { &*cur };
1921        let mut ns_def = n.nsDef;
1922        while !ns_def.is_null() {
1923            let ns = unsafe { &*ns_def };
1924            if !ns.href.is_null()
1925                && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, href) != 0 }
1926            {
1927                return ns_def;
1928            }
1929            ns_def = unsafe { (*ns_def).next };
1930        }
1931        cur = n.parent;
1932    }
1933
1934    ptr::null_mut()
1935}
1936
1937// ═══════════════════════════════════════════════════════════════════════════════
1938// Attribute Operations
1939// ═══════════════════════════════════════════════════════════════════════════════
1940
1941/// Set an attribute on a node.
1942///
1943/// # UPSTREAM-PARITY
1944///
1945/// ```c
1946/// xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name, const xmlChar *value);
1947/// ```
1948///
1949/// Sets the attribute with the given name to the given value.
1950/// If the attribute already exists, its value is updated.
1951/// Creates the attribute if it doesn't exist.
1952///
1953/// Returns the attribute pointer, or NULL on failure.
1954///
1955/// # SAFETY
1956///
1957/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1958/// - `name` must be a valid null-terminated string.
1959/// - `value` must be a valid null-terminated string or NULL.
1960pub unsafe fn set_prop(
1961    node: *mut _xmlNode,
1962    name: *const xmlChar,
1963    value: *const xmlChar,
1964) -> *mut _xmlAttr {
1965    if node.is_null() || name.is_null() {
1966        return ptr::null_mut();
1967    }
1968
1969    let n = unsafe { &mut *node };
1970
1971    // Check if attribute already exists
1972    let mut existing = n.properties;
1973    while !existing.is_null() {
1974        let attr = unsafe { &*existing };
1975        if !attr.name.is_null()
1976            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
1977        {
1978            // Update existing attribute value
1979            // Free old children (text nodes)
1980            if !attr.children.is_null() {
1981                free_node_list(attr.children);
1982                // SAFETY: We need to mutate const fields
1983                let attr_mut = existing;
1984                unsafe { (*attr_mut).children = ptr::null_mut() };
1985                unsafe { (*attr_mut).last = ptr::null_mut() };
1986            }
1987            // Set new value
1988            if !value.is_null() {
1989                let text = new_text(value);
1990                if !text.is_null() {
1991                    let attr_mut = existing;
1992                    unsafe {
1993                        (*attr_mut).children = text;
1994                        (*attr_mut).last = text;
1995                        (*text).parent = existing as *mut _xmlNode;
1996                        (*text).doc = n.doc;
1997                    }
1998                }
1999            }
2000            return existing;
2001        }
2002        existing = unsafe { (*existing).next };
2003    }
2004
2005    // Create new attribute
2006    let attr = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
2007    if attr.is_null() {
2008        return ptr::null_mut();
2009    }
2010
2011    unsafe {
2012        (*attr).type_ = XML_ATTRIBUTE_NODE as c_int;
2013        (*attr).name = dup_xml_str(name);
2014        (*attr).parent = node;
2015        (*attr).doc = n.doc;
2016        // UPSTREAM-PARITY (tree.c xmlNewProp): atype stays 0 for instance
2017        // attributes.
2018
2019        // Set value
2020        if !value.is_null() {
2021            let text = new_text(value);
2022            if !text.is_null() {
2023                (*attr).children = text;
2024                (*attr).last = text;
2025                (*text).parent = attr as *mut _xmlNode;
2026                (*text).doc = n.doc;
2027            }
2028        }
2029
2030        // Add to node's property list
2031        if n.properties.is_null() {
2032            n.properties = attr;
2033        } else {
2034            let mut last = n.properties;
2035            while !(*last).next.is_null() {
2036                last = (*last).next;
2037            }
2038            (*last).next = attr;
2039            (*attr).prev = last;
2040        }
2041    }
2042
2043    attr
2044}
2045
2046/// Get an attribute value by name.
2047///
2048/// # UPSTREAM-PARITY
2049///
2050/// ```c
2051/// xmlChar *xmlGetProp(xmlNodePtr node, const xmlChar *name);
2052/// ```
2053///
2054/// Returns the attribute value as an xmlChar* (caller must free with xmlFree),
2055/// or NULL if the attribute doesn't exist.
2056///
2057/// # SAFETY
2058///
2059/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2060/// - `name` must be a valid null-terminated string.
2061pub unsafe fn get_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut xmlChar {
2062    if node.is_null() || name.is_null() {
2063        return ptr::null_mut();
2064    }
2065
2066    let n = unsafe { &*node };
2067    let mut cur = n.properties;
2068
2069    while !cur.is_null() {
2070        let attr = unsafe { &*cur };
2071        if !attr.name.is_null()
2072            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
2073        {
2074            // Get the text content of the attribute
2075            if !attr.children.is_null() {
2076                let text = unsafe { &*attr.children };
2077                if text.type_ == XML_TEXT_NODE as c_int && !text.content.is_null() {
2078                    return dup_xml_str(text.content);
2079                }
2080            }
2081            return dup_xml_str(b"\0" as *const u8 as *const xmlChar);
2082        }
2083        cur = unsafe { (*cur).next };
2084    }
2085
2086    ptr::null_mut()
2087}
2088
2089/// Get a namespaced attribute value.
2090///
2091/// # UPSTREAM-PARITY
2092///
2093/// ```c
2094/// xmlChar *xmlGetNsProp(xmlNodePtr node, const xmlChar *name, const xmlChar *nameSpace);
2095/// ```
2096///
2097/// Returns the attribute value, or NULL if not found.
2098///
2099/// # SAFETY
2100///
2101/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2102/// - `name` must be a valid null-terminated string.
2103/// - `nameSpace` may be NULL.
2104pub unsafe fn get_ns_prop(
2105    node: *mut _xmlNode,
2106    name: *const xmlChar,
2107    _name_space: *const xmlChar,
2108) -> *mut xmlChar {
2109    // Phase 1: simple attribute lookup (namespace-aware lookup will be
2110    // fully implemented in Phase 2+).
2111    get_prop(node, name)
2112}
2113
2114/// Set a namespaced attribute.
2115///
2116/// # UPSTREAM-PARITY
2117///
2118/// ```c
2119/// xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns, const xmlChar *name, const xmlChar *value);
2120/// ```
2121///
2122/// # SAFETY
2123///
2124/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2125/// - `ns` may be NULL.
2126/// - `name` must be a valid null-terminated string.
2127/// - `value` must be a valid null-terminated string or NULL.
2128pub unsafe fn set_ns_prop(
2129    node: *mut _xmlNode,
2130    _ns: *mut _xmlNs,
2131    name: *const xmlChar,
2132    value: *const xmlChar,
2133) -> *mut _xmlAttr {
2134    // Phase 1: use xmlSetProp (namespace-aware version will be in Phase 2+).
2135    set_prop(node, name, value)
2136}
2137
2138/// Remove a property from a node.
2139///
2140/// # UPSTREAM-PARITY
2141///
2142/// ```c
2143/// int xmlRemoveProp(xmlAttrPtr attr);
2144/// ```
2145///
2146/// Removes the attribute from its parent node and frees it.
2147/// Returns 0 on success, -1 on failure.
2148///
2149/// # SAFETY
2150///
2151/// - `attr` must be a valid pointer to an _xmlAttr, or NULL.
2152pub unsafe fn remove_prop(attr: *mut _xmlAttr) -> c_int {
2153    if attr.is_null() {
2154        return -1;
2155    }
2156
2157    let a = unsafe { &mut *attr };
2158
2159    // Unlink from parent's property list
2160    let parent = a.parent;
2161    if !parent.is_null() {
2162        let p = unsafe { &mut *parent };
2163        if p.properties == attr {
2164            p.properties = a.next;
2165        }
2166    }
2167
2168    // Fix up prev/next chain
2169    if !a.prev.is_null() {
2170        unsafe { (*a.prev).next = a.next };
2171    }
2172    if !a.next.is_null() {
2173        unsafe { (*a.next).prev = a.prev };
2174    }
2175
2176    // Free children (text value nodes)
2177    if !a.children.is_null() {
2178        free_node_list(a.children);
2179    }
2180
2181    // Free name
2182    if !a.name.is_null() {
2183        allocator::xmlFreeImpl(a.name as *mut c_void);
2184    }
2185
2186    allocator::xmlFreeImpl(attr as *mut c_void);
2187    0
2188}
2189
2190/// Check whether a node has a property with the given name (upstream tree.c
2191/// `xmlHasProp`): returns the attribute pointer or NULL.
2192///
2193/// # SAFETY
2194///
2195/// - `node` must be a valid node pointer or NULL.
2196/// - `name` must be a valid null-terminated string.
2197pub unsafe fn has_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlAttr {
2198    if node.is_null() || name.is_null() {
2199        return ptr::null_mut();
2200    }
2201    let mut cur = unsafe { (*node).properties };
2202    while !cur.is_null() {
2203        let attr = unsafe { &*cur };
2204        if !attr.name.is_null()
2205            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
2206            && attr.ns.is_null()
2207        {
2208            return cur;
2209        }
2210        cur = unsafe { (*cur).next };
2211    }
2212    ptr::null_mut()
2213}
2214
2215/// Check whether a node has a namespaced property (upstream tree.c
2216/// `xmlHasNsProp`): returns the attribute pointer or NULL. A NULL
2217/// `nameSpace` matches the no-namespace case.
2218///
2219/// # SAFETY
2220///
2221/// - `node` must be a valid node pointer or NULL.
2222/// - `name` must be a valid null-terminated string.
2223/// - `nameSpace` may be NULL.
2224pub unsafe fn has_ns_prop(
2225    node: *mut _xmlNode,
2226    name: *const xmlChar,
2227    name_space: *const xmlChar,
2228) -> *mut _xmlAttr {
2229    if node.is_null() || name.is_null() {
2230        return ptr::null_mut();
2231    }
2232    let mut cur = unsafe { (*node).properties };
2233    while !cur.is_null() {
2234        let attr = unsafe { &*cur };
2235        if !attr.name.is_null()
2236            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
2237        {
2238            if name_space.is_null() {
2239                if attr.ns.is_null() {
2240                    return cur;
2241                }
2242            } else if !attr.ns.is_null()
2243                && !(*attr.ns).href.is_null()
2244                && unsafe {
2245                    crate::abi::exports_xml2::xmlStrEqual((*attr.ns).href, name_space) != 0
2246                }
2247            {
2248                return cur;
2249            }
2250        }
2251        cur = unsafe { (*cur).next };
2252    }
2253    ptr::null_mut()
2254}
2255
2256/// Remove a property by name from a node (upstream tree.c `xmlUnsetProp`):
2257/// returns 0 on success, -1 if the property does not exist or arguments are
2258/// NULL.
2259///
2260/// # SAFETY
2261///
2262/// - `node` must be a valid node pointer or NULL.
2263/// - `name` must be a valid null-terminated string.
2264pub unsafe fn unset_prop(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
2265    let attr = unsafe { has_prop(node, name) };
2266    if attr.is_null() {
2267        return -1;
2268    }
2269    unsafe { remove_prop(attr) }
2270}
2271
2272/// Remove a namespaced property by name (upstream tree.c `xmlUnsetNsProp`).
2273///
2274/// # SAFETY
2275///
2276/// - `node` must be a valid node pointer or NULL.
2277/// - `name` must be a valid null-terminated string.
2278/// - `nameSpace` may be NULL.
2279pub unsafe fn unset_ns_prop(
2280    node: *mut _xmlNode,
2281    name: *const xmlChar,
2282    name_space: *const xmlChar,
2283) -> c_int {
2284    let attr = unsafe { has_ns_prop(node, name, name_space) };
2285    if attr.is_null() {
2286        return -1;
2287    }
2288    unsafe { remove_prop(attr) }
2289}
2290
2291/// Return the first child ELEMENT of a node, or NULL (upstream tree.c
2292/// `xmlFirstElementChild`).
2293///
2294/// # SAFETY
2295///
2296/// - `node` must be a valid node pointer or NULL.
2297pub unsafe fn first_element_child(node: *mut _xmlNode) -> *mut _xmlNode {
2298    if node.is_null() {
2299        return ptr::null_mut();
2300    }
2301    let mut cur = unsafe { (*node).children };
2302    while !cur.is_null() {
2303        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2304            return cur;
2305        }
2306        cur = unsafe { (*cur).next };
2307    }
2308    ptr::null_mut()
2309}
2310
2311/// Return the last child ELEMENT of a node, or NULL (upstream tree.c
2312/// `xmlLastElementChild`).
2313///
2314/// # SAFETY
2315///
2316/// - `node` must be a valid node pointer or NULL.
2317pub unsafe fn last_element_child(node: *mut _xmlNode) -> *mut _xmlNode {
2318    if node.is_null() {
2319        return ptr::null_mut();
2320    }
2321    let mut cur = unsafe { (*node).last };
2322    while !cur.is_null() {
2323        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2324            return cur;
2325        }
2326        cur = unsafe { (*cur).prev };
2327    }
2328    ptr::null_mut()
2329}
2330
2331/// Return the next ELEMENT sibling of a node, or NULL (upstream tree.c
2332/// `xmlNextElementSibling`).
2333///
2334/// # SAFETY
2335///
2336/// - `node` must be a valid node pointer or NULL.
2337pub unsafe fn next_element_sibling(node: *mut _xmlNode) -> *mut _xmlNode {
2338    if node.is_null() {
2339        return ptr::null_mut();
2340    }
2341    let mut cur = unsafe { (*node).next };
2342    while !cur.is_null() {
2343        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2344            return cur;
2345        }
2346        cur = unsafe { (*cur).next };
2347    }
2348    ptr::null_mut()
2349}
2350
2351/// Return the previous ELEMENT sibling of a node, or NULL (upstream tree.c
2352/// `xmlPreviousElementSibling`).
2353///
2354/// # SAFETY
2355///
2356/// - `node` must be a valid node pointer or NULL.
2357pub unsafe fn previous_element_sibling(node: *mut _xmlNode) -> *mut _xmlNode {
2358    if node.is_null() {
2359        return ptr::null_mut();
2360    }
2361    let mut cur = unsafe { (*node).prev };
2362    while !cur.is_null() {
2363        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2364            return cur;
2365        }
2366        cur = unsafe { (*cur).prev };
2367    }
2368    ptr::null_mut()
2369}
2370
2371/// Count the child ELEMENT nodes of a node (upstream tree.c
2372/// `xmlChildElementCount`).
2373///
2374/// # SAFETY
2375///
2376/// - `node` must be a valid node pointer or NULL.
2377pub unsafe fn child_element_count(node: *mut _xmlNode) -> c_ulong {
2378    if node.is_null() {
2379        return 0;
2380    }
2381    let mut cur = unsafe { (*node).children };
2382    let mut count: c_ulong = 0;
2383    while !cur.is_null() {
2384        if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2385            count += 1;
2386        }
2387        cur = unsafe { (*cur).next };
2388    }
2389    count
2390}
2391
2392/// Concatenate text to a node's content (upstream tree.c `xmlTextConcat`):
2393/// appends `num` bytes of `str` to the node's text content. Returns 0 on
2394/// success, -1 on error.
2395///
2396/// # SAFETY
2397///
2398/// - `node` must be a valid text node or NULL.
2399/// - `str` must be a valid buffer of `num` bytes.
2400pub unsafe fn text_concat(node: *mut _xmlNode, str: *const xmlChar, num: c_int) -> c_int {
2401    if node.is_null() || str.is_null() || num <= 0 {
2402        return -1;
2403    }
2404    let cur = unsafe { &mut *node };
2405    if cur.content.is_null() {
2406        let p = unsafe { allocator::xmlMallocImpl(num as usize + 1) as *mut xmlChar };
2407        if p.is_null() {
2408            return -1;
2409        }
2410        unsafe {
2411            ptr::copy_nonoverlapping(str, p, num as usize);
2412            *p.add(num as usize) = 0;
2413        }
2414        cur.content = p;
2415        return 0;
2416    }
2417    let old_len = unsafe { crate::xml::string::xml_strlen(cur.content) };
2418    let p = unsafe {
2419        allocator::xmlReallocImpl(cur.content as *mut c_void, old_len + num as usize + 1)
2420            as *mut xmlChar
2421    };
2422    if p.is_null() {
2423        return -1;
2424    }
2425    unsafe {
2426        ptr::copy_nonoverlapping(str, p.add(old_len), num as usize);
2427        *p.add(old_len + num as usize) = 0;
2428    }
2429    cur.content = p;
2430    0
2431}
2432
2433/// Merge the text content of two nodes (upstream tree.c `xmlTextMerge`):
2434/// appends `ntext`'s content to `text`'s content and frees `ntext`.
2435/// Returns the first node, or NULL on error.
2436///
2437/// # SAFETY
2438///
2439/// - `text` and `ntext` must be valid text nodes or NULL.
2440pub unsafe fn text_merge(text: *mut _xmlNode, ntext: *mut _xmlNode) -> *mut _xmlNode {
2441    if text.is_null() || ntext.is_null() {
2442        return ptr::null_mut();
2443    }
2444    if unsafe { (*ntext).content.is_null() } {
2445        unsafe { free_node(ntext) };
2446        return text;
2447    }
2448    let num = unsafe { crate::xml::string::xml_strlen((*ntext).content) };
2449    if unsafe { text_concat(text, (*ntext).content, num as c_int) } != 0 {
2450        return ptr::null_mut();
2451    }
2452    unsafe { free_node(ntext) };
2453    text
2454}
2455
2456// ═══════════════════════════════════════════════════════════════════════════════
2457// DTD Operations
2458// ═══════════════════════════════════════════════════════════════════════════════
2459
2460/// Get the internal DTD subset of a document.
2461///
2462/// # UPSTREAM-PARITY
2463///
2464/// ```c
2465/// xmlDtdPtr xmlGetIntSubset(xmlDocPtr doc);
2466/// ```
2467pub const fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
2468    if doc.is_null() {
2469        return ptr::null_mut();
2470    }
2471    let d = unsafe { &*doc };
2472    d.intSubset
2473}
2474
2475/// Create a new DTD node.
2476///
2477/// # UPSTREAM-PARITY
2478///
2479/// ```c
2480/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
2481///                     const xmlChar *ExternalID, const xmlChar *SystemID);
2482/// ```
2483///
2484/// Creates a new DTD and attaches it to the document.
2485///
2486/// # SAFETY
2487///
2488/// - `doc` must be a valid pointer to an _xmlDoc.
2489/// - `name` must be a valid null-terminated string or NULL.
2490/// - `ExternalID`, `SystemID` may be NULL.
2491pub unsafe fn new_dtd(
2492    doc: *mut _xmlDoc,
2493    name: *const xmlChar,
2494    ExternalID: *const xmlChar,
2495    SystemID: *const xmlChar,
2496) -> *mut _xmlDtd {
2497    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
2498    if dtd.is_null() {
2499        return ptr::null_mut();
2500    }
2501
2502    unsafe {
2503        (*dtd).type_ = XML_DTD_NODE as c_int;
2504        (*dtd).name = dup_xml_str(name);
2505        (*dtd).ExternalID = dup_xml_str(ExternalID);
2506        (*dtd).SystemID = dup_xml_str(SystemID);
2507        (*dtd).parent = doc;
2508        (*dtd).doc = doc;
2509
2510        // UPSTREAM-PARITY (tree.c xmlNewDtd): the declaration hash tables are
2511        // created lazily by the xmlAdd* functions on first use; an empty DTD
2512        // exposes NULL table pointers.
2513
2514        // Attach to document
2515        if !doc.is_null() && (*doc).intSubset.is_null() {
2516            (*doc).intSubset = dtd;
2517        }
2518    }
2519
2520    dtd
2521}
2522
2523/// Free a DTD.
2524///
2525/// # SAFETY
2526///
2527/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
2528unsafe fn free_dtd(dtd: *mut _xmlDtd) {
2529    if dtd.is_null() {
2530        return;
2531    }
2532
2533    let _d = unsafe { &mut *dtd };
2534    let d = &mut *dtd;
2535
2536    // UPSTREAM-PARITY (tree.c xmlFreeDtd): element/attribute/entity
2537    // declaration nodes in the child list are owned by the hash tables and
2538    // are freed by the deallocators below; only non-declaration children
2539    // (comments, PIs) are unlinked and freed from the list here. This must
2540    // run BEFORE the hash tables are freed so the decl nodes are still alive
2541    // when their type is inspected.
2542    if !d.children.is_null() {
2543        let mut c = d.children;
2544        while !c.is_null() {
2545            let next = (*c).next;
2546            let t = (*c).type_;
2547            if t != XML_ELEMENT_DECL as c_int
2548                && t != XML_ATTRIBUTE_DECL as c_int
2549                && t != XML_ENTITY_DECL as c_int
2550            {
2551                unlink_node_internal(c, ptr::null_mut());
2552                free_node(c);
2553            }
2554            c = next;
2555        }
2556    }
2557
2558    // Free name
2559    if !d.name.is_null() {
2560        allocator::xmlFreeImpl(d.name as *mut c_void);
2561    }
2562    if !d.ExternalID.is_null() {
2563        allocator::xmlFreeImpl(d.ExternalID as *mut c_void);
2564    }
2565    if !d.SystemID.is_null() {
2566        allocator::xmlFreeImpl(d.SystemID as *mut c_void);
2567    }
2568
2569    // Free hash tables for declarations
2570    /// Hash-table deallocator shim that frees an `_xmlNotation` payload.
2571    ///
2572    /// # Safety
2573    ///
2574    /// - `payload` must be NULL or a valid pointer to an `_xmlNotation` owned
2575    ///   exclusively by the hash table being freed; it is freed with
2576    ///   `free_notation`.
2577    /// - `_name` is unused.
2578    unsafe extern "C" fn free_notation_wrapper(payload: *mut c_void, _name: *mut u8) {
2579        crate::xml::dtd::free_notation(payload as *mut _xmlNotation);
2580    }
2581    /// Hash-table deallocator shim that frees an `_xmlElement` payload.
2582    ///
2583    /// # Safety
2584    ///
2585    /// - `payload` must be NULL or a valid pointer to an `_xmlElement` owned
2586    ///   exclusively by the hash table being freed; it is freed with
2587    ///   `free_element`.
2588    /// - `_name` is unused.
2589    unsafe extern "C" fn free_element_wrapper(payload: *mut c_void, _name: *mut u8) {
2590        crate::xml::dtd::free_element(payload as *mut _xmlElement);
2591    }
2592    /// Hash-table deallocator shim that frees an `_xmlAttribute` payload.
2593    ///
2594    /// # Safety
2595    ///
2596    /// - `payload` must be NULL or a valid pointer to an `_xmlAttribute` owned
2597    ///   exclusively by the hash table being freed; it is freed with
2598    ///   `free_attribute`.
2599    /// - `_name` is unused.
2600    unsafe extern "C" fn free_attribute_wrapper(payload: *mut c_void, _name: *mut u8) {
2601        crate::xml::dtd::free_attribute(payload as *mut _xmlAttribute);
2602    }
2603    /// Hash-table deallocator shim that frees an `_xmlEntity` payload.
2604    ///
2605    /// # Safety
2606    ///
2607    /// - `payload` must be NULL or a valid pointer to an `_xmlEntity` owned
2608    ///   exclusively by the hash table being freed; it is freed with
2609    ///   `free_entity`.
2610    /// - `_name` is unused.
2611    unsafe extern "C" fn free_entity_wrapper(payload: *mut c_void, _name: *mut u8) {
2612        crate::xml::entities::free_entity(payload as *mut _xmlEntity);
2613    }
2614
2615    if !d.notations.is_null() {
2616        crate::xml::hash::hash_free(
2617            d.notations as *mut crate::xml::hash::HashTable,
2618            Some(free_notation_wrapper),
2619        );
2620        d.notations = ptr::null_mut();
2621    }
2622    if !d.elements.is_null() {
2623        crate::xml::hash::hash_free(
2624            d.elements as *mut crate::xml::hash::HashTable,
2625            Some(free_element_wrapper),
2626        );
2627        d.elements = ptr::null_mut();
2628    }
2629    if !d.attributes.is_null() {
2630        crate::xml::hash::hash_free(
2631            d.attributes as *mut crate::xml::hash::HashTable,
2632            Some(free_attribute_wrapper),
2633        );
2634        d.attributes = ptr::null_mut();
2635    }
2636    if !d.entities.is_null() {
2637        crate::xml::hash::hash_free(
2638            d.entities as *mut crate::xml::hash::HashTable,
2639            Some(free_entity_wrapper),
2640        );
2641        d.entities = ptr::null_mut();
2642    }
2643    if !d.pentities.is_null() {
2644        crate::xml::hash::hash_free(
2645            d.pentities as *mut crate::xml::hash::HashTable,
2646            Some(free_entity_wrapper),
2647        );
2648        d.pentities = ptr::null_mut();
2649    }
2650
2651    allocator::xmlFreeImpl(dtd as *mut c_void);
2652}
2653
2654// ═══════════════════════════════════════════════════════════════════════════════
2655// Entity Operations
2656// ═══════════════════════════════════════════════════════════════════════════════
2657
2658/// Create a new entity.
2659///
2660/// # UPSTREAM-PARITY
2661///
2662/// ```c
2663/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
2664///                           const xmlChar *ExternalID, const xmlChar *SystemID,
2665///                           const xmlChar *content);
2666/// ```
2667///
2668/// # SAFETY
2669///
2670/// - `doc` may be NULL.
2671/// - `name` must be a valid null-terminated string.
2672/// - `ExternalID`, `SystemID`, `content` may be NULL.
2673pub unsafe fn new_entity(
2674    _doc: *mut _xmlDoc,
2675    name: *const xmlChar,
2676    etype: c_int,
2677    ExternalID: *const xmlChar,
2678    SystemID: *const xmlChar,
2679    content: *const xmlChar,
2680) -> *mut _xmlEntity {
2681    let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
2682    if entity.is_null() {
2683        return ptr::null_mut();
2684    }
2685
2686    unsafe {
2687        (*entity).type_ = XML_ENTITY_DECL as c_int;
2688        (*entity).name = dup_xml_str(name);
2689        (*entity).etype = etype;
2690        (*entity).ExternalID = dup_xml_str(ExternalID);
2691        (*entity).SystemID = dup_xml_str(SystemID);
2692        (*entity).content = dup_xml_str(content);
2693        (*entity).length = if content.is_null() {
2694            0
2695        } else {
2696            crate::abi::exports_xml2::xmlStrlen(content)
2697        };
2698        (*entity).flags = 0;
2699        (*entity).expandedSize = 0;
2700    }
2701
2702    entity
2703}
2704
2705/// Get a document entity by name.
2706///
2707/// # UPSTREAM-PARITY
2708///
2709/// ```c
2710/// xmlEntityPtr xmlGetDocEntity(xmlDocPtr doc, const xmlChar *name);
2711/// ```
2712///
2713/// Returns the entity, or NULL if not found.
2714///
2715/// # SAFETY
2716///
2717/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2718/// - `name` must be a valid null-terminated string.
2719pub unsafe fn get_doc_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
2720    crate::xml::entities::get_entity(doc as *mut _xmlDoc, name)
2721}
2722
2723/// Add an entity declaration to the document's internal subset (upstream
2724/// entities.c `xmlAddDocEntity`); creates the internal subset when absent.
2725///
2726/// # SAFETY
2727///
2728/// - `doc` must be a valid document pointer or NULL.
2729/// - `name` must be a valid null-terminated string.
2730pub unsafe fn add_doc_entity(
2731    doc: *mut _xmlDoc,
2732    name: *const xmlChar,
2733    etype: c_int,
2734    ExternalID: *const xmlChar,
2735    SystemID: *const xmlChar,
2736    content: *const xmlChar,
2737) -> *mut _xmlEntity {
2738    if doc.is_null() || name.is_null() {
2739        return ptr::null_mut();
2740    }
2741    unsafe {
2742        let mut dtd = (*doc).intSubset;
2743        if dtd.is_null() {
2744            dtd = new_dtd(
2745                doc,
2746                c"internal".as_ptr() as *const xmlChar,
2747                ptr::null(),
2748                ptr::null(),
2749            );
2750            if dtd.is_null() {
2751                return ptr::null_mut();
2752            }
2753        }
2754        crate::xml::entities::add_entity(dtd, name, etype, ExternalID, SystemID, content)
2755    }
2756}
2757
2758/// Add an entity declaration to the document's external subset (upstream
2759/// entities.c `xmlAddDtdEntity`); creates the external subset when absent.
2760///
2761/// # SAFETY
2762///
2763/// - `doc` must be a valid document pointer or NULL.
2764/// - `name` must be a valid null-terminated string.
2765pub unsafe fn add_dtd_entity(
2766    doc: *mut _xmlDoc,
2767    name: *const xmlChar,
2768    etype: c_int,
2769    ExternalID: *const xmlChar,
2770    SystemID: *const xmlChar,
2771    content: *const xmlChar,
2772) -> *mut _xmlEntity {
2773    if doc.is_null() || name.is_null() {
2774        return ptr::null_mut();
2775    }
2776    unsafe {
2777        let mut dtd = (*doc).extSubset;
2778        if dtd.is_null() {
2779            dtd = new_dtd(
2780                doc,
2781                c"internal".as_ptr() as *const xmlChar,
2782                ptr::null(),
2783                ptr::null(),
2784            );
2785            if dtd.is_null() {
2786                return ptr::null_mut();
2787            }
2788            (*doc).extSubset = dtd;
2789        }
2790        crate::xml::entities::add_entity(dtd, name, etype, ExternalID, SystemID, content)
2791    }
2792}
2793
2794/// Get an entity declaration from the internal or external subset (upstream
2795/// entities.c `xmlGetDtdEntity`).
2796///
2797/// # SAFETY
2798///
2799/// - `doc` must be a valid document pointer or NULL.
2800/// - `name` must be a valid null-terminated string.
2801pub unsafe fn get_dtd_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
2802    if doc.is_null() || name.is_null() {
2803        return ptr::null_mut();
2804    }
2805    unsafe {
2806        if !(*doc).intSubset.is_null() {
2807            let e = crate::xml::entities::get_entity_from_dtd((*doc).intSubset, name);
2808            if !e.is_null() {
2809                return e;
2810            }
2811        }
2812        if !(*doc).extSubset.is_null() {
2813            return crate::xml::entities::get_entity_from_dtd((*doc).extSubset, name);
2814        }
2815        ptr::null_mut()
2816    }
2817}
2818
2819/// Get a parameter entity by name.
2820///
2821/// # UPSTREAM-PARITY
2822///
2823/// ```c
2824/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
2825/// ```
2826///
2827/// # SAFETY
2828///
2829/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2830/// - `name` must be a valid null-terminated string.
2831pub unsafe fn get_parameter_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
2832    crate::xml::entities::get_parameter_entity(doc as *mut _xmlDoc, name)
2833}
2834
2835// ═══════════════════════════════════════════════════════════════════════════════
2836// XML Serialization
2837// ═══════════════════════════════════════════════════════════════════════════════
2838//
2839// Functions for serializing XML document/node trees to text.
2840// All output is UTF-8.
2841
2842/// Entity replacement strings (as xmlChar byte slices).
2843const ENTITY_LT: &[xmlChar] = b"&lt;";
2844const ENTITY_GT: &[xmlChar] = b"&gt;";
2845const ENTITY_AMP: &[xmlChar] = b"&amp;";
2846const ENTITY_QUOT: &[xmlChar] = b"&quot;";
2847#[allow(dead_code)]
2848const ENTITY_APOS: &[xmlChar] = b"&apos;";
2849
2850/// Indentation string (libxml2's default `xmlTreeIndentString`).
2851const INDENT: &[xmlChar] = b"  ";
2852
2853/// Maximum indent buffer size (libxml2 `MAX_INDENT` in xmlsave.c).
2854const MAX_INDENT: c_int = 60;
2855
2856/// Serialize text content with XML escaping.
2857///
2858/// # UPSTREAM-PARITY
2859///
2860/// Mirrors libxml2 2.15 `xmlSerializeText` with default flags (no
2861/// `XML_ESCAPE_NON_ASCII`, i.e. the encoding is non-NULL as in the libxslt
2862/// save path): `<` → `&lt;`, `>` → `&gt;`, `&` → `&amp;`, `\r` → `&#13;`,
2863/// other control characters → hexadecimal character references, while `\n`
2864/// and `\t` are emitted literally and non-ASCII bytes are passed through.
2865///
2866/// # SAFETY
2867///
2868/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2869/// - `content` must be a valid pointer to `len` bytes of xmlChar data, or NULL.
2870pub(crate) unsafe fn serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2871    if buf.is_null() || content.is_null() || len <= 0 {
2872        return;
2873    }
2874
2875    let mut i: c_int = 0;
2876    while i < len {
2877        let ch = unsafe { *content.add(i as usize) };
2878
2879        // Check for `]]>` sequence
2880        if ch == b']'
2881            && i + 2 < len
2882            && unsafe { *content.add(i as usize + 1) == b']' }
2883            && unsafe { *content.add(i as usize + 2) == b'>' }
2884        {
2885            // Write `]]&gt;` — escape the `>` that ends `]]>`
2886            io::buf_add(buf, &ch as *const u8, 2); // write `]]`
2887            io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
2888            i += 3;
2889            continue;
2890        }
2891
2892        match ch {
2893            b'<' => {
2894                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
2895            }
2896            b'&' => {
2897                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
2898            }
2899            b'>' => {
2900                // UPSTREAM-PARITY: libxml2 escapes `>` to `&gt;` in text content.
2901                // While the XML spec only requires escaping `>` in `]]>`, libxml2's
2902                // serializer escapes all `>` characters.
2903                io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
2904            }
2905            b'\r' => {
2906                // Carriage return is not allowed literally in XML content.
2907                io::buf_add(buf, b"&#13;" as *const u8, 5);
2908            }
2909            0x01..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F => {
2910                // Other control characters are emitted as hex character refs.
2911                let hex = format!("&#x{:X};", ch);
2912                io::buf_add(buf, hex.as_ptr(), hex.len() as c_int);
2913            }
2914            _ => {
2915                io::buf_add(buf, &ch as *const u8, 1);
2916            }
2917        }
2918        i += 1;
2919    }
2920}
2921
2922/// Serialize an attribute value with XML escaping.
2923///
2924/// # UPSTREAM-PARITY
2925///
2926/// Mirrors libxml2 `xmlBufAttrSerializeTxtContent` (xmlsave.c):
2927/// `\n` → `&#10;`, `\r` → `&#13;`, `\t` → `&#9;`, `"` → `&quot;`,
2928/// `<` → `&lt;`, `>` → `&gt;`, `&` → `&amp;`.
2929///
2930/// # SAFETY
2931///
2932/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2933/// - `value` must be a valid null-terminated xmlChar string, or NULL.
2934pub(crate) unsafe fn serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2935    if buf.is_null() || value.is_null() {
2936        return;
2937    }
2938
2939    let len = xml_strlen(value);
2940    let mut i: c_int = 0;
2941    while i < len {
2942        let ch = unsafe { *value.add(i as usize) };
2943
2944        match ch {
2945            b'\n' => {
2946                io::buf_add(buf, b"&#10;" as *const u8, 5);
2947            }
2948            b'\r' => {
2949                io::buf_add(buf, b"&#13;" as *const u8, 5);
2950            }
2951            b'\t' => {
2952                io::buf_add(buf, b"&#9;" as *const u8, 4);
2953            }
2954            b'<' => {
2955                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
2956            }
2957            b'&' => {
2958                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
2959            }
2960            b'"' => {
2961                io::buf_add(buf, ENTITY_QUOT.as_ptr(), ENTITY_QUOT.len() as c_int);
2962            }
2963            b'>' => {
2964                io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
2965            }
2966            _ => {
2967                io::buf_add(buf, &ch as *const u8, 1);
2968            }
2969        }
2970        i += 1;
2971    }
2972}
2973
2974/// Write indentation.
2975///
2976/// # UPSTREAM-PARITY
2977///
2978/// Mirrors libxml2 `xmlSaveWriteIndent` (xmlsave.c 2.15): the level is
2979/// capped at `MAX_INDENT / indent_size` (= 30 with the default two-space
2980/// indent string).
2981///
2982/// # SAFETY
2983///
2984/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2985unsafe fn write_indent(
2986    buf: *mut _xmlBuffer,
2987    level: c_int,
2988    indent: *const xmlChar,
2989    indent_len: c_int,
2990) {
2991    if buf.is_null() || level <= 0 || indent.is_null() || indent_len <= 0 {
2992        return;
2993    }
2994    let indent_nr = MAX_INDENT / indent_len;
2995    let mut lvl = level;
2996    if lvl > indent_nr {
2997        lvl = indent_nr;
2998    }
2999    for _ in 0..lvl {
3000        io::buf_add(buf, indent, indent_len);
3001    }
3002}
3003
3004/// True if the text node is marked as unescaped (`disable-output-escaping`).
3005///
3006/// # UPSTREAM-PARITY
3007///
3008/// Upstream compares `node->name == xmlStringTextNoenc` (pointer equality
3009/// against a static marker). Our trees carry the marker as a duplicated
3010/// `"textnoenc"` string, so we compare contents.
3011///
3012/// # Safety
3013///
3014/// - `node` must be NULL or a valid pointer to an `_xmlNode`.
3015/// - When `node.name` is non-NULL it must be a valid NUL-terminated `xmlChar`
3016///   string; `c_str_eq_bytes` scans it until the NUL byte.
3017unsafe fn is_noenc_text(node: *mut _xmlNode) -> bool {
3018    if node.is_null() {
3019        return false;
3020    }
3021    let n = unsafe { &*node };
3022    if n.name.is_null() {
3023        return false;
3024    }
3025    c_str_eq_bytes(n.name, b"textnoenc")
3026}
3027
3028/// Compare a NUL-terminated xmlChar string with a byte slice.
3029///
3030/// # Safety
3031///
3032/// - `s` must be a valid pointer to a NUL-terminated `xmlChar` buffer; the
3033///   scan reads `b.len()` bytes plus the terminating NUL at `s[b.len()]`, so
3034///   the buffer must be at least `b.len() + 1` readable bytes.
3035const unsafe fn c_str_eq_bytes(s: *const xmlChar, b: &[u8]) -> bool {
3036    let mut i = 0usize;
3037    while i < b.len() {
3038        if unsafe { *s.add(i) } != b[i] {
3039            return false;
3040        }
3041        i += 1;
3042    }
3043    unsafe { *s.add(i) == 0 }
3044}
3045
3046/// Serialization state mirroring the formatting state of libxml2's
3047/// `xmlSaveCtxt` (xmlsave.c 2.15).
3048#[derive(Clone, Copy)]
3049struct DumpState {
3050    /// `ctxt->format`: 0 = no formatting, 1 = XML_SAVE_FORMAT.
3051    format: c_int,
3052    /// The format value captured at dump entry; restored when leaving an
3053    /// element whose children disabled formatting (upstream local `format`).
3054    saved: c_int,
3055    /// The element whose children disabled formatting (upstream
3056    /// `unformattedNode`).
3057    unformatted: *mut _xmlNode,
3058    /// Per-context indent string (upstream `ctxt->indent`); NULL falls back
3059    /// to the default `xmlTreeIndentString`.
3060    indent: *const xmlChar,
3061    /// Byte length of `indent`.
3062    indent_len: c_int,
3063    /// Suppress the XML declaration (XML_SAVE_NO_DECL, upstream `no_decl`).
3064    no_decl: c_int,
3065}
3066
3067impl DumpState {
3068    const fn new(format: c_int) -> Self {
3069        let f = if format != 0 { 1 } else { 0 };
3070        DumpState {
3071            format: f,
3072            saved: f,
3073            unformatted: ptr::null_mut(),
3074            indent: INDENT.as_ptr(),
3075            indent_len: INDENT.len() as c_int,
3076            no_decl: 0,
3077        }
3078    }
3079
3080    /// Create a state with a custom indent string (xmlSaveSetIndentString)
3081    /// and the XML_SAVE_NO_DECL option.
3082    ///
3083    /// # SAFETY
3084    ///
3085    /// - `indent` must be NULL or a valid NUL-terminated string that stays
3086    ///   alive for the whole dump.
3087    const unsafe fn with_indent(format: c_int, indent: *const xmlChar, no_decl: c_int) -> Self {
3088        let f = if format != 0 { 1 } else { 0 };
3089        let (ptr, len) = if indent.is_null() {
3090            (INDENT.as_ptr(), INDENT.len() as c_int)
3091        } else {
3092            let mut n = 0i32;
3093            while unsafe { *indent.add(n as usize) } != 0 {
3094                n += 1;
3095            }
3096            (indent, n)
3097        };
3098        DumpState {
3099            format: f,
3100            saved: f,
3101            unformatted: ptr::null_mut(),
3102            indent: ptr,
3103            indent_len: len,
3104            no_decl,
3105        }
3106    }
3107}
3108
3109/// Write an element/attribute name with its namespace prefix.
3110///
3111/// # SAFETY
3112///
3113/// - `buf` must be valid; `node` must be a valid element node.
3114unsafe fn write_qname(buf: *mut _xmlBuffer, node: *mut _xmlNode) {
3115    let n = unsafe { &*node };
3116    if !n.ns.is_null() {
3117        let ns = unsafe { &*n.ns };
3118        if !ns.prefix.is_null() {
3119            io::buf_cat(buf, ns.prefix);
3120            io::buf_ccat(buf, b':');
3121        }
3122    }
3123    if !n.name.is_null() {
3124        io::buf_cat(buf, n.name);
3125    }
3126}
3127
3128/// Dump a local namespace definition (upstream `xmlNsDumpOutput`).
3129///
3130/// # SAFETY
3131///
3132/// - `buf` must be valid; `cur` must be a valid `_xmlNs`.
3133unsafe fn ns_dump_output(buf: *mut _xmlBuffer, cur: *mut _xmlNs) {
3134    if cur.is_null() || buf.is_null() {
3135        return;
3136    }
3137    let ns = unsafe { &*cur };
3138    if ns.type_ == XML_LOCAL_NAMESPACE as c_int && !ns.href.is_null() {
3139        // The xml namespace is implicit and never re-declared.
3140        if !ns.prefix.is_null() && c_str_eq_bytes(ns.prefix, b"xml") {
3141            return;
3142        }
3143        io::buf_ccat(buf, b' ');
3144        if !ns.prefix.is_null() {
3145            io::buf_add(buf, b"xmlns:" as *const u8, 6);
3146            io::buf_cat(buf, ns.prefix);
3147        } else {
3148            io::buf_add(buf, b"xmlns" as *const u8, 5);
3149        }
3150        io::buf_add(buf, b"=\"" as *const u8, 2);
3151        serialize_attr_value(buf, ns.href);
3152        io::buf_ccat(buf, b'"');
3153    }
3154}
3155
3156/// Dump an attribute node (upstream `xmlAttrDumpOutput`).
3157///
3158/// # SAFETY
3159///
3160/// - `buf` must be valid; `cur` must be a valid `_xmlAttr`.
3161unsafe fn attr_dump_output(buf: *mut _xmlBuffer, cur: *mut _xmlAttr) {
3162    if cur.is_null() || buf.is_null() {
3163        return;
3164    }
3165    io::buf_ccat(buf, b' ');
3166    let a = unsafe { &*cur };
3167    if !a.ns.is_null() {
3168        let ans = unsafe { &*a.ns };
3169        if !ans.prefix.is_null() {
3170            io::buf_cat(buf, ans.prefix);
3171            io::buf_ccat(buf, b':');
3172        }
3173    }
3174    if !a.name.is_null() {
3175        io::buf_cat(buf, a.name);
3176    }
3177    io::buf_add(buf, b"=\"" as *const u8, 2);
3178    // Attribute content: text children are escaped, entity references are
3179    // emitted as `&name;` (upstream `xmlSaveWriteAttrContent`).
3180    let mut child = a.children;
3181    while !child.is_null() {
3182        let ct = unsafe { (*child).type_ };
3183        if ct == XML_TEXT_NODE as c_int && !unsafe { (*child).content }.is_null() {
3184            serialize_attr_value(buf, unsafe { (*child).content });
3185        } else if ct == XML_ENTITY_REF_NODE as c_int && !unsafe { (*child).name }.is_null() {
3186            io::buf_ccat(buf, b'&');
3187            io::buf_cat(buf, unsafe { (*child).name });
3188            io::buf_ccat(buf, b';');
3189        }
3190        child = unsafe { (*child).next };
3191    }
3192    io::buf_ccat(buf, b'"');
3193}
3194
3195/// Dump a notation declaration (upstream `xmlBufDumpNotationDecl`).
3196///
3197/// # SAFETY
3198///
3199/// - `buf` must be valid; `nota` must be a valid `_xmlNotation`.
3200unsafe fn dump_notation_decl(buf: *mut _xmlBuffer, nota: *mut _xmlNotation) {
3201    let n = unsafe { &*nota };
3202    io::buf_add(buf, b"<!NOTATION " as *const u8, 11);
3203    if !n.name.is_null() {
3204        io::buf_cat(buf, n.name);
3205    }
3206    if !n.PublicID.is_null() {
3207        io::buf_add(buf, b" PUBLIC " as *const u8, 8);
3208        write_quoted_string(buf, n.PublicID);
3209        if !n.SystemID.is_null() {
3210            io::buf_ccat(buf, b' ');
3211            write_quoted_string(buf, n.SystemID);
3212        }
3213    } else {
3214        io::buf_add(buf, b" SYSTEM " as *const u8, 8);
3215        write_quoted_string(buf, n.SystemID);
3216    }
3217    io::buf_add(buf, b" >\n" as *const u8, 4);
3218}
3219
3220/// Dump an occurrence operator (upstream `xmlBufDumpElementOccur`).
3221unsafe fn dump_element_occur(buf: *mut _xmlBuffer, ocur: c_int) {
3222    use crate::abi::types::xmlElementContentOccur::*;
3223    if ocur == XML_ELEMENT_CONTENT_OPT as c_int {
3224        io::buf_ccat(buf, b'?');
3225    } else if ocur == XML_ELEMENT_CONTENT_MULT as c_int {
3226        io::buf_ccat(buf, b'*');
3227    } else if ocur == XML_ELEMENT_CONTENT_PLUS as c_int {
3228        io::buf_ccat(buf, b'+');
3229    }
3230}
3231
3232/// Dump an element content model (upstream `xmlBufDumpElementContent`).
3233///
3234/// # SAFETY
3235///
3236/// - `buf` must be valid; `content` must be a valid content tree or NULL.
3237unsafe fn dump_element_content(buf: *mut _xmlBuffer, content: *mut _xmlElementContent) {
3238    use crate::abi::types::xmlElementContentOccur::*;
3239    use crate::abi::types::xmlElementContentType::*;
3240    if content.is_null() {
3241        return;
3242    }
3243    io::buf_ccat(buf, b'(');
3244    let mut cur = content;
3245    loop {
3246        if cur.is_null() {
3247            return;
3248        }
3249        let c = unsafe { &*cur };
3250        match c.type_ {
3251            t if t == XML_ELEMENT_CONTENT_PCDATA as c_int => {
3252                io::buf_add(buf, b"#PCDATA" as *const u8, 7);
3253            }
3254            t if t == XML_ELEMENT_CONTENT_ELEMENT as c_int => {
3255                if !c.prefix.is_null() {
3256                    io::buf_cat(buf, c.prefix);
3257                    io::buf_ccat(buf, b':');
3258                }
3259                if !c.name.is_null() {
3260                    io::buf_cat(buf, c.name);
3261                }
3262            }
3263            t if t == XML_ELEMENT_CONTENT_SEQ as c_int || t == XML_ELEMENT_CONTENT_OR as c_int => {
3264                if cur != content
3265                    && !c.parent.is_null()
3266                    && (c.type_ != unsafe { (*c.parent).type_ }
3267                        || c.ocur != XML_ELEMENT_CONTENT_ONCE as c_int)
3268                {
3269                    io::buf_ccat(buf, b'(');
3270                }
3271                cur = c.c1;
3272                continue;
3273            }
3274            _ => {}
3275        }
3276
3277        // Walk up until we find the next sibling to process.
3278        while cur != content {
3279            let ccur = unsafe { &*cur };
3280            let parent = ccur.parent;
3281            if parent.is_null() {
3282                return;
3283            }
3284            let p = unsafe { &*parent };
3285            if (ccur.type_ == XML_ELEMENT_CONTENT_OR as c_int
3286                || ccur.type_ == XML_ELEMENT_CONTENT_SEQ as c_int)
3287                && (ccur.type_ != p.type_ || ccur.ocur != XML_ELEMENT_CONTENT_ONCE as c_int)
3288            {
3289                io::buf_ccat(buf, b')');
3290            }
3291            dump_element_occur(buf, ccur.ocur);
3292
3293            if ccur.type_ == XML_ELEMENT_CONTENT_SEQ as c_int {
3294                io::buf_add(buf, b" , " as *const u8, 3);
3295            } else if ccur.type_ == XML_ELEMENT_CONTENT_OR as c_int {
3296                io::buf_add(buf, b" | " as *const u8, 3);
3297            }
3298
3299            if cur == p.c1 {
3300                cur = p.c2;
3301                break;
3302            }
3303            cur = parent;
3304        }
3305        if cur == content {
3306            break;
3307        }
3308    }
3309    io::buf_ccat(buf, b')');
3310    let cc = unsafe { &*content };
3311    dump_element_occur(buf, cc.ocur);
3312}
3313
3314/// Dump an element declaration (upstream `xmlBufDumpElementDecl`).
3315///
3316/// # SAFETY
3317///
3318/// - `buf` must be valid; `elem` must be a valid `_xmlElement`.
3319unsafe fn dump_element_decl(buf: *mut _xmlBuffer, elem: *mut _xmlElement) {
3320    use crate::abi::types::xmlElementTypeVal::*;
3321    let e = unsafe { &*elem };
3322    io::buf_add(buf, b"<!ELEMENT " as *const u8, 10);
3323    if !e.prefix.is_null() {
3324        io::buf_cat(buf, e.prefix);
3325        io::buf_ccat(buf, b':');
3326    }
3327    if !e.name.is_null() {
3328        io::buf_cat(buf, e.name);
3329    }
3330    io::buf_ccat(buf, b' ');
3331    match e.etype {
3332        t if t == XML_ELEMENT_TYPE_EMPTY as c_int => {
3333            io::buf_add(buf, b"EMPTY" as *const u8, 5);
3334        }
3335        t if t == XML_ELEMENT_TYPE_ANY as c_int => {
3336            io::buf_add(buf, b"ANY" as *const u8, 3);
3337        }
3338        t if t == XML_ELEMENT_TYPE_MIXED as c_int || t == XML_ELEMENT_TYPE_ELEMENT as c_int => {
3339            dump_element_content(buf, e.content);
3340        }
3341        _ => {}
3342    }
3343    io::buf_add(buf, b">\n" as *const u8, 2);
3344}
3345
3346/// Dump an enumeration (upstream `xmlBufDumpEnumeration`).
3347///
3348/// # SAFETY
3349///
3350/// - `buf` must be valid; `cur` must be a valid enumeration or NULL.
3351unsafe fn dump_enumeration(buf: *mut _xmlBuffer, cur: *mut _xmlEnumeration) {
3352    let mut e = cur;
3353    while !e.is_null() {
3354        let en = unsafe { &*e };
3355        if !en.name.is_null() {
3356            io::buf_cat(buf, en.name);
3357        }
3358        if !en.next.is_null() {
3359            io::buf_add(buf, b" | " as *const u8, 3);
3360        }
3361        e = en.next;
3362    }
3363    io::buf_ccat(buf, b')');
3364}
3365
3366/// Dump an attribute declaration (upstream `xmlSaveWriteAttributeDecl`).
3367///
3368/// # SAFETY
3369///
3370/// - `buf` must be valid; `attr` must be a valid `_xmlAttribute` decl.
3371unsafe fn dump_attribute_decl(buf: *mut _xmlBuffer, attr: *mut _xmlAttribute) {
3372    use crate::abi::types::xmlAttributeDefault::*;
3373    use crate::abi::types::xmlAttributeType::*;
3374    let a = unsafe { &*attr };
3375    io::buf_add(buf, b"<!ATTLIST " as *const u8, 10);
3376    if !a.elem.is_null() {
3377        io::buf_cat(buf, a.elem);
3378    }
3379    io::buf_ccat(buf, b' ');
3380    if !a.prefix.is_null() {
3381        io::buf_cat(buf, a.prefix);
3382        io::buf_ccat(buf, b':');
3383    }
3384    if !a.name.is_null() {
3385        io::buf_cat(buf, a.name);
3386    }
3387    match a.atype {
3388        t if t == XML_ATTRIBUTE_CDATA as c_int => {
3389            io::buf_add(buf, b" CDATA" as *const u8, 6);
3390        }
3391        t if t == XML_ATTRIBUTE_ID as c_int => {
3392            io::buf_add(buf, b" ID" as *const u8, 3);
3393        }
3394        t if t == XML_ATTRIBUTE_IDREF as c_int => {
3395            io::buf_add(buf, b" IDREF" as *const u8, 6);
3396        }
3397        t if t == XML_ATTRIBUTE_IDREFS as c_int => {
3398            io::buf_add(buf, b" IDREFS" as *const u8, 7);
3399        }
3400        t if t == XML_ATTRIBUTE_ENTITY as c_int => {
3401            io::buf_add(buf, b" ENTITY" as *const u8, 7);
3402        }
3403        t if t == XML_ATTRIBUTE_ENTITIES as c_int => {
3404            io::buf_add(buf, b" ENTITIES" as *const u8, 9);
3405        }
3406        t if t == XML_ATTRIBUTE_NMTOKEN as c_int => {
3407            io::buf_add(buf, b" NMTOKEN" as *const u8, 8);
3408        }
3409        t if t == XML_ATTRIBUTE_NMTOKENS as c_int => {
3410            io::buf_add(buf, b" NMTOKENS" as *const u8, 9);
3411        }
3412        t if t == XML_ATTRIBUTE_ENUMERATION as c_int => {
3413            io::buf_add(buf, b" (" as *const u8, 2);
3414            dump_enumeration(buf, a.tree);
3415        }
3416        t if t == XML_ATTRIBUTE_NOTATION as c_int => {
3417            io::buf_add(buf, b" NOTATION (" as *const u8, 11);
3418            dump_enumeration(buf, a.tree);
3419        }
3420        _ => {}
3421    }
3422    match a.def {
3423        t if t == XML_ATTRIBUTE_REQUIRED as c_int => {
3424            io::buf_add(buf, b" #REQUIRED" as *const u8, 10);
3425        }
3426        t if t == XML_ATTRIBUTE_IMPLIED as c_int => {
3427            io::buf_add(buf, b" #IMPLIED" as *const u8, 9);
3428        }
3429        t if t == XML_ATTRIBUTE_FIXED as c_int => {
3430            io::buf_add(buf, b" #FIXED" as *const u8, 7);
3431        }
3432        _ => {}
3433    }
3434    if !a.defaultValue.is_null() {
3435        io::buf_add(buf, b" \"" as *const u8, 2);
3436        serialize_attr_value(buf, a.defaultValue);
3437        io::buf_ccat(buf, b'"');
3438    }
3439    io::buf_add(buf, b">\n" as *const u8, 2);
3440}
3441
3442/// Write a quoted string (upstream `xmlOutputBufferWriteQuotedString`).
3443///
3444/// # SAFETY
3445///
3446/// - `buf` must be valid; `str` must be a valid NUL-terminated string.
3447unsafe fn write_quoted_string(buf: *mut _xmlBuffer, str: *const xmlChar) {
3448    if buf.is_null() {
3449        return;
3450    }
3451    io::buf_ccat(buf, b'"');
3452    if !str.is_null() {
3453        let mut i = 0usize;
3454        while unsafe { *str.add(i) != 0 } {
3455            let ch = unsafe { *str.add(i) };
3456            if ch == b'"' {
3457                io::buf_add(buf, b"&quot;" as *const u8, 6);
3458            } else {
3459                io::buf_add(buf, &ch as *const u8, 1);
3460            }
3461            i += 1;
3462        }
3463    }
3464    io::buf_ccat(buf, b'"');
3465}
3466
3467/// Dump an entity declaration (upstream `xmlBufDumpEntityDecl`).
3468///
3469/// # SAFETY
3470///
3471/// - `buf` must be valid; `ent` must be a valid `_xmlEntity` decl.
3472unsafe fn dump_entity_decl(buf: *mut _xmlBuffer, ent: *mut _xmlEntity) {
3473    use crate::abi::types::xmlEntityType::*;
3474    let e = unsafe { &*ent };
3475    if e.etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
3476        || e.etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
3477    {
3478        io::buf_add(buf, b"<!ENTITY % " as *const u8, 11);
3479    } else {
3480        io::buf_add(buf, b"<!ENTITY " as *const u8, 9);
3481    }
3482    if !e.name.is_null() {
3483        io::buf_cat(buf, e.name);
3484    }
3485    io::buf_ccat(buf, b' ');
3486
3487    if e.etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
3488        || e.etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int
3489        || e.etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
3490    {
3491        if !e.ExternalID.is_null() {
3492            io::buf_add(buf, b"PUBLIC " as *const u8, 7);
3493            write_quoted_string(buf, e.ExternalID);
3494            io::buf_ccat(buf, b' ');
3495        } else {
3496            io::buf_add(buf, b"SYSTEM " as *const u8, 7);
3497        }
3498        write_quoted_string(buf, e.SystemID);
3499    }
3500
3501    if e.etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int && !e.content.is_null() {
3502        io::buf_add(buf, b" NDATA " as *const u8, 7);
3503        if !e.orig.is_null() {
3504            io::buf_cat(buf, e.orig);
3505        } else if !e.content.is_null() {
3506            io::buf_cat(buf, e.content);
3507        }
3508    }
3509
3510    if e.etype == XML_INTERNAL_GENERAL_ENTITY as c_int
3511        || e.etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
3512    {
3513        if !e.orig.is_null() {
3514            write_quoted_string(buf, e.orig);
3515        } else {
3516            // Entity content is quoted, escaping `"` and `%`.
3517            io::buf_ccat(buf, b'"');
3518            if !e.content.is_null() {
3519                let mut i = 0usize;
3520                while unsafe { *e.content.add(i) != 0 } {
3521                    let ch = unsafe { *e.content.add(i) };
3522                    match ch {
3523                        b'"' => io::buf_add(buf, b"&quot;" as *const u8, 6),
3524                        b'%' => io::buf_add(buf, b"&#x25;" as *const u8, 6),
3525                        _ => io::buf_add(buf, &ch as *const u8, 1),
3526                    };
3527                    i += 1;
3528                }
3529            }
3530            io::buf_ccat(buf, b'"');
3531        }
3532    }
3533    io::buf_add(buf, b">\n" as *const u8, 2);
3534}
3535
3536/// Dump a DTD node (upstream `xmlDtdDumpOutput`).
3537///
3538/// # SAFETY
3539///
3540/// - `buf` must be valid; `cur` must be a valid DTD node.
3541unsafe fn dtd_dump_output(
3542    buf: *mut _xmlBuffer,
3543    cur: *mut _xmlNode,
3544    state: &mut DumpState,
3545    level: &mut c_int,
3546) {
3547    let dtd = cur as *mut _xmlDtd;
3548    let d = unsafe { &*dtd };
3549    io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
3550    if !d.name.is_null() {
3551        io::buf_cat(buf, d.name);
3552    }
3553    if !d.ExternalID.is_null() {
3554        io::buf_add(buf, b" PUBLIC " as *const u8, 8);
3555        write_quoted_string(buf, d.ExternalID);
3556        io::buf_ccat(buf, b' ');
3557        write_quoted_string(buf, d.SystemID);
3558    } else if !d.SystemID.is_null() {
3559        io::buf_add(buf, b" SYSTEM " as *const u8, 8);
3560        write_quoted_string(buf, d.SystemID);
3561    }
3562    // UPSTREAM-PARITY (xmlsave.c xmlDtdDumpOutput): the internal-subset
3563    // brackets are written only when a declaration table is non-NULL — an
3564    // empty DTD (all tables NULL) emits `>`. (hash_size() cannot be used
3565    // here: it returns -1 for NULL tables.)
3566    if d.entities.is_null()
3567        && d.elements.is_null()
3568        && d.attributes.is_null()
3569        && d.notations.is_null()
3570        && d.pentities.is_null()
3571    {
3572        io::buf_ccat(buf, b'>');
3573        return;
3574    }
3575    io::buf_add(buf, b" [\n" as *const u8, 3);
3576    // UPSTREAM-PARITY: declarations are dumped in the upstream order
3577    // (notations, elements, attributes, entities, parameter entities). Our
3578    // decls live in hash tables; iteration order is hash-bucket order, so
3579    // multi-declaration files may differ from upstream's insertion order
3580    // (tracked as RESIDUAL R-DTD-DUMP-ORDER).
3581    let format = state.format;
3582    let lvl = *level;
3583    state.format = 0;
3584    *level = -1;
3585    if !d.notations.is_null() {
3586        crate::xml::hash::hash_scan(
3587            d.notations as *mut crate::xml::hash::HashTable,
3588            Some(dump_notation_decl_cb),
3589            buf as *mut c_void,
3590        );
3591    }
3592    if !d.elements.is_null() {
3593        crate::xml::hash::hash_scan(
3594            d.elements as *mut crate::xml::hash::HashTable,
3595            Some(dump_element_decl_cb),
3596            buf as *mut c_void,
3597        );
3598    }
3599    if !d.attributes.is_null() {
3600        crate::xml::hash::hash_scan(
3601            d.attributes as *mut crate::xml::hash::HashTable,
3602            Some(dump_attribute_decl_cb),
3603            buf as *mut c_void,
3604        );
3605    }
3606    if !d.entities.is_null() {
3607        crate::xml::hash::hash_scan(
3608            d.entities as *mut crate::xml::hash::HashTable,
3609            Some(dump_entity_decl_cb),
3610            buf as *mut c_void,
3611        );
3612    }
3613    if !d.pentities.is_null() {
3614        crate::xml::hash::hash_scan(
3615            d.pentities as *mut crate::xml::hash::HashTable,
3616            Some(dump_entity_decl_cb),
3617            buf as *mut c_void,
3618        );
3619    }
3620    state.format = format;
3621    *level = lvl;
3622    io::buf_add(buf, b"]>" as *const u8, 2);
3623}
3624
3625/// Hash-scan callbacks that route each DTD declaration to its dumper.
3626unsafe extern "C" fn dump_notation_decl_cb(
3627    payload: *mut c_void,
3628    data: *mut c_void,
3629    _name: *const crate::abi::types::xmlChar,
3630) {
3631    if !payload.is_null() && !data.is_null() {
3632        dump_notation_decl(data as *mut _xmlBuffer, payload as *mut _xmlNotation);
3633    }
3634}
3635
3636/// Hash-scan callback for element declarations.
3637unsafe extern "C" fn dump_element_decl_cb(
3638    payload: *mut c_void,
3639    data: *mut c_void,
3640    _name: *const crate::abi::types::xmlChar,
3641) {
3642    if !payload.is_null() && !data.is_null() {
3643        dump_element_decl(data as *mut _xmlBuffer, payload as *mut _xmlElement);
3644    }
3645}
3646
3647/// Hash-scan callback for attribute declarations.
3648unsafe extern "C" fn dump_attribute_decl_cb(
3649    payload: *mut c_void,
3650    data: *mut c_void,
3651    _name: *const crate::abi::types::xmlChar,
3652) {
3653    if !payload.is_null() && !data.is_null() {
3654        dump_attribute_decl(data as *mut _xmlBuffer, payload as *mut _xmlAttribute);
3655    }
3656}
3657
3658/// Hash-scan callback for entity declarations.
3659unsafe extern "C" fn dump_entity_decl_cb(
3660    payload: *mut c_void,
3661    data: *mut c_void,
3662    _name: *const crate::abi::types::xmlChar,
3663) {
3664    if !payload.is_null() && !data.is_null() {
3665        dump_entity_decl(data as *mut _xmlBuffer, payload as *mut _xmlEntity);
3666    }
3667}
3668
3669/// Dump the content of a document (upstream `xmlSaveDocInternal`, XML path).
3670///
3671/// Writes the XML declaration (when not suppressed) followed by each child
3672/// separated by a newline.
3673///
3674/// # SAFETY
3675///
3676/// - `buf` must be valid; `cur` must be a valid document node.
3677unsafe fn doc_content_dump_output(
3678    buf: *mut _xmlBuffer,
3679    cur: *mut _xmlNode,
3680    state: &mut DumpState,
3681    level: &mut c_int,
3682) {
3683    let doc = cur as *mut _xmlDoc;
3684    let d = unsafe { &*doc };
3685
3686    // XML declaration: `<?xml version="..."?>\n`. The encoding is included
3687    // only when the document carries one. Suppressed by the
3688    // XML_SAVE_NO_DECL save option (upstream xmlsave.c `no_decl`).
3689    if state.no_decl == 0 {
3690        io::buf_add(buf, b"<?xml version=\"" as *const u8, 15);
3691        if !d.version.is_null() {
3692            io::buf_cat(buf, d.version);
3693        } else {
3694            io::buf_add(buf, b"1.0" as *const u8, 3);
3695        }
3696        io::buf_ccat(buf, b'"');
3697        if !d.encoding.is_null() {
3698            io::buf_add(buf, b" encoding=\"" as *const u8, 11);
3699            io::buf_cat(buf, d.encoding);
3700            io::buf_ccat(buf, b'"');
3701        }
3702        match d.standalone {
3703            0 => {
3704                io::buf_add(buf, b" standalone=\"no\"" as *const u8, 16);
3705            }
3706            1 => {
3707                io::buf_add(buf, b" standalone=\"yes\"" as *const u8, 17);
3708            }
3709            _ => {}
3710        }
3711        io::buf_add(buf, b"?>\n" as *const u8, 3);
3712    }
3713
3714    // UPSTREAM-PARITY (xmlsave.c xmlSaveDocInternal): the internal subset
3715    // DTD is a member of the children chain (xmlCreateIntSubset inserts it
3716    // before the first element), and the children loop below dumps it once.
3717    // Construction paths that keep the DTD only on doc->intSubset
3718    // (xmlCopyDoc, lazily-created subsets) need the explicit dump. Never
3719    // dump both — that double-prints <!DOCTYPE>.
3720    if !d.intSubset.is_null() {
3721        let mut in_chain = false;
3722        let mut c = d.children;
3723        while !c.is_null() {
3724            if c as *mut c_void == d.intSubset as *mut c_void {
3725                in_chain = true;
3726                break;
3727            }
3728            c = unsafe { (*c).next };
3729        }
3730        if !in_chain {
3731            let mut lvl = 0;
3732            dtd_dump_output(buf, d.intSubset as *mut _xmlNode, state, &mut lvl);
3733            io::buf_ccat(buf, b'\n');
3734        }
3735    }
3736
3737    if !d.children.is_null() {
3738        let mut child = d.children;
3739        while !child.is_null() {
3740            *level = 0;
3741            node_dump_internal(buf, child, child, cur, state, level);
3742            let ct = unsafe { (*child).type_ };
3743            if ct != XML_XINCLUDE_START as c_int && ct != XML_XINCLUDE_END as c_int {
3744                io::buf_ccat(buf, b'\n');
3745            }
3746            child = unsafe { (*child).next };
3747        }
3748    }
3749}
3750
3751/// Faithful port of libxml2's `xmlNodeDumpOutputInternal` (xmlsave.c 2.15).
3752///
3753/// Serializes `cur` and its descendants into `buf`. `root` is the node this
3754/// invocation started with: the root node itself is never indented, and no
3755/// trailing separator is emitted for it (the caller separates siblings).
3756/// `parent` is the expected parent of `cur`, used by the corrupted-tree
3757/// fallback.
3758///
3759/// # UPSTREAM-PARITY
3760///
3761/// - Indentation (two spaces per level, capped at 30 levels) is written
3762///   before every non-root element, PI and comment when formatting.
3763/// - An element whose children include a text, CDATA or entity-reference
3764///   node disables formatting for its whole content (the `unformattedNode`
3765///   mechanism); formatting is restored when its closing tag is emitted.
3766/// - `\n` separators between siblings are emitted after every child of a
3767///   formatted element (the upstream unwind loop).
3768///
3769/// # SAFETY
3770///
3771/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
3772/// - `cur` must be a valid node pointer; `root`/`parent` must be stable
3773///   pointers into the same tree.
3774unsafe fn node_dump_internal(
3775    buf: *mut _xmlBuffer,
3776    cur: *mut _xmlNode,
3777    root: *mut _xmlNode,
3778    parent: *mut _xmlNode,
3779    state: &mut DumpState,
3780    level: &mut c_int,
3781) {
3782    if cur.is_null() || buf.is_null() {
3783        return;
3784    }
3785    let n = unsafe { &*cur };
3786    match n.type_ {
3787        t if t == XML_ELEMENT_NODE as c_int => {
3788            if cur != root && state.format == 1 {
3789                write_indent(buf, *level, state.indent, state.indent_len);
3790            }
3791            // Corrupted-tree fallback (upstream handles nodes passed with a
3792            // broken parent link by dumping the subtree as its own root).
3793            if !n.parent.is_null() && n.parent != parent && !n.children.is_null() {
3794                let mut sub = DumpState::new(state.format);
3795                let mut sub_level = *level;
3796                node_dump_internal(buf, cur, cur, n.parent, &mut sub, &mut sub_level);
3797                return;
3798            }
3799            // Start tag.
3800            io::buf_ccat(buf, b'<');
3801            write_qname(buf, cur);
3802            let mut nsdef = n.nsDef;
3803            while !nsdef.is_null() {
3804                ns_dump_output(buf, nsdef);
3805                nsdef = unsafe { (*nsdef).next };
3806            }
3807            let mut attr = n.properties;
3808            while !attr.is_null() {
3809                attr_dump_output(buf, attr);
3810                attr = unsafe { (*attr).next };
3811            }
3812            if n.children.is_null() {
3813                io::buf_add(buf, b"/>" as *const u8, 2);
3814            } else {
3815                if state.format == 1 {
3816                    // An element with text/CDATA/entity-ref children is
3817                    // serialized unformatted (upstream unformattedNode).
3818                    let mut tmp = n.children;
3819                    while !tmp.is_null() {
3820                        let tt = unsafe { (*tmp).type_ };
3821                        if tt == XML_TEXT_NODE as c_int
3822                            || tt == XML_CDATA_SECTION_NODE as c_int
3823                            || tt == XML_ENTITY_REF_NODE as c_int
3824                        {
3825                            state.format = 0;
3826                            state.unformatted = cur;
3827                            break;
3828                        }
3829                        tmp = unsafe { (*tmp).next };
3830                    }
3831                }
3832                io::buf_ccat(buf, b'>');
3833                if state.format == 1 {
3834                    io::buf_ccat(buf, b'\n');
3835                }
3836                if *level >= 0 {
3837                    *level += 1;
3838                }
3839                let mut child = n.children;
3840                while !child.is_null() {
3841                    node_dump_internal(buf, child, root, cur, state, level);
3842                    if state.format == 1 {
3843                        let ct = unsafe { (*child).type_ };
3844                        if ct != XML_XINCLUDE_START as c_int && ct != XML_XINCLUDE_END as c_int {
3845                            io::buf_ccat(buf, b'\n');
3846                        }
3847                    }
3848                    child = unsafe { (*child).next };
3849                }
3850                // Closing tag.
3851                if *level > 0 {
3852                    *level -= 1;
3853                }
3854                if state.format == 1 {
3855                    write_indent(buf, *level, state.indent, state.indent_len);
3856                }
3857                io::buf_add(buf, b"</" as *const u8, 2);
3858                write_qname(buf, cur);
3859                io::buf_ccat(buf, b'>');
3860                if cur == state.unformatted {
3861                    state.format = state.saved;
3862                    state.unformatted = ptr::null_mut();
3863                }
3864            }
3865        }
3866        t if t == XML_TEXT_NODE as c_int => {
3867            if !n.content.is_null() {
3868                if is_noenc_text(cur) {
3869                    io::buf_cat(buf, n.content);
3870                } else {
3871                    serialize_text(buf, n.content, xml_strlen(n.content));
3872                }
3873            } else if !n.children.is_null() {
3874                // Non-compact text node (entity merge): content lives in a
3875                // child text node.
3876                let c = node_get_content(cur);
3877                if !c.is_null() {
3878                    if is_noenc_text(cur) {
3879                        io::buf_cat(buf, c);
3880                    } else {
3881                        serialize_text(buf, c, xml_strlen(c));
3882                    }
3883                    allocator::xmlFreeImpl(c as *mut c_void);
3884                }
3885            }
3886        }
3887        t if t == XML_CDATA_SECTION_NODE as c_int => {
3888            if n.content.is_null() || unsafe { *n.content == 0 } {
3889                io::buf_add(buf, b"<![CDATA[]]>" as *const u8, 12);
3890            } else {
3891                let len = xml_strlen(n.content) as usize;
3892                let bytes = core::slice::from_raw_parts(n.content, len);
3893                let mut i = 0usize;
3894                let mut seg_start = 0usize;
3895                while i < len {
3896                    if bytes[i] == b']'
3897                        && i + 2 < len
3898                        && bytes[i + 1] == b']'
3899                        && bytes[i + 2] == b'>'
3900                    {
3901                        io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
3902                        io::buf_add(buf, n.content.add(seg_start), (i + 2 - seg_start) as c_int);
3903                        io::buf_add(buf, b"]]>" as *const u8, 3);
3904                        seg_start = i + 2;
3905                        i += 3;
3906                        continue;
3907                    }
3908                    i += 1;
3909                }
3910                if seg_start < len {
3911                    io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
3912                    io::buf_add(buf, n.content.add(seg_start), (len - seg_start) as c_int);
3913                    io::buf_add(buf, b"]]>" as *const u8, 3);
3914                }
3915            }
3916        }
3917        t if t == XML_COMMENT_NODE as c_int => {
3918            if cur != root && state.format == 1 {
3919                write_indent(buf, *level, state.indent, state.indent_len);
3920            }
3921            if !n.content.is_null() {
3922                io::buf_add(buf, b"<!--" as *const u8, 4);
3923                io::buf_cat(buf, n.content);
3924                io::buf_add(buf, b"-->" as *const u8, 3);
3925            }
3926        }
3927        t if t == XML_PI_NODE as c_int => {
3928            if cur != root && state.format == 1 {
3929                write_indent(buf, *level, state.indent, state.indent_len);
3930            }
3931            io::buf_add(buf, b"<?" as *const u8, 2);
3932            if !n.name.is_null() {
3933                io::buf_cat(buf, n.name);
3934            }
3935            if !n.content.is_null() && unsafe { *n.content != 0 } {
3936                io::buf_ccat(buf, b' ');
3937                io::buf_cat(buf, n.content);
3938            }
3939            io::buf_add(buf, b"?>" as *const u8, 2);
3940        }
3941        t if t == XML_ENTITY_REF_NODE as c_int => {
3942            io::buf_ccat(buf, b'&');
3943            if !n.name.is_null() {
3944                io::buf_cat(buf, n.name);
3945            }
3946            io::buf_ccat(buf, b';');
3947        }
3948        t if t == XML_DOCUMENT_NODE as c_int => {
3949            doc_content_dump_output(buf, cur, state, level);
3950        }
3951        t if t == XML_HTML_DOCUMENT_NODE as c_int => {
3952            // HTML documents are serialized by the HTML serializer.
3953            crate::xml::html::serialize_node(cur, buf, state.format, *level);
3954        }
3955        t if t == XML_DTD_NODE as c_int => {
3956            dtd_dump_output(buf, cur, state, level);
3957        }
3958        t if t == XML_ATTRIBUTE_NODE as c_int => {
3959            attr_dump_output(buf, cur as *mut _xmlAttr);
3960        }
3961        t if t == XML_NAMESPACE_DECL as c_int => {
3962            ns_dump_output(buf, cur as *mut _xmlNs);
3963        }
3964        t if t == XML_ELEMENT_DECL as c_int => {
3965            dump_element_decl(buf, cur as *mut _xmlElement);
3966        }
3967        t if t == XML_ATTRIBUTE_DECL as c_int => {
3968            dump_attribute_decl(buf, cur as *mut _xmlAttribute);
3969        }
3970        t if t == XML_ENTITY_DECL as c_int => {
3971            dump_entity_decl(buf, cur as *mut _xmlEntity);
3972        }
3973        _ => {}
3974    }
3975}
3976
3977/// Recursively serialize a node tree to a buffer.
3978///
3979/// `buf` is an `_xmlBuffer*`, `format` controls indentation (non-zero = pretty-print).
3980///
3981/// # UPSTREAM-PARITY
3982///
3983/// Mirrors `xmlNodeDumpOutputInternal` (xmlsave.c 2.15): the node is treated
3984/// as the root of the dump (no leading indentation, no trailing separator).
3985///
3986/// # SAFETY
3987///
3988/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
3989/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
3990pub(crate) unsafe fn serialize_node(
3991    node: *mut _xmlNode,
3992    buf: *mut _xmlBuffer,
3993    format: c_int,
3994    level: c_int,
3995) {
3996    unsafe { serialize_node_opt(node, buf, format, level, ptr::null()) };
3997}
3998
3999/// Like `serialize_node`, with a per-context indent string
4000/// (xmlSaveSetIndentString); NULL indent uses the default.
4001///
4002/// # SAFETY
4003///
4004/// - `indent` must be NULL or a valid NUL-terminated string that stays
4005///   alive for the whole dump.
4006pub(crate) unsafe fn serialize_node_opt(
4007    node: *mut _xmlNode,
4008    buf: *mut _xmlBuffer,
4009    format: c_int,
4010    level: c_int,
4011    indent: *const xmlChar,
4012) {
4013    unsafe { serialize_node_opts(node, buf, format, level, indent, 0) };
4014}
4015
4016/// Like `serialize_node_opt`, plus the XML_SAVE_NO_DECL flag.
4017///
4018/// # SAFETY
4019///
4020/// - `indent` must be NULL or a valid NUL-terminated string that stays
4021///   alive for the whole dump.
4022pub(crate) unsafe fn serialize_node_opts(
4023    node: *mut _xmlNode,
4024    buf: *mut _xmlBuffer,
4025    format: c_int,
4026    level: c_int,
4027    indent: *const xmlChar,
4028    no_decl: c_int,
4029) {
4030    if node.is_null() || buf.is_null() {
4031        return;
4032    }
4033    let parent = unsafe { (*node).parent };
4034    let mut state = DumpState::with_indent(format, indent, no_decl);
4035    let mut lvl = level;
4036    node_dump_internal(buf, node, node, parent, &mut state, &mut lvl);
4037}
4038
4039/// Dump a document to a buffer.
4040///
4041/// Serializes the entire document tree into `buf`.
4042/// Returns the number of bytes written, or -1 on error.
4043///
4044/// # SAFETY
4045///
4046/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4047/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4048pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
4049    if buf.is_null() || doc.is_null() {
4050        return -1;
4051    }
4052
4053    let before = io::buf_length(buf);
4054    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
4055    let after = io::buf_length(buf);
4056
4057    if after < 0 || before < 0 {
4058        return -1;
4059    }
4060    after - before
4061}
4062
4063/// Dump a node tree to a buffer.
4064///
4065/// Serializes the node and its descendants into `buf`.
4066/// `level` is the initial indentation level, `format` controls pretty-printing.
4067/// Returns the number of bytes written, or -1 on error.
4068///
4069/// # SAFETY
4070///
4071/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4072/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4073/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
4074pub(crate) unsafe fn node_dump(
4075    buf: *mut _xmlBuffer,
4076    doc: *mut _xmlDoc,
4077    node: *mut _xmlNode,
4078    level: c_int,
4079    format: c_int,
4080) -> c_int {
4081    let _ = doc; // Used for entity resolution in full implementation
4082    if buf.is_null() || node.is_null() {
4083        return -1;
4084    }
4085
4086    let before = io::buf_length(buf);
4087    serialize_node(node, buf, format, level);
4088    let after = io::buf_length(buf);
4089
4090    if after < 0 || before < 0 {
4091        return -1;
4092    }
4093    after - before
4094}
4095
4096/// Save a document to a file.
4097///
4098/// # SAFETY
4099///
4100/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4101/// - `filename` must be a valid null-terminated C string.
4102pub(crate) unsafe fn save_doc_to_filename(
4103    doc: *mut _xmlDoc,
4104    filename: *const c_char,
4105    compression: c_int,
4106) -> c_int {
4107    if doc.is_null() || filename.is_null() {
4108        return -1;
4109    }
4110
4111    let out = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
4112    if out.is_null() {
4113        return -1;
4114    }
4115
4116    let buf = io::buf_create(-1);
4117    if buf.is_null() {
4118        io::output_buffer_close(out);
4119        return -1;
4120    }
4121
4122    let ret = doc_dump(buf, doc);
4123    if ret >= 0 {
4124        // Flush the buffer content to the output
4125        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
4126        io::output_buffer_flush(out);
4127    }
4128
4129    io::buf_free(buf);
4130    io::output_buffer_close(out);
4131    ret
4132}
4133
4134/// Save a document to a file descriptor.
4135///
4136/// # SAFETY
4137///
4138/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4139/// - `fd` must be a valid open file descriptor.
4140#[allow(dead_code)]
4141pub(crate) unsafe fn save_doc_to_fd(doc: *mut _xmlDoc, fd: c_int, _compression: c_int) -> c_int {
4142    if doc.is_null() || fd < 0 {
4143        return -1;
4144    }
4145
4146    let out = io::output_buffer_create_fd(fd, ptr::null_mut());
4147    if out.is_null() {
4148        return -1;
4149    }
4150
4151    let buf = io::buf_create(-1);
4152    if buf.is_null() {
4153        io::output_buffer_close(out);
4154        return -1;
4155    }
4156
4157    let ret = doc_dump(buf, doc);
4158    if ret >= 0 {
4159        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
4160        io::output_buffer_flush(out);
4161    }
4162
4163    io::buf_free(buf);
4164    io::output_buffer_close(out);
4165    ret
4166}
4167
4168/// Save a document to an xmlBuffer.
4169///
4170/// # SAFETY
4171///
4172/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4173/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4174#[allow(dead_code)]
4175pub(crate) unsafe fn save_doc_to_buf(
4176    doc: *mut _xmlDoc,
4177    buf: *mut _xmlBuffer,
4178    compression: c_int,
4179) -> c_int {
4180    let _ = compression;
4181    if doc.is_null() || buf.is_null() {
4182        return -1;
4183    }
4184
4185    doc_dump(buf, doc)
4186}
4187
4188/// Format (pretty-print) a document to a buffer.
4189///
4190/// # SAFETY
4191///
4192/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4193/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4194#[allow(dead_code)]
4195pub(crate) unsafe fn save_format_doc_to_buf(
4196    doc: *mut _xmlDoc,
4197    buf: *mut _xmlBuffer,
4198    compression: c_int,
4199) -> c_int {
4200    let _ = compression;
4201    if doc.is_null() || buf.is_null() {
4202        return -1;
4203    }
4204
4205    let before = io::buf_length(buf);
4206    serialize_node(doc as *mut _xmlNode, buf, 1, 0);
4207    let after = io::buf_length(buf);
4208
4209    if after < 0 || before < 0 {
4210        return -1;
4211    }
4212    after - before
4213}
4214
4215/// Dump a node to a null-terminated string.
4216///
4217/// Returns a pointer to the string (caller must free with `xmlFree`).
4218/// Returns NULL on error.
4219///
4220/// # SAFETY
4221///
4222/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
4223#[allow(dead_code)]
4224pub(crate) unsafe fn dump_node(node: *mut _xmlNode) -> *mut xmlChar {
4225    if node.is_null() {
4226        return ptr::null_mut();
4227    }
4228
4229    let buf = io::buf_create(-1);
4230    if buf.is_null() {
4231        return ptr::null_mut();
4232    }
4233
4234    serialize_node(node, buf, 0, 0);
4235
4236    let content = io::buf_content(buf);
4237    if content.is_null() {
4238        io::buf_free(buf);
4239        return ptr::null_mut();
4240    }
4241
4242    // Duplicate the string so we can free the buffer
4243    let result = dup_xml_str(content);
4244    io::buf_free(buf);
4245    result
4246}
4247
4248/// Dump a document to a null-terminated string.
4249///
4250/// Returns a pointer to the string (caller must free with `xmlFree`).
4251/// Returns NULL on error.
4252///
4253/// # SAFETY
4254///
4255/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4256pub unsafe fn dump_doc(doc: *mut _xmlDoc) -> *mut xmlChar {
4257    if doc.is_null() {
4258        return ptr::null_mut();
4259    }
4260
4261    let buf = io::buf_create(-1);
4262    if buf.is_null() {
4263        return ptr::null_mut();
4264    }
4265
4266    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
4267
4268    let content = io::buf_content(buf);
4269    if content.is_null() {
4270        io::buf_free(buf);
4271        return ptr::null_mut();
4272    }
4273
4274    let result = dup_xml_str(content);
4275    io::buf_free(buf);
4276    result
4277}
4278
4279// ═══════════════════════════════════════════════════════════════════════════════
4280// ABI-compatible export wrappers
4281// ═══════════════════════════════════════════════════════════════════════════════
4282
4283/// Dump a node to a buffer (ABI wrapper).
4284///
4285/// # UPSTREAM-PARITY
4286///
4287/// ```c
4288/// int xmlNodeDump(xmlBufferPtr buf, xmlDocPtr doc, xmlNodePtr node, int level, int format);
4289/// ```
4290///
4291/// # SAFETY
4292///
4293/// - All pointer arguments must be valid or NULL.
4294pub(crate) unsafe fn xmlNodeDump(
4295    buf: *mut _xmlBuffer,
4296    doc: *mut _xmlDoc,
4297    node: *mut _xmlNode,
4298    level: c_int,
4299    format: c_int,
4300) -> c_int {
4301    node_dump(buf, doc, node, level, format)
4302}
4303
4304/// Dump a document to a FILE*.
4305///
4306/// # UPSTREAM-PARITY
4307///
4308/// ```c
4309/// int xmlDocDump(FILE *fp, xmlDocPtr doc);
4310/// ```
4311///
4312/// # SAFETY
4313///
4314/// - `fp` must be a valid FILE* pointer.
4315/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4316pub(crate) unsafe fn xmlDocDump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
4317    if fp.is_null() || doc.is_null() {
4318        return -1;
4319    }
4320
4321    let buf = io::buf_create(-1);
4322    if buf.is_null() {
4323        return -1;
4324    }
4325
4326    let ret = doc_dump(buf, doc);
4327    if ret < 0 {
4328        io::buf_free(buf);
4329        return -1;
4330    }
4331
4332    let content = io::buf_content(buf);
4333    let len = io::buf_length(buf);
4334    if !content.is_null() && len > 0 {
4335        let written = libc::fwrite(
4336            content as *const c_void,
4337            1,
4338            len as usize,
4339            fp as *mut libc::FILE,
4340        );
4341        io::buf_free(buf);
4342        written as c_int
4343    } else {
4344        io::buf_free(buf);
4345        0
4346    }
4347}
4348
4349/// Dump a document to memory (with format flag).
4350///
4351/// # UPSTREAM-PARITY
4352///
4353/// ```c
4354/// void xmlDocDumpFormatMemory(xmlDocPtr doc, xmlChar **mem, int *size, int format);
4355/// ```
4356///
4357/// # SAFETY
4358///
4359/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4360/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
4361/// - `size` must be a valid pointer to an int that will receive the size.
4362pub(crate) unsafe fn xmlDocDumpFormatMemory(
4363    doc: *mut _xmlDoc,
4364    mem: *mut *mut xmlChar,
4365    size: *mut c_int,
4366    format: c_int,
4367) {
4368    if doc.is_null() || mem.is_null() || size.is_null() {
4369        return;
4370    }
4371
4372    let buf = io::buf_create(-1);
4373    if buf.is_null() {
4374        unsafe {
4375            *mem = ptr::null_mut();
4376            *size = 0;
4377        }
4378        return;
4379    }
4380
4381    serialize_node(doc as *mut _xmlNode, buf, format, 0);
4382
4383    let content = io::buf_content(buf);
4384    let len = io::buf_length(buf);
4385
4386    if !content.is_null() && len > 0 {
4387        // Allocate memory for the result (+1 for null terminator)
4388        let result = allocator::xmlMallocImpl((len + 1) as usize) as *mut xmlChar;
4389        if !result.is_null() {
4390            ptr::copy_nonoverlapping(content, result, len as usize);
4391            *result.add(len as usize) = 0;
4392            unsafe {
4393                *mem = result;
4394                *size = len;
4395            }
4396        } else {
4397            unsafe {
4398                *mem = ptr::null_mut();
4399                *size = 0;
4400            }
4401        }
4402    } else {
4403        unsafe {
4404            *mem = ptr::null_mut();
4405            *size = 0;
4406        }
4407    }
4408
4409    io::buf_free(buf);
4410}
4411
4412/// Dump a document to memory (unformatted).
4413///
4414/// # UPSTREAM-PARITY
4415///
4416/// ```c
4417/// void xmlDocDumpMemory(xmlDocPtr doc, xmlChar **mem, int *size);
4418/// ```
4419///
4420/// # SAFETY
4421///
4422/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4423/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
4424/// - `size` must be a valid pointer to an int that will receive the size.
4425pub(crate) unsafe fn xmlDocDumpMemory(doc: *mut _xmlDoc, mem: *mut *mut xmlChar, size: *mut c_int) {
4426    xmlDocDumpFormatMemory(doc, mem, size, 0)
4427}
4428
4429/// Save a document to a file (ABI wrapper).
4430///
4431/// # UPSTREAM-PARITY
4432///
4433/// ```c
4434/// int xmlSaveFile(const char *filename, xmlDocPtr cur);
4435/// ```
4436///
4437/// # SAFETY
4438///
4439/// - `filename` must be a valid null-terminated C string.
4440/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
4441pub(crate) unsafe fn xmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
4442    save_doc_to_filename(cur, filename, 0)
4443}
4444
4445/// Save a document to a file with encoding.
4446///
4447/// # UPSTREAM-PARITY
4448///
4449/// ```c
4450/// int xmlSaveFileEnc(const char *filename, xmlDocPtr cur, const char *encoding);
4451/// ```
4452///
4453/// # SAFETY
4454///
4455/// - `filename` must be a valid null-terminated C string.
4456/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
4457/// - `encoding` may be NULL (uses UTF-8).
4458pub(crate) unsafe fn xmlSaveFileEnc(
4459    filename: *const c_char,
4460    cur: *mut _xmlDoc,
4461    encoding: *const c_char,
4462) -> c_int {
4463    let _ = encoding; // Future: use encoding to set encoder on output buffer
4464    save_doc_to_filename(cur, filename, 0)
4465}
4466
4467/// Save a document to a file with format flag.
4468///
4469/// # UPSTREAM-PARITY
4470///
4471/// ```c
4472/// int xmlSaveFormatFile(const char *filename, xmlDocPtr cur, int format);
4473/// ```
4474///
4475/// # SAFETY
4476///
4477/// - `filename` must be a valid null-terminated C string.
4478/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
4479pub(crate) unsafe fn xmlSaveFormatFile(
4480    filename: *const c_char,
4481    cur: *mut _xmlDoc,
4482    format: c_int,
4483) -> c_int {
4484    let _ = format;
4485    save_doc_to_filename(cur, filename, 0)
4486}
4487
4488/// Save a document to a file with encoding and format flag.
4489///
4490/// # UPSTREAM-PARITY
4491///
4492/// ```c
4493/// int xmlSaveFormatFileEnc(const char *filename, xmlDocPtr cur, const char *encoding, int format);
4494/// ```
4495///
4496/// # SAFETY
4497///
4498/// - `filename` must be a valid null-terminated C string.
4499/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
4500/// - `encoding` may be NULL (uses UTF-8).
4501pub(crate) unsafe fn xmlSaveFormatFileEnc(
4502    filename: *const c_char,
4503    cur: *mut _xmlDoc,
4504    encoding: *const c_char,
4505    format: c_int,
4506) -> c_int {
4507    let _ = encoding;
4508    let _ = format;
4509    save_doc_to_filename(cur, filename, 0)
4510}
4511
4512/// Get the compression mode of a document.
4513///
4514/// # UPSTREAM-PARITY
4515///
4516/// ```c
4517/// int xmlGetDocCompressMode(xmlDocPtr doc);
4518/// ```
4519///
4520/// # SAFETY
4521///
4522/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4523pub(crate) unsafe fn xmlGetDocCompressMode(doc: *mut _xmlDoc) -> c_int {
4524    if doc.is_null() {
4525        return -1;
4526    }
4527    unsafe { (*doc).compression }
4528}
4529
4530/// Set the compression mode of a document.
4531///
4532/// # UPSTREAM-PARITY
4533///
4534/// ```c
4535/// void xmlSetDocCompressMode(xmlDocPtr doc, int mode);
4536/// ```
4537///
4538/// # SAFETY
4539///
4540/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
4541pub(crate) unsafe fn xmlSetDocCompressMode(doc: *mut _xmlDoc, mode: c_int) {
4542    if doc.is_null() {
4543        return;
4544    }
4545    unsafe {
4546        (*doc).compression = mode;
4547    }
4548}
4549
4550#[cfg(test)]
4551mod tests {
4552    use super::*;
4553    use core::ffi::c_void;
4554
4555    /// Helper: allocate a NUL-terminated xmlChar copy of `s`.
4556    ///
4557    /// # Safety
4558    ///
4559    /// - The returned buffer is heap-allocated with `xmlMallocImpl` and must
4560    ///   be freed by the caller with `xmlFreeImpl`; it may be NULL on OOM, so
4561    ///   callers check it before use.
4562    fn c_str(s: &str) -> *const xmlChar {
4563        let bytes = s.as_bytes();
4564        let buf = unsafe { allocator::xmlMallocImpl(bytes.len() + 1) as *mut u8 };
4565        if !buf.is_null() {
4566            unsafe {
4567                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
4568                *buf.add(bytes.len()) = 0;
4569            }
4570        }
4571        buf as *const xmlChar
4572    }
4573
4574    /// Verify creating and freeing a document.
4575    ///
4576    /// # Safety
4577    ///
4578    /// - `doc` from `new_doc` must be a valid `_xmlDoc` while its fields are
4579    ///   read; it is freed with `free_doc`.
4580    #[test]
4581    fn test_new_free_doc() {
4582        unsafe {
4583            let doc = new_doc(ptr::null());
4584            assert!(!doc.is_null());
4585            assert_eq!((*doc).type_, XML_DOCUMENT_NODE as c_int);
4586            assert_eq!((*doc).standalone, -1);
4587            assert_eq!((*doc).doc, doc);
4588            assert!(!(*doc).version.is_null());
4589            free_doc(doc);
4590        }
4591    }
4592
4593    /// Verify creating a document with a version string.
4594    ///
4595    /// # Safety
4596    ///
4597    /// - The `c_str` buffer must be NUL-terminated and alive while `new_doc`
4598    ///   duplicates it; the buffer is freed with `xmlFreeImpl` and the doc
4599    ///   with `free_doc`.
4600    #[test]
4601    fn test_new_doc_with_version() {
4602        unsafe {
4603            let ver = c_str("2.0");
4604            let doc = new_doc(ver);
4605            assert!(!doc.is_null());
4606            let doc_ver = (*doc).version;
4607            assert!(!doc_ver.is_null());
4608            assert!(crate::abi::exports_xml2::xmlStrEqual(doc_ver, ver,) != 0);
4609            allocator::xmlFreeImpl(ver as *mut c_void);
4610            free_doc(doc);
4611        }
4612    }
4613
4614    /// Verify creating a node.
4615    ///
4616    /// # Safety
4617    ///
4618    /// - The `c_str` buffer must be NUL-terminated and alive while `new_node`
4619    ///   duplicates it; the node is freed with `free_node` and the doc with
4620    ///   `free_doc`.
4621    #[test]
4622    fn test_new_node() {
4623        unsafe {
4624            let doc = new_doc(ptr::null());
4625            let node = new_node(ptr::null_mut(), c_str("root"));
4626            assert!(!node.is_null());
4627            assert_eq!((*node).type_, XML_ELEMENT_NODE as c_int);
4628            assert!(!(*node).name.is_null());
4629            free_node(node);
4630            free_doc(doc);
4631        }
4632    }
4633
4634    /// Verify that `node_get_content` concatenates all descendant text.
4635    ///
4636    /// # Safety
4637    ///
4638    /// - The doc and nodes built by the tree helpers must be valid and linked
4639    ///   before `node_get_content` walks them; `content` is read with
4640    ///   `strlen` and freed with `xmlFreeImpl`, and the doc with `free_doc`.
4641    #[test]
4642    fn test_node_get_content_recurses_descendants() {
4643        // UPSTREAM-PARITY: xmlNodeGetContent (tree.c) concatenates ALL
4644        // descendant text, not just direct text children — the XPath 1.0
4645        // string-value of an element. Regression test for the Phase 9 fix
4646        // where <book><title>Rust</title></book> produced empty content.
4647        unsafe {
4648            let doc = new_doc(ptr::null());
4649            let root = new_node(ptr::null_mut(), c_str("library"));
4650            doc_set_root_element(doc, root);
4651            let book = new_child(root, ptr::null_mut(), c_str("book"));
4652            let title = new_child(book, ptr::null_mut(), c_str("title"));
4653            let text = new_text(c_str("Rust"));
4654            add_child(title, text);
4655
4656            let content = node_get_content(book);
4657            assert!(!content.is_null());
4658            let s = core::slice::from_raw_parts(
4659                content,
4660                libc::strlen(content as *const libc::c_char) as usize,
4661            );
4662            assert_eq!(s, b"Rust", "descendant text not concatenated");
4663            allocator::xmlFreeImpl(content as *mut c_void);
4664
4665            free_doc(doc);
4666        }
4667    }
4668
4669    /// Verify setting the root element of a document.
4670    ///
4671    /// # Safety
4672    ///
4673    /// - `doc` and `root` must be valid pointers while `doc_set_root_element`
4674    ///   relinks them; the doc is freed with `free_doc`.
4675    #[test]
4676    fn test_doc_set_root_element() {
4677        unsafe {
4678            let doc = new_doc(ptr::null());
4679            let root = new_node(ptr::null_mut(), c_str("root"));
4680            let old = doc_set_root_element(doc, root);
4681            assert!(old.is_null());
4682            assert_eq!(doc_get_root_element(doc), root);
4683            assert_eq!((*doc).children, root);
4684            free_doc(doc);
4685        }
4686    }
4687
4688    /// Verify adding children and siblings.
4689    ///
4690    /// # Safety
4691    ///
4692    /// - The doc and nodes built by the tree helpers must be valid and linked
4693    ///   before their `parent`, `children`, `last`, `next`, and `prev` fields
4694    ///   are read; the doc is freed with `free_doc`.
4695    #[test]
4696    fn test_add_child_and_sibling() {
4697        unsafe {
4698            let doc = new_doc(ptr::null());
4699            let root = new_node(ptr::null_mut(), c_str("root"));
4700            doc_set_root_element(doc, root);
4701
4702            let child1 = new_child(root, ptr::null_mut(), c_str("child1"));
4703            assert!(!child1.is_null());
4704            assert_eq!((*child1).parent, root);
4705            assert_eq!((*root).children, child1);
4706            assert_eq!((*root).last, child1);
4707
4708            let child2 = new_child(root, ptr::null_mut(), c_str("child2"));
4709            assert!(!child2.is_null());
4710            assert_eq!((*child2).parent, root);
4711            assert_eq!((*child1).next, child2);
4712            assert_eq!((*child2).prev, child1);
4713            assert_eq!((*root).last, child2);
4714
4715            // Test add_sibling
4716            let sibling = new_node(ptr::null_mut(), c_str("sibling"));
4717            add_sibling(child2, sibling);
4718            assert_eq!((*child2).next, sibling);
4719            assert_eq!((*sibling).prev, child2);
4720            assert_eq!((*root).last, sibling);
4721
4722            free_doc(doc);
4723        }
4724    }
4725
4726    /// Verify unlinking a node.
4727    ///
4728    /// # Safety
4729    ///
4730    /// - The doc and nodes must be valid while `unlink_node` rewrites the
4731    ///   sibling links; the unlinked node is freed with `free_node` and the
4732    ///   doc with `free_doc`.
4733    #[test]
4734    fn test_unlink_node() {
4735        unsafe {
4736            let doc = new_doc(ptr::null());
4737            let root = new_node(ptr::null_mut(), c_str("root"));
4738            doc_set_root_element(doc, root);
4739
4740            let child1 = new_child(root, ptr::null_mut(), c_str("c1"));
4741            let child2 = new_child(root, ptr::null_mut(), c_str("c2"));
4742
4743            unlink_node(child1);
4744            assert!((*child1).parent.is_null());
4745            assert!((*child1).prev.is_null());
4746            assert!((*child1).next.is_null());
4747            assert_eq!((*root).children, child2);
4748            assert_eq!((*root).last, child2);
4749
4750            free_node(child1);
4751            free_doc(doc);
4752        }
4753    }
4754
4755    /// Verify creating text, comment, and PI nodes.
4756    ///
4757    /// # Safety
4758    ///
4759    /// - The `c_str` buffers must be NUL-terminated and alive while the
4760    ///   creators duplicate them; each node is freed with `free_node`.
4761    #[test]
4762    fn test_text_and_comment_nodes() {
4763        unsafe {
4764            let text = new_text(c_str("hello world"));
4765            assert!(!text.is_null());
4766            assert_eq!((*text).type_, XML_TEXT_NODE as c_int);
4767            assert!(!(*text).content.is_null());
4768            free_node(text);
4769
4770            let comment = new_comment(c_str("my comment"));
4771            assert!(!comment.is_null());
4772            assert_eq!((*comment).type_, XML_COMMENT_NODE as c_int);
4773            free_node(comment);
4774
4775            let pi = new_pi(c_str("xml"), c_str("version='1.0'"));
4776            assert!(!pi.is_null());
4777            assert_eq!((*pi).type_, XML_PI_NODE as c_int);
4778            free_node(pi);
4779        }
4780    }
4781
4782    /// Verify setting and getting a property.
4783    ///
4784    /// # Safety
4785    ///
4786    /// - `doc` and `root` must be valid while `set_prop` and `get_prop` run;
4787    ///   the value returned by `get_prop` is freed with `xmlFreeImpl` and the
4788    ///   doc with `free_doc`.
4789    #[test]
4790    fn test_set_and_get_prop() {
4791        unsafe {
4792            let doc = new_doc(ptr::null());
4793            let root = new_node(ptr::null_mut(), c_str("root"));
4794            doc_set_root_element(doc, root);
4795
4796            let attr = set_prop(root, c_str("id"), c_str("42"));
4797            assert!(!attr.is_null());
4798            assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
4799
4800            let value = get_prop(root, c_str("id"));
4801            assert!(!value.is_null());
4802            assert!(crate::abi::exports_xml2::xmlStrEqual(value, c_str("42")) != 0);
4803            allocator::xmlFreeImpl(value as *mut c_void);
4804
4805            free_doc(doc);
4806        }
4807    }
4808
4809    /// Verify removing a property.
4810    ///
4811    /// # Safety
4812    ///
4813    /// - `doc` and `root` must be valid while `set_prop`, `get_prop`, and
4814    ///   `remove_prop` run; values returned by `get_prop` are freed with
4815    ///   `xmlFreeImpl` and the doc with `free_doc`.
4816    #[test]
4817    fn test_remove_prop() {
4818        unsafe {
4819            let doc = new_doc(ptr::null());
4820            let root = new_node(ptr::null_mut(), c_str("root"));
4821            doc_set_root_element(doc, root);
4822
4823            set_prop(root, c_str("a"), c_str("1"));
4824            set_prop(root, c_str("b"), c_str("2"));
4825
4826            let value = get_prop(root, c_str("a"));
4827            assert!(!value.is_null());
4828            allocator::xmlFreeImpl(value as *mut c_void);
4829
4830            // Remove prop
4831            let attr = (*root).properties;
4832            assert!(!attr.is_null());
4833            let result = remove_prop(attr);
4834            assert_eq!(result, 0);
4835
4836            // Should no longer be found
4837            let value2 = get_prop(root, c_str("a"));
4838            assert!(value2.is_null());
4839
4840            free_doc(doc);
4841        }
4842    }
4843
4844    /// Verify namespace operations: creation, binding, and search.
4845    ///
4846    /// # Safety
4847    ///
4848    /// - `doc`, `root`, and `ns` must be valid while the namespace helpers
4849    ///   run; the `c_str` buffers must be NUL-terminated and alive for the
4850    ///   calls. The doc is freed with `free_doc`.
4851    #[test]
4852    fn test_namespace_operations() {
4853        unsafe {
4854            let doc = new_doc(ptr::null());
4855            let root = new_node(ptr::null_mut(), c_str("root"));
4856            doc_set_root_element(doc, root);
4857
4858            let ns = new_ns(root, c_str("http://example.com"), c_str("ex"));
4859            assert!(!ns.is_null());
4860            assert!(!(*root).nsDef.is_null());
4861
4862            set_ns(root, ns);
4863            assert_eq!((*root).ns, ns);
4864
4865            let found = search_ns(doc, root, c_str("ex"));
4866            assert_eq!(found, ns);
4867
4868            let found_href = search_ns_by_href(doc, root, c_str("http://example.com"));
4869            assert_eq!(found_href, ns);
4870
4871            free_doc(doc);
4872        }
4873    }
4874
4875    /// Verify creating a DTD and attaching it to a document.
4876    ///
4877    /// # Safety
4878    ///
4879    /// - `doc` and `dtd` must be valid while `new_dtd` and `get_int_subset`
4880    ///   run; the `c_str` buffers must be NUL-terminated and alive for the
4881    ///   calls. The doc is freed with `free_doc`.
4882    #[test]
4883    fn test_new_dtd() {
4884        unsafe {
4885            let doc = new_doc(ptr::null());
4886            let dtd = new_dtd(doc, c_str("root"), c_str("-//TEST//DTD"), c_str("test.dtd"));
4887            assert!(!dtd.is_null());
4888            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
4889            assert_eq!(get_int_subset(doc), dtd);
4890            free_doc(doc);
4891        }
4892    }
4893
4894    /// Verify deep-copying a node.
4895    ///
4896    /// # Safety
4897    ///
4898    /// - `doc` and `root` must be valid while `copy_node` walks the subtree;
4899    ///   the copy is freed with `free_node` and the doc with `free_doc`.
4900    #[test]
4901    fn test_copy_node_deep() {
4902        unsafe {
4903            let doc = new_doc(ptr::null());
4904            let root = new_node(ptr::null_mut(), c_str("root"));
4905            doc_set_root_element(doc, root);
4906            let _child = new_child(root, ptr::null_mut(), c_str("child"));
4907
4908            let copy = copy_node(root, 1);
4909            assert!(!copy.is_null());
4910            assert_eq!((*copy).type_, XML_ELEMENT_NODE as c_int);
4911            // Check child was copied
4912            assert!(!(*copy).children.is_null());
4913            assert_eq!((*(*copy).children).type_, XML_ELEMENT_NODE as c_int);
4914
4915            free_node(copy);
4916            free_doc(doc);
4917        }
4918    }
4919
4920    /// Verify creating a CDATA section node.
4921    ///
4922    /// # Safety
4923    ///
4924    /// - `doc` must be valid and `content` NUL-terminated and alive while
4925    ///   `new_cdata_block` runs; the node is freed with `free_node` and the
4926    ///   doc with `free_doc`.
4927    #[test]
4928    fn test_new_cdata_block() {
4929        unsafe {
4930            let doc = new_doc(ptr::null());
4931            let content = c_str("some <cdata> content");
4932            let cdata = new_cdata_block(doc, content, 20);
4933            assert!(!cdata.is_null());
4934            assert_eq!((*cdata).type_, XML_CDATA_SECTION_NODE as c_int);
4935            free_node(cdata);
4936            free_doc(doc);
4937        }
4938    }
4939
4940    /// Verify NULL handling in the tree API entry points.
4941    ///
4942    /// # Safety
4943    ///
4944    /// - NULL pointers passed to `new_doc`, `new_node`, `free_node`,
4945    ///   `free_doc`, `unlink_node`, `add_child`, and `add_sibling` must be
4946    ///   accepted without dereference (the test asserts this); all created
4947    ///   objects are freed.
4948    #[test]
4949    fn test_null_handling() {
4950        unsafe {
4951            assert!(!new_doc(ptr::null()).is_null()); // Should succeed with default version
4952            let doc = new_doc(ptr::null());
4953            assert!(!new_node(ptr::null_mut(), ptr::null()).is_null()); // Should succeed
4954            free_node(ptr::null_mut()); // Should not crash
4955            free_doc(ptr::null_mut()); // Should not crash
4956            unlink_node(ptr::null_mut()); // Should not crash
4957            assert!(add_child(ptr::null_mut(), ptr::null_mut()).is_null());
4958            assert!(add_sibling(ptr::null_mut(), ptr::null_mut()).is_null());
4959            free_doc(doc);
4960        }
4961    }
4962
4963    // ═══════════════════════════════════════════════════════════════════
4964    // Serialization Tests
4965    // ═══════════════════════════════════════════════════════════════════
4966
4967    /// Helper: compare a buffer's content to an expected string.
4968    ///
4969    /// # Safety
4970    ///
4971    /// - `buf` must be a valid pointer to an `_xmlBuffer`; its content and
4972    ///   length are read through `io::buf_content` and `io::buf_length`.
4973    unsafe fn buf_eq_str(buf: *mut _xmlBuffer, expected: &str) -> bool {
4974        let content = io::buf_content(buf);
4975        if content.is_null() {
4976            return expected.is_empty();
4977        }
4978        let len = io::buf_length(buf) as usize;
4979        if len != expected.len() {
4980            return false;
4981        }
4982        let slice = unsafe { core::slice::from_raw_parts(content, len) };
4983        slice == expected.as_bytes()
4984    }
4985
4986    /// Verify serializing an empty document.
4987    ///
4988    /// # Safety
4989    ///
4990    /// - `doc` must be a valid `_xmlDoc` and `buf` a valid `_xmlBuffer` while
4991    ///   `doc_dump` runs; `buf` is freed with `io::buf_free` and the doc with
4992    ///   `free_doc`.
4993    #[test]
4994    fn test_serialize_empty_document() {
4995        unsafe {
4996            let doc = new_doc(ptr::null());
4997            let buf = io::buf_create(-1);
4998            assert!(!buf.is_null());
4999
5000            let ret = doc_dump(buf, doc);
5001            assert!(ret >= 0);
5002
5003            // UPSTREAM-PARITY: xmlDocDump writes the declaration with no
5004            // encoding attribute (doc->encoding is NULL) and a trailing
5005            // newline after it.
5006            let expected = "<?xml version=\"1.0\"?>\n";
5007            assert!(buf_eq_str(buf, expected));
5008
5009            io::buf_free(buf);
5010            free_doc(doc);
5011        }
5012    }
5013
5014    /// Verify serializing an element with text content.
5015    ///
5016    /// # Safety
5017    ///
5018    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5019    ///   with `io::buf_free` and the doc with `free_doc`.
5020    #[test]
5021    fn test_serialize_element_with_text() {
5022        unsafe {
5023            let doc = new_doc(ptr::null());
5024            let root = new_node(ptr::null_mut(), c_str("root"));
5025            doc_set_root_element(doc, root);
5026
5027            // Add text child
5028            let text = new_text(c_str("hello world"));
5029            add_child(root, text);
5030
5031            let buf = io::buf_create(-1);
5032            assert!(!buf.is_null());
5033
5034            let ret = doc_dump(buf, doc);
5035            assert!(ret >= 0);
5036
5037            let expected = "<?xml version=\"1.0\"?>\n<root>hello world</root>\n";
5038            assert!(buf_eq_str(buf, expected));
5039
5040            io::buf_free(buf);
5041            free_doc(doc);
5042        }
5043    }
5044
5045    /// Verify serializing an element with attributes.
5046    ///
5047    /// # Safety
5048    ///
5049    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5050    ///   with `io::buf_free` and the doc with `free_doc`.
5051    #[test]
5052    fn test_serialize_element_with_attributes() {
5053        unsafe {
5054            let doc = new_doc(ptr::null());
5055            let root = new_node(ptr::null_mut(), c_str("root"));
5056            doc_set_root_element(doc, root);
5057
5058            set_prop(root, c_str("id"), c_str("42"));
5059            set_prop(root, c_str("name"), c_str("test"));
5060
5061            let buf = io::buf_create(-1);
5062            assert!(!buf.is_null());
5063
5064            let ret = doc_dump(buf, doc);
5065            assert!(ret >= 0);
5066
5067            let expected = "<?xml version=\"1.0\"?>\n<root id=\"42\" name=\"test\"/>\n";
5068            assert!(buf_eq_str(buf, expected));
5069
5070            io::buf_free(buf);
5071            free_doc(doc);
5072        }
5073    }
5074
5075    /// Verify serializing nested elements.
5076    ///
5077    /// # Safety
5078    ///
5079    /// - `doc` and `buf` must be valid while `doc_dump` walks the tree; `buf`
5080    ///   is freed with `io::buf_free` and the doc with `free_doc`.
5081    #[test]
5082    fn test_serialize_nested_elements() {
5083        unsafe {
5084            let doc = new_doc(ptr::null());
5085            let root = new_node(ptr::null_mut(), c_str("root"));
5086            doc_set_root_element(doc, root);
5087
5088            let child = new_child(root, ptr::null_mut(), c_str("child"));
5089            let grandchild = new_child(child, ptr::null_mut(), c_str("gc"));
5090            let text = new_text(c_str("text"));
5091            add_child(grandchild, text);
5092
5093            let buf = io::buf_create(-1);
5094            assert!(!buf.is_null());
5095
5096            let ret = doc_dump(buf, doc);
5097            assert!(ret >= 0);
5098
5099            let expected = "<?xml version=\"1.0\"?>\n<root><child><gc>text</gc></child></root>\n";
5100            assert!(buf_eq_str(buf, expected));
5101
5102            io::buf_free(buf);
5103            free_doc(doc);
5104        }
5105    }
5106
5107    /// Verify formatted serialization output.
5108    ///
5109    /// # Safety
5110    ///
5111    /// - `doc` and `buf` must be valid while `serialize_node` runs; `buf` is
5112    ///   freed with `io::buf_free` and the doc with `free_doc`.
5113    #[test]
5114    fn test_serialize_with_formatting() {
5115        unsafe {
5116            let doc = new_doc(ptr::null());
5117            let root = new_node(ptr::null_mut(), c_str("root"));
5118            doc_set_root_element(doc, root);
5119
5120            let child = new_child(root, ptr::null_mut(), c_str("child"));
5121            let text = new_text(c_str("text"));
5122            add_child(child, text);
5123
5124            let buf = io::buf_create(-1);
5125            assert!(!buf.is_null());
5126
5127            serialize_node(doc as *mut _xmlNode, buf, 1, 0);
5128
5129            let expected = "<?xml version=\"1.0\"?>\n<root>\n  <child>text</child>\n</root>\n";
5130            assert!(buf_eq_str(buf, expected));
5131
5132            io::buf_free(buf);
5133            free_doc(doc);
5134        }
5135    }
5136
5137    /// Verify that `&` is escaped in serialized text.
5138    ///
5139    /// # Safety
5140    ///
5141    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5142    ///   with `io::buf_free` and the doc with `free_doc`.
5143    #[test]
5144    fn test_serialize_escape_ampersand() {
5145        unsafe {
5146            let doc = new_doc(ptr::null());
5147            let root = new_node(ptr::null_mut(), c_str("root"));
5148            doc_set_root_element(doc, root);
5149
5150            let text = new_text(c_str("a & b"));
5151            add_child(root, text);
5152
5153            let buf = io::buf_create(-1);
5154            assert!(!buf.is_null());
5155
5156            let ret = doc_dump(buf, doc);
5157            assert!(ret >= 0);
5158
5159            let expected = "<?xml version=\"1.0\"?>\n<root>a &amp; b</root>\n";
5160            assert!(buf_eq_str(buf, expected));
5161
5162            io::buf_free(buf);
5163            free_doc(doc);
5164        }
5165    }
5166
5167    /// Verify that angle brackets are escaped in serialized text.
5168    ///
5169    /// # Safety
5170    ///
5171    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5172    ///   with `io::buf_free` and the doc with `free_doc`.
5173    #[test]
5174    fn test_serialize_escape_angle_brackets() {
5175        unsafe {
5176            let doc = new_doc(ptr::null());
5177            let root = new_node(ptr::null_mut(), c_str("root"));
5178            doc_set_root_element(doc, root);
5179
5180            let text = new_text(c_str("x < y > z"));
5181            add_child(root, text);
5182
5183            let buf = io::buf_create(-1);
5184            assert!(!buf.is_null());
5185
5186            let ret = doc_dump(buf, doc);
5187            assert!(ret >= 0);
5188
5189            let expected = "<?xml version=\"1.0\"?>\n<root>x &lt; y &gt; z</root>\n";
5190            assert!(buf_eq_str(buf, expected));
5191
5192            io::buf_free(buf);
5193            free_doc(doc);
5194        }
5195    }
5196
5197    /// Verify serializing a comment node.
5198    ///
5199    /// # Safety
5200    ///
5201    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5202    ///   with `io::buf_free` and the doc with `free_doc`.
5203    #[test]
5204    fn test_serialize_comment() {
5205        unsafe {
5206            let doc = new_doc(ptr::null());
5207            let root = new_node(ptr::null_mut(), c_str("root"));
5208            doc_set_root_element(doc, root);
5209
5210            let comment = new_comment(c_str("my comment"));
5211            add_child(root, comment);
5212
5213            let buf = io::buf_create(-1);
5214            assert!(!buf.is_null());
5215
5216            let ret = doc_dump(buf, doc);
5217            assert!(ret >= 0);
5218
5219            let expected = "<?xml version=\"1.0\"?>\n<root><!--my comment--></root>\n";
5220            assert!(buf_eq_str(buf, expected));
5221
5222            io::buf_free(buf);
5223            free_doc(doc);
5224        }
5225    }
5226
5227    /// Verify serializing a processing instruction.
5228    ///
5229    /// # Safety
5230    ///
5231    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5232    ///   with `io::buf_free` and the doc with `free_doc`.
5233    #[test]
5234    fn test_serialize_pi() {
5235        unsafe {
5236            let doc = new_doc(ptr::null());
5237            let root = new_node(ptr::null_mut(), c_str("root"));
5238            doc_set_root_element(doc, root);
5239
5240            let pi = new_pi(
5241                c_str("xml-stylesheet"),
5242                c_str("href=\"style.xsl\" type=\"text/xsl\""),
5243            );
5244            add_child(root, pi);
5245
5246            let buf = io::buf_create(-1);
5247            assert!(!buf.is_null());
5248
5249            let ret = doc_dump(buf, doc);
5250            assert!(ret >= 0);
5251
5252            let expected = "<?xml version=\"1.0\"?>\n<root><?xml-stylesheet href=\"style.xsl\" type=\"text/xsl\"?></root>\n";
5253            assert!(buf_eq_str(buf, expected));
5254
5255            io::buf_free(buf);
5256            free_doc(doc);
5257        }
5258    }
5259
5260    /// Verify serializing an empty element in self-closing form.
5261    ///
5262    /// # Safety
5263    ///
5264    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5265    ///   with `io::buf_free` and the doc with `free_doc`.
5266    #[test]
5267    fn test_serialize_self_closing() {
5268        unsafe {
5269            let doc = new_doc(ptr::null());
5270            let root = new_node(ptr::null_mut(), c_str("empty"));
5271            doc_set_root_element(doc, root);
5272
5273            let buf = io::buf_create(-1);
5274            assert!(!buf.is_null());
5275
5276            let ret = doc_dump(buf, doc);
5277            assert!(ret >= 0);
5278
5279            let expected = "<?xml version=\"1.0\"?>\n<empty/>\n";
5280            assert!(buf_eq_str(buf, expected));
5281
5282            io::buf_free(buf);
5283            free_doc(doc);
5284        }
5285    }
5286
5287    /// Verify dumping a node to a string.
5288    ///
5289    /// # Safety
5290    ///
5291    /// - `node` must be a valid `_xmlNode` while `dump_node` serializes it;
5292    ///   `result` is read with `xml_strlen` and freed with `xmlFreeImpl`, and
5293    ///   the node with `free_node`.
5294    #[test]
5295    fn test_dump_node_to_string() {
5296        unsafe {
5297            let node = new_node(ptr::null_mut(), c_str("foo"));
5298            let text = new_text(c_str("bar"));
5299            add_child(node, text);
5300
5301            let result = dump_node(node);
5302            assert!(!result.is_null());
5303
5304            let len = xml_strlen(result);
5305            let slice = { core::slice::from_raw_parts(result, len as usize) };
5306            assert_eq!(slice, b"<foo>bar</foo>");
5307
5308            allocator::xmlFreeImpl(result as *mut c_void);
5309            free_node(node);
5310        }
5311    }
5312
5313    /// Verify dumping a document to a string.
5314    ///
5315    /// # Safety
5316    ///
5317    /// - `doc` must be a valid `_xmlDoc` while `dump_doc` serializes it;
5318    ///   `result` is read with `xml_strlen` and freed with `xmlFreeImpl`, and
5319    ///   the doc with `free_doc`.
5320    #[test]
5321    fn test_dump_doc_to_string() {
5322        unsafe {
5323            let doc = new_doc(ptr::null());
5324            let root = new_node(ptr::null_mut(), c_str("root"));
5325            doc_set_root_element(doc, root);
5326
5327            let result = dump_doc(doc);
5328            assert!(!result.is_null());
5329
5330            let len = xml_strlen(result);
5331            let slice = { core::slice::from_raw_parts(result, len as usize) };
5332            let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
5333            assert_eq!(slice, expected.as_bytes());
5334
5335            allocator::xmlFreeImpl(result as *mut c_void);
5336            free_doc(doc);
5337        }
5338    }
5339
5340    /// Verify `xmlDocDumpFormatMemory`.
5341    ///
5342    /// # Safety
5343    ///
5344    /// - `doc` must be a valid `_xmlDoc` while the export runs; `mem` receives
5345    ///   a callee-owned buffer read as `size` bytes and freed with
5346    ///   `xmlFreeImpl`, and the doc with `free_doc`.
5347    #[test]
5348    fn test_xmlDocDumpFormatMemory() {
5349        unsafe {
5350            let doc = new_doc(ptr::null());
5351            let root = new_node(ptr::null_mut(), c_str("root"));
5352            doc_set_root_element(doc, root);
5353
5354            let mut mem: *mut xmlChar = ptr::null_mut();
5355            let mut size: c_int = 0;
5356
5357            xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 0);
5358
5359            assert!(!mem.is_null());
5360            assert!(size > 0);
5361
5362            let slice = { core::slice::from_raw_parts(mem, size as usize) };
5363            // UPSTREAM-PARITY: xmlDocDumpFormatMemory with a NULL encoding
5364            // writes no encoding attribute and a newline after each child.
5365            let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
5366            assert_eq!(slice, expected.as_bytes());
5367
5368            allocator::xmlFreeImpl(mem as *mut c_void);
5369            free_doc(doc);
5370        }
5371    }
5372
5373    /// Verify escaping of special characters in attribute values.
5374    ///
5375    /// # Safety
5376    ///
5377    /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
5378    ///   with `io::buf_free` and the doc with `free_doc`.
5379    #[test]
5380    fn test_serialize_escape_attribute() {
5381        unsafe {
5382            let doc = new_doc(ptr::null());
5383            let root = new_node(ptr::null_mut(), c_str("root"));
5384            doc_set_root_element(doc, root);
5385
5386            // Attribute with special chars
5387            set_prop(root, c_str("desc"), c_str("a < b & c \"quoted\""));
5388
5389            let buf = io::buf_create(-1);
5390            assert!(!buf.is_null());
5391
5392            let ret = doc_dump(buf, doc);
5393            assert!(ret >= 0);
5394
5395            let expected =
5396                "<?xml version=\"1.0\"?>\n<root desc=\"a &lt; b &amp; c &quot;quoted&quot;\"/>\n";
5397            assert!(buf_eq_str(buf, expected));
5398
5399            io::buf_free(buf);
5400            free_doc(doc);
5401        }
5402    }
5403}