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