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