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