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 hash tables for declarations
1772    unsafe extern "C" fn free_notation_wrapper(payload: *mut c_void, _name: *mut u8) {
1773        crate::xml::dtd::free_notation(payload as *mut _xmlNotation);
1774    }
1775    unsafe extern "C" fn free_element_wrapper(payload: *mut c_void, _name: *mut u8) {
1776        crate::xml::dtd::free_element(payload as *mut _xmlElement);
1777    }
1778    unsafe extern "C" fn free_attribute_wrapper(payload: *mut c_void, _name: *mut u8) {
1779        crate::xml::dtd::free_attribute(payload as *mut _xmlAttribute);
1780    }
1781    unsafe extern "C" fn free_entity_wrapper(payload: *mut c_void, _name: *mut u8) {
1782        crate::xml::entities::free_entity(payload as *mut _xmlEntity);
1783    }
1784
1785    if !d.notations.is_null() {
1786        crate::xml::hash::hash_free(
1787            d.notations as *mut crate::xml::hash::HashTable,
1788            Some(free_notation_wrapper),
1789        );
1790        d.notations = ptr::null_mut();
1791    }
1792    if !d.elements.is_null() {
1793        crate::xml::hash::hash_free(
1794            d.elements as *mut crate::xml::hash::HashTable,
1795            Some(free_element_wrapper),
1796        );
1797        d.elements = ptr::null_mut();
1798    }
1799    if !d.attributes.is_null() {
1800        crate::xml::hash::hash_free(
1801            d.attributes as *mut crate::xml::hash::HashTable,
1802            Some(free_attribute_wrapper),
1803        );
1804        d.attributes = ptr::null_mut();
1805    }
1806    if !d.entities.is_null() {
1807        crate::xml::hash::hash_free(
1808            d.entities as *mut crate::xml::hash::HashTable,
1809            Some(free_entity_wrapper),
1810        );
1811        d.entities = ptr::null_mut();
1812    }
1813    if !d.pentities.is_null() {
1814        crate::xml::hash::hash_free(
1815            d.pentities as *mut crate::xml::hash::HashTable,
1816            Some(free_entity_wrapper),
1817        );
1818        d.pentities = ptr::null_mut();
1819    }
1820
1821    // Free children
1822    if !d.children.is_null() {
1823        free_node_list(d.children);
1824    }
1825
1826    allocator::xmlFree(dtd as *mut c_void);
1827}
1828
1829// ═══════════════════════════════════════════════════════════════════════════════
1830// Entity Operations
1831// ═══════════════════════════════════════════════════════════════════════════════
1832
1833/// Create a new entity.
1834///
1835/// # UPSTREAM-PARITY
1836///
1837/// ```c
1838/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
1839///                           const xmlChar *ExternalID, const xmlChar *SystemID,
1840///                           const xmlChar *content);
1841/// ```
1842///
1843/// # SAFETY
1844///
1845/// - `doc` may be NULL.
1846/// - `name` must be a valid null-terminated string.
1847/// - `ExternalID`, `SystemID`, `content` may be NULL.
1848pub unsafe fn new_entity(
1849    _doc: *mut _xmlDoc,
1850    name: *const xmlChar,
1851    etype: c_int,
1852    ExternalID: *const xmlChar,
1853    SystemID: *const xmlChar,
1854    content: *const xmlChar,
1855) -> *mut _xmlEntity {
1856    let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
1857    if entity.is_null() {
1858        return ptr::null_mut();
1859    }
1860
1861    unsafe {
1862        (*entity).type_ = XML_ENTITY_DECL as c_int;
1863        (*entity).name = dup_xml_str(name);
1864        (*entity).etype = etype;
1865        (*entity).ExternalID = dup_xml_str(ExternalID);
1866        (*entity).SystemID = dup_xml_str(SystemID);
1867        (*entity).content = dup_xml_str(content);
1868        (*entity).length = if content.is_null() {
1869            0
1870        } else {
1871            crate::abi::exports_xml2::xmlStrlen(content)
1872        };
1873        (*entity).flags = 0;
1874        (*entity).expandedSize = 0;
1875    }
1876
1877    entity
1878}
1879
1880/// Get a document entity by name.
1881///
1882/// # UPSTREAM-PARITY
1883///
1884/// ```c
1885/// xmlEntityPtr xmlGetDocEntity(xmlDocPtr doc, const xmlChar *name);
1886/// ```
1887///
1888/// Returns the entity, or NULL if not found.
1889///
1890/// # SAFETY
1891///
1892/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1893/// - `name` must be a valid null-terminated string.
1894pub unsafe fn get_doc_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1895    crate::xml::entities::get_entity(doc as *mut _xmlDoc, name)
1896}
1897
1898/// Get a parameter entity by name.
1899///
1900/// # UPSTREAM-PARITY
1901///
1902/// ```c
1903/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
1904/// ```
1905///
1906/// # SAFETY
1907///
1908/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1909/// - `name` must be a valid null-terminated string.
1910pub unsafe fn get_parameter_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1911    crate::xml::entities::get_parameter_entity(doc as *mut _xmlDoc, name)
1912}
1913
1914// ═══════════════════════════════════════════════════════════════════════════════
1915// XML Serialization
1916// ═══════════════════════════════════════════════════════════════════════════════
1917//
1918// Functions for serializing XML document/node trees to text.
1919// All output is UTF-8.
1920
1921/// Entity replacement strings (as xmlChar byte slices).
1922const ENTITY_LT: &[xmlChar] = b"&lt;";
1923const ENTITY_GT: &[xmlChar] = b"&gt;";
1924const ENTITY_AMP: &[xmlChar] = b"&amp;";
1925const ENTITY_QUOT: &[xmlChar] = b"&quot;";
1926const ENTITY_APOS: &[xmlChar] = b"&apos;";
1927
1928/// Indentation string (2 spaces).
1929const INDENT: &[xmlChar] = b"  ";
1930
1931/// XML declaration.
1932const XML_DECL: &[xmlChar] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
1933
1934/// Serialize text content with XML escaping.
1935///
1936/// Escapes `<`, `&`, and the `]]>` sequence.
1937///
1938/// # SAFETY
1939///
1940/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1941/// - `content` must be a valid pointer to `len` bytes of xmlChar data, or NULL.
1942pub(crate) unsafe fn serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
1943    if buf.is_null() || content.is_null() || len <= 0 {
1944        return;
1945    }
1946
1947    let mut i: c_int = 0;
1948    while i < len {
1949        let ch = unsafe { *content.add(i as usize) };
1950
1951        // Check for `]]>` sequence
1952        if ch == b']'
1953            && i + 2 < len
1954            && unsafe { *content.add(i as usize + 1) == b']' }
1955            && unsafe { *content.add(i as usize + 2) == b'>' }
1956        {
1957            // Write `]]&gt;` — escape the `>` that ends `]]>`
1958            io::buf_add(buf, &ch as *const u8, 2); // write `]]`
1959            io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
1960            i += 3;
1961            continue;
1962        }
1963
1964        match ch {
1965            b'<' => {
1966                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
1967            }
1968            b'&' => {
1969                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
1970            }
1971            b'>' => {
1972                // UPSTREAM-PARITY: libxml2 escapes `>` to `&gt;` in text content.
1973                // While the XML spec only requires escaping `>` in `]]>`, libxml2's
1974                // xmlNodeDumpOutput escapes all `>` characters via xmlEscapeEntities.
1975                io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.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/// Serialize an attribute value with XML escaping.
1986///
1987/// Escapes `<`, `&`, `"`, and the `]]>` sequence.
1988///
1989/// # SAFETY
1990///
1991/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
1992/// - `value` must be a valid null-terminated xmlChar string, or NULL.
1993pub(crate) unsafe fn serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
1994    if buf.is_null() || value.is_null() {
1995        return;
1996    }
1997
1998    let len = xml_strlen(value);
1999    let mut i: c_int = 0;
2000    while i < len {
2001        let ch = unsafe { *value.add(i as usize) };
2002
2003        // Check for `]]>` sequence
2004        if ch == b']'
2005            && i + 2 < len
2006            && unsafe { *value.add(i as usize + 1) == b']' }
2007            && unsafe { *value.add(i as usize + 2) == b'>' }
2008        {
2009            // Write `]]&gt;` — escape the `>` that ends `]]>`
2010            io::buf_add(buf, &ch as *const u8, 2); // write `]]`
2011            io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
2012            i += 3;
2013            continue;
2014        }
2015
2016        match ch {
2017            b'<' => {
2018                io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
2019            }
2020            b'&' => {
2021                io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
2022            }
2023            b'"' => {
2024                io::buf_add(buf, ENTITY_QUOT.as_ptr(), ENTITY_QUOT.len() as c_int);
2025            }
2026            _ => {
2027                io::buf_add(buf, &ch as *const u8, 1);
2028            }
2029        }
2030        i += 1;
2031    }
2032}
2033
2034/// Write indentation (2 spaces per level).
2035///
2036/// # SAFETY
2037///
2038/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2039unsafe fn write_indent(buf: *mut _xmlBuffer, level: c_int) {
2040    if buf.is_null() || level <= 0 {
2041        return;
2042    }
2043    for _ in 0..level {
2044        io::buf_add(buf, INDENT.as_ptr(), INDENT.len() as c_int);
2045    }
2046}
2047
2048/// Serialize a single node's start tag + attributes.
2049///
2050/// For elements with no children, writes a self-closing tag `<name/>`.
2051///
2052/// # SAFETY
2053///
2054/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2055/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2056unsafe fn serialize_start_tag(
2057    node: *mut _xmlNode,
2058    buf: *mut _xmlBuffer,
2059    format: c_int,
2060    level: c_int,
2061) {
2062    if node.is_null() || buf.is_null() {
2063        return;
2064    }
2065
2066    let n = unsafe { &*node };
2067
2068    // Write `<`
2069    io::buf_ccat(buf, b'<');
2070
2071    // Write element name with optional namespace prefix
2072    if !n.ns.is_null() {
2073        let ns = unsafe { &*n.ns };
2074        if !ns.prefix.is_null() {
2075            io::buf_cat(buf, ns.prefix);
2076            io::buf_ccat(buf, b':');
2077        }
2078    }
2079    if !n.name.is_null() {
2080        io::buf_cat(buf, n.name);
2081    }
2082
2083    // Write attributes
2084    let mut attr = n.properties;
2085    while !attr.is_null() {
2086        let a = unsafe { &*attr };
2087        io::buf_ccat(buf, b' ');
2088
2089        // Attribute name with optional namespace prefix
2090        if !a.ns.is_null() {
2091            let ans = unsafe { &*a.ns };
2092            if !ans.prefix.is_null() {
2093                io::buf_cat(buf, ans.prefix);
2094                io::buf_ccat(buf, b':');
2095            }
2096        }
2097        if !a.name.is_null() {
2098            io::buf_cat(buf, a.name);
2099        }
2100
2101        io::buf_add(buf, b"=\"" as *const u8, 2);
2102
2103        // Attribute value from child text node
2104        if !a.children.is_null() {
2105            let child = unsafe { &*a.children };
2106            if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2107                serialize_attr_value(buf, child.content);
2108            }
2109        }
2110
2111        io::buf_ccat(buf, b'"');
2112
2113        attr = a.next;
2114    }
2115
2116    // Write namespace declarations
2117    let mut ns_def = n.nsDef;
2118    while !ns_def.is_null() {
2119        let nd = unsafe { &*ns_def };
2120        io::buf_add(buf, b" xmlns" as *const u8, 6);
2121        if !nd.prefix.is_null() {
2122            io::buf_ccat(buf, b':');
2123            io::buf_cat(buf, nd.prefix);
2124        }
2125        io::buf_add(buf, b"=\"" as *const u8, 2);
2126        if !nd.href.is_null() {
2127            serialize_attr_value(buf, nd.href);
2128        }
2129        io::buf_ccat(buf, b'"');
2130        ns_def = nd.next;
2131    }
2132
2133    if n.children.is_null() {
2134        // Self-closing tag
2135        io::buf_add(buf, b"/>" as *const u8, 2);
2136    } else {
2137        io::buf_ccat(buf, b'>');
2138    }
2139}
2140
2141/// Serialize a single node's end tag.
2142///
2143/// # SAFETY
2144///
2145/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2146/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2147unsafe fn serialize_end_tag(node: *mut _xmlNode, buf: *mut _xmlBuffer) {
2148    if node.is_null() || buf.is_null() {
2149        return;
2150    }
2151
2152    let n = unsafe { &*node };
2153
2154    io::buf_add(buf, b"</" as *const u8, 2);
2155    if !n.ns.is_null() {
2156        let ns = unsafe { &*n.ns };
2157        if !ns.prefix.is_null() {
2158            io::buf_cat(buf, ns.prefix);
2159            io::buf_ccat(buf, b':');
2160        }
2161    }
2162    if !n.name.is_null() {
2163        io::buf_cat(buf, n.name);
2164    }
2165    io::buf_ccat(buf, b'>');
2166}
2167
2168/// Check if a node is a "text-only" element (exactly one child which is a text node).
2169unsafe fn is_text_only_element(node: *mut _xmlNode) -> bool {
2170    if node.is_null() {
2171        return false;
2172    }
2173    let n = unsafe { &*node };
2174    if n.children.is_null() {
2175        return false;
2176    }
2177    // Only one child?
2178    if n.children != n.last {
2179        return false;
2180    }
2181    let child = unsafe { &*n.children };
2182    child.type_ == XML_TEXT_NODE as c_int
2183}
2184
2185/// Recursively serialize a node tree to a buffer.
2186///
2187/// `buf` is an `_xmlBuffer*`, `format` controls indentation (non-zero = pretty-print).
2188///
2189/// # SAFETY
2190///
2191/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2192/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2193pub(crate) unsafe fn serialize_node(
2194    node: *mut _xmlNode,
2195    buf: *mut _xmlBuffer,
2196    format: c_int,
2197    level: c_int,
2198) {
2199    if node.is_null() || buf.is_null() {
2200        return;
2201    }
2202
2203    let n = unsafe { &*node };
2204
2205    match n.type_ {
2206        t if t == XML_ELEMENT_NODE as c_int => {
2207            let is_text_only = is_text_only_element(node);
2208
2209            // Newline + indent before start tag (if formatting)
2210            if format != 0 && level > 0 {
2211                io::buf_ccat(buf, b'\n');
2212                write_indent(buf, level);
2213            }
2214
2215            serialize_start_tag(node, buf, format, level);
2216
2217            if !n.children.is_null() {
2218                if !is_text_only && format != 0 {
2219                    // Indent children for mixed/structured content
2220                    let mut child = n.children;
2221                    while !child.is_null() {
2222                        serialize_node(child, buf, format, level + 1);
2223                        child = unsafe { (*child).next };
2224                    }
2225                    io::buf_ccat(buf, b'\n');
2226                    write_indent(buf, level);
2227                } else {
2228                    // Text-only or no formatting: serialize children inline
2229                    let mut child = n.children;
2230                    while !child.is_null() {
2231                        serialize_node(child, buf, format, level + 1);
2232                        child = unsafe { (*child).next };
2233                    }
2234                }
2235                serialize_end_tag(node, buf);
2236            }
2237        }
2238        t if t == XML_TEXT_NODE as c_int => {
2239            serialize_text(buf, n.content, xml_strlen(n.content));
2240        }
2241        t if t == XML_CDATA_SECTION_NODE as c_int => {
2242            io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2243            serialize_text(buf, n.content, xml_strlen(n.content));
2244            io::buf_add(buf, b"]]>" as *const u8, 3);
2245        }
2246        t if t == XML_COMMENT_NODE as c_int => {
2247            if format != 0 && level > 0 {
2248                io::buf_ccat(buf, b'\n');
2249                write_indent(buf, level);
2250            }
2251            io::buf_add(buf, b"<!--" as *const u8, 4);
2252            if !n.content.is_null() {
2253                io::buf_cat(buf, n.content);
2254            }
2255            io::buf_add(buf, b"-->" as *const u8, 3);
2256        }
2257        t if t == XML_PI_NODE as c_int => {
2258            if format != 0 && level > 0 {
2259                io::buf_ccat(buf, b'\n');
2260                write_indent(buf, level);
2261            }
2262            io::buf_add(buf, b"<?" as *const u8, 2);
2263            if !n.name.is_null() {
2264                io::buf_cat(buf, n.name);
2265            }
2266            if !n.content.is_null() && unsafe { *n.content != 0 } {
2267                io::buf_ccat(buf, b' ');
2268                io::buf_cat(buf, n.content);
2269            }
2270            io::buf_add(buf, b"?>" as *const u8, 2);
2271        }
2272        t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2273            // XML declaration
2274            io::buf_add(buf, XML_DECL.as_ptr(), XML_DECL.len() as c_int);
2275
2276            // Newline after declaration when formatting
2277            if format != 0 {
2278                io::buf_ccat(buf, b'\n');
2279            }
2280
2281            // Serialize children
2282            let mut child = n.children;
2283            while !child.is_null() {
2284                serialize_node(child, buf, format, 0);
2285                child = unsafe { (*child).next };
2286            }
2287            if format != 0 {
2288                io::buf_ccat(buf, b'\n');
2289            }
2290        }
2291        t if t == XML_DTD_NODE as c_int => {
2292            // Skip DTD nodes in serialization for now
2293        }
2294        _ => {
2295            // For unknown types, just write content if present
2296            if !n.content.is_null() {
2297                serialize_text(buf, n.content, xml_strlen(n.content));
2298            }
2299        }
2300    }
2301}
2302
2303/// Dump a document to a buffer.
2304///
2305/// Serializes the entire document tree into `buf`.
2306/// Returns the number of bytes written, or -1 on error.
2307///
2308/// # SAFETY
2309///
2310/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2311/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2312pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2313    if buf.is_null() || doc.is_null() {
2314        return -1;
2315    }
2316
2317    let before = io::buf_length(buf);
2318    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2319    let after = io::buf_length(buf);
2320
2321    if after < 0 || before < 0 {
2322        return -1;
2323    }
2324    after - before
2325}
2326
2327/// Dump a node tree to a buffer.
2328///
2329/// Serializes the node and its descendants into `buf`.
2330/// `level` is the initial indentation level, `format` controls pretty-printing.
2331/// Returns the number of bytes written, or -1 on error.
2332///
2333/// # SAFETY
2334///
2335/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2336/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2337/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2338pub(crate) unsafe fn node_dump(
2339    buf: *mut _xmlBuffer,
2340    doc: *mut _xmlDoc,
2341    node: *mut _xmlNode,
2342    level: c_int,
2343    format: c_int,
2344) -> c_int {
2345    let _ = doc; // Used for entity resolution in full implementation
2346    if buf.is_null() || node.is_null() {
2347        return -1;
2348    }
2349
2350    let before = io::buf_length(buf);
2351    serialize_node(node, buf, format, level);
2352    let after = io::buf_length(buf);
2353
2354    if after < 0 || before < 0 {
2355        return -1;
2356    }
2357    after - before
2358}
2359
2360/// Save a document to a file.
2361///
2362/// # SAFETY
2363///
2364/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2365/// - `filename` must be a valid null-terminated C string.
2366pub(crate) unsafe fn save_doc_to_filename(
2367    doc: *mut _xmlDoc,
2368    filename: *const c_char,
2369    compression: c_int,
2370) -> c_int {
2371    if doc.is_null() || filename.is_null() {
2372        return -1;
2373    }
2374
2375    let out = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
2376    if out.is_null() {
2377        return -1;
2378    }
2379
2380    let buf = io::buf_create(-1);
2381    if buf.is_null() {
2382        io::output_buffer_close(out);
2383        return -1;
2384    }
2385
2386    let ret = doc_dump(buf, doc);
2387    if ret >= 0 {
2388        // Flush the buffer content to the output
2389        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
2390        io::output_buffer_flush(out);
2391    }
2392
2393    io::buf_free(buf);
2394    io::output_buffer_close(out);
2395    ret
2396}
2397
2398/// Save a document to a file descriptor.
2399///
2400/// # SAFETY
2401///
2402/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2403/// - `fd` must be a valid open file descriptor.
2404pub(crate) unsafe fn save_doc_to_fd(doc: *mut _xmlDoc, fd: c_int, compression: c_int) -> c_int {
2405    if doc.is_null() || fd < 0 {
2406        return -1;
2407    }
2408
2409    let out = io::output_buffer_create_fd(fd, ptr::null_mut());
2410    if out.is_null() {
2411        return -1;
2412    }
2413
2414    let buf = io::buf_create(-1);
2415    if buf.is_null() {
2416        io::output_buffer_close(out);
2417        return -1;
2418    }
2419
2420    let ret = doc_dump(buf, doc);
2421    if ret >= 0 {
2422        io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
2423        io::output_buffer_flush(out);
2424    }
2425
2426    io::buf_free(buf);
2427    io::output_buffer_close(out);
2428    ret
2429}
2430
2431/// Save a document to an xmlBuffer.
2432///
2433/// # SAFETY
2434///
2435/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2436/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2437pub(crate) unsafe fn save_doc_to_buf(
2438    doc: *mut _xmlDoc,
2439    buf: *mut _xmlBuffer,
2440    compression: c_int,
2441) -> c_int {
2442    let _ = compression;
2443    if doc.is_null() || buf.is_null() {
2444        return -1;
2445    }
2446
2447    doc_dump(buf, doc)
2448}
2449
2450/// Format (pretty-print) a document to a buffer.
2451///
2452/// # SAFETY
2453///
2454/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2455/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
2456pub(crate) unsafe fn save_format_doc_to_buf(
2457    doc: *mut _xmlDoc,
2458    buf: *mut _xmlBuffer,
2459    compression: c_int,
2460) -> c_int {
2461    let _ = compression;
2462    if doc.is_null() || buf.is_null() {
2463        return -1;
2464    }
2465
2466    let before = io::buf_length(buf);
2467    serialize_node(doc as *mut _xmlNode, buf, 1, 0);
2468    let after = io::buf_length(buf);
2469
2470    if after < 0 || before < 0 {
2471        return -1;
2472    }
2473    after - before
2474}
2475
2476/// Dump a node to a null-terminated string.
2477///
2478/// Returns a pointer to the string (caller must free with `xmlFree`).
2479/// Returns NULL on error.
2480///
2481/// # SAFETY
2482///
2483/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
2484pub(crate) unsafe fn dump_node(node: *mut _xmlNode) -> *mut xmlChar {
2485    if node.is_null() {
2486        return ptr::null_mut();
2487    }
2488
2489    let buf = io::buf_create(-1);
2490    if buf.is_null() {
2491        return ptr::null_mut();
2492    }
2493
2494    serialize_node(node, buf, 0, 0);
2495
2496    let content = io::buf_content(buf);
2497    if content.is_null() {
2498        io::buf_free(buf);
2499        return ptr::null_mut();
2500    }
2501
2502    // Duplicate the string so we can free the buffer
2503    let result = dup_xml_str(content);
2504    io::buf_free(buf);
2505    result
2506}
2507
2508/// Dump a document to a null-terminated string.
2509///
2510/// Returns a pointer to the string (caller must free with `xmlFree`).
2511/// Returns NULL on error.
2512///
2513/// # SAFETY
2514///
2515/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2516pub(crate) unsafe fn dump_doc(doc: *mut _xmlDoc) -> *mut xmlChar {
2517    if doc.is_null() {
2518        return ptr::null_mut();
2519    }
2520
2521    let buf = io::buf_create(-1);
2522    if buf.is_null() {
2523        return ptr::null_mut();
2524    }
2525
2526    serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2527
2528    let content = io::buf_content(buf);
2529    if content.is_null() {
2530        io::buf_free(buf);
2531        return ptr::null_mut();
2532    }
2533
2534    let result = dup_xml_str(content);
2535    io::buf_free(buf);
2536    result
2537}
2538
2539// ═══════════════════════════════════════════════════════════════════════════════
2540// ABI-compatible export wrappers
2541// ═══════════════════════════════════════════════════════════════════════════════
2542
2543/// Dump a node to a buffer (ABI wrapper).
2544///
2545/// # UPSTREAM-PARITY
2546///
2547/// ```c
2548/// int xmlNodeDump(xmlBufferPtr buf, xmlDocPtr doc, xmlNodePtr node, int level, int format);
2549/// ```
2550///
2551/// # SAFETY
2552///
2553/// - All pointer arguments must be valid or NULL.
2554pub(crate) unsafe fn xmlNodeDump(
2555    buf: *mut _xmlBuffer,
2556    doc: *mut _xmlDoc,
2557    node: *mut _xmlNode,
2558    level: c_int,
2559    format: c_int,
2560) -> c_int {
2561    node_dump(buf, doc, node, level, format)
2562}
2563
2564/// Dump a document to a FILE*.
2565///
2566/// # UPSTREAM-PARITY
2567///
2568/// ```c
2569/// int xmlDocDump(FILE *fp, xmlDocPtr doc);
2570/// ```
2571///
2572/// # SAFETY
2573///
2574/// - `fp` must be a valid FILE* pointer.
2575/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2576pub(crate) unsafe fn xmlDocDump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2577    if fp.is_null() || doc.is_null() {
2578        return -1;
2579    }
2580
2581    let buf = io::buf_create(-1);
2582    if buf.is_null() {
2583        return -1;
2584    }
2585
2586    let ret = doc_dump(buf, doc);
2587    if ret < 0 {
2588        io::buf_free(buf);
2589        return -1;
2590    }
2591
2592    let content = io::buf_content(buf);
2593    let len = io::buf_length(buf);
2594    if !content.is_null() && len > 0 {
2595        let written = libc::fwrite(
2596            content as *const c_void,
2597            1,
2598            len as usize,
2599            fp as *mut libc::FILE,
2600        );
2601        io::buf_free(buf);
2602        written as c_int
2603    } else {
2604        io::buf_free(buf);
2605        0
2606    }
2607}
2608
2609/// Dump a document to memory (with format flag).
2610///
2611/// # UPSTREAM-PARITY
2612///
2613/// ```c
2614/// void xmlDocDumpFormatMemory(xmlDocPtr doc, xmlChar **mem, int *size, int format);
2615/// ```
2616///
2617/// # SAFETY
2618///
2619/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2620/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
2621/// - `size` must be a valid pointer to an int that will receive the size.
2622pub(crate) unsafe fn xmlDocDumpFormatMemory(
2623    doc: *mut _xmlDoc,
2624    mem: *mut *mut xmlChar,
2625    size: *mut c_int,
2626    format: c_int,
2627) {
2628    if doc.is_null() || mem.is_null() || size.is_null() {
2629        return;
2630    }
2631
2632    let buf = io::buf_create(-1);
2633    if buf.is_null() {
2634        unsafe {
2635            *mem = ptr::null_mut();
2636            *size = 0;
2637        }
2638        return;
2639    }
2640
2641    serialize_node(doc as *mut _xmlNode, buf, format, 0);
2642
2643    let content = io::buf_content(buf);
2644    let len = io::buf_length(buf);
2645
2646    if !content.is_null() && len > 0 {
2647        // Allocate memory for the result (+1 for null terminator)
2648        let result = allocator::xmlMalloc((len + 1) as usize) as *mut xmlChar;
2649        if !result.is_null() {
2650            ptr::copy_nonoverlapping(content, result, len as usize);
2651            *result.add(len as usize) = 0;
2652            unsafe {
2653                *mem = result;
2654                *size = len;
2655            }
2656        } else {
2657            unsafe {
2658                *mem = ptr::null_mut();
2659                *size = 0;
2660            }
2661        }
2662    } else {
2663        unsafe {
2664            *mem = ptr::null_mut();
2665            *size = 0;
2666        }
2667    }
2668
2669    io::buf_free(buf);
2670}
2671
2672/// Dump a document to memory (unformatted).
2673///
2674/// # UPSTREAM-PARITY
2675///
2676/// ```c
2677/// void xmlDocDumpMemory(xmlDocPtr doc, xmlChar **mem, int *size);
2678/// ```
2679///
2680/// # SAFETY
2681///
2682/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2683/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
2684/// - `size` must be a valid pointer to an int that will receive the size.
2685pub(crate) unsafe fn xmlDocDumpMemory(doc: *mut _xmlDoc, mem: *mut *mut xmlChar, size: *mut c_int) {
2686    xmlDocDumpFormatMemory(doc, mem, size, 0)
2687}
2688
2689/// Save a document to a file (ABI wrapper).
2690///
2691/// # UPSTREAM-PARITY
2692///
2693/// ```c
2694/// int xmlSaveFile(const char *filename, xmlDocPtr cur);
2695/// ```
2696///
2697/// # SAFETY
2698///
2699/// - `filename` must be a valid null-terminated C string.
2700/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2701pub(crate) unsafe fn xmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
2702    save_doc_to_filename(cur, filename, 0)
2703}
2704
2705/// Save a document to a file with encoding.
2706///
2707/// # UPSTREAM-PARITY
2708///
2709/// ```c
2710/// int xmlSaveFileEnc(const char *filename, xmlDocPtr cur, const char *encoding);
2711/// ```
2712///
2713/// # SAFETY
2714///
2715/// - `filename` must be a valid null-terminated C string.
2716/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2717/// - `encoding` may be NULL (uses UTF-8).
2718pub(crate) unsafe fn xmlSaveFileEnc(
2719    filename: *const c_char,
2720    cur: *mut _xmlDoc,
2721    encoding: *const c_char,
2722) -> c_int {
2723    let _ = encoding; // Future: use encoding to set encoder on output buffer
2724    save_doc_to_filename(cur, filename, 0)
2725}
2726
2727/// Save a document to a file with format flag.
2728///
2729/// # UPSTREAM-PARITY
2730///
2731/// ```c
2732/// int xmlSaveFormatFile(const char *filename, xmlDocPtr cur, int format);
2733/// ```
2734///
2735/// # SAFETY
2736///
2737/// - `filename` must be a valid null-terminated C string.
2738/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2739pub(crate) unsafe fn xmlSaveFormatFile(
2740    filename: *const c_char,
2741    cur: *mut _xmlDoc,
2742    format: c_int,
2743) -> c_int {
2744    let _ = format;
2745    save_doc_to_filename(cur, filename, 0)
2746}
2747
2748/// Save a document to a file with encoding and format flag.
2749///
2750/// # UPSTREAM-PARITY
2751///
2752/// ```c
2753/// int xmlSaveFormatFileEnc(const char *filename, xmlDocPtr cur, const char *encoding, int format);
2754/// ```
2755///
2756/// # SAFETY
2757///
2758/// - `filename` must be a valid null-terminated C string.
2759/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
2760/// - `encoding` may be NULL (uses UTF-8).
2761pub(crate) unsafe fn xmlSaveFormatFileEnc(
2762    filename: *const c_char,
2763    cur: *mut _xmlDoc,
2764    encoding: *const c_char,
2765    format: c_int,
2766) -> c_int {
2767    let _ = encoding;
2768    let _ = format;
2769    save_doc_to_filename(cur, filename, 0)
2770}
2771
2772/// Get the compression mode of a document.
2773///
2774/// # UPSTREAM-PARITY
2775///
2776/// ```c
2777/// int xmlGetDocCompressMode(xmlDocPtr doc);
2778/// ```
2779///
2780/// # SAFETY
2781///
2782/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2783pub(crate) unsafe fn xmlGetDocCompressMode(doc: *mut _xmlDoc) -> c_int {
2784    if doc.is_null() {
2785        return -1;
2786    }
2787    unsafe { (*doc).compression }
2788}
2789
2790/// Set the compression mode of a document.
2791///
2792/// # UPSTREAM-PARITY
2793///
2794/// ```c
2795/// void xmlSetDocCompressMode(xmlDocPtr doc, int mode);
2796/// ```
2797///
2798/// # SAFETY
2799///
2800/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
2801pub(crate) unsafe fn xmlSetDocCompressMode(doc: *mut _xmlDoc, mode: c_int) {
2802    if doc.is_null() {
2803        return;
2804    }
2805    unsafe {
2806        (*doc).compression = mode;
2807    }
2808}
2809
2810#[cfg(test)]
2811mod tests {
2812    use super::*;
2813    use core::ffi::c_void;
2814
2815    fn c_str(s: &str) -> *const xmlChar {
2816        let bytes = s.as_bytes();
2817        let buf = unsafe { allocator::xmlMalloc(bytes.len() + 1) as *mut u8 };
2818        if !buf.is_null() {
2819            unsafe {
2820                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
2821                *buf.add(bytes.len()) = 0;
2822            }
2823        }
2824        buf as *const xmlChar
2825    }
2826
2827    #[test]
2828    fn test_new_free_doc() {
2829        unsafe {
2830            let doc = new_doc(ptr::null());
2831            assert!(!doc.is_null());
2832            assert_eq!((*doc).type_, XML_DOCUMENT_NODE as c_int);
2833            assert_eq!((*doc).standalone, -1);
2834            assert_eq!((*doc).doc, doc);
2835            assert!(!(*doc).version.is_null());
2836            free_doc(doc);
2837        }
2838    }
2839
2840    #[test]
2841    fn test_new_doc_with_version() {
2842        unsafe {
2843            let ver = c_str("2.0");
2844            let doc = new_doc(ver);
2845            assert!(!doc.is_null());
2846            let doc_ver = (*doc).version;
2847            assert!(!doc_ver.is_null());
2848            assert!(crate::abi::exports_xml2::xmlStrEqual(doc_ver, ver,) != 0);
2849            allocator::xmlFree(ver as *mut c_void);
2850            free_doc(doc);
2851        }
2852    }
2853
2854    #[test]
2855    fn test_new_node() {
2856        unsafe {
2857            let doc = new_doc(ptr::null());
2858            let node = new_node(ptr::null_mut(), c_str("root"));
2859            assert!(!node.is_null());
2860            assert_eq!((*node).type_, XML_ELEMENT_NODE as c_int);
2861            assert!(!(*node).name.is_null());
2862            free_node(node);
2863            free_doc(doc);
2864        }
2865    }
2866
2867    #[test]
2868    fn test_doc_set_root_element() {
2869        unsafe {
2870            let doc = new_doc(ptr::null());
2871            let root = new_node(ptr::null_mut(), c_str("root"));
2872            let old = doc_set_root_element(doc, root);
2873            assert!(old.is_null());
2874            assert_eq!(doc_get_root_element(doc), root);
2875            assert_eq!((*doc).children, root as *mut _xmlNode);
2876            free_doc(doc);
2877        }
2878    }
2879
2880    #[test]
2881    fn test_add_child_and_sibling() {
2882        unsafe {
2883            let doc = new_doc(ptr::null());
2884            let root = new_node(ptr::null_mut(), c_str("root"));
2885            doc_set_root_element(doc, root);
2886
2887            let child1 = new_child(root, ptr::null_mut(), c_str("child1"));
2888            assert!(!child1.is_null());
2889            assert_eq!((*child1).parent, root);
2890            assert_eq!((*root).children, child1);
2891            assert_eq!((*root).last, child1);
2892
2893            let child2 = new_child(root, ptr::null_mut(), c_str("child2"));
2894            assert!(!child2.is_null());
2895            assert_eq!((*child2).parent, root);
2896            assert_eq!((*child1).next, child2);
2897            assert_eq!((*child2).prev, child1);
2898            assert_eq!((*root).last, child2);
2899
2900            // Test add_sibling
2901            let sibling = new_node(ptr::null_mut(), c_str("sibling"));
2902            add_sibling(child2, sibling);
2903            assert_eq!((*child2).next, sibling);
2904            assert_eq!((*sibling).prev, child2);
2905            assert_eq!((*root).last, sibling);
2906
2907            free_doc(doc);
2908        }
2909    }
2910
2911    #[test]
2912    fn test_unlink_node() {
2913        unsafe {
2914            let doc = new_doc(ptr::null());
2915            let root = new_node(ptr::null_mut(), c_str("root"));
2916            doc_set_root_element(doc, root);
2917
2918            let child1 = new_child(root, ptr::null_mut(), c_str("c1"));
2919            let child2 = new_child(root, ptr::null_mut(), c_str("c2"));
2920
2921            unlink_node(child1);
2922            assert!((*child1).parent.is_null());
2923            assert!((*child1).prev.is_null());
2924            assert!((*child1).next.is_null());
2925            assert_eq!((*root).children, child2);
2926            assert_eq!((*root).last, child2);
2927
2928            free_node(child1);
2929            free_doc(doc);
2930        }
2931    }
2932
2933    #[test]
2934    fn test_text_and_comment_nodes() {
2935        unsafe {
2936            let text = new_text(c_str("hello world"));
2937            assert!(!text.is_null());
2938            assert_eq!((*text).type_, XML_TEXT_NODE as c_int);
2939            assert!(!(*text).content.is_null());
2940            free_node(text);
2941
2942            let comment = new_comment(c_str("my comment"));
2943            assert!(!comment.is_null());
2944            assert_eq!((*comment).type_, XML_COMMENT_NODE as c_int);
2945            free_node(comment);
2946
2947            let pi = new_pi(c_str("xml"), c_str("version='1.0'"));
2948            assert!(!pi.is_null());
2949            assert_eq!((*pi).type_, XML_PI_NODE as c_int);
2950            free_node(pi);
2951        }
2952    }
2953
2954    #[test]
2955    fn test_set_and_get_prop() {
2956        unsafe {
2957            let doc = new_doc(ptr::null());
2958            let root = new_node(ptr::null_mut(), c_str("root"));
2959            doc_set_root_element(doc, root);
2960
2961            let attr = set_prop(root, c_str("id"), c_str("42"));
2962            assert!(!attr.is_null());
2963            assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
2964
2965            let value = get_prop(root, c_str("id"));
2966            assert!(!value.is_null());
2967            assert!(crate::abi::exports_xml2::xmlStrEqual(value, c_str("42")) != 0);
2968            allocator::xmlFree(value as *mut c_void);
2969
2970            free_doc(doc);
2971        }
2972    }
2973
2974    #[test]
2975    fn test_remove_prop() {
2976        unsafe {
2977            let doc = new_doc(ptr::null());
2978            let root = new_node(ptr::null_mut(), c_str("root"));
2979            doc_set_root_element(doc, root);
2980
2981            set_prop(root, c_str("a"), c_str("1"));
2982            set_prop(root, c_str("b"), c_str("2"));
2983
2984            let value = get_prop(root, c_str("a"));
2985            assert!(!value.is_null());
2986            allocator::xmlFree(value as *mut c_void);
2987
2988            // Remove prop
2989            let attr = (*root).properties;
2990            assert!(!attr.is_null());
2991            let result = remove_prop(attr);
2992            assert_eq!(result, 0);
2993
2994            // Should no longer be found
2995            let value2 = get_prop(root, c_str("a"));
2996            assert!(value2.is_null());
2997
2998            free_doc(doc);
2999        }
3000    }
3001
3002    #[test]
3003    fn test_namespace_operations() {
3004        unsafe {
3005            let doc = new_doc(ptr::null());
3006            let root = new_node(ptr::null_mut(), c_str("root"));
3007            doc_set_root_element(doc, root);
3008
3009            let ns = new_ns(root, c_str("http://example.com"), c_str("ex"));
3010            assert!(!ns.is_null());
3011            assert!(!(*root).nsDef.is_null());
3012
3013            set_ns(root, ns);
3014            assert_eq!((*root).ns, ns);
3015
3016            let found = search_ns(doc, root, c_str("ex"));
3017            assert_eq!(found, ns);
3018
3019            let found_href = search_ns_by_href(doc, root, c_str("http://example.com"));
3020            assert_eq!(found_href, ns);
3021
3022            free_doc(doc);
3023        }
3024    }
3025
3026    #[test]
3027    fn test_new_dtd() {
3028        unsafe {
3029            let doc = new_doc(ptr::null());
3030            let dtd = new_dtd(doc, c_str("root"), c_str("-//TEST//DTD"), c_str("test.dtd"));
3031            assert!(!dtd.is_null());
3032            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
3033            assert_eq!(get_int_subset(doc), dtd);
3034            free_doc(doc);
3035        }
3036    }
3037
3038    #[test]
3039    fn test_copy_node_deep() {
3040        unsafe {
3041            let doc = new_doc(ptr::null());
3042            let root = new_node(ptr::null_mut(), c_str("root"));
3043            doc_set_root_element(doc, root);
3044            let child = new_child(root, ptr::null_mut(), c_str("child"));
3045
3046            let copy = copy_node(root, 1);
3047            assert!(!copy.is_null());
3048            assert_eq!((*copy).type_, XML_ELEMENT_NODE as c_int);
3049            // Check child was copied
3050            assert!(!(*copy).children.is_null());
3051            assert_eq!((*(*copy).children).type_, XML_ELEMENT_NODE as c_int);
3052
3053            free_node(copy);
3054            free_doc(doc);
3055        }
3056    }
3057
3058    #[test]
3059    fn test_new_cdata_block() {
3060        unsafe {
3061            let doc = new_doc(ptr::null());
3062            let content = c_str("some <cdata> content");
3063            let cdata = new_cdata_block(doc, content, 20);
3064            assert!(!cdata.is_null());
3065            assert_eq!((*cdata).type_, XML_CDATA_SECTION_NODE as c_int);
3066            free_node(cdata);
3067            free_doc(doc);
3068        }
3069    }
3070
3071    #[test]
3072    fn test_null_handling() {
3073        unsafe {
3074            assert!(new_doc(ptr::null()).is_null() == false); // Should succeed with default version
3075            let doc = new_doc(ptr::null());
3076            assert!(new_node(ptr::null_mut(), ptr::null()).is_null() == false); // Should succeed
3077            free_node(ptr::null_mut()); // Should not crash
3078            free_doc(ptr::null_mut()); // Should not crash
3079            assert!(unlink_node(ptr::null_mut()) == ()); // Should not crash
3080            assert!(add_child(ptr::null_mut(), ptr::null_mut()).is_null());
3081            assert!(add_sibling(ptr::null_mut(), ptr::null_mut()).is_null());
3082            free_doc(doc);
3083        }
3084    }
3085
3086    // ═══════════════════════════════════════════════════════════════════
3087    // Serialization Tests
3088    // ═══════════════════════════════════════════════════════════════════
3089
3090    /// Helper: compare a buffer's content to an expected string.
3091    unsafe fn buf_eq_str(buf: *mut _xmlBuffer, expected: &str) -> bool {
3092        let content = io::buf_content(buf);
3093        if content.is_null() {
3094            return expected.is_empty();
3095        }
3096        let len = io::buf_length(buf) as usize;
3097        if len != expected.len() {
3098            return false;
3099        }
3100        let slice = unsafe { core::slice::from_raw_parts(content, len) };
3101        slice == expected.as_bytes()
3102    }
3103
3104    #[test]
3105    fn test_serialize_empty_document() {
3106        unsafe {
3107            let doc = new_doc(ptr::null());
3108            let buf = io::buf_create(-1);
3109            assert!(!buf.is_null());
3110
3111            let ret = doc_dump(buf, doc);
3112            assert!(ret >= 0);
3113
3114            // Should have XML declaration
3115            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
3116            assert!(buf_eq_str(buf, expected));
3117
3118            io::buf_free(buf);
3119            free_doc(doc);
3120        }
3121    }
3122
3123    #[test]
3124    fn test_serialize_element_with_text() {
3125        unsafe {
3126            let doc = new_doc(ptr::null());
3127            let root = new_node(ptr::null_mut(), c_str("root"));
3128            doc_set_root_element(doc, root);
3129
3130            // Add text child
3131            let text = new_text(c_str("hello world"));
3132            add_child(root, text);
3133
3134            let buf = io::buf_create(-1);
3135            assert!(!buf.is_null());
3136
3137            let ret = doc_dump(buf, doc);
3138            assert!(ret >= 0);
3139
3140            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>hello world</root>";
3141            assert!(buf_eq_str(buf, expected));
3142
3143            io::buf_free(buf);
3144            free_doc(doc);
3145        }
3146    }
3147
3148    #[test]
3149    fn test_serialize_element_with_attributes() {
3150        unsafe {
3151            let doc = new_doc(ptr::null());
3152            let root = new_node(ptr::null_mut(), c_str("root"));
3153            doc_set_root_element(doc, root);
3154
3155            set_prop(root, c_str("id"), c_str("42"));
3156            set_prop(root, c_str("name"), c_str("test"));
3157
3158            let buf = io::buf_create(-1);
3159            assert!(!buf.is_null());
3160
3161            let ret = doc_dump(buf, doc);
3162            assert!(ret >= 0);
3163
3164            let expected =
3165                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root id=\"42\" name=\"test\"/>";
3166            assert!(buf_eq_str(buf, expected));
3167
3168            io::buf_free(buf);
3169            free_doc(doc);
3170        }
3171    }
3172
3173    #[test]
3174    fn test_serialize_nested_elements() {
3175        unsafe {
3176            let doc = new_doc(ptr::null());
3177            let root = new_node(ptr::null_mut(), c_str("root"));
3178            doc_set_root_element(doc, root);
3179
3180            let child = new_child(root, ptr::null_mut(), c_str("child"));
3181            let grandchild = new_child(child, ptr::null_mut(), c_str("gc"));
3182            let text = new_text(c_str("text"));
3183            add_child(grandchild, text);
3184
3185            let buf = io::buf_create(-1);
3186            assert!(!buf.is_null());
3187
3188            let ret = doc_dump(buf, doc);
3189            assert!(ret >= 0);
3190
3191            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><child><gc>text</gc></child></root>";
3192            assert!(buf_eq_str(buf, expected));
3193
3194            io::buf_free(buf);
3195            free_doc(doc);
3196        }
3197    }
3198
3199    #[test]
3200    fn test_serialize_with_formatting() {
3201        unsafe {
3202            let doc = new_doc(ptr::null());
3203            let root = new_node(ptr::null_mut(), c_str("root"));
3204            doc_set_root_element(doc, root);
3205
3206            let child = new_child(root, ptr::null_mut(), c_str("child"));
3207            let text = new_text(c_str("text"));
3208            add_child(child, text);
3209
3210            let buf = io::buf_create(-1);
3211            assert!(!buf.is_null());
3212
3213            serialize_node(doc as *mut _xmlNode, buf, 1, 0);
3214
3215            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n  <child>text</child>\n</root>\n";
3216            assert!(buf_eq_str(buf, expected));
3217
3218            io::buf_free(buf);
3219            free_doc(doc);
3220        }
3221    }
3222
3223    #[test]
3224    fn test_serialize_escape_ampersand() {
3225        unsafe {
3226            let doc = new_doc(ptr::null());
3227            let root = new_node(ptr::null_mut(), c_str("root"));
3228            doc_set_root_element(doc, root);
3229
3230            let text = new_text(c_str("a & b"));
3231            add_child(root, text);
3232
3233            let buf = io::buf_create(-1);
3234            assert!(!buf.is_null());
3235
3236            let ret = doc_dump(buf, doc);
3237            assert!(ret >= 0);
3238
3239            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>a &amp; b</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_escape_angle_brackets() {
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 text = new_text(c_str("x < y > z"));
3255            add_child(root, text);
3256
3257            let buf = io::buf_create(-1);
3258            assert!(!buf.is_null());
3259
3260            let ret = doc_dump(buf, doc);
3261            assert!(ret >= 0);
3262
3263            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>x &lt; y &gt; z</root>";
3264            assert!(buf_eq_str(buf, expected));
3265
3266            io::buf_free(buf);
3267            free_doc(doc);
3268        }
3269    }
3270
3271    #[test]
3272    fn test_serialize_comment() {
3273        unsafe {
3274            let doc = new_doc(ptr::null());
3275            let root = new_node(ptr::null_mut(), c_str("root"));
3276            doc_set_root_element(doc, root);
3277
3278            let comment = new_comment(c_str("my comment"));
3279            add_child(root, comment);
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 =
3288                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><!--my comment--></root>";
3289            assert!(buf_eq_str(buf, expected));
3290
3291            io::buf_free(buf);
3292            free_doc(doc);
3293        }
3294    }
3295
3296    #[test]
3297    fn test_serialize_pi() {
3298        unsafe {
3299            let doc = new_doc(ptr::null());
3300            let root = new_node(ptr::null_mut(), c_str("root"));
3301            doc_set_root_element(doc, root);
3302
3303            let pi = new_pi(
3304                c_str("xml-stylesheet"),
3305                c_str("href=\"style.xsl\" type=\"text/xsl\""),
3306            );
3307            add_child(root, pi);
3308
3309            let buf = io::buf_create(-1);
3310            assert!(!buf.is_null());
3311
3312            let ret = doc_dump(buf, doc);
3313            assert!(ret >= 0);
3314
3315            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><?xml-stylesheet href=\"style.xsl\" type=\"text/xsl\"?></root>";
3316            assert!(buf_eq_str(buf, expected));
3317
3318            io::buf_free(buf);
3319            free_doc(doc);
3320        }
3321    }
3322
3323    #[test]
3324    fn test_serialize_self_closing() {
3325        unsafe {
3326            let doc = new_doc(ptr::null());
3327            let root = new_node(ptr::null_mut(), c_str("empty"));
3328            doc_set_root_element(doc, root);
3329
3330            let buf = io::buf_create(-1);
3331            assert!(!buf.is_null());
3332
3333            let ret = doc_dump(buf, doc);
3334            assert!(ret >= 0);
3335
3336            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><empty/>";
3337            assert!(buf_eq_str(buf, expected));
3338
3339            io::buf_free(buf);
3340            free_doc(doc);
3341        }
3342    }
3343
3344    #[test]
3345    fn test_dump_node_to_string() {
3346        unsafe {
3347            let node = new_node(ptr::null_mut(), c_str("foo"));
3348            let text = new_text(c_str("bar"));
3349            add_child(node, text);
3350
3351            let result = dump_node(node);
3352            assert!(!result.is_null());
3353
3354            let len = xml_strlen(result);
3355            let slice = unsafe { core::slice::from_raw_parts(result, len as usize) };
3356            assert_eq!(slice, b"<foo>bar</foo>");
3357
3358            allocator::xmlFree(result as *mut c_void);
3359            free_node(node);
3360        }
3361    }
3362
3363    #[test]
3364    fn test_dump_doc_to_string() {
3365        unsafe {
3366            let doc = new_doc(ptr::null());
3367            let root = new_node(ptr::null_mut(), c_str("root"));
3368            doc_set_root_element(doc, root);
3369
3370            let result = dump_doc(doc);
3371            assert!(!result.is_null());
3372
3373            let len = xml_strlen(result);
3374            let slice = unsafe { core::slice::from_raw_parts(result, len as usize) };
3375            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
3376            assert_eq!(slice, expected.as_bytes());
3377
3378            allocator::xmlFree(result as *mut c_void);
3379            free_doc(doc);
3380        }
3381    }
3382
3383    #[test]
3384    fn test_xmlDocDumpFormatMemory() {
3385        unsafe {
3386            let doc = new_doc(ptr::null());
3387            let root = new_node(ptr::null_mut(), c_str("root"));
3388            doc_set_root_element(doc, root);
3389
3390            let mut mem: *mut xmlChar = ptr::null_mut();
3391            let mut size: c_int = 0;
3392
3393            xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 0);
3394
3395            assert!(!mem.is_null());
3396            assert!(size > 0);
3397
3398            let slice = unsafe { core::slice::from_raw_parts(mem, size as usize) };
3399            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>";
3400            assert_eq!(slice, expected.as_bytes());
3401
3402            allocator::xmlFree(mem as *mut c_void);
3403            free_doc(doc);
3404        }
3405    }
3406
3407    #[test]
3408    fn test_serialize_escape_attribute() {
3409        unsafe {
3410            let doc = new_doc(ptr::null());
3411            let root = new_node(ptr::null_mut(), c_str("root"));
3412            doc_set_root_element(doc, root);
3413
3414            // Attribute with special chars
3415            set_prop(root, c_str("desc"), c_str("a < b & c \"quoted\""));
3416
3417            let buf = io::buf_create(-1);
3418            assert!(!buf.is_null());
3419
3420            let ret = doc_dump(buf, doc);
3421            assert!(ret >= 0);
3422
3423            let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root desc=\"a &lt; b &amp; c &quot;quoted&quot;\"/>";
3424            assert!(buf_eq_str(buf, expected));
3425
3426            io::buf_free(buf);
3427            free_doc(doc);
3428        }
3429    }
3430}