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