Skip to main content

libxml_rs/xml/tree/
mod.rs

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