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