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