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