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// ═══════════════════════════════════════════════════════════════════════════════
351// Node Operations
352// ═══════════════════════════════════════════════════════════════════════════════
353
354/// Create a new XML node.
355///
356/// # UPSTREAM-PARITY
357///
358/// ```c
359/// xmlNodePtr xmlNewNode(xmlNsPtr ns, const xmlChar *name);
360/// ```
361///
362/// Creates a new element node with the given name and namespace.
363///
364/// # SAFETY
365///
366/// - `ns` may be NULL.
367/// - `name` must be a valid null-terminated string or NULL.
368pub unsafe fn new_node(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
369    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
370    if node.is_null() {
371        return ptr::null_mut();
372    }
373
374    unsafe {
375        (*node).type_ = XML_ELEMENT_NODE as c_int;
376        (*node).name = dup_xml_str(name);
377        (*node).ns = ns;
378        (*node).line = 0;
379        (*node).extra = 0;
380
381        if !ns.is_null() {
382            (*ns).context = node as *mut _xmlDoc;
383        }
384    }
385
386    node
387}
388
389/// Free a single node (without freeing children).
390///
391/// # UPSTREAM-PARITY
392///
393/// ```c
394/// void xmlFreeNode(xmlNodePtr node);
395/// ```
396///
397/// Frees a node and its properties/namespaces, but NOT its children.
398/// Children must be freed separately or reattached.
399///
400/// # SAFETY
401///
402/// - `node` must be a valid pointer to an _xmlNode, or NULL.
403pub unsafe fn free_node(node: *mut _xmlNode) {
404    if node.is_null() {
405        return;
406    }
407
408    let n = unsafe { &mut *node };
409
410    // Free properties
411    if !n.properties.is_null() {
412        free_prop_list(n.properties);
413    }
414
415    // Free namespace declarations
416    if !n.nsDef.is_null() {
417        free_ns_list(n.nsDef);
418    }
419
420    // Free the name
421    if !n.name.is_null() {
422        allocator::xmlFree(n.name as *mut c_void);
423    }
424
425    // Free content (for text/CDATA nodes)
426    if !n.content.is_null() {
427        let node_type = n.type_;
428        if node_type == XML_TEXT_NODE as c_int
429            || node_type == XML_CDATA_SECTION_NODE as c_int
430            || node_type == XML_COMMENT_NODE as c_int
431            || node_type == XML_PI_NODE as c_int
432        {
433            allocator::xmlFree(n.content as *mut c_void);
434        }
435    }
436
437    allocator::xmlFree(node as *mut c_void);
438}
439
440/// Free a linked list of nodes.
441///
442/// Frees all nodes in the list and their children recursively.
443///
444/// # SAFETY
445///
446/// - `node` must be a valid pointer to an _xmlNode, or NULL.
447pub unsafe fn free_node_list(node: *mut _xmlNode) {
448    let mut cur = node;
449    while !cur.is_null() {
450        let next = unsafe { (*cur).next };
451
452        // Free children recursively
453        if !unsafe { (*cur).children }.is_null() {
454            free_node_list(unsafe { (*cur).children });
455        }
456
457        free_node(cur);
458        cur = next;
459    }
460}
461
462/// Free a linked list of properties.
463///
464/// # SAFETY
465///
466/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
467unsafe fn free_prop_list(prop: *mut _xmlAttr) {
468    let mut cur = prop;
469    while !cur.is_null() {
470        let next = unsafe { (*cur).next };
471
472        // Free children (text nodes with value)
473        if !unsafe { (*cur).children }.is_null() {
474            free_node_list(unsafe { (*cur).children });
475        }
476
477        // Free name
478        if !unsafe { (*cur).name }.is_null() {
479            allocator::xmlFree(unsafe { (*cur).name } as *mut c_void);
480        }
481
482        allocator::xmlFree(cur as *mut c_void);
483        cur = next;
484    }
485}
486
487/// Free a linked list of namespace declarations.
488///
489/// # SAFETY
490///
491/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
492unsafe fn free_ns_list(ns: *mut _xmlNs) {
493    let mut cur = ns;
494    while !cur.is_null() {
495        let next = unsafe { (*cur).next };
496
497        // Free href and prefix
498        if !unsafe { (*cur).href }.is_null() {
499            allocator::xmlFree(unsafe { (*cur).href } as *mut c_void);
500        }
501        if !unsafe { (*cur).prefix }.is_null() {
502            allocator::xmlFree(unsafe { (*cur).prefix } as *mut c_void);
503        }
504
505        allocator::xmlFree(cur as *mut c_void);
506        cur = next;
507    }
508}
509
510/// Copy a node (shallow or deep).
511///
512/// # UPSTREAM-PARITY
513///
514/// ```c
515/// xmlNodePtr xmlCopyNode(xmlNodePtr node, int recursive);
516/// ```
517///
518/// If `recursive` is 1, children are also copied.
519/// Returns the new node, or NULL on failure.
520///
521/// # SAFETY
522///
523/// - `node` must be a valid pointer to an _xmlNode, or NULL.
524pub unsafe fn copy_node(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
525    if node.is_null() {
526        return ptr::null_mut();
527    }
528
529    let n = unsafe { &*node };
530
531    let new_node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
532    if new_node.is_null() {
533        return ptr::null_mut();
534    }
535
536    unsafe {
537        (*new_node).type_ = n.type_;
538        (*new_node).name = dup_xml_str(n.name);
539        (*new_node).line = n.line;
540        (*new_node).extra = n.extra;
541        (*new_node).psvi = n.psvi;
542        (*new_node)._private = n._private;
543
544        // Copy namespace pointer (NOT the ns declaration — just the reference)
545        (*new_node).ns = n.ns;
546
547        // Copy namespace declarations
548        if !n.nsDef.is_null() {
549            (*new_node).nsDef = copy_ns_list(n.nsDef);
550        }
551
552        // Copy content for text/CDATA/comment/PI nodes
553        let node_type = n.type_;
554        if (node_type == XML_TEXT_NODE as c_int
555            || node_type == XML_CDATA_SECTION_NODE as c_int
556            || node_type == XML_COMMENT_NODE as c_int
557            || node_type == XML_PI_NODE as c_int)
558            && !n.content.is_null()
559        {
560            (*new_node).content = dup_xml_str(n.content);
561        }
562
563        // Copy properties
564        if !n.properties.is_null() {
565            (*new_node).properties = copy_prop_list(n.properties);
566            // Update doc links on properties
567            let mut prop = (*new_node).properties;
568            while !prop.is_null() {
569                (*prop).parent = new_node;
570                if !(*prop).children.is_null() {
571                    propagate_doc((*prop).children, (*new_node).doc);
572                }
573                prop = (*prop).next;
574            }
575        }
576
577        // Copy children if recursive
578        if recursive != 0 && !n.children.is_null() {
579            (*new_node).children = copy_node_list(n.children, recursive);
580            if !(*new_node).children.is_null() {
581                (*(*new_node).children).parent = new_node;
582                (*(*new_node).children).doc = (*new_node).doc;
583                propagate_doc((*new_node).children, (*new_node).doc);
584            }
585        }
586    }
587
588    new_node
589}
590
591/// Copy a linked list of nodes.
592///
593/// Returns the first node of the new list, or NULL on failure.
594unsafe fn copy_node_list(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
595    if node.is_null() {
596        return ptr::null_mut();
597    }
598
599    let n = unsafe { &*node };
600    let new_node = copy_node(node, recursive);
601    if new_node.is_null() {
602        return ptr::null_mut();
603    }
604
605    let mut prev = new_node;
606    let mut cur = n.next;
607
608    while !cur.is_null() {
609        let new_cur = copy_node(cur, recursive);
610        if new_cur.is_null() {
611            break;
612        }
613        unsafe {
614            (*prev).next = new_cur;
615            (*new_cur).prev = prev;
616        }
617        prev = new_cur;
618        cur = unsafe { (*cur).next };
619    }
620
621    new_node
622}
623
624/// Copy a linked list of namespace declarations.
625unsafe fn copy_ns_list(ns: *const _xmlNs) -> *mut _xmlNs {
626    if ns.is_null() {
627        return ptr::null_mut();
628    }
629
630    let n = unsafe { &*ns };
631    let new_ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
632    if new_ns.is_null() {
633        return ptr::null_mut();
634    }
635
636    unsafe {
637        (*new_ns).type_ = n.type_;
638        (*new_ns).href = dup_xml_str(n.href);
639        (*new_ns).prefix = dup_xml_str(n.prefix);
640        (*new_ns)._private = n._private;
641    }
642
643    let mut prev = new_ns;
644    let mut cur = n.next;
645
646    while !cur.is_null() {
647        let c = unsafe { &*cur };
648        let new_cur = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
649        if new_cur.is_null() {
650            break;
651        }
652        unsafe {
653            (*new_cur).type_ = c.type_;
654            (*new_cur).href = dup_xml_str(c.href);
655            (*new_cur).prefix = dup_xml_str(c.prefix);
656            (*new_cur)._private = c._private;
657            (*prev).next = new_cur;
658        }
659        prev = new_cur;
660        cur = c.next;
661    }
662
663    new_ns
664}
665
666/// Copy a linked list of properties.
667unsafe fn copy_prop_list(prop: *const _xmlAttr) -> *mut _xmlAttr {
668    if prop.is_null() {
669        return ptr::null_mut();
670    }
671
672    let p = unsafe { &*prop };
673    let new_prop = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
674    if new_prop.is_null() {
675        return ptr::null_mut();
676    }
677
678    unsafe {
679        (*new_prop).type_ = p.type_;
680        (*new_prop).name = dup_xml_str(p.name);
681        (*new_prop).ns = p.ns;
682        (*new_prop).atype = p.atype;
683
684        // Copy children (text value nodes)
685        if !p.children.is_null() {
686            (*new_prop).children = copy_node_list(p.children, 1);
687            if !(*new_prop).children.is_null() {
688                (*(*new_prop).children).parent = new_prop as *mut _xmlNode;
689            }
690        }
691    }
692
693    let mut prev = new_prop;
694    let mut cur = p.next;
695
696    while !cur.is_null() {
697        let c = unsafe { &*cur };
698        let new_cur = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
699        if new_cur.is_null() {
700            break;
701        }
702        unsafe {
703            (*new_cur).type_ = c.type_;
704            (*new_cur).name = dup_xml_str(c.name);
705            (*new_cur).ns = c.ns;
706            (*new_cur).atype = c.atype;
707
708            if !c.children.is_null() {
709                (*new_cur).children = copy_node_list(c.children, 1);
710                if !(*new_cur).children.is_null() {
711                    (*(*new_cur).children).parent = new_cur as *mut _xmlNode;
712                }
713            }
714
715            (*prev).next = new_cur;
716        }
717        prev = new_cur;
718        cur = c.next;
719    }
720
721    new_prop
722}
723
724/// Propagate the document pointer to all descendants of a node.
725unsafe fn propagate_doc(node: *mut _xmlNode, doc: *mut _xmlDoc) {
726    let mut cur = node;
727    while !cur.is_null() {
728        unsafe {
729            (*cur).doc = doc;
730
731            // Propagate to properties
732            let mut prop = (*cur).properties;
733            while !prop.is_null() {
734                (*prop).doc = doc;
735                if !(*prop).children.is_null() {
736                    propagate_doc((*prop).children, doc);
737                }
738                prop = (*prop).next;
739            }
740
741            // Recurse into children
742            if !(*cur).children.is_null() {
743                propagate_doc((*cur).children, doc);
744            }
745        }
746        cur = unsafe { (*cur).next };
747    }
748}
749
750/// Unlink a node from its parent/siblings.
751///
752/// # UPSTREAM-PARITY
753///
754/// ```c
755/// void xmlUnlinkNode(xmlNodePtr node);
756/// ```
757///
758/// Removes the node from its parent's child list and sibling list.
759/// The node's parent, prev, and next pointers are cleared.
760/// The node is NOT freed — the caller is responsible for freeing it.
761///
762/// # SAFETY
763///
764/// - `node` must be a valid pointer to an _xmlNode, or NULL.
765pub unsafe fn unlink_node(node: *mut _xmlNode) {
766    if node.is_null() {
767        return;
768    }
769
770    let n = unsafe { &mut *node };
771
772    // Fix up prev/next chain
773    let prev = n.prev;
774    let next = n.next;
775
776    if !prev.is_null() {
777        unsafe { (*prev).next = next };
778    }
779    if !next.is_null() {
780        unsafe { (*next).prev = prev };
781    }
782
783    // Fix up parent's children/last pointers
784    let parent = n.parent;
785    if !parent.is_null() {
786        if unsafe { (*parent).children } == node {
787            unsafe { (*parent).children = next };
788        }
789        if unsafe { (*parent).last } == node {
790            unsafe { (*parent).last = prev };
791        }
792    }
793
794    // Also fix up doc-level children/last if node is a direct doc child
795    let doc = n.doc;
796    if !doc.is_null() && !parent.is_null() {
797        // Already handled above
798    }
799    if !doc.is_null() && parent.is_null() {
800        // Node is a direct child of the document
801        if unsafe { (*doc).children } == node {
802            unsafe { (*doc).children = next };
803        }
804        if unsafe { (*doc).last } == node {
805            unsafe { (*doc).last = prev };
806        }
807    }
808
809    // Clear the node's links
810    n.parent = ptr::null_mut();
811    n.prev = ptr::null_mut();
812    n.next = ptr::null_mut();
813}
814
815/// Add a child node to a parent.
816///
817/// # UPSTREAM-PARITY
818///
819/// ```c
820/// xmlNodePtr xmlAddChild(xmlNodePtr parent, xmlNodePtr cur);
821/// ```
822///
823/// Adds `cur` as the last child of `parent`.
824/// Returns the child, or NULL on failure.
825///
826/// # SAFETY
827///
828/// - `parent` must be a valid pointer to an _xmlNode.
829/// - `cur` must be a valid pointer to an _xmlNode.
830pub unsafe fn add_child(parent: *mut _xmlNode, cur: *mut _xmlNode) -> *mut _xmlNode {
831    if parent.is_null() || cur.is_null() {
832        return ptr::null_mut();
833    }
834
835    let p = unsafe { &mut *parent };
836    let c = unsafe { &mut *cur };
837
838    // If cur is already linked, unlink it first
839    if !c.parent.is_null() || !c.prev.is_null() || !c.next.is_null() {
840        unlink_node(cur);
841    }
842
843    // Update parent/child links
844    c.parent = parent;
845
846    if p.children.is_null() {
847        // First child
848        p.children = cur;
849        p.last = cur;
850        c.prev = ptr::null_mut();
851        c.next = ptr::null_mut();
852    } else {
853        // Append to end
854        c.prev = p.last;
855        c.next = ptr::null_mut();
856        if !p.last.is_null() {
857            unsafe { (*p.last).next = cur };
858        }
859        p.last = cur;
860    }
861
862    // Update doc
863    let doc = if !p.doc.is_null() {
864        p.doc
865    } else {
866        ptr::null_mut()
867    };
868    if !doc.is_null() && c.doc != doc {
869        propagate_doc(cur, doc);
870    }
871
872    cur
873}
874
875/// Add a sibling node after another.
876///
877/// # UPSTREAM-PARITY
878///
879/// ```c
880/// xmlNodePtr xmlAddSibling(xmlNodePtr cur, xmlNodePtr elem);
881/// ```
882///
883/// Adds `elem` as the next sibling of `cur`.
884/// Returns `elem`, or NULL on failure.
885///
886/// # SAFETY
887///
888/// - `cur` must be a valid pointer to an _xmlNode.
889/// - `elem` must be a valid pointer to an _xmlNode.
890pub unsafe fn add_sibling(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
891    if cur.is_null() || elem.is_null() {
892        return ptr::null_mut();
893    }
894
895    let c = unsafe { &mut *cur };
896
897    // If elem is already linked, unlink it first
898    let e = unsafe { &mut *elem };
899    if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
900        unlink_node(elem);
901    }
902
903    // Set parent
904    e.parent = c.parent;
905
906    // Link elem after cur
907    e.prev = cur;
908    e.next = c.next;
909
910    if !c.next.is_null() {
911        unsafe { (*c.next).prev = elem };
912    }
913    c.next = elem;
914
915    // Update parent's last if needed
916    let parent = c.parent;
917    if !parent.is_null() && unsafe { (*parent).last } == cur {
918        unsafe { (*parent).last = elem };
919    }
920
921    // Update doc
922    if !c.doc.is_null() && e.doc != c.doc {
923        propagate_doc(elem, c.doc);
924    }
925
926    elem
927}
928
929/// Create a new child element.
930///
931/// # UPSTREAM-PARITY
932///
933/// ```c
934/// xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns, const xmlChar *name);
935/// ```
936///
937/// Creates a new element and adds it as the last child of `parent`.
938///
939/// # SAFETY
940///
941/// - `parent` must be a valid pointer to an _xmlNode, or NULL.
942/// - `name` must be a valid null-terminated string or NULL.
943pub unsafe fn new_child(
944    parent: *mut _xmlNode,
945    ns: *mut _xmlNs,
946    name: *const xmlChar,
947) -> *mut _xmlNode {
948    let node = new_node(ns, name);
949    if node.is_null() {
950        return ptr::null_mut();
951    }
952
953    if !parent.is_null() {
954        add_child(parent, node);
955    }
956
957    node
958}
959
960// ═══════════════════════════════════════════════════════════════════════════════
961// Text / Content Nodes
962// ═══════════════════════════════════════════════════════════════════════════════
963
964/// Create a new text node.
965///
966/// # UPSTREAM-PARITY
967///
968/// ```c
969/// xmlNodePtr xmlNewText(const xmlChar *content);
970/// ```
971///
972/// Creates a text node with the given content.
973/// If content is NULL, creates an empty text node.
974///
975/// # SAFETY
976///
977/// - `content` must be a valid null-terminated string or NULL.
978pub unsafe fn new_text(content: *const xmlChar) -> *mut _xmlNode {
979    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
980    if node.is_null() {
981        return ptr::null_mut();
982    }
983
984    unsafe {
985        (*node).type_ = XML_TEXT_NODE as c_int;
986        (*node).name = dup_xml_str(b"text\0" as *const u8 as *const xmlChar);
987        (*node).content = if content.is_null() {
988            let empty = allocator::xmlMalloc(1) as *mut xmlChar;
989            if !empty.is_null() {
990                *empty = 0;
991            }
992            empty
993        } else {
994            dup_xml_str(content)
995        };
996        (*node).line = 0;
997    }
998
999    node
1000}
1001
1002/// Create a new comment node.
1003///
1004/// # UPSTREAM-PARITY
1005///
1006/// ```c
1007/// xmlNodePtr xmlNewComment(const xmlChar *content);
1008/// ```
1009///
1010/// Creates a comment node with the given content.
1011///
1012/// # SAFETY
1013///
1014/// - `content` must be a valid null-terminated string or NULL.
1015pub unsafe fn new_comment(content: *const xmlChar) -> *mut _xmlNode {
1016    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1017    if node.is_null() {
1018        return ptr::null_mut();
1019    }
1020
1021    unsafe {
1022        (*node).type_ = XML_COMMENT_NODE as c_int;
1023        (*node).name = dup_xml_str(b"comment\0" as *const u8 as *const xmlChar);
1024        (*node).content = dup_xml_str(content);
1025        (*node).line = 0;
1026    }
1027
1028    node
1029}
1030
1031/// Create a new processing instruction node.
1032///
1033/// # UPSTREAM-PARITY
1034///
1035/// ```c
1036/// xmlNodePtr xmlNewPI(const xmlChar *name, const xmlChar *content);
1037/// ```
1038///
1039/// Creates a PI node with the given target name and content.
1040///
1041/// # SAFETY
1042///
1043/// - `name` must be a valid null-terminated string.
1044/// - `content` must be a valid null-terminated string or NULL.
1045pub unsafe fn new_pi(name: *const xmlChar, content: *const xmlChar) -> *mut _xmlNode {
1046    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1047    if node.is_null() {
1048        return ptr::null_mut();
1049    }
1050
1051    unsafe {
1052        (*node).type_ = XML_PI_NODE as c_int;
1053        (*node).name = dup_xml_str(name);
1054        (*node).content = dup_xml_str(content);
1055        (*node).line = 0;
1056    }
1057
1058    node
1059}
1060
1061/// Create a new CDATA section node.
1062///
1063/// # UPSTREAM-PARITY
1064///
1065/// ```c
1066/// xmlNodePtr xmlNewCDataBlock(xmlDocPtr doc, const xmlChar *content, int len);
1067/// ```
1068///
1069/// Creates a CDATA section node with the given content.
1070///
1071/// # SAFETY
1072///
1073/// - `doc` may be NULL.
1074/// - `content` must be a valid pointer to a buffer of at least `len` bytes,
1075///   or NULL.
1076pub unsafe fn new_cdata_block(
1077    doc: *mut _xmlDoc,
1078    content: *const xmlChar,
1079    len: c_int,
1080) -> *mut _xmlNode {
1081    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1082    if node.is_null() {
1083        return ptr::null_mut();
1084    }
1085
1086    unsafe {
1087        (*node).type_ = XML_CDATA_SECTION_NODE as c_int;
1088        (*node).name = dup_xml_str(b"cdata\0" as *const u8 as *const xmlChar);
1089        (*node).doc = doc;
1090
1091        if !content.is_null() && len > 0 {
1092            (*node).content = allocator::xmlMalloc((len + 1) as usize) as *mut xmlChar;
1093            if !(*node).content.is_null() {
1094                ptr::copy_nonoverlapping(content, (*node).content, len as usize);
1095                *((*node).content.add(len as usize)) = 0;
1096            }
1097        } else {
1098            let empty = allocator::xmlMalloc(1) as *mut xmlChar;
1099            if !empty.is_null() {
1100                *empty = 0;
1101            }
1102            (*node).content = empty;
1103        }
1104
1105        (*node).line = 0;
1106    }
1107
1108    node
1109}
1110
1111// ═══════════════════════════════════════════════════════════════════════════════
1112// Namespace Operations
1113// ═══════════════════════════════════════════════════════════════════════════════
1114
1115/// Create a new namespace declaration.
1116///
1117/// # UPSTREAM-PARITY
1118///
1119/// ```c
1120/// xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
1121/// ```
1122///
1123/// Creates a new namespace declaration on the given node.
1124/// The namespace is added to the node's nsDef list.
1125///
1126/// If `href` is NULL, the namespace is a default namespace undeclaration.
1127/// If `prefix` is NULL, this is the default namespace (xmlns="...").
1128///
1129/// # SAFETY
1130///
1131/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1132/// - `href` must be a valid null-terminated string or NULL.
1133/// - `prefix` must be a valid null-terminated string or NULL.
1134pub unsafe fn new_ns(
1135    node: *mut _xmlNode,
1136    href: *const xmlChar,
1137    prefix: *const xmlChar,
1138) -> *mut _xmlNs {
1139    let ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1140    if ns.is_null() {
1141        return ptr::null_mut();
1142    }
1143
1144    unsafe {
1145        (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
1146        (*ns).href = dup_xml_str(href);
1147        (*ns).prefix = dup_xml_str(prefix);
1148        (*ns).context = node as *mut _xmlDoc;
1149
1150        // Add to node's nsDef list
1151        if !node.is_null() {
1152            let n = &mut *node;
1153            if n.nsDef.is_null() {
1154                n.nsDef = ns;
1155            } else {
1156                // Append to end
1157                let mut last = n.nsDef;
1158                while !(*last).next.is_null() {
1159                    last = (*last).next;
1160                }
1161                (*last).next = ns;
1162            }
1163        }
1164    }
1165
1166    ns
1167}
1168
1169/// Set the namespace of a node.
1170///
1171/// # UPSTREAM-PARITY
1172///
1173/// ```c
1174/// void xmlSetNs(xmlNodePtr node, xmlNsPtr ns);
1175/// ```
1176///
1177/// # SAFETY
1178///
1179/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1180/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
1181pub unsafe fn set_ns(node: *mut _xmlNode, ns: *mut _xmlNs) {
1182    if node.is_null() {
1183        return;
1184    }
1185    unsafe {
1186        (*node).ns = ns;
1187    }
1188}
1189
1190/// Get a list of namespaces in scope for a node.
1191///
1192/// # UPSTREAM-PARITY
1193///
1194/// ```c
1195/// xmlNsPtr *xmlGetNsList(xmlDocPtr doc, xmlNodePtr node);
1196/// ```
1197///
1198/// Returns a NULL-terminated array of namespace pointers in scope,
1199/// or NULL on failure.
1200///
1201/// # SAFETY
1202///
1203/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1204/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1205pub unsafe fn get_ns_list(doc: *mut _xmlDoc, node: *mut _xmlNode) -> *mut *mut _xmlNs {
1206    // Phase 1: basic implementation
1207    // A more complete implementation would walk the node's ancestors
1208    // and collect all in-scope namespaces.
1209    if node.is_null() {
1210        return ptr::null_mut();
1211    }
1212
1213    // Collect namespaces from this node and ancestors
1214    let mut ns_ptrs: Vec<*mut _xmlNs> = Vec::new();
1215    let mut cur = node;
1216
1217    while !cur.is_null() {
1218        let n = unsafe { &*cur };
1219        let mut ns_def = n.nsDef;
1220        while !ns_def.is_null() {
1221            // Avoid duplicates
1222            let ns = unsafe { &*ns_def };
1223            let mut found = false;
1224            for &existing in &ns_ptrs {
1225                if existing == ns_def {
1226                    found = true;
1227                    break;
1228                }
1229                let e = unsafe { &*existing };
1230                if !ns.href.is_null() && !e.href.is_null() {
1231                    let href_match =
1232                        unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, e.href) != 0 };
1233                    if href_match {
1234                        if ns.prefix.is_null() && e.prefix.is_null() {
1235                            found = true;
1236                            break;
1237                        }
1238                        if !ns.prefix.is_null() && !e.prefix.is_null() {
1239                            let prefix_match = unsafe {
1240                                crate::abi::exports_xml2::xmlStrEqual(ns.prefix, e.prefix) != 0
1241                            };
1242                            if prefix_match {
1243                                found = true;
1244                                break;
1245                            }
1246                        }
1247                    }
1248                }
1249            }
1250            if !found {
1251                ns_ptrs.push(ns_def);
1252            }
1253            ns_def = unsafe { (*ns_def).next };
1254        }
1255        cur = n.parent;
1256    }
1257
1258    if ns_ptrs.is_empty() {
1259        return ptr::null_mut();
1260    }
1261
1262    // Allocate NULL-terminated array
1263    let arr =
1264        allocator::xmlMalloc((ns_ptrs.len() + 1) * size_of::<*mut _xmlNs>()) as *mut *mut _xmlNs;
1265    if arr.is_null() {
1266        return ptr::null_mut();
1267    }
1268
1269    for (i, ns) in ns_ptrs.iter().enumerate() {
1270        unsafe { *arr.add(i) = *ns };
1271    }
1272    unsafe { *arr.add(ns_ptrs.len()) = ptr::null_mut() };
1273
1274    arr
1275}
1276
1277/// Search for a namespace by prefix.
1278///
1279/// # UPSTREAM-PARITY
1280///
1281/// ```c
1282/// xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace);
1283/// ```
1284///
1285/// Searches for a namespace declaration with the given prefix.
1286/// If `nameSpace` is NULL, searches for the default namespace.
1287///
1288/// # SAFETY
1289///
1290/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1291/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1292/// - `nameSpace` must be a valid null-terminated string or NULL.
1293pub unsafe fn search_ns(
1294    doc: *mut _xmlDoc,
1295    node: *mut _xmlNode,
1296    name_space: *const xmlChar,
1297) -> *mut _xmlNs {
1298    if node.is_null() {
1299        return ptr::null_mut();
1300    }
1301
1302    let mut cur = node;
1303    while !cur.is_null() {
1304        let n = unsafe { &*cur };
1305        let mut ns_def = n.nsDef;
1306        while !ns_def.is_null() {
1307            let ns = unsafe { &*ns_def };
1308            let match_prefix = if name_space.is_null() {
1309                // Default namespace: prefix should be NULL
1310                ns.prefix.is_null()
1311            } else {
1312                !ns.prefix.is_null()
1313                    && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, name_space) != 0 }
1314            };
1315            if match_prefix {
1316                return ns_def;
1317            }
1318            ns_def = unsafe { (*ns_def).next };
1319        }
1320        cur = n.parent;
1321    }
1322
1323    ptr::null_mut()
1324}
1325
1326/// Search for a namespace by href (URI).
1327///
1328/// # UPSTREAM-PARITY
1329///
1330/// ```c
1331/// xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar *href);
1332/// ```
1333///
1334/// Searches for a namespace declaration with the given URI.
1335///
1336/// # SAFETY
1337///
1338/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1339/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1340/// - `href` must be a valid null-terminated string or NULL.
1341pub unsafe fn search_ns_by_href(
1342    doc: *mut _xmlDoc,
1343    node: *mut _xmlNode,
1344    href: *const xmlChar,
1345) -> *mut _xmlNs {
1346    if node.is_null() || href.is_null() {
1347        return ptr::null_mut();
1348    }
1349
1350    let mut cur = node;
1351    while !cur.is_null() {
1352        let n = unsafe { &*cur };
1353        let mut ns_def = n.nsDef;
1354        while !ns_def.is_null() {
1355            let ns = unsafe { &*ns_def };
1356            if !ns.href.is_null()
1357                && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, href) != 0 }
1358            {
1359                return ns_def;
1360            }
1361            ns_def = unsafe { (*ns_def).next };
1362        }
1363        cur = n.parent;
1364    }
1365
1366    ptr::null_mut()
1367}
1368
1369// ═══════════════════════════════════════════════════════════════════════════════
1370// Attribute Operations
1371// ═══════════════════════════════════════════════════════════════════════════════
1372
1373/// Set an attribute on a node.
1374///
1375/// # UPSTREAM-PARITY
1376///
1377/// ```c
1378/// xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name, const xmlChar *value);
1379/// ```
1380///
1381/// Sets the attribute with the given name to the given value.
1382/// If the attribute already exists, its value is updated.
1383/// Creates the attribute if it doesn't exist.
1384///
1385/// Returns the attribute pointer, or NULL on failure.
1386///
1387/// # SAFETY
1388///
1389/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1390/// - `name` must be a valid null-terminated string.
1391/// - `value` must be a valid null-terminated string or NULL.
1392pub unsafe fn set_prop(
1393    node: *mut _xmlNode,
1394    name: *const xmlChar,
1395    value: *const xmlChar,
1396) -> *mut _xmlAttr {
1397    if node.is_null() || name.is_null() {
1398        return ptr::null_mut();
1399    }
1400
1401    let n = unsafe { &mut *node };
1402
1403    // Check if attribute already exists
1404    let mut existing = n.properties;
1405    while !existing.is_null() {
1406        let attr = unsafe { &*existing };
1407        if !attr.name.is_null()
1408            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
1409        {
1410            // Update existing attribute value
1411            // Free old children (text nodes)
1412            if !attr.children.is_null() {
1413                free_node_list(attr.children);
1414                // SAFETY: We need to mutate const fields
1415                let attr_mut = existing as *mut _xmlAttr;
1416                unsafe { (*attr_mut).children = ptr::null_mut() };
1417                unsafe { (*attr_mut).last = ptr::null_mut() };
1418            }
1419            // Set new value
1420            if !value.is_null() {
1421                let text = new_text(value);
1422                if !text.is_null() {
1423                    let attr_mut = existing as *mut _xmlAttr;
1424                    unsafe {
1425                        (*attr_mut).children = text;
1426                        (*attr_mut).last = text;
1427                        (*text).parent = existing as *mut _xmlNode;
1428                        (*text).doc = n.doc;
1429                    }
1430                }
1431            }
1432            return existing;
1433        }
1434        existing = unsafe { (*existing).next };
1435    }
1436
1437    // Create new attribute
1438    let attr = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1439    if attr.is_null() {
1440        return ptr::null_mut();
1441    }
1442
1443    unsafe {
1444        (*attr).type_ = XML_ATTRIBUTE_NODE as c_int;
1445        (*attr).name = dup_xml_str(name);
1446        (*attr).parent = node;
1447        (*attr).doc = n.doc;
1448        (*attr).atype = XML_ATTRIBUTE_CDATA as c_int;
1449
1450        // Set value
1451        if !value.is_null() {
1452            let text = new_text(value);
1453            if !text.is_null() {
1454                (*attr).children = text;
1455                (*attr).last = text;
1456                (*text).parent = attr as *mut _xmlNode;
1457                (*text).doc = n.doc;
1458            }
1459        }
1460
1461        // Add to node's property list
1462        if n.properties.is_null() {
1463            n.properties = attr;
1464        } else {
1465            let mut last = n.properties;
1466            while !(*last).next.is_null() {
1467                last = (*last).next;
1468            }
1469            (*last).next = attr;
1470            (*attr).prev = last;
1471        }
1472    }
1473
1474    attr
1475}
1476
1477/// Get an attribute value by name.
1478///
1479/// # UPSTREAM-PARITY
1480///
1481/// ```c
1482/// xmlChar *xmlGetProp(xmlNodePtr node, const xmlChar *name);
1483/// ```
1484///
1485/// Returns the attribute value as an xmlChar* (caller must free with xmlFree),
1486/// or NULL if the attribute doesn't exist.
1487///
1488/// # SAFETY
1489///
1490/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1491/// - `name` must be a valid null-terminated string.
1492pub unsafe fn get_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut xmlChar {
1493    if node.is_null() || name.is_null() {
1494        return ptr::null_mut();
1495    }
1496
1497    let n = unsafe { &*node };
1498    let mut cur = n.properties;
1499
1500    while !cur.is_null() {
1501        let attr = unsafe { &*cur };
1502        if !attr.name.is_null()
1503            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
1504        {
1505            // Get the text content of the attribute
1506            if !attr.children.is_null() {
1507                let text = unsafe { &*attr.children };
1508                if text.type_ == XML_TEXT_NODE as c_int && !text.content.is_null() {
1509                    return dup_xml_str(text.content);
1510                }
1511            }
1512            return dup_xml_str(b"\0" as *const u8 as *const xmlChar);
1513        }
1514        cur = unsafe { (*cur).next };
1515    }
1516
1517    ptr::null_mut()
1518}
1519
1520/// Get a namespaced attribute value.
1521///
1522/// # UPSTREAM-PARITY
1523///
1524/// ```c
1525/// xmlChar *xmlGetNsProp(xmlNodePtr node, const xmlChar *name, const xmlChar *nameSpace);
1526/// ```
1527///
1528/// Returns the attribute value, or NULL if not found.
1529///
1530/// # SAFETY
1531///
1532/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1533/// - `name` must be a valid null-terminated string.
1534/// - `nameSpace` may be NULL.
1535pub unsafe fn get_ns_prop(
1536    node: *mut _xmlNode,
1537    name: *const xmlChar,
1538    _name_space: *const xmlChar,
1539) -> *mut xmlChar {
1540    // Phase 1: simple attribute lookup (namespace-aware lookup will be
1541    // fully implemented in Phase 2+).
1542    get_prop(node, name)
1543}
1544
1545/// Set a namespaced attribute.
1546///
1547/// # UPSTREAM-PARITY
1548///
1549/// ```c
1550/// xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns, const xmlChar *name, const xmlChar *value);
1551/// ```
1552///
1553/// # SAFETY
1554///
1555/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1556/// - `ns` may be NULL.
1557/// - `name` must be a valid null-terminated string.
1558/// - `value` must be a valid null-terminated string or NULL.
1559pub unsafe fn set_ns_prop(
1560    node: *mut _xmlNode,
1561    _ns: *mut _xmlNs,
1562    name: *const xmlChar,
1563    value: *const xmlChar,
1564) -> *mut _xmlAttr {
1565    // Phase 1: use xmlSetProp (namespace-aware version will be in Phase 2+).
1566    set_prop(node, name, value)
1567}
1568
1569/// Remove a property from a node.
1570///
1571/// # UPSTREAM-PARITY
1572///
1573/// ```c
1574/// int xmlRemoveProp(xmlAttrPtr attr);
1575/// ```
1576///
1577/// Removes the attribute from its parent node and frees it.
1578/// Returns 0 on success, -1 on failure.
1579///
1580/// # SAFETY
1581///
1582/// - `attr` must be a valid pointer to an _xmlAttr, or NULL.
1583pub unsafe fn remove_prop(attr: *mut _xmlAttr) -> c_int {
1584    if attr.is_null() {
1585        return -1;
1586    }
1587
1588    let a = unsafe { &mut *attr };
1589
1590    // Unlink from parent's property list
1591    let parent = a.parent;
1592    if !parent.is_null() {
1593        let p = unsafe { &mut *parent };
1594        if p.properties == attr {
1595            p.properties = a.next;
1596        }
1597    }
1598
1599    // Fix up prev/next chain
1600    if !a.prev.is_null() {
1601        unsafe { (*a.prev).next = a.next };
1602    }
1603    if !a.next.is_null() {
1604        unsafe { (*a.next).prev = a.prev };
1605    }
1606
1607    // Free children (text value nodes)
1608    if !a.children.is_null() {
1609        free_node_list(a.children);
1610    }
1611
1612    // Free name
1613    if !a.name.is_null() {
1614        allocator::xmlFree(a.name as *mut c_void);
1615    }
1616
1617    allocator::xmlFree(attr as *mut c_void);
1618    0
1619}
1620
1621// ═══════════════════════════════════════════════════════════════════════════════
1622// DTD Operations
1623// ═══════════════════════════════════════════════════════════════════════════════
1624
1625/// Get the internal DTD subset of a document.
1626///
1627/// # UPSTREAM-PARITY
1628///
1629/// ```c
1630/// xmlDtdPtr xmlGetIntSubset(xmlDocPtr doc);
1631/// ```
1632pub fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
1633    if doc.is_null() {
1634        return ptr::null_mut();
1635    }
1636    let d = unsafe { &*doc };
1637    d.intSubset
1638}
1639
1640/// Create a new DTD node.
1641///
1642/// # UPSTREAM-PARITY
1643///
1644/// ```c
1645/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
1646///                     const xmlChar *ExternalID, const xmlChar *SystemID);
1647/// ```
1648///
1649/// Creates a new DTD and attaches it to the document.
1650///
1651/// # SAFETY
1652///
1653/// - `doc` must be a valid pointer to an _xmlDoc.
1654/// - `name` must be a valid null-terminated string or NULL.
1655/// - `ExternalID`, `SystemID` may be NULL.
1656pub unsafe fn new_dtd(
1657    doc: *mut _xmlDoc,
1658    name: *const xmlChar,
1659    ExternalID: *const xmlChar,
1660    SystemID: *const xmlChar,
1661) -> *mut _xmlDtd {
1662    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
1663    if dtd.is_null() {
1664        return ptr::null_mut();
1665    }
1666
1667    unsafe {
1668        (*dtd).type_ = XML_DTD_NODE as c_int;
1669        (*dtd).name = dup_xml_str(name);
1670        (*dtd).ExternalID = dup_xml_str(ExternalID);
1671        (*dtd).SystemID = dup_xml_str(SystemID);
1672        (*dtd).parent = doc;
1673        (*dtd).doc = doc;
1674
1675        // Attach to document
1676        if !doc.is_null() {
1677            if (*doc).intSubset.is_null() {
1678                (*doc).intSubset = dtd;
1679            }
1680        }
1681    }
1682
1683    dtd
1684}
1685
1686/// Free a DTD.
1687///
1688/// # SAFETY
1689///
1690/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1691unsafe fn free_dtd(dtd: *mut _xmlDtd) {
1692    if dtd.is_null() {
1693        return;
1694    }
1695
1696    let d = unsafe { &mut *dtd };
1697
1698    // Free name
1699    if !d.name.is_null() {
1700        allocator::xmlFree(d.name as *mut c_void);
1701    }
1702    if !d.ExternalID.is_null() {
1703        allocator::xmlFree(d.ExternalID as *mut c_void);
1704    }
1705    if !d.SystemID.is_null() {
1706        allocator::xmlFree(d.SystemID as *mut c_void);
1707    }
1708
1709    // Free children
1710    if !d.children.is_null() {
1711        free_node_list(d.children);
1712    }
1713
1714    allocator::xmlFree(dtd as *mut c_void);
1715}
1716
1717// ═══════════════════════════════════════════════════════════════════════════════
1718// Entity Operations
1719// ═══════════════════════════════════════════════════════════════════════════════
1720
1721/// Create a new entity.
1722///
1723/// # UPSTREAM-PARITY
1724///
1725/// ```c
1726/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
1727///                           const xmlChar *ExternalID, const xmlChar *SystemID,
1728///                           const xmlChar *content);
1729/// ```
1730///
1731/// # SAFETY
1732///
1733/// - `doc` may be NULL.
1734/// - `name` must be a valid null-terminated string.
1735/// - `ExternalID`, `SystemID`, `content` may be NULL.
1736pub unsafe fn new_entity(
1737    _doc: *mut _xmlDoc,
1738    name: *const xmlChar,
1739    etype: c_int,
1740    ExternalID: *const xmlChar,
1741    SystemID: *const xmlChar,
1742    content: *const xmlChar,
1743) -> *mut _xmlEntity {
1744    let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
1745    if entity.is_null() {
1746        return ptr::null_mut();
1747    }
1748
1749    unsafe {
1750        (*entity).type_ = XML_ENTITY_DECL as c_int;
1751        (*entity).name = dup_xml_str(name);
1752        (*entity).etype = etype;
1753        (*entity).ExternalID = dup_xml_str(ExternalID);
1754        (*entity).SystemID = dup_xml_str(SystemID);
1755        (*entity).content = dup_xml_str(content);
1756        (*entity).length = if content.is_null() {
1757            0
1758        } else {
1759            crate::abi::exports_xml2::xmlStrlen(content)
1760        };
1761        (*entity).flags = 0;
1762        (*entity).expandedSize = 0;
1763    }
1764
1765    entity
1766}
1767
1768/// Get a document entity by name.
1769///
1770/// # UPSTREAM-PARITY
1771///
1772/// ```c
1773/// xmlEntityPtr xmlGetDocEntity(xmlDocPtr doc, const xmlChar *name);
1774/// ```
1775///
1776/// Returns the entity, or NULL if not found.
1777///
1778/// # SAFETY
1779///
1780/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1781/// - `name` must be a valid null-terminated string.
1782pub unsafe fn get_doc_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1783    // Phase 1: minimal implementation. Full entity table lookup will be
1784    // in Phase 2+ when the DTD module is implemented.
1785    let _ = doc;
1786    let _ = name;
1787    ptr::null_mut()
1788}
1789
1790/// Get a parameter entity by name.
1791///
1792/// # UPSTREAM-PARITY
1793///
1794/// ```c
1795/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
1796/// ```
1797///
1798/// # SAFETY
1799///
1800/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1801/// - `name` must be a valid null-terminated string.
1802pub unsafe fn get_parameter_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1803    // Phase 1: minimal implementation.
1804    let _ = doc;
1805    let _ = name;
1806    ptr::null_mut()
1807}
1808
1809// ═══════════════════════════════════════════════════════════════════════════════
1810// XML Serialization
1811// ═══════════════════════════════════════════════════════════════════════════════
1812//
1813// Functions for serializing XML document/node trees to text.
1814// All output is UTF-8.
1815
1816/// Entity replacement strings (as xmlChar byte slices).
1817const ENTITY_LT: &[xmlChar] = b"&lt;";
1818const ENTITY_GT: &[xmlChar] = b"&gt;";
1819const ENTITY_AMP: &[xmlChar] = b"&amp;";
1820const ENTITY_QUOT: &[xmlChar] = b"&quot;";
1821const ENTITY_APOS: &[xmlChar] = b"&apos;";
1822
1823/// Indentation string (2 spaces).
1824const INDENT: &[xmlChar] = b"  ";
1825
1826/// XML declaration.
1827const XML_DECL: &[xmlChar] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
1828
1829/// Serialize text content with XML escaping.
1830///
1831/// Escapes `<`, `&`, and the `]]>` sequence.
1832///
1833/// # SAFETY
1834///
1835/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1836/// - `content` must be a valid pointer to `len` bytes of xmlChar data, or NULL.
1837unsafe fn serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
1838    if buf.is_null() || content.is_null() || len <= 0 {
1839        return;
1840    }
1841
1842    let mut i: c_int = 0;
1843    while i < len {
1844        let ch = unsafe { *content.add(i as usize) };
1845
1846        // Check for `]]>` sequence
1847        if ch == b']'
1848            && i + 2 < len
1849            && unsafe { *content.add(i as usize + 1) == b']' }
1850            && unsafe { *content.add(i as usize + 2) == b'>' }
1851        {
1852            // Write `]]&gt;` — escape the `>` that ends `]]>`
1853            io::buf_add(buf, &ch as *const u8, 2); // write `]]`
1854            io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
1855            i += 3;
1856            continue;
1857        }
1858
1859        match ch {
1860            b'<' => {
1861                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
1862            }
1863            b'&' => {
1864                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
1865            }
1866            _ => {
1867                io::buf_add(buf, &ch as *const u8, 1);
1868            }
1869        }
1870        i += 1;
1871    }
1872}
1873
1874/// Serialize an attribute value with XML escaping.
1875///
1876/// Escapes `<`, `&`, `"`, and the `]]>` sequence.
1877///
1878/// # SAFETY
1879///
1880/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1881/// - `value` must be a valid null-terminated xmlChar string, or NULL.
1882unsafe fn serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
1883    if buf.is_null() || value.is_null() {
1884        return;
1885    }
1886
1887    let len = xml_strlen(value);
1888    let mut i: c_int = 0;
1889    while i < len {
1890        let ch = unsafe { *value.add(i as usize) };
1891
1892        // Check for `]]>` sequence
1893        if ch == b']'
1894            && i + 2 < len
1895            && unsafe { *value.add(i as usize + 1) == b']' }
1896            && unsafe { *value.add(i as usize + 2) == b'>' }
1897        {
1898            // Write `]]&gt;` — escape the `>` that ends `]]>`
1899            io::buf_add(buf, &ch as *const u8, 2); // write `]]`
1900            io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
1901            i += 3;
1902            continue;
1903        }
1904
1905        match ch {
1906            b'<' => {
1907                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
1908            }
1909            b'&' => {
1910                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
1911            }
1912            b'"' => {
1913                io::buf_add(buf, ENTITY_QUOT.as_ptr(), ENTITY_QUOT.len() as c_int);
1914            }
1915            _ => {
1916                io::buf_add(buf, &ch as *const u8, 1);
1917            }
1918        }
1919        i += 1;
1920    }
1921}
1922
1923/// Write indentation (2 spaces per level).
1924///
1925/// # SAFETY
1926///
1927/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1928unsafe fn write_indent(buf: *mut _xmlBuffer, level: c_int) {
1929    if buf.is_null() || level <= 0 {
1930        return;
1931    }
1932    for _ in 0..level {
1933        io::buf_add(buf, INDENT.as_ptr(), INDENT.len() as c_int);
1934    }
1935}
1936
1937/// Serialize a single node's start tag + attributes.
1938///
1939/// For elements with no children, writes a self-closing tag `<name/>`.
1940///
1941/// # SAFETY
1942///
1943/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1944/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
1945unsafe fn serialize_start_tag(
1946    node: *mut _xmlNode,
1947    buf: *mut _xmlBuffer,
1948    format: c_int,
1949    level: c_int,
1950) {
1951    if node.is_null() || buf.is_null() {
1952        return;
1953    }
1954
1955    let n = unsafe { &*node };
1956
1957    // Write `<`
1958    io::buf_ccat(buf, b'<');
1959
1960    // Write element name with optional namespace prefix
1961    if !n.ns.is_null() {
1962        let ns = unsafe { &*n.ns };
1963        if !ns.prefix.is_null() {
1964            io::buf_cat(buf, ns.prefix);
1965            io::buf_ccat(buf, b':');
1966        }
1967    }
1968    if !n.name.is_null() {
1969        io::buf_cat(buf, n.name);
1970    }
1971
1972    // Write attributes
1973    let mut attr = n.properties;
1974    while !attr.is_null() {
1975        let a = unsafe { &*attr };
1976        io::buf_ccat(buf, b' ');
1977
1978        // Attribute name with optional namespace prefix
1979        if !a.ns.is_null() {
1980            let ans = unsafe { &*a.ns };
1981            if !ans.prefix.is_null() {
1982                io::buf_cat(buf, ans.prefix);
1983                io::buf_ccat(buf, b':');
1984            }
1985        }
1986        if !a.name.is_null() {
1987            io::buf_cat(buf, a.name);
1988        }
1989
1990        io::buf_add(buf, b"=\"" as *const u8, 2);
1991
1992        // Attribute value from child text node
1993        if !a.children.is_null() {
1994            let child = unsafe { &*a.children };
1995            if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
1996                serialize_attr_value(buf, child.content);
1997            }
1998        }
1999
2000        io::buf_ccat(buf, b'"');
2001
2002        attr = a.next;
2003    }
2004
2005    // Write namespace declarations
2006    let mut ns_def = n.nsDef;
2007    while !ns_def.is_null() {
2008        let nd = unsafe { &*ns_def };
2009        io::buf_add(buf, b" xmlns" as *const u8, 6);
2010        if !nd.prefix.is_null() {
2011            io::buf_ccat(buf, b':');
2012            io::buf_cat(buf, nd.prefix);
2013        }
2014        io::buf_add(buf, b"=\"" as *const u8, 2);
2015        if !nd.href.is_null() {
2016            serialize_attr_value(buf, nd.href);
2017        }
2018        io::buf_ccat(buf, b'"');
2019        ns_def = nd.next;
2020    }
2021
2022    if n.children.is_null() {
2023        // Self-closing tag
2024        io::buf_add(buf, b"/>" as *const u8, 2);
2025    } else {
2026        io::buf_ccat(buf, b'>');
2027    }
2028}
2029
2030/// Serialize a single node's end tag.
2031///
2032/// # SAFETY
2033///
2034/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2035/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2036unsafe fn serialize_end_tag(node: *mut _xmlNode, buf: *mut _xmlBuffer) {
2037    if node.is_null() || buf.is_null() {
2038        return;
2039    }
2040
2041    let n = unsafe { &*node };
2042
2043    io::buf_add(buf, b"</" as *const u8, 2);
2044    if !n.ns.is_null() {
2045        let ns = unsafe { &*n.ns };
2046        if !ns.prefix.is_null() {
2047            io::buf_cat(buf, ns.prefix);
2048            io::buf_ccat(buf, b':');
2049        }
2050    }
2051    if !n.name.is_null() {
2052        io::buf_cat(buf, n.name);
2053    }
2054    io::buf_ccat(buf, b'>');
2055}
2056
2057/// Check if a node is a "text-only" element (exactly one child which is a text node).
2058unsafe fn is_text_only_element(node: *mut _xmlNode) -> bool {
2059    if node.is_null() {
2060        return false;
2061    }
2062    let n = unsafe { &*node };
2063    if n.children.is_null() {
2064        return false;
2065    }
2066    // Only one child?
2067    if n.children != n.last {
2068        return false;
2069    }
2070    let child = unsafe { &*n.children };
2071    child.type_ == XML_TEXT_NODE as c_int
2072}
2073
2074/// Recursively serialize a node tree to a buffer.
2075///
2076/// `buf` is an `_xmlBuffer*`, `format` controls indentation (non-zero = pretty-print).
2077///
2078/// # SAFETY
2079///
2080/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2081/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2082pub(crate) unsafe fn serialize_node(
2083    node: *mut _xmlNode,
2084    buf: *mut _xmlBuffer,
2085    format: c_int,
2086    level: c_int,
2087) {
2088    if node.is_null() || buf.is_null() {
2089        return;
2090    }
2091
2092    let n = unsafe { &*node };
2093
2094    match n.type_ {
2095        t if t == XML_ELEMENT_NODE as c_int => {
2096            let is_text_only = is_text_only_element(node);
2097
2098            // Newline + indent before start tag (if formatting)
2099            if format != 0 && level > 0 {
2100                io::buf_ccat(buf, b'\n');
2101                write_indent(buf, level);
2102            }
2103
2104            serialize_start_tag(node, buf, format, level);
2105
2106            if !n.children.is_null() {
2107                if !is_text_only && format != 0 {
2108                    // Indent children for mixed/structured content
2109                    let mut child = n.children;
2110                    while !child.is_null() {
2111                        serialize_node(child, buf, format, level + 1);
2112                        child = unsafe { (*child).next };
2113                    }
2114                    io::buf_ccat(buf, b'\n');
2115                    write_indent(buf, level);
2116                } else {
2117                    // Text-only or no formatting: serialize children inline
2118                    let mut child = n.children;
2119                    while !child.is_null() {
2120                        serialize_node(child, buf, format, level + 1);
2121                        child = unsafe { (*child).next };
2122                    }
2123                }
2124                serialize_end_tag(node, buf);
2125            }
2126        }
2127        t if t == XML_TEXT_NODE as c_int => {
2128            serialize_text(buf, n.content, xml_strlen(n.content));
2129        }
2130        t if t == XML_CDATA_SECTION_NODE as c_int => {
2131            io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2132            serialize_text(buf, n.content, xml_strlen(n.content));
2133            io::buf_add(buf, b"]]>" as *const u8, 3);
2134        }
2135        t if t == XML_COMMENT_NODE as c_int => {
2136            if format != 0 && level > 0 {
2137                io::buf_ccat(buf, b'\n');
2138                write_indent(buf, level);
2139            }
2140            io::buf_add(buf, b"<!--" as *const u8, 4);
2141            if !n.content.is_null() {
2142                io::buf_cat(buf, n.content);
2143            }
2144            io::buf_add(buf, b"-->" as *const u8, 3);
2145        }
2146        t if t == XML_PI_NODE as c_int => {
2147            if format != 0 && level > 0 {
2148                io::buf_ccat(buf, b'\n');
2149                write_indent(buf, level);
2150            }
2151            io::buf_add(buf, b"<?" as *const u8, 2);
2152            if !n.name.is_null() {
2153                io::buf_cat(buf, n.name);
2154            }
2155            if !n.content.is_null() && unsafe { *n.content != 0 } {
2156                io::buf_ccat(buf, b' ');
2157                io::buf_cat(buf, n.content);
2158            }
2159            io::buf_add(buf, b"?>" as *const u8, 2);
2160        }
2161        t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2162            // XML declaration
2163            io::buf_add(buf, XML_DECL.as_ptr(), XML_DECL.len() as c_int);
2164
2165            // Newline after declaration when formatting
2166            if format != 0 {
2167                io::buf_ccat(buf, b'\n');
2168            }
2169
2170            // Serialize children
2171            let mut child = n.children;
2172            while !child.is_null() {
2173                serialize_node(child, buf, format, 0);
2174                child = unsafe { (*child).next };
2175            }
2176            if format != 0 {
2177                io::buf_ccat(buf, b'\n');
2178            }
2179        }
2180        t if t == XML_DTD_NODE as c_int => {
2181            // Skip DTD nodes in serialization for now
2182        }
2183        _ => {
2184            // For unknown types, just write content if present
2185            if !n.content.is_null() {
2186                serialize_text(buf, n.content, xml_strlen(n.content));
2187            }
2188        }
2189    }
2190}
2191
2192/// Dump a document to a buffer.
2193///
2194/// Serializes the entire document tree into `buf`.
2195/// Returns the number of bytes written, or -1 on error.
2196///
2197/// # SAFETY
2198///
2199/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2200/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2201pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2202    if buf.is_null() || doc.is_null() {
2203        return -1;
2204    }
2205
2206    let before = io::buf_length(buf);
2207    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2208    let after = io::buf_length(buf);
2209
2210    if after < 0 || before < 0 {
2211        return -1;
2212    }
2213    after - before
2214}
2215
2216/// Dump a node tree to a buffer.
2217///
2218/// Serializes the node and its descendants into `buf`.
2219/// `level` is the initial indentation level, `format` controls pretty-printing.
2220/// Returns the number of bytes written, or -1 on error.
2221///
2222/// # SAFETY
2223///
2224/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2225/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2226/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2227pub(crate) unsafe fn node_dump(
2228    buf: *mut _xmlBuffer,
2229    doc: *mut _xmlDoc,
2230    node: *mut _xmlNode,
2231    level: c_int,
2232    format: c_int,
2233) -> c_int {
2234    let _ = doc; // Used for entity resolution in full implementation
2235    if buf.is_null() || node.is_null() {
2236        return -1;
2237    }
2238
2239    let before = io::buf_length(buf);
2240    serialize_node(node, buf, format, level);
2241    let after = io::buf_length(buf);
2242
2243    if after < 0 || before < 0 {
2244        return -1;
2245    }
2246    after - before
2247}
2248
2249/// Save a document to a file.
2250///
2251/// # SAFETY
2252///
2253/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2254/// - `filename` must be a valid null-terminated C string.
2255pub(crate) unsafe fn save_doc_to_filename(
2256    doc: *mut _xmlDoc,
2257    filename: *const c_char,
2258    compression: c_int,
2259) -> c_int {
2260    if doc.is_null() || filename.is_null() {
2261        return -1;
2262    }
2263
2264    let out = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
2265    if out.is_null() {
2266        return -1;
2267    }
2268
2269    let buf = io::buf_create(-1);
2270    if buf.is_null() {
2271        io::output_buffer_close(out);
2272        return -1;
2273    }
2274
2275    let ret = doc_dump(buf, doc);
2276    if ret >= 0 {
2277        // Flush the buffer content to the output
2278        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
2279        io::output_buffer_flush(out);
2280    }
2281
2282    io::buf_free(buf);
2283    io::output_buffer_close(out);
2284    ret
2285}
2286
2287/// Save a document to a file descriptor.
2288///
2289/// # SAFETY
2290///
2291/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2292/// - `fd` must be a valid open file descriptor.
2293pub(crate) unsafe fn save_doc_to_fd(doc: *mut _xmlDoc, fd: c_int, compression: c_int) -> c_int {
2294    if doc.is_null() || fd < 0 {
2295        return -1;
2296    }
2297
2298    let out = io::output_buffer_create_fd(fd, ptr::null_mut());
2299    if out.is_null() {
2300        return -1;
2301    }
2302
2303    let buf = io::buf_create(-1);
2304    if buf.is_null() {
2305        io::output_buffer_close(out);
2306        return -1;
2307    }
2308
2309    let ret = doc_dump(buf, doc);
2310    if ret >= 0 {
2311        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
2312        io::output_buffer_flush(out);
2313    }
2314
2315    io::buf_free(buf);
2316    io::output_buffer_close(out);
2317    ret
2318}
2319
2320/// Save a document to an xmlBuffer.
2321///
2322/// # SAFETY
2323///
2324/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2325/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2326pub(crate) unsafe fn save_doc_to_buf(
2327    doc: *mut _xmlDoc,
2328    buf: *mut _xmlBuffer,
2329    compression: c_int,
2330) -> c_int {
2331    let _ = compression;
2332    if doc.is_null() || buf.is_null() {
2333        return -1;
2334    }
2335
2336    doc_dump(buf, doc)
2337}
2338
2339/// Format (pretty-print) a document to a buffer.
2340///
2341/// # SAFETY
2342///
2343/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2344/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2345pub(crate) unsafe fn save_format_doc_to_buf(
2346    doc: *mut _xmlDoc,
2347    buf: *mut _xmlBuffer,
2348    compression: c_int,
2349) -> c_int {
2350    let _ = compression;
2351    if doc.is_null() || buf.is_null() {
2352        return -1;
2353    }
2354
2355    let before = io::buf_length(buf);
2356    serialize_node(doc as *mut _xmlNode, buf, 1, 0);
2357    let after = io::buf_length(buf);
2358
2359    if after < 0 || before < 0 {
2360        return -1;
2361    }
2362    after - before
2363}
2364
2365/// Dump a node to a null-terminated string.
2366///
2367/// Returns a pointer to the string (caller must free with `xmlFree`).
2368/// Returns NULL on error.
2369///
2370/// # SAFETY
2371///
2372/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2373pub(crate) unsafe fn dump_node(node: *mut _xmlNode) -> *mut xmlChar {
2374    if node.is_null() {
2375        return ptr::null_mut();
2376    }
2377
2378    let buf = io::buf_create(-1);
2379    if buf.is_null() {
2380        return ptr::null_mut();
2381    }
2382
2383    serialize_node(node, buf, 0, 0);
2384
2385    let content = io::buf_content(buf);
2386    if content.is_null() {
2387        io::buf_free(buf);
2388        return ptr::null_mut();
2389    }
2390
2391    // Duplicate the string so we can free the buffer
2392    let result = dup_xml_str(content);
2393    io::buf_free(buf);
2394    result
2395}
2396
2397/// Dump a document to a null-terminated string.
2398///
2399/// Returns a pointer to the string (caller must free with `xmlFree`).
2400/// Returns NULL on error.
2401///
2402/// # SAFETY
2403///
2404/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2405pub(crate) unsafe fn dump_doc(doc: *mut _xmlDoc) -> *mut xmlChar {
2406    if doc.is_null() {
2407        return ptr::null_mut();
2408    }
2409
2410    let buf = io::buf_create(-1);
2411    if buf.is_null() {
2412        return ptr::null_mut();
2413    }
2414
2415    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2416
2417    let content = io::buf_content(buf);
2418    if content.is_null() {
2419        io::buf_free(buf);
2420        return ptr::null_mut();
2421    }
2422
2423    let result = dup_xml_str(content);
2424    io::buf_free(buf);
2425    result
2426}
2427
2428// ═══════════════════════════════════════════════════════════════════════════════
2429// ABI-compatible export wrappers
2430// ═══════════════════════════════════════════════════════════════════════════════
2431
2432/// Dump a node to a buffer (ABI wrapper).
2433///
2434/// # UPSTREAM-PARITY
2435///
2436/// ```c
2437/// int xmlNodeDump(xmlBufferPtr buf, xmlDocPtr doc, xmlNodePtr node, int level, int format);
2438/// ```
2439///
2440/// # SAFETY
2441///
2442/// - All pointer arguments must be valid or NULL.
2443pub(crate) unsafe fn xmlNodeDump(
2444    buf: *mut _xmlBuffer,
2445    doc: *mut _xmlDoc,
2446    node: *mut _xmlNode,
2447    level: c_int,
2448    format: c_int,
2449) -> c_int {
2450    node_dump(buf, doc, node, level, format)
2451}
2452
2453/// Dump a document to a FILE*.
2454///
2455/// # UPSTREAM-PARITY
2456///
2457/// ```c
2458/// int xmlDocDump(FILE *fp, xmlDocPtr doc);
2459/// ```
2460///
2461/// # SAFETY
2462///
2463/// - `fp` must be a valid FILE* pointer.
2464/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2465pub(crate) unsafe fn xmlDocDump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2466    if fp.is_null() || doc.is_null() {
2467        return -1;
2468    }
2469
2470    let buf = io::buf_create(-1);
2471    if buf.is_null() {
2472        return -1;
2473    }
2474
2475    let ret = doc_dump(buf, doc);
2476    if ret < 0 {
2477        io::buf_free(buf);
2478        return -1;
2479    }
2480
2481    let content = io::buf_content(buf);
2482    let len = io::buf_length(buf);
2483    if !content.is_null() && len > 0 {
2484        let written = libc::fwrite(
2485            content as *const c_void,
2486            1,
2487            len as usize,
2488            fp as *mut libc::FILE,
2489        );
2490        io::buf_free(buf);
2491        written as c_int
2492    } else {
2493        io::buf_free(buf);
2494        0
2495    }
2496}
2497
2498/// Dump a document to memory (with format flag).
2499///
2500/// # UPSTREAM-PARITY
2501///
2502/// ```c
2503/// void xmlDocDumpFormatMemory(xmlDocPtr doc, xmlChar **mem, int *size, int format);
2504/// ```
2505///
2506/// # SAFETY
2507///
2508/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2509/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
2510/// - `size` must be a valid pointer to an int that will receive the size.
2511pub(crate) unsafe fn xmlDocDumpFormatMemory(
2512    doc: *mut _xmlDoc,
2513    mem: *mut *mut xmlChar,
2514    size: *mut c_int,
2515    format: c_int,
2516) {
2517    if doc.is_null() || mem.is_null() || size.is_null() {
2518        return;
2519    }
2520
2521    let buf = io::buf_create(-1);
2522    if buf.is_null() {
2523        unsafe {
2524            *mem = ptr::null_mut();
2525            *size = 0;
2526        }
2527        return;
2528    }
2529
2530    serialize_node(doc as *mut _xmlNode, buf, format, 0);
2531
2532    let content = io::buf_content(buf);
2533    let len = io::buf_length(buf);
2534
2535    if !content.is_null() && len > 0 {
2536        // Allocate memory for the result (+1 for null terminator)
2537        let result = allocator::xmlMalloc((len + 1) as usize) as *mut xmlChar;
2538        if !result.is_null() {
2539            ptr::copy_nonoverlapping(content, result, len as usize);
2540            *result.add(len as usize) = 0;
2541            unsafe {
2542                *mem = result;
2543                *size = len;
2544            }
2545        } else {
2546            unsafe {
2547                *mem = ptr::null_mut();
2548                *size = 0;
2549            }
2550        }
2551    } else {
2552        unsafe {
2553            *mem = ptr::null_mut();
2554            *size = 0;
2555        }
2556    }
2557
2558    io::buf_free(buf);
2559}
2560
2561/// Dump a document to memory (unformatted).
2562///
2563/// # UPSTREAM-PARITY
2564///
2565/// ```c
2566/// void xmlDocDumpMemory(xmlDocPtr doc, xmlChar **mem, int *size);
2567/// ```
2568///
2569/// # SAFETY
2570///
2571/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2572/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
2573/// - `size` must be a valid pointer to an int that will receive the size.
2574pub(crate) unsafe fn xmlDocDumpMemory(doc: *mut _xmlDoc, mem: *mut *mut xmlChar, size: *mut c_int) {
2575    xmlDocDumpFormatMemory(doc, mem, size, 0)
2576}
2577
2578/// Save a document to a file (ABI wrapper).
2579///
2580/// # UPSTREAM-PARITY
2581///
2582/// ```c
2583/// int xmlSaveFile(const char *filename, xmlDocPtr cur);
2584/// ```
2585///
2586/// # SAFETY
2587///
2588/// - `filename` must be a valid null-terminated C string.
2589/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2590pub(crate) unsafe fn xmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
2591    save_doc_to_filename(cur, filename, 0)
2592}
2593
2594/// Save a document to a file with encoding.
2595///
2596/// # UPSTREAM-PARITY
2597///
2598/// ```c
2599/// int xmlSaveFileEnc(const char *filename, xmlDocPtr cur, const char *encoding);
2600/// ```
2601///
2602/// # SAFETY
2603///
2604/// - `filename` must be a valid null-terminated C string.
2605/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2606/// - `encoding` may be NULL (uses UTF-8).
2607pub(crate) unsafe fn xmlSaveFileEnc(
2608    filename: *const c_char,
2609    cur: *mut _xmlDoc,
2610    encoding: *const c_char,
2611) -> c_int {
2612    let _ = encoding; // Future: use encoding to set encoder on output buffer
2613    save_doc_to_filename(cur, filename, 0)
2614}
2615
2616/// Save a document to a file with format flag.
2617///
2618/// # UPSTREAM-PARITY
2619///
2620/// ```c
2621/// int xmlSaveFormatFile(const char *filename, xmlDocPtr cur, int format);
2622/// ```
2623///
2624/// # SAFETY
2625///
2626/// - `filename` must be a valid null-terminated C string.
2627/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2628pub(crate) unsafe fn xmlSaveFormatFile(
2629    filename: *const c_char,
2630    cur: *mut _xmlDoc,
2631    format: c_int,
2632) -> c_int {
2633    let _ = format;
2634    save_doc_to_filename(cur, filename, 0)
2635}
2636
2637/// Save a document to a file with encoding and format flag.
2638///
2639/// # UPSTREAM-PARITY
2640///
2641/// ```c
2642/// int xmlSaveFormatFileEnc(const char *filename, xmlDocPtr cur, const char *encoding, int format);
2643/// ```
2644///
2645/// # SAFETY
2646///
2647/// - `filename` must be a valid null-terminated C string.
2648/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2649/// - `encoding` may be NULL (uses UTF-8).
2650pub(crate) unsafe fn xmlSaveFormatFileEnc(
2651    filename: *const c_char,
2652    cur: *mut _xmlDoc,
2653    encoding: *const c_char,
2654    format: c_int,
2655) -> c_int {
2656    let _ = encoding;
2657    let _ = format;
2658    save_doc_to_filename(cur, filename, 0)
2659}
2660
2661/// Get the compression mode of a document.
2662///
2663/// # UPSTREAM-PARITY
2664///
2665/// ```c
2666/// int xmlGetDocCompressMode(xmlDocPtr doc);
2667/// ```
2668///
2669/// # SAFETY
2670///
2671/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2672pub(crate) unsafe fn xmlGetDocCompressMode(doc: *mut _xmlDoc) -> c_int {
2673    if doc.is_null() {
2674        return -1;
2675    }
2676    unsafe { (*doc).compression }
2677}
2678
2679/// Set the compression mode of a document.
2680///
2681/// # UPSTREAM-PARITY
2682///
2683/// ```c
2684/// void xmlSetDocCompressMode(xmlDocPtr doc, int mode);
2685/// ```
2686///
2687/// # SAFETY
2688///
2689/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2690pub(crate) unsafe fn xmlSetDocCompressMode(doc: *mut _xmlDoc, mode: c_int) {
2691    if doc.is_null() {
2692        return;
2693    }
2694    unsafe {
2695        (*doc).compression = mode;
2696    }
2697}
2698
2699#[cfg(test)]
2700mod tests {
2701    use super::*;
2702    use core::ffi::c_void;
2703
2704    fn c_str(s: &str) -> *const xmlChar {
2705        let bytes = s.as_bytes();
2706        let buf = unsafe { allocator::xmlMalloc(bytes.len() + 1) as *mut u8 };
2707        if !buf.is_null() {
2708            unsafe {
2709                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
2710                *buf.add(bytes.len()) = 0;
2711            }
2712        }
2713        buf as *const xmlChar
2714    }
2715
2716    #[test]
2717    fn test_new_free_doc() {
2718        unsafe {
2719            let doc = new_doc(ptr::null());
2720            assert!(!doc.is_null());
2721            assert_eq!((*doc).type_, XML_DOCUMENT_NODE as c_int);
2722            assert_eq!((*doc).standalone, -1);
2723            assert_eq!((*doc).doc, doc);
2724            assert!(!(*doc).version.is_null());
2725            free_doc(doc);
2726        }
2727    }
2728
2729    #[test]
2730    fn test_new_doc_with_version() {
2731        unsafe {
2732            let ver = c_str("2.0");
2733            let doc = new_doc(ver);
2734            assert!(!doc.is_null());
2735            let doc_ver = (*doc).version;
2736            assert!(!doc_ver.is_null());
2737            assert!(crate::abi::exports_xml2::xmlStrEqual(doc_ver, ver,) != 0);
2738            allocator::xmlFree(ver as *mut c_void);
2739            free_doc(doc);
2740        }
2741    }
2742
2743    #[test]
2744    fn test_new_node() {
2745        unsafe {
2746            let doc = new_doc(ptr::null());
2747            let node = new_node(ptr::null_mut(), c_str("root"));
2748            assert!(!node.is_null());
2749            assert_eq!((*node).type_, XML_ELEMENT_NODE as c_int);
2750            assert!(!(*node).name.is_null());
2751            free_node(node);
2752            free_doc(doc);
2753        }
2754    }
2755
2756    #[test]
2757    fn test_doc_set_root_element() {
2758        unsafe {
2759            let doc = new_doc(ptr::null());
2760            let root = new_node(ptr::null_mut(), c_str("root"));
2761            let old = doc_set_root_element(doc, root);
2762            assert!(old.is_null());
2763            assert_eq!(doc_get_root_element(doc), root);
2764            assert_eq!((*doc).children, root as *mut _xmlNode);
2765            free_doc(doc);
2766        }
2767    }
2768
2769    #[test]
2770    fn test_add_child_and_sibling() {
2771        unsafe {
2772            let doc = new_doc(ptr::null());
2773            let root = new_node(ptr::null_mut(), c_str("root"));
2774            doc_set_root_element(doc, root);
2775
2776            let child1 = new_child(root, ptr::null_mut(), c_str("child1"));
2777            assert!(!child1.is_null());
2778            assert_eq!((*child1).parent, root);
2779            assert_eq!((*root).children, child1);
2780            assert_eq!((*root).last, child1);
2781
2782            let child2 = new_child(root, ptr::null_mut(), c_str("child2"));
2783            assert!(!child2.is_null());
2784            assert_eq!((*child2).parent, root);
2785            assert_eq!((*child1).next, child2);
2786            assert_eq!((*child2).prev, child1);
2787            assert_eq!((*root).last, child2);
2788
2789            // Test add_sibling
2790            let sibling = new_node(ptr::null_mut(), c_str("sibling"));
2791            add_sibling(child2, sibling);
2792            assert_eq!((*child2).next, sibling);
2793            assert_eq!((*sibling).prev, child2);
2794            assert_eq!((*root).last, sibling);
2795
2796            free_doc(doc);
2797        }
2798    }
2799
2800    #[test]
2801    fn test_unlink_node() {
2802        unsafe {
2803            let doc = new_doc(ptr::null());
2804            let root = new_node(ptr::null_mut(), c_str("root"));
2805            doc_set_root_element(doc, root);
2806
2807            let child1 = new_child(root, ptr::null_mut(), c_str("c1"));
2808            let child2 = new_child(root, ptr::null_mut(), c_str("c2"));
2809
2810            unlink_node(child1);
2811            assert!((*child1).parent.is_null());
2812            assert!((*child1).prev.is_null());
2813            assert!((*child1).next.is_null());
2814            assert_eq!((*root).children, child2);
2815            assert_eq!((*root).last, child2);
2816
2817            free_node(child1);
2818            free_doc(doc);
2819        }
2820    }
2821
2822    #[test]
2823    fn test_text_and_comment_nodes() {
2824        unsafe {
2825            let text = new_text(c_str("hello world"));
2826            assert!(!text.is_null());
2827            assert_eq!((*text).type_, XML_TEXT_NODE as c_int);
2828            assert!(!(*text).content.is_null());
2829            free_node(text);
2830
2831            let comment = new_comment(c_str("my comment"));
2832            assert!(!comment.is_null());
2833            assert_eq!((*comment).type_, XML_COMMENT_NODE as c_int);
2834            free_node(comment);
2835
2836            let pi = new_pi(c_str("xml"), c_str("version='1.0'"));
2837            assert!(!pi.is_null());
2838            assert_eq!((*pi).type_, XML_PI_NODE as c_int);
2839            free_node(pi);
2840        }
2841    }
2842
2843    #[test]
2844    fn test_set_and_get_prop() {
2845        unsafe {
2846            let doc = new_doc(ptr::null());
2847            let root = new_node(ptr::null_mut(), c_str("root"));
2848            doc_set_root_element(doc, root);
2849
2850            let attr = set_prop(root, c_str("id"), c_str("42"));
2851            assert!(!attr.is_null());
2852            assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
2853
2854            let value = get_prop(root, c_str("id"));
2855            assert!(!value.is_null());
2856            assert!(crate::abi::exports_xml2::xmlStrEqual(value, c_str("42")) != 0);
2857            allocator::xmlFree(value as *mut c_void);
2858
2859            free_doc(doc);
2860        }
2861    }
2862
2863    #[test]
2864    fn test_remove_prop() {
2865        unsafe {
2866            let doc = new_doc(ptr::null());
2867            let root = new_node(ptr::null_mut(), c_str("root"));
2868            doc_set_root_element(doc, root);
2869
2870            set_prop(root, c_str("a"), c_str("1"));
2871            set_prop(root, c_str("b"), c_str("2"));
2872
2873            let value = get_prop(root, c_str("a"));
2874            assert!(!value.is_null());
2875            allocator::xmlFree(value as *mut c_void);
2876
2877            // Remove prop
2878            let attr = (*root).properties;
2879            assert!(!attr.is_null());
2880            let result = remove_prop(attr);
2881            assert_eq!(result, 0);
2882
2883            // Should no longer be found
2884            let value2 = get_prop(root, c_str("a"));
2885            assert!(value2.is_null());
2886
2887            free_doc(doc);
2888        }
2889    }
2890
2891    #[test]
2892    fn test_namespace_operations() {
2893        unsafe {
2894            let doc = new_doc(ptr::null());
2895            let root = new_node(ptr::null_mut(), c_str("root"));
2896            doc_set_root_element(doc, root);
2897
2898            let ns = new_ns(root, c_str("http://example.com"), c_str("ex"));
2899            assert!(!ns.is_null());
2900            assert!(!(*root).nsDef.is_null());
2901
2902            set_ns(root, ns);
2903            assert_eq!((*root).ns, ns);
2904
2905            let found = search_ns(doc, root, c_str("ex"));
2906            assert_eq!(found, ns);
2907
2908            let found_href = search_ns_by_href(doc, root, c_str("http://example.com"));
2909            assert_eq!(found_href, ns);
2910
2911            free_doc(doc);
2912        }
2913    }
2914
2915    #[test]
2916    fn test_new_dtd() {
2917        unsafe {
2918            let doc = new_doc(ptr::null());
2919            let dtd = new_dtd(doc, c_str("root"), c_str("-//TEST//DTD"), c_str("test.dtd"));
2920            assert!(!dtd.is_null());
2921            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
2922            assert_eq!(get_int_subset(doc), dtd);
2923            free_doc(doc);
2924        }
2925    }
2926
2927    #[test]
2928    fn test_copy_node_deep() {
2929        unsafe {
2930            let doc = new_doc(ptr::null());
2931            let root = new_node(ptr::null_mut(), c_str("root"));
2932            doc_set_root_element(doc, root);
2933            let child = new_child(root, ptr::null_mut(), c_str("child"));
2934
2935            let copy = copy_node(root, 1);
2936            assert!(!copy.is_null());
2937            assert_eq!((*copy).type_, XML_ELEMENT_NODE as c_int);
2938            // Check child was copied
2939            assert!(!(*copy).children.is_null());
2940            assert_eq!((*(*copy).children).type_, XML_ELEMENT_NODE as c_int);
2941
2942            free_node(copy);
2943            free_doc(doc);
2944        }
2945    }
2946
2947    #[test]
2948    fn test_new_cdata_block() {
2949        unsafe {
2950            let doc = new_doc(ptr::null());
2951            let content = c_str("some <cdata> content");
2952            let cdata = new_cdata_block(doc, content, 20);
2953            assert!(!cdata.is_null());
2954            assert_eq!((*cdata).type_, XML_CDATA_SECTION_NODE as c_int);
2955            free_node(cdata);
2956            free_doc(doc);
2957        }
2958    }
2959
2960    #[test]
2961    fn test_null_handling() {
2962        unsafe {
2963            assert!(new_doc(ptr::null()).is_null() == false); // Should succeed with default version
2964            let doc = new_doc(ptr::null());
2965            assert!(new_node(ptr::null_mut(), ptr::null()).is_null() == false); // Should succeed
2966            free_node(ptr::null_mut()); // Should not crash
2967            free_doc(ptr::null_mut()); // Should not crash
2968            assert!(unlink_node(ptr::null_mut()) == ()); // Should not crash
2969            assert!(add_child(ptr::null_mut(), ptr::null_mut()).is_null());
2970            assert!(add_sibling(ptr::null_mut(), ptr::null_mut()).is_null());
2971            free_doc(doc);
2972        }
2973    }
2974
2975    // ═══════════════════════════════════════════════════════════════════
2976    // Serialization Tests
2977    // ═══════════════════════════════════════════════════════════════════
2978
2979    /// Helper: compare a buffer's content to an expected string.
2980    unsafe fn buf_eq_str(buf: *mut _xmlBuffer, expected: &str) -> bool {
2981        let content = io::buf_content(buf);
2982        if content.is_null() {
2983            return expected.is_empty();
2984        }
2985        let len = io::buf_length(buf) as usize;
2986        if len != expected.len() {
2987            return false;
2988        }
2989        let slice = unsafe { core::slice::from_raw_parts(content, len) };
2990        slice == expected.as_bytes()
2991    }
2992
2993    #[test]
2994    fn test_serialize_empty_document() {
2995        unsafe {
2996            let doc = new_doc(ptr::null());
2997            let buf = io::buf_create(-1);
2998            assert!(!buf.is_null());
2999
3000            let ret = doc_dump(buf, doc);
3001            assert!(ret >= 0);
3002
3003            // Should have XML declaration
3004            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
3005            assert!(buf_eq_str(buf, expected));
3006
3007            io::buf_free(buf);
3008            free_doc(doc);
3009        }
3010    }
3011
3012    #[test]
3013    fn test_serialize_element_with_text() {
3014        unsafe {
3015            let doc = new_doc(ptr::null());
3016            let root = new_node(ptr::null_mut(), c_str("root"));
3017            doc_set_root_element(doc, root);
3018
3019            // Add text child
3020            let text = new_text(c_str("hello world"));
3021            add_child(root, text);
3022
3023            let buf = io::buf_create(-1);
3024            assert!(!buf.is_null());
3025
3026            let ret = doc_dump(buf, doc);
3027            assert!(ret >= 0);
3028
3029            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>hello world</root>";
3030            assert!(buf_eq_str(buf, expected));
3031
3032            io::buf_free(buf);
3033            free_doc(doc);
3034        }
3035    }
3036
3037    #[test]
3038    fn test_serialize_element_with_attributes() {
3039        unsafe {
3040            let doc = new_doc(ptr::null());
3041            let root = new_node(ptr::null_mut(), c_str("root"));
3042            doc_set_root_element(doc, root);
3043
3044            set_prop(root, c_str("id"), c_str("42"));
3045            set_prop(root, c_str("name"), c_str("test"));
3046
3047            let buf = io::buf_create(-1);
3048            assert!(!buf.is_null());
3049
3050            let ret = doc_dump(buf, doc);
3051            assert!(ret >= 0);
3052
3053            let expected =
3054                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root id=\"42\" name=\"test\"/>";
3055            assert!(buf_eq_str(buf, expected));
3056
3057            io::buf_free(buf);
3058            free_doc(doc);
3059        }
3060    }
3061
3062    #[test]
3063    fn test_serialize_nested_elements() {
3064        unsafe {
3065            let doc = new_doc(ptr::null());
3066            let root = new_node(ptr::null_mut(), c_str("root"));
3067            doc_set_root_element(doc, root);
3068
3069            let child = new_child(root, ptr::null_mut(), c_str("child"));
3070            let grandchild = new_child(child, ptr::null_mut(), c_str("gc"));
3071            let text = new_text(c_str("text"));
3072            add_child(grandchild, text);
3073
3074            let buf = io::buf_create(-1);
3075            assert!(!buf.is_null());
3076
3077            let ret = doc_dump(buf, doc);
3078            assert!(ret >= 0);
3079
3080            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><child><gc>text</gc></child></root>";
3081            assert!(buf_eq_str(buf, expected));
3082
3083            io::buf_free(buf);
3084            free_doc(doc);
3085        }
3086    }
3087
3088    #[test]
3089    fn test_serialize_with_formatting() {
3090        unsafe {
3091            let doc = new_doc(ptr::null());
3092            let root = new_node(ptr::null_mut(), c_str("root"));
3093            doc_set_root_element(doc, root);
3094
3095            let child = new_child(root, ptr::null_mut(), c_str("child"));
3096            let text = new_text(c_str("text"));
3097            add_child(child, text);
3098
3099            let buf = io::buf_create(-1);
3100            assert!(!buf.is_null());
3101
3102            serialize_node(doc as *mut _xmlNode, buf, 1, 0);
3103
3104            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n  <child>text</child>\n</root>\n";
3105            assert!(buf_eq_str(buf, expected));
3106
3107            io::buf_free(buf);
3108            free_doc(doc);
3109        }
3110    }
3111
3112    #[test]
3113    fn test_serialize_escape_ampersand() {
3114        unsafe {
3115            let doc = new_doc(ptr::null());
3116            let root = new_node(ptr::null_mut(), c_str("root"));
3117            doc_set_root_element(doc, root);
3118
3119            let text = new_text(c_str("a & b"));
3120            add_child(root, text);
3121
3122            let buf = io::buf_create(-1);
3123            assert!(!buf.is_null());
3124
3125            let ret = doc_dump(buf, doc);
3126            assert!(ret >= 0);
3127
3128            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>a &amp; b</root>";
3129            assert!(buf_eq_str(buf, expected));
3130
3131            io::buf_free(buf);
3132            free_doc(doc);
3133        }
3134    }
3135
3136    #[test]
3137    fn test_serialize_escape_angle_brackets() {
3138        unsafe {
3139            let doc = new_doc(ptr::null());
3140            let root = new_node(ptr::null_mut(), c_str("root"));
3141            doc_set_root_element(doc, root);
3142
3143            let text = new_text(c_str("x < y > z"));
3144            add_child(root, text);
3145
3146            let buf = io::buf_create(-1);
3147            assert!(!buf.is_null());
3148
3149            let ret = doc_dump(buf, doc);
3150            assert!(ret >= 0);
3151
3152            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>x &lt; y > z</root>";
3153            assert!(buf_eq_str(buf, expected));
3154
3155            io::buf_free(buf);
3156            free_doc(doc);
3157        }
3158    }
3159
3160    #[test]
3161    fn test_serialize_comment() {
3162        unsafe {
3163            let doc = new_doc(ptr::null());
3164            let root = new_node(ptr::null_mut(), c_str("root"));
3165            doc_set_root_element(doc, root);
3166
3167            let comment = new_comment(c_str("my comment"));
3168            add_child(root, comment);
3169
3170            let buf = io::buf_create(-1);
3171            assert!(!buf.is_null());
3172
3173            let ret = doc_dump(buf, doc);
3174            assert!(ret >= 0);
3175
3176            let expected =
3177                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><!--my comment--></root>";
3178            assert!(buf_eq_str(buf, expected));
3179
3180            io::buf_free(buf);
3181            free_doc(doc);
3182        }
3183    }
3184
3185    #[test]
3186    fn test_serialize_pi() {
3187        unsafe {
3188            let doc = new_doc(ptr::null());
3189            let root = new_node(ptr::null_mut(), c_str("root"));
3190            doc_set_root_element(doc, root);
3191
3192            let pi = new_pi(
3193                c_str("xml-stylesheet"),
3194                c_str("href=\"style.xsl\" type=\"text/xsl\""),
3195            );
3196            add_child(root, pi);
3197
3198            let buf = io::buf_create(-1);
3199            assert!(!buf.is_null());
3200
3201            let ret = doc_dump(buf, doc);
3202            assert!(ret >= 0);
3203
3204            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><?xml-stylesheet href=\"style.xsl\" type=\"text/xsl\"?></root>";
3205            assert!(buf_eq_str(buf, expected));
3206
3207            io::buf_free(buf);
3208            free_doc(doc);
3209        }
3210    }
3211
3212    #[test]
3213    fn test_serialize_self_closing() {
3214        unsafe {
3215            let doc = new_doc(ptr::null());
3216            let root = new_node(ptr::null_mut(), c_str("empty"));
3217            doc_set_root_element(doc, root);
3218
3219            let buf = io::buf_create(-1);
3220            assert!(!buf.is_null());
3221
3222            let ret = doc_dump(buf, doc);
3223            assert!(ret >= 0);
3224
3225            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><empty/>";
3226            assert!(buf_eq_str(buf, expected));
3227
3228            io::buf_free(buf);
3229            free_doc(doc);
3230        }
3231    }
3232
3233    #[test]
3234    fn test_dump_node_to_string() {
3235        unsafe {
3236            let node = new_node(ptr::null_mut(), c_str("foo"));
3237            let text = new_text(c_str("bar"));
3238            add_child(node, text);
3239
3240            let result = dump_node(node);
3241            assert!(!result.is_null());
3242
3243            let len = xml_strlen(result);
3244            let slice = unsafe { core::slice::from_raw_parts(result, len as usize) };
3245            assert_eq!(slice, b"<foo>bar</foo>");
3246
3247            allocator::xmlFree(result as *mut c_void);
3248            free_node(node);
3249        }
3250    }
3251
3252    #[test]
3253    fn test_dump_doc_to_string() {
3254        unsafe {
3255            let doc = new_doc(ptr::null());
3256            let root = new_node(ptr::null_mut(), c_str("root"));
3257            doc_set_root_element(doc, root);
3258
3259            let result = dump_doc(doc);
3260            assert!(!result.is_null());
3261
3262            let len = xml_strlen(result);
3263            let slice = unsafe { core::slice::from_raw_parts(result, len as usize) };
3264            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
3265            assert_eq!(slice, expected.as_bytes());
3266
3267            allocator::xmlFree(result as *mut c_void);
3268            free_doc(doc);
3269        }
3270    }
3271
3272    #[test]
3273    fn test_xmlDocDumpFormatMemory() {
3274        unsafe {
3275            let doc = new_doc(ptr::null());
3276            let root = new_node(ptr::null_mut(), c_str("root"));
3277            doc_set_root_element(doc, root);
3278
3279            let mut mem: *mut xmlChar = ptr::null_mut();
3280            let mut size: c_int = 0;
3281
3282            xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 0);
3283
3284            assert!(!mem.is_null());
3285            assert!(size > 0);
3286
3287            let slice = unsafe { core::slice::from_raw_parts(mem, size as usize) };
3288            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
3289            assert_eq!(slice, expected.as_bytes());
3290
3291            allocator::xmlFree(mem as *mut c_void);
3292            free_doc(doc);
3293        }
3294    }
3295
3296    #[test]
3297    fn test_serialize_escape_attribute() {
3298        unsafe {
3299            let doc = new_doc(ptr::null());
3300            let root = new_node(ptr::null_mut(), c_str("root"));
3301            doc_set_root_element(doc, root);
3302
3303            // Attribute with special chars
3304            set_prop(root, c_str("desc"), c_str("a < b & c \"quoted\""));
3305
3306            let buf = io::buf_create(-1);
3307            assert!(!buf.is_null());
3308
3309            let ret = doc_dump(buf, doc);
3310            assert!(ret >= 0);
3311
3312            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root desc=\"a &lt; b &amp; c &quot;quoted&quot;\"/>";
3313            assert!(buf_eq_str(buf, expected));
3314
3315            io::buf_free(buf);
3316            free_doc(doc);
3317        }
3318    }
3319}