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