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