Skip to main content

libxml_rs/xml/tree/
mod.rs

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