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;
49
50// ═══════════════════════════════════════════════════════════════════════════════
51// String Helpers
52// ═══════════════════════════════════════════════════════════════════════════════
53
54/// Duplicate an xmlChar string using xmlMalloc.
55///
56/// # SAFETY
57///
58/// - `str` must be a valid null-terminated xmlChar* or NULL.
59unsafe fn dup_xml_str(str: *const xmlChar) -> *mut xmlChar {
60    if str.is_null() {
61        return ptr::null_mut();
62    }
63    let len = unsafe { crate::abi::exports_xml2::xmlStrlen(str) as usize };
64    if len == 0 {
65        // Return a pointer to a null byte
66        let buf = unsafe { allocator::xmlMalloc(1) as *mut xmlChar };
67        if !buf.is_null() {
68            unsafe { *buf = 0 };
69        }
70        return buf;
71    }
72    let buf = unsafe { allocator::xmlMalloc(len + 1) as *mut xmlChar };
73    if !buf.is_null() {
74        unsafe {
75            ptr::copy_nonoverlapping(str, buf, len + 1);
76        }
77    }
78    buf
79}
80
81/// Copy an xmlChar string into an already-allocated buffer, or return NULL.
82unsafe fn copy_xml_str_content(dest: *mut xmlChar, src: *const xmlChar, max_len: usize) -> bool {
83    if src.is_null() || dest.is_null() || max_len == 0 {
84        return false;
85    }
86    let len = unsafe { crate::abi::exports_xml2::xmlStrlen(src) as usize };
87    if len >= max_len {
88        return false;
89    }
90    unsafe {
91        ptr::copy_nonoverlapping(src, dest, len);
92        *dest.add(len) = 0;
93    }
94    true
95}
96
97/// Get the length of a null-terminated xmlChar string.
98pub unsafe fn xml_strlen(str: *const xmlChar) -> c_int {
99    if str.is_null() {
100        return 0;
101    }
102    let mut len: c_int = 0;
103    while unsafe { *str.add(len as usize) != 0 } {
104        len += 1;
105    }
106    len
107}
108
109// ═══════════════════════════════════════════════════════════════════════════════
110// Document Operations
111// ═══════════════════════════════════════════════════════════════════════════════
112
113/// Create a new XML document.
114///
115/// # UPSTREAM-PARITY
116///
117/// ```c
118/// xmlDocPtr xmlNewDoc(const xmlChar *version);
119/// ```
120///
121/// Creates a new document with the given version string (or "1.0" if NULL).
122/// The document is initialized with:
123/// - type = XML_DOCUMENT_NODE
124/// - standalone = -1 (unknown)
125/// - doc->doc = self (self-reference)
126/// - properties = XML_DOC_WELLFORMED
127///
128/// # SAFETY
129///
130/// - `version` must be a valid null-terminated string or NULL.
131pub unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
132    // SAFETY: Allocate zero-initialized memory for the document.
133    let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
134    if doc.is_null() {
135        return ptr::null_mut();
136    }
137
138    unsafe {
139        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
140        (*doc).standalone = -1; // unknown
141        (*doc).doc = doc; // self-reference
142        (*doc).properties = XML_DOC_WELLFORMED as c_int;
143        (*doc).charset = XML_CHAR_ENCODING_UTF8 as c_int;
144
145        // Set version
146        let ver = if version.is_null() {
147            XML_DEFAULT_VERSION.as_ptr() as *const xmlChar
148        } else {
149            version
150        };
151        (*doc).version = dup_xml_str(ver);
152    }
153
154    doc
155}
156
157/// Free a document and all its contents.
158///
159/// # UPSTREAM-PARITY
160///
161/// ```c
162/// void xmlFreeDoc(xmlDocPtr doc);
163/// ```
164///
165/// Frees the document, its DTDs, and all nodes in the tree.
166///
167/// # SAFETY
168///
169/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
170pub unsafe fn free_doc(doc: *mut _xmlDoc) {
171    if doc.is_null() {
172        return;
173    }
174
175    let d = unsafe { &mut *doc };
176
177    // Free internal subset (DTD)
178    if !d.intSubset.is_null() {
179        free_dtd(d.intSubset);
180    }
181
182    // Free external subset (DTD)
183    if !d.extSubset.is_null() {
184        free_dtd(d.extSubset);
185    }
186
187    // Free the tree
188    if !d.children.is_null() {
189        free_node_list(d.children);
190    }
191
192    // Free oldNs list
193    if !d.oldNs.is_null() {
194        free_ns_list(d.oldNs);
195    }
196
197    // Free strings
198    if !d.version.is_null() {
199        allocator::xmlFree(d.version as *mut c_void);
200    }
201    if !d.encoding.is_null() {
202        allocator::xmlFree(d.encoding as *mut c_void);
203    }
204    if !d.URL.is_null() {
205        allocator::xmlFree(d.URL as *mut c_void);
206    }
207
208    // Free the document itself
209    allocator::xmlFree(doc as *mut c_void);
210}
211
212/// Copy a document (deep copy by default).
213///
214/// # UPSTREAM-PARITY
215///
216/// ```c
217/// xmlDocPtr xmlCopyDoc(xmlDocPtr doc, int recursive);
218/// ```
219///
220/// If `recursive` is 1, the entire tree is copied.
221/// If `recursive` is 0, only the document structure is copied (no children).
222///
223/// # SAFETY
224///
225/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
226pub unsafe fn copy_doc(doc: *const _xmlDoc, recursive: c_int) -> *mut _xmlDoc {
227    if doc.is_null() {
228        return ptr::null_mut();
229    }
230
231    let d = unsafe { &*doc };
232
233    let new_doc = new_doc(d.version);
234    if new_doc.is_null() {
235        return ptr::null_mut();
236    }
237
238    unsafe {
239        (*new_doc).type_ = d.type_;
240        (*new_doc).standalone = d.standalone;
241        (*new_doc).encoding = dup_xml_str(d.encoding);
242        (*new_doc).URL = dup_xml_str(d.URL);
243        (*new_doc).charset = d.charset;
244        (*new_doc).properties = d.properties;
245
246        if recursive != 0 && !d.children.is_null() {
247            (*new_doc).children = copy_node_list(d.children, recursive);
248            if !(*new_doc).children.is_null() {
249                (*(*new_doc).children).parent = ptr::null_mut(); // root element parent is NULL
250                (*(*new_doc).children).doc = new_doc;
251                // Update doc for all descendants
252                propagate_doc((*new_doc).children, new_doc);
253            }
254        }
255    }
256
257    new_doc
258}
259
260/// Set the root element of a document.
261///
262/// # UPSTREAM-PARITY
263///
264/// ```c
265/// xmlNodePtr xmlDocSetRootElement(xmlDocPtr doc, xmlNodePtr root);
266/// ```
267///
268/// If the document already has a root element, the old root is returned.
269/// The new root is added as a child of the document.
270///
271/// # SAFETY
272///
273/// - `doc` must be a valid pointer to an _xmlDoc.
274/// - `root` must be a valid pointer to an _xmlNode, or NULL.
275pub unsafe fn doc_set_root_element(doc: *mut _xmlDoc, root: *mut _xmlNode) -> *mut _xmlNode {
276    if doc.is_null() {
277        return ptr::null_mut();
278    }
279
280    let d = unsafe { &mut *doc };
281
282    let old_root = doc_get_root_element(doc);
283
284    if !root.is_null() {
285        unsafe {
286            (*root).parent = ptr::null_mut();
287            (*root).doc = doc;
288        }
289        d.children = root;
290        d.last = root;
291        unsafe {
292            (*root).prev = ptr::null_mut();
293            (*root).next = ptr::null_mut();
294        }
295    } else {
296        d.children = ptr::null_mut();
297        d.last = ptr::null_mut();
298    }
299
300    old_root
301}
302
303/// Get the root element of a document.
304///
305/// # UPSTREAM-PARITY
306///
307/// ```c
308/// xmlNodePtr xmlDocGetRootElement(xmlDocPtr doc);
309/// ```
310///
311/// Returns the root element, or NULL if the document has no root element.
312/// Skips non-element nodes (like PIs, comments) at the document level.
313pub fn doc_get_root_element(doc: *mut _xmlDoc) -> *mut _xmlNode {
314    if doc.is_null() {
315        return ptr::null_mut();
316    }
317
318    let d = unsafe { &*doc };
319    let mut cur = d.children;
320
321    while !cur.is_null() {
322        let node = unsafe { &*cur };
323        if node.type_ == XML_ELEMENT_NODE as c_int {
324            return cur;
325        }
326        cur = node.next;
327    }
328
329    ptr::null_mut()
330}
331
332/// Get the line number of a node.
333///
334/// # UPSTREAM-PARITY
335///
336/// ```c
337/// long xmlGetLineNo(xmlNodePtr node);
338/// ```
339///
340/// Returns the line number, or 0 if not available.
341pub fn get_line_no(node: *const _xmlNode) -> c_int {
342    if node.is_null() {
343        return 0;
344    }
345    let n = unsafe { &*node };
346    n.line as c_int
347}
348
349// ═══════════════════════════════════════════════════════════════════════════════
350// Node Operations
351// ═══════════════════════════════════════════════════════════════════════════════
352
353/// Create a new XML node.
354///
355/// # UPSTREAM-PARITY
356///
357/// ```c
358/// xmlNodePtr xmlNewNode(xmlNsPtr ns, const xmlChar *name);
359/// ```
360///
361/// Creates a new element node with the given name and namespace.
362///
363/// # SAFETY
364///
365/// - `ns` may be NULL.
366/// - `name` must be a valid null-terminated string or NULL.
367pub unsafe fn new_node(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
368    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
369    if node.is_null() {
370        return ptr::null_mut();
371    }
372
373    unsafe {
374        (*node).type_ = XML_ELEMENT_NODE as c_int;
375        (*node).name = dup_xml_str(name);
376        (*node).ns = ns;
377        (*node).line = 0;
378        (*node).extra = 0;
379
380        if !ns.is_null() {
381            (*ns).context = node as *mut _xmlDoc;
382        }
383    }
384
385    node
386}
387
388/// Free a single node (without freeing children).
389///
390/// # UPSTREAM-PARITY
391///
392/// ```c
393/// void xmlFreeNode(xmlNodePtr node);
394/// ```
395///
396/// Frees a node and its properties/namespaces, but NOT its children.
397/// Children must be freed separately or reattached.
398///
399/// # SAFETY
400///
401/// - `node` must be a valid pointer to an _xmlNode, or NULL.
402pub unsafe fn free_node(node: *mut _xmlNode) {
403    if node.is_null() {
404        return;
405    }
406
407    let n = unsafe { &mut *node };
408
409    // Free properties
410    if !n.properties.is_null() {
411        free_prop_list(n.properties);
412    }
413
414    // Free namespace declarations
415    if !n.nsDef.is_null() {
416        free_ns_list(n.nsDef);
417    }
418
419    // Free the name
420    if !n.name.is_null() {
421        allocator::xmlFree(n.name as *mut c_void);
422    }
423
424    // Free content (for text/CDATA nodes)
425    if !n.content.is_null() {
426        let node_type = n.type_;
427        if node_type == XML_TEXT_NODE as c_int
428            || node_type == XML_CDATA_SECTION_NODE as c_int
429            || node_type == XML_COMMENT_NODE as c_int
430            || node_type == XML_PI_NODE as c_int
431        {
432            allocator::xmlFree(n.content as *mut c_void);
433        }
434    }
435
436    allocator::xmlFree(node as *mut c_void);
437}
438
439/// Free a linked list of nodes.
440///
441/// Frees all nodes in the list and their children recursively.
442///
443/// # SAFETY
444///
445/// - `node` must be a valid pointer to an _xmlNode, or NULL.
446pub unsafe fn free_node_list(node: *mut _xmlNode) {
447    let mut cur = node;
448    while !cur.is_null() {
449        let next = unsafe { (*cur).next };
450
451        // Free children recursively
452        if !unsafe { (*cur).children }.is_null() {
453            free_node_list(unsafe { (*cur).children });
454        }
455
456        free_node(cur);
457        cur = next;
458    }
459}
460
461/// Free a linked list of properties.
462///
463/// # SAFETY
464///
465/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
466unsafe fn free_prop_list(prop: *mut _xmlAttr) {
467    let mut cur = prop;
468    while !cur.is_null() {
469        let next = unsafe { (*cur).next };
470
471        // Free children (text nodes with value)
472        if !unsafe { (*cur).children }.is_null() {
473            free_node_list(unsafe { (*cur).children });
474        }
475
476        // Free name
477        if !unsafe { (*cur).name }.is_null() {
478            allocator::xmlFree(unsafe { (*cur).name } as *mut c_void);
479        }
480
481        allocator::xmlFree(cur as *mut c_void);
482        cur = next;
483    }
484}
485
486/// Free a linked list of namespace declarations.
487///
488/// # SAFETY
489///
490/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
491unsafe fn free_ns_list(ns: *mut _xmlNs) {
492    let mut cur = ns;
493    while !cur.is_null() {
494        let next = unsafe { (*cur).next };
495
496        // Free href and prefix
497        if !unsafe { (*cur).href }.is_null() {
498            allocator::xmlFree(unsafe { (*cur).href } as *mut c_void);
499        }
500        if !unsafe { (*cur).prefix }.is_null() {
501            allocator::xmlFree(unsafe { (*cur).prefix } as *mut c_void);
502        }
503
504        allocator::xmlFree(cur as *mut c_void);
505        cur = next;
506    }
507}
508
509/// Copy a node (shallow or deep).
510///
511/// # UPSTREAM-PARITY
512///
513/// ```c
514/// xmlNodePtr xmlCopyNode(xmlNodePtr node, int recursive);
515/// ```
516///
517/// If `recursive` is 1, children are also copied.
518/// Returns the new node, or NULL on failure.
519///
520/// # SAFETY
521///
522/// - `node` must be a valid pointer to an _xmlNode, or NULL.
523pub unsafe fn copy_node(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
524    if node.is_null() {
525        return ptr::null_mut();
526    }
527
528    let n = unsafe { &*node };
529
530    let new_node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
531    if new_node.is_null() {
532        return ptr::null_mut();
533    }
534
535    unsafe {
536        (*new_node).type_ = n.type_;
537        (*new_node).name = dup_xml_str(n.name);
538        (*new_node).line = n.line;
539        (*new_node).extra = n.extra;
540        (*new_node).psvi = n.psvi;
541        (*new_node)._private = n._private;
542
543        // Copy namespace pointer (NOT the ns declaration — just the reference)
544        (*new_node).ns = n.ns;
545
546        // Copy namespace declarations
547        if !n.nsDef.is_null() {
548            (*new_node).nsDef = copy_ns_list(n.nsDef);
549        }
550
551        // Copy content for text/CDATA/comment/PI nodes
552        let node_type = n.type_;
553        if (node_type == XML_TEXT_NODE as c_int
554            || node_type == XML_CDATA_SECTION_NODE as c_int
555            || node_type == XML_COMMENT_NODE as c_int
556            || node_type == XML_PI_NODE as c_int)
557            && !n.content.is_null()
558        {
559            (*new_node).content = dup_xml_str(n.content);
560        }
561
562        // Copy properties
563        if !n.properties.is_null() {
564            (*new_node).properties = copy_prop_list(n.properties);
565            // Update doc links on properties
566            let mut prop = (*new_node).properties;
567            while !prop.is_null() {
568                (*prop).parent = new_node;
569                if !(*prop).children.is_null() {
570                    propagate_doc((*prop).children, (*new_node).doc);
571                }
572                prop = (*prop).next;
573            }
574        }
575
576        // Copy children if recursive
577        if recursive != 0 && !n.children.is_null() {
578            (*new_node).children = copy_node_list(n.children, recursive);
579            if !(*new_node).children.is_null() {
580                (*(*new_node).children).parent = new_node;
581                (*(*new_node).children).doc = (*new_node).doc;
582                propagate_doc((*new_node).children, (*new_node).doc);
583            }
584        }
585    }
586
587    new_node
588}
589
590/// Copy a linked list of nodes.
591///
592/// Returns the first node of the new list, or NULL on failure.
593unsafe fn copy_node_list(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
594    if node.is_null() {
595        return ptr::null_mut();
596    }
597
598    let n = unsafe { &*node };
599    let new_node = copy_node(node, recursive);
600    if new_node.is_null() {
601        return ptr::null_mut();
602    }
603
604    let mut prev = new_node;
605    let mut cur = n.next;
606
607    while !cur.is_null() {
608        let new_cur = copy_node(cur, recursive);
609        if new_cur.is_null() {
610            break;
611        }
612        unsafe {
613            (*prev).next = new_cur;
614            (*new_cur).prev = prev;
615        }
616        prev = new_cur;
617        cur = unsafe { (*cur).next };
618    }
619
620    new_node
621}
622
623/// Copy a linked list of namespace declarations.
624unsafe fn copy_ns_list(ns: *const _xmlNs) -> *mut _xmlNs {
625    if ns.is_null() {
626        return ptr::null_mut();
627    }
628
629    let n = unsafe { &*ns };
630    let new_ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
631    if new_ns.is_null() {
632        return ptr::null_mut();
633    }
634
635    unsafe {
636        (*new_ns).type_ = n.type_;
637        (*new_ns).href = dup_xml_str(n.href);
638        (*new_ns).prefix = dup_xml_str(n.prefix);
639        (*new_ns)._private = n._private;
640    }
641
642    let mut prev = new_ns;
643    let mut cur = n.next;
644
645    while !cur.is_null() {
646        let c = unsafe { &*cur };
647        let new_cur = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
648        if new_cur.is_null() {
649            break;
650        }
651        unsafe {
652            (*new_cur).type_ = c.type_;
653            (*new_cur).href = dup_xml_str(c.href);
654            (*new_cur).prefix = dup_xml_str(c.prefix);
655            (*new_cur)._private = c._private;
656            (*prev).next = new_cur;
657        }
658        prev = new_cur;
659        cur = c.next;
660    }
661
662    new_ns
663}
664
665/// Copy a linked list of properties.
666unsafe fn copy_prop_list(prop: *const _xmlAttr) -> *mut _xmlAttr {
667    if prop.is_null() {
668        return ptr::null_mut();
669    }
670
671    let p = unsafe { &*prop };
672    let new_prop = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
673    if new_prop.is_null() {
674        return ptr::null_mut();
675    }
676
677    unsafe {
678        (*new_prop).type_ = p.type_;
679        (*new_prop).name = dup_xml_str(p.name);
680        (*new_prop).ns = p.ns;
681        (*new_prop).atype = p.atype;
682
683        // Copy children (text value nodes)
684        if !p.children.is_null() {
685            (*new_prop).children = copy_node_list(p.children, 1);
686            if !(*new_prop).children.is_null() {
687                (*(*new_prop).children).parent = new_prop as *mut _xmlNode;
688            }
689        }
690    }
691
692    let mut prev = new_prop;
693    let mut cur = p.next;
694
695    while !cur.is_null() {
696        let c = unsafe { &*cur };
697        let new_cur = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
698        if new_cur.is_null() {
699            break;
700        }
701        unsafe {
702            (*new_cur).type_ = c.type_;
703            (*new_cur).name = dup_xml_str(c.name);
704            (*new_cur).ns = c.ns;
705            (*new_cur).atype = c.atype;
706
707            if !c.children.is_null() {
708                (*new_cur).children = copy_node_list(c.children, 1);
709                if !(*new_cur).children.is_null() {
710                    (*(*new_cur).children).parent = new_cur as *mut _xmlNode;
711                }
712            }
713
714            (*prev).next = new_cur;
715        }
716        prev = new_cur;
717        cur = c.next;
718    }
719
720    new_prop
721}
722
723/// Propagate the document pointer to all descendants of a node.
724unsafe fn propagate_doc(node: *mut _xmlNode, doc: *mut _xmlDoc) {
725    let mut cur = node;
726    while !cur.is_null() {
727        unsafe {
728            (*cur).doc = doc;
729
730            // Propagate to properties
731            let mut prop = (*cur).properties;
732            while !prop.is_null() {
733                (*prop).doc = doc;
734                if !(*prop).children.is_null() {
735                    propagate_doc((*prop).children, doc);
736                }
737                prop = (*prop).next;
738            }
739
740            // Recurse into children
741            if !(*cur).children.is_null() {
742                propagate_doc((*cur).children, doc);
743            }
744        }
745        cur = unsafe { (*cur).next };
746    }
747}
748
749/// Unlink a node from its parent/siblings.
750///
751/// # UPSTREAM-PARITY
752///
753/// ```c
754/// void xmlUnlinkNode(xmlNodePtr node);
755/// ```
756///
757/// Removes the node from its parent's child list and sibling list.
758/// The node's parent, prev, and next pointers are cleared.
759/// The node is NOT freed — the caller is responsible for freeing it.
760///
761/// # SAFETY
762///
763/// - `node` must be a valid pointer to an _xmlNode, or NULL.
764pub unsafe fn unlink_node(node: *mut _xmlNode) {
765    if node.is_null() {
766        return;
767    }
768
769    let n = unsafe { &mut *node };
770
771    // Fix up prev/next chain
772    let prev = n.prev;
773    let next = n.next;
774
775    if !prev.is_null() {
776        unsafe { (*prev).next = next };
777    }
778    if !next.is_null() {
779        unsafe { (*next).prev = prev };
780    }
781
782    // Fix up parent's children/last pointers
783    let parent = n.parent;
784    if !parent.is_null() {
785        if unsafe { (*parent).children } == node {
786            unsafe { (*parent).children = next };
787        }
788        if unsafe { (*parent).last } == node {
789            unsafe { (*parent).last = prev };
790        }
791    }
792
793    // Also fix up doc-level children/last if node is a direct doc child
794    let doc = n.doc;
795    if !doc.is_null() && !parent.is_null() {
796        // Already handled above
797    }
798    if !doc.is_null() && parent.is_null() {
799        // Node is a direct child of the document
800        if unsafe { (*doc).children } == node {
801            unsafe { (*doc).children = next };
802        }
803        if unsafe { (*doc).last } == node {
804            unsafe { (*doc).last = prev };
805        }
806    }
807
808    // Clear the node's links
809    n.parent = ptr::null_mut();
810    n.prev = ptr::null_mut();
811    n.next = ptr::null_mut();
812}
813
814/// Add a child node to a parent.
815///
816/// # UPSTREAM-PARITY
817///
818/// ```c
819/// xmlNodePtr xmlAddChild(xmlNodePtr parent, xmlNodePtr cur);
820/// ```
821///
822/// Adds `cur` as the last child of `parent`.
823/// Returns the child, or NULL on failure.
824///
825/// # SAFETY
826///
827/// - `parent` must be a valid pointer to an _xmlNode.
828/// - `cur` must be a valid pointer to an _xmlNode.
829pub unsafe fn add_child(parent: *mut _xmlNode, cur: *mut _xmlNode) -> *mut _xmlNode {
830    if parent.is_null() || cur.is_null() {
831        return ptr::null_mut();
832    }
833
834    let p = unsafe { &mut *parent };
835    let c = unsafe { &mut *cur };
836
837    // If cur is already linked, unlink it first
838    if !c.parent.is_null() || !c.prev.is_null() || !c.next.is_null() {
839        unlink_node(cur);
840    }
841
842    // Update parent/child links
843    c.parent = parent;
844
845    if p.children.is_null() {
846        // First child
847        p.children = cur;
848        p.last = cur;
849        c.prev = ptr::null_mut();
850        c.next = ptr::null_mut();
851    } else {
852        // Append to end
853        c.prev = p.last;
854        c.next = ptr::null_mut();
855        if !p.last.is_null() {
856            unsafe { (*p.last).next = cur };
857        }
858        p.last = cur;
859    }
860
861    // Update doc
862    let doc = if !p.doc.is_null() {
863        p.doc
864    } else {
865        ptr::null_mut()
866    };
867    if !doc.is_null() && c.doc != doc {
868        propagate_doc(cur, doc);
869    }
870
871    cur
872}
873
874/// Add a sibling node after another.
875///
876/// # UPSTREAM-PARITY
877///
878/// ```c
879/// xmlNodePtr xmlAddSibling(xmlNodePtr cur, xmlNodePtr elem);
880/// ```
881///
882/// Adds `elem` as the next sibling of `cur`.
883/// Returns `elem`, or NULL on failure.
884///
885/// # SAFETY
886///
887/// - `cur` must be a valid pointer to an _xmlNode.
888/// - `elem` must be a valid pointer to an _xmlNode.
889pub unsafe fn add_sibling(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
890    if cur.is_null() || elem.is_null() {
891        return ptr::null_mut();
892    }
893
894    let c = unsafe { &mut *cur };
895
896    // If elem is already linked, unlink it first
897    let e = unsafe { &mut *elem };
898    if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
899        unlink_node(elem);
900    }
901
902    // Set parent
903    e.parent = c.parent;
904
905    // Link elem after cur
906    e.prev = cur;
907    e.next = c.next;
908
909    if !c.next.is_null() {
910        unsafe { (*c.next).prev = elem };
911    }
912    c.next = elem;
913
914    // Update parent's last if needed
915    let parent = c.parent;
916    if !parent.is_null() && unsafe { (*parent).last } == cur {
917        unsafe { (*parent).last = elem };
918    }
919
920    // Update doc
921    if !c.doc.is_null() && e.doc != c.doc {
922        propagate_doc(elem, c.doc);
923    }
924
925    elem
926}
927
928/// Create a new child element.
929///
930/// # UPSTREAM-PARITY
931///
932/// ```c
933/// xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns, const xmlChar *name);
934/// ```
935///
936/// Creates a new element and adds it as the last child of `parent`.
937///
938/// # SAFETY
939///
940/// - `parent` must be a valid pointer to an _xmlNode, or NULL.
941/// - `name` must be a valid null-terminated string or NULL.
942pub unsafe fn new_child(
943    parent: *mut _xmlNode,
944    ns: *mut _xmlNs,
945    name: *const xmlChar,
946) -> *mut _xmlNode {
947    let node = new_node(ns, name);
948    if node.is_null() {
949        return ptr::null_mut();
950    }
951
952    if !parent.is_null() {
953        add_child(parent, node);
954    }
955
956    node
957}
958
959// ═══════════════════════════════════════════════════════════════════════════════
960// Text / Content Nodes
961// ═══════════════════════════════════════════════════════════════════════════════
962
963/// Create a new text node.
964///
965/// # UPSTREAM-PARITY
966///
967/// ```c
968/// xmlNodePtr xmlNewText(const xmlChar *content);
969/// ```
970///
971/// Creates a text node with the given content.
972/// If content is NULL, creates an empty text node.
973///
974/// # SAFETY
975///
976/// - `content` must be a valid null-terminated string or NULL.
977pub unsafe fn new_text(content: *const xmlChar) -> *mut _xmlNode {
978    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
979    if node.is_null() {
980        return ptr::null_mut();
981    }
982
983    unsafe {
984        (*node).type_ = XML_TEXT_NODE as c_int;
985        (*node).name = dup_xml_str(b"text\0" as *const u8 as *const xmlChar);
986        (*node).content = if content.is_null() {
987            let empty = allocator::xmlMalloc(1) as *mut xmlChar;
988            if !empty.is_null() {
989                *empty = 0;
990            }
991            empty
992        } else {
993            dup_xml_str(content)
994        };
995        (*node).line = 0;
996    }
997
998    node
999}
1000
1001/// Create a new comment node.
1002///
1003/// # UPSTREAM-PARITY
1004///
1005/// ```c
1006/// xmlNodePtr xmlNewComment(const xmlChar *content);
1007/// ```
1008///
1009/// Creates a comment node with the given content.
1010///
1011/// # SAFETY
1012///
1013/// - `content` must be a valid null-terminated string or NULL.
1014pub unsafe fn new_comment(content: *const xmlChar) -> *mut _xmlNode {
1015    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1016    if node.is_null() {
1017        return ptr::null_mut();
1018    }
1019
1020    unsafe {
1021        (*node).type_ = XML_COMMENT_NODE as c_int;
1022        (*node).name = dup_xml_str(b"comment\0" as *const u8 as *const xmlChar);
1023        (*node).content = dup_xml_str(content);
1024        (*node).line = 0;
1025    }
1026
1027    node
1028}
1029
1030/// Create a new processing instruction node.
1031///
1032/// # UPSTREAM-PARITY
1033///
1034/// ```c
1035/// xmlNodePtr xmlNewPI(const xmlChar *name, const xmlChar *content);
1036/// ```
1037///
1038/// Creates a PI node with the given target name and content.
1039///
1040/// # SAFETY
1041///
1042/// - `name` must be a valid null-terminated string.
1043/// - `content` must be a valid null-terminated string or NULL.
1044pub unsafe fn new_pi(name: *const xmlChar, content: *const xmlChar) -> *mut _xmlNode {
1045    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1046    if node.is_null() {
1047        return ptr::null_mut();
1048    }
1049
1050    unsafe {
1051        (*node).type_ = XML_PI_NODE as c_int;
1052        (*node).name = dup_xml_str(name);
1053        (*node).content = dup_xml_str(content);
1054        (*node).line = 0;
1055    }
1056
1057    node
1058}
1059
1060/// Create a new CDATA section node.
1061///
1062/// # UPSTREAM-PARITY
1063///
1064/// ```c
1065/// xmlNodePtr xmlNewCDataBlock(xmlDocPtr doc, const xmlChar *content, int len);
1066/// ```
1067///
1068/// Creates a CDATA section node with the given content.
1069///
1070/// # SAFETY
1071///
1072/// - `doc` may be NULL.
1073/// - `content` must be a valid pointer to a buffer of at least `len` bytes,
1074///   or NULL.
1075pub unsafe fn new_cdata_block(
1076    doc: *mut _xmlDoc,
1077    content: *const xmlChar,
1078    len: c_int,
1079) -> *mut _xmlNode {
1080    let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1081    if node.is_null() {
1082        return ptr::null_mut();
1083    }
1084
1085    unsafe {
1086        (*node).type_ = XML_CDATA_SECTION_NODE as c_int;
1087        (*node).name = dup_xml_str(b"cdata\0" as *const u8 as *const xmlChar);
1088        (*node).doc = doc;
1089
1090        if !content.is_null() && len > 0 {
1091            (*node).content = allocator::xmlMalloc((len + 1) as usize) as *mut xmlChar;
1092            if !(*node).content.is_null() {
1093                ptr::copy_nonoverlapping(content, (*node).content, len as usize);
1094                *((*node).content.add(len as usize)) = 0;
1095            }
1096        } else {
1097            let empty = allocator::xmlMalloc(1) as *mut xmlChar;
1098            if !empty.is_null() {
1099                *empty = 0;
1100            }
1101            (*node).content = empty;
1102        }
1103
1104        (*node).line = 0;
1105    }
1106
1107    node
1108}
1109
1110// ═══════════════════════════════════════════════════════════════════════════════
1111// Namespace Operations
1112// ═══════════════════════════════════════════════════════════════════════════════
1113
1114/// Create a new namespace declaration.
1115///
1116/// # UPSTREAM-PARITY
1117///
1118/// ```c
1119/// xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
1120/// ```
1121///
1122/// Creates a new namespace declaration on the given node.
1123/// The namespace is added to the node's nsDef list.
1124///
1125/// If `href` is NULL, the namespace is a default namespace undeclaration.
1126/// If `prefix` is NULL, this is the default namespace (xmlns="...").
1127///
1128/// # SAFETY
1129///
1130/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1131/// - `href` must be a valid null-terminated string or NULL.
1132/// - `prefix` must be a valid null-terminated string or NULL.
1133pub unsafe fn new_ns(
1134    node: *mut _xmlNode,
1135    href: *const xmlChar,
1136    prefix: *const xmlChar,
1137) -> *mut _xmlNs {
1138    let ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1139    if ns.is_null() {
1140        return ptr::null_mut();
1141    }
1142
1143    unsafe {
1144        (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
1145        (*ns).href = dup_xml_str(href);
1146        (*ns).prefix = dup_xml_str(prefix);
1147        (*ns).context = node as *mut _xmlDoc;
1148
1149        // Add to node's nsDef list
1150        if !node.is_null() {
1151            let n = &mut *node;
1152            if n.nsDef.is_null() {
1153                n.nsDef = ns;
1154            } else {
1155                // Append to end
1156                let mut last = n.nsDef;
1157                while !(*last).next.is_null() {
1158                    last = (*last).next;
1159                }
1160                (*last).next = ns;
1161            }
1162        }
1163    }
1164
1165    ns
1166}
1167
1168/// Set the namespace of a node.
1169///
1170/// # UPSTREAM-PARITY
1171///
1172/// ```c
1173/// void xmlSetNs(xmlNodePtr node, xmlNsPtr ns);
1174/// ```
1175///
1176/// # SAFETY
1177///
1178/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1179/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
1180pub unsafe fn set_ns(node: *mut _xmlNode, ns: *mut _xmlNs) {
1181    if node.is_null() {
1182        return;
1183    }
1184    unsafe {
1185        (*node).ns = ns;
1186    }
1187}
1188
1189/// Get a list of namespaces in scope for a node.
1190///
1191/// # UPSTREAM-PARITY
1192///
1193/// ```c
1194/// xmlNsPtr *xmlGetNsList(xmlDocPtr doc, xmlNodePtr node);
1195/// ```
1196///
1197/// Returns a NULL-terminated array of namespace pointers in scope,
1198/// or NULL on failure.
1199///
1200/// # SAFETY
1201///
1202/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1203/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1204pub unsafe fn get_ns_list(doc: *mut _xmlDoc, node: *mut _xmlNode) -> *mut *mut _xmlNs {
1205    // Phase 1: basic implementation
1206    // A more complete implementation would walk the node's ancestors
1207    // and collect all in-scope namespaces.
1208    if node.is_null() {
1209        return ptr::null_mut();
1210    }
1211
1212    // Collect namespaces from this node and ancestors
1213    let mut ns_ptrs: Vec<*mut _xmlNs> = Vec::new();
1214    let mut cur = node;
1215
1216    while !cur.is_null() {
1217        let n = unsafe { &*cur };
1218        let mut ns_def = n.nsDef;
1219        while !ns_def.is_null() {
1220            // Avoid duplicates
1221            let ns = unsafe { &*ns_def };
1222            let mut found = false;
1223            for &existing in &ns_ptrs {
1224                if existing == ns_def {
1225                    found = true;
1226                    break;
1227                }
1228                let e = unsafe { &*existing };
1229                if !ns.href.is_null() && !e.href.is_null() {
1230                    let href_match =
1231                        unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, e.href) != 0 };
1232                    if href_match {
1233                        if ns.prefix.is_null() && e.prefix.is_null() {
1234                            found = true;
1235                            break;
1236                        }
1237                        if !ns.prefix.is_null() && !e.prefix.is_null() {
1238                            let prefix_match = unsafe {
1239                                crate::abi::exports_xml2::xmlStrEqual(ns.prefix, e.prefix) != 0
1240                            };
1241                            if prefix_match {
1242                                found = true;
1243                                break;
1244                            }
1245                        }
1246                    }
1247                }
1248            }
1249            if !found {
1250                ns_ptrs.push(ns_def);
1251            }
1252            ns_def = unsafe { (*ns_def).next };
1253        }
1254        cur = n.parent;
1255    }
1256
1257    if ns_ptrs.is_empty() {
1258        return ptr::null_mut();
1259    }
1260
1261    // Allocate NULL-terminated array
1262    let arr =
1263        allocator::xmlMalloc((ns_ptrs.len() + 1) * size_of::<*mut _xmlNs>()) as *mut *mut _xmlNs;
1264    if arr.is_null() {
1265        return ptr::null_mut();
1266    }
1267
1268    for (i, ns) in ns_ptrs.iter().enumerate() {
1269        unsafe { *arr.add(i) = *ns };
1270    }
1271    unsafe { *arr.add(ns_ptrs.len()) = ptr::null_mut() };
1272
1273    arr
1274}
1275
1276/// Search for a namespace by prefix.
1277///
1278/// # UPSTREAM-PARITY
1279///
1280/// ```c
1281/// xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace);
1282/// ```
1283///
1284/// Searches for a namespace declaration with the given prefix.
1285/// If `nameSpace` is NULL, searches for the default namespace.
1286///
1287/// # SAFETY
1288///
1289/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1290/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1291/// - `nameSpace` must be a valid null-terminated string or NULL.
1292pub unsafe fn search_ns(
1293    doc: *mut _xmlDoc,
1294    node: *mut _xmlNode,
1295    name_space: *const xmlChar,
1296) -> *mut _xmlNs {
1297    if node.is_null() {
1298        return ptr::null_mut();
1299    }
1300
1301    let mut cur = node;
1302    while !cur.is_null() {
1303        let n = unsafe { &*cur };
1304        let mut ns_def = n.nsDef;
1305        while !ns_def.is_null() {
1306            let ns = unsafe { &*ns_def };
1307            let match_prefix = if name_space.is_null() {
1308                // Default namespace: prefix should be NULL
1309                ns.prefix.is_null()
1310            } else {
1311                !ns.prefix.is_null()
1312                    && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, name_space) != 0 }
1313            };
1314            if match_prefix {
1315                return ns_def;
1316            }
1317            ns_def = unsafe { (*ns_def).next };
1318        }
1319        cur = n.parent;
1320    }
1321
1322    ptr::null_mut()
1323}
1324
1325/// Search for a namespace by href (URI).
1326///
1327/// # UPSTREAM-PARITY
1328///
1329/// ```c
1330/// xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar *href);
1331/// ```
1332///
1333/// Searches for a namespace declaration with the given URI.
1334///
1335/// # SAFETY
1336///
1337/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1338/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1339/// - `href` must be a valid null-terminated string or NULL.
1340pub unsafe fn search_ns_by_href(
1341    doc: *mut _xmlDoc,
1342    node: *mut _xmlNode,
1343    href: *const xmlChar,
1344) -> *mut _xmlNs {
1345    if node.is_null() || href.is_null() {
1346        return ptr::null_mut();
1347    }
1348
1349    let mut cur = node;
1350    while !cur.is_null() {
1351        let n = unsafe { &*cur };
1352        let mut ns_def = n.nsDef;
1353        while !ns_def.is_null() {
1354            let ns = unsafe { &*ns_def };
1355            if !ns.href.is_null()
1356                && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, href) != 0 }
1357            {
1358                return ns_def;
1359            }
1360            ns_def = unsafe { (*ns_def).next };
1361        }
1362        cur = n.parent;
1363    }
1364
1365    ptr::null_mut()
1366}
1367
1368// ═══════════════════════════════════════════════════════════════════════════════
1369// Attribute Operations
1370// ═══════════════════════════════════════════════════════════════════════════════
1371
1372/// Set an attribute on a node.
1373///
1374/// # UPSTREAM-PARITY
1375///
1376/// ```c
1377/// xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name, const xmlChar *value);
1378/// ```
1379///
1380/// Sets the attribute with the given name to the given value.
1381/// If the attribute already exists, its value is updated.
1382/// Creates the attribute if it doesn't exist.
1383///
1384/// Returns the attribute pointer, or NULL on failure.
1385///
1386/// # SAFETY
1387///
1388/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1389/// - `name` must be a valid null-terminated string.
1390/// - `value` must be a valid null-terminated string or NULL.
1391pub unsafe fn set_prop(
1392    node: *mut _xmlNode,
1393    name: *const xmlChar,
1394    value: *const xmlChar,
1395) -> *mut _xmlAttr {
1396    if node.is_null() || name.is_null() {
1397        return ptr::null_mut();
1398    }
1399
1400    let n = unsafe { &mut *node };
1401
1402    // Check if attribute already exists
1403    let mut existing = n.properties;
1404    while !existing.is_null() {
1405        let attr = unsafe { &*existing };
1406        if !attr.name.is_null()
1407            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
1408        {
1409            // Update existing attribute value
1410            // Free old children (text nodes)
1411            if !attr.children.is_null() {
1412                free_node_list(attr.children);
1413                // SAFETY: We need to mutate const fields
1414                let attr_mut = existing as *mut _xmlAttr;
1415                unsafe { (*attr_mut).children = ptr::null_mut() };
1416                unsafe { (*attr_mut).last = ptr::null_mut() };
1417            }
1418            // Set new value
1419            if !value.is_null() {
1420                let text = new_text(value);
1421                if !text.is_null() {
1422                    let attr_mut = existing as *mut _xmlAttr;
1423                    unsafe {
1424                        (*attr_mut).children = text;
1425                        (*attr_mut).last = text;
1426                        (*text).parent = existing as *mut _xmlNode;
1427                        (*text).doc = n.doc;
1428                    }
1429                }
1430            }
1431            return existing;
1432        }
1433        existing = unsafe { (*existing).next };
1434    }
1435
1436    // Create new attribute
1437    let attr = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1438    if attr.is_null() {
1439        return ptr::null_mut();
1440    }
1441
1442    unsafe {
1443        (*attr).type_ = XML_ATTRIBUTE_NODE as c_int;
1444        (*attr).name = dup_xml_str(name);
1445        (*attr).parent = node;
1446        (*attr).doc = n.doc;
1447        (*attr).atype = XML_ATTRIBUTE_CDATA as c_int;
1448
1449        // Set value
1450        if !value.is_null() {
1451            let text = new_text(value);
1452            if !text.is_null() {
1453                (*attr).children = text;
1454                (*attr).last = text;
1455                (*text).parent = attr as *mut _xmlNode;
1456                (*text).doc = n.doc;
1457            }
1458        }
1459
1460        // Add to node's property list
1461        if n.properties.is_null() {
1462            n.properties = attr;
1463        } else {
1464            let mut last = n.properties;
1465            while !(*last).next.is_null() {
1466                last = (*last).next;
1467            }
1468            (*last).next = attr;
1469            (*attr).prev = last;
1470        }
1471    }
1472
1473    attr
1474}
1475
1476/// Get an attribute value by name.
1477///
1478/// # UPSTREAM-PARITY
1479///
1480/// ```c
1481/// xmlChar *xmlGetProp(xmlNodePtr node, const xmlChar *name);
1482/// ```
1483///
1484/// Returns the attribute value as an xmlChar* (caller must free with xmlFree),
1485/// or NULL if the attribute doesn't exist.
1486///
1487/// # SAFETY
1488///
1489/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1490/// - `name` must be a valid null-terminated string.
1491pub unsafe fn get_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut xmlChar {
1492    if node.is_null() || name.is_null() {
1493        return ptr::null_mut();
1494    }
1495
1496    let n = unsafe { &*node };
1497    let mut cur = n.properties;
1498
1499    while !cur.is_null() {
1500        let attr = unsafe { &*cur };
1501        if !attr.name.is_null()
1502            && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
1503        {
1504            // Get the text content of the attribute
1505            if !attr.children.is_null() {
1506                let text = unsafe { &*attr.children };
1507                if text.type_ == XML_TEXT_NODE as c_int && !text.content.is_null() {
1508                    return dup_xml_str(text.content);
1509                }
1510            }
1511            return dup_xml_str(b"\0" as *const u8 as *const xmlChar);
1512        }
1513        cur = unsafe { (*cur).next };
1514    }
1515
1516    ptr::null_mut()
1517}
1518
1519/// Get a namespaced attribute value.
1520///
1521/// # UPSTREAM-PARITY
1522///
1523/// ```c
1524/// xmlChar *xmlGetNsProp(xmlNodePtr node, const xmlChar *name, const xmlChar *nameSpace);
1525/// ```
1526///
1527/// Returns the attribute value, or NULL if not found.
1528///
1529/// # SAFETY
1530///
1531/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1532/// - `name` must be a valid null-terminated string.
1533/// - `nameSpace` may be NULL.
1534pub unsafe fn get_ns_prop(
1535    node: *mut _xmlNode,
1536    name: *const xmlChar,
1537    _name_space: *const xmlChar,
1538) -> *mut xmlChar {
1539    // Phase 1: simple attribute lookup (namespace-aware lookup will be
1540    // fully implemented in Phase 2+).
1541    get_prop(node, name)
1542}
1543
1544/// Set a namespaced attribute.
1545///
1546/// # UPSTREAM-PARITY
1547///
1548/// ```c
1549/// xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns, const xmlChar *name, const xmlChar *value);
1550/// ```
1551///
1552/// # SAFETY
1553///
1554/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1555/// - `ns` may be NULL.
1556/// - `name` must be a valid null-terminated string.
1557/// - `value` must be a valid null-terminated string or NULL.
1558pub unsafe fn set_ns_prop(
1559    node: *mut _xmlNode,
1560    _ns: *mut _xmlNs,
1561    name: *const xmlChar,
1562    value: *const xmlChar,
1563) -> *mut _xmlAttr {
1564    // Phase 1: use xmlSetProp (namespace-aware version will be in Phase 2+).
1565    set_prop(node, name, value)
1566}
1567
1568/// Remove a property from a node.
1569///
1570/// # UPSTREAM-PARITY
1571///
1572/// ```c
1573/// int xmlRemoveProp(xmlAttrPtr attr);
1574/// ```
1575///
1576/// Removes the attribute from its parent node and frees it.
1577/// Returns 0 on success, -1 on failure.
1578///
1579/// # SAFETY
1580///
1581/// - `attr` must be a valid pointer to an _xmlAttr, or NULL.
1582pub unsafe fn remove_prop(attr: *mut _xmlAttr) -> c_int {
1583    if attr.is_null() {
1584        return -1;
1585    }
1586
1587    let a = unsafe { &mut *attr };
1588
1589    // Unlink from parent's property list
1590    let parent = a.parent;
1591    if !parent.is_null() {
1592        let p = unsafe { &mut *parent };
1593        if p.properties == attr {
1594            p.properties = a.next;
1595        }
1596    }
1597
1598    // Fix up prev/next chain
1599    if !a.prev.is_null() {
1600        unsafe { (*a.prev).next = a.next };
1601    }
1602    if !a.next.is_null() {
1603        unsafe { (*a.next).prev = a.prev };
1604    }
1605
1606    // Free children (text value nodes)
1607    if !a.children.is_null() {
1608        free_node_list(a.children);
1609    }
1610
1611    // Free name
1612    if !a.name.is_null() {
1613        allocator::xmlFree(a.name as *mut c_void);
1614    }
1615
1616    allocator::xmlFree(attr as *mut c_void);
1617    0
1618}
1619
1620// ═══════════════════════════════════════════════════════════════════════════════
1621// DTD Operations
1622// ═══════════════════════════════════════════════════════════════════════════════
1623
1624/// Get the internal DTD subset of a document.
1625///
1626/// # UPSTREAM-PARITY
1627///
1628/// ```c
1629/// xmlDtdPtr xmlGetIntSubset(xmlDocPtr doc);
1630/// ```
1631pub fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
1632    if doc.is_null() {
1633        return ptr::null_mut();
1634    }
1635    let d = unsafe { &*doc };
1636    d.intSubset
1637}
1638
1639/// Create a new DTD node.
1640///
1641/// # UPSTREAM-PARITY
1642///
1643/// ```c
1644/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
1645///                     const xmlChar *ExternalID, const xmlChar *SystemID);
1646/// ```
1647///
1648/// Creates a new DTD and attaches it to the document.
1649///
1650/// # SAFETY
1651///
1652/// - `doc` must be a valid pointer to an _xmlDoc.
1653/// - `name` must be a valid null-terminated string or NULL.
1654/// - `ExternalID`, `SystemID` may be NULL.
1655pub unsafe fn new_dtd(
1656    doc: *mut _xmlDoc,
1657    name: *const xmlChar,
1658    ExternalID: *const xmlChar,
1659    SystemID: *const xmlChar,
1660) -> *mut _xmlDtd {
1661    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
1662    if dtd.is_null() {
1663        return ptr::null_mut();
1664    }
1665
1666    unsafe {
1667        (*dtd).type_ = XML_DTD_NODE as c_int;
1668        (*dtd).name = dup_xml_str(name);
1669        (*dtd).ExternalID = dup_xml_str(ExternalID);
1670        (*dtd).SystemID = dup_xml_str(SystemID);
1671        (*dtd).parent = doc;
1672        (*dtd).doc = doc;
1673
1674        // Attach to document
1675        if !doc.is_null() {
1676            if (*doc).intSubset.is_null() {
1677                (*doc).intSubset = dtd;
1678            }
1679        }
1680    }
1681
1682    dtd
1683}
1684
1685/// Free a DTD.
1686///
1687/// # SAFETY
1688///
1689/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1690unsafe fn free_dtd(dtd: *mut _xmlDtd) {
1691    if dtd.is_null() {
1692        return;
1693    }
1694
1695    let d = unsafe { &mut *dtd };
1696
1697    // Free name
1698    if !d.name.is_null() {
1699        allocator::xmlFree(d.name as *mut c_void);
1700    }
1701    if !d.ExternalID.is_null() {
1702        allocator::xmlFree(d.ExternalID as *mut c_void);
1703    }
1704    if !d.SystemID.is_null() {
1705        allocator::xmlFree(d.SystemID as *mut c_void);
1706    }
1707
1708    // Free children
1709    if !d.children.is_null() {
1710        free_node_list(d.children);
1711    }
1712
1713    allocator::xmlFree(dtd as *mut c_void);
1714}
1715
1716// ═══════════════════════════════════════════════════════════════════════════════
1717// Entity Operations
1718// ═══════════════════════════════════════════════════════════════════════════════
1719
1720/// Create a new entity.
1721///
1722/// # UPSTREAM-PARITY
1723///
1724/// ```c
1725/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
1726///                           const xmlChar *ExternalID, const xmlChar *SystemID,
1727///                           const xmlChar *content);
1728/// ```
1729///
1730/// # SAFETY
1731///
1732/// - `doc` may be NULL.
1733/// - `name` must be a valid null-terminated string.
1734/// - `ExternalID`, `SystemID`, `content` may be NULL.
1735pub unsafe fn new_entity(
1736    _doc: *mut _xmlDoc,
1737    name: *const xmlChar,
1738    etype: c_int,
1739    ExternalID: *const xmlChar,
1740    SystemID: *const xmlChar,
1741    content: *const xmlChar,
1742) -> *mut _xmlEntity {
1743    let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
1744    if entity.is_null() {
1745        return ptr::null_mut();
1746    }
1747
1748    unsafe {
1749        (*entity).type_ = XML_ENTITY_DECL as c_int;
1750        (*entity).name = dup_xml_str(name);
1751        (*entity).etype = etype;
1752        (*entity).ExternalID = dup_xml_str(ExternalID);
1753        (*entity).SystemID = dup_xml_str(SystemID);
1754        (*entity).content = dup_xml_str(content);
1755        (*entity).length = if content.is_null() {
1756            0
1757        } else {
1758            crate::abi::exports_xml2::xmlStrlen(content)
1759        };
1760        (*entity).flags = 0;
1761        (*entity).expandedSize = 0;
1762    }
1763
1764    entity
1765}
1766
1767/// Get a document entity by name.
1768///
1769/// # UPSTREAM-PARITY
1770///
1771/// ```c
1772/// xmlEntityPtr xmlGetDocEntity(xmlDocPtr doc, const xmlChar *name);
1773/// ```
1774///
1775/// Returns the entity, or NULL if not found.
1776///
1777/// # SAFETY
1778///
1779/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1780/// - `name` must be a valid null-terminated string.
1781pub unsafe fn get_doc_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1782    // Phase 1: minimal implementation. Full entity table lookup will be
1783    // in Phase 2+ when the DTD module is implemented.
1784    let _ = doc;
1785    let _ = name;
1786    ptr::null_mut()
1787}
1788
1789/// Get a parameter entity by name.
1790///
1791/// # UPSTREAM-PARITY
1792///
1793/// ```c
1794/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
1795/// ```
1796///
1797/// # SAFETY
1798///
1799/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1800/// - `name` must be a valid null-terminated string.
1801pub unsafe fn get_parameter_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
1802    // Phase 1: minimal implementation.
1803    let _ = doc;
1804    let _ = name;
1805    ptr::null_mut()
1806}
1807
1808// ═══════════════════════════════════════════════════════════════════════════════
1809// Tests
1810// ═══════════════════════════════════════════════════════════════════════════════
1811
1812#[cfg(test)]
1813mod tests {
1814    use super::*;
1815    use core::ffi::c_void;
1816
1817    fn c_str(s: &str) -> *const xmlChar {
1818        let bytes = s.as_bytes();
1819        let buf = unsafe { allocator::xmlMalloc(bytes.len() + 1) as *mut u8 };
1820        if !buf.is_null() {
1821            unsafe {
1822                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
1823                *buf.add(bytes.len()) = 0;
1824            }
1825        }
1826        buf as *const xmlChar
1827    }
1828
1829    #[test]
1830    fn test_new_free_doc() {
1831        unsafe {
1832            let doc = new_doc(ptr::null());
1833            assert!(!doc.is_null());
1834            assert_eq!((*doc).type_, XML_DOCUMENT_NODE as c_int);
1835            assert_eq!((*doc).standalone, -1);
1836            assert_eq!((*doc).doc, doc);
1837            assert!(!(*doc).version.is_null());
1838            free_doc(doc);
1839        }
1840    }
1841
1842    #[test]
1843    fn test_new_doc_with_version() {
1844        unsafe {
1845            let ver = c_str("2.0");
1846            let doc = new_doc(ver);
1847            assert!(!doc.is_null());
1848            let doc_ver = (*doc).version;
1849            assert!(!doc_ver.is_null());
1850            assert!(crate::abi::exports_xml2::xmlStrEqual(doc_ver, ver,) != 0);
1851            allocator::xmlFree(ver as *mut c_void);
1852            free_doc(doc);
1853        }
1854    }
1855
1856    #[test]
1857    fn test_new_node() {
1858        unsafe {
1859            let doc = new_doc(ptr::null());
1860            let node = new_node(ptr::null_mut(), c_str("root"));
1861            assert!(!node.is_null());
1862            assert_eq!((*node).type_, XML_ELEMENT_NODE as c_int);
1863            assert!(!(*node).name.is_null());
1864            free_node(node);
1865            free_doc(doc);
1866        }
1867    }
1868
1869    #[test]
1870    fn test_doc_set_root_element() {
1871        unsafe {
1872            let doc = new_doc(ptr::null());
1873            let root = new_node(ptr::null_mut(), c_str("root"));
1874            let old = doc_set_root_element(doc, root);
1875            assert!(old.is_null());
1876            assert_eq!(doc_get_root_element(doc), root);
1877            assert_eq!((*doc).children, root as *mut _xmlNode);
1878            free_doc(doc);
1879        }
1880    }
1881
1882    #[test]
1883    fn test_add_child_and_sibling() {
1884        unsafe {
1885            let doc = new_doc(ptr::null());
1886            let root = new_node(ptr::null_mut(), c_str("root"));
1887            doc_set_root_element(doc, root);
1888
1889            let child1 = new_child(root, ptr::null_mut(), c_str("child1"));
1890            assert!(!child1.is_null());
1891            assert_eq!((*child1).parent, root);
1892            assert_eq!((*root).children, child1);
1893            assert_eq!((*root).last, child1);
1894
1895            let child2 = new_child(root, ptr::null_mut(), c_str("child2"));
1896            assert!(!child2.is_null());
1897            assert_eq!((*child2).parent, root);
1898            assert_eq!((*child1).next, child2);
1899            assert_eq!((*child2).prev, child1);
1900            assert_eq!((*root).last, child2);
1901
1902            // Test add_sibling
1903            let sibling = new_node(ptr::null_mut(), c_str("sibling"));
1904            add_sibling(child2, sibling);
1905            assert_eq!((*child2).next, sibling);
1906            assert_eq!((*sibling).prev, child2);
1907            assert_eq!((*root).last, sibling);
1908
1909            free_doc(doc);
1910        }
1911    }
1912
1913    #[test]
1914    fn test_unlink_node() {
1915        unsafe {
1916            let doc = new_doc(ptr::null());
1917            let root = new_node(ptr::null_mut(), c_str("root"));
1918            doc_set_root_element(doc, root);
1919
1920            let child1 = new_child(root, ptr::null_mut(), c_str("c1"));
1921            let child2 = new_child(root, ptr::null_mut(), c_str("c2"));
1922
1923            unlink_node(child1);
1924            assert!((*child1).parent.is_null());
1925            assert!((*child1).prev.is_null());
1926            assert!((*child1).next.is_null());
1927            assert_eq!((*root).children, child2);
1928            assert_eq!((*root).last, child2);
1929
1930            free_node(child1);
1931            free_doc(doc);
1932        }
1933    }
1934
1935    #[test]
1936    fn test_text_and_comment_nodes() {
1937        unsafe {
1938            let text = new_text(c_str("hello world"));
1939            assert!(!text.is_null());
1940            assert_eq!((*text).type_, XML_TEXT_NODE as c_int);
1941            assert!(!(*text).content.is_null());
1942            free_node(text);
1943
1944            let comment = new_comment(c_str("my comment"));
1945            assert!(!comment.is_null());
1946            assert_eq!((*comment).type_, XML_COMMENT_NODE as c_int);
1947            free_node(comment);
1948
1949            let pi = new_pi(c_str("xml"), c_str("version='1.0'"));
1950            assert!(!pi.is_null());
1951            assert_eq!((*pi).type_, XML_PI_NODE as c_int);
1952            free_node(pi);
1953        }
1954    }
1955
1956    #[test]
1957    fn test_set_and_get_prop() {
1958        unsafe {
1959            let doc = new_doc(ptr::null());
1960            let root = new_node(ptr::null_mut(), c_str("root"));
1961            doc_set_root_element(doc, root);
1962
1963            let attr = set_prop(root, c_str("id"), c_str("42"));
1964            assert!(!attr.is_null());
1965            assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
1966
1967            let value = get_prop(root, c_str("id"));
1968            assert!(!value.is_null());
1969            assert!(crate::abi::exports_xml2::xmlStrEqual(value, c_str("42")) != 0);
1970            allocator::xmlFree(value as *mut c_void);
1971
1972            free_doc(doc);
1973        }
1974    }
1975
1976    #[test]
1977    fn test_remove_prop() {
1978        unsafe {
1979            let doc = new_doc(ptr::null());
1980            let root = new_node(ptr::null_mut(), c_str("root"));
1981            doc_set_root_element(doc, root);
1982
1983            set_prop(root, c_str("a"), c_str("1"));
1984            set_prop(root, c_str("b"), c_str("2"));
1985
1986            let value = get_prop(root, c_str("a"));
1987            assert!(!value.is_null());
1988            allocator::xmlFree(value as *mut c_void);
1989
1990            // Remove prop
1991            let attr = (*root).properties;
1992            assert!(!attr.is_null());
1993            let result = remove_prop(attr);
1994            assert_eq!(result, 0);
1995
1996            // Should no longer be found
1997            let value2 = get_prop(root, c_str("a"));
1998            assert!(value2.is_null());
1999
2000            free_doc(doc);
2001        }
2002    }
2003
2004    #[test]
2005    fn test_namespace_operations() {
2006        unsafe {
2007            let doc = new_doc(ptr::null());
2008            let root = new_node(ptr::null_mut(), c_str("root"));
2009            doc_set_root_element(doc, root);
2010
2011            let ns = new_ns(root, c_str("http://example.com"), c_str("ex"));
2012            assert!(!ns.is_null());
2013            assert!(!(*root).nsDef.is_null());
2014
2015            set_ns(root, ns);
2016            assert_eq!((*root).ns, ns);
2017
2018            let found = search_ns(doc, root, c_str("ex"));
2019            assert_eq!(found, ns);
2020
2021            let found_href = search_ns_by_href(doc, root, c_str("http://example.com"));
2022            assert_eq!(found_href, ns);
2023
2024            free_doc(doc);
2025        }
2026    }
2027
2028    #[test]
2029    fn test_new_dtd() {
2030        unsafe {
2031            let doc = new_doc(ptr::null());
2032            let dtd = new_dtd(doc, c_str("root"), c_str("-//TEST//DTD"), c_str("test.dtd"));
2033            assert!(!dtd.is_null());
2034            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
2035            assert_eq!(get_int_subset(doc), dtd);
2036            free_doc(doc);
2037        }
2038    }
2039
2040    #[test]
2041    fn test_copy_node_deep() {
2042        unsafe {
2043            let doc = new_doc(ptr::null());
2044            let root = new_node(ptr::null_mut(), c_str("root"));
2045            doc_set_root_element(doc, root);
2046            let child = new_child(root, ptr::null_mut(), c_str("child"));
2047
2048            let copy = copy_node(root, 1);
2049            assert!(!copy.is_null());
2050            assert_eq!((*copy).type_, XML_ELEMENT_NODE as c_int);
2051            // Check child was copied
2052            assert!(!(*copy).children.is_null());
2053            assert_eq!((*(*copy).children).type_, XML_ELEMENT_NODE as c_int);
2054
2055            free_node(copy);
2056            free_doc(doc);
2057        }
2058    }
2059
2060    #[test]
2061    fn test_new_cdata_block() {
2062        unsafe {
2063            let doc = new_doc(ptr::null());
2064            let content = c_str("some <cdata> content");
2065            let cdata = new_cdata_block(doc, content, 20);
2066            assert!(!cdata.is_null());
2067            assert_eq!((*cdata).type_, XML_CDATA_SECTION_NODE as c_int);
2068            free_node(cdata);
2069            free_doc(doc);
2070        }
2071    }
2072
2073    #[test]
2074    fn test_null_handling() {
2075        unsafe {
2076            assert!(new_doc(ptr::null()).is_null() == false); // Should succeed with default version
2077            let doc = new_doc(ptr::null());
2078            assert!(new_node(ptr::null_mut(), ptr::null()).is_null() == false); // Should succeed
2079            free_node(ptr::null_mut()); // Should not crash
2080            free_doc(ptr::null_mut()); // Should not crash
2081            assert!(unlink_node(ptr::null_mut()) == ()); // Should not crash
2082            assert!(add_child(ptr::null_mut(), ptr::null_mut()).is_null());
2083            assert!(add_sibling(ptr::null_mut(), ptr::null_mut()).is_null());
2084            free_doc(doc);
2085        }
2086    }
2087}