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//! The libxml2 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//!
36//! # Upstream contract
37//!
38//! Mirrors upstream tree.c and buf.c (SRC-LIBXML2-2.15.0, oracle tree
39//! `oracle/historical/src/libxml2-2.15.0/`). The tree is an observable data
40//! structure: pointer topology (parent, children, last, next, prev, doc, ns,
41//! properties, nsDef) is part of the compatibility contract and must be
42//! court-tested. Parity target: the system libxml2 2.15.3 oracle.
43//!
44//! # Conceptual behavior
45//!
46//! Complete tree construction/manipulation: namespaces, attributes,
47//! dictionaries, entity structures, document ownership, copying, linking and
48//! freeing. Nodes are C-layout mirrors (tree.h); copy/link/free semantics
49//! follow xmlCopyNode / xmlAddChild / xmlFreeNodeList / xmlFreeDoc.
50//!
51//! # Ownership & safety invariants
52//!
53//! Documents own all their nodes; freeing the document frees the subtree.
54//! node->parent, node->doc, node->ns, node->next/prev are borrowed pointers —
55//! never freed by the reader. Allocator domain: xmlMalloc, freed with xmlFree
56//! (atlas/OWNERSHIP_ATLAS.md). SAFETY: the Rust mirrors enforce layout exactly
57//! (`#[repr(C)]`); `_xmlElement` is 104 bytes upstream and must stay that size
58//! (R-000139: a 56-byte mirror under-allocated every element declaration).
59//!
60//! # Historical quirks & epochs
61//!
62//! QUIRK-0002 / LORE-0006: namespace nodes have no parent — a long-standing
63//! divergence upstream was aware of since the c14n fix commit 044fc6b7
64//! (2002). E-004: entity-content text nodes became TEXT compact at 2.13.0
65//! (commit 8d04f0ee). The 11.1-N structural alignment (R-000164) pinned
66//! doc->children DTD placement, CDATA node names, standalone=-2 and the
67//! attribute hash (name,prefix,elem) key order.
68//!
69//! # Deliberate oddities
70//!
71//! Deliberate oddities preserved for parity: an xmlns= declaration with an
72//! empty value yields href pointing at an empty string (not NULL), parsed
73//! attributes keep atype=0, the DTD node joins doc->children before the first
74//! element, and xmlGetLineNo returns long with the upstream -1 walk for
75//! non-element nodes (all R-000164).
76//!
77//! # Proving courts
78//!
79//! OWNERSHIP and TREE-STRUCTURE court families; TREE-001 (27-block structural
80//! fingerprint of 20 corpus docs x 8 option variants, byte-identical), ASan
81//! full-suite runs, and `cargo test --lib` (counts generated into
82//! atlas/TEST_COUNTS.json by tools/evidence/test_counts.py). Receipts under
83//! courts/receipts/phase-11.
84//!
85//! # Tempting simplifications that would break parity
86//!
87//! A tempting simplification is a nicer Rust node type instead of the exact
88//! `_xmlNode` / `_xmlElement` mirrors — it would break the C ABI layout
89//! (R-000139 class) and every C consumer reading fields at upstream offsets.
90//! Do not auto-maintain parent pointers for namespace nodes (QUIRK-0002); do
91//! not drop the last/next/prev links — TREE-001 fingerprints them.
92//!
93//! # Safety
94//!
95//! - The unsafe entry points in this module accept raw pointers that must be
96//! valid, correctly typed, and live for the duration of the call: `_xmlDoc`,
97//! `_xmlNode`, `_xmlAttr`, `_xmlNs`, `_xmlDtd`, `_xmlBuffer`, and
98//! NUL-terminated `xmlChar` strings. NULL is permitted only where an
99//! individual function's contract explicitly allows it.
100//! - Tree links (`parent`, `children`, `last`, `next`, `prev`, `properties`,
101//! `nsDef`) must form a consistent, live tree; documents own their node
102//! subtrees, so callers must not free a node that still belongs to a live
103//! document.
104
105use core::ffi::c_void;
106use core::ptr;
107use std::os::raw::{c_char, c_int, c_long, c_ulong};
108
109use crate::abi::allocator;
110use crate::abi::constants::*;
111use crate::abi::structs::*;
112use crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8;
113use crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT;
114use crate::abi::types::xmlElementType::*;
115use crate::abi::types::*;
116use crate::xml::io;
117
118// ═══════════════════════════════════════════════════════════════════════════════
119// String Helpers
120// ═══════════════════════════════════════════════════════════════════════════════
121
122/// Duplicate an xmlChar string using xmlMalloc.
123///
124/// # SAFETY
125///
126/// - `str` must be a valid null-terminated xmlChar* or NULL.
127unsafe fn dup_xml_str(str: *const xmlChar) -> *mut xmlChar {
128 if str.is_null() {
129 return ptr::null_mut();
130 }
131 let len = unsafe { crate::abi::exports_xml2::xmlStrlen(str) as usize };
132 if len == 0 {
133 // Return a pointer to a null byte
134 let buf = unsafe { allocator::xmlMallocImpl(1) as *mut xmlChar };
135 if !buf.is_null() {
136 unsafe { *buf = 0 };
137 }
138 return buf;
139 }
140 let buf = unsafe { allocator::xmlMallocImpl(len + 1) as *mut xmlChar };
141 if !buf.is_null() {
142 unsafe {
143 ptr::copy_nonoverlapping(str, buf, len + 1);
144 }
145 }
146 buf
147}
148
149/// Copy an xmlChar string into an already-allocated buffer, or return NULL.
150#[allow(dead_code)]
151unsafe fn copy_xml_str_content(dest: *mut xmlChar, src: *const xmlChar, max_len: usize) -> bool {
152 if src.is_null() || dest.is_null() || max_len == 0 {
153 return false;
154 }
155 let len = unsafe { crate::abi::exports_xml2::xmlStrlen(src) as usize };
156 if len >= max_len {
157 return false;
158 }
159 unsafe {
160 ptr::copy_nonoverlapping(src, dest, len);
161 *dest.add(len) = 0;
162 }
163 true
164}
165
166/// Get the length of a null-terminated xmlChar string.
167///
168/// # SAFETY
169///
170///
171/// - `str` must point to valid NUL-terminated
172/// strings (or NULL where the C contract allows) for the lifetime
173/// of the call.
174///
175/// The caller must not race this call with concurrent mutation of the
176/// same objects from other threads (per-object state is not internally
177/// synchronized). Violating any of the above is undefined behavior.
178///
179/// Exercised by the C-API differential courts
180/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
181/// courts; those pass byte-for-byte against the upstream oracle.
182pub const unsafe fn xml_strlen(str: *const xmlChar) -> c_int {
183 if str.is_null() {
184 return 0;
185 }
186 let mut len: c_int = 0;
187 while unsafe { *str.add(len as usize) != 0 } {
188 len += 1;
189 }
190 len
191}
192
193// ═══════════════════════════════════════════════════════════════════════════════
194// Document Operations
195// ═══════════════════════════════════════════════════════════════════════════════
196
197/// Create a new XML document.
198///
199/// # UPSTREAM-PARITY
200///
201/// ```c
202/// xmlDocPtr xmlNewDoc(const xmlChar *version);
203/// ```
204///
205/// Creates a new document with the given version string (or "1.0" if NULL).
206/// The document is initialized with:
207/// - type = XML_DOCUMENT_NODE
208/// - standalone = -1 (unknown)
209/// - doc->doc = self (self-reference)
210/// - properties = XML_DOC_WELLFORMED
211///
212/// # SAFETY
213///
214/// - `version` must be a valid null-terminated string or NULL.
215pub unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
216 // SAFETY: Allocate zero-initialized memory for the document.
217 let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
218 if doc.is_null() {
219 return ptr::null_mut();
220 }
221
222 unsafe {
223 (*doc).type_ = XML_DOCUMENT_NODE as c_int;
224 (*doc).standalone = -1; // unknown
225 (*doc).doc = doc; // self-reference
226 (*doc).properties = XML_DOC_USERBUILT as c_int;
227 (*doc).compression = -1; // not initialized (upstream xmlNewDoc)
228 (*doc).charset = XML_CHAR_ENCODING_UTF8 as c_int;
229
230 // Set version
231 let ver = if version.is_null() {
232 XML_DEFAULT_VERSION.as_ptr() as *const xmlChar
233 } else {
234 version
235 };
236 (*doc).version = dup_xml_str(ver);
237 }
238
239 // UPSTREAM-PARITY (tree.c xmlNewDoc): the document node is registered.
240 crate::abi::data_globals::register_node_hook(doc as *mut _xmlNode);
241
242 doc
243}
244
245/// Free a document and all its contents.
246///
247/// # UPSTREAM-PARITY
248///
249/// ```c
250/// void xmlFreeDoc(xmlDocPtr doc);
251/// ```
252///
253/// Frees the document, its DTDs, and all nodes in the tree.
254///
255/// # SAFETY
256///
257/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
258pub unsafe fn free_doc(doc: *mut _xmlDoc) {
259 if doc.is_null() {
260 return;
261 }
262
263 // UPSTREAM-PARITY (tree.c xmlFreeDoc): the deregister hook fires before
264 // the document is torn down.
265 crate::abi::data_globals::deregister_node_hook(doc as *mut _xmlNode);
266
267 let d = unsafe { &mut *doc };
268
269 // UPSTREAM-PARITY (tree.c xmlFreeDoc): the ID/REF tables are freed
270 // BEFORE the tree walk — attribute frees must not need ID-table lookups
271 // during document teardown (xmlRemoveID on a NULL table is a no-op).
272 if !d.ids.is_null() {
273 crate::xml::validation::free_id_table(d.ids as *mut crate::xml::hash::HashTable);
274 d.ids = ptr::null_mut();
275 }
276
277 // UPSTREAM-PARITY (tree.c xmlFreeDoc): the subset DTD nodes are unlinked
278 // from the child list before the tree is freed (they may be part of
279 // doc->children after a parse).
280 let dict = d.dict;
281 let mut ext_subset = d.extSubset;
282 let int_subset = d.intSubset;
283 if !ext_subset.is_null() && ext_subset == int_subset {
284 ext_subset = ptr::null_mut();
285 }
286 if !ext_subset.is_null() {
287 unlink_node_internal(ext_subset as *mut _xmlNode, d as *mut _xmlDoc);
288 d.extSubset = ptr::null_mut();
289 free_dtd(ext_subset);
290 }
291 if !int_subset.is_null() {
292 unlink_node_internal(int_subset as *mut _xmlNode, d as *mut _xmlDoc);
293 d.intSubset = ptr::null_mut();
294 free_dtd(int_subset);
295 }
296
297 // Free the tree
298 if !d.children.is_null() {
299 free_node_list(d.children);
300 }
301
302 // Free oldNs list
303 if !d.oldNs.is_null() {
304 free_ns_list(d.oldNs);
305 }
306
307 // Free strings
308 if !d.version.is_null() {
309 allocator::xmlFreeImpl(d.version as *mut c_void);
310 }
311 if !d.encoding.is_null() {
312 allocator::xmlFreeImpl(d.encoding as *mut c_void);
313 }
314 if !d.URL.is_null() {
315 allocator::xmlFreeImpl(d.URL as *mut c_void);
316 }
317
318 // Free the document itself
319 allocator::xmlFreeImpl(doc as *mut c_void);
320
321 // UPSTREAM-PARITY: the document holds a reference on its dictionary.
322 if !dict.is_null() {
323 crate::abi::exports_xml2::xmlDictFree(dict);
324 }
325}
326
327/// Rebind the `ns` pointer of one element (and each of its attributes) to a
328/// namespace declaration owned by `new_doc`'s tree, declaring it on `top`
329/// when nothing in scope matches the prefix (upstream tree.c
330/// `xmlStaticCopyNode`: `xmlSearchNsSafe(ret, node->ns->prefix, &ns)` then
331/// "search it in the original tree and add it at the top of the new tree").
332///
333/// # Safety
334///
335/// - `new_doc`/`el`/`top` must be valid tree pointers in the same document.
336unsafe fn rebind_copied_ns(new_doc: *mut _xmlDoc, top: *mut _xmlNode, el: *mut _xmlNode) {
337 unsafe {
338 let bind = |n: *mut _xmlNode| {
339 let ns = (*n).ns;
340 if ns.is_null() {
341 return;
342 }
343 let prefix = (*ns).prefix;
344 let mut found = search_ns(new_doc, el, prefix);
345 if found.is_null() {
346 found = new_ns(top, (*ns).href, prefix);
347 }
348 if !found.is_null() {
349 (*n).ns = found;
350 }
351 };
352 if (*el).type_ == XML_ELEMENT_NODE as c_int {
353 bind(el);
354 let mut a = (*el).properties;
355 while !a.is_null() {
356 if !(*a).ns.is_null() && a as *mut _xmlNode != el {
357 // Attributes resolve their prefix against the element's
358 // in-scope declarations.
359 let prefix = (*(*a).ns).prefix;
360 let mut found = search_ns(new_doc, el, prefix);
361 if found.is_null() {
362 found = new_ns(top, (*(*a).ns).href, prefix);
363 }
364 if !found.is_null() {
365 (*a).ns = found;
366 }
367 }
368 a = (*a).next;
369 }
370 }
371 }
372}
373
374/// Rebind every element/attribute namespace in a freshly deep-copied
375/// document to declarations owned by the copy (upstream xmlStaticCopyNode
376/// semantics for `xmlCopyDoc`). The generic copy keeps the source pointers
377/// verbatim; without this pass the copied tree's namespace pointers dangle
378/// as soon as the source document is freed.
379///
380/// # Safety
381///
382/// - `new_doc` must be the fresh copy; its tree must be fully built.
383pub(crate) unsafe fn reconcile_copied_tree_ns(new_doc: *mut _xmlDoc) {
384 if new_doc.is_null() {
385 return;
386 }
387 unsafe {
388 // The top element is where new declarations are attached (upstream
389 // adds missing namespaces "at the top of the new tree").
390 let mut top: *mut _xmlNode = ptr::null_mut();
391 let mut c = (*new_doc).children;
392 while !c.is_null() {
393 if (*c).type_ == XML_ELEMENT_NODE as c_int {
394 top = c;
395 break;
396 }
397 c = (*c).next;
398 }
399 if top.is_null() {
400 return;
401 }
402 // Pre-order walk: parents are processed before children, so a
403 // declaration attached to the top element is found by descendants.
404 let mut stack: Vec<*mut _xmlNode> = vec![top];
405 while let Some(el) = stack.pop() {
406 rebind_copied_ns(new_doc, top, el);
407 // Push children in reverse so they pop in document order.
408 let mut kids: Vec<*mut _xmlNode> = Vec::new();
409 let mut ch = (*el).children;
410 while !ch.is_null() {
411 if (*ch).type_ == XML_ELEMENT_NODE as c_int {
412 kids.push(ch);
413 }
414 ch = (*ch).next;
415 }
416 for k in kids.into_iter().rev() {
417 stack.push(k);
418 }
419 }
420 }
421}
422
423/// Unlink a node from its parent's child list without freeing it
424/// (upstream xmlUnlinkNodeInternal semantics; doc is used for ID/ref
425/// bookkeeping which the candidate does not maintain on unlink).
426unsafe fn unlink_node_internal(node: *mut _xmlNode, _doc: *mut _xmlDoc) {
427 if node.is_null() {
428 return;
429 }
430 let parent = (*node).parent;
431 if parent.is_null() {
432 return;
433 }
434 if (*node).prev.is_null() {
435 (*parent).children = (*node).next;
436 } else {
437 (*(*node).prev).next = (*node).next;
438 }
439 if (*node).next.is_null() {
440 (*parent).last = (*node).prev;
441 } else {
442 (*(*node).next).prev = (*node).prev;
443 }
444 (*node).next = ptr::null_mut();
445 (*node).prev = ptr::null_mut();
446 (*node).parent = ptr::null_mut();
447}
448
449/// Copy a document (deep copy by default).
450///
451/// # UPSTREAM-PARITY
452///
453/// ```c
454/// xmlDocPtr xmlCopyDoc(xmlDocPtr doc, int recursive);
455/// ```
456///
457/// If `recursive` is 1, the entire tree is copied.
458/// If `recursive` is 0, only the document structure is copied (no children).
459///
460/// # SAFETY
461///
462/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
463pub unsafe fn copy_doc(doc: *const _xmlDoc, recursive: c_int) -> *mut _xmlDoc {
464 if doc.is_null() {
465 return ptr::null_mut();
466 }
467
468 let d = unsafe { &*doc };
469
470 let new_doc = new_doc(d.version);
471 if new_doc.is_null() {
472 return ptr::null_mut();
473 }
474
475 unsafe {
476 (*new_doc).type_ = d.type_;
477 (*new_doc).standalone = d.standalone;
478 (*new_doc).encoding = dup_xml_str(d.encoding);
479 (*new_doc).URL = dup_xml_str(d.URL);
480 (*new_doc).charset = d.charset;
481 (*new_doc).properties = d.properties;
482
483 // UPSTREAM-PARITY (tree.c xmlCopyDoc + xmlStaticCopyNodeList): the
484 // internal subset is copied FIRST (a plain DTD copy) and the SAME
485 // node is then linked into the children list at the DTD child's
486 // position — one shared DTD node serves both `intSubset` and the
487 // DocumentType child (the copy is NOT duplicated). The generic node
488 // copy must never be applied to a DTD child (its ExternalID/SystemID
489 // live at offsets the node copy leaves untouched — the corrupt-clone
490 // SEGV in DOMNode_isEqualNode / clone-of-doctype docs).
491 if recursive != 0 {
492 if !d.intSubset.is_null() {
493 let dtd_copy = crate::xml::dtd::copy_dtd(d.intSubset);
494 if !dtd_copy.is_null() {
495 (*new_doc).intSubset = dtd_copy;
496 }
497 }
498 }
499
500 if recursive != 0 && !d.children.is_null() {
501 // DTD-aware children walk (upstream xmlStaticCopyNodeList): a DTD
502 // child contributes the intSubset copy (created above, or a fresh
503 // DTD copy when the source child is not the recorded subset) at
504 // its original position; every other child is copied generically.
505 let mut head: *mut _xmlNode = ptr::null_mut();
506 let mut tail: *mut _xmlNode = ptr::null_mut();
507 let mut cur = d.children;
508 while !cur.is_null() {
509 let ct = unsafe { (*cur).type_ };
510 let copy: *mut _xmlNode = if ct == XML_DTD_NODE as c_int {
511 unsafe {
512 if (*new_doc).intSubset.is_null() {
513 let dc = crate::xml::dtd::copy_dtd(
514 cur as *const crate::abi::structs::_xmlDtd,
515 );
516 if dc.is_null() {
517 break;
518 }
519 (*new_doc).intSubset = dc;
520 dc as *mut _xmlNode
521 } else {
522 (*new_doc).intSubset as *mut _xmlNode
523 }
524 }
525 } else {
526 copy_node(cur, recursive)
527 };
528 if copy.is_null() {
529 break;
530 }
531 unsafe {
532 if tail.is_null() {
533 head = copy;
534 } else {
535 (*tail).next = copy;
536 (*copy).prev = tail;
537 }
538 tail = copy;
539 }
540 cur = unsafe { (*cur).next };
541 }
542 (*new_doc).children = head;
543 (*new_doc).last = tail;
544 if !head.is_null() {
545 // UPSTREAM-PARITY (tree.c xmlCopyDoc): every copied top-level
546 // child keeps the new DOCUMENT node as its parent
547 // (`xmlStaticCopyNodeList(doc->children, ret,
548 // (xmlNodePtr)ret)`). The pre-fix NULL parent made PHP treat a
549 // cloned document's root element as ownerless: its proxy
550 // teardown (php_libxml_node_free_resource, `parent == NULL`
551 // branch) freed the whole subtree while the cloned doc still
552 // referenced it, so the doc teardown double-freed the root
553 // (Phase 14.3 Bug-3 — DOMDocument clone + navigation).
554 let mut child = head;
555 while !child.is_null() {
556 (*child).parent = new_doc as *mut _xmlNode;
557 (*child).doc = new_doc;
558 child = (*child).next;
559 }
560 propagate_doc(head, new_doc);
561 // UPSTREAM-PARITY (tree.c xmlStaticCopyNode): after a
562 // CROSS-document deep copy every element/attribute namespace
563 // pointer must reference namespace declarations owned by the
564 // COPY (the generic copy keeps the original pointers
565 // verbatim, which dangle once the source document is freed).
566 // Resolve each against the copied tree and declare missing
567 // namespaces on the top element — xmlCopyNode (same-doc)
568 // keeps verbatim pointers, only xmlCopyDoc/xmlDocCopyNode
569 // reconcile.
570 reconcile_copied_tree_ns(new_doc);
571 }
572 }
573 }
574
575 new_doc
576}
577
578/// Set the root element of a document.
579///
580/// # UPSTREAM-PARITY
581///
582/// ```c
583/// xmlNodePtr xmlDocSetRootElement(xmlDocPtr doc, xmlNodePtr root);
584/// ```
585///
586/// If the document already has a root element, the old root is returned.
587/// The new root is added as a child of the document.
588///
589/// # SAFETY
590///
591/// - `doc` must be a valid pointer to an _xmlDoc.
592/// - `root` must be a valid pointer to an _xmlNode, or NULL.
593pub unsafe fn doc_set_root_element(doc: *mut _xmlDoc, root: *mut _xmlNode) -> *mut _xmlNode {
594 if doc.is_null() || root.is_null() || unsafe { (*root).type_ } == XML_NAMESPACE_DECL as c_int {
595 return ptr::null_mut();
596 }
597
598 let old_root = doc_get_root_element(doc);
599 if old_root == root {
600 return old_root;
601 }
602
603 unsafe {
604 // UPSTREAM-PARITY (tree.c xmlDocSetRootElement): unlink the node
605 // from its current tree, move doc pointers, and set the parent to
606 // the DOCUMENT NODE itself. A NULL parent here is observable: lxml
607 // walks `parent` to decide whether a node is still in a document
608 // (proxy.pxi getDeallocationTop) and would free an orphaned-looking
609 // root directly, after which free_doc walks the doc children and
610 // frees it again — a double free.
611 if !(*root).parent.is_null() {
612 unlink_node(root);
613 }
614 if (*root).doc != doc {
615 propagate_doc(root, doc);
616 }
617 (*root).parent = doc as *mut _xmlNode;
618 (*root).doc = doc;
619
620 if old_root.is_null() {
621 // No previous root element: append after the existing doc-level
622 // nodes (PIs/comments may precede the root).
623 if (*doc).children.is_null() {
624 (*doc).children = root;
625 (*doc).last = root;
626 (*root).prev = ptr::null_mut();
627 (*root).next = ptr::null_mut();
628 } else {
629 add_sibling((*doc).last, root);
630 }
631 } else {
632 // Replace the old root in position (upstream xmlReplaceNode).
633 if (*old_root).prev.is_null() {
634 (*doc).children = root;
635 (*root).prev = ptr::null_mut();
636 } else {
637 (*(*old_root).prev).next = root;
638 (*root).prev = (*old_root).prev;
639 }
640 if (*old_root).next.is_null() {
641 (*doc).last = root;
642 (*root).next = ptr::null_mut();
643 } else {
644 (*(*old_root).next).prev = root;
645 (*root).next = (*old_root).next;
646 }
647 (*old_root).parent = ptr::null_mut();
648 (*old_root).prev = ptr::null_mut();
649 (*old_root).next = ptr::null_mut();
650 }
651 }
652
653 old_root
654}
655
656/// Get the root element of a document.
657///
658/// # UPSTREAM-PARITY
659///
660/// ```c
661/// xmlNodePtr xmlDocGetRootElement(xmlDocPtr doc);
662/// ```
663///
664/// Returns the root element, or NULL if the document has no root element.
665/// Skips non-element nodes (like PIs, comments) at the document level.
666///
667/// # Safety
668///
669/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL; when non-NULL it
670/// is dereferenced and its `children` chain is walked.
671pub fn doc_get_root_element(doc: *mut _xmlDoc) -> *mut _xmlNode {
672 if doc.is_null() {
673 return ptr::null_mut();
674 }
675
676 let d = unsafe { &*doc };
677 let mut cur = d.children;
678
679 while !cur.is_null() {
680 let node = unsafe { &*cur };
681 if node.type_ == XML_ELEMENT_NODE as c_int {
682 return cur;
683 }
684 cur = node.next;
685 }
686
687 ptr::null_mut()
688}
689
690/// Get the line number of a node.
691///
692/// # UPSTREAM-PARITY
693///
694/// ```c
695/// long xmlGetLineNo(xmlNodePtr node);
696/// ```
697///
698/// Returns the line number, or 0 if not available.
699///
700/// # Safety
701///
702/// - `node` must be NULL or a valid pointer to an `_xmlNode`; it is forwarded
703/// to `get_line_no_internal`, which walks the tree links.
704pub fn get_line_no(node: *const _xmlNode) -> c_long {
705 unsafe { get_line_no_internal(node, 0) }
706}
707
708/// UPSTREAM-PARITY (tree.c xmlGetLineNoInternal): element/text/comment/PI
709/// nodes report their stored line; other node types (DTD nodes, entity
710/// references, declarations, ...) walk to the nearest previous or ancestor
711/// element-ish node and return -1 when none exists.
712///
713/// # Safety
714///
715/// - `node` must be NULL or a valid pointer to an `_xmlNode` in a consistent
716/// tree: the `children`, `next`, `prev`, and `parent` links it follows must
717/// themselves point to valid `_xmlNode` structs.
718/// - `depth` bounds the recursion; callers start at 0.
719unsafe fn get_line_no_internal(node: *const _xmlNode, depth: c_int) -> c_long {
720 if depth >= 5 {
721 return -1;
722 }
723 if node.is_null() {
724 return -1;
725 }
726 let n = unsafe { &*node };
727 if n.type_ == XML_ELEMENT_NODE as c_int
728 || n.type_ == XML_TEXT_NODE as c_int
729 || n.type_ == XML_COMMENT_NODE as c_int
730 || n.type_ == XML_PI_NODE as c_int
731 {
732 if n.line == 65535 {
733 // UPSTREAM-PARITY (tree.c xmlGetLineNoInternal): text nodes whose
734 // real line exceeded USHRT_MAX at parse time store the line in
735 // psvi (XML_PARSE_BIG_LINES / the PHP html5 lexbor bridge does
736 // XML_INT_TO_PTR(line)); read it back (XML_PTR_TO_INT).
737 if n.type_ == XML_TEXT_NODE as c_int && !n.psvi.is_null() {
738 return n.psvi as usize as c_long;
739 }
740 if n.type_ == XML_ELEMENT_NODE as c_int && !n.children.is_null() {
741 let r = unsafe { get_line_no_internal(n.children, depth + 1) };
742 if r != -1 {
743 return r;
744 }
745 }
746 if !n.next.is_null() {
747 let r = unsafe { get_line_no_internal(n.next, depth + 1) };
748 if r != -1 {
749 return r;
750 }
751 }
752 if !n.prev.is_null() {
753 let r = unsafe { get_line_no_internal(n.prev, depth + 1) };
754 if r != -1 {
755 return r;
756 }
757 }
758 }
759 n.line as c_long
760 } else if !n.prev.is_null()
761 && (unsafe { (*n.prev).type_ } == XML_ELEMENT_NODE as c_int
762 || unsafe { (*n.prev).type_ } == XML_TEXT_NODE as c_int
763 || unsafe { (*n.prev).type_ } == XML_COMMENT_NODE as c_int
764 || unsafe { (*n.prev).type_ } == XML_PI_NODE as c_int)
765 {
766 unsafe { get_line_no_internal(n.prev, depth + 1) }
767 } else if !n.parent.is_null() && unsafe { (*n.parent).type_ } == XML_ELEMENT_NODE as c_int {
768 unsafe { get_line_no_internal(n.parent, depth + 1) }
769 } else {
770 -1
771 }
772}
773
774/// Get the content of a node, recursively concatenating child text.
775///
776/// # UPSTREAM-PARITY
777///
778/// ```c
779/// xmlChar *xmlNodeGetContent(const xmlNode *cur);
780/// ```
781///
782/// Oracle behavior (tree.c `xmlNodeGetContent`):
783/// - For text/CDATA nodes: returns the content directly.
784/// - For element nodes: recursively concatenates the string values of
785/// children (text and CDATA; entity references are expanded via their
786/// content when available).
787/// - For attribute nodes: returns the attribute value (first child).
788/// - For comments/PIs: returns the content field.
789/// - For documents: returns content of the root element.
790/// - Returns NULL on error, empty string for empty nodes.
791///
792/// Returns a newly allocated string; caller frees with `xmlFree`.
793///
794/// # SAFETY
795///
796/// - `node` must be valid pointers (or NULL
797/// where the upstream C contract allows), obtained from the
798/// matching constructor/owner and not yet freed; the callee may
799/// take or keep ownership exactly as the C API specifies.
800///
801/// The caller must not race this call with concurrent mutation of the
802/// same objects from other threads (per-object state is not internally
803/// synchronized). Violating any of the above is undefined behavior.
804///
805/// Exercised by the C-API differential courts
806/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
807/// courts; those pass byte-for-byte against the upstream oracle.
808pub unsafe fn node_get_content(node: *mut _xmlNode) -> *mut xmlChar {
809 if node.is_null() {
810 return ptr::null_mut();
811 }
812 let typ = (*node).type_;
813 let mut result: Vec<u8> = Vec::new();
814 match typ {
815 t if t == XML_TEXT_NODE as c_int
816 || t == XML_CDATA_SECTION_NODE as c_int
817 || t == XML_COMMENT_NODE as c_int
818 || t == XML_PI_NODE as c_int =>
819 {
820 if !(*node).content.is_null() {
821 let len = crate::abi::exports_xml2::xmlStrlen((*node).content);
822 result
823 .extend_from_slice(core::slice::from_raw_parts((*node).content, len as usize));
824 } else if t == XML_TEXT_NODE as c_int && !(*node).children.is_null() {
825 // Non-compact text node (entity merge): content lives in the
826 // child text nodes.
827 let mut child = (*node).children;
828 while !child.is_null() {
829 if !(*child).content.is_null() {
830 let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
831 result.extend_from_slice(core::slice::from_raw_parts(
832 (*child).content,
833 len as usize,
834 ));
835 }
836 child = (*child).next;
837 }
838 }
839 }
840 t if t == XML_ATTRIBUTE_NODE as c_int => {
841 // Attribute: the value is the concatenation of ALL children (text
842 // runs plus expanded entity references) — upstream
843 // xmlNodeGetContent(attr) walks the children via
844 // xmlBufGetChildContent. A single text child is the common case
845 // (php_libxml_attr_value fast path); values containing references
846 // keep entity-REF children (`<root a="x&ent;x"/>` reads "xfoox"
847 // while serialization round-trips `&ent;`).
848 let mut child = (*node).children;
849 while !child.is_null() {
850 let ctype = (*child).type_;
851 if ctype == XML_TEXT_NODE as c_int || ctype == XML_CDATA_SECTION_NODE as c_int {
852 if !(*child).content.is_null() {
853 let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
854 result.extend_from_slice(core::slice::from_raw_parts(
855 (*child).content,
856 len as usize,
857 ));
858 }
859 } else if ctype == XML_ENTITY_REF_NODE as c_int
860 || ctype == XML_ELEMENT_NODE as c_int
861 {
862 let sub = node_get_content(child);
863 if !sub.is_null() {
864 let len = crate::abi::exports_xml2::xmlStrlen(sub);
865 result.extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
866 allocator::xmlFreeImpl(sub as *mut c_void);
867 }
868 }
869 child = (*child).next;
870 }
871 }
872 t if t == XML_ENTITY_REF_NODE as c_int => {
873 // Entity reference: expand via the declaration its `children`
874 // points at (xmlNewReference / the parser bind the entity decl
875 // there), falling back to the document/predefined lookup.
876 //
877 // UPSTREAM-PARITY (tree.c xmlBufGetEntityRefContent): a
878 // PREDEFINED entity contributes its `content`; any OTHER entity
879 // contributes its CHILD content (the parsed replacement tree).
880 // An internal entity declaration created from `<!ENTITY test
881 // "...">` carries only `content` and NO child tree, so a
882 // reference to it reads as "" — php delayed_freeing/
883 // entity_reference expects exactly this (and
884 // `new DOMEntityReference("amp")` reads "&").
885 let mut ent = if (*node).children.is_null() {
886 ptr::null_mut()
887 } else {
888 (*node).children as *mut _xmlEntity
889 };
890 if ent.is_null() {
891 let name = (*node).name;
892 if !name.is_null() {
893 ent = crate::xml::tree::get_doc_entity((*node).doc, name);
894 }
895 }
896 if !ent.is_null() {
897 let is_predef = (*ent).etype
898 == crate::abi::types::xmlEntityType::XML_INTERNAL_PREDEFINED_ENTITY as c_int;
899 if is_predef {
900 if !(*ent).content.is_null() {
901 let len = crate::abi::exports_xml2::xmlStrlen((*ent).content);
902 result.extend_from_slice(core::slice::from_raw_parts(
903 (*ent).content,
904 len as usize,
905 ));
906 }
907 } else if (*ent).flags & (1 << 3) == 0 {
908 // UPSTREAM-PARITY (tree.c xmlBufGetEntityRefContent): the
909 // XML_ENT_EXPANDING flag (candidate bit 1 << 3, parser
910 // convention) breaks self-referential loops while the
911 // declaration's replacement tree is walked.
912 (*ent).flags |= 1 << 3;
913 let mut child = (*ent).children;
914 while !child.is_null() {
915 let sub = node_get_content(child);
916 if !sub.is_null() {
917 let len = crate::abi::exports_xml2::xmlStrlen(sub);
918 result
919 .extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
920 allocator::xmlFreeImpl(sub as *mut c_void);
921 }
922 child = (*child).next;
923 }
924 (*ent).flags &= !(1 << 3);
925 }
926 }
927 }
928 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
929 let root = doc_get_root_element(node as *mut _xmlDoc);
930 if !root.is_null() {
931 let sub = node_get_content(root);
932 if !sub.is_null() {
933 let len = crate::abi::exports_xml2::xmlStrlen(sub);
934 result.extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
935 allocator::xmlFreeImpl(sub as *mut c_void);
936 }
937 }
938 }
939 t if t == XML_NAMESPACE_DECL as c_int => {
940 // XPath namespace node (an independent `_xmlNs` copy cast to
941 // `_xmlNode`, xmlXPathNodeSetDupNs semantics): its string-value is
942 // the namespace URI. The copy has NO tree links — the fields that
943 // _xmlNode accessors read (children at the `prefix` offset, etc.)
944 // are ns data, so this must not fall into the element arm.
945 // php's DOMXPath php:functionString relies on this conversion
946 // (xmlXPathCastToString of a namespace node-set).
947 let ns = node as *mut crate::abi::structs::_xmlNs;
948 if !(*ns).href.is_null() {
949 let len = crate::abi::exports_xml2::xmlStrlen((*ns).href);
950 result.extend_from_slice(core::slice::from_raw_parts((*ns).href, len as usize));
951 }
952 }
953 _ => {
954 // Element and everything else: concatenate descendant text
955 // content (XPath 1.0 string-value semantics — §4.2 / tree.c
956 // xmlNodeGetContent, which walks the full subtree, not just
957 // direct text children).
958 let mut child = (*node).children;
959 while !child.is_null() {
960 let ctype = (*child).type_;
961 if ctype == XML_TEXT_NODE as c_int || ctype == XML_CDATA_SECTION_NODE as c_int {
962 if !(*child).content.is_null() {
963 let len = crate::abi::exports_xml2::xmlStrlen((*child).content);
964 result.extend_from_slice(core::slice::from_raw_parts(
965 (*child).content,
966 len as usize,
967 ));
968 }
969 } else if ctype == XML_ENTITY_REF_NODE as c_int
970 || ctype == XML_ELEMENT_NODE as c_int
971 {
972 let sub = node_get_content(child);
973 if !sub.is_null() {
974 let len = crate::abi::exports_xml2::xmlStrlen(sub);
975 result.extend_from_slice(core::slice::from_raw_parts(sub, len as usize));
976 allocator::xmlFreeImpl(sub as *mut c_void);
977 }
978 }
979 child = (*child).next;
980 }
981 }
982 }
983 // Allocate the C string.
984 let buf = allocator::xmlMallocImpl(result.len() + 1) as *mut xmlChar;
985 if buf.is_null() {
986 return ptr::null_mut();
987 }
988 if !result.is_empty() {
989 ptr::copy_nonoverlapping(result.as_ptr(), buf, result.len());
990 }
991 *buf.add(result.len()) = 0;
992 buf
993}
994
995// ═══════════════════════════════════════════════════════════════════════════════
996// Node Operations
997// ═══════════════════════════════════════════════════════════════════════════════
998
999/// Create a new XML node.
1000///
1001/// # UPSTREAM-PARITY
1002///
1003/// ```c
1004/// xmlNodePtr xmlNewNode(xmlNsPtr ns, const xmlChar *name);
1005/// ```
1006///
1007/// Creates a new element node with the given name and namespace.
1008///
1009/// # SAFETY
1010///
1011/// - `ns` may be NULL.
1012/// - `name` must be a valid null-terminated string or NULL (NULL returns
1013/// NULL — upstream tree.c `xmlNewNode` rejects a NULL name up front,
1014/// HOSTILE-ABI A48).
1015pub unsafe fn new_node(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
1016 if name.is_null() {
1017 return ptr::null_mut();
1018 }
1019 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1020 if node.is_null() {
1021 return ptr::null_mut();
1022 }
1023
1024 unsafe {
1025 (*node).type_ = XML_ELEMENT_NODE as c_int;
1026 (*node).name = dup_xml_str(name);
1027 (*node).ns = ns;
1028 (*node).line = 0;
1029 (*node).extra = 0;
1030
1031 if !ns.is_null() {
1032 (*ns).context = node as *mut _xmlDoc;
1033 }
1034 }
1035
1036 // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
1037 // node is fully initialised.
1038 crate::abi::data_globals::register_node_hook(node);
1039
1040 node
1041}
1042
1043/// Create a new XML element node whose name is BORROWED (not duplicated).
1044///
1045/// # UPSTREAM-PARITY
1046///
1047/// ```c
1048/// xmlNodePtr xmlNewDocNodeEatName(xmlDocPtr doc, xmlNsPtr ns,
1049/// const xmlChar *name, const xmlChar *content);
1050/// ```
1051///
1052/// The name pointer is stored as-is; the caller keeps ownership. The XML
1053/// parser uses this when `dictNames` is enabled: the name is an interned
1054/// dictionary string owned by the document dictionary, and consumers (lxml
1055/// objectify `_tagMatches`) rely on node names being pointer-identical to
1056/// `xmlDictLookup`/`xmlDictExists` results. `free_node` consults
1057/// `xmlDictOwns` (UPSTREAM-PARITY `DICT_FREE`) so borrowed dictionary names
1058/// are never freed.
1059///
1060/// # SAFETY
1061///
1062/// - `ns` may be NULL.
1063/// - `name` must be non-NULL and remain valid for the lifetime of the node.
1064pub unsafe fn new_node_eat_name(ns: *mut _xmlNs, name: *const xmlChar) -> *mut _xmlNode {
1065 if name.is_null() {
1066 return ptr::null_mut();
1067 }
1068 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1069 if node.is_null() {
1070 return ptr::null_mut();
1071 }
1072
1073 unsafe {
1074 (*node).type_ = XML_ELEMENT_NODE as c_int;
1075 (*node).name = name as *mut xmlChar;
1076 (*node).ns = ns;
1077 (*node).line = 0;
1078 (*node).extra = 0;
1079
1080 if !ns.is_null() {
1081 (*ns).context = node as *mut _xmlDoc;
1082 }
1083 }
1084
1085 // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
1086 // node is fully initialised.
1087 crate::abi::data_globals::register_node_hook(node);
1088
1089 node
1090}
1091
1092/// Free a single node (without freeing children).
1093///
1094/// # UPSTREAM-PARITY
1095///
1096/// ```c
1097/// void xmlFreeNode(xmlNodePtr node);
1098/// ```
1099///
1100/// Frees a node and its properties/namespaces, but NOT its children.
1101/// Children must be freed separately or reattached.
1102///
1103/// # SAFETY
1104///
1105/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1106pub unsafe fn free_node(node: *mut _xmlNode) {
1107 if node.is_null() {
1108 return;
1109 }
1110
1111 let n = unsafe { &mut *node };
1112
1113 // UPSTREAM-PARITY (tree.c xmlFreeNode): declaration nodes and namespace
1114 // declarations are routed to their dedicated free functions (their
1115 // struct layouts diverge from _xmlNode).
1116 if n.type_ == XML_DTD_NODE as c_int {
1117 free_dtd(node as *mut _xmlDtd);
1118 return;
1119 } else if n.type_ == XML_NAMESPACE_DECL as c_int {
1120 free_ns(node as *mut _xmlNs);
1121 return;
1122 } else if n.type_ == XML_ATTRIBUTE_NODE as c_int {
1123 free_prop(node as *mut _xmlAttr);
1124 return;
1125 } else if n.type_ == XML_ELEMENT_DECL as c_int {
1126 crate::xml::dtd::free_element(node as *mut _xmlElement);
1127 return;
1128 } else if n.type_ == XML_ATTRIBUTE_DECL as c_int {
1129 crate::xml::dtd::free_attribute(node as *mut _xmlAttribute);
1130 return;
1131 } else if n.type_ == XML_ENTITY_DECL as c_int {
1132 crate::xml::entities::free_entity(node as *mut _xmlEntity);
1133 return;
1134 }
1135
1136 // UPSTREAM-PARITY (tree.c xmlFreeNode): the deregister hook fires before
1137 // the node is torn down.
1138 crate::abi::data_globals::deregister_node_hook(node);
1139
1140 // Free properties and namespace declarations. Only element nodes carry
1141 // them; compact text nodes store their inline content at the address of
1142 // the `properties` field (and the following `nsDef` field), so touching
1143 // these for other node types would read text bytes as pointers.
1144 let is_element = n.type_ == XML_ELEMENT_NODE as c_int;
1145 if is_element && !n.properties.is_null() {
1146 free_prop_list(n.properties);
1147 }
1148
1149 if is_element && !n.nsDef.is_null() {
1150 free_ns_list(n.nsDef);
1151 }
1152
1153 // UPSTREAM-PARITY (tree.c xmlFreeNode + DICT_FREE): node names and
1154 // content may live in the document dictionary — consumers (lxml
1155 // `_fixHtmlDictNodeNames`) intern HTML element/attribute names with
1156 // xmlDictLookup, so the same interned pointer is shared by every node
1157 // with that name. Dict-owned strings must NOT be freed here; the guard
1158 // is: free iff there is no dict, or the dict does not own the string.
1159 let dict = if n.doc.is_null() {
1160 ptr::null_mut()
1161 } else {
1162 unsafe { (*n.doc).dict }
1163 };
1164
1165 // Free the name.
1166 // UPSTREAM-PARITY (tree.c xmlNewText / xmlNewComment + xmlCopyNode): text,
1167 // CDATA, comment and PI nodes store their `name` as one of the SHARED static
1168 // markers xmlStringText / xmlStringTextNoenc / xmlStringComment (never
1169 // dict- or heap-owned) and those must never be freed here — freeing one
1170 // aborts with "free(): invalid pointer" at teardown (PHP modern/spec
1171 // Node_isDefaultNamespace was the trigger). The only non-dict node names the
1172 // candidate attaches are these statics, so pointer-equality guards fully
1173 // close the leak/double-free.
1174 if !n.name.is_null() && !crate::abi::exports_hash::dict_owns_str(dict, n.name) {
1175 let sentinels = [
1176 crate::abi::data_globals::xmlStringText.as_ptr() as *const c_void,
1177 crate::abi::data_globals::xmlStringTextNoenc.as_ptr() as *const c_void,
1178 crate::abi::data_globals::xmlStringComment.as_ptr() as *const c_void,
1179 ];
1180 let is_sentinel = sentinels.iter().any(|&p| p == n.name as *const c_void);
1181 if !is_sentinel {
1182 allocator::xmlFreeImpl(n.name as *mut c_void);
1183 }
1184 }
1185
1186 // Free content (for text/CDATA nodes). Compact text content lives inside
1187 // the node struct (at the `properties` field address) and must not be
1188 // freed separately. UPSTREAM-PARITY: entity-reference content is shared
1189 // with the entity declaration and must not be freed here.
1190 if !n.content.is_null() {
1191 let node_type = n.type_;
1192 if node_type == XML_TEXT_NODE as c_int
1193 || node_type == XML_CDATA_SECTION_NODE as c_int
1194 || node_type == XML_COMMENT_NODE as c_int
1195 || node_type == XML_PI_NODE as c_int
1196 {
1197 let inline_addr = std::ptr::addr_of_mut!((*node).properties) as *const c_void;
1198 if n.content as *const c_void != inline_addr
1199 && !crate::abi::exports_hash::dict_owns_str(dict, n.content)
1200 {
1201 allocator::xmlFreeImpl(n.content as *mut c_void);
1202 }
1203 }
1204 }
1205
1206 allocator::xmlFreeImpl(node as *mut c_void);
1207}
1208
1209/// Free a linked list of nodes.
1210///
1211/// Frees all nodes in the list and their children (depth-first, children
1212/// before parents) WITHOUT C recursion — upstream xmlFreeNodeList walks the
1213/// tree with an explicit depth counter (tree.c 2.15), and a 100k-deep
1214/// document must tear down without overflowing the stack (GH-22570: the
1215/// recursive version segv'd at php shutdown on the deep Dom\XMLDocument).
1216///
1217/// # UPSTREAM-PARITY
1218///
1219/// ```c
1220/// void xmlFreeNodeList(xmlNodePtr node);
1221/// ```
1222///
1223/// Does NOT descend into `XML_ENTITY_REF_NODE` children (their child list
1224/// points at the shared entity declaration, owned by the DTD) nor free
1225/// `XML_DTD_NODE` children (a DTD node in the list is unlinked, not freed —
1226/// xmlFreeDtd owns the subset teardown).
1227///
1228/// # SAFETY
1229///
1230/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1231pub unsafe fn free_node_list(mut cur: *mut _xmlNode) {
1232 // Explicit resume frames replace the old recursion: each frame is a node
1233 // whose children-subtree free is in progress, plus the node's next
1234 // sibling read before the descent (the struct is freed on resume, so the
1235 // `next` pointer must not be read afterwards).
1236 let mut frames: Vec<(*mut _xmlNode, *mut _xmlNode)> = Vec::new();
1237 loop {
1238 // Process the current sibling list; descend into the first node that
1239 // carries a freeable children subtree (post-order: children first).
1240 while !cur.is_null() {
1241 let next = unsafe { (*cur).next };
1242 let t = unsafe { (*cur).type_ };
1243
1244 if t == XML_DTD_NODE as c_int {
1245 // UPSTREAM-PARITY: DTD nodes are unlinked but not freed here.
1246 unsafe {
1247 (*cur).prev = ptr::null_mut();
1248 (*cur).next = ptr::null_mut();
1249 }
1250 cur = next;
1251 continue;
1252 }
1253
1254 // Free children first (entity-ref children are shared with the
1255 // entity declaration and are owned by the DTD).
1256 if t != XML_ENTITY_REF_NODE as c_int && !unsafe { (*cur).children }.is_null() {
1257 frames.push((cur, next));
1258 cur = unsafe { (*cur).children };
1259 continue;
1260 }
1261
1262 free_node(cur);
1263 cur = next;
1264 }
1265 // Sibling list exhausted: resume the innermost descended parent.
1266 let Some((parent, next)) = frames.pop() else {
1267 break;
1268 };
1269 free_node(parent);
1270 cur = next;
1271 }
1272}
1273
1274/// Free a linked list of properties.
1275///
1276/// # SAFETY
1277///
1278/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
1279unsafe fn free_prop_list(prop: *mut _xmlAttr) {
1280 let mut cur = prop;
1281 while !cur.is_null() {
1282 let next = unsafe { (*cur).next };
1283 free_prop(cur);
1284 cur = next;
1285 }
1286}
1287
1288/// Free a single attribute (upstream `xmlFreeProp`).
1289///
1290/// # SAFETY
1291///
1292/// - `prop` must be a valid pointer to an _xmlAttr, or NULL.
1293unsafe fn free_prop(prop: *mut _xmlAttr) {
1294 if prop.is_null() {
1295 return;
1296 }
1297
1298 // UPSTREAM-PARITY (tree.c xmlFreeProp): freeing an ID attribute removes
1299 // its entry from the document's ID table (xmlRemoveID), so xmlGetID stops
1300 // reporting it. A NULL doc->ids (document teardown) is a no-op.
1301 if !unsafe { (*prop).doc }.is_null() && !unsafe { (*prop).id }.is_null() {
1302 crate::xml::validation::remove_id(unsafe { (*prop).doc }, prop);
1303 }
1304
1305 // Free children (text nodes with value)
1306 if !unsafe { (*prop).children }.is_null() {
1307 free_node_list(unsafe { (*prop).children });
1308 }
1309
1310 // UPSTREAM-PARITY (tree.c xmlFreeProp + DICT_FREE): attribute names may
1311 // be dict-interned (lxml `_fixHtmlDictNodeNames`); interned names are
1312 // shared and must not be freed here.
1313 let dict = if unsafe { (*prop).doc }.is_null() {
1314 ptr::null_mut()
1315 } else {
1316 unsafe { (*(*prop).doc).dict }
1317 };
1318
1319 // Free name
1320 if !unsafe { (*prop).name }.is_null()
1321 && !crate::abi::exports_hash::dict_owns_str(dict, unsafe { (*prop).name })
1322 {
1323 allocator::xmlFreeImpl(unsafe { (*prop).name } as *mut c_void);
1324 }
1325
1326 allocator::xmlFreeImpl(prop as *mut c_void);
1327}
1328
1329/// Free a linked list of namespace declarations.
1330///
1331/// # SAFETY
1332///
1333/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
1334unsafe fn free_ns_list(ns: *mut _xmlNs) {
1335 let mut cur = ns;
1336 while !cur.is_null() {
1337 let next = unsafe { (*cur).next };
1338 free_ns(cur);
1339 cur = next;
1340 }
1341}
1342
1343/// Free a single namespace declaration (upstream `xmlFreeNs`).
1344///
1345/// # SAFETY
1346///
1347/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
1348unsafe fn free_ns(ns: *mut _xmlNs) {
1349 if ns.is_null() {
1350 return;
1351 }
1352
1353 // Free href and prefix
1354 if !unsafe { (*ns).href }.is_null() {
1355 allocator::xmlFreeImpl(unsafe { (*ns).href } as *mut c_void);
1356 }
1357 if !unsafe { (*ns).prefix }.is_null() {
1358 allocator::xmlFreeImpl(unsafe { (*ns).prefix } as *mut c_void);
1359 }
1360
1361 allocator::xmlFreeImpl(ns as *mut c_void);
1362}
1363
1364/// Copy a node (shallow or deep).
1365///
1366/// # UPSTREAM-PARITY
1367///
1368/// ```c
1369/// xmlNodePtr xmlCopyNode(xmlNodePtr node, int recursive);
1370/// ```
1371///
1372/// If `recursive` is 1, children are also copied.
1373/// Returns the new node, or NULL on failure.
1374///
1375/// # SAFETY
1376///
1377/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1378pub unsafe fn copy_node(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
1379 if node.is_null() {
1380 return ptr::null_mut();
1381 }
1382
1383 let n = unsafe { &*node };
1384
1385 let new_node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
1386 if new_node.is_null() {
1387 return ptr::null_mut();
1388 }
1389
1390 unsafe {
1391 (*new_node).type_ = n.type_;
1392 (*new_node).name = dup_xml_str(n.name);
1393 // UPSTREAM-PARITY (tree.c xmlStaticCopyNode): the line number is
1394 // copied for element nodes only; text/CDATA/comment/PI copies keep
1395 // line 0.
1396 if n.type_ == XML_ELEMENT_NODE as c_int {
1397 (*new_node).line = n.line;
1398 }
1399 (*new_node).extra = n.extra;
1400 (*new_node).psvi = n.psvi;
1401 // UPSTREAM-PARITY (tree.c xmlStaticCopyNode): _private is NOT copied
1402 // — the copy is a fresh zeroed node (memset 0). Consumers (PHP's
1403 // php_libxml_* wrappers) key their registrations on node->_private:
1404 // a copied node must look UNREGISTERED so the clone gets its own
1405 // wrapper and document binding (ext/simplexml bug63575 — the cloned
1406 // document's nodes inherited the original's php wrappers, so XPath
1407 // and mutations resolved into the ORIGINAL document).
1408
1409 // Copy namespace pointer (NOT the ns declaration — just the reference)
1410 (*new_node).ns = n.ns;
1411
1412 // Copy namespace declarations (element nodes only; compact text nodes
1413 // store inline content over the `properties`/`nsDef` fields).
1414 let is_element = n.type_ == XML_ELEMENT_NODE as c_int;
1415 if is_element && !n.nsDef.is_null() {
1416 (*new_node).nsDef = copy_ns_list(n.nsDef);
1417 }
1418
1419 // Copy content for text/CDATA/comment/PI nodes
1420 let node_type = n.type_;
1421 if (node_type == XML_TEXT_NODE as c_int
1422 || node_type == XML_CDATA_SECTION_NODE as c_int
1423 || node_type == XML_COMMENT_NODE as c_int
1424 || node_type == XML_PI_NODE as c_int)
1425 && !n.content.is_null()
1426 {
1427 (*new_node).content = dup_xml_str(n.content);
1428 }
1429
1430 // Copy properties (element nodes only).
1431 if is_element && !n.properties.is_null() {
1432 (*new_node).properties = copy_prop_list(n.properties);
1433 // Update doc links on properties
1434 let mut prop = (*new_node).properties;
1435 while !prop.is_null() {
1436 (*prop).parent = new_node;
1437 if !(*prop).children.is_null() {
1438 propagate_doc((*prop).children, (*new_node).doc);
1439 }
1440 prop = (*prop).next;
1441 }
1442 }
1443
1444 // Copy children if recursive (each copied child gets its parent and
1445 // document pointers; `last` follows the upstream link order).
1446 if recursive != 0 && !n.children.is_null() {
1447 (*new_node).children = copy_node_list(n.children, recursive);
1448 if !(*new_node).children.is_null() {
1449 let mut child = (*new_node).children;
1450 let mut last_child = child;
1451 while !child.is_null() {
1452 (*child).parent = new_node;
1453 (*child).doc = (*new_node).doc;
1454 propagate_doc(child, (*new_node).doc);
1455 if (*child).next.is_null() {
1456 last_child = child;
1457 }
1458 child = (*child).next;
1459 }
1460 (*new_node).last = last_child;
1461 }
1462 }
1463 }
1464
1465 new_node
1466}
1467
1468/// Copy a linked list of nodes.
1469///
1470/// Returns the first node of the new list, or NULL on failure.
1471///
1472/// # Safety
1473///
1474/// - `node` must be NULL or a valid pointer to an `_xmlNode`; when non-NULL it
1475/// is copied with `copy_node` and its `next` chain is walked, so every node
1476/// reachable through `next` must be valid and alive.
1477/// - `recursive` is forwarded to `copy_node` and selects deep versus shallow
1478/// copy; deep copies require valid `children` subtrees.
1479unsafe fn copy_node_list(node: *const _xmlNode, recursive: c_int) -> *mut _xmlNode {
1480 if node.is_null() {
1481 return ptr::null_mut();
1482 }
1483
1484 let n = unsafe { &*node };
1485 let new_node = copy_node(node, recursive);
1486 if new_node.is_null() {
1487 return ptr::null_mut();
1488 }
1489
1490 let mut prev = new_node;
1491 let mut cur = n.next;
1492
1493 while !cur.is_null() {
1494 let new_cur = copy_node(cur, recursive);
1495 if new_cur.is_null() {
1496 break;
1497 }
1498 unsafe {
1499 (*prev).next = new_cur;
1500 (*new_cur).prev = prev;
1501 }
1502 prev = new_cur;
1503 cur = unsafe { (*cur).next };
1504 }
1505
1506 new_node
1507}
1508
1509/// Copy a linked list of namespace declarations.
1510///
1511/// # Safety
1512///
1513/// - `ns` must be NULL or a valid pointer to an `_xmlNs`; when non-NULL its
1514/// fields are read and its `next` chain is walked, so every reachable
1515/// `_xmlNs` must be valid and alive.
1516/// - `href` and `prefix` of each entry may be NULL or NUL-terminated `xmlChar`
1517/// strings; `dup_xml_str` reads them as C strings.
1518unsafe fn copy_ns_list(ns: *const _xmlNs) -> *mut _xmlNs {
1519 if ns.is_null() {
1520 return ptr::null_mut();
1521 }
1522
1523 let n = unsafe { &*ns };
1524 let new_ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1525 if new_ns.is_null() {
1526 return ptr::null_mut();
1527 }
1528
1529 unsafe {
1530 (*new_ns).type_ = n.type_;
1531 (*new_ns).href = dup_xml_str(n.href);
1532 (*new_ns).prefix = dup_xml_str(n.prefix);
1533 // UPSTREAM-PARITY (tree.c xmlCopyNamespaceList): _private is NOT
1534 // copied (PHP tags some namespace declarations through ns->_private;
1535 // a copied declaration must not alias the original's registration).
1536 }
1537
1538 let mut prev = new_ns;
1539 let mut cur = n.next;
1540
1541 while !cur.is_null() {
1542 let c = unsafe { &*cur };
1543 let new_cur = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
1544 if new_cur.is_null() {
1545 break;
1546 }
1547 unsafe {
1548 (*new_cur).type_ = c.type_;
1549 (*new_cur).href = dup_xml_str(c.href);
1550 (*new_cur).prefix = dup_xml_str(c.prefix);
1551 (*prev).next = new_cur;
1552 }
1553 prev = new_cur;
1554 cur = c.next;
1555 }
1556
1557 new_ns
1558}
1559
1560/// Copy a linked list of properties.
1561///
1562/// # Safety
1563///
1564/// - `prop` must be NULL or a valid pointer to an `_xmlAttr`; its fields are
1565/// read and its `next` chain is walked, so every reachable `_xmlAttr` must
1566/// be valid and alive.
1567/// - `children` of each attribute must be NULL or a valid node list; it is
1568/// copied recursively via `copy_node_list`. `name` may be NULL or a
1569/// NUL-terminated `xmlChar` string read by `dup_xml_str`.
1570unsafe fn copy_prop_list(prop: *const _xmlAttr) -> *mut _xmlAttr {
1571 if prop.is_null() {
1572 return ptr::null_mut();
1573 }
1574
1575 let p = unsafe { &*prop };
1576 let new_prop = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1577 if new_prop.is_null() {
1578 return ptr::null_mut();
1579 }
1580
1581 unsafe {
1582 (*new_prop).type_ = p.type_;
1583 (*new_prop).name = dup_xml_str(p.name);
1584 (*new_prop).ns = p.ns;
1585 (*new_prop).atype = p.atype;
1586
1587 // Copy children (text value nodes)
1588 if !p.children.is_null() {
1589 (*new_prop).children = copy_node_list(p.children, 1);
1590 if !(*new_prop).children.is_null() {
1591 (*(*new_prop).children).parent = new_prop as *mut _xmlNode;
1592 }
1593 }
1594 }
1595
1596 let mut prev = new_prop;
1597 let mut cur = p.next;
1598
1599 while !cur.is_null() {
1600 let c = unsafe { &*cur };
1601 let new_cur = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
1602 if new_cur.is_null() {
1603 break;
1604 }
1605 unsafe {
1606 (*new_cur).type_ = c.type_;
1607 (*new_cur).name = dup_xml_str(c.name);
1608 (*new_cur).ns = c.ns;
1609 (*new_cur).atype = c.atype;
1610
1611 if !c.children.is_null() {
1612 (*new_cur).children = copy_node_list(c.children, 1);
1613 if !(*new_cur).children.is_null() {
1614 (*(*new_cur).children).parent = new_cur as *mut _xmlNode;
1615 }
1616 }
1617
1618 (*prev).next = new_cur;
1619 }
1620 prev = new_cur;
1621 cur = c.next;
1622 }
1623
1624 new_prop
1625}
1626
1627/// Propagate the document pointer to all descendants of a node.
1628///
1629/// # Safety
1630///
1631/// - `node` must be NULL or a valid pointer to an `_xmlNode`; the function
1632/// walks the whole subtree through `properties`, `children`, and `next`
1633/// links, so every reachable node and attribute must be a valid, live
1634/// struct.
1635/// - `doc` may be NULL or a valid pointer to an `_xmlDoc`; it is only stored
1636/// into `doc` fields, never dereferenced.
1637unsafe fn propagate_doc(node: *mut _xmlNode, doc: *mut _xmlDoc) {
1638 let mut cur = node;
1639 while !cur.is_null() {
1640 unsafe {
1641 // UPSTREAM-PARITY (tree.c xmlSetTreeDoc/xmlNodeSetDoc): a node
1642 // whose document actually changes must move its dict-owned
1643 // name/content into the destination document's dictionary (or
1644 // heap copies) and drop its ID-table entry, otherwise the source
1645 // doc's teardown frees strings the moved subtree still points at
1646 // (double free). No-op delegation when the doc pointer already
1647 // matches.
1648 if (*cur).doc != doc {
1649 crate::abi::exports_tree::node_set_doc_impl(cur, doc);
1650 }
1651
1652 // Propagate to properties (element nodes only; other node types
1653 // never carry properties, and compact text nodes store inline
1654 // content at the `properties` field address).
1655 if (*cur).type_ == XML_ELEMENT_NODE as c_int {
1656 let mut prop = (*cur).properties;
1657 while !prop.is_null() {
1658 if (*prop).doc != doc {
1659 crate::abi::exports_tree::node_set_doc_impl(prop as *mut _xmlNode, doc);
1660 }
1661 if !(*prop).children.is_null() {
1662 propagate_doc((*prop).children, doc);
1663 }
1664 prop = (*prop).next;
1665 }
1666 }
1667
1668 // Recurse into children
1669 if !(*cur).children.is_null() {
1670 propagate_doc((*cur).children, doc);
1671 }
1672 }
1673 cur = unsafe { (*cur).next };
1674 }
1675}
1676
1677/// Unlink a node from its parent/siblings.
1678///
1679/// # UPSTREAM-PARITY
1680///
1681/// ```c
1682/// void xmlUnlinkNode(xmlNodePtr node);
1683/// ```
1684///
1685/// Removes the node from its parent's child list and sibling list.
1686/// The node's parent, prev, and next pointers are cleared.
1687/// The node is NOT freed — the caller is responsible for freeing it.
1688///
1689/// # SAFETY
1690///
1691/// - `node` must be a valid pointer to an _xmlNode, or NULL.
1692pub unsafe fn unlink_node(node: *mut _xmlNode) {
1693 if node.is_null() {
1694 return;
1695 }
1696
1697 let n = unsafe { &mut *node };
1698
1699 // UPSTREAM-PARITY (tree.c xmlUnlinkNode): an unlinked DTD node is
1700 // detached from the document's internal/external subset pointers so a
1701 // later xmlFreeDoc doesn't free it again (nokogiri pins an unlinked DTD
1702 // and frees it via xmlFreeDtd, then frees the doc — without clearing
1703 // doc->intSubset the doc double-frees the DTD).
1704 let doc = n.doc;
1705 if n.type_ == XML_DTD_NODE as c_int && !doc.is_null() {
1706 if (*doc).intSubset == node as *mut _xmlDtd {
1707 (*doc).intSubset = ptr::null_mut();
1708 }
1709 if (*doc).extSubset == node as *mut _xmlDtd {
1710 (*doc).extSubset = ptr::null_mut();
1711 }
1712 }
1713
1714 // Fix up prev/next chain
1715 let prev = n.prev;
1716 let next = n.next;
1717
1718 if !prev.is_null() {
1719 unsafe { (*prev).next = next };
1720 }
1721 if !next.is_null() {
1722 unsafe { (*next).prev = prev };
1723 }
1724
1725 // Fix up parent's children/last pointers. UPSTREAM-PARITY
1726 // (tree.c xmlUnlinkNodeInternal): an attribute node is tracked by the
1727 // parent element's `properties` chain, not by children/last — when the
1728 // unlinked node is an attribute, advance the `properties` head past it so
1729 // the parent no longer references a (possibly freed) attribute. Without
1730 // this, nokogiri unlinks an attribute and later frees it while the element
1731 // still hangs off the stale `properties` slot, and an XPath attribute-axis
1732 // walk dereferences freed memory.
1733 let parent = n.parent;
1734 if !parent.is_null() {
1735 if n.type_ == XML_ATTRIBUTE_NODE as c_int {
1736 if unsafe { (*parent).properties } == node as *mut _xmlAttr {
1737 unsafe { (*parent).properties = next as *mut _xmlAttr };
1738 }
1739 } else {
1740 if unsafe { (*parent).children } == node {
1741 unsafe { (*parent).children = next };
1742 }
1743 if unsafe { (*parent).last } == node {
1744 unsafe { (*parent).last = prev };
1745 }
1746 }
1747 }
1748
1749 // Also fix up doc-level children/last if node is a direct doc child
1750 let doc = n.doc;
1751 if !doc.is_null() && !parent.is_null() {
1752 // Already handled above
1753 }
1754 if !doc.is_null() && parent.is_null() {
1755 // Node is a direct child of the document
1756 if unsafe { (*doc).children } == node {
1757 unsafe { (*doc).children = next };
1758 }
1759 if unsafe { (*doc).last } == node {
1760 unsafe { (*doc).last = prev };
1761 }
1762 }
1763
1764 // Clear the node's links
1765 n.parent = ptr::null_mut();
1766 n.prev = ptr::null_mut();
1767 n.next = ptr::null_mut();
1768}
1769
1770/// Add a child node to a parent.
1771///
1772/// # UPSTREAM-PARITY
1773///
1774/// ```c
1775/// xmlNodePtr xmlAddChild(xmlNodePtr parent, xmlNodePtr cur);
1776/// ```
1777///
1778/// Adds `cur` as the last child of `parent`.
1779/// Returns the child, or NULL on failure.
1780///
1781/// # SAFETY
1782///
1783/// - `parent` must be a valid pointer to an _xmlNode.
1784/// - `cur` must be a valid pointer to an _xmlNode.
1785pub unsafe fn add_child(parent: *mut _xmlNode, cur: *mut _xmlNode) -> *mut _xmlNode {
1786 if parent.is_null() || cur.is_null() {
1787 return ptr::null_mut();
1788 }
1789
1790 let p = unsafe { &mut *parent };
1791 let c = unsafe { &mut *cur };
1792
1793 // If cur is already linked, unlink it first
1794 if !c.parent.is_null() || !c.prev.is_null() || !c.next.is_null() {
1795 unlink_node(cur);
1796 }
1797
1798 // UPSTREAM-PARITY (tree.c xmlAddChild): attaching an ATTRIBUTE node routes
1799 // it into the parent element's PROPERTIES list, not its children list
1800 // (lxml/PHP `element->setAttributeNode(attr)` calls xmlAddChild(elem, attr)
1801 // literally). Without this branch the attribute was appended to `children`
1802 // and then serialized as a bogus child text node / doubly freed on teardown.
1803 // The attribute keeps its own name (already set by xmlNewProp) and is
1804 // appended after the existing properties, mirroring how set_prop links new
1805 // attributes so serialization, clone, and free all treat it as a real attr.
1806 if c.type_ == XML_ATTRIBUTE_NODE as c_int {
1807 c.parent = parent;
1808 c.prev = ptr::null_mut();
1809 c.next = ptr::null_mut();
1810 if p.properties.is_null() {
1811 p.properties = cur as *mut crate::abi::structs::_xmlAttr;
1812 } else {
1813 let mut last = p.properties;
1814 while !unsafe { (*last).next }.is_null() {
1815 last = unsafe { (*last).next };
1816 }
1817 unsafe { (*last).next = cur as *mut crate::abi::structs::_xmlAttr };
1818 c.prev = last as *mut _xmlNode;
1819 }
1820 // Re-parent into the element's document so the attribute and its text
1821 // value share the owner element's doc (propagate_doc also descends into
1822 // attribute text children).
1823 if !p.doc.is_null() && c.doc != p.doc {
1824 propagate_doc(cur, p.doc);
1825 }
1826 return cur;
1827 }
1828
1829 // Update parent/child links
1830 c.parent = parent;
1831
1832 if p.children.is_null() {
1833 // First child
1834 p.children = cur;
1835 p.last = cur;
1836 c.prev = ptr::null_mut();
1837 c.next = ptr::null_mut();
1838 } else {
1839 // Append to end
1840 c.prev = p.last;
1841 c.next = ptr::null_mut();
1842 if !p.last.is_null() {
1843 unsafe { (*p.last).next = cur };
1844 }
1845 p.last = cur;
1846 }
1847
1848 // Update doc
1849 let doc = if !p.doc.is_null() {
1850 p.doc
1851 } else {
1852 ptr::null_mut()
1853 };
1854 if !doc.is_null() && c.doc != doc {
1855 propagate_doc(cur, doc);
1856 }
1857
1858 cur
1859}
1860
1861/// Add a sibling node after another.
1862///
1863/// # UPSTREAM-PARITY
1864///
1865/// ```c
1866/// xmlNodePtr xmlAddSibling(xmlNodePtr cur, xmlNodePtr elem);
1867/// ```
1868///
1869/// Adds `elem` as the next sibling of `cur`.
1870/// Returns `elem`, or NULL on failure.
1871///
1872/// # SAFETY
1873///
1874/// - `cur` must be a valid pointer to an _xmlNode.
1875/// - `elem` must be a valid pointer to an _xmlNode.
1876pub unsafe fn add_sibling(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
1877 if cur.is_null() || elem.is_null() {
1878 return ptr::null_mut();
1879 }
1880
1881 let c = unsafe { &mut *cur };
1882
1883 // If elem is already linked, unlink it first
1884 let e = unsafe { &mut *elem };
1885 if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
1886 unlink_node(elem);
1887 }
1888
1889 // Set parent
1890 e.parent = c.parent;
1891
1892 // Link elem after cur
1893 e.prev = cur;
1894 e.next = c.next;
1895
1896 if !c.next.is_null() {
1897 unsafe { (*c.next).prev = elem };
1898 }
1899 c.next = elem;
1900
1901 // Update parent's last if needed
1902 let parent = c.parent;
1903 if !parent.is_null() && unsafe { (*parent).last } == cur {
1904 unsafe { (*parent).last = elem };
1905 }
1906
1907 // Update doc
1908 if !c.doc.is_null() && e.doc != c.doc {
1909 propagate_doc(elem, c.doc);
1910 }
1911
1912 elem
1913}
1914
1915/// Add a sibling node before another.
1916///
1917/// # UPSTREAM-PARITY
1918///
1919/// ```c
1920/// xmlNodePtr xmlAddPrevSibling(xmlNodePtr cur, xmlNodePtr elem);
1921/// ```
1922///
1923/// Adds `elem` as the previous sibling of `cur`.
1924/// Returns `elem`, or NULL on failure.
1925///
1926/// # SAFETY
1927///
1928/// - `cur` must be a valid pointer to an _xmlNode.
1929/// - `elem` must be a valid pointer to an _xmlNode.
1930pub unsafe fn add_sibling_before(cur: *mut _xmlNode, elem: *mut _xmlNode) -> *mut _xmlNode {
1931 if cur.is_null() || elem.is_null() {
1932 return ptr::null_mut();
1933 }
1934
1935 let c = unsafe { &mut *cur };
1936
1937 // If elem is already linked, unlink it first
1938 let e = unsafe { &mut *elem };
1939 if !e.parent.is_null() || !e.prev.is_null() || !e.next.is_null() {
1940 unlink_node(elem);
1941 }
1942
1943 // Set parent
1944 e.parent = c.parent;
1945
1946 // Link elem before cur
1947 e.prev = c.prev;
1948 e.next = cur;
1949
1950 if !c.prev.is_null() {
1951 unsafe { (*c.prev).next = elem };
1952 }
1953 c.prev = elem;
1954
1955 // Update parent's first if needed
1956 let parent = c.parent;
1957 if !parent.is_null() && unsafe { (*parent).children } == cur {
1958 unsafe { (*parent).children = elem };
1959 }
1960
1961 // Update doc-level children if node is a direct doc child
1962 let doc = c.doc;
1963 if !doc.is_null() && parent.is_null() && unsafe { (*doc).children } == cur {
1964 unsafe { (*doc).children = elem };
1965 }
1966
1967 // Update doc
1968 if !c.doc.is_null() && e.doc != c.doc {
1969 propagate_doc(elem, c.doc);
1970 }
1971
1972 elem
1973}
1974
1975/// Create a new child element.
1976///
1977/// # UPSTREAM-PARITY
1978///
1979/// ```c
1980/// xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns, const xmlChar *name);
1981/// ```
1982///
1983/// Creates a new element and adds it as the last child of `parent`.
1984///
1985/// # SAFETY
1986///
1987/// - `parent` must be a valid pointer to an _xmlNode, or NULL.
1988/// - `name` must be a valid null-terminated string or NULL.
1989pub unsafe fn new_child(
1990 parent: *mut _xmlNode,
1991 ns: *mut _xmlNs,
1992 name: *const xmlChar,
1993) -> *mut _xmlNode {
1994 let node = new_node(ns, name);
1995 if node.is_null() {
1996 return ptr::null_mut();
1997 }
1998
1999 if !parent.is_null() {
2000 add_child(parent, node);
2001 }
2002
2003 node
2004}
2005
2006// ═══════════════════════════════════════════════════════════════════════════════
2007// Text / Content Nodes
2008// ═══════════════════════════════════════════════════════════════════════════════
2009
2010/// Create a new text node.
2011///
2012/// # UPSTREAM-PARITY
2013///
2014/// ```c
2015/// xmlNodePtr xmlNewText(const xmlChar *content);
2016/// ```
2017///
2018/// Creates a text node with the given content.
2019/// If content is NULL, creates an empty text node.
2020///
2021/// # SAFETY
2022///
2023/// - `content` must be a valid null-terminated string or NULL.
2024pub unsafe fn new_text(content: *const xmlChar) -> *mut _xmlNode {
2025 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
2026 if node.is_null() {
2027 return ptr::null_mut();
2028 }
2029
2030 unsafe {
2031 (*node).type_ = XML_TEXT_NODE as c_int;
2032 (*node).name = dup_xml_str(b"text\0" as *const u8 as *const xmlChar);
2033 (*node).content = if content.is_null() {
2034 let empty = allocator::xmlMallocImpl(1) as *mut xmlChar;
2035 if !empty.is_null() {
2036 *empty = 0;
2037 }
2038 empty
2039 } else {
2040 dup_xml_str(content)
2041 };
2042 (*node).line = 0;
2043 }
2044
2045 // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
2046 // node is fully initialised.
2047 crate::abi::data_globals::register_node_hook(node);
2048
2049 node
2050}
2051
2052/// Create a new comment node.
2053///
2054/// # UPSTREAM-PARITY
2055///
2056/// ```c
2057/// xmlNodePtr xmlNewComment(const xmlChar *content);
2058/// ```
2059///
2060/// Creates a comment node with the given content.
2061///
2062/// # SAFETY
2063///
2064/// - `content` must be a valid null-terminated string or NULL.
2065pub unsafe fn new_comment(content: *const xmlChar) -> *mut _xmlNode {
2066 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
2067 if node.is_null() {
2068 return ptr::null_mut();
2069 }
2070
2071 unsafe {
2072 (*node).type_ = XML_COMMENT_NODE as c_int;
2073 (*node).name = dup_xml_str(b"comment\0" as *const u8 as *const xmlChar);
2074 (*node).content = dup_xml_str(content);
2075 (*node).line = 0;
2076 }
2077
2078 // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
2079 // node is fully initialised.
2080 crate::abi::data_globals::register_node_hook(node);
2081
2082 node
2083}
2084
2085/// Create a new processing instruction node.
2086///
2087/// # UPSTREAM-PARITY
2088///
2089/// ```c
2090/// xmlNodePtr xmlNewPI(const xmlChar *name, const xmlChar *content);
2091/// ```
2092///
2093/// Creates a PI node with the given target name and content.
2094///
2095/// # SAFETY
2096///
2097/// - `name` must be a valid null-terminated string.
2098/// - `content` must be a valid null-terminated string or NULL.
2099pub unsafe fn new_pi(name: *const xmlChar, content: *const xmlChar) -> *mut _xmlNode {
2100 // UPSTREAM-PARITY (tree.c xmlNewPI): a NULL target name is rejected up
2101 // front — HOSTILE-ABI A46.
2102 if name.is_null() {
2103 return ptr::null_mut();
2104 }
2105 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
2106 if node.is_null() {
2107 return ptr::null_mut();
2108 }
2109
2110 unsafe {
2111 (*node).type_ = XML_PI_NODE as c_int;
2112 (*node).name = dup_xml_str(name);
2113 (*node).content = dup_xml_str(content);
2114 (*node).line = 0;
2115 }
2116
2117 // UPSTREAM-PARITY (tree.c): the node-registration hook fires after a
2118 // node is fully initialised.
2119 crate::abi::data_globals::register_node_hook(node);
2120
2121 node
2122}
2123
2124/// Create a new CDATA section node.
2125///
2126/// # UPSTREAM-PARITY
2127///
2128/// ```c
2129/// xmlNodePtr xmlNewCDataBlock(xmlDocPtr doc, const xmlChar *content, int len);
2130/// ```
2131///
2132/// Creates a CDATA section node with the given content.
2133///
2134/// # SAFETY
2135///
2136/// - `doc` may be NULL.
2137/// - `content` must be a valid pointer to a buffer of at least `len` bytes,
2138/// or NULL.
2139pub unsafe fn new_cdata_block(
2140 doc: *mut _xmlDoc,
2141 content: *const xmlChar,
2142 len: c_int,
2143) -> *mut _xmlNode {
2144 let node = allocator::xmlMallocZero(size_of::<_xmlNode>() as usize) as *mut _xmlNode;
2145 if node.is_null() {
2146 return ptr::null_mut();
2147 }
2148
2149 unsafe {
2150 (*node).type_ = XML_CDATA_SECTION_NODE as c_int;
2151 // UPSTREAM-PARITY (tree.c xmlNewCDataBlock): the name field is left
2152 // NULL (zero-initialised).
2153 (*node).doc = doc;
2154
2155 if !content.is_null() && len > 0 {
2156 (*node).content = allocator::xmlMallocImpl((len + 1) as usize) as *mut xmlChar;
2157 if !(*node).content.is_null() {
2158 ptr::copy_nonoverlapping(content, (*node).content, len as usize);
2159 *((*node).content.add(len as usize)) = 0;
2160 }
2161 } else {
2162 let empty = allocator::xmlMallocImpl(1) as *mut xmlChar;
2163 if !empty.is_null() {
2164 *empty = 0;
2165 }
2166 (*node).content = empty;
2167 }
2168
2169 (*node).line = 0;
2170 }
2171
2172 node
2173}
2174
2175// ═══════════════════════════════════════════════════════════════════════════════
2176// Namespace Operations
2177// ═══════════════════════════════════════════════════════════════════════════════
2178
2179/// Create a new namespace declaration.
2180///
2181/// # UPSTREAM-PARITY
2182///
2183/// ```c
2184/// xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
2185/// ```
2186///
2187/// Creates a new namespace declaration on the given node.
2188/// The namespace is added to the node's nsDef list.
2189///
2190/// If `href` is NULL, the namespace is a default namespace undeclaration.
2191/// If `prefix` is NULL, this is the default namespace (xmlns="...").
2192///
2193/// # SAFETY
2194///
2195/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2196/// - `href` must be a valid null-terminated string or NULL.
2197/// - `prefix` must be a valid null-terminated string or NULL.
2198pub unsafe fn new_ns(
2199 node: *mut _xmlNode,
2200 href: *const xmlChar,
2201 prefix: *const xmlChar,
2202) -> *mut _xmlNs {
2203 let ns = allocator::xmlMallocZero(size_of::<_xmlNs>() as usize) as *mut _xmlNs;
2204 if ns.is_null() {
2205 return ptr::null_mut();
2206 }
2207
2208 unsafe {
2209 (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
2210 (*ns).href = dup_xml_str(href);
2211 (*ns).prefix = dup_xml_str(prefix);
2212 (*ns).context = node as *mut _xmlDoc;
2213
2214 // UPSTREAM-PARITY (tree.c xmlNewNs): a node may not declare the
2215 // same prefix twice — when an existing declaration on the node's
2216 // OWN nsDef chain shares the prefix and has a non-NULL href the new
2217 // declaration is rejected (freed, NULL returned). PHP's
2218 // setAttributeNS/createAttributeNS conflict resolution relies on
2219 // this NULL to allocate a FRESH prefix instead (dom_get_ns_unchecked
2220 // -> dom_get_ns_resolve_prefix_conflict: xmlns:default,
2221 // xmlns:default1, ...) — createAttributeNS_prefix_conflicts /
2222 // Element_setAttributeNS.
2223 if !node.is_null() {
2224 let n = &mut *node;
2225 let same_prefix = |a: *mut _xmlNs| {
2226 let other = &*a;
2227 // xmlStrEqual semantics: NULL == NULL (two default
2228 // declarations conflict), otherwise byte equality.
2229 match ((*ns).prefix, other.prefix) {
2230 (x, y) if x == y => true,
2231 (x, y) if x.is_null() || y.is_null() => false,
2232 (x, y) => crate::abi::exports_xml2::xmlStrEqual(x, y) != 0,
2233 }
2234 };
2235 let conflict = |a: *mut _xmlNs| -> bool {
2236 let other = &*a;
2237 same_prefix(a) && !other.href.is_null()
2238 };
2239 if n.nsDef.is_null() {
2240 n.nsDef = ns;
2241 } else {
2242 // Mirror upstream's first-element check then the walk.
2243 let mut prev = n.nsDef;
2244 if conflict(prev) {
2245 free_ns(ns);
2246 return ptr::null_mut();
2247 }
2248 while !(*prev).next.is_null() {
2249 prev = (*prev).next;
2250 if conflict(prev) {
2251 free_ns(ns);
2252 return ptr::null_mut();
2253 }
2254 }
2255 (*prev).next = ns;
2256 }
2257 }
2258 }
2259
2260 ns
2261}
2262
2263/// Set the namespace of a node.
2264///
2265/// # UPSTREAM-PARITY
2266///
2267/// ```c
2268/// void xmlSetNs(xmlNodePtr node, xmlNsPtr ns);
2269/// ```
2270///
2271/// # SAFETY
2272///
2273/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2274/// - `ns` must be a valid pointer to an _xmlNs, or NULL.
2275pub unsafe fn set_ns(node: *mut _xmlNode, ns: *mut _xmlNs) {
2276 if node.is_null() {
2277 return;
2278 }
2279 unsafe {
2280 (*node).ns = ns;
2281 }
2282}
2283
2284/// Get a list of namespaces in scope for a node.
2285///
2286/// # UPSTREAM-PARITY
2287///
2288/// ```c
2289/// xmlNsPtr *xmlGetNsList(xmlDocPtr doc, xmlNodePtr node);
2290/// ```
2291///
2292/// Returns a NULL-terminated array of namespace pointers in scope,
2293/// or NULL on failure.
2294///
2295/// # SAFETY
2296///
2297/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2298/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2299pub unsafe fn get_ns_list(_doc: *mut _xmlDoc, node: *mut _xmlNode) -> *mut *mut _xmlNs {
2300 // Phase 1: basic implementation
2301 // A more complete implementation would walk the node's ancestors
2302 // and collect all in-scope namespaces.
2303 if node.is_null() {
2304 return ptr::null_mut();
2305 }
2306
2307 // Collect namespaces from this node and ancestors
2308 let mut ns_ptrs: Vec<*mut _xmlNs> = Vec::new();
2309 let mut cur = node;
2310
2311 while !cur.is_null() {
2312 let n = unsafe { &*cur };
2313 let mut ns_def = n.nsDef;
2314 while !ns_def.is_null() {
2315 // Avoid duplicates
2316 let ns = unsafe { &*ns_def };
2317 let mut found = false;
2318 for &existing in &ns_ptrs {
2319 if existing == ns_def {
2320 found = true;
2321 break;
2322 }
2323 let e = unsafe { &*existing };
2324 if !ns.href.is_null() && !e.href.is_null() {
2325 let href_match =
2326 unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, e.href) != 0 };
2327 if href_match {
2328 if ns.prefix.is_null() && e.prefix.is_null() {
2329 found = true;
2330 break;
2331 }
2332 if !ns.prefix.is_null() && !e.prefix.is_null() {
2333 let prefix_match = unsafe {
2334 crate::abi::exports_xml2::xmlStrEqual(ns.prefix, e.prefix) != 0
2335 };
2336 if prefix_match {
2337 found = true;
2338 break;
2339 }
2340 }
2341 }
2342 }
2343 }
2344 if !found {
2345 ns_ptrs.push(ns_def);
2346 }
2347 ns_def = unsafe { (*ns_def).next };
2348 }
2349 cur = n.parent;
2350 }
2351
2352 if ns_ptrs.is_empty() {
2353 return ptr::null_mut();
2354 }
2355
2356 // Allocate NULL-terminated array
2357 let arr = allocator::xmlMallocImpl((ns_ptrs.len() + 1) * size_of::<*mut _xmlNs>())
2358 as *mut *mut _xmlNs;
2359 if arr.is_null() {
2360 return ptr::null_mut();
2361 }
2362
2363 for (i, ns) in ns_ptrs.iter().enumerate() {
2364 unsafe { *arr.add(i) = *ns };
2365 }
2366 unsafe { *arr.add(ns_ptrs.len()) = ptr::null_mut() };
2367
2368 arr
2369}
2370
2371/// Search for a namespace by prefix.
2372///
2373/// # UPSTREAM-PARITY
2374///
2375/// ```c
2376/// xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace);
2377/// ```
2378///
2379/// Searches for a namespace declaration with the given prefix.
2380/// If `nameSpace` is NULL, searches for the default namespace.
2381///
2382/// # SAFETY
2383///
2384/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2385/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2386/// - `nameSpace` must be a valid null-terminated string or NULL.
2387/// `IS_STR_XML` from upstream tree.c: the string is exactly "xml".
2388fn is_str_xml(s: *const xmlChar) -> bool {
2389 if s.is_null() {
2390 return false;
2391 }
2392 let b = unsafe { core::slice::from_raw_parts(s, 3) };
2393 b[0] == b'x' && b[1] == b'm' && b[2] == b'l'
2394}
2395
2396/// UPSTREAM-PARITY (tree.c xmlNsInScope): walk from `node` up to (excluding)
2397/// `ancestor`, checking that no closer declaration binds the same `prefix`
2398/// (NULL prefix = the default namespace). Returns 1 when `ancestor`'s decl is
2399/// still in scope, 0 when it is shadowed, -1 when the walk cannot reach
2400/// `ancestor` or crosses an entity boundary.
2401unsafe fn ns_in_scope(
2402 node: *mut _xmlNode,
2403 ancestor: *mut _xmlNode,
2404 prefix: *const xmlChar,
2405) -> c_int {
2406 let mut cur = node;
2407 while !cur.is_null() && cur != ancestor {
2408 let t = unsafe { (*cur).type_ };
2409 if t == XML_ENTITY_REF_NODE as c_int || t == XML_ENTITY_DECL as c_int {
2410 return -1;
2411 }
2412 if t == XML_ELEMENT_NODE as c_int {
2413 let mut tst = unsafe { (*cur).nsDef };
2414 while !tst.is_null() {
2415 let ns = unsafe { &*tst };
2416 if ns.prefix.is_null() && prefix.is_null() {
2417 return 0;
2418 }
2419 if !ns.prefix.is_null()
2420 && !prefix.is_null()
2421 && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, prefix) != 0 }
2422 {
2423 return 0;
2424 }
2425 tst = unsafe { (*tst).next };
2426 }
2427 }
2428 cur = unsafe { (*cur).parent };
2429 }
2430 if cur != ancestor {
2431 return -1;
2432 }
2433 1
2434}
2435
2436/// Ensure `doc->oldNs` holds the implicit XML namespace declaration (upstream
2437/// xmlTreeEnsureXMLDecl). The engine models `doc->oldNs` as a list whose HEAD is
2438/// always the xml declaration; parked/retired declarations live after it.
2439unsafe fn ensure_doc_xml_ns(doc: *mut _xmlDoc) -> *mut _xmlNs {
2440 if doc.is_null() {
2441 return ptr::null_mut();
2442 }
2443 if !(*doc).oldNs.is_null() {
2444 return (*doc).oldNs;
2445 }
2446 let ns = allocator::xmlMallocZero(size_of::<_xmlNs>()) as *mut _xmlNs;
2447 if ns.is_null() {
2448 return ptr::null_mut();
2449 }
2450 unsafe {
2451 (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
2452 (*ns).href = crate::xml::string::xml_strdup(
2453 c"http://www.w3.org/XML/1998/namespace".as_ptr() as *const xmlChar,
2454 );
2455 (*ns).prefix = crate::xml::string::xml_strdup(c"xml".as_ptr() as *const xmlChar);
2456 (*doc).oldNs = ns;
2457 }
2458 ns
2459}
2460
2461/// UPSTREAM-PARITY (tree.c xmlSearchNsSafe): search for a namespace bound to
2462/// the given PREFIX in scope of `node`. A NULL `name_space` searches the
2463/// default namespace. The walk only ever reads ELEMENT nodes' `nsDef` chains
2464/// (the document is NOT an element: its `oldNs` list must never be mistaken for
2465/// declarations), and a declaration with a NULL href does not bind its prefix.
2466pub unsafe fn search_ns(
2467 doc: *mut _xmlDoc,
2468 node: *mut _xmlNode,
2469 name_space: *const xmlChar,
2470) -> *mut _xmlNs {
2471 if node.is_null() || unsafe { (*node).type_ } == XML_NAMESPACE_DECL as c_int {
2472 return ptr::null_mut();
2473 }
2474 let orig = node;
2475
2476 // The XML-1.0 namespace is implicitly bound to the prefix "xml" on every
2477 // document (xmlTreeEnsureXMLDecl keeps it on doc->oldNs).
2478 if !doc.is_null() && is_str_xml(name_space) {
2479 return unsafe { ensure_doc_xml_ns(doc) };
2480 }
2481
2482 // Climb from a non-element node to its owning element, if any.
2483 let mut cur = node;
2484 while unsafe { (*cur).type_ } != XML_ELEMENT_NODE as c_int {
2485 cur = unsafe { (*cur).parent };
2486 if cur.is_null() {
2487 return ptr::null_mut();
2488 }
2489 }
2490 let parent = cur;
2491
2492 // UPSTREAM-PARITY: `while ((node != NULL) && (node->type ==
2493 // XML_ELEMENT_NODE))` — a detached element (parent == NULL) terminates the
2494 // walk instead of dereferencing NULL.
2495 while !cur.is_null() && unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2496 let mut ns_def = unsafe { (*cur).nsDef };
2497 while !ns_def.is_null() {
2498 let ns = unsafe { &*ns_def };
2499 if unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, name_space) != 0 }
2500 && !ns.href.is_null()
2501 {
2502 return ns_def;
2503 }
2504 ns_def = unsafe { (*ns_def).next };
2505 }
2506 if orig != cur {
2507 let el_ns = unsafe { (*cur).ns };
2508 if !el_ns.is_null()
2509 && unsafe {
2510 crate::abi::exports_xml2::xmlStrEqual((*el_ns).prefix, name_space) != 0
2511 }
2512 && !(*el_ns).href.is_null()
2513 {
2514 return el_ns;
2515 }
2516 }
2517 cur = unsafe { (*cur).parent };
2518 }
2519
2520 // No document but the node belongs to a doc-less tree: exceptionally create
2521 // the xml declaration on the nearest element (upstream tree.c).
2522 if doc.is_null() && is_str_xml(name_space) {
2523 let ns = allocator::xmlMallocZero(size_of::<_xmlNs>()) as *mut _xmlNs;
2524 if !ns.is_null() {
2525 unsafe {
2526 (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
2527 (*ns).href = crate::xml::string::xml_strdup(
2528 c"http://www.w3.org/XML/1998/namespace".as_ptr() as *const xmlChar,
2529 );
2530 (*ns).prefix = crate::xml::string::xml_strdup(c"xml".as_ptr() as *const xmlChar);
2531 (*ns).next = (*parent).nsDef;
2532 (*parent).nsDef = ns;
2533 }
2534 return ns;
2535 }
2536 }
2537
2538 ptr::null_mut()
2539}
2540
2541/// Search for a namespace by href (URI).
2542///
2543/// # UPSTREAM-PARITY
2544///
2545/// ```c
2546/// xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar *href);
2547/// ```
2548///
2549/// Searches for a namespace declaration with the given URI.
2550///
2551/// # SAFETY
2552///
2553/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2554/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2555/// - `href` must be a valid null-terminated string or NULL.
2556/// UPSTREAM-PARITY (tree.c xmlSearchNsByHrefSafe): search for a namespace
2557/// declaration with the given URI in scope of `node`. Attributes are never in
2558/// the default namespace, so a prefix-less declaration cannot satisfy an
2559/// attribute search (`is_attr`). A declaration only counts when it is actually
2560/// in scope (not shadowed between `node` and the declaring element, and never
2561/// across entity boundaries). The walk only reads ELEMENT nodes.
2562pub unsafe fn search_ns_by_href(
2563 doc: *mut _xmlDoc,
2564 node: *mut _xmlNode,
2565 href: *const xmlChar,
2566) -> *mut _xmlNs {
2567 if node.is_null() || href.is_null() || unsafe { (*node).type_ } == XML_NAMESPACE_DECL as c_int {
2568 return ptr::null_mut();
2569 }
2570
2571 let orig = node;
2572
2573 // The XML-1.0 namespace is implicitly in scope everywhere via the prefix
2574 // "xml" (xmlTreeEnsureXMLDecl keeps it on doc->oldNs).
2575 let is_xml_ns_uri = unsafe {
2576 crate::abi::exports_xml2::xmlStrEqual(
2577 href,
2578 c"http://www.w3.org/XML/1998/namespace".as_ptr() as *const xmlChar,
2579 ) != 0
2580 };
2581 if is_xml_ns_uri && !doc.is_null() {
2582 return unsafe { ensure_doc_xml_ns(doc) };
2583 }
2584
2585 let is_attr = unsafe { (*node).type_ } == XML_ATTRIBUTE_NODE as c_int;
2586
2587 // Climb from a non-element node to its owning element, if any.
2588 let mut cur = node;
2589 while unsafe { (*cur).type_ } != XML_ELEMENT_NODE as c_int {
2590 cur = unsafe { (*cur).parent };
2591 if cur.is_null() {
2592 return ptr::null_mut();
2593 }
2594 }
2595 let parent = cur;
2596
2597 // UPSTREAM-PARITY: `while ((node != NULL) && (node->type ==
2598 // XML_ELEMENT_NODE))` — a detached element (parent == NULL) terminates the
2599 // walk instead of dereferencing NULL.
2600 while !cur.is_null() && unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
2601 let mut ns_def = unsafe { (*cur).nsDef };
2602 while !ns_def.is_null() {
2603 let ns = unsafe { &*ns_def };
2604 if !ns.href.is_null()
2605 && unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.href, href) != 0 }
2606 {
2607 let ns_prefix = ns.prefix;
2608 if ((!is_attr) || !ns_prefix.is_null())
2609 && unsafe { ns_in_scope(orig, cur, ns_prefix) } == 1
2610 {
2611 return ns_def;
2612 }
2613 }
2614 ns_def = unsafe { (*ns_def).next };
2615 }
2616 if orig != cur {
2617 let el_ns = unsafe { (*cur).ns };
2618 if !el_ns.is_null() {
2619 let eh = (*el_ns).href;
2620 if !eh.is_null() && unsafe { crate::abi::exports_xml2::xmlStrEqual(eh, href) != 0 }
2621 {
2622 let el_prefix = (*el_ns).prefix;
2623 if ((!is_attr) || !el_prefix.is_null())
2624 && unsafe { ns_in_scope(orig, cur, el_prefix) } == 1
2625 {
2626 return el_ns;
2627 }
2628 }
2629 }
2630 }
2631 cur = unsafe { (*cur).parent };
2632 }
2633
2634 // No document but the node belongs to a doc-less tree: exceptionally create
2635 // the xml declaration on the nearest element (upstream tree.c).
2636 if doc.is_null() && is_xml_ns_uri {
2637 let ns = allocator::xmlMallocZero(size_of::<_xmlNs>()) as *mut _xmlNs;
2638 if !ns.is_null() {
2639 unsafe {
2640 (*ns).type_ = XML_LOCAL_NAMESPACE as c_int;
2641 (*ns).href = crate::xml::string::xml_strdup(
2642 c"http://www.w3.org/XML/1998/namespace".as_ptr() as *const xmlChar,
2643 );
2644 (*ns).prefix = crate::xml::string::xml_strdup(c"xml".as_ptr() as *const xmlChar);
2645 (*ns).next = (*parent).nsDef;
2646 (*parent).nsDef = ns;
2647 }
2648 return ns;
2649 }
2650 }
2651
2652 ptr::null_mut()
2653}
2654
2655// ═══════════════════════════════════════════════════════════════════════════════
2656// Attribute Operations
2657// ═══════════════════════════════════════════════════════════════════════════════
2658
2659/// UPSTREAM-PARITY (tree.c xmlGetPropNodeInternal DTD arm): when an element has
2660/// no matching real attribute, an ATTLIST default/#FIXED declaration whose
2661/// `defaultValue` is non-NULL is reported as the attribute (returned as an
2662/// `XML_ATTRIBUTE_DECL` node). `ns_name` is the namespace URI: NULL selects
2663/// prefix-less declarations, the XML namespace selects the reserved "xml"
2664/// prefix, anything else is matched through the in-scope prefixes bound to that
2665/// URI. Returns the declaration cast to `*mut _xmlAttr`, or NULL.
2666unsafe fn dtd_default_decl_lookup(
2667 node: *mut _xmlNode,
2668 name: *const xmlChar,
2669 ns_name: *const xmlChar,
2670) -> *mut _xmlAttr {
2671 let n = unsafe { &*node };
2672 if n.doc.is_null() {
2673 return ptr::null_mut();
2674 }
2675 let doc = unsafe { &*(n.doc) };
2676 if doc.intSubset.is_null() && doc.extSubset.is_null() {
2677 return ptr::null_mut();
2678 }
2679
2680 // We need the QName of the element for the DTD lookup.
2681 let ns = n.ns;
2682 let mut tmp: *mut xmlChar = ptr::null_mut();
2683 let elem_qname: *const xmlChar = if !ns.is_null() && !unsafe { (*ns).prefix.is_null() } {
2684 tmp = unsafe { crate::abi::exports_xml2::xmlStrdup((*ns).prefix) };
2685 if !tmp.is_null() {
2686 tmp = unsafe {
2687 crate::abi::exports_xml2::xmlStrcat(tmp, b":\0" as *const u8 as *const xmlChar)
2688 };
2689 }
2690 if !tmp.is_null() {
2691 tmp = unsafe { crate::abi::exports_xml2::xmlStrcat(tmp, n.name) };
2692 }
2693 if tmp.is_null() {
2694 return ptr::null_mut();
2695 }
2696 tmp
2697 } else {
2698 n.name
2699 };
2700
2701 let mut attr_decl: *mut crate::abi::structs::_xmlAttribute = ptr::null_mut();
2702 let xml_ns_uri = b"http://www.w3.org/XML/1998/namespace\0";
2703 if ns_name.is_null() {
2704 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2705 doc.intSubset,
2706 elem_qname,
2707 name,
2708 ptr::null(),
2709 );
2710 if attr_decl.is_null() && !doc.extSubset.is_null() {
2711 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2712 doc.extSubset,
2713 elem_qname,
2714 name,
2715 ptr::null(),
2716 );
2717 }
2718 } else if unsafe {
2719 crate::abi::exports_xml2::xmlStrEqual(ns_name, xml_ns_uri.as_ptr() as *const xmlChar) != 0
2720 } {
2721 // The XML namespace must be bound to prefix 'xml'.
2722 let xml_prefix = b"xml\0";
2723 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2724 doc.intSubset,
2725 elem_qname,
2726 name,
2727 xml_prefix.as_ptr() as *const xmlChar,
2728 );
2729 if attr_decl.is_null() && !doc.extSubset.is_null() {
2730 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2731 doc.extSubset,
2732 elem_qname,
2733 name,
2734 xml_prefix.as_ptr() as *const xmlChar,
2735 );
2736 }
2737 } else {
2738 // The ugly case: search using the prefixes of in-scope ns-decls
2739 // corresponding to ns_name.
2740 let ns_list = unsafe { get_ns_list(n.doc, node) };
2741 if ns_list.is_null() {
2742 if !tmp.is_null() {
2743 allocator::xmlFreeImpl(tmp as *mut c_void);
2744 }
2745 return ptr::null_mut();
2746 }
2747 let mut cur = ns_list;
2748 while !unsafe { *cur }.is_null() {
2749 let d = unsafe { *cur };
2750 if !unsafe { (*d).href }.is_null()
2751 && unsafe { crate::abi::exports_xml2::xmlStrEqual((*d).href, ns_name) != 0 }
2752 {
2753 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2754 doc.intSubset,
2755 elem_qname,
2756 name,
2757 (*d).prefix,
2758 );
2759 if attr_decl.is_null() && !doc.extSubset.is_null() {
2760 attr_decl = crate::xml::validation::get_dtd_qattr_desc(
2761 doc.extSubset,
2762 elem_qname,
2763 name,
2764 (*d).prefix,
2765 );
2766 }
2767 if !attr_decl.is_null() {
2768 break;
2769 }
2770 }
2771 cur = cur.add(1);
2772 }
2773 allocator::xmlFreeImpl(ns_list as *mut c_void);
2774 }
2775 if !tmp.is_null() {
2776 allocator::xmlFreeImpl(tmp as *mut c_void);
2777 }
2778
2779 if !attr_decl.is_null() && !unsafe { (*attr_decl).defaultValue.is_null() } {
2780 return attr_decl as *mut _xmlAttr;
2781 }
2782 ptr::null_mut()
2783}
2784
2785/// UPSTREAM-PARITY (tree.c xmlHasProp DTD arm): plain-element-name variant of
2786/// the default/#FIXED lookup (no QName prefix expansion, matching upstream
2787/// xmlHasProp's xmlGetDtdAttrDesc call).
2788unsafe fn dtd_default_decl_lookup_plain(
2789 node: *mut _xmlNode,
2790 name: *const xmlChar,
2791) -> *mut _xmlAttr {
2792 let n = unsafe { &*node };
2793 if n.doc.is_null() {
2794 return ptr::null_mut();
2795 }
2796 let doc = unsafe { &*(n.doc) };
2797 let mut attr_decl: *mut crate::abi::structs::_xmlAttribute = ptr::null_mut();
2798 if !doc.intSubset.is_null() {
2799 attr_decl = crate::xml::validation::get_dtd_attr_desc(doc.intSubset, n.name, name);
2800 if attr_decl.is_null() && !doc.extSubset.is_null() {
2801 attr_decl = crate::xml::validation::get_dtd_attr_desc(doc.extSubset, n.name, name);
2802 }
2803 if !attr_decl.is_null() && !unsafe { (*attr_decl).defaultValue.is_null() } {
2804 return attr_decl as *mut _xmlAttr;
2805 }
2806 }
2807 ptr::null_mut()
2808}
2809
2810/// Set an attribute on a node.
2811///
2812/// # UPSTREAM-PARITY
2813///
2814/// ```c
2815/// xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name, const xmlChar *value);
2816/// ```
2817///
2818/// Sets the attribute with the given name to the given value.
2819/// If the attribute already exists, its value is updated.
2820/// Creates the attribute if it doesn't exist.
2821///
2822/// Returns the attribute pointer, or NULL on failure.
2823///
2824/// # SAFETY
2825///
2826/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2827/// - `name` must be a valid null-terminated string.
2828/// - `value` must be a valid null-terminated string or NULL.
2829/// UPSTREAM-PARITY (tree.c xmlSetProp): set an attribute given its QName.
2830/// A prefixed name resolves the prefix through the in-scope namespace
2831/// declarations and delegates to `xmlSetNsProp` with the LOCAL name; when the
2832/// prefix is unbound (or the name is unprefixed) the attribute is set in no
2833/// namespace under its (raw) name — an unprefixed attribute is never in a
2834/// namespace, and an unbound prefix keeps the raw QName (matching the SAX2
2835/// tree-builder convention for undefined prefixes).
2836pub unsafe fn set_prop(
2837 node: *mut _xmlNode,
2838 name: *const xmlChar,
2839 value: *const xmlChar,
2840) -> *mut _xmlAttr {
2841 if node.is_null() || name.is_null() || unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
2842 return ptr::null_mut();
2843 }
2844
2845 let mut prefix: *mut xmlChar = ptr::null_mut();
2846 let localname = crate::xml::validation::split_qname4(name, &mut prefix);
2847 if localname.is_null() {
2848 if !prefix.is_null() {
2849 allocator::xmlFreeImpl(prefix as *mut c_void);
2850 }
2851 return ptr::null_mut();
2852 }
2853 if !prefix.is_null() {
2854 let ns = unsafe { search_ns((*node).doc, node, prefix) };
2855 if !ns.is_null() {
2856 allocator::xmlFreeImpl(prefix as *mut c_void);
2857 return unsafe { set_ns_prop(node, ns, localname, value) };
2858 }
2859 allocator::xmlFreeImpl(prefix as *mut c_void);
2860 return unsafe { set_ns_prop(node, ptr::null_mut(), name, value) };
2861 }
2862
2863 unsafe { set_ns_prop(node, ptr::null_mut(), name, value) }
2864}
2865
2866/// Get an attribute value by name.
2867///
2868/// # UPSTREAM-PARITY
2869///
2870/// ```c
2871/// xmlChar *xmlGetProp(xmlNodePtr node, const xmlChar *name);
2872/// ```
2873///
2874/// Returns the attribute value as an xmlChar* (caller must free with xmlFree),
2875/// or NULL if the attribute doesn't exist.
2876///
2877/// # SAFETY
2878///
2879/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2880/// - `name` must be a valid null-terminated string.
2881pub unsafe fn get_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut xmlChar {
2882 if node.is_null() || name.is_null() || unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
2883 return ptr::null_mut();
2884 }
2885
2886 let n = unsafe { &*node };
2887 let mut cur = n.properties;
2888
2889 while !cur.is_null() {
2890 let attr = unsafe { &*cur };
2891 if !attr.name.is_null()
2892 && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
2893 {
2894 // Get the text content of the attribute
2895 if !attr.children.is_null() {
2896 let text = unsafe { &*attr.children };
2897 if text.type_ == XML_TEXT_NODE as c_int && !text.content.is_null() {
2898 return dup_xml_str(text.content);
2899 }
2900 }
2901 return dup_xml_str(b"\0" as *const u8 as *const xmlChar);
2902 }
2903 cur = unsafe { (*cur).next };
2904 }
2905
2906 // UPSTREAM-PARITY (tree.c xmlGetProp/xmlHasProp): when the element has no
2907 // matching real attribute, an ATTLIST default/#FIXED declaration value is
2908 // returned.
2909 let decl = unsafe { dtd_default_decl_lookup_plain(node, name) };
2910 if !decl.is_null() {
2911 let a = decl as *mut crate::abi::structs::_xmlAttribute;
2912 let dv = unsafe { (*a).defaultValue };
2913 if !dv.is_null() {
2914 return dup_xml_str(dv);
2915 }
2916 }
2917
2918 ptr::null_mut()
2919}
2920
2921/// Get a namespaced attribute value.
2922///
2923/// # UPSTREAM-PARITY
2924///
2925/// ```c
2926/// xmlChar *xmlGetNsProp(xmlNodePtr node, const xmlChar *name, const xmlChar *nameSpace);
2927/// ```
2928///
2929/// Returns the attribute value, or NULL if not found.
2930///
2931/// # SAFETY
2932///
2933/// - `node` must be a valid pointer to an _xmlNode, or NULL.
2934/// - `name` must be a valid null-terminated string.
2935/// - `nameSpace` may be NULL.
2936pub unsafe fn get_ns_prop(
2937 node: *mut _xmlNode,
2938 name: *const xmlChar,
2939 name_space: *const xmlChar,
2940) -> *mut xmlChar {
2941 if node.is_null() || name.is_null() || unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
2942 return ptr::null_mut();
2943 }
2944 // UPSTREAM-PARITY (tree.c xmlGetNsProp/xmlGetPropNodeInternal): match the
2945 // LOCAL name plus the namespace — a NULL nameSpace matches only
2946 // UNPREFIXED attributes; a non-NULL nameSpace matches only attributes
2947 // whose ns href equals it (an unprefixed attribute is NEVER in a
2948 // namespace, not even the element's default one). Returns the value (""
2949 // when empty) or NULL.
2950 let mut cur = unsafe { (*node).properties };
2951 while !cur.is_null() {
2952 let attr = unsafe { &*cur };
2953 if !attr.name.is_null()
2954 && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
2955 {
2956 let matches = if name_space.is_null() {
2957 attr.ns.is_null()
2958 } else if !attr.ns.is_null() && !(*attr.ns).href.is_null() {
2959 unsafe { crate::abi::exports_xml2::xmlStrEqual((*attr.ns).href, name_space) != 0 }
2960 } else {
2961 false
2962 };
2963 if matches {
2964 if !attr.children.is_null() {
2965 let text = unsafe { &*attr.children };
2966 if text.type_ == XML_TEXT_NODE as c_int && !text.content.is_null() {
2967 return dup_xml_str(text.content);
2968 }
2969 }
2970 return dup_xml_str(b"\0" as *const u8 as *const xmlChar);
2971 }
2972 }
2973 cur = unsafe { (*cur).next };
2974 }
2975
2976 // UPSTREAM-PARITY (tree.c xmlGetPropNodeInternal useDTD arm): DTD
2977 // default/#FIXED declarations are reported as present attributes.
2978 let decl = unsafe { dtd_default_decl_lookup(node, name, name_space) };
2979 if !decl.is_null() {
2980 let a = decl as *mut crate::abi::structs::_xmlAttribute;
2981 let dv = unsafe { (*a).defaultValue };
2982 if !dv.is_null() {
2983 return dup_xml_str(dv);
2984 }
2985 }
2986 ptr::null_mut()
2987}
2988
2989/// Set a namespaced attribute.
2990///
2991/// # UPSTREAM-PARITY
2992///
2993/// ```c
2994/// xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns, const xmlChar *name, const xmlChar *value);
2995/// ```
2996///
2997/// # SAFETY
2998///
2999/// - `node` must be a valid pointer to an _xmlNode, or NULL.
3000/// - `ns` may be NULL.
3001/// - `name` must be a valid null-terminated string.
3002/// - `value` must be a valid null-terminated string or NULL.
3003pub unsafe fn set_ns_prop(
3004 node: *mut _xmlNode,
3005 ns: *mut _xmlNs,
3006 name: *const xmlChar,
3007 value: *const xmlChar,
3008) -> *mut _xmlAttr {
3009 if node.is_null() || name.is_null() {
3010 return ptr::null_mut();
3011 }
3012 let n = unsafe { &mut *node };
3013
3014 // Namespace-aware variant of xmlSetProp (upstream xmlSetNsProp / tree.c
3015 // xmlSetNsPropInternal): find an existing attribute that shares name AND
3016 // namespace (href); when found, update its value, otherwise create a new
3017 // namespaced attribute bound to `ns`. The legacy Phase-1 stub ignored `ns`
3018 // and created an UNNAMESPACED attribute, which broke the modern DOM ns
3019 // mapper that materialises xmlns declarations as real XMLNS-ns attributes
3020 // (php dom_mark_namespaces_as_attributes_too -> xmlSetNsProp).
3021
3022 let mut existing = n.properties;
3023 while !existing.is_null() {
3024 let attr = unsafe { &*existing };
3025 let same_ns = if !ns.is_null() {
3026 // Both declare the same href (prefix may legitimately come from
3027 // different mapper instances, so compare by href).
3028 if unsafe { (*existing).ns }.is_null() {
3029 false
3030 } else {
3031 let an = unsafe { &*(*existing).ns };
3032 let bn = unsafe { &*ns };
3033 (!an.href.is_null()
3034 && !bn.href.is_null()
3035 && unsafe {
3036 crate::abi::exports_xml2::xmlStrEqual(
3037 an.href as *const crate::abi::types::xmlChar,
3038 bn.href as *const crate::abi::types::xmlChar,
3039 ) != 0
3040 })
3041 || (an.href.is_null() && bn.href.is_null())
3042 }
3043 } else {
3044 unsafe { (*existing).ns }.is_null()
3045 };
3046 if !attr.name.is_null()
3047 && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
3048 && same_ns
3049 {
3050 // UPSTREAM-PARITY (tree.c xmlSetNsProp modify branch): the
3051 // attribute is rebound to the NEW namespace — the passed `ns`
3052 // may carry a different prefix for the same URI (the modern
3053 // DOM ns-mapper allocates a fresh prefix per qualified name,
3054 // so setAttributeNS("urn:a", "y:foo", ...) renames x:foo to
3055 // y:foo rather than keeping the stale prefix).
3056 let attr_mut = existing;
3057 unsafe { (*attr_mut).ns = ns };
3058 // UPSTREAM-PARITY (tree.c xmlSetNsProp modify branch): an
3059 // attribute whose current value is registered as an ID drops its
3060 // old entry and keeps its ID type, so the new value is
3061 // re-registered below (xml:id / HTML id value changes move the
3062 // doc->ids mapping — bug79701).
3063 let mut was_id = false;
3064 if !n.doc.is_null() && !attr.id.is_null() {
3065 crate::xml::validation::remove_id(n.doc, existing);
3066 was_id = true;
3067 let am = existing;
3068 unsafe {
3069 (*am).atype = crate::abi::types::xmlAttributeType::XML_ATTRIBUTE_ID as c_int;
3070 }
3071 }
3072 // Update existing attribute value (mirror set_prop: free old
3073 // text children and set the new text value).
3074 if !attr.children.is_null() {
3075 free_node_list(attr.children);
3076 let attr_mut = existing;
3077 unsafe {
3078 (*attr_mut).children = ptr::null_mut();
3079 (*attr_mut).last = ptr::null_mut();
3080 }
3081 }
3082 if !value.is_null() {
3083 let text = new_text(value);
3084 if !text.is_null() {
3085 let attr_mut = existing;
3086 unsafe {
3087 (*attr_mut).children = text;
3088 (*attr_mut).last = text;
3089 (*text).parent = existing as *mut _xmlNode;
3090 (*text).doc = n.doc;
3091 }
3092 }
3093 if was_id {
3094 crate::xml::validation::add_id_safe(existing, value);
3095 }
3096 }
3097 return existing;
3098 }
3099 existing = unsafe { (*existing).next };
3100 }
3101
3102 // Create a new namespaced attribute.
3103 let attr = allocator::xmlMallocZero(size_of::<_xmlAttr>() as usize) as *mut _xmlAttr;
3104 if attr.is_null() {
3105 return ptr::null_mut();
3106 }
3107 unsafe {
3108 (*attr).type_ = XML_ATTRIBUTE_NODE as c_int;
3109 (*attr).name = dup_xml_str(name);
3110 (*attr).ns = ns;
3111 (*attr).parent = node;
3112 (*attr).doc = n.doc;
3113 }
3114 if !value.is_null() {
3115 let text = new_text(value);
3116 if !text.is_null() {
3117 unsafe {
3118 (*attr).children = text;
3119 (*attr).last = text;
3120 (*text).parent = attr as *mut _xmlNode;
3121 (*text).doc = n.doc;
3122 }
3123 }
3124 // UPSTREAM-PARITY (tree.c xmlNewPropInternal): a newly created
3125 // attribute whose name/type makes it an ID (HTML id, xml:id, DTD ID
3126 // declarations) is registered against the document's ID table
3127 // immediately.
3128 if !n.doc.is_null() {
3129 let res = crate::xml::validation::is_id(n.doc, node, attr);
3130 if res > 0 {
3131 crate::xml::validation::add_id_safe(attr, value);
3132 }
3133 }
3134 }
3135 // Attach to the node's property list.
3136 unsafe {
3137 if n.properties.is_null() {
3138 n.properties = attr;
3139 } else {
3140 let mut last = n.properties;
3141 while !(*last).next.is_null() {
3142 last = (*last).next;
3143 }
3144 (*last).next = attr;
3145 (*attr).prev = last;
3146 }
3147 }
3148 attr
3149}
3150
3151/// Remove a property from a node.
3152///
3153/// # UPSTREAM-PARITY
3154///
3155/// ```c
3156/// int xmlRemoveProp(xmlAttrPtr attr);
3157/// ```
3158///
3159/// Removes the attribute from its parent node and frees it.
3160/// Returns 0 on success, -1 on failure.
3161///
3162/// # SAFETY
3163///
3164/// - `attr` must be a valid pointer to an _xmlAttr, or NULL.
3165pub unsafe fn remove_prop(attr: *mut _xmlAttr) -> c_int {
3166 if attr.is_null() {
3167 return -1;
3168 }
3169
3170 let a = unsafe { &mut *attr };
3171
3172 // UPSTREAM-PARITY (tree.c xmlRemoveProp): a NULL parent means the
3173 // attribute is not attached to any element — upstream returns -1
3174 // without freeing (the caller keeps ownership).
3175 if a.parent.is_null() {
3176 return -1;
3177 }
3178
3179 // Unlink from the parent's property list. Upstream scans the list for
3180 // `attr`; once found it rethreads prev/next and hands the attribute to
3181 // xmlFreeProp. When the attribute is not in the list, -1 is returned
3182 // and nothing is freed.
3183 let parent = a.parent;
3184 let p = unsafe { &mut *parent };
3185 if p.properties == attr {
3186 p.properties = a.next;
3187 if !a.next.is_null() {
3188 unsafe { (*a.next).prev = ptr::null_mut() };
3189 }
3190 free_prop(attr);
3191 return 0;
3192 }
3193 let mut tmp = p.properties;
3194 while !tmp.is_null() {
3195 let next = unsafe { (*tmp).next };
3196 if next == attr {
3197 unsafe { (*tmp).next = a.next };
3198 if !a.next.is_null() {
3199 unsafe { (*a.next).prev = tmp };
3200 }
3201 free_prop(attr);
3202 return 0;
3203 }
3204 tmp = next;
3205 }
3206 -1
3207}
3208
3209/// Check whether a node has a property with the given name (upstream tree.c
3210/// `xmlHasProp`): returns the attribute pointer or NULL.
3211///
3212/// # SAFETY
3213///
3214/// - `node` must be a valid node pointer or NULL.
3215/// - `name` must be a valid null-terminated string.
3216pub unsafe fn has_prop(node: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlAttr {
3217 if node.is_null() || name.is_null() || unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
3218 return ptr::null_mut();
3219 }
3220 let mut cur = unsafe { (*node).properties };
3221 while !cur.is_null() {
3222 let attr = unsafe { &*cur };
3223 // UPSTREAM-PARITY (tree.c xmlHasProp): the search matches the LOCAL
3224 // name only — namespaced attributes are found too (php's
3225 // setAttributeNode replacement lookup relies on this). The NULL-
3226 // namespace restriction belongs to xmlHasNsProp.
3227 if !attr.name.is_null()
3228 && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
3229 {
3230 return cur;
3231 }
3232 cur = unsafe { (*cur).next };
3233 }
3234
3235 // UPSTREAM-PARITY (tree.c xmlHasProp): report ATTLIST default/#FIXED
3236 // declarations as present (php removeAttribute/toggleAttribute rely on the
3237 // returned XML_ATTRIBUTE_DECL being non-removable).
3238 unsafe { dtd_default_decl_lookup_plain(node, name) }
3239}
3240
3241/// Check whether a node has a namespaced property (upstream tree.c
3242/// `xmlHasNsProp`): returns the attribute pointer or NULL. A NULL
3243/// `nameSpace` matches the no-namespace case.
3244///
3245/// # SAFETY
3246///
3247/// - `node` must be a valid node pointer or NULL.
3248/// - `name` must be a valid null-terminated string.
3249/// - `nameSpace` may be NULL.
3250pub unsafe fn has_ns_prop(
3251 node: *mut _xmlNode,
3252 name: *const xmlChar,
3253 name_space: *const xmlChar,
3254) -> *mut _xmlAttr {
3255 if node.is_null() || name.is_null() || unsafe { (*node).type_ } != XML_ELEMENT_NODE as c_int {
3256 return ptr::null_mut();
3257 }
3258 let mut cur = unsafe { (*node).properties };
3259 while !cur.is_null() {
3260 let attr = unsafe { &*cur };
3261 if !attr.name.is_null()
3262 && unsafe { crate::abi::exports_xml2::xmlStrEqual(attr.name, name) != 0 }
3263 {
3264 if name_space.is_null() {
3265 if attr.ns.is_null() {
3266 return cur;
3267 }
3268 } else if !attr.ns.is_null()
3269 && !(*attr.ns).href.is_null()
3270 && unsafe {
3271 crate::abi::exports_xml2::xmlStrEqual((*attr.ns).href, name_space) != 0
3272 }
3273 {
3274 return cur;
3275 }
3276 }
3277 cur = unsafe { (*cur).next };
3278 }
3279
3280 // UPSTREAM-PARITY (tree.c xmlGetPropNodeInternal useDTD arm): report
3281 // ATTLIST default/#FIXED declarations as present (php's
3282 // setAttributeNode replacement lookup treats the returned
3283 // XML_ATTRIBUTE_DECL as "no existing attribute").
3284 unsafe { dtd_default_decl_lookup(node, name, name_space) }
3285}
3286
3287/// Remove a property by name from a node (upstream tree.c `xmlUnsetProp`):
3288/// returns 0 on success, -1 if the property does not exist or arguments are
3289/// NULL.
3290///
3291/// # SAFETY
3292///
3293/// - `node` must be a valid node pointer or NULL.
3294/// - `name` must be a valid null-terminated string.
3295pub unsafe fn unset_prop(node: *mut _xmlNode, name: *const xmlChar) -> c_int {
3296 let attr = unsafe { has_prop(node, name) };
3297 if attr.is_null() {
3298 return -1;
3299 }
3300 unsafe { remove_prop(attr) }
3301}
3302
3303/// Remove a namespaced property by name (upstream tree.c `xmlUnsetNsProp`).
3304///
3305/// # SAFETY
3306///
3307/// - `node` must be a valid node pointer or NULL.
3308/// - `name` must be a valid null-terminated string.
3309/// - `nameSpace` may be NULL.
3310pub unsafe fn unset_ns_prop(
3311 node: *mut _xmlNode,
3312 name: *const xmlChar,
3313 name_space: *const xmlChar,
3314) -> c_int {
3315 let attr = unsafe { has_ns_prop(node, name, name_space) };
3316 if attr.is_null() {
3317 return -1;
3318 }
3319 unsafe { remove_prop(attr) }
3320}
3321
3322/// Return the first child ELEMENT of a node, or NULL (upstream tree.c
3323/// `xmlFirstElementChild`).
3324///
3325/// # SAFETY
3326///
3327/// - `node` must be a valid node pointer or NULL.
3328pub unsafe fn first_element_child(node: *mut _xmlNode) -> *mut _xmlNode {
3329 if node.is_null() {
3330 return ptr::null_mut();
3331 }
3332 let mut cur = unsafe { (*node).children };
3333 while !cur.is_null() {
3334 if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
3335 return cur;
3336 }
3337 cur = unsafe { (*cur).next };
3338 }
3339 ptr::null_mut()
3340}
3341
3342/// Return the last child ELEMENT of a node, or NULL (upstream tree.c
3343/// `xmlLastElementChild`).
3344///
3345/// # SAFETY
3346///
3347/// - `node` must be a valid node pointer or NULL.
3348pub unsafe fn last_element_child(node: *mut _xmlNode) -> *mut _xmlNode {
3349 if node.is_null() {
3350 return ptr::null_mut();
3351 }
3352 let mut cur = unsafe { (*node).last };
3353 while !cur.is_null() {
3354 if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
3355 return cur;
3356 }
3357 cur = unsafe { (*cur).prev };
3358 }
3359 ptr::null_mut()
3360}
3361
3362/// Return the next ELEMENT sibling of a node, or NULL (upstream tree.c
3363/// `xmlNextElementSibling`).
3364///
3365/// # SAFETY
3366///
3367/// - `node` must be a valid node pointer or NULL.
3368pub unsafe fn next_element_sibling(node: *mut _xmlNode) -> *mut _xmlNode {
3369 if node.is_null() {
3370 return ptr::null_mut();
3371 }
3372 let mut cur = unsafe { (*node).next };
3373 while !cur.is_null() {
3374 if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
3375 return cur;
3376 }
3377 cur = unsafe { (*cur).next };
3378 }
3379 ptr::null_mut()
3380}
3381
3382/// Return the previous ELEMENT sibling of a node, or NULL (upstream tree.c
3383/// `xmlPreviousElementSibling`).
3384///
3385/// # SAFETY
3386///
3387/// - `node` must be a valid node pointer or NULL.
3388pub unsafe fn previous_element_sibling(node: *mut _xmlNode) -> *mut _xmlNode {
3389 if node.is_null() {
3390 return ptr::null_mut();
3391 }
3392 let mut cur = unsafe { (*node).prev };
3393 while !cur.is_null() {
3394 if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
3395 return cur;
3396 }
3397 cur = unsafe { (*cur).prev };
3398 }
3399 ptr::null_mut()
3400}
3401
3402/// Count the child ELEMENT nodes of a node (upstream tree.c
3403/// `xmlChildElementCount`).
3404///
3405/// # SAFETY
3406///
3407/// - `node` must be a valid node pointer or NULL.
3408pub unsafe fn child_element_count(node: *mut _xmlNode) -> c_ulong {
3409 if node.is_null() {
3410 return 0;
3411 }
3412 let mut cur = unsafe { (*node).children };
3413 let mut count: c_ulong = 0;
3414 while !cur.is_null() {
3415 if unsafe { (*cur).type_ } == XML_ELEMENT_NODE as c_int {
3416 count += 1;
3417 }
3418 cur = unsafe { (*cur).next };
3419 }
3420 count
3421}
3422
3423/// Concatenate text to a node's content (upstream tree.c `xmlTextConcat`):
3424/// appends `num` bytes of `str` to the node's text content. Returns 0 on
3425/// success, -1 on error.
3426///
3427/// # SAFETY
3428///
3429/// - `node` must be a valid text node or NULL.
3430/// - `str` must be a valid buffer of `num` bytes.
3431pub unsafe fn text_concat(node: *mut _xmlNode, str: *const xmlChar, num: c_int) -> c_int {
3432 if node.is_null() || str.is_null() || num <= 0 {
3433 return -1;
3434 }
3435 let cur = unsafe { &mut *node };
3436 if cur.content.is_null() {
3437 let p = unsafe { allocator::xmlMallocImpl(num as usize + 1) as *mut xmlChar };
3438 if p.is_null() {
3439 return -1;
3440 }
3441 unsafe {
3442 ptr::copy_nonoverlapping(str, p, num as usize);
3443 *p.add(num as usize) = 0;
3444 }
3445 cur.content = p;
3446 return 0;
3447 }
3448 let old_len = unsafe { crate::xml::string::xml_strlen(cur.content) };
3449 let p = unsafe {
3450 allocator::xmlReallocImpl(cur.content as *mut c_void, old_len + num as usize + 1)
3451 as *mut xmlChar
3452 };
3453 if p.is_null() {
3454 return -1;
3455 }
3456 unsafe {
3457 ptr::copy_nonoverlapping(str, p.add(old_len), num as usize);
3458 *p.add(old_len + num as usize) = 0;
3459 }
3460 cur.content = p;
3461 0
3462}
3463
3464/// Merge the text content of two nodes (upstream tree.c `xmlTextMerge`):
3465/// appends `ntext`'s content to `text`'s content and frees `ntext`.
3466/// Returns the first node, or NULL on error.
3467///
3468/// # SAFETY
3469///
3470/// - `text` and `ntext` must be valid text nodes or NULL.
3471pub unsafe fn text_merge(text: *mut _xmlNode, ntext: *mut _xmlNode) -> *mut _xmlNode {
3472 if text.is_null() || ntext.is_null() {
3473 return ptr::null_mut();
3474 }
3475 if unsafe { (*ntext).content.is_null() } {
3476 unsafe { free_node(ntext) };
3477 return text;
3478 }
3479 let num = unsafe { crate::xml::string::xml_strlen((*ntext).content) };
3480 if unsafe { text_concat(text, (*ntext).content, num as c_int) } != 0 {
3481 return ptr::null_mut();
3482 }
3483 unsafe { free_node(ntext) };
3484 text
3485}
3486
3487// ═══════════════════════════════════════════════════════════════════════════════
3488// DTD Operations
3489// ═══════════════════════════════════════════════════════════════════════════════
3490
3491/// Get the internal DTD subset of a document.
3492///
3493/// # UPSTREAM-PARITY
3494///
3495/// ```c
3496/// xmlDtdPtr xmlGetIntSubset(xmlDocPtr doc);
3497/// ```
3498pub const fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
3499 if doc.is_null() {
3500 return ptr::null_mut();
3501 }
3502 let d = unsafe { &*doc };
3503 d.intSubset
3504}
3505
3506/// Create a new DTD node.
3507///
3508/// # UPSTREAM-PARITY
3509///
3510/// ```c
3511/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
3512/// const xmlChar *ExternalID, const xmlChar *SystemID);
3513/// ```
3514///
3515/// Creates a new DTD and attaches it to the document.
3516///
3517/// # SAFETY
3518///
3519/// - `doc` must be a valid pointer to an _xmlDoc.
3520/// - `name` must be a valid null-terminated string or NULL.
3521/// - `ExternalID`, `SystemID` may be NULL.
3522pub unsafe fn new_dtd(
3523 doc: *mut _xmlDoc,
3524 name: *const xmlChar,
3525 ExternalID: *const xmlChar,
3526 SystemID: *const xmlChar,
3527) -> *mut _xmlDtd {
3528 let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
3529 if dtd.is_null() {
3530 return ptr::null_mut();
3531 }
3532
3533 // UPSTREAM-PARITY (tree.c xmlNewDtd): a document that already has an
3534 // external subset cannot take another — upstream returns NULL.
3535 if !doc.is_null() && !(*doc).extSubset.is_null() {
3536 allocator::xmlFreeImpl(dtd as *mut c_void);
3537 return ptr::null_mut();
3538 }
3539
3540 unsafe {
3541 (*dtd).type_ = XML_DTD_NODE as c_int;
3542 (*dtd).name = dup_xml_str(name);
3543 (*dtd).ExternalID = dup_xml_str(ExternalID);
3544 (*dtd).SystemID = dup_xml_str(SystemID);
3545 (*dtd).parent = doc;
3546 (*dtd).doc = doc;
3547
3548 // UPSTREAM-PARITY (tree.c xmlNewDtd): a freshly created DTD node
3549 // becomes the document's EXTERNAL subset (doc->extSubset); the
3550 // internal subset is created via xmlCreateIntSubset.
3551 // nokogiri create_external_subset reads doc->extSubset back as
3552 // Document#external_subset.
3553
3554 // Attach to document as the external subset
3555 if !doc.is_null() {
3556 (*doc).extSubset = dtd;
3557 }
3558 }
3559
3560 dtd
3561}
3562
3563/// Free a DTD.
3564///
3565/// # SAFETY
3566///
3567/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
3568unsafe fn free_dtd(dtd: *mut _xmlDtd) {
3569 if dtd.is_null() {
3570 return;
3571 }
3572
3573 let _d = unsafe { &mut *dtd };
3574 let d = &mut *dtd;
3575
3576 // UPSTREAM-PARITY (tree.c xmlFreeDtd): element/attribute/entity
3577 // declaration nodes in the child list are owned by the hash tables and
3578 // are freed by the deallocators below; only non-declaration children
3579 // (comments, PIs) are unlinked and freed from the list here. This must
3580 // run BEFORE the hash tables are freed so the decl nodes are still alive
3581 // when their type is inspected.
3582 if !d.children.is_null() {
3583 let mut c = d.children;
3584 while !c.is_null() {
3585 let next = (*c).next;
3586 let t = (*c).type_;
3587 if t != XML_ELEMENT_DECL as c_int
3588 && t != XML_ATTRIBUTE_DECL as c_int
3589 && t != XML_ENTITY_DECL as c_int
3590 {
3591 unlink_node_internal(c, ptr::null_mut());
3592 free_node(c);
3593 }
3594 c = next;
3595 }
3596 }
3597
3598 // Free name
3599 if !d.name.is_null() {
3600 allocator::xmlFreeImpl(d.name as *mut c_void);
3601 }
3602 if !d.ExternalID.is_null() {
3603 allocator::xmlFreeImpl(d.ExternalID as *mut c_void);
3604 }
3605 if !d.SystemID.is_null() {
3606 allocator::xmlFreeImpl(d.SystemID as *mut c_void);
3607 }
3608
3609 // Free hash tables for declarations
3610 /// Hash-table deallocator shim that frees an `_xmlNotation` payload.
3611 ///
3612 /// # Safety
3613 ///
3614 /// - `payload` must be NULL or a valid pointer to an `_xmlNotation` owned
3615 /// exclusively by the hash table being freed; it is freed with
3616 /// `free_notation`.
3617 /// - `_name` is unused.
3618 unsafe extern "C" fn free_notation_wrapper(payload: *mut c_void, _name: *mut u8) {
3619 crate::xml::dtd::free_notation(payload as *mut _xmlNotation);
3620 }
3621 /// Hash-table deallocator shim that frees an `_xmlElement` payload.
3622 ///
3623 /// # Safety
3624 ///
3625 /// - `payload` must be NULL or a valid pointer to an `_xmlElement` owned
3626 /// exclusively by the hash table being freed; it is freed with
3627 /// `free_element`.
3628 /// - `_name` is unused.
3629 unsafe extern "C" fn free_element_wrapper(payload: *mut c_void, _name: *mut u8) {
3630 crate::xml::dtd::free_element(payload as *mut _xmlElement);
3631 }
3632 /// Hash-table deallocator shim that frees an `_xmlAttribute` payload.
3633 ///
3634 /// # Safety
3635 ///
3636 /// - `payload` must be NULL or a valid pointer to an `_xmlAttribute` owned
3637 /// exclusively by the hash table being freed; it is freed with
3638 /// `free_attribute`.
3639 /// - `_name` is unused.
3640 unsafe extern "C" fn free_attribute_wrapper(payload: *mut c_void, _name: *mut u8) {
3641 crate::xml::dtd::free_attribute(payload as *mut _xmlAttribute);
3642 }
3643 /// Hash-table deallocator shim that frees an `_xmlEntity` payload.
3644 ///
3645 /// # Safety
3646 ///
3647 /// - `payload` must be NULL or a valid pointer to an `_xmlEntity` owned
3648 /// exclusively by the hash table being freed; it is freed with
3649 /// `free_entity`.
3650 /// - `_name` is unused.
3651 unsafe extern "C" fn free_entity_wrapper(payload: *mut c_void, _name: *mut u8) {
3652 crate::xml::entities::free_entity(payload as *mut _xmlEntity);
3653 }
3654
3655 if !d.notations.is_null() {
3656 crate::xml::hash::hash_free(
3657 d.notations as *mut crate::xml::hash::HashTable,
3658 Some(free_notation_wrapper),
3659 );
3660 d.notations = ptr::null_mut();
3661 }
3662 if !d.elements.is_null() {
3663 crate::xml::hash::hash_free(
3664 d.elements as *mut crate::xml::hash::HashTable,
3665 Some(free_element_wrapper),
3666 );
3667 d.elements = ptr::null_mut();
3668 }
3669 if !d.attributes.is_null() {
3670 crate::xml::hash::hash_free(
3671 d.attributes as *mut crate::xml::hash::HashTable,
3672 Some(free_attribute_wrapper),
3673 );
3674 d.attributes = ptr::null_mut();
3675 }
3676 if !d.entities.is_null() {
3677 crate::xml::hash::hash_free(
3678 d.entities as *mut crate::xml::hash::HashTable,
3679 Some(free_entity_wrapper),
3680 );
3681 d.entities = ptr::null_mut();
3682 }
3683 if !d.pentities.is_null() {
3684 crate::xml::hash::hash_free(
3685 d.pentities as *mut crate::xml::hash::HashTable,
3686 Some(free_entity_wrapper),
3687 );
3688 d.pentities = ptr::null_mut();
3689 }
3690
3691 allocator::xmlFreeImpl(dtd as *mut c_void);
3692}
3693
3694// ═══════════════════════════════════════════════════════════════════════════════
3695// Entity Operations
3696// ═══════════════════════════════════════════════════════════════════════════════
3697
3698/// Create a new entity.
3699///
3700/// # UPSTREAM-PARITY
3701///
3702/// ```c
3703/// xmlEntityPtr xmlNewEntity(xmlDocPtr doc, const xmlChar *name, int type,
3704/// const xmlChar *ExternalID, const xmlChar *SystemID,
3705/// const xmlChar *content);
3706/// ```
3707///
3708/// # SAFETY
3709///
3710/// - `doc` may be NULL.
3711/// - `name` must be a valid null-terminated string.
3712/// - `ExternalID`, `SystemID`, `content` may be NULL.
3713pub unsafe fn new_entity(
3714 _doc: *mut _xmlDoc,
3715 name: *const xmlChar,
3716 etype: c_int,
3717 ExternalID: *const xmlChar,
3718 SystemID: *const xmlChar,
3719 content: *const xmlChar,
3720) -> *mut _xmlEntity {
3721 let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
3722 if entity.is_null() {
3723 return ptr::null_mut();
3724 }
3725
3726 unsafe {
3727 (*entity).type_ = XML_ENTITY_DECL as c_int;
3728 (*entity).name = dup_xml_str(name);
3729 (*entity).etype = etype;
3730 (*entity).ExternalID = dup_xml_str(ExternalID);
3731 (*entity).SystemID = dup_xml_str(SystemID);
3732 (*entity).content = dup_xml_str(content);
3733 (*entity).length = if content.is_null() {
3734 0
3735 } else {
3736 crate::abi::exports_xml2::xmlStrlen(content)
3737 };
3738 (*entity).flags = 0;
3739 (*entity).expandedSize = 0;
3740 }
3741
3742 entity
3743}
3744
3745/// Get a document entity by name.
3746///
3747/// # UPSTREAM-PARITY
3748///
3749/// ```c
3750/// xmlEntityPtr xmlGetDocEntity(xmlDocPtr doc, const xmlChar *name);
3751/// ```
3752///
3753/// Returns the entity, or NULL if not found.
3754///
3755/// # SAFETY
3756///
3757/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
3758/// - `name` must be a valid null-terminated string.
3759pub unsafe fn get_doc_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
3760 crate::xml::entities::get_entity(doc as *mut _xmlDoc, name)
3761}
3762
3763/// Add an entity declaration to the document's internal subset (upstream
3764/// entities.c `xmlAddDocEntity`); creates the internal subset when absent.
3765///
3766/// # SAFETY
3767///
3768/// - `doc` must be a valid document pointer or NULL.
3769/// - `name` must be a valid null-terminated string.
3770pub unsafe fn add_doc_entity(
3771 doc: *mut _xmlDoc,
3772 name: *const xmlChar,
3773 etype: c_int,
3774 ExternalID: *const xmlChar,
3775 SystemID: *const xmlChar,
3776 content: *const xmlChar,
3777) -> *mut _xmlEntity {
3778 if doc.is_null() || name.is_null() {
3779 return ptr::null_mut();
3780 }
3781 unsafe {
3782 let mut dtd = (*doc).intSubset;
3783 if dtd.is_null() {
3784 dtd = new_dtd(
3785 doc,
3786 c"internal".as_ptr() as *const xmlChar,
3787 ptr::null(),
3788 ptr::null(),
3789 );
3790 if dtd.is_null() {
3791 return ptr::null_mut();
3792 }
3793 }
3794 crate::xml::entities::add_entity(dtd, name, etype, ExternalID, SystemID, content)
3795 }
3796}
3797
3798/// Add an entity declaration to the document's external subset (upstream
3799/// entities.c `xmlAddDtdEntity`); creates the external subset when absent.
3800///
3801/// # SAFETY
3802///
3803/// - `doc` must be a valid document pointer or NULL.
3804/// - `name` must be a valid null-terminated string.
3805pub unsafe fn add_dtd_entity(
3806 doc: *mut _xmlDoc,
3807 name: *const xmlChar,
3808 etype: c_int,
3809 ExternalID: *const xmlChar,
3810 SystemID: *const xmlChar,
3811 content: *const xmlChar,
3812) -> *mut _xmlEntity {
3813 if doc.is_null() || name.is_null() {
3814 return ptr::null_mut();
3815 }
3816 unsafe {
3817 let mut dtd = (*doc).extSubset;
3818 if dtd.is_null() {
3819 dtd = new_dtd(
3820 doc,
3821 c"internal".as_ptr() as *const xmlChar,
3822 ptr::null(),
3823 ptr::null(),
3824 );
3825 if dtd.is_null() {
3826 return ptr::null_mut();
3827 }
3828 (*doc).extSubset = dtd;
3829 }
3830 crate::xml::entities::add_entity(dtd, name, etype, ExternalID, SystemID, content)
3831 }
3832}
3833
3834/// Get an entity declaration from the internal or external subset (upstream
3835/// entities.c `xmlGetDtdEntity`).
3836///
3837/// # SAFETY
3838///
3839/// - `doc` must be a valid document pointer or NULL.
3840/// - `name` must be a valid null-terminated string.
3841pub unsafe fn get_dtd_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
3842 if doc.is_null() || name.is_null() {
3843 return ptr::null_mut();
3844 }
3845 unsafe {
3846 if !(*doc).intSubset.is_null() {
3847 let e = crate::xml::entities::get_entity_from_dtd((*doc).intSubset, name);
3848 if !e.is_null() {
3849 return e;
3850 }
3851 }
3852 if !(*doc).extSubset.is_null() {
3853 return crate::xml::entities::get_entity_from_dtd((*doc).extSubset, name);
3854 }
3855 ptr::null_mut()
3856 }
3857}
3858
3859/// Get a parameter entity by name.
3860///
3861/// # UPSTREAM-PARITY
3862///
3863/// ```c
3864/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
3865/// ```
3866///
3867/// # SAFETY
3868///
3869/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
3870/// - `name` must be a valid null-terminated string.
3871pub unsafe fn get_parameter_entity(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
3872 crate::xml::entities::get_parameter_entity(doc as *mut _xmlDoc, name)
3873}
3874
3875// ═══════════════════════════════════════════════════════════════════════════════
3876// XML Serialization
3877// ═══════════════════════════════════════════════════════════════════════════════
3878//
3879// Functions for serializing XML document/node trees to text.
3880// All output is UTF-8.
3881
3882/// Entity replacement strings (as xmlChar byte slices).
3883const ENTITY_LT: &[xmlChar] = b"<";
3884const ENTITY_GT: &[xmlChar] = b">";
3885const ENTITY_AMP: &[xmlChar] = b"&";
3886const ENTITY_QUOT: &[xmlChar] = b""";
3887#[allow(dead_code)]
3888const ENTITY_APOS: &[xmlChar] = b"'";
3889
3890/// Indentation string (libxml2's default `xmlTreeIndentString`).
3891const INDENT: &[xmlChar] = b" ";
3892
3893/// Maximum indent buffer size (libxml2 `MAX_INDENT` in xmlsave.c).
3894const MAX_INDENT: c_int = 60;
3895
3896/// Serialize text content with XML escaping.
3897///
3898/// Decode one UTF-8 sequence whose first byte is at `bytes[i]` and write the
3899/// upstream `xmlSerializeHexCharRef` form (`&#x%X;`, uppercase hex, no
3900/// padding). Returns the number of input bytes consumed (1 on invalid
3901/// sequences — the upstream U+FFFD fallback advances one byte).
3902///
3903/// # Safety
3904///
3905/// - `bytes` must be readable for `len` bytes with `i < len`.
3906unsafe fn write_utf8_hex_char_ref(buf: *mut _xmlBuffer, bytes: &[u8], i: usize) -> usize {
3907 // SAFETY: caller guarantees i < len.
3908 let first = bytes[i];
3909 let (n, mut val): (usize, u32) = if first < 0x80 {
3910 (1, first as u32)
3911 } else if first < 0xE0 {
3912 if i + 1 < bytes.len() {
3913 (
3914 2,
3915 ((first & 0x1F) as u32) << 6 | (bytes[i + 1] & 0x3F) as u32,
3916 )
3917 } else {
3918 (1, 0xFFFD)
3919 }
3920 } else if first < 0xF0 {
3921 if i + 2 < bytes.len() {
3922 (
3923 3,
3924 ((first & 0x0F) as u32) << 12
3925 | ((bytes[i + 1] & 0x3F) as u32) << 6
3926 | (bytes[i + 2] & 0x3F) as u32,
3927 )
3928 } else {
3929 (1, 0xFFFD)
3930 }
3931 } else if first < 0xF8 {
3932 if i + 3 < bytes.len() {
3933 (
3934 4,
3935 ((first & 0x07) as u32) << 18
3936 | ((bytes[i + 1] & 0x3F) as u32) << 12
3937 | ((bytes[i + 2] & 0x3F) as u32) << 6
3938 | (bytes[i + 3] & 0x3F) as u32,
3939 )
3940 } else {
3941 (1, 0xFFFD)
3942 }
3943 } else {
3944 (1, 0xFFFD)
3945 };
3946 if val == 0xFFFE || val == 0xFFFF || val > 0x10FFFF {
3947 val = 0xFFFD;
3948 }
3949 // SAFETY: writes the formatted reference (xmlSerializeHexCharRef).
3950 let hex = format!("&#x{:X};", val);
3951 io::buf_add(buf, hex.as_ptr(), hex.len() as c_int);
3952 n
3953}
3954
3955/// Serialize text content with XML escaping.
3956///
3957/// # UPSTREAM-PARITY
3958///
3959/// Mirrors libxml2 2.15 `xmlSerializeText`. When `escape_non_ascii` is set
3960/// (upstream `XML_ESCAPE_NON_ASCII` — chosen by xmlsave.c `xmlSaveWriteText`
3961/// when the save context has NO output encoder, `ctxt->encoding == NULL`),
3962/// every non-ASCII byte is decoded as UTF-8 and written as a hexadecimal
3963/// character reference — `café` → `café`, U+00A0 → ` ` (ext/dom
3964/// dom005's xml save of html-origin text, xmlsave oracle parity). Without the
3965/// flag (a non-NULL save encoding, as in the libxslt save path): `<` →
3966/// `<`, `>` → `>`, `&` → `&`, `\r` → ` `, other control
3967/// characters → hexadecimal character references, while `\n` and `\t` are
3968/// emitted literally and non-ASCII bytes are passed through.
3969///
3970/// # SAFETY
3971///
3972/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
3973/// - `content` must be a valid pointer to `len` bytes of xmlChar data, or NULL.
3974pub(crate) unsafe fn serialize_text_flags(
3975 buf: *mut _xmlBuffer,
3976 content: *const xmlChar,
3977 len: c_int,
3978 escape_non_ascii: bool,
3979) {
3980 if buf.is_null() || content.is_null() || len <= 0 {
3981 return;
3982 }
3983
3984 let bytes = core::slice::from_raw_parts(content, len as usize);
3985 let mut i: usize = 0;
3986 while i < bytes.len() {
3987 let ch = bytes[i];
3988
3989 // Check for `]]>` sequence
3990 if ch == b']' && i + 2 < bytes.len() && bytes[i + 1] == b']' && bytes[i + 2] == b'>' {
3991 // Write `]]>` — escape the `>` that ends `]]>`
3992 io::buf_add(buf, b"]]" as *const u8, 2);
3993 io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
3994 i += 3;
3995 continue;
3996 }
3997
3998 if ch >= 0x80 && escape_non_ascii {
3999 // SAFETY: i < bytes.len() holds; the helper decodes the UTF-8
4000 // sequence and advances past it.
4001 i += unsafe { write_utf8_hex_char_ref(buf, bytes, i) };
4002 continue;
4003 }
4004
4005 match ch {
4006 b'<' => {
4007 io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
4008 }
4009 b'&' => {
4010 io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
4011 }
4012 b'>' => {
4013 // UPSTREAM-PARITY: libxml2 escapes `>` to `>` in text content.
4014 // While the XML spec only requires escaping `>` in `]]>`, libxml2's
4015 // serializer escapes all `>` characters.
4016 io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
4017 }
4018 b'\r' => {
4019 // Carriage return is not allowed literally in XML content.
4020 io::buf_add(buf, b" " as *const u8, 5);
4021 }
4022 0x01..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F => {
4023 // Other control characters are emitted as hex character refs.
4024 let hex = format!("&#x{:X};", ch);
4025 io::buf_add(buf, hex.as_ptr(), hex.len() as c_int);
4026 }
4027 _ => {
4028 io::buf_add(buf, &ch as *const u8, 1);
4029 }
4030 }
4031 i += 1;
4032 }
4033}
4034
4035/// Serialize an attribute value with XML escaping.
4036///
4037/// # UPSTREAM-PARITY
4038///
4039/// Mirrors libxml2 `xmlBufAttrSerializeTxtContent` (xmlsave.c):
4040/// `\n` → ` `, `\r` → ` `, `\t` → `	`, `"` → `"`,
4041/// `<` → `<`, `>` → `>`, `&` → `&`.
4042///
4043/// # SAFETY
4044///
4045/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4046/// - `value` must be a valid null-terminated xmlChar string, or NULL.
4047pub(crate) unsafe fn serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
4048 unsafe { serialize_attr_value_flags(buf, value, false) };
4049}
4050
4051/// Like [`serialize_attr_value`] plus the upstream `XML_ESCAPE_NON_ASCII`
4052/// flag (xmlsave.c `xmlSaveWriteText` / `xmlBufAttrSerializeTxtContent`):
4053/// non-ASCII bytes become hexadecimal character references when the save
4054/// context has no output encoder.
4055///
4056/// # SAFETY
4057///
4058/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4059/// - `value` must be a valid null-terminated xmlChar string, or NULL.
4060pub(crate) unsafe fn serialize_attr_value_flags(
4061 buf: *mut _xmlBuffer,
4062 value: *const xmlChar,
4063 escape_non_ascii: bool,
4064) {
4065 if buf.is_null() || value.is_null() {
4066 return;
4067 }
4068
4069 let len = xml_strlen(value);
4070 let bytes = core::slice::from_raw_parts(value, len as usize);
4071 let mut i: usize = 0;
4072 while i < bytes.len() {
4073 let ch = bytes[i];
4074
4075 if ch >= 0x80 && escape_non_ascii {
4076 // SAFETY: i < bytes.len() holds; the helper decodes the UTF-8
4077 // sequence and advances past it.
4078 i += unsafe { write_utf8_hex_char_ref(buf, bytes, i) };
4079 continue;
4080 }
4081
4082 match ch {
4083 b'\n' => {
4084 io::buf_add(buf, b" " as *const u8, 5);
4085 }
4086 b'\r' => {
4087 io::buf_add(buf, b" " as *const u8, 5);
4088 }
4089 b'\t' => {
4090 io::buf_add(buf, b"	" as *const u8, 4);
4091 }
4092 b'<' => {
4093 io::buf_add(buf, ENTITY_LT.as_ptr(), ENTITY_LT.len() as c_int);
4094 }
4095 b'&' => {
4096 io::buf_add(buf, ENTITY_AMP.as_ptr(), ENTITY_AMP.len() as c_int);
4097 }
4098 b'"' => {
4099 io::buf_add(buf, ENTITY_QUOT.as_ptr(), ENTITY_QUOT.len() as c_int);
4100 }
4101 b'>' => {
4102 io::buf_add(buf, ENTITY_GT.as_ptr(), ENTITY_GT.len() as c_int);
4103 }
4104 _ => {
4105 io::buf_add(buf, &ch as *const u8, 1);
4106 }
4107 }
4108 i += 1;
4109 }
4110}
4111
4112/// Write indentation.
4113///
4114/// # UPSTREAM-PARITY
4115///
4116/// Mirrors libxml2 `xmlSaveWriteIndent` (xmlsave.c 2.15): the level is
4117/// capped at `MAX_INDENT / indent_size` (= 30 with the default two-space
4118/// indent string).
4119///
4120/// # SAFETY
4121///
4122/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
4123unsafe fn write_indent(
4124 buf: *mut _xmlBuffer,
4125 level: c_int,
4126 indent: *const xmlChar,
4127 indent_len: c_int,
4128) {
4129 if buf.is_null() || level <= 0 || indent.is_null() || indent_len <= 0 {
4130 return;
4131 }
4132 let indent_nr = MAX_INDENT / indent_len;
4133 let mut lvl = level;
4134 if lvl > indent_nr {
4135 lvl = indent_nr;
4136 }
4137 for _ in 0..lvl {
4138 io::buf_add(buf, indent, indent_len);
4139 }
4140}
4141
4142/// Whether the save context should escape non-ASCII as hex references:
4143/// upstream xmlSaveWriteText sets XML_ESCAPE_NON_ASCII only when
4144/// `ctxt->encoding == NULL` after xmlSaveDocInternal fell back to the
4145/// document's own encoding and tried to switch an output encoder — a real
4146/// (non-native) document encoding gets a converter (no escape; the converter
4147/// produces the target bytes), while a NULL/UTF-8/US-ASCII encoding leaves
4148/// no encoder (escape). The candidate has no converters, so a declared
4149/// non-native encoding keeps the pre-existing pass-through.
4150///
4151/// # Safety
4152///
4153/// - `doc` may be NULL or a valid `_xmlDoc`.
4154pub(crate) fn save_escapes_non_ascii(save_encoding: *const xmlChar, doc: *mut _xmlDoc) -> bool {
4155 if !save_encoding.is_null() {
4156 return false;
4157 }
4158 if doc.is_null() || unsafe { (*doc).encoding }.is_null() {
4159 return true;
4160 }
4161 let enc = unsafe { (*doc).encoding };
4162 let name = unsafe { std::ffi::CStr::from_ptr(enc as *const c_char) }
4163 .to_string_lossy()
4164 .into_owned();
4165 let lower = name.to_ascii_lowercase();
4166 !(lower == "utf-8" || lower == "utf8" || lower == "us-ascii" || lower == "ascii")
4167}
4168
4169/// True if the text node is marked as unescaped (`disable-output-escaping`).
4170///
4171/// # UPSTREAM-PARITY
4172///
4173/// Upstream compares `node->name == xmlStringTextNoenc` (pointer equality
4174/// against a static marker). Our trees carry the marker as a duplicated
4175/// `"textnoenc"` string, so we compare contents.
4176///
4177/// # Safety
4178///
4179/// - `node` must be NULL or a valid pointer to an `_xmlNode`.
4180/// - When `node.name` is non-NULL it must be a valid NUL-terminated `xmlChar`
4181/// string; `c_str_eq_bytes` scans it until the NUL byte.
4182unsafe fn is_noenc_text(node: *mut _xmlNode) -> bool {
4183 if node.is_null() {
4184 return false;
4185 }
4186 let n = unsafe { &*node };
4187 if n.name.is_null() {
4188 return false;
4189 }
4190 c_str_eq_bytes(n.name, b"textnoenc")
4191}
4192
4193/// Compare a NUL-terminated xmlChar string with a byte slice.
4194///
4195/// # Safety
4196///
4197/// - `s` must be a valid pointer to a NUL-terminated `xmlChar` buffer; the
4198/// scan reads `b.len()` bytes plus the terminating NUL at `s[b.len()]`, so
4199/// the buffer must be at least `b.len() + 1` readable bytes.
4200const unsafe fn c_str_eq_bytes(s: *const xmlChar, b: &[u8]) -> bool {
4201 let mut i = 0usize;
4202 while i < b.len() {
4203 if unsafe { *s.add(i) } != b[i] {
4204 return false;
4205 }
4206 i += 1;
4207 }
4208 unsafe { *s.add(i) == 0 }
4209}
4210
4211/// Serialization state mirroring the formatting state of libxml2's
4212/// `xmlSaveCtxt` (xmlsave.c 2.15).
4213#[derive(Clone, Copy)]
4214struct DumpState {
4215 /// `ctxt->format`: 0 = no formatting, 1 = XML_SAVE_FORMAT.
4216 format: c_int,
4217 /// The format value captured at dump entry; restored when leaving an
4218 /// element whose children disabled formatting (upstream local `format`).
4219 saved: c_int,
4220 /// The element whose children disabled formatting (upstream
4221 /// `unformattedNode`).
4222 unformatted: *mut _xmlNode,
4223 /// Per-context indent string (upstream `ctxt->indent`); NULL falls back
4224 /// to the default `xmlTreeIndentString`.
4225 indent: *const xmlChar,
4226 /// Byte length of `indent`.
4227 indent_len: c_int,
4228 /// Suppress the XML declaration (XML_SAVE_NO_DECL, upstream `no_decl`).
4229 no_decl: c_int,
4230 /// The encoding name for the XML declaration (upstream `ctxt->encoding`);
4231 /// NULL means "use the document's own encoding" (upstream
4232 /// `if (encoding == NULL) encoding = cur->encoding;`).
4233 encoding: *const xmlChar,
4234 /// True when this dump entered through the full save path
4235 /// (serialize_node_opts_enc_full — xmlSaveDoc / xmlNodeDumpOutput-style
4236 /// saves): only those honor the upstream XML_ESCAPE_NON_ASCII decision.
4237 /// Bare node dumps (xslt per-child output, debug helpers) keep the raw
4238 /// pass-through.
4239 explicit_save: bool,
4240 /// XHTML mode (upstream `xhtmlNodeDumpOutput`): the document's DTD is an
4241 /// XHTML DTD, so a bare `html` element gets
4242 /// `xmlns="http://www.w3.org/1999/xhtml"` and non-HTML-empty elements
4243 /// serialize as open/close instead of self-closing.
4244 xhtml: bool,
4245 /// HTML output mode (nokogiri SaveOptions::AS_HTML): empty HTML void
4246 /// elements stay `<br>`-style (no slash, no end tag) and other empty
4247 /// non-void elements serialize as `<a></a>`.
4248 as_html: bool,
4249 /// `XML_SAVE_NO_EMPTY`: empty non-void elements get an explicit end tag.
4250 no_empty: bool,
4251}
4252
4253impl DumpState {
4254 const fn new(format: c_int) -> Self {
4255 let f = if format != 0 { 1 } else { 0 };
4256 DumpState {
4257 format: f,
4258 saved: f,
4259 unformatted: ptr::null_mut(),
4260 indent: INDENT.as_ptr(),
4261 indent_len: INDENT.len() as c_int,
4262 no_decl: 0,
4263 encoding: ptr::null(),
4264 xhtml: false,
4265 as_html: false,
4266 no_empty: false,
4267 explicit_save: false,
4268 }
4269 }
4270
4271 /// Create a state with a custom indent string (xmlSaveSetIndentString),
4272 /// the XML_SAVE_NO_DECL option, and an encoding name for the XML
4273 /// declaration (upstream `ctxt->encoding`; NULL = use `doc->encoding`).
4274 ///
4275 /// When `indent` is NULL the caller's global `xmlTreeIndentString` is
4276 /// used (upstream xmlsave.c: `if (ctxt->indent == NULL) indent =
4277 /// xmlTreeIndentString`; nokogiri sets that global before xmlSaveToIO).
4278 ///
4279 /// # SAFETY
4280 ///
4281 /// - `indent` must be NULL or a valid NUL-terminated string that stays
4282 /// alive for the whole dump.
4283 /// - `encoding` must be NULL or a valid NUL-terminated string that stays
4284 /// alive for the whole dump.
4285 unsafe fn with_indent_enc(
4286 format: c_int,
4287 indent: *const xmlChar,
4288 no_decl: c_int,
4289 encoding: *const xmlChar,
4290 ) -> Self {
4291 let f = if format != 0 { 1 } else { 0 };
4292 let (ptr, len) = if indent.is_null() {
4293 let g = crate::xml::globals::get_tree_indent_string();
4294 if g.is_null() {
4295 (INDENT.as_ptr(), INDENT.len() as c_int)
4296 } else {
4297 let mut n = 0i32;
4298 while unsafe { *g.add(n as usize) } != 0 {
4299 n += 1;
4300 }
4301 (g, n)
4302 }
4303 } else {
4304 let mut n = 0i32;
4305 while unsafe { *indent.add(n as usize) } != 0 {
4306 n += 1;
4307 }
4308 (indent, n)
4309 };
4310 DumpState {
4311 format: f,
4312 saved: f,
4313 unformatted: ptr::null_mut(),
4314 indent: ptr,
4315 indent_len: len,
4316 no_decl,
4317 encoding,
4318 as_html: false,
4319 no_empty: false,
4320 xhtml: false,
4321 explicit_save: false,
4322 }
4323 }
4324}
4325
4326/// UPSTREAM-PARITY (tree.c xmlIsXHTML): whether the document's internal
4327/// subset is one of the XHTML 1.0 DTDs (strict, frameset or transitional).
4328/// The XML serializer (xmlsave.c xhtmlNodeDumpOutput) switches to XHTML mode
4329/// when this returns 1.
4330pub(crate) unsafe fn xml_is_xhtml(doc: *mut _xmlDoc) -> bool {
4331 unsafe {
4332 if doc.is_null() {
4333 return false;
4334 }
4335 let d = &*doc;
4336 if d.intSubset.is_null() {
4337 return false;
4338 }
4339 let dtd = &*d.intSubset;
4340 if !dtd.ExternalID.is_null()
4341 && (c_str_eq_bytes(dtd.ExternalID, b"-//W3C//DTD XHTML 1.0 Strict//EN")
4342 || c_str_eq_bytes(dtd.ExternalID, b"-//W3C//DTD XHTML 1.0 Frameset//EN")
4343 || c_str_eq_bytes(dtd.ExternalID, b"-//W3C//DTD XHTML 1.0 Transitional//EN"))
4344 {
4345 return true;
4346 }
4347 if !dtd.SystemID.is_null()
4348 && (c_str_eq_bytes(
4349 dtd.SystemID,
4350 b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd",
4351 ) || c_str_eq_bytes(
4352 dtd.SystemID,
4353 b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd",
4354 ) || c_str_eq_bytes(
4355 dtd.SystemID,
4356 b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd",
4357 ))
4358 {
4359 return true;
4360 }
4361 false
4362 }
4363}
4364
4365/// UPSTREAM-PARITY (xmlsave.c xhtmlIsEmpty): HTML empty elements which stay
4366/// self-closing in XHTML mode.
4367const unsafe fn xhtml_is_empty(name: *const xmlChar) -> bool {
4368 unsafe {
4369 if name.is_null() {
4370 return false;
4371 }
4372 c_str_eq_bytes(name, b"area")
4373 || c_str_eq_bytes(name, b"base")
4374 || c_str_eq_bytes(name, b"basefont")
4375 || c_str_eq_bytes(name, b"br")
4376 || c_str_eq_bytes(name, b"col")
4377 || c_str_eq_bytes(name, b"frame")
4378 || c_str_eq_bytes(name, b"hr")
4379 || c_str_eq_bytes(name, b"img")
4380 || c_str_eq_bytes(name, b"input")
4381 || c_str_eq_bytes(name, b"isindex")
4382 || c_str_eq_bytes(name, b"link")
4383 || c_str_eq_bytes(name, b"meta")
4384 || c_str_eq_bytes(name, b"param")
4385 }
4386}
4387
4388/// Write an element/attribute name with its namespace prefix.
4389///
4390/// # SAFETY
4391///
4392/// - `buf` must be valid; `node` must be a valid element node.
4393unsafe fn write_qname(buf: *mut _xmlBuffer, node: *mut _xmlNode) {
4394 let n = unsafe { &*node };
4395 if !n.ns.is_null() {
4396 let ns = unsafe { &*n.ns };
4397 if !ns.prefix.is_null() {
4398 io::buf_cat(buf, ns.prefix);
4399 io::buf_ccat(buf, b':');
4400 }
4401 }
4402 if !n.name.is_null() {
4403 io::buf_cat(buf, n.name);
4404 }
4405}
4406
4407/// Dump a local namespace definition (upstream `xmlNsDumpOutput`).
4408///
4409/// # SAFETY
4410///
4411/// - `buf` must be valid; `cur` must be a valid `_xmlNs`.
4412unsafe fn ns_dump_output(buf: *mut _xmlBuffer, cur: *mut _xmlNs) {
4413 if cur.is_null() || buf.is_null() {
4414 return;
4415 }
4416 let ns = unsafe { &*cur };
4417 if ns.type_ == XML_LOCAL_NAMESPACE as c_int && !ns.href.is_null() {
4418 // The xml namespace is implicit and never re-declared.
4419 if !ns.prefix.is_null() && c_str_eq_bytes(ns.prefix, b"xml") {
4420 return;
4421 }
4422 io::buf_ccat(buf, b' ');
4423 if !ns.prefix.is_null() {
4424 io::buf_add(buf, b"xmlns:" as *const u8, 6);
4425 io::buf_cat(buf, ns.prefix);
4426 } else {
4427 io::buf_add(buf, b"xmlns" as *const u8, 5);
4428 }
4429 io::buf_add(buf, b"=\"" as *const u8, 2);
4430 serialize_attr_value(buf, ns.href);
4431 io::buf_ccat(buf, b'"');
4432 }
4433}
4434
4435/// Dump an attribute node (upstream `xmlAttrDumpOutput`).
4436///
4437/// `escape_non_ascii` mirrors xmlsave.c `xmlSaveWriteText`'s
4438/// `XML_ESCAPE_NON_ASCII` flag: attribute CONTENT is escaped as hex char
4439/// references when the save context has no output encoder.
4440///
4441/// # SAFETY
4442///
4443/// - `buf` must be valid; `cur` must be a valid `_xmlAttr`.
4444unsafe fn attr_dump_output(buf: *mut _xmlBuffer, cur: *mut _xmlAttr, escape_non_ascii: bool) {
4445 if cur.is_null() || buf.is_null() {
4446 return;
4447 }
4448 io::buf_ccat(buf, b' ');
4449 let a = unsafe { &*cur };
4450 if !a.ns.is_null() {
4451 let ans = unsafe { &*a.ns };
4452 if !ans.prefix.is_null() {
4453 io::buf_cat(buf, ans.prefix);
4454 io::buf_ccat(buf, b':');
4455 }
4456 }
4457 if !a.name.is_null() {
4458 io::buf_cat(buf, a.name);
4459 }
4460 io::buf_add(buf, b"=\"" as *const u8, 2);
4461 // Attribute content: text children are escaped, entity references are
4462 // emitted as `&name;` (upstream `xmlSaveWriteAttrContent`).
4463 let mut child = a.children;
4464 while !child.is_null() {
4465 let ct = unsafe { (*child).type_ };
4466 if ct == XML_TEXT_NODE as c_int && !unsafe { (*child).content }.is_null() {
4467 serialize_attr_value_flags(buf, unsafe { (*child).content }, escape_non_ascii);
4468 } else if ct == XML_ENTITY_REF_NODE as c_int && !unsafe { (*child).name }.is_null() {
4469 io::buf_ccat(buf, b'&');
4470 io::buf_cat(buf, unsafe { (*child).name });
4471 io::buf_ccat(buf, b';');
4472 }
4473 child = unsafe { (*child).next };
4474 }
4475 io::buf_ccat(buf, b'"');
4476}
4477
4478/// Dump a notation declaration (upstream `xmlBufDumpNotationDecl`).
4479///
4480/// # SAFETY
4481///
4482/// - `buf` must be valid; `nota` must be a valid `_xmlNotation`.
4483unsafe fn dump_notation_decl(buf: *mut _xmlBuffer, nota: *mut _xmlNotation) {
4484 let n = unsafe { &*nota };
4485 io::buf_add(buf, b"<!NOTATION " as *const u8, 11);
4486 if !n.name.is_null() {
4487 io::buf_cat(buf, n.name);
4488 }
4489 if !n.PublicID.is_null() {
4490 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
4491 write_quoted_string(buf, n.PublicID);
4492 if !n.SystemID.is_null() {
4493 io::buf_ccat(buf, b' ');
4494 write_quoted_string(buf, n.SystemID);
4495 }
4496 } else {
4497 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
4498 write_quoted_string(buf, n.SystemID);
4499 }
4500 io::buf_add(buf, b" >\n" as *const u8, 4);
4501}
4502
4503/// Dump an occurrence operator (upstream `xmlBufDumpElementOccur`).
4504unsafe fn dump_element_occur(buf: *mut _xmlBuffer, ocur: c_int) {
4505 use crate::abi::types::xmlElementContentOccur::*;
4506 if ocur == XML_ELEMENT_CONTENT_OPT as c_int {
4507 io::buf_ccat(buf, b'?');
4508 } else if ocur == XML_ELEMENT_CONTENT_MULT as c_int {
4509 io::buf_ccat(buf, b'*');
4510 } else if ocur == XML_ELEMENT_CONTENT_PLUS as c_int {
4511 io::buf_ccat(buf, b'+');
4512 }
4513}
4514
4515/// Dump an element content model (upstream `xmlBufDumpElementContent`).
4516///
4517/// # SAFETY
4518///
4519/// - `buf` must be valid; `content` must be a valid content tree or NULL.
4520unsafe fn dump_element_content(buf: *mut _xmlBuffer, content: *mut _xmlElementContent) {
4521 use crate::abi::types::xmlElementContentOccur::*;
4522 use crate::abi::types::xmlElementContentType::*;
4523 if content.is_null() {
4524 return;
4525 }
4526 io::buf_ccat(buf, b'(');
4527 let mut cur = content;
4528 loop {
4529 if cur.is_null() {
4530 return;
4531 }
4532 let c = unsafe { &*cur };
4533 match c.type_ {
4534 t if t == XML_ELEMENT_CONTENT_PCDATA as c_int => {
4535 io::buf_add(buf, b"#PCDATA" as *const u8, 7);
4536 }
4537 t if t == XML_ELEMENT_CONTENT_ELEMENT as c_int => {
4538 if !c.prefix.is_null() {
4539 io::buf_cat(buf, c.prefix);
4540 io::buf_ccat(buf, b':');
4541 }
4542 if !c.name.is_null() {
4543 io::buf_cat(buf, c.name);
4544 }
4545 }
4546 t if t == XML_ELEMENT_CONTENT_SEQ as c_int || t == XML_ELEMENT_CONTENT_OR as c_int => {
4547 if cur != content
4548 && !c.parent.is_null()
4549 && (c.type_ != unsafe { (*c.parent).type_ }
4550 || c.ocur != XML_ELEMENT_CONTENT_ONCE as c_int)
4551 {
4552 io::buf_ccat(buf, b'(');
4553 }
4554 cur = c.c1;
4555 continue;
4556 }
4557 _ => {}
4558 }
4559
4560 // Walk up until we find the next sibling to process.
4561 while cur != content {
4562 let ccur = unsafe { &*cur };
4563 let parent = ccur.parent;
4564 if parent.is_null() {
4565 return;
4566 }
4567 let p = unsafe { &*parent };
4568 if (ccur.type_ == XML_ELEMENT_CONTENT_OR as c_int
4569 || ccur.type_ == XML_ELEMENT_CONTENT_SEQ as c_int)
4570 && (ccur.type_ != p.type_ || ccur.ocur != XML_ELEMENT_CONTENT_ONCE as c_int)
4571 {
4572 io::buf_ccat(buf, b')');
4573 }
4574 dump_element_occur(buf, ccur.ocur);
4575
4576 if cur == p.c1 {
4577 // UPSTREAM-PARITY (xmlsave.c xmlBufDumpElementContent): the
4578 // " , "/" | " separator belongs to the PARENT combinator and
4579 // is written when ascending from the c1 subtree to c2 — a
4580 // plain-element c1 must still separate (a sequence like
4581 // (title, author) previously lost its separators entirely).
4582 if p.type_ == XML_ELEMENT_CONTENT_SEQ as c_int {
4583 io::buf_add(buf, b" , " as *const u8, 3);
4584 } else if p.type_ == XML_ELEMENT_CONTENT_OR as c_int {
4585 io::buf_add(buf, b" | " as *const u8, 3);
4586 }
4587 cur = p.c2;
4588 break;
4589 }
4590 cur = parent;
4591 }
4592 if cur == content {
4593 break;
4594 }
4595 }
4596 io::buf_ccat(buf, b')');
4597 let cc = unsafe { &*content };
4598 dump_element_occur(buf, cc.ocur);
4599}
4600
4601/// Dump an element declaration (upstream `xmlBufDumpElementDecl`).
4602///
4603/// # SAFETY
4604///
4605/// - `buf` must be valid; `elem` must be a valid `_xmlElement`.
4606unsafe fn dump_element_decl(buf: *mut _xmlBuffer, elem: *mut _xmlElement) {
4607 use crate::abi::types::xmlElementTypeVal::*;
4608 let e = unsafe { &*elem };
4609 io::buf_add(buf, b"<!ELEMENT " as *const u8, 10);
4610 if !e.prefix.is_null() {
4611 io::buf_cat(buf, e.prefix);
4612 io::buf_ccat(buf, b':');
4613 }
4614 if !e.name.is_null() {
4615 io::buf_cat(buf, e.name);
4616 }
4617 io::buf_ccat(buf, b' ');
4618 match e.etype {
4619 t if t == XML_ELEMENT_TYPE_EMPTY as c_int => {
4620 io::buf_add(buf, b"EMPTY" as *const u8, 5);
4621 }
4622 t if t == XML_ELEMENT_TYPE_ANY as c_int => {
4623 io::buf_add(buf, b"ANY" as *const u8, 3);
4624 }
4625 t if t == XML_ELEMENT_TYPE_MIXED as c_int || t == XML_ELEMENT_TYPE_ELEMENT as c_int => {
4626 dump_element_content(buf, e.content);
4627 }
4628 _ => {}
4629 }
4630 io::buf_add(buf, b">\n" as *const u8, 2);
4631}
4632
4633/// Dump an enumeration (upstream `xmlBufDumpEnumeration`).
4634///
4635/// # SAFETY
4636///
4637/// - `buf` must be valid; `cur` must be a valid enumeration or NULL.
4638unsafe fn dump_enumeration(buf: *mut _xmlBuffer, cur: *mut _xmlEnumeration) {
4639 let mut e = cur;
4640 while !e.is_null() {
4641 let en = unsafe { &*e };
4642 if !en.name.is_null() {
4643 io::buf_cat(buf, en.name);
4644 }
4645 if !en.next.is_null() {
4646 io::buf_add(buf, b" | " as *const u8, 3);
4647 }
4648 e = en.next;
4649 }
4650 io::buf_ccat(buf, b')');
4651}
4652
4653/// Dump an attribute declaration (upstream `xmlSaveWriteAttributeDecl`).
4654///
4655/// # SAFETY
4656///
4657/// - `buf` must be valid; `attr` must be a valid `_xmlAttribute` decl.
4658unsafe fn dump_attribute_decl(buf: *mut _xmlBuffer, attr: *mut _xmlAttribute) {
4659 use crate::abi::types::xmlAttributeDefault::*;
4660 use crate::abi::types::xmlAttributeType::*;
4661 let a = unsafe { &*attr };
4662 io::buf_add(buf, b"<!ATTLIST " as *const u8, 10);
4663 if !a.elem.is_null() {
4664 io::buf_cat(buf, a.elem);
4665 }
4666 io::buf_ccat(buf, b' ');
4667 if !a.prefix.is_null() {
4668 io::buf_cat(buf, a.prefix);
4669 io::buf_ccat(buf, b':');
4670 }
4671 if !a.name.is_null() {
4672 io::buf_cat(buf, a.name);
4673 }
4674 match a.atype {
4675 t if t == XML_ATTRIBUTE_CDATA as c_int => {
4676 io::buf_add(buf, b" CDATA" as *const u8, 6);
4677 }
4678 t if t == XML_ATTRIBUTE_ID as c_int => {
4679 io::buf_add(buf, b" ID" as *const u8, 3);
4680 }
4681 t if t == XML_ATTRIBUTE_IDREF as c_int => {
4682 io::buf_add(buf, b" IDREF" as *const u8, 6);
4683 }
4684 t if t == XML_ATTRIBUTE_IDREFS as c_int => {
4685 io::buf_add(buf, b" IDREFS" as *const u8, 7);
4686 }
4687 t if t == XML_ATTRIBUTE_ENTITY as c_int => {
4688 io::buf_add(buf, b" ENTITY" as *const u8, 7);
4689 }
4690 t if t == XML_ATTRIBUTE_ENTITIES as c_int => {
4691 io::buf_add(buf, b" ENTITIES" as *const u8, 9);
4692 }
4693 t if t == XML_ATTRIBUTE_NMTOKEN as c_int => {
4694 io::buf_add(buf, b" NMTOKEN" as *const u8, 8);
4695 }
4696 t if t == XML_ATTRIBUTE_NMTOKENS as c_int => {
4697 io::buf_add(buf, b" NMTOKENS" as *const u8, 9);
4698 }
4699 t if t == XML_ATTRIBUTE_ENUMERATION as c_int => {
4700 io::buf_add(buf, b" (" as *const u8, 2);
4701 dump_enumeration(buf, a.tree);
4702 }
4703 t if t == XML_ATTRIBUTE_NOTATION as c_int => {
4704 io::buf_add(buf, b" NOTATION (" as *const u8, 11);
4705 dump_enumeration(buf, a.tree);
4706 }
4707 _ => {}
4708 }
4709 match a.def {
4710 t if t == XML_ATTRIBUTE_REQUIRED as c_int => {
4711 io::buf_add(buf, b" #REQUIRED" as *const u8, 10);
4712 }
4713 t if t == XML_ATTRIBUTE_IMPLIED as c_int => {
4714 io::buf_add(buf, b" #IMPLIED" as *const u8, 9);
4715 }
4716 t if t == XML_ATTRIBUTE_FIXED as c_int => {
4717 io::buf_add(buf, b" #FIXED" as *const u8, 7);
4718 }
4719 _ => {}
4720 }
4721 if !a.defaultValue.is_null() {
4722 io::buf_add(buf, b" \"" as *const u8, 2);
4723 serialize_attr_value(buf, a.defaultValue);
4724 io::buf_ccat(buf, b'"');
4725 }
4726 io::buf_add(buf, b">\n" as *const u8, 2);
4727}
4728
4729/// Write a quoted string (upstream `xmlOutputBufferWriteQuotedString`).
4730///
4731/// # SAFETY
4732///
4733/// - `buf` must be valid; `str` must be a valid NUL-terminated string.
4734unsafe fn write_quoted_string(buf: *mut _xmlBuffer, str: *const xmlChar) {
4735 if buf.is_null() {
4736 return;
4737 }
4738 io::buf_ccat(buf, b'"');
4739 if !str.is_null() {
4740 let mut i = 0usize;
4741 while unsafe { *str.add(i) != 0 } {
4742 let ch = unsafe { *str.add(i) };
4743 if ch == b'"' {
4744 io::buf_add(buf, b""" as *const u8, 6);
4745 } else {
4746 io::buf_add(buf, &ch as *const u8, 1);
4747 }
4748 i += 1;
4749 }
4750 }
4751 io::buf_ccat(buf, b'"');
4752}
4753
4754/// Dump an entity declaration (upstream `xmlBufDumpEntityDecl`).
4755///
4756/// # SAFETY
4757///
4758/// - `buf` must be valid; `ent` must be a valid `_xmlEntity` decl.
4759unsafe fn dump_entity_decl(buf: *mut _xmlBuffer, ent: *mut _xmlEntity) {
4760 use crate::abi::types::xmlEntityType::*;
4761 let e = unsafe { &*ent };
4762 if e.etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
4763 || e.etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
4764 {
4765 io::buf_add(buf, b"<!ENTITY % " as *const u8, 11);
4766 } else {
4767 io::buf_add(buf, b"<!ENTITY " as *const u8, 9);
4768 }
4769 if !e.name.is_null() {
4770 io::buf_cat(buf, e.name);
4771 }
4772 io::buf_ccat(buf, b' ');
4773
4774 if e.etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
4775 || e.etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int
4776 || e.etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
4777 {
4778 if !e.ExternalID.is_null() {
4779 io::buf_add(buf, b"PUBLIC " as *const u8, 7);
4780 write_quoted_string(buf, e.ExternalID);
4781 io::buf_ccat(buf, b' ');
4782 } else {
4783 io::buf_add(buf, b"SYSTEM " as *const u8, 7);
4784 }
4785 write_quoted_string(buf, e.SystemID);
4786 }
4787
4788 if e.etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int && !e.content.is_null() {
4789 io::buf_add(buf, b" NDATA " as *const u8, 7);
4790 if !e.orig.is_null() {
4791 io::buf_cat(buf, e.orig);
4792 } else if !e.content.is_null() {
4793 io::buf_cat(buf, e.content);
4794 }
4795 }
4796
4797 if e.etype == XML_INTERNAL_GENERAL_ENTITY as c_int
4798 || e.etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
4799 {
4800 if !e.orig.is_null() {
4801 write_quoted_string(buf, e.orig);
4802 } else {
4803 // Entity content is quoted, escaping `"` and `%`.
4804 io::buf_ccat(buf, b'"');
4805 if !e.content.is_null() {
4806 let mut i = 0usize;
4807 while unsafe { *e.content.add(i) != 0 } {
4808 let ch = unsafe { *e.content.add(i) };
4809 match ch {
4810 b'"' => io::buf_add(buf, b""" as *const u8, 6),
4811 b'%' => io::buf_add(buf, b"%" as *const u8, 6),
4812 _ => io::buf_add(buf, &ch as *const u8, 1),
4813 };
4814 i += 1;
4815 }
4816 }
4817 io::buf_ccat(buf, b'"');
4818 }
4819 }
4820 io::buf_add(buf, b">\n" as *const u8, 2);
4821}
4822
4823/// Dump a DTD node (upstream `xmlDtdDumpOutput`).
4824///
4825/// # SAFETY
4826///
4827/// - `buf` must be valid; `cur` must be a valid DTD node.
4828unsafe fn dtd_dump_output(
4829 buf: *mut _xmlBuffer,
4830 cur: *mut _xmlNode,
4831 state: &mut DumpState,
4832 level: &mut c_int,
4833) {
4834 let dtd = cur as *mut _xmlDtd;
4835 let d = unsafe { &*dtd };
4836 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
4837 if !d.name.is_null() {
4838 io::buf_cat(buf, d.name);
4839 }
4840 if !d.ExternalID.is_null() {
4841 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
4842 write_quoted_string(buf, d.ExternalID);
4843 io::buf_ccat(buf, b' ');
4844 write_quoted_string(buf, d.SystemID);
4845 } else if !d.SystemID.is_null() {
4846 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
4847 write_quoted_string(buf, d.SystemID);
4848 }
4849 // UPSTREAM-PARITY (xmlsave.c xmlDtdDumpOutput): the internal-subset
4850 // brackets are written only when a declaration table is non-NULL — an
4851 // empty DTD (all tables NULL) emits `>`. (hash_size() cannot be used
4852 // here: it returns -1 for NULL tables.)
4853 if d.entities.is_null()
4854 && d.elements.is_null()
4855 && d.attributes.is_null()
4856 && d.notations.is_null()
4857 && d.pentities.is_null()
4858 {
4859 io::buf_ccat(buf, b'>');
4860 return;
4861 }
4862 io::buf_add(buf, b" [\n" as *const u8, 3);
4863 // UPSTREAM-PARITY (xmlsave.c xmlDtdDumpOutput): the internal-subset
4864 // declarations are dumped from the DTD NODE's children list — the
4865 // declaration nodes are linked there in declaration order — NOT from the
4866 // hash tables (which upstream also keeps, but only for lookups; notations
4867 // are hash-only because they never join the child list). The old
4868 // hash-scan emitted declarations in hash-bucket order, which reversed
4869 // multi-declaration files (RESIDUAL R-DTD-DUMP-ORDER; ext/dom +
4870 // ext/simplexml xml_parsing_LIBXML_NO_XXE show `xxe` before `foo`).
4871 let format = state.format;
4872 let lvl = *level;
4873 state.format = 0;
4874 *level = -1;
4875 // Notations first: upstream xmlBufDumpNotationTable (hash-only table).
4876 if !d.notations.is_null() {
4877 crate::xml::hash::hash_scan(
4878 d.notations as *mut crate::xml::hash::HashTable,
4879 Some(dump_notation_decl_cb),
4880 buf as *mut c_void,
4881 );
4882 }
4883 // Declarations in child-list (declaration) order: element, attribute,
4884 // entity and parameter-entity declaration nodes.
4885 let mut decl = d.children;
4886 while !decl.is_null() {
4887 let dt = unsafe { (*decl).type_ };
4888 match dt {
4889 t if t == XML_ELEMENT_DECL as c_int => {
4890 dump_element_decl(buf, decl as *mut _xmlElement);
4891 }
4892 t if t == XML_ATTRIBUTE_DECL as c_int => {
4893 dump_attribute_decl(buf, decl as *mut _xmlAttribute);
4894 }
4895 t if t == XML_ENTITY_DECL as c_int => {
4896 dump_entity_decl(buf, decl as *mut _xmlEntity);
4897 }
4898 _ => {}
4899 }
4900 decl = unsafe { (*decl).next };
4901 }
4902 state.format = format;
4903 *level = lvl;
4904 io::buf_add(buf, b"]>" as *const u8, 2);
4905}
4906
4907/// Hash-scan callback for notation declarations.
4908unsafe extern "C" fn dump_notation_decl_cb(
4909 payload: *mut c_void,
4910 data: *mut c_void,
4911 _name: *const crate::abi::types::xmlChar,
4912) {
4913 if !payload.is_null() && !data.is_null() {
4914 dump_notation_decl(data as *mut _xmlBuffer, payload as *mut _xmlNotation);
4915 }
4916}
4917
4918/// Dump the content of a document (upstream `xmlSaveDocInternal`, XML path).
4919///
4920/// Writes the XML declaration (when not suppressed) followed by each child
4921/// separated by a newline.
4922///
4923/// # SAFETY
4924///
4925/// - `buf` must be valid; `cur` must be a valid document node.
4926unsafe fn doc_content_dump_output(
4927 buf: *mut _xmlBuffer,
4928 cur: *mut _xmlNode,
4929 state: &mut DumpState,
4930 level: &mut c_int,
4931) {
4932 let doc = cur as *mut _xmlDoc;
4933 let d = unsafe { &*doc };
4934
4935 // XML declaration: `<?xml version="..."?>` plus the encoding and
4936 // standalone attributes. The encoding is the save-context encoding when
4937 // one is set (upstream `ctxt->encoding`), falling back to the
4938 // document's own encoding (upstream xmlsave.c xmlSaveDocInternal:
4939 // `if (encoding == NULL) encoding = cur->encoding;`). Suppressed by the
4940 // XML_SAVE_NO_DECL save option (upstream `no_decl`).
4941 if state.no_decl == 0 {
4942 io::buf_add(buf, b"<?xml version=\"" as *const u8, 15);
4943 if !d.version.is_null() {
4944 io::buf_cat(buf, d.version);
4945 } else {
4946 io::buf_add(buf, b"1.0" as *const u8, 3);
4947 }
4948 io::buf_ccat(buf, b'"');
4949 let enc = if state.encoding.is_null() {
4950 d.encoding
4951 } else {
4952 state.encoding
4953 };
4954 if !enc.is_null() {
4955 io::buf_add(buf, b" encoding=\"" as *const u8, 11);
4956 io::buf_cat(buf, enc);
4957 io::buf_ccat(buf, b'"');
4958 }
4959 match d.standalone {
4960 0 => {
4961 io::buf_add(buf, b" standalone=\"no\"" as *const u8, 16);
4962 }
4963 1 => {
4964 io::buf_add(buf, b" standalone=\"yes\"" as *const u8, 17);
4965 }
4966 _ => {}
4967 }
4968 io::buf_add(buf, b"?>\n" as *const u8, 3);
4969 }
4970
4971 // UPSTREAM-PARITY (xmlsave.c xmlSaveDocInternal): the internal subset
4972 // DTD is a member of the children chain (xmlCreateIntSubset inserts it
4973 // before the first element), and the children loop below dumps it once.
4974 // Construction paths that keep the DTD only on doc->intSubset
4975 // (xmlCopyDoc, lazily-created subsets) need the explicit dump. Never
4976 // dump both — that double-prints <!DOCTYPE>. When doc->children is
4977 // EMPTY the DTD must NOT be dumped either: php's modern serializer
4978 // temporarily NULLs doc->children around xmlSaveDoc (to get a
4979 // declaration-only pass) and re-dumps the children itself — dumping the
4980 // intSubset there produced a duplicated <!DOCTYPE> (ext/dom +
4981 // ext/simplexml xml_parsing_LIBXML_NO_XXE).
4982 if !d.children.is_null() && !d.intSubset.is_null() {
4983 let mut in_chain = false;
4984 let mut c = d.children;
4985 while !c.is_null() {
4986 if c as *mut c_void == d.intSubset as *mut c_void {
4987 in_chain = true;
4988 break;
4989 }
4990 c = unsafe { (*c).next };
4991 }
4992 if !in_chain {
4993 let mut lvl = 0;
4994 dtd_dump_output(buf, d.intSubset as *mut _xmlNode, state, &mut lvl);
4995 io::buf_ccat(buf, b'\n');
4996 }
4997 }
4998
4999 if !d.children.is_null() {
5000 let mut child = d.children;
5001 while !child.is_null() {
5002 *level = 0;
5003 node_dump_internal(buf, child, child, cur, state, level);
5004 let ct = unsafe { (*child).type_ };
5005 if ct != XML_XINCLUDE_START as c_int && ct != XML_XINCLUDE_END as c_int {
5006 io::buf_ccat(buf, b'\n');
5007 }
5008 child = unsafe { (*child).next };
5009 }
5010 }
5011}
5012
5013/// Faithful port of libxml2's `xmlNodeDumpOutputInternal` (xmlsave.c 2.15).
5014///
5015/// Serializes `cur` and its descendants into `buf`. `root` is the node this
5016/// invocation started with: the root node itself is never indented, and no
5017/// trailing separator is emitted for it (the caller separates siblings).
5018/// `parent` is the expected parent of `cur`, used by the corrupted-tree
5019/// fallback.
5020///
5021/// # UPSTREAM-PARITY
5022///
5023/// - Indentation (two spaces per level, capped at 30 levels) is written
5024/// before every non-root element, PI and comment when formatting.
5025/// - An element whose children include a text, CDATA or entity-reference
5026/// node disables formatting for its whole content (the `unformattedNode`
5027/// mechanism); formatting is restored when its closing tag is emitted.
5028/// - `\n` separators between siblings are emitted after every child of a
5029/// formatted element (the upstream unwind loop).
5030///
5031/// # SAFETY
5032///
5033/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5034/// - `cur` must be a valid node pointer; `root`/`parent` must be stable
5035/// pointers into the same tree.
5036unsafe fn node_dump_internal(
5037 buf: *mut _xmlBuffer,
5038 cur: *mut _xmlNode,
5039 root: *mut _xmlNode,
5040 parent: *mut _xmlNode,
5041 state: &mut DumpState,
5042 level: &mut c_int,
5043) {
5044 if cur.is_null() || buf.is_null() {
5045 return;
5046 }
5047 let n = unsafe { &*cur };
5048 match n.type_ {
5049 t if t == XML_ELEMENT_NODE as c_int => {
5050 if cur != root && state.format == 1 {
5051 write_indent(buf, *level, state.indent, state.indent_len);
5052 }
5053 // Corrupted-tree fallback (upstream handles nodes passed with a
5054 // broken parent link by dumping the subtree as its own root).
5055 if !n.parent.is_null() && n.parent != parent && !n.children.is_null() {
5056 let mut sub = DumpState::new(state.format);
5057 let mut sub_level = *level;
5058 node_dump_internal(buf, cur, cur, n.parent, &mut sub, &mut sub_level);
5059 return;
5060 }
5061 // Start tag.
5062 io::buf_ccat(buf, b'<');
5063 write_qname(buf, cur);
5064 // UPSTREAM-PARITY (xmlsave.c xhtmlNodeDumpOutput): in XHTML mode
5065 // a bare <html> element (no ns, no nsDef) gets the XHTML default
5066 // namespace declaration.
5067 if state.xhtml
5068 && !n.name.is_null()
5069 && c_str_eq_bytes(n.name, b"html")
5070 && n.ns.is_null()
5071 && n.nsDef.is_null()
5072 {
5073 io::buf_add(
5074 buf,
5075 b" xmlns=\"http://www.w3.org/1999/xhtml\"" as *const u8,
5076 37,
5077 );
5078 }
5079 let mut nsdef = n.nsDef;
5080 while !nsdef.is_null() {
5081 ns_dump_output(buf, nsdef);
5082 nsdef = unsafe { (*nsdef).next };
5083 }
5084 let mut attr = n.properties;
5085 while !attr.is_null() {
5086 attr_dump_output(
5087 buf,
5088 attr,
5089 state.explicit_save
5090 && !state.as_html
5091 && save_escapes_non_ascii(state.encoding, unsafe { (*attr).doc }),
5092 );
5093 attr = unsafe { (*attr).next };
5094 }
5095 if n.children.is_null() {
5096 if state.as_html {
5097 // UPSTREAM-PARITY (nokogiri to_html / AS_HTML save): in
5098 // HTML output an empty HTML void element stays
5099 // `<br>`-style (no slash, no end tag); any other empty
5100 // element serializes as `<a></a>`.
5101 if xhtml_is_empty(n.name) {
5102 io::buf_ccat(buf, b'>');
5103 } else {
5104 io::buf_ccat(buf, b'>');
5105 io::buf_add(buf, b"</" as *const u8, 2);
5106 write_qname(buf, cur);
5107 io::buf_ccat(buf, b'>');
5108 }
5109 } else if state.no_empty {
5110 // UPSTREAM-PARITY (xmlsave.c XML_SAVE_NO_EMPTY): empty
5111 // elements get an explicit end tag.
5112 io::buf_ccat(buf, b'>');
5113 io::buf_add(buf, b"</" as *const u8, 2);
5114 write_qname(buf, cur);
5115 io::buf_ccat(buf, b'>');
5116 } else if state.xhtml && !xhtml_is_empty(n.name) {
5117 // UPSTREAM-PARITY (xhtmlNodeDumpOutput C.2): in XHTML
5118 // mode only the HTML-empty elements stay self-closing;
5119 // everything else (e.g. <html>) serializes as
5120 // open/close.
5121 io::buf_ccat(buf, b'>');
5122 io::buf_add(buf, b"</" as *const u8, 2);
5123 write_qname(buf, cur);
5124 io::buf_ccat(buf, b'>');
5125 } else {
5126 io::buf_add(buf, b"/>" as *const u8, 2);
5127 }
5128 } else {
5129 if state.format == 1 {
5130 // An element with text/CDATA/entity-ref children is
5131 // serialized unformatted (upstream unformattedNode).
5132 let mut tmp = n.children;
5133 while !tmp.is_null() {
5134 let tt = unsafe { (*tmp).type_ };
5135 if tt == XML_TEXT_NODE as c_int
5136 || tt == XML_CDATA_SECTION_NODE as c_int
5137 || tt == XML_ENTITY_REF_NODE as c_int
5138 {
5139 state.format = 0;
5140 state.unformatted = cur;
5141 break;
5142 }
5143 tmp = unsafe { (*tmp).next };
5144 }
5145 }
5146 io::buf_ccat(buf, b'>');
5147 if state.format == 1 {
5148 io::buf_ccat(buf, b'\n');
5149 }
5150 if *level >= 0 {
5151 *level += 1;
5152 }
5153 let mut child = n.children;
5154 while !child.is_null() {
5155 node_dump_internal(buf, child, root, cur, state, level);
5156 if state.format == 1 {
5157 let ct = unsafe { (*child).type_ };
5158 if ct != XML_XINCLUDE_START as c_int && ct != XML_XINCLUDE_END as c_int {
5159 io::buf_ccat(buf, b'\n');
5160 }
5161 }
5162 child = unsafe { (*child).next };
5163 }
5164 // Closing tag.
5165 if *level > 0 {
5166 *level -= 1;
5167 }
5168 if state.format == 1 {
5169 write_indent(buf, *level, state.indent, state.indent_len);
5170 }
5171 io::buf_add(buf, b"</" as *const u8, 2);
5172 write_qname(buf, cur);
5173 io::buf_ccat(buf, b'>');
5174 if cur == state.unformatted {
5175 state.format = state.saved;
5176 state.unformatted = ptr::null_mut();
5177 }
5178 }
5179 }
5180 t if t == XML_TEXT_NODE as c_int => {
5181 // UPSTREAM-PARITY (xmlsave.c xmlSaveWriteText): with no output
5182 // encoder on the save context, non-ASCII text is written as hex
5183 // character references. HTML-method output (XSLT method=html /
5184 // AS_HTML) writes raw like upstream's HTML serializer.
5185 let esc = state.explicit_save
5186 && !state.as_html
5187 && save_escapes_non_ascii(state.encoding, n.doc);
5188 if !n.content.is_null() {
5189 if is_noenc_text(cur) {
5190 io::buf_cat(buf, n.content);
5191 } else {
5192 serialize_text_flags(buf, n.content, xml_strlen(n.content), esc);
5193 }
5194 } else if !n.children.is_null() {
5195 // Non-compact text node (entity merge): content lives in a
5196 // child text node.
5197 let c = node_get_content(cur);
5198 if !c.is_null() {
5199 if is_noenc_text(cur) {
5200 io::buf_cat(buf, c);
5201 } else {
5202 serialize_text_flags(buf, c, xml_strlen(c), esc);
5203 }
5204 allocator::xmlFreeImpl(c as *mut c_void);
5205 }
5206 }
5207 }
5208 t if t == XML_CDATA_SECTION_NODE as c_int => {
5209 if n.content.is_null() || unsafe { *n.content == 0 } {
5210 io::buf_add(buf, b"<![CDATA[]]>" as *const u8, 12);
5211 } else {
5212 let len = xml_strlen(n.content) as usize;
5213 let bytes = core::slice::from_raw_parts(n.content, len);
5214 let mut i = 0usize;
5215 let mut seg_start = 0usize;
5216 while i < len {
5217 if bytes[i] == b']'
5218 && i + 2 < len
5219 && bytes[i + 1] == b']'
5220 && bytes[i + 2] == b'>'
5221 {
5222 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
5223 io::buf_add(buf, n.content.add(seg_start), (i + 2 - seg_start) as c_int);
5224 io::buf_add(buf, b"]]>" as *const u8, 3);
5225 seg_start = i + 2;
5226 i += 3;
5227 continue;
5228 }
5229 i += 1;
5230 }
5231 if seg_start < len {
5232 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
5233 io::buf_add(buf, n.content.add(seg_start), (len - seg_start) as c_int);
5234 io::buf_add(buf, b"]]>" as *const u8, 3);
5235 }
5236 }
5237 }
5238 t if t == XML_COMMENT_NODE as c_int => {
5239 if cur != root && state.format == 1 {
5240 write_indent(buf, *level, state.indent, state.indent_len);
5241 }
5242 if !n.content.is_null() {
5243 io::buf_add(buf, b"<!--" as *const u8, 4);
5244 io::buf_cat(buf, n.content);
5245 io::buf_add(buf, b"-->" as *const u8, 3);
5246 }
5247 }
5248 t if t == XML_PI_NODE as c_int => {
5249 if cur != root && state.format == 1 {
5250 write_indent(buf, *level, state.indent, state.indent_len);
5251 }
5252 io::buf_add(buf, b"<?" as *const u8, 2);
5253 if !n.name.is_null() {
5254 io::buf_cat(buf, n.name);
5255 }
5256 if !n.content.is_null() && unsafe { *n.content != 0 } {
5257 io::buf_ccat(buf, b' ');
5258 io::buf_cat(buf, n.content);
5259 }
5260 io::buf_add(buf, b"?>" as *const u8, 2);
5261 }
5262 t if t == XML_ENTITY_REF_NODE as c_int => {
5263 io::buf_ccat(buf, b'&');
5264 if !n.name.is_null() {
5265 io::buf_cat(buf, n.name);
5266 }
5267 io::buf_ccat(buf, b';');
5268 }
5269 t if t == XML_DOCUMENT_FRAG_NODE as c_int => {
5270 // UPSTREAM-PARITY (xmlsave.c 2.15 xmlNodeDumpOutputInternal
5271 // `case XML_DOCUMENT_FRAG_NODE:`): a document fragment is a
5272 // transparent container. Upstream trampolines from the fragment
5273 // into its children (validated against `cur->parent`) and dumps
5274 // them as free siblings — the fragment emits no tags, no XML
5275 // declaration, and no indentation of its own. Children keep the
5276 // caller/level semantics of the enclosing context.
5277 let mut child = n.children;
5278 while !child.is_null() {
5279 // Each child is dumped as its own root so a fragment's first
5280 // child gets no leading indent and the caller separates
5281 // sibling content (mirrors doc children handling).
5282 let mut sublevel = *level;
5283 node_dump_internal(buf, child, child, cur, state, &mut sublevel);
5284 child = unsafe { (*child).next };
5285 }
5286 }
5287 t if t == XML_DOCUMENT_NODE as c_int => {
5288 doc_content_dump_output(buf, cur, state, level);
5289 }
5290 t if t == XML_HTML_DOCUMENT_NODE as c_int => {
5291 // UPSTREAM-PARITY (xmlsave.c xmlSaveDocInternal): an HTML
5292 // document is serialized by the HTML serializer only when an
5293 // HTML/XHTML save was requested. Saved as XML (PHP
5294 // DOMDocument::saveXML passes XML_SAVE_AS_XML), the document is
5295 // dumped by the XML serializer — XML declaration (with
5296 // doc->standalone), XML escaping — which is what makes a
5297 // loadHTML()'d document saveXml() print `<?xml version="1.0"
5298 // standalone="yes"?>` (ext/dom dom005/gh15670/gh16535/...).
5299 if state.as_html {
5300 crate::xml::html::serialize_node(cur, buf, state.format, *level);
5301 } else {
5302 doc_content_dump_output(buf, cur, state, level);
5303 }
5304 }
5305 t if t == XML_DTD_NODE as c_int => {
5306 dtd_dump_output(buf, cur, state, level);
5307 }
5308 t if t == XML_ATTRIBUTE_NODE as c_int => {
5309 attr_dump_output(
5310 buf,
5311 cur as *mut _xmlAttr,
5312 save_escapes_non_ascii(state.encoding, unsafe { (*cur).doc }),
5313 );
5314 }
5315 t if t == XML_NAMESPACE_DECL as c_int => {
5316 ns_dump_output(buf, cur as *mut _xmlNs);
5317 }
5318 t if t == XML_ELEMENT_DECL as c_int => {
5319 dump_element_decl(buf, cur as *mut _xmlElement);
5320 }
5321 t if t == XML_ATTRIBUTE_DECL as c_int => {
5322 dump_attribute_decl(buf, cur as *mut _xmlAttribute);
5323 }
5324 t if t == XML_ENTITY_DECL as c_int => {
5325 dump_entity_decl(buf, cur as *mut _xmlEntity);
5326 }
5327 _ => {}
5328 }
5329}
5330
5331/// Recursively serialize a node tree to a buffer.
5332///
5333/// `buf` is an `_xmlBuffer*`, `format` controls indentation (non-zero = pretty-print).
5334///
5335/// # UPSTREAM-PARITY
5336///
5337/// Mirrors `xmlNodeDumpOutputInternal` (xmlsave.c 2.15): the node is treated
5338/// as the root of the dump (no leading indentation, no trailing separator).
5339///
5340/// # SAFETY
5341///
5342/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
5343/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5344pub(crate) unsafe fn serialize_node(
5345 node: *mut _xmlNode,
5346 buf: *mut _xmlBuffer,
5347 format: c_int,
5348 level: c_int,
5349) {
5350 unsafe { serialize_node_opt(node, buf, format, level, ptr::null()) };
5351}
5352
5353/// Like `serialize_node`, with a per-context indent string
5354/// (xmlSaveSetIndentString); NULL indent uses the default.
5355///
5356/// # SAFETY
5357///
5358/// - `indent` must be NULL or a valid NUL-terminated string that stays
5359/// alive for the whole dump.
5360pub(crate) unsafe fn serialize_node_opt(
5361 node: *mut _xmlNode,
5362 buf: *mut _xmlBuffer,
5363 format: c_int,
5364 level: c_int,
5365 indent: *const xmlChar,
5366) {
5367 unsafe { serialize_node_opts(node, buf, format, level, indent, 0) };
5368}
5369
5370/// Like `serialize_node_opt`, plus the XML_SAVE_NO_DECL flag.
5371///
5372/// # SAFETY
5373///
5374/// - `indent` must be NULL or a valid NUL-terminated string that stays
5375/// alive for the whole dump.
5376pub(crate) unsafe fn serialize_node_opts(
5377 node: *mut _xmlNode,
5378 buf: *mut _xmlBuffer,
5379 format: c_int,
5380 level: c_int,
5381 indent: *const xmlChar,
5382 no_decl: c_int,
5383) {
5384 unsafe { serialize_node_opts_enc(node, buf, format, level, indent, no_decl, ptr::null()) };
5385}
5386
5387/// Like `serialize_node_opts`, plus an encoding name for the XML
5388/// declaration (upstream `ctxt->encoding`; NULL = use `doc->encoding`).
5389///
5390/// # SAFETY
5391///
5392/// - `indent` must be NULL or a valid NUL-terminated string that stays
5393/// alive for the whole dump.
5394/// - `encoding` must be NULL or a valid NUL-terminated string that stays
5395/// alive for the whole dump.
5396pub(crate) unsafe fn serialize_node_opts_enc(
5397 node: *mut _xmlNode,
5398 buf: *mut _xmlBuffer,
5399 format: c_int,
5400 level: c_int,
5401 indent: *const xmlChar,
5402 no_decl: c_int,
5403 encoding: *const xmlChar,
5404) {
5405 unsafe {
5406 serialize_node_opts_xhtml(node, buf, format, level, indent, no_decl, encoding, false)
5407 };
5408}
5409
5410/// Serialize a node with the full save-option set (node, buffer, format,
5411/// level, indent, no-declaration, no-empty-tags, HTML mode, encoding).
5412/// Mirrors the fields nokogiri's `xmlSaveToIO`/`xmlSaveTree` path threads so
5413/// HTML serialization (`SaveOptions::AS_HTML`) controls empty-element output.
5414///
5415/// # SAFETY
5416///
5417/// - `node` must be NULL or a valid `_xmlNode`; `buf` a valid `_xmlBuffer`;
5418/// `indent`/`encoding` NULL or valid NUL-terminated strings.
5419#[allow(clippy::too_many_arguments)]
5420pub(crate) unsafe fn serialize_node_opts_enc_full(
5421 node: *mut _xmlNode,
5422 buf: *mut _xmlBuffer,
5423 format: c_int,
5424 level: c_int,
5425 indent: *const xmlChar,
5426 no_decl: c_int,
5427 no_empty: c_int,
5428 as_html: c_int,
5429 encoding: *const xmlChar,
5430) {
5431 unsafe {
5432 if node.is_null() || buf.is_null() {
5433 return;
5434 }
5435 let parent = (*node).parent;
5436 let mut state = DumpState::with_indent_enc(format, indent, no_decl, encoding);
5437 state.no_empty = no_empty != 0;
5438 state.as_html = as_html != 0;
5439 state.explicit_save = true;
5440 let mut lvl = level;
5441 node_dump_internal(buf, node, node, parent, &mut state, &mut lvl);
5442 }
5443}
5444
5445/// Serialize a node with full options plus XHTML mode (upstream
5446/// `xhtmlNodeDumpOutput`). When `xhtml` is set, a bare `<html>` element
5447/// receives the XHTML default namespace and non-HTML-empty elements are
5448/// serialized as open/close pairs.
5449///
5450/// # SAFETY
5451///
5452/// - `node` must be NULL or a valid `_xmlNode`; `buf` a valid `_xmlBuffer`;
5453/// `indent`/`encoding` NULL or valid NUL-terminated strings.
5454// The 8 parameters mirror the upstream xmlsave.c dump state (node, buffer,
5455// format, level, indent, no-declaration, encoding, xhtml mode) — the XHTML
5456// flag is threaded alongside the existing serializer state rather than
5457// through the DumpState struct to keep the xhtml gate visible at the call
5458// site.
5459#[allow(clippy::too_many_arguments)]
5460pub(crate) unsafe fn serialize_node_opts_xhtml(
5461 node: *mut _xmlNode,
5462 buf: *mut _xmlBuffer,
5463 format: c_int,
5464 level: c_int,
5465 indent: *const xmlChar,
5466 no_decl: c_int,
5467 encoding: *const xmlChar,
5468 xhtml: bool,
5469) {
5470 unsafe {
5471 if node.is_null() || buf.is_null() {
5472 return;
5473 }
5474 let parent = (*node).parent;
5475 let mut state = DumpState::with_indent_enc(format, indent, no_decl, encoding);
5476 state.xhtml = xhtml;
5477 let mut lvl = level;
5478 node_dump_internal(buf, node, node, parent, &mut state, &mut lvl);
5479 }
5480}
5481
5482/// Dump a document to a buffer.
5483///
5484/// Serializes the entire document tree into `buf`.
5485/// Returns the number of bytes written, or -1 on error.
5486///
5487/// # SAFETY
5488///
5489/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5490/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5491pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
5492 if buf.is_null() || doc.is_null() {
5493 return -1;
5494 }
5495
5496 let before = io::buf_length(buf);
5497 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
5498 let after = io::buf_length(buf);
5499
5500 if after < 0 || before < 0 {
5501 return -1;
5502 }
5503 after - before
5504}
5505
5506/// Dump a node tree to a buffer.
5507///
5508/// Serializes the node and its descendants into `buf`.
5509/// `level` is the initial indentation level, `format` controls pretty-printing.
5510/// Returns the number of bytes written, or -1 on error.
5511///
5512/// # SAFETY
5513///
5514/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5515/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5516/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
5517pub(crate) unsafe fn node_dump(
5518 buf: *mut _xmlBuffer,
5519 doc: *mut _xmlDoc,
5520 node: *mut _xmlNode,
5521 level: c_int,
5522 format: c_int,
5523) -> c_int {
5524 let _ = doc; // Used for entity resolution in full implementation
5525 if buf.is_null() || node.is_null() {
5526 return -1;
5527 }
5528
5529 let before = io::buf_length(buf);
5530 serialize_node(node, buf, format, level);
5531 let after = io::buf_length(buf);
5532
5533 if after < 0 || before < 0 {
5534 return -1;
5535 }
5536 after - before
5537}
5538
5539/// Save a document to a file descriptor.
5540///
5541/// # SAFETY
5542///
5543/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5544/// - `fd` must be a valid open file descriptor.
5545#[allow(dead_code)]
5546pub(crate) unsafe fn save_doc_to_fd(doc: *mut _xmlDoc, fd: c_int, _compression: c_int) -> c_int {
5547 if doc.is_null() || fd < 0 {
5548 return -1;
5549 }
5550
5551 let out = io::output_buffer_create_fd(fd, ptr::null_mut());
5552 if out.is_null() {
5553 return -1;
5554 }
5555
5556 let buf = io::buf_create(-1);
5557 if buf.is_null() {
5558 io::output_buffer_close(out);
5559 return -1;
5560 }
5561
5562 let ret = doc_dump(buf, doc);
5563 if ret >= 0 {
5564 io::output_buffer_write_string(out, io::buf_content(buf) as *const c_char);
5565 io::output_buffer_flush(out);
5566 }
5567
5568 io::buf_free(buf);
5569 io::output_buffer_close(out);
5570 ret
5571}
5572
5573/// Save a document to an xmlBuffer.
5574///
5575/// # SAFETY
5576///
5577/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5578/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5579#[allow(dead_code)]
5580pub(crate) unsafe fn save_doc_to_buf(
5581 doc: *mut _xmlDoc,
5582 buf: *mut _xmlBuffer,
5583 compression: c_int,
5584) -> c_int {
5585 let _ = compression;
5586 if doc.is_null() || buf.is_null() {
5587 return -1;
5588 }
5589
5590 doc_dump(buf, doc)
5591}
5592
5593/// Format (pretty-print) a document to a buffer.
5594///
5595/// # SAFETY
5596///
5597/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5598/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
5599#[allow(dead_code)]
5600pub(crate) unsafe fn save_format_doc_to_buf(
5601 doc: *mut _xmlDoc,
5602 buf: *mut _xmlBuffer,
5603 compression: c_int,
5604) -> c_int {
5605 let _ = compression;
5606 if doc.is_null() || buf.is_null() {
5607 return -1;
5608 }
5609
5610 let before = io::buf_length(buf);
5611 serialize_node(doc as *mut _xmlNode, buf, 1, 0);
5612 let after = io::buf_length(buf);
5613
5614 if after < 0 || before < 0 {
5615 return -1;
5616 }
5617 after - before
5618}
5619
5620/// Dump a node to a null-terminated string.
5621///
5622/// Returns a pointer to the string (caller must free with `xmlFree`).
5623/// Returns NULL on error.
5624///
5625/// # SAFETY
5626///
5627/// - `node` must be a valid pointer to an `_xmlNode`, or NULL.
5628#[allow(dead_code)]
5629pub(crate) unsafe fn dump_node(node: *mut _xmlNode) -> *mut xmlChar {
5630 if node.is_null() {
5631 return ptr::null_mut();
5632 }
5633
5634 let buf = io::buf_create(-1);
5635 if buf.is_null() {
5636 return ptr::null_mut();
5637 }
5638
5639 serialize_node(node, buf, 0, 0);
5640
5641 let content = io::buf_content(buf);
5642 if content.is_null() {
5643 io::buf_free(buf);
5644 return ptr::null_mut();
5645 }
5646
5647 // Duplicate the string so we can free the buffer
5648 let result = dup_xml_str(content);
5649 io::buf_free(buf);
5650 result
5651}
5652
5653/// Dump a document to a null-terminated string.
5654///
5655/// Returns a pointer to the string (caller must free with `xmlFree`).
5656/// Returns NULL on error.
5657///
5658/// # SAFETY
5659///
5660/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5661pub unsafe fn dump_doc(doc: *mut _xmlDoc) -> *mut xmlChar {
5662 if doc.is_null() {
5663 return ptr::null_mut();
5664 }
5665
5666 let buf = io::buf_create(-1);
5667 if buf.is_null() {
5668 return ptr::null_mut();
5669 }
5670
5671 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
5672
5673 let content = io::buf_content(buf);
5674 if content.is_null() {
5675 io::buf_free(buf);
5676 return ptr::null_mut();
5677 }
5678
5679 let result = dup_xml_str(content);
5680 io::buf_free(buf);
5681 result
5682}
5683
5684// ═══════════════════════════════════════════════════════════════════════════════
5685// ABI-compatible export wrappers
5686// ═══════════════════════════════════════════════════════════════════════════════
5687
5688/// Dump a node to a buffer (ABI wrapper).
5689///
5690/// # UPSTREAM-PARITY
5691///
5692/// ```c
5693/// int xmlNodeDump(xmlBufferPtr buf, xmlDocPtr doc, xmlNodePtr node, int level, int format);
5694/// ```
5695///
5696/// # SAFETY
5697///
5698/// - All pointer arguments must be valid or NULL.
5699pub(crate) unsafe fn xmlNodeDump(
5700 buf: *mut _xmlBuffer,
5701 doc: *mut _xmlDoc,
5702 node: *mut _xmlNode,
5703 level: c_int,
5704 format: c_int,
5705) -> c_int {
5706 node_dump(buf, doc, node, level, format)
5707}
5708
5709/// Dump a document to a FILE*.
5710///
5711/// # UPSTREAM-PARITY
5712///
5713/// ```c
5714/// int xmlDocDump(FILE *fp, xmlDocPtr doc);
5715/// ```
5716///
5717/// # SAFETY
5718///
5719/// - `fp` must be a valid FILE* pointer.
5720/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5721pub(crate) unsafe fn xmlDocDump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
5722 if fp.is_null() || doc.is_null() {
5723 return -1;
5724 }
5725
5726 let buf = io::buf_create(-1);
5727 if buf.is_null() {
5728 return -1;
5729 }
5730
5731 let ret = doc_dump(buf, doc);
5732 if ret < 0 {
5733 io::buf_free(buf);
5734 return -1;
5735 }
5736
5737 let content = io::buf_content(buf);
5738 let len = io::buf_length(buf);
5739 if !content.is_null() && len > 0 {
5740 let written = libc::fwrite(
5741 content as *const c_void,
5742 1,
5743 len as usize,
5744 fp as *mut libc::FILE,
5745 );
5746 io::buf_free(buf);
5747 written as c_int
5748 } else {
5749 io::buf_free(buf);
5750 0
5751 }
5752}
5753
5754/// Dump a document to memory (with format flag).
5755///
5756/// # UPSTREAM-PARITY
5757///
5758/// ```c
5759/// void xmlDocDumpFormatMemory(xmlDocPtr doc, xmlChar **mem, int *size, int format);
5760/// ```
5761///
5762/// # SAFETY
5763///
5764/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5765/// - `mem` must be a valid pointer to an xmlChar* that will receive the
5766/// allocated memory.
5767/// - `size` may be NULL (upstream tree.c xmlDocDumpFormatMemoryEnc only
5768/// writes the length when the pointer is given; `mem` is still produced —
5769/// the recover-cont-probe passes NULL for the length).
5770pub(crate) unsafe fn xmlDocDumpFormatMemory(
5771 doc: *mut _xmlDoc,
5772 mem: *mut *mut xmlChar,
5773 size: *mut c_int,
5774 format: c_int,
5775) {
5776 if doc.is_null() || mem.is_null() {
5777 return;
5778 }
5779
5780 let buf = io::buf_create(-1);
5781 if buf.is_null() {
5782 unsafe {
5783 *mem = ptr::null_mut();
5784 if !size.is_null() {
5785 *size = 0;
5786 }
5787 }
5788 return;
5789 }
5790
5791 serialize_node(doc as *mut _xmlNode, buf, format, 0);
5792
5793 let content = io::buf_content(buf);
5794 let len = io::buf_length(buf);
5795
5796 if !content.is_null() && len > 0 {
5797 // Allocate memory for the result (+1 for null terminator)
5798 let result = allocator::xmlMallocImpl((len + 1) as usize) as *mut xmlChar;
5799 if !result.is_null() {
5800 ptr::copy_nonoverlapping(content, result, len as usize);
5801 *result.add(len as usize) = 0;
5802 unsafe {
5803 *mem = result;
5804 if !size.is_null() {
5805 *size = len;
5806 }
5807 }
5808 } else {
5809 unsafe {
5810 *mem = ptr::null_mut();
5811 if !size.is_null() {
5812 *size = 0;
5813 }
5814 }
5815 }
5816 } else {
5817 unsafe {
5818 *mem = ptr::null_mut();
5819 if !size.is_null() {
5820 *size = 0;
5821 }
5822 }
5823 }
5824
5825 io::buf_free(buf);
5826}
5827
5828/// Dump a document to memory (unformatted).
5829///
5830/// # UPSTREAM-PARITY
5831///
5832/// ```c
5833/// void xmlDocDumpMemory(xmlDocPtr doc, xmlChar **mem, int *size);
5834/// ```
5835///
5836/// # SAFETY
5837///
5838/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5839/// - `mem` must be a valid pointer to an xmlChar* that will receive the allocated memory.
5840/// - `size` may be NULL (upstream tree.c xmlDocDumpFormatMemoryEnc).
5841pub(crate) unsafe fn xmlDocDumpMemory(doc: *mut _xmlDoc, mem: *mut *mut xmlChar, size: *mut c_int) {
5842 xmlDocDumpFormatMemory(doc, mem, size, 0)
5843}
5844
5845/// Save a document to a file (ABI wrapper). Upstream tree.c 2.15:
5846/// `xmlSaveFile` = `xmlSaveFormatFileEnc(filename, cur, NULL, 0)`.
5847///
5848/// # UPSTREAM-PARITY
5849///
5850/// ```c
5851/// int xmlSaveFile(const char *filename, xmlDocPtr cur);
5852/// ```
5853///
5854/// # SAFETY
5855///
5856/// - `filename` must be a valid null-terminated C string.
5857/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
5858pub(crate) unsafe fn xmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
5859 unsafe { xmlSaveFormatFileEnc(filename, cur, ptr::null(), 0) }
5860}
5861
5862/// Save a document to a file with encoding. Upstream tree.c 2.15:
5863/// `xmlSaveFileEnc` = `xmlSaveFormatFileEnc(filename, cur, encoding, 0)`.
5864///
5865/// # UPSTREAM-PARITY
5866///
5867/// ```c
5868/// int xmlSaveFileEnc(const char *filename, xmlDocPtr cur, const char *encoding);
5869/// ```
5870///
5871/// # SAFETY
5872///
5873/// - `filename` must be a valid null-terminated C string.
5874/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
5875/// - `encoding` may be NULL (uses UTF-8).
5876pub(crate) unsafe fn xmlSaveFileEnc(
5877 filename: *const c_char,
5878 cur: *mut _xmlDoc,
5879 encoding: *const c_char,
5880) -> c_int {
5881 unsafe { xmlSaveFormatFileEnc(filename, cur, encoding, 0) }
5882}
5883
5884/// Save a document to a file with format flag. Upstream tree.c 2.15:
5885/// `xmlSaveFormatFile` = `xmlSaveFormatFileEnc(filename, cur, NULL, format)`.
5886///
5887/// # UPSTREAM-PARITY
5888///
5889/// ```c
5890/// int xmlSaveFormatFile(const char *filename, xmlDocPtr cur, int format);
5891/// ```
5892///
5893/// # SAFETY
5894///
5895/// - `filename` must be a valid null-terminated C string.
5896/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
5897pub(crate) unsafe fn xmlSaveFormatFile(
5898 filename: *const c_char,
5899 cur: *mut _xmlDoc,
5900 format: c_int,
5901) -> c_int {
5902 unsafe { xmlSaveFormatFileEnc(filename, cur, ptr::null(), format) }
5903}
5904
5905/// Save a document to a file with encoding and format flag.
5906///
5907/// Mirrors upstream xmlsave.c 2.15 `xmlSaveFormatFileEnc`: create the output
5908/// buffer ("-" maps to stdout like the oracle), serialize through the save
5909/// machinery (formatting + the encoding declaration), and return the close
5910/// result. The save context's encoding is emitted in the XML declaration and
5911/// drives the output-buffer encoder exactly like upstream xmlDocDumpInternal.
5912///
5913/// # UPSTREAM-PARITY
5914///
5915/// ```c
5916/// int xmlSaveFormatFileEnc(const char *filename, xmlDocPtr cur, const char *encoding, int format);
5917/// ```
5918///
5919/// # SAFETY
5920///
5921/// - `filename` must be a valid null-terminated C string.
5922/// - `cur` must be a valid pointer to an `_xmlDoc`, or NULL.
5923/// - `encoding` may be NULL (uses UTF-8).
5924pub(crate) unsafe fn xmlSaveFormatFileEnc(
5925 filename: *const c_char,
5926 cur: *mut _xmlDoc,
5927 encoding: *const c_char,
5928 format: c_int,
5929) -> c_int {
5930 if cur.is_null() {
5931 return -1;
5932 }
5933 let options = if format != 0 {
5934 crate::xml::save::XML_SAVE_FORMAT
5935 } else {
5936 0
5937 };
5938 let ctxt = crate::xml::save::xmlSaveToFilename(filename, encoding, options);
5939 if ctxt.is_null() {
5940 return -1;
5941 }
5942 crate::xml::save::xmlSaveDoc(ctxt, cur);
5943 crate::xml::save::xmlSaveClose(ctxt)
5944}
5945
5946/// Get the compression mode of a document.
5947///
5948/// # UPSTREAM-PARITY
5949///
5950/// ```c
5951/// int xmlGetDocCompressMode(xmlDocPtr doc);
5952/// ```
5953///
5954/// # SAFETY
5955///
5956/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5957pub(crate) unsafe fn xmlGetDocCompressMode(doc: *mut _xmlDoc) -> c_int {
5958 if doc.is_null() {
5959 return -1;
5960 }
5961 unsafe { (*doc).compression }
5962}
5963
5964/// Set the compression mode of a document.
5965///
5966/// # UPSTREAM-PARITY
5967///
5968/// ```c
5969/// void xmlSetDocCompressMode(xmlDocPtr doc, int mode);
5970/// ```
5971///
5972/// # SAFETY
5973///
5974/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
5975pub(crate) unsafe fn xmlSetDocCompressMode(doc: *mut _xmlDoc, mode: c_int) {
5976 if doc.is_null() {
5977 return;
5978 }
5979 unsafe {
5980 (*doc).compression = mode;
5981 }
5982}
5983
5984#[cfg(test)]
5985mod tests {
5986 use super::*;
5987 use core::ffi::c_void;
5988
5989 /// Helper: allocate a NUL-terminated xmlChar copy of `s`.
5990 ///
5991 /// # Safety
5992 ///
5993 /// - The returned buffer is heap-allocated with `xmlMallocImpl` and must
5994 /// be freed by the caller with `xmlFreeImpl`; it may be NULL on OOM, so
5995 /// callers check it before use.
5996 fn c_str(s: &str) -> *const xmlChar {
5997 let bytes = s.as_bytes();
5998 let buf = unsafe { allocator::xmlMallocImpl(bytes.len() + 1) as *mut u8 };
5999 if !buf.is_null() {
6000 unsafe {
6001 ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
6002 *buf.add(bytes.len()) = 0;
6003 }
6004 }
6005 buf as *const xmlChar
6006 }
6007
6008 /// Verify creating and freeing a document.
6009 ///
6010 /// # Safety
6011 ///
6012 /// - `doc` from `new_doc` must be a valid `_xmlDoc` while its fields are
6013 /// read; it is freed with `free_doc`.
6014 #[test]
6015 fn test_new_free_doc() {
6016 unsafe {
6017 let doc = new_doc(ptr::null());
6018 assert!(!doc.is_null());
6019 assert_eq!((*doc).type_, XML_DOCUMENT_NODE as c_int);
6020 assert_eq!((*doc).standalone, -1);
6021 assert_eq!((*doc).doc, doc);
6022 assert!(!(*doc).version.is_null());
6023 free_doc(doc);
6024 }
6025 }
6026
6027 /// Verify creating a document with a version string.
6028 ///
6029 /// # Safety
6030 ///
6031 /// - The `c_str` buffer must be NUL-terminated and alive while `new_doc`
6032 /// duplicates it; the buffer is freed with `xmlFreeImpl` and the doc
6033 /// with `free_doc`.
6034 #[test]
6035 fn test_new_doc_with_version() {
6036 unsafe {
6037 let ver = c_str("2.0");
6038 let doc = new_doc(ver);
6039 assert!(!doc.is_null());
6040 let doc_ver = (*doc).version;
6041 assert!(!doc_ver.is_null());
6042 assert!(crate::abi::exports_xml2::xmlStrEqual(doc_ver, ver,) != 0);
6043 allocator::xmlFreeImpl(ver as *mut c_void);
6044 free_doc(doc);
6045 }
6046 }
6047
6048 /// Verify creating a node.
6049 ///
6050 /// # Safety
6051 ///
6052 /// - The `c_str` buffer must be NUL-terminated and alive while `new_node`
6053 /// duplicates it; the node is freed with `free_node` and the doc with
6054 /// `free_doc`.
6055 #[test]
6056 fn test_new_node() {
6057 unsafe {
6058 let doc = new_doc(ptr::null());
6059 let node = new_node(ptr::null_mut(), c_str("root"));
6060 assert!(!node.is_null());
6061 assert_eq!((*node).type_, XML_ELEMENT_NODE as c_int);
6062 assert!(!(*node).name.is_null());
6063 free_node(node);
6064 free_doc(doc);
6065 }
6066 }
6067
6068 /// Verify that `node_get_content` concatenates all descendant text.
6069 ///
6070 /// # Safety
6071 ///
6072 /// - The doc and nodes built by the tree helpers must be valid and linked
6073 /// before `node_get_content` walks them; `content` is read with
6074 /// `strlen` and freed with `xmlFreeImpl`, and the doc with `free_doc`.
6075 #[test]
6076 fn test_node_get_content_recurses_descendants() {
6077 // UPSTREAM-PARITY: xmlNodeGetContent (tree.c) concatenates ALL
6078 // descendant text, not just direct text children — the XPath 1.0
6079 // string-value of an element. Regression test for the Phase 9 fix
6080 // where <book><title>Rust</title></book> produced empty content.
6081 unsafe {
6082 let doc = new_doc(ptr::null());
6083 let root = new_node(ptr::null_mut(), c_str("library"));
6084 doc_set_root_element(doc, root);
6085 let book = new_child(root, ptr::null_mut(), c_str("book"));
6086 let title = new_child(book, ptr::null_mut(), c_str("title"));
6087 let text = new_text(c_str("Rust"));
6088 add_child(title, text);
6089
6090 let content = node_get_content(book);
6091 assert!(!content.is_null());
6092 let s = core::slice::from_raw_parts(
6093 content,
6094 libc::strlen(content as *const libc::c_char) as usize,
6095 );
6096 assert_eq!(s, b"Rust", "descendant text not concatenated");
6097 allocator::xmlFreeImpl(content as *mut c_void);
6098
6099 free_doc(doc);
6100 }
6101 }
6102
6103 /// Verify setting the root element of a document.
6104 ///
6105 /// # Safety
6106 ///
6107 /// - `doc` and `root` must be valid pointers while `doc_set_root_element`
6108 /// relinks them; the doc is freed with `free_doc`.
6109 #[test]
6110 fn test_doc_set_root_element() {
6111 unsafe {
6112 let doc = new_doc(ptr::null());
6113 let root = new_node(ptr::null_mut(), c_str("root"));
6114 let old = doc_set_root_element(doc, root);
6115 assert!(old.is_null());
6116 assert_eq!(doc_get_root_element(doc), root);
6117 assert_eq!((*doc).children, root);
6118 free_doc(doc);
6119 }
6120 }
6121
6122 /// Verify adding children and siblings.
6123 ///
6124 /// # Safety
6125 ///
6126 /// - The doc and nodes built by the tree helpers must be valid and linked
6127 /// before their `parent`, `children`, `last`, `next`, and `prev` fields
6128 /// are read; the doc is freed with `free_doc`.
6129 #[test]
6130 fn test_add_child_and_sibling() {
6131 unsafe {
6132 let doc = new_doc(ptr::null());
6133 let root = new_node(ptr::null_mut(), c_str("root"));
6134 doc_set_root_element(doc, root);
6135
6136 let child1 = new_child(root, ptr::null_mut(), c_str("child1"));
6137 assert!(!child1.is_null());
6138 assert_eq!((*child1).parent, root);
6139 assert_eq!((*root).children, child1);
6140 assert_eq!((*root).last, child1);
6141
6142 let child2 = new_child(root, ptr::null_mut(), c_str("child2"));
6143 assert!(!child2.is_null());
6144 assert_eq!((*child2).parent, root);
6145 assert_eq!((*child1).next, child2);
6146 assert_eq!((*child2).prev, child1);
6147 assert_eq!((*root).last, child2);
6148
6149 // Test add_sibling
6150 let sibling = new_node(ptr::null_mut(), c_str("sibling"));
6151 add_sibling(child2, sibling);
6152 assert_eq!((*child2).next, sibling);
6153 assert_eq!((*sibling).prev, child2);
6154 assert_eq!((*root).last, sibling);
6155
6156 free_doc(doc);
6157 }
6158 }
6159
6160 /// Phase 14 PHP DOM regression (domattributes): attaching an ATTRIBUTE
6161 /// node with `xmlAddChild(element, attr)` must route it into the element's
6162 /// PROPERTIES list, not its children list. php's `element->setAttributeNode`
6163 /// (new DOMAttr + setAttributeNode) does exactly this via xmlAddChild. The
6164 /// pre-fix behaviour appended the attribute to `children` — the attribute
6165 /// serialized as a bogus child text node and doubly freed on teardown.
6166 ///
6167 /// # Safety
6168 ///
6169 /// - `root`/`attr` are built and linked as in upstream: an element under a
6170 /// doc, and a standalone attr from `xmlNewProp(NULL, ...)`, then attached
6171 /// with `add_child`. The doc is freed once with `free_doc` (proving no
6172 /// double free of the attached attribute).
6173 #[test]
6174 fn test_add_child_attribute_goes_to_properties() {
6175 unsafe {
6176 let doc = new_doc(ptr::null());
6177 let root = new_node(ptr::null_mut(), c_str("chapter"));
6178 doc_set_root_element(doc, root);
6179
6180 // Mirror php DOMAttr::__construct: a standalone (doc NULL,
6181 // unlinked) attribute with a text value.
6182 let attr = crate::abi::exports_tree::xmlNewProp(
6183 ptr::null_mut(),
6184 c"num".as_ptr() as *const crate::abi::types::xmlChar,
6185 c"1".as_ptr() as *const crate::abi::types::xmlChar,
6186 );
6187 assert!(!attr.is_null());
6188 assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
6189
6190 // xmlAddChild(element, attr) — upstream routes attrs to properties.
6191 let ret = add_child(root, attr as *mut _xmlNode);
6192 assert_eq!(ret, attr as *mut _xmlNode);
6193 assert_eq!((*root).properties, attr);
6194 assert!(
6195 (*root).children.is_null(),
6196 "attr must NOT become a child node"
6197 );
6198 assert!(!(*attr).children.is_null());
6199 assert_eq!((*(*attr).children).parent, attr as *mut _xmlNode);
6200
6201 // Crucially: the doc teardown must not double-free the attribute.
6202 free_doc(doc);
6203 }
6204 }
6205
6206 /// Phase 14 PHP DOM regression (modern/spec serialize_*_xmlns and the
6207 /// modern Dom\XMLDocument namespace mapper): xmlSetNsProp must bind the new
6208 /// attribute to the given `ns`, not drop it. php's modern DOM materialises
6209 /// xmlns declarations as real attributes in the XMLNS namespace via
6210 /// xmlSetNsProp(node, xmlnsNs, prefix|xmlns, href); the legacy stub ignored
6211 /// `ns` and created an UNNAMESPACED `a="urn:a"` attribute, so the doc
6212 /// serialized with a spurious bare attribute and a missing/duplicated
6213 /// namespace declaration.
6214 ///
6215 /// # Safety
6216 ///
6217 /// - The doc/root/ns are valid while `set_ns_prop` runs; the namespace is
6218 /// a detached, dedicated declaration on `root`; the doc is freed with
6219 /// `free_doc`.
6220 #[test]
6221 fn test_set_ns_prop_binds_namespace() {
6222 unsafe {
6223 let doc = new_doc(ptr::null());
6224 let root = new_node(ptr::null_mut(), c_str("root"));
6225 doc_set_root_element(doc, root);
6226
6227 // xmlNewNs(node, href, prefix): declares xmlns:a="urn:a" on root.
6228 let ns = new_ns(root, c_str("urn:a"), c_str("a"));
6229
6230 let attr = set_ns_prop(root, ns, c_str("a"), c_str("urn:a"));
6231 assert!(!attr.is_null());
6232 assert_eq!((*root).properties, attr);
6233 // CRITICAL: the attribute must carry the namespace, not be bare.
6234 assert_eq!((*attr).ns, ns);
6235 assert!(!(*attr).name.is_null());
6236
6237 free_doc(doc);
6238 }
6239 }
6240
6241 /// Phase 14 PHP DOM regression (gh12870_b): searching for the reserved XML
6242 /// namespace URI must bind prefix `xml` (it is implicitly in scope), even
6243 /// on a freshly built document with no xmlns:xml declaration. Without the
6244 /// doc->oldNs fallback php DOMDocument::createAttributeNS
6245 /// ('http://www.w3.org/XML/1998/namespace', 'xml') resolved to a synthetic
6246 /// "default" prefix instead of the fixed xml prefix.
6247 ///
6248 /// # Safety
6249 ///
6250 /// - The doc/root are valid while search_ns_by_href runs; the doc is freed
6251 /// with `free_doc` (freeing the materialised doc-level xml declaration).
6252 #[test]
6253 fn test_search_ns_by_href_xml_namespace_returns_xml_prefix() {
6254 unsafe {
6255 let doc = new_doc(ptr::null());
6256 let root = new_node(ptr::null_mut(), c_str("root"));
6257 doc_set_root_element(doc, root);
6258 // root->doc is set by doc_set_root_element; documentElement::first
6259 // usage in php starts from root. Query must succeed.
6260 let found = search_ns_by_href(
6261 doc,
6262 root,
6263 c"http://www.w3.org/XML/1998/namespace".as_ptr() as *const xmlChar,
6264 );
6265 assert!(!found.is_null());
6266 assert!(!(*found).prefix.is_null());
6267 assert!(
6268 crate::abi::exports_xml2::xmlStrEqual(
6269 (*found).prefix,
6270 c"xml".as_ptr() as *const xmlChar
6271 ) != 0
6272 );
6273 free_doc(doc);
6274 }
6275 }
6276
6277 /// Phase 14 PHP DOM regression (modern/spec Node_isDefaultNamespace): text,
6278 /// CDATA, comment and PI nodes may carry their `name` as the SHARED static
6279 /// marker `xmlStringText`/`xmlStringComment` (upstream tree.c xmlNewText/
6280 /// xmlNewComment). `free_node` must not `xmlFree` such a name or teardown
6281 /// aborts with `free(): invalid pointer`. Regression guard for the sentinel
6282 /// name-free skip.
6283 ///
6284 /// # Safety
6285 ///
6286 /// - The node is a freshly xmlMalloc'd text node, fully zero-initialised
6287 /// except its type and sentinel `name`; it owns no other allocations, so
6288 /// `free_node` releases only the struct.
6289 #[test]
6290 fn test_free_text_node_with_static_name_sentinel() {
6291 unsafe {
6292 let node = allocator::xmlMallocZero(size_of::<_xmlNode>()) as *mut _xmlNode;
6293 assert!(!node.is_null());
6294 (*node).type_ = XML_TEXT_NODE as c_int;
6295 // A text node whose name aliases the shared static marker text.
6296 (*node).name = crate::abi::data_globals::xmlStringText.as_ptr();
6297 // no children/content owned by the node.
6298 free_node(node); // must not free the static marker.
6299 }
6300 }
6301
6302 /// Verify unlinking a node.
6303 ///
6304 /// # Safety
6305 ///
6306 /// - The doc and nodes must be valid while `unlink_node` rewrites the
6307 /// sibling links; the unlinked node is freed with `free_node` and the
6308 /// doc with `free_doc`.
6309 #[test]
6310 fn test_unlink_node() {
6311 unsafe {
6312 let doc = new_doc(ptr::null());
6313 let root = new_node(ptr::null_mut(), c_str("root"));
6314 doc_set_root_element(doc, root);
6315
6316 let child1 = new_child(root, ptr::null_mut(), c_str("c1"));
6317 let child2 = new_child(root, ptr::null_mut(), c_str("c2"));
6318
6319 unlink_node(child1);
6320 assert!((*child1).parent.is_null());
6321 assert!((*child1).prev.is_null());
6322 assert!((*child1).next.is_null());
6323 assert_eq!((*root).children, child2);
6324 assert_eq!((*root).last, child2);
6325
6326 free_node(child1);
6327 free_doc(doc);
6328 }
6329 }
6330
6331 /// Verify creating text, comment, and PI nodes.
6332 ///
6333 /// # Safety
6334 ///
6335 /// - The `c_str` buffers must be NUL-terminated and alive while the
6336 /// creators duplicate them; each node is freed with `free_node`.
6337 #[test]
6338 fn test_text_and_comment_nodes() {
6339 unsafe {
6340 let text = new_text(c_str("hello world"));
6341 assert!(!text.is_null());
6342 assert_eq!((*text).type_, XML_TEXT_NODE as c_int);
6343 assert!(!(*text).content.is_null());
6344 free_node(text);
6345
6346 let comment = new_comment(c_str("my comment"));
6347 assert!(!comment.is_null());
6348 assert_eq!((*comment).type_, XML_COMMENT_NODE as c_int);
6349 free_node(comment);
6350
6351 let pi = new_pi(c_str("xml"), c_str("version='1.0'"));
6352 assert!(!pi.is_null());
6353 assert_eq!((*pi).type_, XML_PI_NODE as c_int);
6354 free_node(pi);
6355 }
6356 }
6357
6358 /// Verify setting and getting a property.
6359 ///
6360 /// # Safety
6361 ///
6362 /// - `doc` and `root` must be valid while `set_prop` and `get_prop` run;
6363 /// the value returned by `get_prop` is freed with `xmlFreeImpl` and the
6364 /// doc with `free_doc`.
6365 #[test]
6366 fn test_set_and_get_prop() {
6367 unsafe {
6368 let doc = new_doc(ptr::null());
6369 let root = new_node(ptr::null_mut(), c_str("root"));
6370 doc_set_root_element(doc, root);
6371
6372 let attr = set_prop(root, c_str("id"), c_str("42"));
6373 assert!(!attr.is_null());
6374 assert_eq!((*attr).type_, XML_ATTRIBUTE_NODE as c_int);
6375
6376 let value = get_prop(root, c_str("id"));
6377 assert!(!value.is_null());
6378 assert!(crate::abi::exports_xml2::xmlStrEqual(value, c_str("42")) != 0);
6379 allocator::xmlFreeImpl(value as *mut c_void);
6380
6381 free_doc(doc);
6382 }
6383 }
6384
6385 /// Verify removing a property.
6386 ///
6387 /// # Safety
6388 ///
6389 /// - `doc` and `root` must be valid while `set_prop`, `get_prop`, and
6390 /// `remove_prop` run; values returned by `get_prop` are freed with
6391 /// `xmlFreeImpl` and the doc with `free_doc`.
6392 #[test]
6393 fn test_remove_prop() {
6394 unsafe {
6395 let doc = new_doc(ptr::null());
6396 let root = new_node(ptr::null_mut(), c_str("root"));
6397 doc_set_root_element(doc, root);
6398
6399 set_prop(root, c_str("a"), c_str("1"));
6400 set_prop(root, c_str("b"), c_str("2"));
6401
6402 let value = get_prop(root, c_str("a"));
6403 assert!(!value.is_null());
6404 allocator::xmlFreeImpl(value as *mut c_void);
6405
6406 // Remove prop
6407 let attr = (*root).properties;
6408 assert!(!attr.is_null());
6409 let result = remove_prop(attr);
6410 assert_eq!(result, 0);
6411
6412 // Should no longer be found
6413 let value2 = get_prop(root, c_str("a"));
6414 assert!(value2.is_null());
6415
6416 free_doc(doc);
6417 }
6418 }
6419
6420 /// Verify namespace operations: creation, binding, and search.
6421 ///
6422 /// # Safety
6423 ///
6424 /// - `doc`, `root`, and `ns` must be valid while the namespace helpers
6425 /// run; the `c_str` buffers must be NUL-terminated and alive for the
6426 /// calls. The doc is freed with `free_doc`.
6427 #[test]
6428 fn test_namespace_operations() {
6429 unsafe {
6430 let doc = new_doc(ptr::null());
6431 let root = new_node(ptr::null_mut(), c_str("root"));
6432 doc_set_root_element(doc, root);
6433
6434 let ns = new_ns(root, c_str("http://example.com"), c_str("ex"));
6435 assert!(!ns.is_null());
6436 assert!(!(*root).nsDef.is_null());
6437
6438 set_ns(root, ns);
6439 assert_eq!((*root).ns, ns);
6440
6441 let found = search_ns(doc, root, c_str("ex"));
6442 assert_eq!(found, ns);
6443
6444 let found_href = search_ns_by_href(doc, root, c_str("http://example.com"));
6445 assert_eq!(found_href, ns);
6446
6447 free_doc(doc);
6448 }
6449 }
6450
6451 /// Verify creating a DTD and attaching it to a document.
6452 ///
6453 /// # Safety
6454 ///
6455 /// - `doc` and `dtd` must be valid while `new_dtd` and `get_int_subset`
6456 /// run; the `c_str` buffers must be NUL-terminated and alive for the
6457 /// calls. The doc is freed with `free_doc`.
6458 #[test]
6459 fn test_new_dtd() {
6460 unsafe {
6461 let doc = new_doc(ptr::null());
6462 let dtd = new_dtd(doc, c_str("root"), c_str("-//TEST//DTD"), c_str("test.dtd"));
6463 assert!(!dtd.is_null());
6464 assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
6465 // UPSTREAM-PARITY (tree.c xmlNewDtd): xmlNewDtd creates the
6466 // document's EXTERNAL subset, so the DTD is reachable as
6467 // doc->extSubset (the xmlCreateIntSubset call sets intSubset).
6468 assert_eq!((*doc).extSubset, dtd);
6469 free_doc(doc);
6470 }
6471 }
6472
6473 /// Verify deep-copying a node.
6474 ///
6475 /// # Safety
6476 ///
6477 /// - `doc` and `root` must be valid while `copy_node` walks the subtree;
6478 /// the copy is freed with `free_node` and the doc with `free_doc`.
6479 #[test]
6480 fn test_copy_node_deep() {
6481 unsafe {
6482 let doc = new_doc(ptr::null());
6483 let root = new_node(ptr::null_mut(), c_str("root"));
6484 doc_set_root_element(doc, root);
6485 let _child = new_child(root, ptr::null_mut(), c_str("child"));
6486
6487 let copy = copy_node(root, 1);
6488 assert!(!copy.is_null());
6489 assert_eq!((*copy).type_, XML_ELEMENT_NODE as c_int);
6490 // Check child was copied
6491 assert!(!(*copy).children.is_null());
6492 assert_eq!((*(*copy).children).type_, XML_ELEMENT_NODE as c_int);
6493
6494 free_node(copy);
6495 free_doc(doc);
6496 }
6497 }
6498
6499 /// Verify creating a CDATA section node.
6500 ///
6501 /// # Safety
6502 ///
6503 /// - `doc` must be valid and `content` NUL-terminated and alive while
6504 /// `new_cdata_block` runs; the node is freed with `free_node` and the
6505 /// doc with `free_doc`.
6506 #[test]
6507 fn test_new_cdata_block() {
6508 unsafe {
6509 let doc = new_doc(ptr::null());
6510 let content = c_str("some <cdata> content");
6511 let cdata = new_cdata_block(doc, content, 20);
6512 assert!(!cdata.is_null());
6513 assert_eq!((*cdata).type_, XML_CDATA_SECTION_NODE as c_int);
6514 free_node(cdata);
6515 free_doc(doc);
6516 }
6517 }
6518
6519 /// A half-million-deep element chain must free WITHOUT C recursion —
6520 /// upstream xmlFreeNodeList walks iteratively (tree.c 2.15) and the
6521 /// recursive version overflowed the stack at php shutdown on the deep
6522 /// Dom\XMLDocument (GH-22570: segv after saveXml's "Maximum call stack
6523 /// size" Error, which this test's process would hit too on a regression
6524 /// because the test thread stack is far below 500k frames).
6525 ///
6526 /// # Safety
6527 ///
6528 /// - doc/root/chain are built with the tree API and freed exactly once
6529 /// through `free_doc` (children before parents, post-order).
6530 #[test]
6531 fn test_free_deeply_nested_chain_is_iterative() {
6532 unsafe {
6533 let doc = new_doc(c"1.0".as_ptr() as *const xmlChar);
6534 assert!(!doc.is_null());
6535 const DEPTH: usize = 500_000;
6536 let root = new_node(ptr::null_mut(), c_str("a"));
6537 assert!(!root.is_null());
6538 doc_set_root_element(doc, root);
6539 let mut child = root;
6540 for _ in 0..DEPTH {
6541 let next = new_node(ptr::null_mut(), c_str("a"));
6542 assert!(!next.is_null());
6543 assert!(!add_child(child, next).is_null());
6544 child = next;
6545 }
6546 free_doc(doc);
6547 }
6548 }
6549
6550 /// Verify NULL handling in the tree API entry points.
6551 ///
6552 /// # Safety
6553 ///
6554 /// - NULL pointers passed to `new_doc`, `new_node`, `free_node`,
6555 /// `free_doc`, `unlink_node`, `add_child`, and `add_sibling` must be
6556 /// accepted without dereference (the test asserts this); all created
6557 /// objects are freed.
6558 #[test]
6559 fn test_null_handling() {
6560 unsafe {
6561 assert!(!new_doc(ptr::null()).is_null()); // Should succeed with default version
6562 let doc = new_doc(ptr::null());
6563 // UPSTREAM-PARITY (tree.c xmlNewNode): a NULL name is rejected
6564 // up front (HOSTILE-ABI A48).
6565 assert!(new_node(ptr::null_mut(), ptr::null()).is_null());
6566 free_node(ptr::null_mut()); // Should not crash
6567 free_doc(ptr::null_mut()); // Should not crash
6568 unlink_node(ptr::null_mut()); // Should not crash
6569 assert!(add_child(ptr::null_mut(), ptr::null_mut()).is_null());
6570 assert!(add_sibling(ptr::null_mut(), ptr::null_mut()).is_null());
6571 free_doc(doc);
6572 }
6573 }
6574
6575 // ═══════════════════════════════════════════════════════════════════
6576 // Serialization Tests
6577 // ═══════════════════════════════════════════════════════════════════
6578
6579 /// Helper: compare a buffer's content to an expected string.
6580 ///
6581 /// # Safety
6582 ///
6583 /// - `buf` must be a valid pointer to an `_xmlBuffer`; its content and
6584 /// length are read through `io::buf_content` and `io::buf_length`.
6585 unsafe fn buf_eq_str(buf: *mut _xmlBuffer, expected: &str) -> bool {
6586 let content = io::buf_content(buf);
6587 if content.is_null() {
6588 return expected.is_empty();
6589 }
6590 let len = io::buf_length(buf) as usize;
6591 if len != expected.len() {
6592 return false;
6593 }
6594 let slice = unsafe { core::slice::from_raw_parts(content, len) };
6595 slice == expected.as_bytes()
6596 }
6597
6598 /// Verify serializing an empty document.
6599 ///
6600 /// # Safety
6601 ///
6602 /// - `doc` must be a valid `_xmlDoc` and `buf` a valid `_xmlBuffer` while
6603 /// `doc_dump` runs; `buf` is freed with `io::buf_free` and the doc with
6604 /// `free_doc`.
6605 #[test]
6606 fn test_serialize_empty_document() {
6607 unsafe {
6608 let doc = new_doc(ptr::null());
6609 let buf = io::buf_create(-1);
6610 assert!(!buf.is_null());
6611
6612 let ret = doc_dump(buf, doc);
6613 assert!(ret >= 0);
6614
6615 // UPSTREAM-PARITY: xmlDocDump writes the declaration with no
6616 // encoding attribute (doc->encoding is NULL) and a trailing
6617 // newline after it.
6618 let expected = "<?xml version=\"1.0\"?>\n";
6619 assert!(buf_eq_str(buf, expected));
6620
6621 io::buf_free(buf);
6622 free_doc(doc);
6623 }
6624 }
6625
6626 /// Verify serializing an element with text content.
6627 ///
6628 /// # Safety
6629 ///
6630 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6631 /// with `io::buf_free` and the doc with `free_doc`.
6632 #[test]
6633 fn test_serialize_element_with_text() {
6634 unsafe {
6635 let doc = new_doc(ptr::null());
6636 let root = new_node(ptr::null_mut(), c_str("root"));
6637 doc_set_root_element(doc, root);
6638
6639 // Add text child
6640 let text = new_text(c_str("hello world"));
6641 add_child(root, text);
6642
6643 let buf = io::buf_create(-1);
6644 assert!(!buf.is_null());
6645
6646 let ret = doc_dump(buf, doc);
6647 assert!(ret >= 0);
6648
6649 let expected = "<?xml version=\"1.0\"?>\n<root>hello world</root>\n";
6650 assert!(buf_eq_str(buf, expected));
6651
6652 io::buf_free(buf);
6653 free_doc(doc);
6654 }
6655 }
6656
6657 /// Verify serializing an element with attributes.
6658 ///
6659 /// # Safety
6660 ///
6661 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6662 /// with `io::buf_free` and the doc with `free_doc`.
6663 #[test]
6664 fn test_serialize_element_with_attributes() {
6665 unsafe {
6666 let doc = new_doc(ptr::null());
6667 let root = new_node(ptr::null_mut(), c_str("root"));
6668 doc_set_root_element(doc, root);
6669
6670 set_prop(root, c_str("id"), c_str("42"));
6671 set_prop(root, c_str("name"), c_str("test"));
6672
6673 let buf = io::buf_create(-1);
6674 assert!(!buf.is_null());
6675
6676 let ret = doc_dump(buf, doc);
6677 assert!(ret >= 0);
6678
6679 let expected = "<?xml version=\"1.0\"?>\n<root id=\"42\" name=\"test\"/>\n";
6680 assert!(buf_eq_str(buf, expected));
6681
6682 io::buf_free(buf);
6683 free_doc(doc);
6684 }
6685 }
6686
6687 /// Verify serializing nested elements.
6688 ///
6689 /// # Safety
6690 ///
6691 /// - `doc` and `buf` must be valid while `doc_dump` walks the tree; `buf`
6692 /// is freed with `io::buf_free` and the doc with `free_doc`.
6693 #[test]
6694 fn test_serialize_nested_elements() {
6695 unsafe {
6696 let doc = new_doc(ptr::null());
6697 let root = new_node(ptr::null_mut(), c_str("root"));
6698 doc_set_root_element(doc, root);
6699
6700 let child = new_child(root, ptr::null_mut(), c_str("child"));
6701 let grandchild = new_child(child, ptr::null_mut(), c_str("gc"));
6702 let text = new_text(c_str("text"));
6703 add_child(grandchild, text);
6704
6705 let buf = io::buf_create(-1);
6706 assert!(!buf.is_null());
6707
6708 let ret = doc_dump(buf, doc);
6709 assert!(ret >= 0);
6710
6711 let expected = "<?xml version=\"1.0\"?>\n<root><child><gc>text</gc></child></root>\n";
6712 assert!(buf_eq_str(buf, expected));
6713
6714 io::buf_free(buf);
6715 free_doc(doc);
6716 }
6717 }
6718
6719 /// Verify formatted serialization output.
6720 ///
6721 /// # Safety
6722 ///
6723 /// - `doc` and `buf` must be valid while `serialize_node` runs; `buf` is
6724 /// freed with `io::buf_free` and the doc with `free_doc`.
6725 #[test]
6726 fn test_serialize_with_formatting() {
6727 unsafe {
6728 let doc = new_doc(ptr::null());
6729 let root = new_node(ptr::null_mut(), c_str("root"));
6730 doc_set_root_element(doc, root);
6731
6732 let child = new_child(root, ptr::null_mut(), c_str("child"));
6733 let text = new_text(c_str("text"));
6734 add_child(child, text);
6735
6736 let buf = io::buf_create(-1);
6737 assert!(!buf.is_null());
6738
6739 serialize_node(doc as *mut _xmlNode, buf, 1, 0);
6740
6741 let expected = "<?xml version=\"1.0\"?>\n<root>\n <child>text</child>\n</root>\n";
6742 assert!(buf_eq_str(buf, expected));
6743
6744 io::buf_free(buf);
6745 free_doc(doc);
6746 }
6747 }
6748
6749 /// Verify that `&` is escaped in serialized text.
6750 ///
6751 /// # Safety
6752 ///
6753 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6754 /// with `io::buf_free` and the doc with `free_doc`.
6755 #[test]
6756 fn test_serialize_escape_ampersand() {
6757 unsafe {
6758 let doc = new_doc(ptr::null());
6759 let root = new_node(ptr::null_mut(), c_str("root"));
6760 doc_set_root_element(doc, root);
6761
6762 let text = new_text(c_str("a & b"));
6763 add_child(root, text);
6764
6765 let buf = io::buf_create(-1);
6766 assert!(!buf.is_null());
6767
6768 let ret = doc_dump(buf, doc);
6769 assert!(ret >= 0);
6770
6771 let expected = "<?xml version=\"1.0\"?>\n<root>a & b</root>\n";
6772 assert!(buf_eq_str(buf, expected));
6773
6774 io::buf_free(buf);
6775 free_doc(doc);
6776 }
6777 }
6778
6779 /// Verify that angle brackets are escaped in serialized text.
6780 ///
6781 /// # Safety
6782 ///
6783 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6784 /// with `io::buf_free` and the doc with `free_doc`.
6785 #[test]
6786 fn test_serialize_escape_angle_brackets() {
6787 unsafe {
6788 let doc = new_doc(ptr::null());
6789 let root = new_node(ptr::null_mut(), c_str("root"));
6790 doc_set_root_element(doc, root);
6791
6792 let text = new_text(c_str("x < y > z"));
6793 add_child(root, text);
6794
6795 let buf = io::buf_create(-1);
6796 assert!(!buf.is_null());
6797
6798 let ret = doc_dump(buf, doc);
6799 assert!(ret >= 0);
6800
6801 let expected = "<?xml version=\"1.0\"?>\n<root>x < y > z</root>\n";
6802 assert!(buf_eq_str(buf, expected));
6803
6804 io::buf_free(buf);
6805 free_doc(doc);
6806 }
6807 }
6808
6809 /// Verify serializing a comment node.
6810 ///
6811 /// # Safety
6812 ///
6813 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6814 /// with `io::buf_free` and the doc with `free_doc`.
6815 #[test]
6816 fn test_serialize_comment() {
6817 unsafe {
6818 let doc = new_doc(ptr::null());
6819 let root = new_node(ptr::null_mut(), c_str("root"));
6820 doc_set_root_element(doc, root);
6821
6822 let comment = new_comment(c_str("my comment"));
6823 add_child(root, comment);
6824
6825 let buf = io::buf_create(-1);
6826 assert!(!buf.is_null());
6827
6828 let ret = doc_dump(buf, doc);
6829 assert!(ret >= 0);
6830
6831 let expected = "<?xml version=\"1.0\"?>\n<root><!--my comment--></root>\n";
6832 assert!(buf_eq_str(buf, expected));
6833
6834 io::buf_free(buf);
6835 free_doc(doc);
6836 }
6837 }
6838
6839 /// Verify serializing a processing instruction.
6840 ///
6841 /// # Safety
6842 ///
6843 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6844 /// with `io::buf_free` and the doc with `free_doc`.
6845 #[test]
6846 fn test_serialize_pi() {
6847 unsafe {
6848 let doc = new_doc(ptr::null());
6849 let root = new_node(ptr::null_mut(), c_str("root"));
6850 doc_set_root_element(doc, root);
6851
6852 let pi = new_pi(
6853 c_str("xml-stylesheet"),
6854 c_str("href=\"style.xsl\" type=\"text/xsl\""),
6855 );
6856 add_child(root, pi);
6857
6858 let buf = io::buf_create(-1);
6859 assert!(!buf.is_null());
6860
6861 let ret = doc_dump(buf, doc);
6862 assert!(ret >= 0);
6863
6864 let expected = "<?xml version=\"1.0\"?>\n<root><?xml-stylesheet href=\"style.xsl\" type=\"text/xsl\"?></root>\n";
6865 assert!(buf_eq_str(buf, expected));
6866
6867 io::buf_free(buf);
6868 free_doc(doc);
6869 }
6870 }
6871
6872 /// Verify serializing an empty element in self-closing form.
6873 ///
6874 /// # Safety
6875 ///
6876 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6877 /// with `io::buf_free` and the doc with `free_doc`.
6878 #[test]
6879 fn test_serialize_self_closing() {
6880 unsafe {
6881 let doc = new_doc(ptr::null());
6882 let root = new_node(ptr::null_mut(), c_str("empty"));
6883 doc_set_root_element(doc, root);
6884
6885 let buf = io::buf_create(-1);
6886 assert!(!buf.is_null());
6887
6888 let ret = doc_dump(buf, doc);
6889 assert!(ret >= 0);
6890
6891 let expected = "<?xml version=\"1.0\"?>\n<empty/>\n";
6892 assert!(buf_eq_str(buf, expected));
6893
6894 io::buf_free(buf);
6895 free_doc(doc);
6896 }
6897 }
6898
6899 /// Verify dumping a node to a string.
6900 ///
6901 /// # Safety
6902 ///
6903 /// - `node` must be a valid `_xmlNode` while `dump_node` serializes it;
6904 /// `result` is read with `xml_strlen` and freed with `xmlFreeImpl`, and
6905 /// the node with `free_node`.
6906 #[test]
6907 fn test_dump_node_to_string() {
6908 unsafe {
6909 let node = new_node(ptr::null_mut(), c_str("foo"));
6910 let text = new_text(c_str("bar"));
6911 add_child(node, text);
6912
6913 let result = dump_node(node);
6914 assert!(!result.is_null());
6915
6916 let len = xml_strlen(result);
6917 let slice = { core::slice::from_raw_parts(result, len as usize) };
6918 assert_eq!(slice, b"<foo>bar</foo>");
6919
6920 allocator::xmlFreeImpl(result as *mut c_void);
6921 free_node(node);
6922 }
6923 }
6924
6925 /// Verify dumping a document to a string.
6926 ///
6927 /// # Safety
6928 ///
6929 /// - `doc` must be a valid `_xmlDoc` while `dump_doc` serializes it;
6930 /// `result` is read with `xml_strlen` and freed with `xmlFreeImpl`, and
6931 /// the doc with `free_doc`.
6932 #[test]
6933 fn test_dump_doc_to_string() {
6934 unsafe {
6935 let doc = new_doc(ptr::null());
6936 let root = new_node(ptr::null_mut(), c_str("root"));
6937 doc_set_root_element(doc, root);
6938
6939 let result = dump_doc(doc);
6940 assert!(!result.is_null());
6941
6942 let len = xml_strlen(result);
6943 let slice = { core::slice::from_raw_parts(result, len as usize) };
6944 let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
6945 assert_eq!(slice, expected.as_bytes());
6946
6947 allocator::xmlFreeImpl(result as *mut c_void);
6948 free_doc(doc);
6949 }
6950 }
6951
6952 /// Verify `xmlDocDumpFormatMemory`.
6953 ///
6954 /// # Safety
6955 ///
6956 /// - `doc` must be a valid `_xmlDoc` while the export runs; `mem` receives
6957 /// a callee-owned buffer read as `size` bytes and freed with
6958 /// `xmlFreeImpl`, and the doc with `free_doc`.
6959 #[test]
6960 fn test_xmlDocDumpFormatMemory() {
6961 unsafe {
6962 let doc = new_doc(ptr::null());
6963 let root = new_node(ptr::null_mut(), c_str("root"));
6964 doc_set_root_element(doc, root);
6965
6966 let mut mem: *mut xmlChar = ptr::null_mut();
6967 let mut size: c_int = 0;
6968
6969 xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 0);
6970
6971 assert!(!mem.is_null());
6972 assert!(size > 0);
6973
6974 let slice = { core::slice::from_raw_parts(mem, size as usize) };
6975 // UPSTREAM-PARITY: xmlDocDumpFormatMemory with a NULL encoding
6976 // writes no encoding attribute and a newline after each child.
6977 let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
6978 assert_eq!(slice, expected.as_bytes());
6979
6980 allocator::xmlFreeImpl(mem as *mut c_void);
6981 free_doc(doc);
6982 }
6983 }
6984
6985 /// Verify escaping of special characters in attribute values.
6986 ///
6987 /// # Safety
6988 ///
6989 /// - `doc` and `buf` must be valid while `doc_dump` runs; `buf` is freed
6990 /// with `io::buf_free` and the doc with `free_doc`.
6991 #[test]
6992 fn test_serialize_escape_attribute() {
6993 unsafe {
6994 let doc = new_doc(ptr::null());
6995 let root = new_node(ptr::null_mut(), c_str("root"));
6996 doc_set_root_element(doc, root);
6997
6998 // Attribute with special chars
6999 set_prop(root, c_str("desc"), c_str("a < b & c \"quoted\""));
7000
7001 let buf = io::buf_create(-1);
7002 assert!(!buf.is_null());
7003
7004 let ret = doc_dump(buf, doc);
7005 assert!(ret >= 0);
7006
7007 let expected =
7008 "<?xml version=\"1.0\"?>\n<root desc=\"a < b & c "quoted"\"/>\n";
7009 assert!(buf_eq_str(buf, expected));
7010
7011 io::buf_free(buf);
7012 free_doc(doc);
7013 }
7014 }
7015
7016 /// Phase 14.3 PHP regression (DOMParentNode_empty_argument): serializing a
7017 /// document-fragment node must emit its children (transparent container).
7018 /// The missing XML_DOCUMENT_FRAG_NODE arm returned empty output (and a
7019 /// downstream PHP double-destroy) where upstream xmlsave.c trampolines the
7020 /// fragment's children.
7021 ///
7022 /// # Safety
7023 ///
7024 /// - `frag`/`foo` are built under `doc`; `node_dump` walks them; the buffer
7025 /// is freed with `io::buf_free` and the doc with `free_doc`.
7026 #[test]
7027 fn test_dump_document_fragment_serializes_children() {
7028 unsafe {
7029 let doc = new_doc(ptr::null());
7030 let frag = crate::abi::exports_tree::xmlNewDocFragment(doc);
7031 assert!(!frag.is_null());
7032 // add an element child `<foo/>` to the fragment
7033 let foo = new_child(frag, ptr::null_mut(), c_str("foo"));
7034 assert!(!foo.is_null());
7035
7036 let buf = io::buf_create(-1);
7037 assert!(!buf.is_null());
7038 // Serialize the fragment itself (xmlNodeDump semantics, same
7039 // node_dump_internal path as xmlNodeDumpOutput).
7040 let ret = node_dump(buf, doc, frag, 0, 0);
7041 assert!(ret >= 0);
7042 assert!(buf_eq_str(buf, "<foo/>"));
7043
7044 io::buf_free(buf);
7045 free_doc(doc);
7046 }
7047 }
7048
7049 /// Phase 14.3 Bug-3 regression: `copy_doc` must link the copied
7050 /// top-level children with the NEW document node as their parent (and set
7051 /// `doc->last`), mirroring upstream xmlCopyDoc
7052 /// (`xmlStaticCopyNodeList(doc->children, ret, (xmlNodePtr)ret)`). The
7053 /// pre-fix NULL parent made PHP treat a cloned document's root element as
7054 /// ownerless: its proxy teardown (php_libxml_node_free_resource,
7055 /// `parent == NULL` branch) freed the whole subtree while the cloned doc
7056 /// still referenced it, so the doc teardown double-freed the root
7057 /// (DOMDocument clone + navigation crash).
7058 ///
7059 /// # Safety
7060 ///
7061 /// - doc/root are valid while copy_doc runs; both docs are freed with
7062 /// `free_doc`.
7063 #[test]
7064 fn test_copy_doc_keeps_doc_as_root_parent() {
7065 unsafe {
7066 let doc = new_doc(ptr::null());
7067 let root = new_node(ptr::null_mut(), c_str("root"));
7068 doc_set_root_element(doc, root);
7069 let child = new_child(root, ptr::null_mut(), c_str("kid"));
7070 assert!(!child.is_null());
7071
7072 let copy = copy_doc(doc, 1);
7073 assert!(!copy.is_null());
7074 let copied_root = (*copy).children;
7075 assert!(!copied_root.is_null());
7076 // UPSTREAM-PARITY: root's parent is the DOCUMENT node.
7077 assert_eq!((*copied_root).parent as *mut c_void, copy as *mut c_void);
7078 assert_eq!((*copied_root).doc as *mut c_void, copy as *mut c_void);
7079 // doc->last tracks the final child.
7080 assert_eq!((*copy).last as *mut c_void, copied_root as *mut c_void);
7081 // child keeps its element parent.
7082 let copied_child = (*copied_root).children;
7083 assert!(!copied_child.is_null());
7084 assert_eq!(
7085 (*copied_child).parent as *mut c_void,
7086 copied_root as *mut c_void
7087 );
7088
7089 free_doc(copy);
7090 free_doc(doc);
7091 }
7092 }
7093}
7094