sup_xml_tree/dom.rs
1//! Bumpalo-backed, libxml2-shaped DOM.
2//!
3//! This is the v2 tree representation that replaces the per-node-`malloc`
4//! design in [`crate::node`]. It's not wired into the parser yet — that
5//! happens in Milestone 2. Until then, the rest of the codebase keeps using
6//! the existing [`crate::node`] types unchanged.
7//!
8//! # Design
9//!
10//! * **One arena per document.** A [`bumpalo::Bump`] owns all node, attribute,
11//! namespace, and string allocations. Per-node alloc cost drops to a
12//! pointer bump; drop is free per node — the whole arena is freed at once.
13//!
14//! * **libxml2-shaped nodes.** A single [`Node`] struct (not a Rust enum)
15//! with a [`NodeKind`] tag and inline fields for every variant. Children
16//! form a doubly-linked list via `first_child`/`last_child` on the parent
17//! and `next_sibling`/`prev_sibling` + `parent` on each child. Attributes
18//! form their own doubly-linked list on the element. Field offsets and
19//! semantics mirror `xmlNode` so a future `extern "C"` shim can expose the
20//! same memory to libxml2 callers verbatim.
21//!
22//! * **Cell-of-references for links.** All sibling/child/parent pointers
23//! are `Cell<Option<&'doc Node<'doc>>>`. This is the standard idiomatic
24//! pattern for graph-shaped data in an arena: you get mutation without
25//! `RefCell` overhead and without raw pointers in the public API.
26//!
27//! * **Strings borrowed where possible.** Names and text contents are
28//! `&'doc str`. When the parser can borrow from the source slice
29//! directly it does; otherwise it allocates a copy in the arena. No
30//! `Arc<str>` per name.
31//!
32//! # The self-referential `Document` wrapper
33//!
34//! [`Document`] owns a `Bump` *and* holds a root pointer (`&Node`) into that
35//! same `Bump`. That's a self-referential struct, which safe Rust doesn't
36//! express directly. The arena nodes have stable addresses (bumpalo never
37//! relocates allocations), so the references are sound in principle. We
38//! encode it with one contained `unsafe` block in [`Document::root`], audited by:
39//!
40//! 1. The `Bump` is stored in an `Arc<Bump>` — heap-allocated, never
41//! moved while any clone of the Arc lives. C-ABI consumers
42//! (`sup-xml-compat`) share a single thread-local arena across
43//! every document, so cross-doc node grafts (libxml2 consumers
44//! like lxml moving nodes between documents) are safe by
45//! construction — node memory outlives any individual doc.
46//! 2. The root pointer is built from `&Bump::alloc(...)` of the same `Bump`,
47//! so the lifetime is genuinely `'self` (as in: tied to the `Document`).
48//! 3. The `Document`'s public methods hand out references with a borrow of
49//! `&self`, which the borrow checker treats as `'self`-bounded. Outside
50//! those methods, no `'doc` reference can escape.
51//!
52//! See [`Document`] for the API.
53
54#![allow(unsafe_code)] // see module docs § "self-referential Document wrapper"
55
56use std::cell::{Cell, RefCell};
57use std::sync::Arc;
58
59use bumpalo::Bump;
60
61// ── HTML metadata ────────────────────────────────────────────────────────────
62
63/// HTML5 quirks-mode flag set from the DOCTYPE. Only meaningful for HTML
64/// documents; XML documents always carry `None` for `Document::html_metadata`.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum QuirksMode {
67 NoQuirks,
68 LimitedQuirks,
69 Quirks,
70}
71
72/// Captured DOCTYPE content from an HTML document. Stored verbatim; HTML5 does
73/// not validate against DTDs, but the public/system identifiers are surfaced
74/// for tools that want to introspect them (e.g. detecting XHTML vs HTML5 input).
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct HtmlDoctype {
77 pub name: String,
78 pub public_id: String,
79 pub system_id: String,
80}
81
82/// HTML-specific document metadata. Set when the document came from
83/// `parse_html_*`; `None` for XML documents.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct HtmlMeta {
86 pub quirks_mode: QuirksMode,
87 pub doctype: Option<HtmlDoctype>,
88}
89
90// ── node kinds ──────────────────────────────────────────────────────────────
91
92/// Discriminant for [`Node::kind`].
93///
94/// `#[repr(u32)]` chosen to match libxml2's `xmlElementType` numeric layout —
95/// the future `extern "C"` shim can transmute / cast directly. Variant values
96/// are pinned to libxml2's enum order; do not reorder without a coordinated
97/// ABI update.
98#[repr(u32)]
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum NodeKind {
101 /// `<element>...</element>`.
102 Element = 1,
103 /// An attribute. Only used as the `kind` of an
104 /// [`Attribute`](crate::dom::Attribute) under the `c-abi` feature
105 /// — never appears on a real [`Node`]; the libxml2 ABI requires
106 /// xmlAttr's `type` field at offset 8 to carry this discriminant
107 /// so generic walkers casting `xmlAttr*` to `xmlNode*` see the
108 /// right type.
109 Attribute = 2,
110 /// Character data between tags. `content` holds the (entity-expanded) text.
111 Text = 3,
112 /// `<![CDATA[…]]>`. Preserved as a distinct kind for round-trip serialization.
113 CData = 4,
114 /// An unresolved entity reference — `&name;` left literal in the
115 /// tree. Emitted only when the parser is configured with
116 /// `resolve_entities: false`; the default expands entity
117 /// references inline. `name` holds the entity name (e.g.
118 /// `"foo"` for `&foo;`); `content` holds the literal source
119 /// form `"&foo;"` so serialization round-trips by writing
120 /// `content` verbatim, and lxml-compat code can return
121 /// `node.text == "&foo;"`.
122 ///
123 /// libxml2's enum value for `XML_ENTITY_REF_NODE` is 5 —
124 /// pinned here so generic walkers see the right tag.
125 EntityRef = 5,
126 /// `<!-- … -->`.
127 Comment = 8,
128 /// `<?target content?>`.
129 Pi = 7,
130 /// The document root. Only used as the `kind` of an
131 /// [`XmlDoc`](crate::dom::XmlDoc) under the `c-abi` feature — never
132 /// appears on a real [`Node`]. libxml2's enum value for
133 /// `XML_DOCUMENT_NODE` is 9.
134 Document = 9,
135 /// A detached subtree root with no semantic content of its own —
136 /// just a parent for an ordered list of child nodes. Returned by
137 /// `xmlNewDocFragment` and used by callers that want to build a
138 /// composite subtree before grafting it into a real document.
139 ///
140 /// libxml2's enum value for `XML_DOCUMENT_FRAG_NODE` is 11.
141 DocumentFragment = 11,
142 /// The DOCTYPE internal-subset node itself (`XML_DTD_NODE` = 14).
143 /// Never allocated in the arena — sup-xml models the DTD through
144 /// the compat shim's 128-byte `xmlDtd` struct, which shares the
145 /// node header (`kind@8`, `children@24`, `parent@40`, `next@48`,
146 /// `prev@56`, `doc@64`) with [`Node`]. The shim splices that
147 /// struct into the document's sibling chain at the DOCTYPE's true
148 /// position; reinterpreted as a `Node` it reports this kind, so
149 /// every tree walker traverses past it as an inert node (it emits
150 /// no markup of its own — lxml serializes the subset via
151 /// `doc->intSubset` directly).
152 Dtd = 14,
153 /// A document-type internal-subset declaration block (`<!ENTITY …>`,
154 /// `<!ELEMENT …>`, `<!ATTLIST …>`, …). Held as the single child of
155 /// the internal-subset DTD node; `content` carries the raw markup
156 /// declarations (each terminated by a newline) verbatim, which the
157 /// serializer emits unescaped inside the DOCTYPE's `[ … ]`. The
158 /// discriminant sits in libxml2's declaration-type range
159 /// (`XML_ELEMENT_DECL` = 15) so generic walkers treat it as a DTD
160 /// declaration; sup-xml does not model per-declaration nodes.
161 DtdDecl = 15,
162}
163
164// ── core types ──────────────────────────────────────────────────────────────
165
166/// Thin NUL-terminated UTF-8 pointer with arena lifetime — c-abi-only.
167///
168/// Used in the [`Node`] / [`Attribute`] / [`Namespace`] structs under
169/// the `c-abi` feature to match libxml2's `xmlChar*` (8-byte thin
170/// pointer to NUL-terminated UTF-8) at fixed byte offsets. On the
171/// lean (default) build these fields are `&'doc str` (16 bytes —
172/// ptr + len); `ArenaCStr` is the 8-byte alternative used when the
173/// libxml2 ABI window applies.
174///
175/// Callers reach the bytes via [`as_ptr`](Self::as_ptr) (for C FFI —
176/// cast to `*const xmlChar`) or [`as_str`](Self::as_str) (for Rust
177/// — does a strlen + `from_utf8_unchecked`).
178///
179/// Builder discipline guarantees the trailing `\0` and that the
180/// bytes are valid UTF-8.
181#[cfg(feature = "c-abi")]
182#[repr(transparent)]
183#[derive(Copy, Clone)]
184pub struct ArenaCStr<'doc> {
185 /// Non-null because libxml2's `xmlChar*` slots use `NULL` to mean
186 /// "absent" — we model the absent case explicitly with
187 /// `Option<ArenaCStr>` and rely on the niche-optimization here to
188 /// keep `Option<ArenaCStr>` 8 bytes wide (matches `xmlChar*` slot
189 /// width in `_xmlNs` etc.).
190 ptr: std::ptr::NonNull<u8>,
191 _marker: std::marker::PhantomData<&'doc u8>,
192}
193
194#[cfg(feature = "c-abi")]
195impl<'doc> ArenaCStr<'doc> {
196 /// Construct from a raw pointer. Caller asserts NUL-terminated
197 /// UTF-8 with at least `'doc` lifetime AND non-null. Use
198 /// [`empty`](Self::empty) when "absent" is the intent.
199 ///
200 /// # Safety
201 /// The pointed-to byte array must be NUL-terminated UTF-8 valid
202 /// for the entire `'doc` lifetime, and `ptr` must be non-null.
203 #[inline]
204 pub unsafe fn from_raw(ptr: *const u8) -> Self {
205 debug_assert!(!ptr.is_null(), "ArenaCStr::from_raw given NULL");
206 // SAFETY: caller asserts non-null.
207 Self {
208 ptr: unsafe { std::ptr::NonNull::new_unchecked(ptr as *mut u8) },
209 _marker: std::marker::PhantomData,
210 }
211 }
212
213 /// Raw pointer suitable for C FFI as `*const xmlChar`.
214 #[inline]
215 pub fn as_ptr(&self) -> *const u8 { self.ptr.as_ptr() }
216
217 /// Decode to `&'doc str`. Does a strlen scan; bytes are
218 /// guaranteed valid UTF-8 by builder discipline.
219 pub fn as_str(&self) -> &'doc str {
220 // SAFETY: builder allocates with trailing `\0`; bytes are UTF-8.
221 unsafe {
222 let cstr = std::ffi::CStr::from_ptr(self.ptr.as_ptr() as *const std::os::raw::c_char);
223 std::str::from_utf8_unchecked(cstr.to_bytes())
224 }
225 }
226
227 /// Empty-string singleton — points at a static NUL byte.
228 /// Used for "this kind of node doesn't have a name/content"
229 /// initialization without a per-instance allocation.
230 pub fn empty() -> Self {
231 static EMPTY: u8 = 0;
232 Self {
233 ptr: std::ptr::NonNull::from(&EMPTY),
234 _marker: std::marker::PhantomData,
235 }
236 }
237
238 /// Construct from the address of `xmlStringText` — the static
239 /// `"text\0"` exported as a libxml2-ABI symbol. libxslt's
240 /// `xsltCopyText` and similar helpers compare `node->name`
241 /// against `xmlStringText` *by pointer* to identify text nodes,
242 /// so every Text node we create must point at this exact byte.
243 #[inline]
244 pub fn text_name() -> Self {
245 // SAFETY: xml_string_text() returns the address of a
246 // 'static byte array that's NUL-terminated.
247 unsafe { Self::from_raw(xml_string_text()) }
248 }
249
250 /// Construct from the address of `xmlStringTextNoenc` — the
251 /// disable-output-escaping marker. Same pointer-equality
252 /// contract as `text_name`.
253 #[inline]
254 pub fn text_noenc_name() -> Self {
255 unsafe { Self::from_raw(xml_string_text_noenc()) }
256 }
257}
258
259/// libxml2's `xmlStringText` — exported as a C symbol so callers
260/// (libxslt, lxml) that test `node->name == xmlStringText` by
261/// pointer get the canonical address. Defined in tree (rather than
262/// compat) so the tree builder can reach it when constructing text
263/// nodes without a dependency cycle.
264#[cfg(feature = "c-abi")]
265#[unsafe(no_mangle)]
266pub static xmlStringText: [u8; 5] = *b"text\0";
267
268/// Sibling of `xmlStringText`, for text nodes with
269/// `disable-output-escaping="yes"`.
270#[cfg(feature = "c-abi")]
271#[unsafe(no_mangle)]
272pub static xmlStringTextNoenc: [u8; 10] = *b"textnoenc\0";
273
274/// Address of `xmlStringText` for in-crate use.
275#[cfg(feature = "c-abi")]
276#[inline]
277fn xml_string_text() -> *const u8 { (&xmlStringText) as *const _ as *const u8 }
278
279#[cfg(feature = "c-abi")]
280#[inline]
281fn xml_string_text_noenc() -> *const u8 { (&xmlStringTextNoenc) as *const _ as *const u8 }
282
283#[cfg(feature = "c-abi")]
284impl std::fmt::Debug for ArenaCStr<'_> {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 write!(f, "{:?}", self.as_str())
287 }
288}
289
290/// A namespace binding (`prefix → href`). Lifetime-bound to the document's arena.
291///
292/// `prefix` is `None` for the default namespace (`xmlns="…"`).
293///
294/// # Layout
295/// On the lean (default) build, `prefix` and `href` are `&'doc str`
296/// (fat slice). Under the `c-abi` feature this struct is byte-exact
297/// with libxml2's `_xmlNs` as verified against libxml2 2.9.13 and
298/// 2.15.3. We do not claim layout compatibility with libxml2
299/// versions we have not verified against; the `t-upstream-layout`
300/// c-test fails the build when the installed libxml2 header
301/// disagrees with the offsets here:
302///
303/// ```text
304/// _xmlNs (64-bit), the odd duck among libxml2 structs because
305/// `next` precedes `_private`:
306/// xmlNs *next; // offset 0 (chain pointer)
307/// xmlNsType type; // offset 8 (XML_LOCAL_NAMESPACE = 18)
308/// const xmlChar *href; // offset 16
309/// const xmlChar *prefix; // offset 24
310/// void *_private; // offset 32
311/// xmlDoc *context; // offset 40
312/// // sizeof == 48
313/// ```
314///
315/// We populate `next` via [`DocumentBuilder::append_ns_def`] so
316/// `xmlSearchNs` can walk the per-element ns_def chain. `_private`
317/// and `context` are zero-initialized; we don't use them but the
318/// slots have to exist for ABI compatibility.
319#[cfg(not(feature = "c-abi"))]
320#[repr(C)]
321#[derive(Debug)]
322pub struct Namespace<'doc> {
323 pub prefix: Option<&'doc str>,
324 pub href: &'doc str,
325}
326
327#[cfg(feature = "c-abi")]
328#[repr(C)]
329pub struct Namespace<'doc> {
330 /// Next namespace declaration on the same element (chain head
331 /// rooted at `Node::ns_def`).
332 pub next: Cell<Option<&'doc Namespace<'doc>>>, // 0
333 /// libxml2's `xmlNsType`. Always `XML_LOCAL_NAMESPACE = 18`
334 /// for the only kind of namespace record we produce.
335 pub kind: i32, // 8
336 _pad_kind: i32, // 12
337 pub href: ArenaCStr<'doc>, // 16
338 pub prefix: Option<ArenaCStr<'doc>>, // 24
339 pub _private: Cell<*mut std::os::raw::c_void>, // 32
340 pub context: Cell<*mut std::os::raw::c_void>, // 40 (xmlDoc*)
341}
342
343#[cfg(feature = "c-abi")]
344impl std::fmt::Debug for Namespace<'_> {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("Namespace")
347 .field("prefix", &self.prefix.map(|p| p.as_str()))
348 .field("href", &self.href.as_str())
349 .finish()
350 }
351}
352
353impl<'doc> Namespace<'doc> {
354 /// Namespace prefix (`"xlink"`, etc.), or `None` for the default
355 /// namespace declaration (`xmlns="..."`).
356 ///
357 /// **Prefer this method over direct `ns.prefix` field access.**
358 /// On the lean rlib build it just returns the field; on the
359 /// `c-abi` build the storage type changes to a thin
360 /// NUL-terminated `ArenaCStr` and this method handles the
361 /// conversion.
362 #[inline]
363 pub fn prefix(&self) -> Option<&'doc str> {
364 #[cfg(not(feature = "c-abi"))]
365 { self.prefix }
366 #[cfg(feature = "c-abi")]
367 { self.prefix.map(|p| p.as_str()) }
368 }
369
370 /// Namespace URI (the right-hand side of `xmlns="..."`). Same
371 /// rationale as [`prefix`](Self::prefix) for preferring this
372 /// over direct field access.
373 #[inline]
374 pub fn href(&self) -> &'doc str {
375 #[cfg(not(feature = "c-abi"))]
376 { self.href }
377 #[cfg(feature = "c-abi")]
378 { self.href.as_str() }
379 }
380}
381
382/// An attribute on an element. Belongs to a doubly-linked list rooted at
383/// [`Node::first_attribute`] / [`Node::last_attribute`] on its owning element.
384///
385/// # Layout
386/// libxml2's `_xmlAttr` shares its first 8 fields with `_xmlNode`:
387/// generic walkers cast `xmlAttr*` to `xmlNode*` and read `.type` /
388/// `.name`. Under the `c-abi` feature this struct's layout matches
389/// `_xmlAttr` byte-exact.
390///
391/// ```text
392/// libxml2 _xmlAttr (64-bit):
393/// void *_private; // offset 0
394/// xmlElementType type; // offset 8 (XML_ATTRIBUTE_NODE = 2)
395/// const xmlChar *name; // offset 16
396/// xmlNode *children; // offset 24 (text/entity content nodes)
397/// xmlNode *last; // offset 32
398/// xmlNode *parent; // offset 40 (the element)
399/// xmlAttr *next; // offset 48
400/// xmlAttr *prev; // offset 56
401/// xmlDoc *doc; // offset 64
402/// xmlNs *ns; // offset 72
403/// xmlAttributeType atype; // offset 80 (DTD type info)
404/// void *psvi; // offset 88
405/// ...
406/// ```
407#[cfg(not(feature = "c-abi"))]
408#[repr(C)]
409pub struct Attribute<'doc> {
410 pub name: &'doc str,
411 pub namespace: Cell<Option<&'doc Namespace<'doc>>>,
412 /// Already entity-expanded by the parser.
413 pub value: &'doc str,
414 pub next: Cell<Option<&'doc Attribute<'doc>>>,
415 pub prev: Cell<Option<&'doc Attribute<'doc>>>,
416 /// Element this attribute belongs to. `None` only for a freshly-allocated
417 /// attribute not yet attached via [`DocumentBuilder::append_attribute`].
418 pub parent: Cell<Option<&'doc Node<'doc>>>,
419}
420
421/// xmlAttr-mirror layout (c-abi build). See type-level doc for the
422/// libxml2 reference layout. Note: libxml2's `xmlAttr` puts the
423/// **value** in a child text node chain (`children`/`last`), not in a
424/// dedicated `value` field. Our `value: ArenaCStr` is in the
425/// sup-xml-only tail (after `psvi`). C callers that follow
426/// `xmlAttr::children` get the text node our serializer materialises;
427/// callers using `xmlGetProp(...)` (the documented path) get the
428/// value directly through that function.
429#[cfg(feature = "c-abi")]
430#[repr(C)]
431pub struct Attribute<'doc> {
432 pub _private: Cell<*mut std::os::raw::c_void>, // 0
433 pub kind: NodeKind, // XML_ATTRIBUTE_NODE // 8
434 _pad_kind: u32, // 12
435 pub name: ArenaCStr<'doc>, // 16
436 pub children: Cell<Option<&'doc Node<'doc>>>, // 24 (text-node chain)
437 pub last: Cell<Option<&'doc Node<'doc>>>, // 32
438 pub parent: Cell<Option<&'doc Node<'doc>>>, // 40
439 pub next: Cell<Option<&'doc Attribute<'doc>>>, // 48
440 pub prev: Cell<Option<&'doc Attribute<'doc>>>, // 56
441 pub doc: Cell<*mut std::os::raw::c_void>, // 64 (xmlDoc*)
442 pub namespace: Cell<Option<&'doc Namespace<'doc>>>, // 72 (`ns`)
443 pub atype: u32, // 80 (xmlAttributeType)
444 _pad_atype: u32, // 84
445 pub psvi: Cell<*mut std::os::raw::c_void>, // 88
446 // ── sup-xml-only tail (NOT part of ABI window) ──
447 /// Already entity-expanded by the parser. Direct slot — libxml2
448 /// callers normally use `xmlGetProp(...)` rather than reading it
449 /// from the struct directly.
450 pub value: ArenaCStr<'doc>,
451}
452
453impl<'doc> Attribute<'doc> {
454 /// Attribute name (`xlink:href`, `id`, etc.).
455 ///
456 /// **Prefer this method over direct `attr.name` field access.**
457 /// The `pub name` field stays stable on the lean rlib build; in
458 /// the `c-abi` build it becomes a thin NUL-terminated
459 /// `ArenaCStr`. This method returns `&str` either way.
460 #[inline]
461 pub fn name(&self) -> &'doc str {
462 #[cfg(not(feature = "c-abi"))]
463 { self.name }
464 #[cfg(feature = "c-abi")]
465 { self.name.as_str() }
466 }
467
468 /// The attribute's local name (the part after any `prefix:`).
469 ///
470 /// Layout-agnostic, mirroring [`Node::local_name`]: on the lean
471 /// build [`name`](Self::name) is the full QName, while on the
472 /// `c-abi` build the prefix is stripped at parse time so `name()`
473 /// is already local. Callers matching a namespaced attribute by
474 /// its local part should use this rather than `name()`.
475 #[inline]
476 pub fn local_name(&self) -> &'doc str {
477 let n = self.name();
478 match n.rfind(':') {
479 Some(i) => &n[i + 1..],
480 None => n,
481 }
482 }
483
484 /// Attribute value (entity-expanded by the parser). Same
485 /// rationale as [`name`](Self::name) for preferring this over
486 /// direct field access.
487 ///
488 /// In the c-abi build, attribute values are stored two ways:
489 /// the sup-xml-only `value` slot (set by our parser /
490 /// `new_attribute` API) and the libxml2-standard text-node
491 /// chain rooted at `children` (the path libxslt writes to when
492 /// it builds attributes during an XSLT transform — it doesn't
493 /// know about our tail field). When `value` is empty, fall
494 /// back to walking `children` so attributes built post-parse
495 /// by libxslt still report the right text.
496 #[inline]
497 pub fn value(&self) -> &'doc str {
498 #[cfg(not(feature = "c-abi"))]
499 { self.value }
500 #[cfg(feature = "c-abi")]
501 {
502 let v = self.value.as_str();
503 if !v.is_empty() { return v; }
504 let mut child = self.children.get();
505 while let Some(c) = child {
506 let s = c.content();
507 if !s.is_empty() { return s; }
508 child = c.next_sibling.get();
509 }
510 ""
511 }
512 }
513
514 /// Iterate the attribute's text-node child chain — the libxml2
515 /// representation of the attribute's value. Only meaningful in
516 /// the c-abi build (where `Attribute` carries `children`); the
517 /// lean build holds the value directly in [`value`](Self::value).
518 #[cfg(feature = "c-abi")]
519 pub fn children(&self) -> ChildIter<'doc> {
520 ChildIter::from_head(self.children.get())
521 }
522}
523
524impl std::fmt::Debug for Attribute<'_> {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 f.debug_struct("Attribute")
527 .field("name", &self.name())
528 .field("value", &self.value())
529 .finish()
530 }
531}
532
533/// A node in the XML tree. One struct for every kind; fields are used per-kind.
534///
535/// # Layout (libxml2 mirror)
536///
537/// The struct is `#[repr(C)]` with field order chosen to align with
538/// `_xmlNode` in libxml2. When we later expose an `extern "C"` shim,
539/// callers can treat `*mut Node` as `xmlNode*` (modulo a few private slots
540/// we may still need to add — `_private`, `psvi`, etc.).
541///
542/// # Field usage by kind
543///
544/// | Kind | `name` | `content` | `first_*` / `last_*` |
545/// |--------------|--------------|----------------------------|----------------------|
546/// | `Element` | tag name | unused (`""`) | children + attrs |
547/// | `Text` | `""` | text data | unused |
548/// | `CData` | `""` | CDATA payload | unused |
549/// | `Comment` | `""` | comment text | unused |
550/// | `Pi` | target | content after target | unused |
551#[cfg(not(feature = "c-abi"))]
552#[repr(C)]
553pub struct Node<'doc> {
554 pub kind: NodeKind,
555 pub name: &'doc str,
556 pub namespace: Cell<Option<&'doc Namespace<'doc>>>,
557 /// Linked-list head for element attributes (None for non-elements).
558 pub first_attribute: Cell<Option<&'doc Attribute<'doc>>>,
559 pub last_attribute: Cell<Option<&'doc Attribute<'doc>>>,
560 pub first_child: Cell<Option<&'doc Node<'doc>>>,
561 pub last_child: Cell<Option<&'doc Node<'doc>>>,
562 pub parent: Cell<Option<&'doc Node<'doc>>>,
563 pub next_sibling: Cell<Option<&'doc Node<'doc>>>,
564 pub prev_sibling: Cell<Option<&'doc Node<'doc>>>,
565 /// Text/CData/Comment/Pi payload. `None` mirrors libxml2's NULL
566 /// `content` — notably a processing instruction with no data
567 /// (`<?foo?>`), which serializes without the trailing space that a
568 /// PI carrying a (possibly empty) data section gets.
569 pub content: Cell<Option<&'doc str>>,
570 /// 1-based source line of the opening tag. 0 for nodes constructed
571 /// programmatically.
572 pub line: u32,
573 /// 0-based byte offset of the opening tag's name in the source
574 /// buffer. 0 for nodes not produced by the parser. Saturates at
575 /// `u32::MAX` for inputs past 4 GiB. Line and column are derived
576 /// from this offset against the document's source buffer
577 /// ([`Document::source`]) — the offset is the single ground truth.
578 pub source_offset: u32,
579}
580
581/// libxml2-shape Node (c-abi build). Byte-exact match with `_xmlNode`
582/// for the public window (offsets 0..120). See type-level doc and
583/// `thoughts/c_abi_implementation_plan.md` for the offset table.
584///
585/// Field naming choices for the c-abi build:
586/// - `first_child` / `last_child` rather than libxml2's `children` /
587/// `last` (the field SLOT matches; we use the more descriptive
588/// sup-xml name).
589/// - `first_attribute` rather than libxml2's `properties` (same
590/// reasoning).
591/// - `namespace` rather than libxml2's `ns` (same).
592/// - `ns_def` matches libxml2's name verbatim.
593#[cfg(feature = "c-abi")]
594#[repr(C)]
595pub struct Node<'doc> {
596 pub _private: Cell<*mut std::os::raw::c_void>, // 0
597 pub kind: NodeKind, // xmlElementType // 8
598 _pad_kind: u32, // 12
599 /// The element/PI/etc. name (`xmlChar*` at offset 16). A `Cell`
600 /// because `xmlNodeSetName` renames a node in place, and nodes are
601 /// only ever reached through shared `&Node` references — mutation of a
602 /// bare field through such a reference is undefined behavior.
603 pub name: Cell<ArenaCStr<'doc>>, // 16
604 pub first_child: Cell<Option<&'doc Node<'doc>>>, // 24
605 pub last_child: Cell<Option<&'doc Node<'doc>>>, // 32
606 pub parent: Cell<Option<&'doc Node<'doc>>>, // 40
607 pub next_sibling: Cell<Option<&'doc Node<'doc>>>, // 48
608 pub prev_sibling: Cell<Option<&'doc Node<'doc>>>, // 56
609 pub doc: Cell<*mut std::os::raw::c_void>, // 64 (xmlDoc*)
610 pub namespace: Cell<Option<&'doc Namespace<'doc>>>, // 72
611 pub content: Cell<Option<ArenaCStr<'doc>>>, // 80
612 pub first_attribute: Cell<Option<&'doc Attribute<'doc>>>, // 88
613 pub ns_def: Cell<Option<&'doc Namespace<'doc>>>, // 96
614 pub psvi: Cell<*mut std::os::raw::c_void>, // 104
615 /// Source line (`xmlChar`-era `unsigned short`, offset 112). A `Cell`
616 /// because the push parser back-patches it after the node is created,
617 /// and nodes are only reached through shared `&Node` references.
618 pub line: Cell<u16>, // 112
619 pub extra: u16, // 114
620 _pad_extra: [u8; 4], // 116..120
621 // ── sup-xml-only tail (NOT part of ABI window) ──
622 /// Tail of the attribute linked list for O(1) append. libxml2
623 /// walks `first_attribute->next->...->NULL` to find the tail;
624 /// we cache it for the parse hot path.
625 pub last_attribute: Cell<Option<&'doc Attribute<'doc>>>,
626 /// Full-width source line, uncapped. The ABI `line` field is a
627 /// `u16` (libxml2's `unsigned short`) so it saturates at 65535; this
628 /// shadow copy keeps the real line for files past that, which
629 /// `xmlGetLineNo` returns in preference. `0` for nodes not produced
630 /// by the parser (created via the tree API), which fall back to
631 /// `line`. libxml2 instead stashes big lines in `psvi` on text nodes
632 /// and recurses in `xmlGetLineNo`; a dedicated field is exact (no
633 /// neighbour-line guessing for childless nodes like `<br/>`).
634 pub full_line: Cell<u32>,
635 /// 0-based byte offset of the opening tag's name in the source
636 /// buffer. 0 for nodes not produced by the parser. Saturates at
637 /// `u32::MAX` for inputs past 4 GiB. Outside the libxml2 ABI
638 /// window, so it does not affect `_xmlNode` compatibility. Line and
639 /// column are derived from this offset against the document's source
640 /// buffer ([`Document::source`]) — the offset is the ground truth.
641 pub source_offset: u32,
642}
643
644// ── compile-time layout assertions (c-abi build) ────────────────────────────
645//
646// These `const _:` blocks verify that our `#[repr(C)]` struct layouts
647// match libxml2 byte-exact at every field offset in the public ABI
648// window. Drift = compile error.
649//
650// The offsets below are pinned to libxml2 2.9.13 and 2.15.3, the
651// versions we have verified compat against. We do NOT make a
652// forward-compatibility claim — if libxml2 ships a version that
653// appends fields to `_xmlNode` / `_xmlAttr` / `_xmlDoc`, our Box
654// allocations would be undersized for callers reading at the new
655// offsets and compat needs a coordinated bump on our side.
656//
657// Two checks defend against drifting unawares:
658// - `crates/compat/c-tests/t-upstream-layout.c` compiles
659// `_Static_assert(offsetof(...))` against the *live installed*
660// `<libxml/...>` headers. If the dev/CI host has a libxml2
661// version newer than what we've pinned, the build breaks at the
662// exact offset that moved.
663// - `crates/compat/c-tests/t-layout-{03,04}.c` plus `t-err-03.c`
664// use locally-typedef'd reference structs to verify these Rust
665// offsets match compat's expected C-side view.
666// - `crates/compat/c-tests/t-libxml2-headers.c` uses the real
667// headers at runtime — would have caught the earlier mistake
668// where we mis-encoded `_xmlNs`.
669
670#[cfg(feature = "c-abi")]
671const _: () = {
672 use std::mem::offset_of;
673
674 // ── _xmlNode (64-bit) ──
675 assert!(offset_of!(Node<'static>, _private) == 0, "Node::_private @ 0");
676 assert!(offset_of!(Node<'static>, kind) == 8, "Node::kind @ 8");
677 assert!(offset_of!(Node<'static>, name) == 16, "Node::name @ 16");
678 assert!(offset_of!(Node<'static>, first_child) == 24, "Node::first_child (children) @ 24");
679 assert!(offset_of!(Node<'static>, last_child) == 32, "Node::last_child (last) @ 32");
680 assert!(offset_of!(Node<'static>, parent) == 40, "Node::parent @ 40");
681 assert!(offset_of!(Node<'static>, next_sibling) == 48, "Node::next_sibling (next) @ 48");
682 assert!(offset_of!(Node<'static>, prev_sibling) == 56, "Node::prev_sibling (prev) @ 56");
683 assert!(offset_of!(Node<'static>, doc) == 64, "Node::doc @ 64");
684 assert!(offset_of!(Node<'static>, namespace) == 72, "Node::namespace (ns) @ 72");
685 assert!(offset_of!(Node<'static>, content) == 80, "Node::content @ 80");
686 assert!(offset_of!(Node<'static>, first_attribute) == 88, "Node::first_attribute (properties) @ 88");
687 assert!(offset_of!(Node<'static>, ns_def) == 96, "Node::ns_def @ 96");
688 assert!(offset_of!(Node<'static>, psvi) == 104, "Node::psvi @ 104");
689 assert!(offset_of!(Node<'static>, line) == 112, "Node::line @ 112");
690 assert!(offset_of!(Node<'static>, extra) == 114, "Node::extra @ 114");
691 // The libxml2 ABI window ends at offset 120. Our sup-xml-only
692 // tail (last_attribute) starts there or after — we don't pin its
693 // offset since it's not part of the contract.
694 assert!(offset_of!(Node<'static>, last_attribute) >= 120, "Node tail starts after ABI window");
695
696 // ── _xmlAttr (64-bit) — shares first 8 fields with _xmlNode ──
697 assert!(offset_of!(Attribute<'static>, _private) == 0, "Attribute::_private @ 0");
698 assert!(offset_of!(Attribute<'static>, kind) == 8, "Attribute::kind @ 8");
699 assert!(offset_of!(Attribute<'static>, name) == 16, "Attribute::name @ 16");
700 assert!(offset_of!(Attribute<'static>, children) == 24, "Attribute::children @ 24");
701 assert!(offset_of!(Attribute<'static>, last) == 32, "Attribute::last @ 32");
702 assert!(offset_of!(Attribute<'static>, parent) == 40, "Attribute::parent @ 40");
703 assert!(offset_of!(Attribute<'static>, next) == 48, "Attribute::next @ 48");
704 assert!(offset_of!(Attribute<'static>, prev) == 56, "Attribute::prev @ 56");
705 assert!(offset_of!(Attribute<'static>, doc) == 64, "Attribute::doc @ 64");
706 assert!(offset_of!(Attribute<'static>, namespace) == 72, "Attribute::namespace (ns) @ 72");
707 assert!(offset_of!(Attribute<'static>, atype) == 80, "Attribute::atype @ 80");
708 assert!(offset_of!(Attribute<'static>, psvi) == 88, "Attribute::psvi @ 88");
709 // value lives in the sup-xml-only tail.
710 assert!(offset_of!(Attribute<'static>, value) >= 96, "Attribute value in tail");
711
712 // ── Struct sizes ──
713 // _xmlNode public window is 120 bytes. Our struct is 120 + tail
714 // (last_attribute = 8 bytes, no padding).
715 assert!(std::mem::size_of::<Node<'static>>() >= 120, "Node size >= 120 (ABI window)");
716 // Attribute's ABI window is exposed up through `psvi` at offset 88
717 // (so the window is 96 bytes including alignment padding); the
718 // value field in the tail brings us to 96 + 8 = 104+ minimum.
719 assert!(std::mem::size_of::<Attribute<'static>>() >= 96, "Attribute size >= ABI window");
720
721 // ── ArenaCStr is 1 pointer wide (matches xmlChar*) ──
722 assert!(std::mem::size_of::<ArenaCStr<'static>>() == std::mem::size_of::<*const u8>(),
723 "ArenaCStr must be a single thin pointer to match xmlChar*");
724 // ── Option<ArenaCStr> must niche-opt to a single pointer too,
725 // since libxml2 uses NULL `xmlChar*` to mean "absent" and our
726 // Option<ArenaCStr> sits in the xmlChar* slot of _xmlNs::prefix.
727 assert!(std::mem::size_of::<Option<ArenaCStr<'static>>>() == std::mem::size_of::<*const u8>(),
728 "Option<ArenaCStr> must niche-opt to a single pointer");
729
730 // ── _xmlNs (64-bit) — the odd duck: `next` precedes `_private`
731 // whereas every other public libxml2 struct puts `_private` first.
732 // Layout verified against libxml2 2.9.13 and 2.15.3.
733 assert!(offset_of!(Namespace<'static>, next) == 0, "Namespace::next @ 0");
734 assert!(offset_of!(Namespace<'static>, kind) == 8, "Namespace::kind @ 8");
735 assert!(offset_of!(Namespace<'static>, href) == 16, "Namespace::href @ 16");
736 assert!(offset_of!(Namespace<'static>, prefix) == 24, "Namespace::prefix @ 24");
737 assert!(offset_of!(Namespace<'static>, _private) == 32, "Namespace::_private @ 32");
738 assert!(offset_of!(Namespace<'static>, context) == 40, "Namespace::context @ 40");
739 assert!(std::mem::size_of::<Namespace<'static>>() == 48, "sizeof(Namespace) == 48");
740};
741
742impl<'doc> Node<'doc> {
743 pub fn is_element(&self) -> bool { matches!(self.kind, NodeKind::Element) }
744 pub fn is_text(&self) -> bool { matches!(self.kind, NodeKind::Text) }
745 pub fn is_entity_ref(&self) -> bool { matches!(self.kind, NodeKind::EntityRef) }
746
747 /// Element name (or PI target for PI nodes). Empty `""` for
748 /// Text/CData/Comment.
749 ///
750 /// **Prefer this method over direct `node.name` field access in
751 /// internal / downstream code.** The `pub name` field stays
752 /// stable on the lean rlib build, but when sup-xml is built with
753 /// the `c-abi` feature the storage type changes to a thin
754 /// NUL-terminated `ArenaCStr`. This method returns `&str` on
755 /// both configs — direct field access does not.
756 #[inline]
757 pub fn name(&self) -> &'doc str {
758 #[cfg(not(feature = "c-abi"))]
759 { self.name }
760 #[cfg(feature = "c-abi")]
761 { self.name.get().as_str() }
762 }
763
764 /// Source line number as recorded by the parser (`0` for nodes built
765 /// through the tree API). Abstracts the per-config storage: a plain
766 /// `u32` on the lean build, a `Cell<u16>` (libxml2's `unsigned short`)
767 /// on the `c-abi` build — so callers don't reach into the field.
768 #[inline]
769 pub fn line_no(&self) -> u32 {
770 #[cfg(not(feature = "c-abi"))]
771 { self.line }
772 #[cfg(feature = "c-abi")]
773 { self.line.get() as u32 }
774 }
775
776 /// Text payload for `Text`/`CData`/`Comment`/`Pi` nodes; `""` for
777 /// elements. See [`name`](Self::name) for why to prefer this
778 /// method over direct field access.
779 #[inline]
780 pub fn content(&self) -> &'doc str {
781 self.content_opt().unwrap_or("")
782 }
783
784 /// The payload as libxml2 stores it: `None` when the underlying
785 /// `content` pointer is NULL, `Some` (possibly `""`) otherwise.
786 /// Most callers want [`content`](Self::content); the serializer uses
787 /// this to reproduce libxml2's PI rule (a NULL-data PI omits the
788 /// space a non-NULL-but-empty-data PI emits).
789 pub fn content_opt(&self) -> Option<&'doc str> {
790 #[cfg(not(feature = "c-abi"))]
791 { self.content.get() }
792 #[cfg(feature = "c-abi")]
793 { self.content.get().map(|c| c.as_str()) }
794 }
795
796 /// Replace the node's content (text/CData/comment/PI payload).
797 /// Used by the HTML sink and other in-place tree mutators. Allocates
798 /// a NUL-terminated copy in the arena when the c-abi feature is on.
799 ///
800 /// `arena` must be the same `DocumentBuilder` that owns this node
801 /// (lifetimes enforce it).
802 pub fn set_content(&self, arena: &'doc DocumentBuilder, content: &'doc str) {
803 #[cfg(not(feature = "c-abi"))]
804 {
805 let _ = arena; // unused in lean build — content fits straight as &str
806 self.content.set(Some(content));
807 }
808 #[cfg(feature = "c-abi")]
809 {
810 self.content.set(Some(arena.alloc_arena_cstr(content)));
811 }
812 }
813
814 /// Iterate child nodes in document order.
815 pub fn children(&self) -> ChildIter<'doc> {
816 ChildIter { cur: self.first_child.get() }
817 }
818
819 /// Iterate attributes in document order. Returns an empty iterator for
820 /// non-elements.
821 pub fn attributes(&self) -> AttrIter<'doc> {
822 AttrIter { cur: self.first_attribute.get() }
823 }
824
825 /// Local part of this element/attr-like node's name — `"foo"` for both
826 /// `<foo/>` and `<ns:foo/>`. On the lean build [`name`](Self::name)
827 /// returns the full QName including any prefix; on the `c-abi` build
828 /// the prefix is stripped at parse time and `name()` is already the
829 /// local part. `local_name()` is the layout-agnostic accessor:
830 /// callers that want to match by local name should use this.
831 #[inline]
832 pub fn local_name(&self) -> &'doc str {
833 let n = self.name();
834 match n.rfind(':') {
835 Some(i) => &n[i + 1..],
836 None => n,
837 }
838 }
839
840 /// Iterate this element's xmlns declarations as `(prefix, href)` —
841 /// `prefix` is `None` for the default namespace (`xmlns="..."`),
842 /// `Some("dc")` for `xmlns:dc="..."`. Empty for non-elements.
843 ///
844 /// Storage differs by build: the c-abi build keeps xmlns declarations
845 /// on the element's `ns_def` chain (matching libxml2's `_xmlNode`);
846 /// the lean build keeps them in [`attributes`](Self::attributes)
847 /// instead. A namespace-blind parse (`namespace_aware: false`) does
848 /// no namespace processing and leaves xmlns declarations in the
849 /// attribute list under *either* build, so the c-abi iterator walks
850 /// `ns_def` first and then the attribute list — the two are mutually
851 /// exclusive (a given parse populates one or the other), so nothing is
852 /// reported twice. Callers see every declaration regardless of build
853 /// or parse mode.
854 pub fn ns_declarations(&self) -> NsDeclIter<'doc> {
855 #[cfg(feature = "c-abi")]
856 {
857 NsDeclIter::CAbi { cur: self.ns_def.get(), attrs: self.first_attribute.get() }
858 }
859 #[cfg(not(feature = "c-abi"))]
860 {
861 NsDeclIter::Lean { cur: self.first_attribute.get() }
862 }
863 }
864
865 /// First child element matching `name`, or `None`. O(n) walk; for repeated
866 /// lookups, iterate `children()` yourself.
867 pub fn find_child(&self, name: &str) -> Option<&'doc Node<'doc>> {
868 self.children().find(|n| n.is_element() && n.name() == name)
869 }
870
871 /// For text-bearing kinds returns the content; for elements returns the
872 /// first text-or-CDATA child's content; otherwise `None`.
873 pub fn text_content(&self) -> Option<&'doc str> {
874 match self.kind {
875 NodeKind::Text | NodeKind::CData => Some(self.content()),
876 NodeKind::Element => self.children().find_map(|c| match c.kind {
877 NodeKind::Text | NodeKind::CData => Some(c.content()),
878 _ => None,
879 }),
880 _ => None,
881 }
882 }
883}
884
885impl std::fmt::Debug for Node<'_> {
886 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
887 let mut s = f.debug_struct("Node");
888 s.field("kind", &self.kind);
889 let name = self.name();
890 if !name.is_empty() { s.field("name", &name); }
891 let content = self.content();
892 if !content.is_empty() { s.field("content", &content); }
893 s.finish()
894 }
895}
896
897/// Iterator over a node's children.
898pub struct ChildIter<'doc> { cur: Option<&'doc Node<'doc>> }
899impl<'doc> ChildIter<'doc> {
900 /// Construct from an explicit head pointer — used by
901 /// [`Attribute::children`] which holds its child chain in a
902 /// different field than [`Node::children`].
903 #[inline]
904 pub fn from_head(head: Option<&'doc Node<'doc>>) -> Self {
905 Self { cur: head }
906 }
907}
908impl<'doc> Iterator for ChildIter<'doc> {
909 type Item = &'doc Node<'doc>;
910 fn next(&mut self) -> Option<Self::Item> {
911 let c = self.cur?;
912 self.cur = c.next_sibling.get();
913 Some(c)
914 }
915}
916
917/// Iterator over an element's attributes.
918pub struct AttrIter<'doc> { cur: Option<&'doc Attribute<'doc>> }
919impl<'doc> Iterator for AttrIter<'doc> {
920 type Item = &'doc Attribute<'doc>;
921 fn next(&mut self) -> Option<Self::Item> {
922 let a = self.cur?;
923 self.cur = a.next.get();
924 Some(a)
925 }
926}
927
928/// Iterator yielded by [`Node::ns_declarations`]. Each item is
929/// `(prefix, href)` where `prefix` is `None` for the default
930/// namespace declaration. The variant in use depends on the build
931/// feature flags — callers should treat it as opaque.
932pub enum NsDeclIter<'doc> {
933 #[cfg(feature = "c-abi")]
934 CAbi {
935 cur: Option<&'doc Namespace<'doc>>,
936 attrs: Option<&'doc Attribute<'doc>>,
937 },
938 #[cfg(not(feature = "c-abi"))]
939 Lean { cur: Option<&'doc Attribute<'doc>> },
940}
941
942/// Advance an attribute cursor to the next `xmlns`/`xmlns:*` declaration,
943/// yielding `(prefix, href)` and skipping ordinary attributes.
944#[inline]
945fn next_xmlns_attr<'doc>(
946 cur: &mut Option<&'doc Attribute<'doc>>,
947) -> Option<(Option<&'doc str>, &'doc str)> {
948 loop {
949 let a = (*cur)?;
950 *cur = a.next.get();
951 let n = a.name();
952 if n == "xmlns" {
953 return Some((None, a.value()));
954 } else if let Some(local) = n.strip_prefix("xmlns:") {
955 return Some((Some(local), a.value()));
956 }
957 }
958}
959
960impl<'doc> Iterator for NsDeclIter<'doc> {
961 type Item = (Option<&'doc str>, &'doc str);
962 fn next(&mut self) -> Option<Self::Item> {
963 match self {
964 #[cfg(feature = "c-abi")]
965 NsDeclIter::CAbi { cur, attrs } => {
966 if let Some(ns) = *cur {
967 *cur = ns.next.get();
968 return Some((ns.prefix(), ns.href()));
969 }
970 // `ns_def` exhausted — surface any xmlns declarations that a
971 // namespace-blind parse left in the attribute list.
972 next_xmlns_attr(attrs)
973 }
974 #[cfg(not(feature = "c-abi"))]
975 NsDeclIter::Lean { cur } => next_xmlns_attr(cur),
976 }
977 }
978}
979
980// ── builder ─────────────────────────────────────────────────────────────────
981
982/// Constructs a [`Document`] one node at a time. The parser uses this
983/// internally; consumers building trees by hand use the same.
984///
985/// The builder owns a [`Bump`]. Allocated nodes live for the lifetime of
986/// the builder (and, after `build()`, of the resulting [`Document`]).
987///
988/// # Build flow
989///
990/// ```
991/// # use sup_xml_tree::dom::DocumentBuilder;
992/// let b = DocumentBuilder::new();
993/// let root = b.new_element(b.alloc_str("catalog"));
994/// // … allocate more nodes, link them via append_child / append_attribute …
995/// b.set_root(root);
996/// let doc = b.build();
997/// ```
998///
999/// `set_root` returns immediately (no borrow on the builder is held past the
1000/// call), so the subsequent `build()` is free to consume `self` even though
1001/// `root` borrowed from the builder during construction.
1002pub struct DocumentBuilder {
1003 /// Refcounted heap allocation. Stored as `Arc<Bump>` rather than
1004 /// `Box<Bump>` so a node grafted into another document can keep its
1005 /// origin arena alive past the origin document's drop — see
1006 /// [`DocumentBuilder::new_with_dict_and_arena`] and the
1007 /// `compat::dict::new_doc_arena` rationale. Cloning is a pure
1008 /// refcount bump.
1009 ///
1010 /// `Bump` is `!Sync`, which makes `Arc<Bump>` `!Send` per Rust's
1011 /// auto-trait rules. The containing `Document` re-asserts `Send`
1012 /// via `unsafe impl` — that's safe because the GIL contract on the
1013 /// consumer side (and our own internal single-threaded discipline)
1014 /// guarantees we never use two `&Bump` derived from the same Arc on
1015 /// two threads concurrently.
1016 bump: Arc<Bump>,
1017 /// Per-document name interner. Element / attribute /
1018 /// namespace names route through this rather than the bumpalo
1019 /// arena so consumers see pointer-equality across identical
1020 /// names — e.g. every `<p>` tag in a 100k-element doc shares
1021 /// one canonical name pointer instead of 100k arena copies.
1022 ///
1023 /// Holds *one* refcounted reference; the underlying dict may be
1024 /// shared with the parser context that originally created it
1025 /// (when [`new_with_dict`](Self::new_with_dict) is used) or be
1026 /// owned solely by this builder otherwise. Released on drop.
1027 #[cfg(feature = "c-abi")]
1028 dict: *mut crate::dict::Dict,
1029 /// Root pointer. Stored with `'static` lifetime erasure; the borrow-check
1030 /// rigor lives in `set_root` (lifetime ties root to `&self`) and the
1031 /// `Document::root()` accessor (re-binds lifetime to `&'a self`).
1032 root: Cell<*const Node<'static>>,
1033 /// XML declaration version (default `"1.0"`). Set via
1034 /// [`set_version`](Self::set_version); plumbed into [`Document::version`].
1035 version: std::cell::RefCell<String>,
1036 /// XML declaration encoding (default `"UTF-8"`). Set via
1037 /// [`set_encoding`](Self::set_encoding); plumbed into [`Document::encoding`].
1038 encoding: std::cell::RefCell<String>,
1039 /// XML declaration `standalone="…"` value, or `None` when absent.
1040 /// Set via [`set_standalone`](Self::set_standalone).
1041 standalone: Cell<Option<bool>>,
1042 /// URI the document is being loaded from, if known. Set via
1043 /// [`set_base_url`](Self::set_base_url); plumbed into
1044 /// [`Document::base_url`].
1045 base_url: std::cell::RefCell<Option<String>>,
1046 /// HTML-specific metadata. Set by the HTML parser sink via
1047 /// [`set_html_metadata`](Self::set_html_metadata); `None` for XML
1048 /// documents. Plumbed into [`Document::html_metadata`] on `build`.
1049 html_metadata: std::cell::RefCell<Option<HtmlMeta>>,
1050 /// Owned source buffer (raw pointer + length) that arena strings may
1051 /// point into via [`alloc_str_borrow`](Self::alloc_str_borrow).
1052 ///
1053 /// **Raw pointer rather than `Box<[u8]>` is intentional.** Storing
1054 /// as `Pin<Box<[u8]>>` triggers a Stacked Borrows violation: when
1055 /// [`build`](Self::build) consumes `self`, the Box field is moved
1056 /// into the [`Document`] and the move performs a Unique retag of
1057 /// the parent. Any `SharedReadOnly` borrows we'd handed out into
1058 /// the source bytes (i.e. arena names and text content) get
1059 /// invalidated by that Unique retag — a real `cargo +nightly miri`
1060 /// failure. Storing the bytes via a raw pointer that the parent
1061 /// only ever observes by-value avoids the retag chain entirely.
1062 /// See `parse_owned_bytes` in `sup-xml-core::parser` for the
1063 /// matching parser-side discipline.
1064 ///
1065 /// Ownership: the `Box<[u8]>` is leaked at [`set_source`](Self::set_source)
1066 /// time; the resulting raw pointer is then either reclaimed by
1067 /// [`build`](Self::build) (which transfers ownership to the
1068 /// [`Document`]'s `Drop`) or by the builder's own `Drop` if `build`
1069 /// was never called.
1070 source_ptr: Cell<*mut u8>,
1071 source_len: Cell<usize>,
1072 /// Orphan leaves (comments / PIs) encountered before the root
1073 /// element opened. Each entry is a type-erased pointer to a
1074 /// node already allocated in this builder's arena. On
1075 /// [`build`](Self::build) these are linked as `root.prev`
1076 /// siblings (in order) so consumers walking the document's
1077 /// children see comments-before-root + root + comments-after-
1078 /// root, just like libxml2. Empty when the document has no
1079 /// prolog/epilogue content (the common case).
1080 prolog_orphans: RefCell<Vec<*const Node<'static>>>,
1081 /// Same as [`prolog_orphans`] for nodes that follow the root.
1082 epilogue_orphans: RefCell<Vec<*const Node<'static>>>,
1083}
1084
1085impl DocumentBuilder {
1086 pub fn new() -> Self {
1087 Self {
1088 bump: Arc::new(Bump::new()),
1089 #[cfg(feature = "c-abi")]
1090 dict: crate::dict::Dict::new_refcounted(),
1091 root: Cell::new(std::ptr::null()),
1092 version: std::cell::RefCell::new("1.0".to_string()),
1093 // Default to empty — libxml2 leaves doc->encoding NULL
1094 // when the source XML had no `<?xml encoding="…"?>`
1095 // declaration; serializers then omit the encoding
1096 // attribute on output. The parser explicitly sets this
1097 // when it sees a declaration.
1098 encoding: std::cell::RefCell::new(String::new()),
1099 standalone: Cell::new(None),
1100 base_url: std::cell::RefCell::new(None),
1101 html_metadata: std::cell::RefCell::new(None),
1102 source_ptr: Cell::new(std::ptr::null_mut()),
1103 source_len: Cell::new(0),
1104 prolog_orphans: RefCell::new(Vec::new()),
1105 epilogue_orphans: RefCell::new(Vec::new()),
1106 }
1107 }
1108
1109 /// Build with an externally-supplied name dict (refcount bumped
1110 /// for our reference). Used when a parser context already owns
1111 /// a dict that should be shared with the new document — e.g.
1112 /// libxml2's `xmlCtxtReadMemory` flow, where `ctxt->dict` is
1113 /// the thread-shared interner the consumer wants names to live
1114 /// in. Names interned through this builder reuse that dict's
1115 /// canonical pointers.
1116 ///
1117 /// # Safety
1118 ///
1119 /// `dict` must be a valid pointer returned by
1120 /// [`crate::dict::Dict::new_refcounted`] (or otherwise refcount-
1121 /// managed by the libxml2 ABI), with at least one outstanding
1122 /// reference.
1123 #[cfg(feature = "c-abi")]
1124 pub unsafe fn new_with_dict(dict: *mut crate::dict::Dict) -> Self {
1125 // SAFETY: caller asserts `dict` is live with a positive
1126 // refcount; bumping is sound.
1127 unsafe { (*dict).add_ref(); }
1128 Self {
1129 bump: Arc::new(Bump::new()),
1130 dict,
1131 root: Cell::new(std::ptr::null()),
1132 version: std::cell::RefCell::new("1.0".to_string()),
1133 // Default to empty — libxml2 leaves doc->encoding NULL
1134 // when the source XML had no `<?xml encoding="…"?>`
1135 // declaration; serializers then omit the encoding
1136 // attribute on output. The parser explicitly sets this
1137 // when it sees a declaration.
1138 encoding: std::cell::RefCell::new(String::new()),
1139 standalone: Cell::new(None),
1140 base_url: std::cell::RefCell::new(None),
1141 html_metadata: std::cell::RefCell::new(None),
1142 source_ptr: Cell::new(std::ptr::null_mut()),
1143 source_len: Cell::new(0),
1144 prolog_orphans: RefCell::new(Vec::new()),
1145 epilogue_orphans: RefCell::new(Vec::new()),
1146 }
1147 }
1148
1149 /// Build with both an externally-supplied dict and arena. The
1150 /// arena is an [`Arc`]-shared [`Bump`]; cloning into a Document
1151 /// is a pure refcount bump, no allocation. Used by C-ABI
1152 /// consumers that route all per-thread doc creation through a
1153 /// single shared arena — node memory then survives any
1154 /// individual doc's drop, which makes cross-doc graft
1155 /// operations (libxml2 consumers' `_appendChild` /
1156 /// `moveNodeToDocument`) safe by construction.
1157 ///
1158 /// # Safety
1159 ///
1160 /// `dict` must be a valid pointer returned by
1161 /// [`crate::dict::Dict::new_refcounted`] with at least one
1162 /// outstanding reference.
1163 #[cfg(feature = "c-abi")]
1164 pub unsafe fn new_with_dict_and_arena(
1165 dict: *mut crate::dict::Dict,
1166 arena: Arc<Bump>,
1167 ) -> Self {
1168 // SAFETY: caller asserts `dict` is live with a positive refcount.
1169 unsafe { (*dict).add_ref(); }
1170 Self {
1171 bump: arena,
1172 dict,
1173 root: Cell::new(std::ptr::null()),
1174 version: std::cell::RefCell::new("1.0".to_string()),
1175 // Default to empty — libxml2 leaves doc->encoding NULL
1176 // when the source XML had no `<?xml encoding="…"?>`
1177 // declaration; serializers then omit the encoding
1178 // attribute on output. The parser explicitly sets this
1179 // when it sees a declaration.
1180 encoding: std::cell::RefCell::new(String::new()),
1181 standalone: Cell::new(None),
1182 base_url: std::cell::RefCell::new(None),
1183 html_metadata: std::cell::RefCell::new(None),
1184 source_ptr: Cell::new(std::ptr::null_mut()),
1185 source_len: Cell::new(0),
1186 prolog_orphans: RefCell::new(Vec::new()),
1187 epilogue_orphans: RefCell::new(Vec::new()),
1188 }
1189 }
1190
1191 /// Pre-allocate `capacity` bytes for the arena. Useful when parsing large
1192 /// documents — avoids the initial small-chunk allocations.
1193 pub fn with_capacity(capacity: usize) -> Self {
1194 Self {
1195 bump: Arc::new(Bump::with_capacity(capacity)),
1196 #[cfg(feature = "c-abi")]
1197 dict: crate::dict::Dict::new_refcounted(),
1198 root: Cell::new(std::ptr::null()),
1199 version: std::cell::RefCell::new("1.0".to_string()),
1200 // Default to empty — libxml2 leaves doc->encoding NULL
1201 // when the source XML had no `<?xml encoding="…"?>`
1202 // declaration; serializers then omit the encoding
1203 // attribute on output. The parser explicitly sets this
1204 // when it sees a declaration.
1205 encoding: std::cell::RefCell::new(String::new()),
1206 standalone: Cell::new(None),
1207 base_url: std::cell::RefCell::new(None),
1208 html_metadata: std::cell::RefCell::new(None),
1209 source_ptr: Cell::new(std::ptr::null_mut()),
1210 source_len: Cell::new(0),
1211 prolog_orphans: RefCell::new(Vec::new()),
1212 epilogue_orphans: RefCell::new(Vec::new()),
1213 }
1214 }
1215
1216 /// Stash an owned source buffer that arena strings may borrow from.
1217 /// The parser calls this with the (possibly transcoded) input bytes so
1218 /// that subsequent [`alloc_str_borrow`](Self::alloc_str_borrow) calls
1219 /// can hand back zero-copy `&str` slices into these bytes instead of
1220 /// memcpy'ing them into the arena.
1221 ///
1222 /// Ownership transfers to the [`Document`] on [`build`](Self::build),
1223 /// so the borrowed slices stay valid for the document's lifetime.
1224 /// Calling `set_source` twice on the same builder is allowed and
1225 /// frees the prior buffer.
1226 pub fn set_source(&self, bytes: Box<[u8]>) {
1227 // Free any previous source first.
1228 self.free_source();
1229 let leaked: &'static mut [u8] = Box::leak(bytes);
1230 self.source_ptr.set(leaked.as_mut_ptr());
1231 self.source_len.set(leaked.len());
1232 }
1233
1234 /// Free the leaked source bytes (if any). Used by `Drop` and by
1235 /// `set_source` for replace semantics.
1236 fn free_source(&self) {
1237 let ptr = self.source_ptr.get();
1238 let len = self.source_len.get();
1239 if !ptr.is_null() {
1240 // SAFETY: `ptr` came from `Box::leak`'d `Box<[u8]>` of length `len`.
1241 // Recover the Box and drop it.
1242 unsafe {
1243 let _: Box<[u8]> = Box::from_raw(
1244 std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]
1245 );
1246 }
1247 self.source_ptr.set(std::ptr::null_mut());
1248 self.source_len.set(0);
1249 }
1250 }
1251
1252 /// Raw mutable pointer to the stashed source buffer. Returns null
1253 /// if no source has been set. Used by `parse_bytes_in_place` to
1254 /// construct a `&mut [u8]` view into the leaked buffer that the
1255 /// in-place reader will mutate. Not part of the public stable API
1256 /// — the parser is the only intended caller.
1257 #[doc(hidden)]
1258 pub fn source_ptr_for_inplace(&self) -> *mut u8 {
1259 self.source_ptr.get()
1260 }
1261 /// Companion to [`source_ptr_for_inplace`].
1262 #[doc(hidden)]
1263 pub fn source_len_for_inplace(&self) -> usize {
1264 self.source_len.get()
1265 }
1266
1267 /// Return the stashed source bytes, if any. Returns a slice whose
1268 /// lifetime is bounded by `&self` — but in practice the bytes live
1269 /// at a stable heap address until [`build`](Self::build) moves
1270 /// ownership of them to the [`Document`].
1271 pub fn source(&self) -> Option<&[u8]> {
1272 let ptr = self.source_ptr.get();
1273 let len = self.source_len.get();
1274 if ptr.is_null() {
1275 None
1276 } else {
1277 // SAFETY: `ptr` came from `Box::leak`, points at `len` valid
1278 // bytes that we own. The returned slice is bounded by
1279 // `&self`'s lifetime; it stays valid until `build()` is
1280 // called (which transfers the pointer to a `Document` whose
1281 // own `Drop` will eventually free the bytes).
1282 Some(unsafe { std::slice::from_raw_parts(ptr, len) })
1283 }
1284 }
1285
1286 /// Set the XML declaration version (e.g. `"1.0"`, `"1.1"`). Default is
1287 /// `"1.0"`. Plumbed into [`Document::version`] on [`build`](Self::build).
1288 pub fn set_version(&self, v: impl Into<String>) {
1289 *self.version.borrow_mut() = v.into();
1290 }
1291
1292 /// Set the XML declaration encoding (e.g. `"UTF-8"`). Default is
1293 /// `"UTF-8"`. Plumbed into [`Document::encoding`] on [`build`](Self::build).
1294 pub fn set_encoding(&self, e: impl Into<String>) {
1295 *self.encoding.borrow_mut() = e.into();
1296 }
1297
1298 /// Set the URI the document is being loaded from. Plumbed into
1299 /// [`Document::base_url`] on [`build`](Self::build).
1300 pub fn set_base_url(&self, uri: Option<String>) {
1301 *self.base_url.borrow_mut() = uri;
1302 }
1303
1304 /// Set the XML declaration `standalone="…"` value. `None` means the
1305 /// declaration did not include `standalone`; `Some(true)` / `Some(false)`
1306 /// correspond to `standalone="yes"` / `standalone="no"`.
1307 pub fn set_standalone(&self, s: Option<bool>) {
1308 self.standalone.set(s);
1309 }
1310
1311 /// Attach HTML-specific document metadata. Used by the HTML parser sink
1312 /// to record quirks-mode and DOCTYPE info; XML callers leave this as
1313 /// `None` (the default). Plumbed into [`Document::html_metadata`] on
1314 /// [`build`](Self::build).
1315 pub fn set_html_metadata(&self, m: Option<HtmlMeta>) {
1316 *self.html_metadata.borrow_mut() = m;
1317 }
1318
1319 /// Direct access to the underlying [`Bump`]. Use sparingly — most allocation
1320 /// should go through the typed [`new_element`](Self::new_element) etc. methods.
1321 pub fn bump(&self) -> &Bump { &self.bump }
1322
1323 /// Copy `s` into the arena and return an arena-lifetime slice. When the
1324 /// caller can prove `s` already lives at least as long as the eventual
1325 /// [`Document`] (e.g. it borrows from the input string slice that the
1326 /// caller will keep alive), [`alloc_str_borrow`](Self::alloc_str_borrow)
1327 /// is cheaper — no copy.
1328 pub fn alloc_str<'a>(&'a self, s: &str) -> &'a str {
1329 self.bump.alloc_str(s)
1330 }
1331
1332 /// Pass through a `&str` that the caller has guaranteed lives at least
1333 /// until the [`Document`] drops. Typically the input source slice.
1334 /// Zero-copy.
1335 ///
1336 /// # Safety
1337 ///
1338 /// The caller must guarantee `s` outlives the returned reference. In
1339 /// practice this means `s` borrows from the same input the parser is
1340 /// reading and the [`Document`] will be tied to that same input via the
1341 /// caller's outer lifetime. For untrusted lifetimes use [`alloc_str`](Self::alloc_str).
1342 pub unsafe fn alloc_str_borrow<'a>(&'a self, s: &'a str) -> &'a str {
1343 // SAFETY: extending the lifetime from the caller's '_ to 'a (which is
1344 // 'self-bounded) is sound because the caller guarantees s outlives 'a.
1345 unsafe { &*(s as *const str) }
1346 }
1347
1348 /// Construct a new node with the given `kind`, `name`, and `content`.
1349 /// Internal helper — public builder methods call this with the right
1350 /// shape for each kind. Centralises the field-init dance so the
1351 /// cfg-gated layouts only diverge in one place.
1352 fn new_node_impl<'a>(
1353 &'a self,
1354 kind: NodeKind,
1355 name: &'a str,
1356 content: Option<&'a str>,
1357 ) -> &'a mut Node<'a> {
1358 #[cfg(not(feature = "c-abi"))]
1359 {
1360 self.bump.alloc(Node {
1361 kind,
1362 name,
1363 namespace: Cell::new(None),
1364 first_attribute: Cell::new(None),
1365 last_attribute: Cell::new(None),
1366 first_child: Cell::new(None),
1367 last_child: Cell::new(None),
1368 parent: Cell::new(None),
1369 next_sibling: Cell::new(None),
1370 prev_sibling: Cell::new(None),
1371 content: Cell::new(content),
1372 line: 0,
1373 source_offset: 0,
1374 })
1375 }
1376 #[cfg(feature = "c-abi")]
1377 {
1378 // Names dedup through the dict (pointer-equal across
1379 // duplicate tags); content stays in the arena (rarely
1380 // repeats).
1381 // Text / CData nodes pin `name` to the libxml2
1382 // `xmlStringText` / `xmlStringTextNoenc` statics — C
1383 // consumers (libxslt's xsltCopyText, lxml's smart-string
1384 // wrapping) compare against those by pointer to identify
1385 // text-kinds. Any other `name` would silently break the
1386 // dispatch and surface as `Internal error in xsltCopyText`.
1387 let name_c = match kind {
1388 NodeKind::Text | NodeKind::CData => ArenaCStr::text_name(),
1389 _ if name.is_empty() => ArenaCStr::empty(),
1390 _ => self.intern_arena_cstr(name),
1391 };
1392 let content_c = content.map(|c| if c.is_empty() { ArenaCStr::empty() } else { self.alloc_arena_cstr(c) });
1393 self.bump.alloc(Node {
1394 _private: Cell::new(std::ptr::null_mut()),
1395 kind,
1396 _pad_kind: 0,
1397 name: Cell::new(name_c),
1398 first_child: Cell::new(None),
1399 last_child: Cell::new(None),
1400 parent: Cell::new(None),
1401 next_sibling: Cell::new(None),
1402 prev_sibling: Cell::new(None),
1403 doc: Cell::new(std::ptr::null_mut()),
1404 namespace: Cell::new(None),
1405 content: Cell::new(content_c),
1406 first_attribute: Cell::new(None),
1407 ns_def: Cell::new(None),
1408 psvi: Cell::new(std::ptr::null_mut()),
1409 line: Cell::new(0),
1410 extra: 0,
1411 _pad_extra: [0u8; 4],
1412 last_attribute: Cell::new(None),
1413 full_line: Cell::new(0),
1414 source_offset: 0,
1415 })
1416 }
1417 }
1418
1419 pub fn new_element<'a>(&'a self, name: &'a str) -> &'a mut Node<'a> {
1420 self.new_node_impl(NodeKind::Element, name, None)
1421 }
1422
1423 pub fn new_text<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
1424 self.new_node_impl(NodeKind::Text, "", Some(content))
1425 }
1426
1427 pub fn new_cdata<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
1428 self.new_node_impl(NodeKind::CData, "", Some(content))
1429 }
1430
1431 pub fn new_comment<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
1432 self.new_node_impl(NodeKind::Comment, "", Some(content))
1433 }
1434
1435 /// Build a processing-instruction node. `content` is `None` for a
1436 /// PI with no data section (`<?foo?>`) and `Some` — possibly `""` —
1437 /// for one that has one (`<?foo?>` created via `xmlNewDocPI` with an
1438 /// empty string). The distinction is libxml2's NULL-vs-empty
1439 /// `content` and governs the trailing space the serializer emits.
1440 pub fn new_pi<'a>(&'a self, target: &'a str, content: Option<&'a str>) -> &'a mut Node<'a> {
1441 self.new_node_impl(NodeKind::Pi, target, content)
1442 }
1443
1444 /// Build a DTD internal-subset declaration node. `content` is the
1445 /// raw markup declarations (each newline-terminated), emitted
1446 /// verbatim by the serializer inside the DOCTYPE's `[ … ]`. Held
1447 /// as the single child of the internal-subset DTD node.
1448 pub fn new_dtd_decl<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
1449 self.new_node_impl(NodeKind::DtdDecl, "", Some(content))
1450 }
1451
1452 /// Allocate an empty document-fragment node. Used by the libxml2
1453 /// compat shim's `xmlNewDocFragment`; the fragment is a transparent
1454 /// container that holds an ordered child list before being grafted
1455 /// into a real document subtree.
1456 pub fn new_fragment<'a>(&'a self) -> &'a mut Node<'a> {
1457 self.new_node_impl(NodeKind::DocumentFragment, "", None)
1458 }
1459
1460 /// Build an unresolved entity-reference node. `name` is the
1461 /// entity's NCName (e.g. `"foo"` for `&foo;`); `content` is the
1462 /// literal source form including the leading `&` and trailing
1463 /// `;`, which the serializer writes verbatim to round-trip the
1464 /// reference back to source. Emitted by the parser only when
1465 /// `ParseOptions::resolve_entities` is `false`.
1466 pub fn new_entity_ref<'a>(&'a self, name: &'a str, content: &'a str) -> &'a mut Node<'a> {
1467 self.new_node_impl(NodeKind::EntityRef, name, Some(content))
1468 }
1469
1470 pub fn new_attribute<'a>(&'a self, name: &'a str, value: &'a str) -> &'a mut Attribute<'a> {
1471 #[cfg(not(feature = "c-abi"))]
1472 {
1473 self.bump.alloc(Attribute {
1474 name,
1475 namespace: Cell::new(None),
1476 value,
1477 next: Cell::new(None),
1478 prev: Cell::new(None),
1479 parent: Cell::new(None),
1480 })
1481 }
1482 #[cfg(feature = "c-abi")]
1483 {
1484 // Materialise a text-node child holding `value` so
1485 // libxml2 ABI consumers (libxslt, lxml's XPath etc.)
1486 // that read `attr->children->content` directly see the
1487 // attribute's value through the documented C path,
1488 // not just our sup-xml-only `value` tail field.
1489 //
1490 // Without this, libxslt walks attribute children
1491 // looking for the text node, finds NULL, and treats
1492 // every attribute value as empty — surfacing as
1493 // "could not compile select expression ''" errors when
1494 // libxslt parses a stylesheet through our shim.
1495 let text_child: &Node = self.new_text(value);
1496 self.bump.alloc(Attribute {
1497 _private: Cell::new(std::ptr::null_mut()),
1498 kind: NodeKind::Attribute,
1499 _pad_kind: 0,
1500 name: self.intern_arena_cstr(name),
1501 children: Cell::new(Some(text_child)),
1502 last: Cell::new(Some(text_child)),
1503 parent: Cell::new(None),
1504 next: Cell::new(None),
1505 prev: Cell::new(None),
1506 doc: Cell::new(std::ptr::null_mut()),
1507 namespace: Cell::new(None),
1508 atype: 0,
1509 _pad_atype: 0,
1510 psvi: Cell::new(std::ptr::null_mut()),
1511 value: self.alloc_arena_cstr(value),
1512 })
1513 }
1514 }
1515
1516 /// Chain `ns` onto `el`'s `ns_def` list in document order
1517 /// (last-in-list ordering — libxml2's `xmlNewNs` does the same).
1518 /// c-abi-only because the lean Namespace has no `next` field.
1519 #[cfg(feature = "c-abi")]
1520 pub fn append_ns_def<'a>(&'a self, el: &'a Node<'a>, ns: &'a Namespace<'a>) {
1521 match el.ns_def.get() {
1522 None => el.ns_def.set(Some(ns)),
1523 Some(head) => {
1524 // Walk to the tail of the chain and link there.
1525 let mut cur = head;
1526 while let Some(n) = cur.next.get() {
1527 cur = n;
1528 }
1529 cur.next.set(Some(ns));
1530 }
1531 }
1532 }
1533
1534 pub fn new_namespace<'a>(&'a self, prefix: Option<&'a str>, href: &'a str) -> &'a Namespace<'a> {
1535 #[cfg(not(feature = "c-abi"))]
1536 {
1537 self.bump.alloc(Namespace { prefix, href })
1538 }
1539 #[cfg(feature = "c-abi")]
1540 {
1541 // c-abi: stamp NUL-terminated copies into the arena so
1542 // `as_ptr()` is a valid `xmlChar*`. The full libxml2-shape
1543 // _xmlNs needs `_private`, `next`, `kind` slots populated
1544 // too — `next` is None at creation and gets stitched up
1545 // by the parser when chaining onto `node->ns_def`.
1546 // Namespace URI + prefix go through the dict — XML
1547 // commonly has many elements sharing the same xmlns,
1548 // and pointer-equal namespaces let consumers do
1549 // O(1) "same namespace?" checks across the tree.
1550 let href_c = self.intern_arena_cstr(href);
1551 let prefix_c = prefix.map(|p| self.intern_arena_cstr(p));
1552 self.bump.alloc(Namespace {
1553 next: Cell::new(None),
1554 kind: 18, // XML_LOCAL_NAMESPACE
1555 _pad_kind: 0,
1556 href: href_c,
1557 prefix: prefix_c,
1558 _private: Cell::new(std::ptr::null_mut()),
1559 context: Cell::new(std::ptr::null_mut()),
1560 })
1561 }
1562 }
1563
1564 /// Allocate a NUL-terminated copy of `s` in the arena, returning
1565 /// an [`ArenaCStr`] pointing at the start byte. C-ABI-only.
1566 ///
1567 /// Used for *values* — text content, attribute values, etc. —
1568 /// which are typically unique per occurrence and don't benefit
1569 /// from dedup. For *names* (element / attribute / namespace
1570 /// names) prefer [`intern_arena_cstr`](Self::intern_arena_cstr)
1571 /// so consumers get stable pointer-equality across identical
1572 /// names without paying repeated arena copies.
1573 #[cfg(feature = "c-abi")]
1574 fn alloc_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
1575 // Allocate len + 1 bytes, copy in s, NUL the last byte.
1576 let bytes = s.as_bytes();
1577 let dst: &mut [u8] = self.bump.alloc_slice_fill_with(bytes.len() + 1, |i| {
1578 if i < bytes.len() { bytes[i] } else { 0 }
1579 });
1580 // SAFETY: dst is bytes.len()+1 long with trailing 0; valid UTF-8
1581 // by construction (input was &str).
1582 unsafe { ArenaCStr::from_raw(dst.as_ptr()) }
1583 }
1584
1585 /// Intern `s` through the per-document name dict, returning a
1586 /// canonical [`ArenaCStr`] that is byte-equal for any identical
1587 /// input. Use this for any string a downstream consumer might
1588 /// pointer-compare (element / attribute / namespace names) or
1589 /// might pass to a "free if not dict-owned" path. C-ABI-only.
1590 ///
1591 /// Repeated calls with the same bytes return the same pointer
1592 /// in O(1) amortised; on a miss the dict pays one allocation.
1593 /// Strings live until the owning document drops.
1594 #[cfg(feature = "c-abi")]
1595 fn intern_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
1596 // SAFETY: self.dict is a refcount-managed pointer owned by
1597 // this struct for as long as &self is live; we hold a
1598 // reference (drop releases it). Dict::intern_str returns a
1599 // pointer into one of the dict's own Box<[u8]>s, valid for
1600 // the dict's lifetime.
1601 let ptr = unsafe { (*self.dict).intern_str(s) };
1602 unsafe { ArenaCStr::from_raw(ptr) }
1603 }
1604
1605 /// Append `child` as the last child of `parent`. Sets `parent`/sibling
1606 /// pointers consistently.
1607 ///
1608 /// # Panics
1609 ///
1610 /// Panics if `child` is already attached somewhere (its `parent` is not
1611 /// `None`) — callers should detach first. We treat double-attach as a
1612 /// builder bug, not a recoverable error.
1613 pub fn append_child<'a>(&'a self, parent: &'a Node<'a>, child: &'a Node<'a>) {
1614 debug_assert!(child.parent.get().is_none(), "append_child: child is already attached");
1615 child.parent.set(Some(parent));
1616 match parent.last_child.get() {
1617 None => {
1618 parent.first_child.set(Some(child));
1619 parent.last_child.set(Some(child));
1620 }
1621 Some(prev_last) => {
1622 prev_last.next_sibling.set(Some(child));
1623 child.prev_sibling.set(Some(prev_last));
1624 parent.last_child.set(Some(child));
1625 }
1626 }
1627 }
1628
1629 /// Append `attr` to the end of `element`'s attribute list. Sets the
1630 /// attribute's `parent` and links siblings.
1631 pub fn append_attribute<'a>(&'a self, element: &'a Node<'a>, attr: &'a Attribute<'a>) {
1632 debug_assert!(element.is_element(), "append_attribute: not an element");
1633 debug_assert!(attr.parent.get().is_none(), "append_attribute: attr already attached");
1634 attr.parent.set(Some(element));
1635 match element.last_attribute.get() {
1636 None => {
1637 element.first_attribute.set(Some(attr));
1638 element.last_attribute.set(Some(attr));
1639 }
1640 Some(prev_last) => {
1641 prev_last.next.set(Some(attr));
1642 attr.prev.set(Some(prev_last));
1643 element.last_attribute.set(Some(attr));
1644 }
1645 }
1646 }
1647
1648 /// Detach `child` from its current parent (no-op if already detached).
1649 /// Repairs sibling links on the parent's child list.
1650 pub fn detach<'a>(&'a self, child: &'a Node<'a>) {
1651 let Some(parent) = child.parent.get() else { return; };
1652 let prev = child.prev_sibling.get();
1653 let next = child.next_sibling.get();
1654 match prev {
1655 Some(p) => p.next_sibling.set(next),
1656 None => parent.first_child.set(next),
1657 }
1658 match next {
1659 Some(n) => n.prev_sibling.set(prev),
1660 None => parent.last_child.set(prev),
1661 }
1662 child.parent.set(None);
1663 child.prev_sibling.set(None);
1664 child.next_sibling.set(None);
1665 }
1666
1667 /// Record an orphan leaf (comment / PI) that appeared in the
1668 /// document's prolog (before the root element opened). Linked
1669 /// as `root.prev_sibling` chain on [`build`](Self::build).
1670 pub fn attach_prolog_orphan<'a>(&'a self, node: &'a Node<'a>) {
1671 let p: *const Node<'static> = node as *const _ as *const Node<'static>;
1672 self.prolog_orphans.borrow_mut().push(p);
1673 }
1674
1675 /// Record an orphan leaf (comment / PI) that appeared in the
1676 /// document's epilogue (after `</root>`). Linked as
1677 /// `root.next_sibling` chain on [`build`](Self::build).
1678 pub fn attach_epilogue_orphan<'a>(&'a self, node: &'a Node<'a>) {
1679 let p: *const Node<'static> = node as *const _ as *const Node<'static>;
1680 self.epilogue_orphans.borrow_mut().push(p);
1681 }
1682
1683 /// Mark `root` as the document's root element. Call before [`build`](Self::build).
1684 /// The borrow on `&self` only lives for this call, so subsequent
1685 /// `build()` can freely consume the builder.
1686 pub fn set_root<'a>(&'a self, root: &'a Node<'a>) {
1687 // SAFETY: erasing 'a → 'static is sound because:
1688 // - `root` borrows from `self.bump`, which we own.
1689 // - On `build()` we move `self.bump` into the `Document`, where it
1690 // remains pinned in heap memory. The pointer stays valid.
1691 // - The Cell type prevents anyone from reading the raw pointer with
1692 // a longer lifetime than the eventual `Document::root()` call,
1693 // which re-binds to `&self`.
1694 let p: *const Node<'static> = root as *const _ as *const Node<'static>;
1695 self.root.set(p);
1696 }
1697
1698 /// Finalize the build. Must be called after [`set_root`](Self::set_root).
1699 ///
1700 /// # Panics
1701 ///
1702 /// Panics if `set_root` was never called.
1703 pub fn build(self) -> Document {
1704 let root = self.root.get();
1705 assert!(!root.is_null(),
1706 "DocumentBuilder::build called without set_root — call set_root(node) first");
1707 // Wire any prolog/epilogue orphan leaves (comments/PIs from
1708 // outside the root) as siblings of `root` so the resulting
1709 // document mirrors libxml2's children list: prolog → root →
1710 // epilogue. No-op for the common case (no out-of-root
1711 // content).
1712 //
1713 // SAFETY: each entry was registered via `attach_orphan` with a
1714 // pointer into this builder's arena, which moves into the
1715 // returned Document and stays valid for its lifetime.
1716 let first_sibling: *const Node<'static> = unsafe {
1717 link_doc_level_orphans(
1718 root,
1719 &self.prolog_orphans.borrow(),
1720 &self.epilogue_orphans.borrow(),
1721 )
1722 };
1723 // We have a `Drop` impl on `DocumentBuilder` (to free the leaked
1724 // source bytes when build() is *not* called). That Drop blocks
1725 // by-value move-out of fields, so we use the standard
1726 // ManuallyDrop + ptr::read pattern: suppress Drop, then move each
1727 // owned field out by raw pointer.
1728 let s = std::mem::ManuallyDrop::new(self);
1729 let source_ptr = s.source_ptr.get();
1730 let source_len = s.source_len.get();
1731 let standalone = s.standalone.get();
1732 // SAFETY: ManuallyDrop suppresses the destructor; each owned field
1733 // is read exactly once and ownership is transferred either to a
1734 // local binding (then into the returned Document) or explicitly
1735 // dropped here. Final destination is the returned Document, which
1736 // now owns these fields (including the source buffer, freed by
1737 // Document's Drop).
1738 let bump = unsafe { std::ptr::read(&s.bump) };
1739 let version = unsafe { std::ptr::read(&s.version) }.into_inner();
1740 let encoding = unsafe { std::ptr::read(&s.encoding) }.into_inner();
1741 let html_metadata = unsafe { std::ptr::read(&s.html_metadata) }.into_inner();
1742 let base_url = unsafe { std::ptr::read(&s.base_url) }.into_inner();
1743 // The orphan Vecs were consumed above by `link_doc_level_orphans`.
1744 // Their contents (the *const Node pointers) are now wired into the
1745 // document, but the Vec's heap-allocated buffer itself still needs
1746 // to be freed — ManuallyDrop suppresses the field-level drop, so
1747 // read them out into local bindings that drop at scope end.
1748 let _prolog_orphans: std::cell::RefCell<Vec<*const Node<'static>>>
1749 = unsafe { std::ptr::read(&s.prolog_orphans) };
1750 let _epilogue_orphans: std::cell::RefCell<Vec<*const Node<'static>>>
1751 = unsafe { std::ptr::read(&s.epilogue_orphans) };
1752 // Transfer the refcounted dict pointer. No add_ref needed
1753 // — the builder's reference becomes the document's.
1754 #[cfg(feature = "c-abi")]
1755 let dict = s.dict;
1756 Document {
1757 bump,
1758 #[cfg(feature = "c-abi")]
1759 #[cfg(feature = "c-abi")]
1760 dict,
1761 root,
1762 first_sibling,
1763 version,
1764 encoding,
1765 standalone,
1766 html_metadata,
1767 source_ptr,
1768 source_len,
1769 unparsed_entities: std::sync::Arc::new(std::collections::HashMap::new()),
1770 id_attributes: std::sync::Arc::new(std::collections::HashMap::new()),
1771 idref_attributes: std::sync::Arc::new(std::collections::HashMap::new()),
1772 base_url,
1773 }
1774 }
1775}
1776
1777/// Link prolog/epilogue orphan leaves as siblings of `root` and
1778/// return the first node in the resulting sibling chain (either the
1779/// first prolog node, or `root` if there is no prolog).
1780///
1781/// # Safety
1782///
1783/// `root` and every entry in `prolog` / `epilogue` must be valid
1784/// pointers into the same arena that the returned chain will live
1785/// in. Lifetimes are erased to `'static` for storage; callers must
1786/// ensure the arena outlives all reads.
1787unsafe fn link_doc_level_orphans(
1788 root: *const Node<'static>,
1789 prolog: &[*const Node<'static>],
1790 epilogue: &[*const Node<'static>],
1791) -> *const Node<'static> {
1792 if prolog.is_empty() && epilogue.is_empty() {
1793 return root;
1794 }
1795 // SAFETY: `root` is a non-null pointer into the arena; the
1796 // returned reference borrows from a 'static-erased pointer but
1797 // is only used to thread sibling links here.
1798 let root_ref: &Node<'static> = unsafe { &*root };
1799 // Prolog: chain prev_sibling links so the first prolog entry
1800 // becomes the head, then ... → root.
1801 let mut prev_node: Option<&Node<'static>> = None;
1802 for &p in prolog {
1803 let n: &Node<'static> = unsafe { &*p };
1804 if let Some(pv) = prev_node {
1805 n.prev_sibling.set(Some(pv));
1806 pv.next_sibling.set(Some(n));
1807 }
1808 prev_node = Some(n);
1809 }
1810 if let Some(last_prolog) = prev_node {
1811 last_prolog.next_sibling.set(Some(root_ref));
1812 root_ref.prev_sibling.set(Some(last_prolog));
1813 }
1814 // Epilogue: chain next_sibling links after root.
1815 let mut prev_node = Some(root_ref);
1816 for &p in epilogue {
1817 let n: &Node<'static> = unsafe { &*p };
1818 if let Some(pv) = prev_node {
1819 pv.next_sibling.set(Some(n));
1820 n.prev_sibling.set(Some(pv));
1821 }
1822 prev_node = Some(n);
1823 }
1824 // First sibling is either the first prolog entry or root.
1825 if let Some(&first) = prolog.first() { first } else { root }
1826}
1827
1828impl Default for DocumentBuilder {
1829 fn default() -> Self { Self::new() }
1830}
1831
1832impl Drop for DocumentBuilder {
1833 fn drop(&mut self) {
1834 // If `build()` wasn't called, the leaked source bytes are still ours.
1835 // `free_source` is a no-op when the pointer is null (post-`build`).
1836 self.free_source();
1837 // Release our dict reference. When `build()` is called the
1838 // dict pointer is transferred to the Document via `ptr::read`
1839 // and our drop only fires when `build` ISN'T called — so we
1840 // need to release here to balance the `new()` add-ref.
1841 //
1842 // build() uses ManuallyDrop, so our drop doesn't fire after
1843 // a successful build.
1844 #[cfg(feature = "c-abi")]
1845 unsafe {
1846 if !self.dict.is_null() {
1847 crate::dict::Dict::release(self.dict);
1848 }
1849 }
1850 }
1851}
1852
1853// ── document (self-ref wrapper) ─────────────────────────────────────────────
1854
1855/// An owned XML document with an arena-allocated tree.
1856///
1857/// An unparsed external general entity declared with an `NDATA`
1858/// annotation (XML 1.0 § 4.2.2): `<!ENTITY name SYSTEM "uri" NDATA n>`
1859/// or the `PUBLIC "fpi" "uri"` form. Backs XSLT's
1860/// `unparsed-entity-uri()` and `unparsed-entity-public-id()`
1861/// (XSLT 1.0 § 12.4).
1862#[derive(Clone, Debug, Default, PartialEq)]
1863pub struct UnparsedEntity {
1864 /// The entity's SYSTEM identifier — the URI a non-XML processor
1865 /// would fetch. Stored as declared; callers resolve it against
1866 /// the document's base URI.
1867 pub system_id: String,
1868 /// The entity's PUBLIC identifier (FPI), when declared with the
1869 /// `PUBLIC` form; `None` for `SYSTEM`-only declarations.
1870 pub public_id: Option<String>,
1871}
1872
1873/// `Document` owns a [`Bump`] and a pointer to the root [`Node`] inside that
1874/// `Bump`. The struct is self-referential and the unsafety is *contained* —
1875/// see the module-level docs for the safety argument.
1876///
1877/// Outside this type, all node references are bounded by `&self` so they
1878/// can never escape the `Document`.
1879pub struct Document {
1880 // SAFETY-RELEVANT FIELD ORDER: `root` is dropped before `bump` (Rust drops
1881 // fields in declaration order), but neither field needs Drop, so the order
1882 // is academic — bumpalo's Drop on `bump` walks its own chunks; `root` is
1883 // just a raw pointer. Keeping the order anyway as a defensive measure.
1884 root: *const Node<'static>,
1885 /// First sibling in the document's top-level chain — equals
1886 /// `root` when the document has no prolog/epilogue content
1887 /// (the common case), or points to the first prolog comment /
1888 /// PI otherwise. Walking `next_sibling` from here visits
1889 /// every document-level node in source order.
1890 ///
1891 /// Used by the C-ABI shim's `XmlDoc.children` field (which
1892 /// libxml2 consumers walk to find prolog + root + epilogue)
1893 /// and by the serializer's `Document` write path.
1894 first_sibling: *const Node<'static>,
1895 /// Refcounted heap allocation. See [`DocumentBuilder::bump`] for the
1896 /// rationale. Multiple documents created on the same thread
1897 /// share this same `Arc<Bump>` — node memory survives any
1898 /// individual doc's drop and the libxml2-style cross-doc graft
1899 /// (lxml's `_appendChild` / `moveNodeToDocument`) is safe by
1900 /// construction.
1901 bump: Arc<Bump>,
1902 /// Per-document name interner. Element / attribute / namespace
1903 /// names point into the boxes this dict owns; pointer equality
1904 /// across identical names matters to C-ABI consumers that
1905 /// pointer-compare names or check "did this string come from
1906 /// the dict?" before freeing.
1907 ///
1908 /// Refcount-managed (see [`crate::dict::Dict`]) — the underlying
1909 /// dict may be shared with the parser context that produced this
1910 /// document. Document owns one reference; drop releases it.
1911 #[cfg(feature = "c-abi")]
1912 dict: *mut crate::dict::Dict,
1913 /// XML declaration version (e.g. `"1.0"`). Defaults to `"1.0"` when
1914 /// the document had no `<?xml ... ?>` declaration.
1915 pub version: String,
1916 /// XML declaration encoding (e.g. `"UTF-8"`). Defaults to `"UTF-8"`
1917 /// when the declaration omitted `encoding=…` or was absent entirely.
1918 pub encoding: String,
1919 /// XML declaration `standalone="…"` value, or `None` when absent.
1920 pub standalone: Option<bool>,
1921 /// HTML-specific metadata when this document was produced by an HTML
1922 /// parser; `None` for XML documents. Use [`Document::is_html`] to
1923 /// discriminate.
1924 pub html_metadata: Option<HtmlMeta>,
1925 /// Owned source bytes (raw pointer + length) that arena strings may
1926 /// point into via the parser's borrow-from-source optimization. The
1927 /// buffer is heap-allocated via `Box::leak` at parse time; the
1928 /// `Document`'s `Drop` impl reclaims it.
1929 ///
1930 /// **Raw pointer storage is intentional**, not just paranoia. Using
1931 /// `Pin<Box<[u8]>>` directly causes a Stacked Borrows violation when
1932 /// `DocumentBuilder::build` moves the `Box` field into the
1933 /// `Document` — the move performs a Unique retag of the parent,
1934 /// invalidating any `SharedReadOnly` borrows the parser handed out
1935 /// into the bytes. Storing as a raw pointer (which is `Copy`) means
1936 /// the build transfer is a pointer copy, not a parent retag, so the
1937 /// borrows survive. Verified clean under `cargo +nightly miri`.
1938 ///
1939 /// `source_ptr` is null when the parser ran in pure-copy mode
1940 /// (everything alloc_str'd into the bump and no source was stashed).
1941 source_ptr: *mut u8,
1942 source_len: usize,
1943 /// Unparsed external general entities declared in the DTD with
1944 /// an `NDATA` annotation (XML 1.0 § 4.2.2). Keyed by entity
1945 /// name → SYSTEM identifier. Populated by the parser when the
1946 /// document carries a `<!DOCTYPE>` with one or more
1947 /// `<!ENTITY name SYSTEM "uri" NDATA notation>` declarations;
1948 /// otherwise empty. Surfaces through
1949 /// [`Document::unparsed_entity_uri`] for XSLT's
1950 /// `unparsed-entity-uri()` function (XSLT 1.0 § 12.4). Wrapped
1951 /// in `Arc` so XSLT can share a cheap handle across the
1952 /// transform's lifetime without copying the map per-call.
1953 unparsed_entities: std::sync::Arc<std::collections::HashMap<String, UnparsedEntity>>,
1954 /// DTD-declared ID attribute typing. Keyed by element name
1955 /// (the parent of the attribute), value is the list of attribute
1956 /// names typed as `ID` in that element's `<!ATTLIST>`. Empty
1957 /// when the document had no DTD or no ID-typed declarations.
1958 /// Consulted by XPath 1.0 §4.1's `id()` function.
1959 id_attributes: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
1960 /// DTD-derived IDREF/IDREFS-attribute map: element-name → attribute
1961 /// names declared `<!ATTLIST e a IDREF>` / `IDREFS`. Empty when the
1962 /// document had no such declarations. Consulted by XPath 2.0
1963 /// §14.5.5's `idref()` function.
1964 idref_attributes: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
1965 /// URI the document was loaded from, when known — the value of
1966 /// [`ParseOptions::base_url`](crate) at parse time, or the URI a
1967 /// loader resolved for `doc()`/`document()`. `None` for documents
1968 /// built in memory with no source URI. Surfaces as the document
1969 /// node's base URI for XPath `fn:base-uri()` / `fn:document-uri()`
1970 /// (XPath 2.0 §2.5, §15.5.3): a source document's base URI is the
1971 /// URI it was retrieved from, independent of the stylesheet's own
1972 /// static base URI.
1973 base_url: Option<String>,
1974}
1975
1976// SAFETY: `Node` and friends contain `Cell<&Node>` which is !Sync.
1977// Document is !Sync as a result. `Arc<Bump>` is !Send because `Bump`
1978// is !Sync — auto-Send is blocked on that ground. We re-assert Send
1979// here under the contract that callers serialize all access to the
1980// document (Python GIL on the lxml shim, single-threaded API on the
1981// Rust side). Moving the Document moves the Arc<Bump>, whose
1982// underlying chunks stay at the same heap address; the root pointer
1983// remains valid. The raw `source_ptr` is safe to send since the
1984// bytes it points at aren't shared.
1985unsafe impl Send for Document {}
1986
1987impl Drop for Document {
1988 fn drop(&mut self) {
1989 // Drop order: Rust drops `bump` (an `Arc`) after this body
1990 // runs. The arena only releases its bumpalo memory when
1991 // the last `Arc<Bump>` clone drops — in c-abi compat the
1992 // thread keeps a clone alive until thread exit, so per-doc
1993 // drops are just refcount decrements.
1994 //
1995 // Source bytes: reclaimed unconditionally here. In c-abi
1996 // mode no node field references them post-parse (names
1997 // intern into the dict, content/values copy into the bump),
1998 // so freeing is safe regardless of who else holds the
1999 // arena. In the lean build callers that graft nodes
2000 // across documents must keep the source alive themselves —
2001 // we don't try to track those references.
2002 if !self.source_ptr.is_null() {
2003 // SAFETY: `source_ptr` came from `Box::leak`'d
2004 // `Box<[u8]>` of length `source_len` (set in
2005 // `DocumentBuilder::set_source`), and ownership was
2006 // transferred to this `Document` by
2007 // `DocumentBuilder::build`. No other code frees it.
2008 unsafe {
2009 let _: Box<[u8]> = Box::from_raw(
2010 std::slice::from_raw_parts_mut(self.source_ptr, self.source_len)
2011 as *mut [u8]
2012 );
2013 }
2014 }
2015 // Release our dict reference. Other holders (the parser
2016 // context that originated it, the thread-local dict slot)
2017 // keep it alive; only the last release frees the interned
2018 // strings.
2019 #[cfg(feature = "c-abi")]
2020 unsafe {
2021 if !self.dict.is_null() {
2022 crate::dict::Dict::release(self.dict);
2023 }
2024 }
2025 }
2026}
2027
2028impl Document {
2029 /// The original source bytes this document was parsed from, when the
2030 /// parser stashed them (the borrow-from-source path). `None` in
2031 /// pure-copy mode or for programmatically built documents. Element
2032 /// byte offsets ([`Node::source_offset`]) index into this buffer.
2033 pub fn source(&self) -> Option<&[u8]> {
2034 if self.source_ptr.is_null() {
2035 None
2036 } else {
2037 // SAFETY: `source_ptr`/`source_len` describe a `Box::leak`ed
2038 // buffer owned by this `Document` (reclaimed in `Drop`), so it
2039 // stays valid for `&self`.
2040 Some(unsafe { std::slice::from_raw_parts(self.source_ptr, self.source_len) })
2041 }
2042 }
2043
2044 /// The document root. Lifetime is bound to `&self`, so node references
2045 /// cannot outlive the `Document`.
2046 pub fn root<'a>(&'a self) -> &'a Node<'a> {
2047 // SAFETY: `self.root` points into `self.bump`, which we own. The
2048 // returned reference is bounded by `&'a self`, so it cannot outlive
2049 // the `Document` — and therefore cannot outlive the `Bump`. The
2050 // pointer was set via `DocumentBuilder::set_root` from a `&'b Node<'b>`
2051 // borrowed from `self.bump` while it lived in the builder; moving the
2052 // pinned `Bump` from the builder to `Document` did not relocate the
2053 // node (bumpalo allocations have stable addresses).
2054 unsafe { &*(self.root as *const Node<'a>) }
2055 }
2056
2057 /// Approximate bytes of memory the document holds. Includes every
2058 /// allocation made by the parser (nodes, attributes, interned-style
2059 /// strings, the source slice). Useful for memory diagnostics.
2060 pub fn memory_bytes(&self) -> usize {
2061 self.bump.allocated_bytes()
2062 }
2063
2064 /// SYSTEM identifier of the unparsed external general entity
2065 /// declared as `<!ENTITY name SYSTEM "uri" NDATA notation>` in
2066 /// the source document's DTD. Returns `None` when no such
2067 /// entity is declared. Backs XSLT 1.0 §12.4's
2068 /// `unparsed-entity-uri()` function.
2069 pub fn unparsed_entity_uri(&self, name: &str) -> Option<&str> {
2070 self.unparsed_entities.get(name).map(|e| e.system_id.as_str())
2071 }
2072
2073 /// PUBLIC identifier (FPI) of the unparsed external general entity
2074 /// named `name`, when it was declared with the `PUBLIC` form.
2075 /// Backs XSLT 2.0 §16.6.3's `unparsed-entity-public-id()`.
2076 pub fn unparsed_entity_public_id(&self, name: &str) -> Option<&str> {
2077 self.unparsed_entities.get(name)
2078 .and_then(|e| e.public_id.as_deref())
2079 }
2080
2081 /// String-value of a node previously allocated in *some* document's
2082 /// arena (typically this one) and reachable by raw pointer. Used
2083 /// by foreign-pointer code paths (XPath's `ForeignNodeSet`, EXSLT
2084 /// `str:tokenize` result nodes) where the type system can't track
2085 /// the lifetime back to a `Document`. Returns `""` for null.
2086 ///
2087 /// SAFETY-encapsulated: the unsafe deref is contained here. The
2088 /// caller's contract is that `ptr` was minted by *some* live
2089 /// `Document::new_*` (or this crate's parser) and the underlying
2090 /// arena has not been freed. Passing a stale or alien pointer
2091 /// is undefined behavior.
2092 pub fn node_string_value_by_ptr(ptr: *const Node<'static>) -> String {
2093 if ptr.is_null() { return String::new(); }
2094 // SAFETY: caller's contract — see method doc.
2095 let n: &Node<'_> = unsafe { &*(ptr as *const Node<'_>) };
2096 n.content().to_string()
2097 }
2098
2099 /// Replace the text content of the node at `node_ptr` with
2100 /// `content` (allocated in this document's arena).
2101 /// SAFETY-encapsulated: the caller's contract is that
2102 /// `node_ptr` was minted by *this* document's `new_*` (so
2103 /// `Cell<…>::set` writes into our own arena's matching
2104 /// lifetime). Mismatched docs are debug-asserted at the
2105 /// arena boundary. Silently no-ops on null.
2106 pub fn set_node_text_content_by_ptr(&self, node_ptr: *const Node<'static>, content: &str) {
2107 if node_ptr.is_null() { return; }
2108 // SAFETY: caller's contract — `node_ptr` came from this doc's `new_*`.
2109 let node: &Node<'_> = unsafe { &*(node_ptr as *const Node<'_>) };
2110 self.set_node_text_content(node, content);
2111 }
2112
2113 /// Replace the text content of `node` with `content` (allocated
2114 /// in this document's arena). Safe wrapper around the
2115 /// `Cell<…>`-typed content field — handles both the lean
2116 /// (`Cell<&str>`) and the c-abi (`Cell<ArenaCStr>`) builds.
2117 /// `node` must be a `Text` / `CData` / `Comment` / `Pi` node
2118 /// that lives in *this* document's arena; mismatched docs
2119 /// will silently store a pointer that drops with the wrong
2120 /// arena (debug-asserted to catch misuse early).
2121 pub fn set_node_text_content<'a>(&'a self, node: &'a Node<'a>, content: &str) {
2122 #[cfg(not(feature = "c-abi"))]
2123 {
2124 let s: &'a str = self.bump.alloc_str(content);
2125 node.content.set(Some(s));
2126 }
2127 #[cfg(feature = "c-abi")]
2128 {
2129 let dst: &mut [u8] = self.bump.alloc_slice_fill_with(content.len() + 1, |i| {
2130 if i < content.len() { content.as_bytes()[i] } else { 0 }
2131 });
2132 // SAFETY: `dst` is a NUL-terminated UTF-8 slice owned by the
2133 // arena. `ArenaCStr::from_raw` requires exactly that.
2134 let new_content = unsafe { ArenaCStr::from_raw(dst.as_ptr()) };
2135 node.content.set(Some(new_content));
2136 }
2137 }
2138
2139 /// Borrow the full unparsed-entity map by reference. Cheap to
2140 /// clone (it's an `Arc`); used by the XSLT engine to capture a
2141 /// snapshot for its function dispatcher.
2142 pub fn unparsed_entities(&self) -> &std::sync::Arc<std::collections::HashMap<String, UnparsedEntity>> {
2143 &self.unparsed_entities
2144 }
2145
2146 /// Replace the unparsed-entity table. Called by the parser
2147 /// after DTD ingestion; not part of the stable public surface.
2148 #[doc(hidden)]
2149 pub fn set_unparsed_entities(
2150 &mut self,
2151 map: std::collections::HashMap<String, UnparsedEntity>,
2152 ) {
2153 self.unparsed_entities = std::sync::Arc::new(map);
2154 }
2155
2156 /// Borrow the DTD-derived ID-attribute map: element-name →
2157 /// list of attribute names declared with `<!ATTLIST e a ID>`.
2158 /// Empty when the document had no DTD-typed ID attributes.
2159 pub fn id_attributes(
2160 &self,
2161 ) -> &std::sync::Arc<std::collections::HashMap<String, Vec<String>>> {
2162 &self.id_attributes
2163 }
2164
2165 /// Replace the ID-attribute map. Called by the parser after DTD
2166 /// ingestion; not part of the stable public surface.
2167 #[doc(hidden)]
2168 pub fn set_id_attributes(
2169 &mut self,
2170 map: std::collections::HashMap<String, Vec<String>>,
2171 ) {
2172 self.id_attributes = std::sync::Arc::new(map);
2173 }
2174
2175 /// Borrow the DTD-derived IDREF-attribute map: element-name → list
2176 /// of attribute names declared `<!ATTLIST e a IDREF>` / `IDREFS`.
2177 /// Empty when the document had no DTD-typed IDREF attributes.
2178 pub fn idref_attributes(
2179 &self,
2180 ) -> &std::sync::Arc<std::collections::HashMap<String, Vec<String>>> {
2181 &self.idref_attributes
2182 }
2183
2184 /// Replace the IDREF-attribute map. Called by the parser after DTD
2185 /// ingestion; not part of the stable public surface.
2186 #[doc(hidden)]
2187 pub fn set_idref_attributes(
2188 &mut self,
2189 map: std::collections::HashMap<String, Vec<String>>,
2190 ) {
2191 self.idref_attributes = std::sync::Arc::new(map);
2192 }
2193
2194 /// URI the document was loaded from, if known. See the
2195 /// [`base_url`](Self::base_url) field: this is the document node's
2196 /// base URI for `fn:base-uri()` / `fn:document-uri()`, distinct
2197 /// from any host stylesheet's static base URI.
2198 pub fn base_url(&self) -> Option<&str> {
2199 self.base_url.as_deref()
2200 }
2201
2202 /// Record the URI the document was loaded from. Called by the
2203 /// parser / loader once the source URI is known; not part of the
2204 /// stable public surface.
2205 #[doc(hidden)]
2206 pub fn set_base_url(&mut self, uri: Option<String>) {
2207 self.base_url = uri;
2208 }
2209
2210 /// First sibling in the document's top-level chain (prolog
2211 /// comment / PI, or root if none). Walking `next_sibling`
2212 /// from here visits every document-level node. Equivalent to
2213 /// libxml2's `xmlDoc.children`.
2214 pub fn first_sibling<'a>(&'a self) -> &'a Node<'a> {
2215 let p = if self.first_sibling.is_null() { self.root } else { self.first_sibling };
2216 // SAFETY: same invariants as [`root`](Self::root).
2217 unsafe { &*(p as *const Node<'a>) }
2218 }
2219
2220 /// True when this document was produced by an HTML parser.
2221 pub fn is_html(&self) -> bool {
2222 self.html_metadata.is_some()
2223 }
2224
2225 /// Re-point the document's root. Used by mutation APIs that
2226 /// replace the root post-build (libxml2's `xmlDocSetRootElement`).
2227 ///
2228 /// # Safety
2229 ///
2230 /// `node` must be a valid arena-resident pointer inside `self.bump`,
2231 /// OR NULL. The previous root pointer is dropped — its target
2232 /// remains allocated in the arena (leaked unless still reachable
2233 /// via the new tree).
2234 pub unsafe fn set_root_ptr(&mut self, node: *const Node<'static>) {
2235 self.root = node;
2236 }
2237
2238 /// Set the document's first top-level node (the head of the
2239 /// document-level sibling chain — prolog comments/PIs precede the
2240 /// root element). [`first_sibling`](Self::first_sibling) returns
2241 /// this when non-NULL, otherwise it falls back to
2242 /// [`root`](Self::root).
2243 ///
2244 /// # Safety
2245 ///
2246 /// `node` must be a valid arena-resident pointer, or NULL.
2247 pub unsafe fn set_first_sibling_ptr(&mut self, node: *const Node<'static>) {
2248 self.first_sibling = node;
2249 }
2250
2251 /// Direct access to the document's bumpalo arena.
2252 ///
2253 /// Use to allocate new nodes/attributes/strings into the same
2254 /// arena that owns the existing tree. The caller is responsible
2255 /// for tree-invariant maintenance — e.g. when linking a freshly
2256 /// allocated node into the tree via `append_child`, the new
2257 /// node's parent/sibling pointers must be wired consistently.
2258 pub fn bump(&self) -> &Bump {
2259 &self.bump
2260 }
2261
2262 /// Clone this document's arena handle. Used by the C-ABI shim to
2263 /// pin a foreign document's arena onto a destination when a node is
2264 /// grafted across documents (cross-thread), so the moved node's
2265 /// memory outlives a drop of its origin document.
2266 #[cfg(feature = "c-abi")]
2267 pub fn bump_arc(&self) -> Arc<Bump> {
2268 Arc::clone(&self.bump)
2269 }
2270
2271 // ── post-parse mutation helpers ──────────────────────────────────
2272 //
2273 // These mirror the `DocumentBuilder::new_*` set so callers can
2274 // allocate additional nodes into an existing `Document`'s arena.
2275 // Same allocation semantics (bumpalo), same field-init dance.
2276
2277 /// Allocate a fresh element node in the document's arena. The
2278 /// returned node is detached — link it into the tree via
2279 /// [`append_child`](Self::append_child).
2280 pub fn new_element<'a>(&'a self, name: &'a str) -> &'a mut Node<'a> {
2281 self.alloc_node(NodeKind::Element, name, None)
2282 }
2283
2284 /// Allocate a text node.
2285 pub fn new_text<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
2286 self.alloc_node(NodeKind::Text, "", Some(content))
2287 }
2288
2289 /// Allocate a CDATA section.
2290 pub fn new_cdata<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
2291 self.alloc_node(NodeKind::CData, "", Some(content))
2292 }
2293
2294 /// Allocate a comment node.
2295 pub fn new_comment<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
2296 self.alloc_node(NodeKind::Comment, "", Some(content))
2297 }
2298
2299 /// Allocate a processing-instruction node. `content` is `None` for
2300 /// a PI with no data section, `Some` (possibly `""`) otherwise — see
2301 /// [`DocumentBuilder::new_pi`].
2302 pub fn new_pi<'a>(&'a self, target: &'a str, content: Option<&'a str>) -> &'a mut Node<'a> {
2303 self.alloc_node(NodeKind::Pi, target, content)
2304 }
2305
2306 /// Allocate a DTD internal-subset declaration node (raw markup
2307 /// declarations, newline-terminated). Mirror of
2308 /// [`DocumentBuilder::new_dtd_decl`] for post-parse construction.
2309 pub fn new_dtd_decl<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
2310 self.alloc_node(NodeKind::DtdDecl, "", Some(content))
2311 }
2312
2313 /// Allocate an empty document-fragment node. Mirror of
2314 /// [`DocumentBuilder::new_fragment`] for post-parse construction.
2315 pub fn new_fragment<'a>(&'a self) -> &'a mut Node<'a> {
2316 self.alloc_node(NodeKind::DocumentFragment, "", None)
2317 }
2318
2319 /// Allocate an unresolved-entity-reference node. See
2320 /// [`DocumentBuilder::new_entity_ref`] for semantics.
2321 pub fn new_entity_ref<'a>(&'a self, name: &'a str, content: &'a str) -> &'a mut Node<'a> {
2322 self.alloc_node(NodeKind::EntityRef, name, Some(content))
2323 }
2324
2325 /// Deep-copy a subtree from another arena into this document.
2326 ///
2327 /// Despite the name (chosen for familiarity with DOM's `adoptNode`),
2328 /// this is a **copy**, not a move — the source subtree stays in its
2329 /// original arena. Arenas don't release individual allocations, so
2330 /// a true move isn't representable; in practice consumers either
2331 /// drop the source document afterward (handing ownership semantics)
2332 /// or keep both copies live (template-and-reuse semantics).
2333 ///
2334 /// Walks the source subtree depth-first. Each foreign element,
2335 /// text, CDATA, comment, PI, entity-reference, or document-fragment
2336 /// node gets a fresh allocation in `self`'s arena. Attributes are
2337 /// copied alongside their owning element.
2338 ///
2339 /// **Namespaces are not yet copied** — element `namespace` and
2340 /// `ns_def` pointers on the result are left `None`. Callers that
2341 /// need namespace-aware adoption should set them up after the fact
2342 /// via `bump_new_namespace` + `attach_ns_def` (c-abi build). A
2343 /// future revision will resolve namespaces by URI against the
2344 /// target document's existing declarations.
2345 ///
2346 /// Returns a fresh detached node — link it into the tree via
2347 /// [`append_child`](Self::append_child).
2348 ///
2349 /// # Examples
2350 ///
2351 /// ```ignore
2352 /// # // ignored: requires building two docs end-to-end
2353 /// let scratch = Document::new();
2354 /// let template = scratch.new_element("metadata");
2355 /// // ... build out template subtree ...
2356 ///
2357 /// let real_doc = parse_str("<root/>", &ParseOptions::default()).unwrap();
2358 /// let adopted = real_doc.adopt_subtree(template);
2359 /// real_doc.append_child(real_doc.root(), adopted);
2360 /// ```
2361 pub fn adopt_subtree<'a>(&'a self, foreign: &Node<'_>) -> &'a Node<'a> {
2362 self.adopt_node_inner(foreign)
2363 }
2364
2365 fn adopt_node_inner<'a>(&'a self, src: &Node<'_>) -> &'a Node<'a> {
2366 let copy: &Node<'a> = match src.kind {
2367 NodeKind::Element => {
2368 let name = self.bump().alloc_str(src.name());
2369 let new_el = self.new_element(name);
2370 for attr in src.attributes() {
2371 let aname = self.bump().alloc_str(attr.name());
2372 let aval = self.bump().alloc_str(attr.value());
2373 let new_attr = self.new_attribute(aname, aval);
2374 self.append_attribute(new_el, new_attr);
2375 }
2376 new_el
2377 }
2378 NodeKind::Text => {
2379 let content = self.bump().alloc_str(src.content());
2380 self.new_text(content)
2381 }
2382 NodeKind::CData => {
2383 let content = self.bump().alloc_str(src.content());
2384 self.new_cdata(content)
2385 }
2386 NodeKind::Comment => {
2387 let content = self.bump().alloc_str(src.content());
2388 self.new_comment(content)
2389 }
2390 NodeKind::Pi => {
2391 let name = self.bump().alloc_str(src.name());
2392 let content = src.content_opt().map(|c| &*self.bump().alloc_str(c));
2393 self.new_pi(name, content)
2394 }
2395 NodeKind::EntityRef => {
2396 let name = self.bump().alloc_str(src.name());
2397 let content = self.bump().alloc_str(src.content());
2398 self.new_entity_ref(name, content)
2399 }
2400 NodeKind::DtdDecl => {
2401 let content = self.bump().alloc_str(src.content());
2402 self.new_dtd_decl(content)
2403 }
2404 NodeKind::DocumentFragment => self.new_fragment(),
2405 // c-abi-only discriminants — these never appear as a real
2406 // node-kind on a Node<'_> the caller could hold.
2407 NodeKind::Attribute => unreachable!(
2408 "adopt_subtree: NodeKind::Attribute is not a Node — use new_attribute directly"
2409 ),
2410 NodeKind::Document => unreachable!(
2411 "adopt_subtree: NodeKind::Document marker never appears on a Node — pass the root element instead"
2412 ),
2413 NodeKind::Dtd => unreachable!(
2414 "adopt_subtree: NodeKind::Dtd is a compat-shim sibling node (xmlDtd), never an arena Node reached by a subtree copy"
2415 ),
2416 };
2417 // Recurse into children for container kinds.
2418 if matches!(src.kind, NodeKind::Element | NodeKind::DocumentFragment) {
2419 for child in src.children() {
2420 let child_copy = self.adopt_node_inner(child);
2421 self.append_child(copy, child_copy);
2422 }
2423 }
2424 copy
2425 }
2426
2427 /// Allocate an attribute (detached — link via
2428 /// [`append_attribute`](Self::append_attribute)).
2429 pub fn new_attribute<'a>(&'a self, name: &'a str, value: &'a str) -> &'a mut Attribute<'a> {
2430 #[cfg(not(feature = "c-abi"))]
2431 {
2432 self.bump.alloc(Attribute {
2433 name, value,
2434 namespace: Cell::new(None),
2435 next: Cell::new(None),
2436 prev: Cell::new(None),
2437 parent: Cell::new(None),
2438 })
2439 }
2440 #[cfg(feature = "c-abi")]
2441 {
2442 // See DocumentBuilder::new_attribute for the
2443 // "attribute child text-node" rationale — same
2444 // requirement applies to attributes allocated
2445 // post-build via the Document mutation API.
2446 let text_child: &Node = self.new_text(value);
2447 // Intern the name through the dict (as DocumentBuilder and
2448 // element allocation do): consumers that match attributes by
2449 // dict-canonical pointer — lxml's _MultiTagMatcher behind
2450 // strip_attributes / objectify.deannotate, and objectify's
2451 // own child lookup — only match when post-build attributes
2452 // carry the same canonical name pointer as parsed ones.
2453 let name_c = self.intern_arena_cstr(name);
2454 let value_c = self.alloc_arena_cstr(value);
2455 self.bump.alloc(Attribute {
2456 _private: Cell::new(std::ptr::null_mut()),
2457 kind: NodeKind::Attribute,
2458 _pad_kind: 0,
2459 name: name_c,
2460 children: Cell::new(Some(text_child)),
2461 last: Cell::new(Some(text_child)),
2462 parent: Cell::new(None),
2463 next: Cell::new(None),
2464 prev: Cell::new(None),
2465 doc: Cell::new(std::ptr::null_mut()),
2466 namespace: Cell::new(None),
2467 atype: 0,
2468 _pad_atype: 0,
2469 psvi: Cell::new(std::ptr::null_mut()),
2470 value: value_c,
2471 })
2472 }
2473 }
2474
2475 /// Link `child` as the last child of `parent`. Updates
2476 /// parent/sibling pointers consistently.
2477 pub fn append_child<'a>(&'a self, parent: &'a Node<'a>, child: &'a Node<'a>) {
2478 debug_assert!(child.parent.get().is_none(),
2479 "Document::append_child: child is already attached");
2480 child.parent.set(Some(parent));
2481 match parent.last_child.get() {
2482 None => {
2483 parent.first_child.set(Some(child));
2484 parent.last_child.set(Some(child));
2485 }
2486 Some(last) => {
2487 last.next_sibling.set(Some(child));
2488 child.prev_sibling.set(Some(last));
2489 parent.last_child.set(Some(child));
2490 }
2491 }
2492 }
2493
2494 /// Allocate a namespace in this document's arena. c-abi-only:
2495 /// the lean build's `Namespace` doesn't carry the chain pointer
2496 /// we'd need for tree-attached usage.
2497 #[cfg(feature = "c-abi")]
2498 pub fn bump_new_namespace<'a>(
2499 &'a self,
2500 prefix: Option<&'a str>,
2501 href: &'a str,
2502 ) -> &'a Namespace<'a> {
2503 let href_c = self.alloc_arena_cstr(href);
2504 let prefix_c = prefix.map(|p| self.alloc_arena_cstr(p));
2505 self.bump.alloc(Namespace {
2506 next: Cell::new(None),
2507 kind: 18, // XML_LOCAL_NAMESPACE
2508 _pad_kind: 0,
2509 href: href_c,
2510 prefix: prefix_c,
2511 _private: Cell::new(std::ptr::null_mut()),
2512 context: Cell::new(std::ptr::null_mut()),
2513 })
2514 }
2515
2516 /// Append `ns` to `element`'s `ns_def` chain. Used by the
2517 /// mutation API after creating a fresh namespace via
2518 /// [`bump_new_namespace`].
2519 #[cfg(feature = "c-abi")]
2520 pub fn attach_ns_def<'a>(&'a self, element: &'a Node<'a>, ns: &'a Namespace<'a>) {
2521 match element.ns_def.get() {
2522 None => element.ns_def.set(Some(ns)),
2523 Some(head) => {
2524 // Walk to the tail and link.
2525 let mut cur = head;
2526 while let Some(n) = cur.next.get() {
2527 cur = n;
2528 }
2529 cur.next.set(Some(ns));
2530 }
2531 }
2532 }
2533
2534 /// Link `attr` as the last attribute on `element`.
2535 pub fn append_attribute<'a>(&'a self, element: &'a Node<'a>, attr: &'a Attribute<'a>) {
2536 debug_assert!(element.is_element(),
2537 "Document::append_attribute: not an element");
2538 debug_assert!(attr.parent.get().is_none(),
2539 "Document::append_attribute: attr already attached");
2540 attr.parent.set(Some(element));
2541 match element.last_attribute.get() {
2542 None => {
2543 element.first_attribute.set(Some(attr));
2544 element.last_attribute.set(Some(attr));
2545 }
2546 Some(last) => {
2547 last.next.set(Some(attr));
2548 attr.prev.set(Some(last));
2549 element.last_attribute.set(Some(attr));
2550 }
2551 }
2552 }
2553
2554 fn alloc_node<'a>(
2555 &'a self,
2556 kind: NodeKind,
2557 name: &'a str,
2558 content: Option<&'a str>,
2559 ) -> &'a mut Node<'a> {
2560 #[cfg(not(feature = "c-abi"))]
2561 {
2562 self.bump.alloc(Node {
2563 kind,
2564 name,
2565 namespace: Cell::new(None),
2566 first_attribute: Cell::new(None),
2567 last_attribute: Cell::new(None),
2568 first_child: Cell::new(None),
2569 last_child: Cell::new(None),
2570 parent: Cell::new(None),
2571 next_sibling: Cell::new(None),
2572 prev_sibling: Cell::new(None),
2573 content: Cell::new(content),
2574 line: 0,
2575 source_offset: 0,
2576 })
2577 }
2578 #[cfg(feature = "c-abi")]
2579 {
2580 // Names dedup through the dict (pointer-equal across
2581 // duplicate tags); content stays in the arena (rarely
2582 // repeats).
2583 // Text / CData nodes pin `name` to the libxml2
2584 // `xmlStringText` / `xmlStringTextNoenc` statics — C
2585 // consumers (libxslt's xsltCopyText, lxml's smart-string
2586 // wrapping) compare against those by pointer to identify
2587 // text-kinds. Any other `name` would silently break the
2588 // dispatch and surface as `Internal error in xsltCopyText`.
2589 let name_c = match kind {
2590 NodeKind::Text | NodeKind::CData => ArenaCStr::text_name(),
2591 _ if name.is_empty() => ArenaCStr::empty(),
2592 _ => self.intern_arena_cstr(name),
2593 };
2594 let content_c = content.map(|c| if c.is_empty() { ArenaCStr::empty() } else { self.alloc_arena_cstr(c) });
2595 self.bump.alloc(Node {
2596 _private: Cell::new(std::ptr::null_mut()),
2597 kind,
2598 _pad_kind: 0,
2599 name: Cell::new(name_c),
2600 first_child: Cell::new(None),
2601 last_child: Cell::new(None),
2602 parent: Cell::new(None),
2603 next_sibling: Cell::new(None),
2604 prev_sibling: Cell::new(None),
2605 doc: Cell::new(std::ptr::null_mut()),
2606 namespace: Cell::new(None),
2607 content: Cell::new(content_c),
2608 first_attribute: Cell::new(None),
2609 ns_def: Cell::new(None),
2610 psvi: Cell::new(std::ptr::null_mut()),
2611 line: Cell::new(0),
2612 extra: 0,
2613 _pad_extra: [0u8; 4],
2614 last_attribute: Cell::new(None),
2615 full_line: Cell::new(0),
2616 source_offset: 0,
2617 })
2618 }
2619 }
2620
2621 /// Internal — allocate a NUL-terminated copy of `s` in this
2622 /// document's bump arena, returning a c-abi-shaped `ArenaCStr`.
2623 /// Used for values (content / attribute values); for names
2624 /// prefer [`intern_arena_cstr`](Self::intern_arena_cstr).
2625 #[cfg(feature = "c-abi")]
2626 fn alloc_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
2627 let bytes = s.as_bytes();
2628 let dst: &mut [u8] = self.bump.alloc_slice_fill_with(bytes.len() + 1, |i| {
2629 if i < bytes.len() { bytes[i] } else { 0 }
2630 });
2631 // SAFETY: dst is bytes.len()+1 long with trailing 0; valid UTF-8.
2632 unsafe { ArenaCStr::from_raw(dst.as_ptr()) }
2633 }
2634
2635 /// Intern `s` through the document's name dict; same semantics
2636 /// as [`DocumentBuilder::intern_arena_cstr`]. Used by post-parse
2637 /// mutation paths that need name pointers compatible with the
2638 /// parser-built tree.
2639 #[cfg(feature = "c-abi")]
2640 fn intern_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
2641 // SAFETY: self.dict is refcount-managed by this Document for
2642 // as long as &self is live.
2643 let ptr = unsafe { (*self.dict).intern_str(s) };
2644 unsafe { ArenaCStr::from_raw(ptr) }
2645 }
2646
2647 /// Raw pointer to the document's name dict. The dict is
2648 /// refcount-managed; callers wanting to retain a separate
2649 /// reference must invoke
2650 /// [`crate::dict::Dict::add_ref`] explicitly.
2651 #[cfg(feature = "c-abi")]
2652 pub fn dict_ptr(&self) -> *mut crate::dict::Dict {
2653 self.dict
2654 }
2655}
2656
2657impl std::fmt::Debug for Document {
2658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2659 f.debug_struct("Document")
2660 .field("root", self.root())
2661 .field("memory_bytes", &self.memory_bytes())
2662 .finish()
2663 }
2664}
2665
2666// ── libxml2-shape document wrapper (c-abi only) ─────────────────────────────
2667//
2668// `XmlDoc` is the byte-exact mirror of libxml2's `_xmlDoc`. Allocated on
2669// the heap by [`Document::into_xml_doc`]; consumed by [`XmlDoc::free`].
2670// The pointer returned to C callers IS the address of the libxml2 ABI
2671// window (the struct's first field is at offset 0 of the heap allocation,
2672// so `*mut XmlDoc` == address of `_private`).
2673//
2674// `_doc` lives at the *tail* of the struct (past the 176-byte ABI window).
2675// It owns the arena and source bytes that every pointer in the header
2676// reaches into; dropping the Box drops `_doc` last, which is fine because
2677// no one reads the header fields after free.
2678
2679/// libxml2-shape `xmlDoc`. Byte-exact match with `_xmlDoc` (64-bit) for
2680/// the public ABI window (offsets 0..176). The `_doc` tail field is
2681/// sup-xml-only — it owns the arena that backs every pointer in the
2682/// header — and never appears in the ABI contract.
2683///
2684/// Construction:
2685/// - [`Document::into_xml_doc`] consumes a `Document` and returns a
2686/// `*mut XmlDoc`. Caller owns the allocation.
2687/// - [`XmlDoc::free`] reclaims a `*mut XmlDoc`, dropping the embedded
2688/// arena. Idempotent on NULL.
2689///
2690/// All pointers in the header (`children`, `version`, `encoding`, etc.)
2691/// reach into `_doc`'s arena. They become dangling at `free` time; no
2692/// one reads the header after that.
2693#[cfg(feature = "c-abi")]
2694#[repr(C)]
2695pub struct XmlDoc {
2696 pub _private: Cell<*mut std::os::raw::c_void>, // 0
2697 pub kind: NodeKind, // 8 (XML_DOCUMENT_NODE = 9)
2698 _pad_kind: u32, // 12
2699 /// libxml2 stores the doc's URL here as `char*`. We populate it
2700 /// from the `url` argument to `xmlReadMemory` when provided; NULL
2701 /// otherwise. Note: this is `char*` not `xmlChar*` (a libxml2
2702 /// quirk — every other string in xmlDoc is `xmlChar*`).
2703 pub name: *const std::os::raw::c_char, // 16
2704 /// First child (typically the root element; may also be a comment
2705 /// or PI that precedes/follows the root element).
2706 pub children: Cell<*mut Node<'static>>, // 24
2707 pub last: Cell<*mut Node<'static>>, // 32
2708 /// Always NULL for documents (docs have no parent).
2709 pub parent: Cell<*mut Node<'static>>, // 40
2710 /// `next` / `prev` chain — used when a doc is linked into a
2711 /// container (libxslt does this). We don't link our docs; NULL.
2712 pub next: Cell<*mut XmlDoc>, // 48
2713 pub prev: Cell<*mut XmlDoc>, // 56
2714 /// libxml2 sets `doc` to point at the doc itself. We mirror that
2715 /// after heap-allocation (see [`Document::into_xml_doc`]).
2716 pub doc: Cell<*mut XmlDoc>, // 64
2717 /// libxml2 compression level (gzip). Always 0 for us — we don't
2718 /// compress output.
2719 pub compression: i32, // 72
2720 /// `standalone` declaration: -2 (no decl), -1 (yes), 0 (no), 1 (yes).
2721 /// libxml2's quirky tri-state encoding; we mirror it.
2722 pub standalone: i32, // 76
2723 pub int_subset: *mut std::os::raw::c_void, // 80
2724 pub ext_subset: *mut std::os::raw::c_void, // 88
2725 /// Chain of "global" ns declarations not attached to any element —
2726 /// rare in modern XML, kept for ABI fidelity; we never populate it.
2727 pub old_ns: *mut std::os::raw::c_void, // 96
2728 pub version: ArenaCStr<'static>, // 104
2729 /// libxml2's `xmlDoc.encoding` is `NULL` when the source carried no
2730 /// `<?xml encoding="…"?>` declaration; serializers omit the encoding
2731 /// attribute on output in that case. We model that NULL explicitly as
2732 /// `None` (niche-optimized into the single `xmlChar*` slot) rather than
2733 /// punning a null into a non-null `ArenaCStr`, and set the final value
2734 /// at construction so no post-construction write into this slot — which
2735 /// a `&Node` view of the document would freeze — is ever needed.
2736 pub encoding: Option<ArenaCStr<'static>>, // 112
2737 pub ids: *mut std::os::raw::c_void, // 120
2738 pub refs: *mut std::os::raw::c_void, // 128
2739 pub url: *const std::os::raw::c_char, // 136
2740 pub charset: i32, // 144
2741 _pad_charset: u32, // 148
2742 pub dict: *mut std::os::raw::c_void, // 152
2743 pub psvi: *mut std::os::raw::c_void, // 160
2744 pub parse_flags: i32, // 168
2745 pub properties: i32, // 172
2746 // ── sup-xml-only tail (past the 176-byte ABI window) ──
2747 /// The Rust [`Document`] that owns every arena allocation reached
2748 /// by the header pointers above. Stored as the last field so its
2749 /// offset doesn't pin the ABI. Dropped last (declaration order),
2750 /// at which point the arena and source bytes are released.
2751 pub _doc: std::mem::ManuallyDrop<Document>,
2752}
2753
2754#[cfg(feature = "c-abi")]
2755const _: () = {
2756 use std::mem::offset_of;
2757 assert!(offset_of!(XmlDoc, _private) == 0, "XmlDoc::_private @ 0");
2758 assert!(offset_of!(XmlDoc, kind) == 8, "XmlDoc::kind @ 8");
2759 assert!(offset_of!(XmlDoc, name) == 16, "XmlDoc::name @ 16");
2760 assert!(offset_of!(XmlDoc, children) == 24, "XmlDoc::children @ 24");
2761 assert!(offset_of!(XmlDoc, last) == 32, "XmlDoc::last @ 32");
2762 assert!(offset_of!(XmlDoc, parent) == 40, "XmlDoc::parent @ 40");
2763 assert!(offset_of!(XmlDoc, next) == 48, "XmlDoc::next @ 48");
2764 assert!(offset_of!(XmlDoc, prev) == 56, "XmlDoc::prev @ 56");
2765 assert!(offset_of!(XmlDoc, doc) == 64, "XmlDoc::doc @ 64");
2766 assert!(offset_of!(XmlDoc, compression) == 72, "XmlDoc::compression @ 72");
2767 assert!(offset_of!(XmlDoc, standalone) == 76, "XmlDoc::standalone @ 76");
2768 assert!(offset_of!(XmlDoc, int_subset) == 80, "XmlDoc::int_subset @ 80");
2769 assert!(offset_of!(XmlDoc, ext_subset) == 88, "XmlDoc::ext_subset @ 88");
2770 assert!(offset_of!(XmlDoc, old_ns) == 96, "XmlDoc::old_ns @ 96");
2771 assert!(offset_of!(XmlDoc, version) == 104, "XmlDoc::version @ 104");
2772 assert!(offset_of!(XmlDoc, encoding) == 112, "XmlDoc::encoding @ 112");
2773 assert!(offset_of!(XmlDoc, ids) == 120, "XmlDoc::ids @ 120");
2774 assert!(offset_of!(XmlDoc, refs) == 128, "XmlDoc::refs @ 128");
2775 assert!(offset_of!(XmlDoc, url) == 136, "XmlDoc::url @ 136");
2776 assert!(offset_of!(XmlDoc, charset) == 144, "XmlDoc::charset @ 144");
2777 assert!(offset_of!(XmlDoc, dict) == 152, "XmlDoc::dict @ 152");
2778 assert!(offset_of!(XmlDoc, psvi) == 160, "XmlDoc::psvi @ 160");
2779 assert!(offset_of!(XmlDoc, parse_flags) == 168, "XmlDoc::parse_flags @ 168");
2780 assert!(offset_of!(XmlDoc, properties) == 172, "XmlDoc::properties @ 172");
2781 // The libxml2 ABI window ends at offset 176. Our `_doc` tail starts
2782 // there or after (Rust may insert padding before `ManuallyDrop<Document>`
2783 // depending on Document's alignment).
2784 assert!(offset_of!(XmlDoc, _doc) >= 176, "XmlDoc tail starts after ABI window");
2785};
2786
2787#[cfg(feature = "c-abi")]
2788impl Document {
2789 /// Consume `self` and produce a heap-allocated libxml2-shape
2790 /// [`XmlDoc`]. The returned pointer is what `xmlReadMemory`-style
2791 /// FFI entry points hand back to C callers. Caller takes ownership;
2792 /// reclaim via [`XmlDoc::free`].
2793 ///
2794 /// All pointers in the resulting [`XmlDoc`] reach into the same arena
2795 /// that this `Document` owns — `_doc` keeps that arena alive for the
2796 /// lifetime of the heap allocation.
2797 pub fn into_xml_doc(self) -> *mut XmlDoc {
2798 // Allocate arena strings for version/encoding via the embedded
2799 // Document's bump. These slots in libxml2's xmlDoc are
2800 // `xmlChar*` (NUL-terminated UTF-8); ArenaCStr matches that.
2801 let version_str = self.version.clone();
2802 let encoding_str = self.encoding.clone();
2803 // libxml2 uses -1 (not -2) when the XML declaration carried no
2804 // `standalone` attribute (or there was no declaration): xmlNewDoc
2805 // initialises the field to -1, and consumers treat -1 as
2806 // "unspecified" (lxml's `isstandalone` maps -1 -> None, 1 -> True,
2807 // and everything else, including 0, -> False).
2808 let standalone_i32 = match self.standalone {
2809 None => -1,
2810 Some(true) => 1,
2811 Some(false) => 0,
2812 };
2813 // First-child pointer: equals the first prolog sibling
2814 // (comment / PI before the root) when present, otherwise
2815 // the root element. libxml2 consumers walk `xmlDoc.children`
2816 // expecting this prolog-first layout.
2817 let root_ptr: *mut Node<'static> =
2818 self.root as *const Node<'static> as *mut Node<'static>;
2819 let first_child_ptr: *mut Node<'static> = if self.first_sibling.is_null() {
2820 root_ptr
2821 } else {
2822 self.first_sibling as *const Node<'static> as *mut Node<'static>
2823 };
2824 // For `xmlDoc.last`, walk to the tail of the sibling chain
2825 // — root or the last epilogue node.
2826 let last_child_ptr: *mut Node<'static> = {
2827 // SAFETY: first_child_ptr is a valid Node pointer into
2828 // the arena owned by `self.bump`, which moves into the
2829 // returned XmlDoc and stays alive.
2830 let mut cur: &Node<'static> = unsafe { &*first_child_ptr };
2831 while let Some(next) = cur.next_sibling.get() {
2832 // Reborrow with 'static lifetime so the while loop
2833 // can keep updating `cur` without the previous
2834 // borrow blocking.
2835 let next_ptr = next as *const Node<'_> as *const Node<'static>;
2836 // SAFETY: next is a sibling pointer into the same arena.
2837 cur = unsafe { &*next_ptr };
2838 }
2839 cur as *const Node<'_> as *mut Node<'static>
2840 };
2841
2842 // Plant the document's name dict at offset 152 so consumers
2843 // walking the libxml2-shape `xmlDoc.dict` field can use it.
2844 // The refcount is bumped once *here* so the planted pointer
2845 // owns its own reference, independent of the embedded
2846 // `Document`'s own reference. Consequences:
2847 //
2848 // * `XmlDoc::free` releases the dict slot (one decrement)
2849 // and then drops the embedded Document (another
2850 // decrement for `Document.dict`). Net: both refs
2851 // released; the dict survives as long as any other
2852 // holder (a different doc parsed on the same thread,
2853 // the thread's own stash) still has a reference.
2854 //
2855 // * Consumers that "free" `c_doc.dict` (e.g. lxml's
2856 // `initThreadDictRef` decides our dict differs from its
2857 // thread dict and calls `xmlDictFree` on it) are
2858 // decrementing the slot's own reference; they may then
2859 // overwrite the slot with their own dict pointer (with
2860 // a fresh `xmlDictReference` of their own). In either
2861 // case our refcount math stays balanced.
2862 let dict_for_slot: *mut std::os::raw::c_void = {
2863 let d = self.dict;
2864 if !d.is_null() {
2865 // SAFETY: `self.dict` is a live, refcount-managed
2866 // Dict (set at builder construction and not yet
2867 // released — the embedded Document hasn't been
2868 // dropped). Bumping is sound.
2869 unsafe { (*d).add_ref(); }
2870 }
2871 d as *mut std::os::raw::c_void
2872 };
2873
2874 // Allocate version + encoding inside the embedded bump via a
2875 // temporary borrow. After this, no more allocations happen in
2876 // the bump.
2877 // The encoding slot mirrors libxml2's NULL-when-undeclared
2878 // convention: an empty source encoding yields `None` (a null
2879 // `xmlChar*`), never an allocated empty string. Building it here
2880 // — before the document is reinterpreted as a `&Node` to stamp
2881 // top-level parents — means the slot is never written again, so a
2882 // shared `&Node` view can't be invalidated by a later raw store.
2883 let (version_c, encoding_c) = {
2884 let bytes_v = version_str.as_bytes();
2885 let dst_v: &mut [u8] = self.bump.alloc_slice_fill_with(bytes_v.len() + 1, |i| {
2886 if i < bytes_v.len() { bytes_v[i] } else { 0 }
2887 });
2888 let encoding_c = if encoding_str.is_empty() {
2889 None
2890 } else {
2891 let bytes_e = encoding_str.as_bytes();
2892 let dst_e: &mut [u8] = self.bump.alloc_slice_fill_with(bytes_e.len() + 1, |i| {
2893 if i < bytes_e.len() { bytes_e[i] } else { 0 }
2894 });
2895 // SAFETY: NUL-terminated UTF-8, lifetime tied to the bump
2896 // which moves into `_doc` (heap-pinned via Pin<Box<Bump>>).
2897 Some(unsafe { ArenaCStr::from_raw(dst_e.as_ptr()) })
2898 };
2899 // SAFETY: NUL-terminated UTF-8, lifetime tied to the bump
2900 // which moves into `_doc` (heap-pinned via Pin<Box<Bump>>).
2901 (unsafe { ArenaCStr::from_raw(dst_v.as_ptr()) }, encoding_c)
2902 };
2903
2904 let boxed = Box::new(XmlDoc {
2905 _private: Cell::new(std::ptr::null_mut()),
2906 kind: NodeKind::Document,
2907 _pad_kind: 0,
2908 name: std::ptr::null(),
2909 children: Cell::new(first_child_ptr),
2910 last: Cell::new(last_child_ptr),
2911 parent: Cell::new(std::ptr::null_mut()),
2912 next: Cell::new(std::ptr::null_mut()),
2913 prev: Cell::new(std::ptr::null_mut()),
2914 doc: Cell::new(std::ptr::null_mut()),
2915 compression: 0,
2916 standalone: standalone_i32,
2917 int_subset: std::ptr::null_mut(),
2918 ext_subset: std::ptr::null_mut(),
2919 old_ns: std::ptr::null_mut(),
2920 version: version_c,
2921 encoding: encoding_c,
2922 ids: std::ptr::null_mut(),
2923 refs: std::ptr::null_mut(),
2924 url: std::ptr::null(),
2925 charset: 1, // libxml2's XML_CHAR_ENCODING_UTF8 = 1
2926 _pad_charset: 0,
2927 dict: dict_for_slot,
2928 psvi: std::ptr::null_mut(),
2929 parse_flags: 0,
2930 properties: 0,
2931 _doc: std::mem::ManuallyDrop::new(self),
2932 });
2933 let raw = Box::into_raw(boxed);
2934 // Set the self-pointer for libxml2 compatibility (`doc->doc == doc`).
2935 // SAFETY: `raw` is a freshly allocated, valid pointer.
2936 unsafe { (*raw).doc.set(raw); }
2937 // Walk the tree once to stamp `node->doc = raw` on every node,
2938 // matching libxml2's invariant. Mutation API (`xmlNewProp`,
2939 // `xmlAddChild` cross-doc detection) reads this; without it
2940 // every post-parse mutation would have to walk up to find the
2941 // owning doc.
2942 // SAFETY: raw is valid; the tree was just built by the parser.
2943 unsafe {
2944 let owned = &*raw;
2945 let raw_void = raw as *mut std::os::raw::c_void;
2946 let mut stack: Vec<*const Node<'static>> = Vec::new();
2947 stack.push(owned.children.get() as *const _);
2948 // Seed with every top-level sibling (prolog → root →
2949 // epilogue), not just `children`, so we stamp the whole
2950 // top-level chain in document-order. Without this,
2951 // sibling chains after the first node would be missed.
2952 //
2953 // Top-level nodes (prolog comments, the root, epilogue
2954 // comments) also get their `parent` field stamped to the
2955 // doc-cast-as-Node. libxml2 uses this so consumers can
2956 // walk up from any document-level node, and so that
2957 // `xmlUnlinkNode` on a prolog/epilogue node correctly
2958 // adjusts the doc's `children` / `last` pointers (the
2959 // sibling chain-update reads them off the parent). The
2960 // type pun is sound because `XmlDoc` and `Node` share
2961 // identical layout at offsets 0–64 (private, kind, name,
2962 // children, last, parent, next, prev, doc).
2963 let doc_as_node: &Node<'static> = &*(raw as *const Node<'static>);
2964 let mut cur_sib: *const Node<'static> = owned.children.get() as *const _;
2965 while !cur_sib.is_null() {
2966 stack.push(cur_sib);
2967 let s_ref = &*cur_sib;
2968 s_ref.parent.set(Some(doc_as_node));
2969 cur_sib = s_ref.next_sibling.get()
2970 .map(|n| n as *const _)
2971 .unwrap_or(std::ptr::null());
2972 }
2973 while let Some(np) = stack.pop() {
2974 if np.is_null() { continue; }
2975 let n = &*np;
2976 n.doc.set(raw_void);
2977 // Stamp attributes' doc field too.
2978 let mut a = n.first_attribute.get();
2979 while let Some(attr) = a {
2980 attr.doc.set(raw_void);
2981 a = attr.next.get();
2982 }
2983 // Recurse via stack into children only — siblings
2984 // of the top-level sequence are already enqueued.
2985 let mut c = n.first_child.get();
2986 while let Some(ch) = c {
2987 stack.push(ch as *const _);
2988 c = ch.next_sibling.get();
2989 }
2990 }
2991 }
2992 raw
2993 }
2994}
2995
2996#[cfg(feature = "c-abi")]
2997impl XmlDoc {
2998 /// Reclaim a heap-allocated [`XmlDoc`]. No-op on NULL. After this
2999 /// returns, every pointer that was read out of the doc (children,
3000 /// version, encoding, etc.) is dangling — caller is responsible for
3001 /// not retaining them.
3002 ///
3003 /// # Safety
3004 /// `ptr` must be NULL or a pointer returned by
3005 /// [`Document::into_xml_doc`] that has not yet been freed.
3006 pub unsafe fn free(ptr: *mut XmlDoc) {
3007 if ptr.is_null() { return; }
3008 // SAFETY: precondition — caller guarantees ptr was from
3009 // into_xml_doc and not yet freed. Box::from_raw reconstructs
3010 // ownership; the Box drops at end-of-scope. Inside the drop,
3011 // _doc (ManuallyDrop) needs explicit drop to release the
3012 // embedded Document's arena and source bytes.
3013 let mut boxed = unsafe { Box::from_raw(ptr) };
3014 // Reclaim the leaked URL CString (set by C-ABI consumers
3015 // that record a source URL post-build). NULL when no URL
3016 // was recorded; safe to skip.
3017 if !boxed.url.is_null() {
3018 // SAFETY: url was `CString::into_raw`'d by the consumer
3019 // (e.g. xml_read_memory_with_dict). Reclaim and drop.
3020 unsafe { let _ = std::ffi::CString::from_raw(boxed.url as *mut std::os::raw::c_char); }
3021 }
3022 // Release the dict reference planted in the XmlDoc.dict slot.
3023 // libxml2's `xmlFreeDoc` semantically calls `xmlDictFree` on
3024 // `doc->dict` — consumers (like lxml) may have swapped the
3025 // pointer out for their own thread-shared dict before
3026 // freeing, but the reference is owned by the field regardless
3027 // of which dict it currently points at. Mirroring this
3028 // matches the libxml2 ABI contract.
3029 let dict_at_free = boxed.dict;
3030 if !dict_at_free.is_null() {
3031 // SAFETY: `dict_at_free` was either planted by
3032 // `into_xml_doc` (with a bumped refcount) or swapped in
3033 // by a consumer that bumped the refcount itself. Either
3034 // way one outstanding reference belongs to this slot.
3035 unsafe { crate::dict::Dict::release(dict_at_free as *mut crate::dict::Dict); }
3036 }
3037 unsafe { std::mem::ManuallyDrop::drop(&mut boxed._doc); }
3038 drop(boxed);
3039 }
3040}
3041
3042// ── tests ───────────────────────────────────────────────────────────────────
3043
3044#[cfg(test)]
3045mod tests {
3046 use super::*;
3047
3048 /// Every [`NodeKind`] discriminant must match libxml2's
3049 /// `xmlElementType` value for the corresponding node type.
3050 ///
3051 /// C consumers do `if (node->type == XML_ELEMENT_NODE)` against
3052 /// the integer; drift here silently misclassifies nodes in any
3053 /// tree walker linked through the cdylib. Numbers verified
3054 /// against `xmlElementType` in
3055 /// `/opt/homebrew/Cellar/libxml2/<version>/include/libxml2/libxml/tree.h`.
3056 ///
3057 /// We model 8 of libxml2's 20 element types today. The missing
3058 /// 12 (XML_ENTITY_NODE=6, XML_DOCUMENT_TYPE_NODE=10,
3059 /// XML_DOCUMENT_FRAG_NODE=11, XML_NOTATION_NODE=12,
3060 /// XML_HTML_DOCUMENT_NODE=13, XML_DTD_NODE=14,
3061 /// XML_ELEMENT_DECL=15, XML_ATTRIBUTE_DECL=16,
3062 /// XML_ENTITY_DECL=17, XML_NAMESPACE_DECL=18,
3063 /// XML_XINCLUDE_START=19, XML_XINCLUDE_END=20) are intentionally
3064 /// absent — their slot numbers stay free for future variants
3065 /// rather than being shadowed. Any addition lands here too.
3066 #[test]
3067 fn node_kind_libxml2_values_match() {
3068 assert_eq!(NodeKind::Element as u32, 1);
3069 assert_eq!(NodeKind::Attribute as u32, 2);
3070 assert_eq!(NodeKind::Text as u32, 3);
3071 assert_eq!(NodeKind::CData as u32, 4);
3072 assert_eq!(NodeKind::EntityRef as u32, 5);
3073 assert_eq!(NodeKind::Pi as u32, 7);
3074 assert_eq!(NodeKind::Comment as u32, 8);
3075 assert_eq!(NodeKind::Document as u32, 9);
3076 }
3077
3078 fn build_simple_tree() -> Document {
3079 let b = DocumentBuilder::new();
3080 let root = b.new_element(b.alloc_str("catalog"));
3081 let book = b.new_element(b.alloc_str("book"));
3082 let id = b.new_attribute(b.alloc_str("id"), b.alloc_str("1"));
3083 b.append_attribute(book, id);
3084 let title = b.new_element(b.alloc_str("title"));
3085 let title_text = b.new_text(b.alloc_str("Dune"));
3086 b.append_child(title, title_text);
3087 b.append_child(book, title);
3088 b.append_child(root, book);
3089 b.set_root(root);
3090 b.build()
3091 }
3092
3093 #[test]
3094 fn basic_tree_navigation() {
3095 let doc = build_simple_tree();
3096 let root = doc.root();
3097 assert_eq!(root.name(), "catalog");
3098 assert!(root.is_element());
3099
3100 let book = root.children().next().unwrap();
3101 assert_eq!(book.name(), "book");
3102 assert!(book.parent.get().is_some());
3103
3104 let title = book.find_child("title").unwrap();
3105 assert_eq!(title.name(), "title");
3106 assert_eq!(title.text_content(), Some("Dune"));
3107 }
3108
3109 #[test]
3110 fn attribute_iteration() {
3111 let b = DocumentBuilder::new();
3112 let el = b.new_element(b.alloc_str("el"));
3113 for (n, v) in [("a", "1"), ("b", "2"), ("c", "3")] {
3114 let attr = b.new_attribute(b.alloc_str(n), b.alloc_str(v));
3115 b.append_attribute(el, attr);
3116 }
3117 b.set_root(el); let doc = b.build();
3118 let pairs: Vec<(String, String)> = doc.root().attributes()
3119 .map(|a| (a.name().to_owned(), a.value().to_owned()))
3120 .collect();
3121 assert_eq!(pairs, vec![
3122 ("a".into(), "1".into()),
3123 ("b".into(), "2".into()),
3124 ("c".into(), "3".into()),
3125 ]);
3126 }
3127
3128 #[test]
3129 fn sibling_links_are_doubly_threaded() {
3130 let b = DocumentBuilder::new();
3131 let root = b.new_element(b.alloc_str("r"));
3132 for n in &["a", "b", "c"] {
3133 let child = b.new_element(b.alloc_str(*n));
3134 b.append_child(root, child);
3135 }
3136 b.set_root(root); let doc = b.build();
3137 let r = doc.root();
3138
3139 let names_fwd: Vec<&str> = r.children().map(|c| c.name()).collect();
3140 assert_eq!(names_fwd, vec!["a", "b", "c"]);
3141
3142 // Walk backwards from last_child via prev_sibling
3143 let mut names_back: Vec<&str> = Vec::new();
3144 let mut cur = r.last_child.get();
3145 while let Some(n) = cur {
3146 names_back.push(n.name());
3147 cur = n.prev_sibling.get();
3148 }
3149 assert_eq!(names_back, vec!["c", "b", "a"]);
3150
3151 // Parent pointers are set
3152 for c in r.children() {
3153 assert!(std::ptr::eq(c.parent.get().unwrap(), r));
3154 }
3155 }
3156
3157 #[test]
3158 fn detach_middle_child_repairs_links() {
3159 let b = DocumentBuilder::new();
3160 let root = b.new_element(b.alloc_str("r"));
3161 let a = b.new_element(b.alloc_str("a"));
3162 let b_node = b.new_element(b.alloc_str("b"));
3163 let c = b.new_element(b.alloc_str("c"));
3164 b.append_child(root, a);
3165 b.append_child(root, b_node);
3166 b.append_child(root, c);
3167
3168 b.detach(b_node);
3169
3170 let names: Vec<&str> = root.children().map(|n| n.name()).collect();
3171 assert_eq!(names, vec!["a", "c"]);
3172 assert!(b_node.parent.get().is_none());
3173 assert!(b_node.prev_sibling.get().is_none());
3174 assert!(b_node.next_sibling.get().is_none());
3175 // a.next now points to c (skipping b_node)
3176 assert!(std::ptr::eq(a.next_sibling.get().unwrap(), c));
3177 assert!(std::ptr::eq(c.prev_sibling.get().unwrap(), a));
3178 }
3179
3180 #[test]
3181 fn detach_first_and_last() {
3182 let b = DocumentBuilder::new();
3183 let root = b.new_element(b.alloc_str("r"));
3184 let a = b.new_element(b.alloc_str("a"));
3185 let c = b.new_element(b.alloc_str("c"));
3186 b.append_child(root, a);
3187 b.append_child(root, c);
3188
3189 b.detach(a); // first
3190 assert!(std::ptr::eq(root.first_child.get().unwrap(), c));
3191 assert!(root.last_child.get().is_some());
3192 assert!(c.prev_sibling.get().is_none());
3193
3194 b.detach(c); // last (now only child)
3195 assert!(root.first_child.get().is_none());
3196 assert!(root.last_child.get().is_none());
3197 }
3198
3199 #[test]
3200 fn document_is_send() {
3201 // Compile-time check: Document: Send.
3202 fn assert_send<T: Send>() {}
3203 assert_send::<Document>();
3204 }
3205
3206 #[test]
3207 fn root_lifetime_bounded_by_document() {
3208 // This test exists to document the property; the trybuild-style
3209 // "this should fail to compile" check is below in a doc comment.
3210 let doc = build_simple_tree();
3211 let root = doc.root();
3212 assert_eq!(root.name(), "catalog");
3213 // `root` cannot outlive `doc` — the lifetime is tied to `&doc`.
3214 drop(doc);
3215 // (We intentionally do NOT use `root` here — it would not compile.)
3216 }
3217
3218 #[test]
3219 fn mixed_content_children() {
3220 let b = DocumentBuilder::new();
3221 let root = b.new_element(b.alloc_str("r"));
3222 b.append_child(root, b.new_text(b.alloc_str("before ")));
3223 let em = b.new_element(b.alloc_str("em"));
3224 b.append_child(em, b.new_text(b.alloc_str("middle")));
3225 b.append_child(root, em);
3226 b.append_child(root, b.new_text(b.alloc_str(" after")));
3227 b.append_child(root, b.new_comment(b.alloc_str(" note ")));
3228 b.append_child(root, b.new_cdata(b.alloc_str("<raw>")));
3229
3230 b.set_root(root); let doc = b.build();
3231 let r = doc.root();
3232 let kinds: Vec<NodeKind> = r.children().map(|c| c.kind).collect();
3233 assert_eq!(kinds, vec![NodeKind::Text, NodeKind::Element, NodeKind::Text,
3234 NodeKind::Comment, NodeKind::CData]);
3235
3236 // text_content() on a mixed element returns first text/cdata
3237 assert_eq!(r.text_content(), Some("before "));
3238 }
3239
3240 #[test]
3241 fn namespace_attached_to_element() {
3242 let b = DocumentBuilder::new();
3243 let ns = b.new_namespace(Some(b.alloc_str("dc")),
3244 b.alloc_str("http://purl.org/dc/elements/1.1/"));
3245 let el = b.new_element(b.alloc_str("dc:title"));
3246 el.namespace.set(Some(ns));
3247 b.set_root(el); let doc = b.build();
3248 let ns_got = doc.root().namespace.get().unwrap();
3249 assert_eq!(ns_got.prefix(), Some("dc"));
3250 assert_eq!(ns_got.href(), "http://purl.org/dc/elements/1.1/");
3251 }
3252
3253 #[test]
3254 fn memory_bytes_grows_with_alloc() {
3255 let b = DocumentBuilder::new();
3256 let root = b.new_element(b.alloc_str("r"));
3257 for i in 0..100 {
3258 let child = b.new_element(b.alloc_str(&format!("c{i}")));
3259 b.append_child(root, child);
3260 }
3261 b.set_root(root); let doc = b.build();
3262 assert!(doc.memory_bytes() > 0);
3263 assert_eq!(doc.root().children().count(), 100);
3264 }
3265
3266 #[test]
3267 fn pi_node_holds_target_and_content() {
3268 let b = DocumentBuilder::new();
3269 let root = b.new_element(b.alloc_str("r"));
3270 let pi = b.new_pi(b.alloc_str("xml-stylesheet"),
3271 Some(b.alloc_str(r#"type="text/xsl" href="s.xsl""#)));
3272 b.append_child(root, pi);
3273 b.set_root(root); let doc = b.build();
3274 let pi = doc.root().children().next().unwrap();
3275 assert_eq!(pi.kind, NodeKind::Pi);
3276 assert_eq!(pi.name(), "xml-stylesheet");
3277 assert_eq!(pi.content(), r#"type="text/xsl" href="s.xsl""#);
3278 }
3279
3280 // ── builder default + with_capacity ──────────────────────────
3281
3282 #[test]
3283 fn builder_default_is_same_as_new() {
3284 let b = DocumentBuilder::default();
3285 // Same shape — should let us build an empty-ish doc.
3286 let root = b.new_element(b.alloc_str("r"));
3287 b.set_root(root);
3288 let doc = b.build();
3289 assert_eq!(doc.root().name(), "r");
3290 }
3291
3292 #[test]
3293 fn builder_with_capacity_reserves_arena() {
3294 let b = DocumentBuilder::with_capacity(8 * 1024);
3295 let root = b.new_element(b.alloc_str("r"));
3296 b.set_root(root);
3297 let doc = b.build();
3298 // Smoke check — capacity matters for performance, not behaviour.
3299 assert!(doc.memory_bytes() > 0);
3300 assert_eq!(doc.root().name(), "r");
3301 }
3302
3303 #[test]
3304 fn builder_bump_accessor() {
3305 // Direct access to the underlying Bump.
3306 let b = DocumentBuilder::new();
3307 let _: &bumpalo::Bump = b.bump();
3308 }
3309
3310 // ── source() returns None until set_source_inplace_buffer is called ──
3311
3312 #[test]
3313 fn builder_source_returns_none_when_no_inplace_buffer() {
3314 let b = DocumentBuilder::new();
3315 assert!(b.source().is_none());
3316 }
3317
3318 // ── is_entity_ref / entity-ref allocation ────────────────────
3319
3320 #[test]
3321 fn entity_ref_node_via_builder() {
3322 let b = DocumentBuilder::new();
3323 let root = b.new_element(b.alloc_str("r"));
3324 let er = b.new_entity_ref(b.alloc_str("foo"), b.alloc_str("&foo;"));
3325 b.append_child(root, er);
3326 b.set_root(root); let doc = b.build();
3327 let e = doc.root().children().next().unwrap();
3328 assert!(e.is_entity_ref());
3329 assert!(!e.is_element());
3330 assert!(!e.is_text());
3331 assert_eq!(e.name(), "foo");
3332 assert_eq!(e.content(), "&foo;");
3333 }
3334
3335 // ── text_content fallthroughs ────────────────────────────────
3336
3337 #[test]
3338 fn text_content_on_element_without_text_returns_none() {
3339 // Element with only element children → text_content returns None
3340 // (text_content's find_map exhausts without finding Text/CData,
3341 // hitting the `_ => None` arm).
3342 let b = DocumentBuilder::new();
3343 let root = b.new_element(b.alloc_str("r"));
3344 let child = b.new_element(b.alloc_str("c"));
3345 b.append_child(root, child);
3346 b.set_root(root); let doc = b.build();
3347 assert_eq!(doc.root().text_content(), None);
3348 }
3349
3350 #[test]
3351 fn text_content_on_comment_returns_none() {
3352 // Comment kind → outer `_ => None` arm.
3353 let b = DocumentBuilder::new();
3354 let root = b.new_element(b.alloc_str("r"));
3355 let c = b.new_comment(b.alloc_str(" hi "));
3356 b.append_child(root, c);
3357 b.set_root(root); let doc = b.build();
3358 let comment = doc.root().children().next().unwrap();
3359 assert_eq!(comment.text_content(), None);
3360 }
3361
3362 // ── Debug impls ──────────────────────────────────────────────
3363
3364 #[test]
3365 fn node_debug_shows_kind_and_relevant_fields() {
3366 let b = DocumentBuilder::new();
3367 let root = b.new_element(b.alloc_str("foo"));
3368 b.append_child(root, b.new_text(b.alloc_str("hello")));
3369 b.set_root(root); let doc = b.build();
3370 let s = format!("{:?}", doc.root());
3371 assert!(s.contains("Element"), "got {s}");
3372 assert!(s.contains("foo"), "got {s}");
3373
3374 // Text node — name is empty, content carries.
3375 let t = doc.root().children().next().unwrap();
3376 let s = format!("{t:?}");
3377 assert!(s.contains("Text"), "got {s}");
3378 assert!(s.contains("hello"), "got {s}");
3379 }
3380
3381 #[test]
3382 fn attribute_debug_shows_name_and_value() {
3383 let b = DocumentBuilder::new();
3384 let el = b.new_element(b.alloc_str("el"));
3385 let attr = b.new_attribute(b.alloc_str("id"), b.alloc_str("123"));
3386 b.append_attribute(el, attr);
3387 b.set_root(el); let doc = b.build();
3388 let attr = doc.root().attributes().next().unwrap();
3389 let s = format!("{attr:?}");
3390 assert!(s.contains("Attribute"), "got {s}");
3391 assert!(s.contains("id"), "got {s}");
3392 assert!(s.contains("123"), "got {s}");
3393 }
3394
3395 #[test]
3396 fn document_debug_shows_root_and_memory() {
3397 let b = DocumentBuilder::new();
3398 b.set_root(b.new_element(b.alloc_str("rootname")));
3399 let doc = b.build();
3400 let s = format!("{doc:?}");
3401 assert!(s.contains("Document"), "got {s}");
3402 assert!(s.contains("memory_bytes"), "got {s}");
3403 assert!(s.contains("rootname"), "got {s}");
3404 }
3405
3406 // ── ChildIter::from_head ─────────────────────────────────────
3407
3408 #[test]
3409 fn child_iter_from_head_walks_chain() {
3410 let b = DocumentBuilder::new();
3411 let root = b.new_element(b.alloc_str("r"));
3412 for n in &["a", "b", "c"] {
3413 b.append_child(root, b.new_element(b.alloc_str(n)));
3414 }
3415 b.set_root(root); let doc = b.build();
3416 // ChildIter::from_head used internally by Attribute::children.
3417 // Test it directly with the root's first_child.
3418 let head = doc.root().first_child.get();
3419 let names: Vec<&str> = ChildIter::from_head(head).map(|c| c.name()).collect();
3420 assert_eq!(names, vec!["a", "b", "c"]);
3421
3422 // None head yields no nodes.
3423 let empty: Vec<&str> = ChildIter::from_head(None).map(|c| c.name()).collect();
3424 assert!(empty.is_empty());
3425 }
3426
3427 // ── Document::first_sibling and is_html ─────────────────────
3428
3429 #[test]
3430 fn document_first_sibling_equals_root_by_default() {
3431 let b = DocumentBuilder::new();
3432 b.set_root(b.new_element(b.alloc_str("r")));
3433 let doc = b.build();
3434 // No prolog → first_sibling is the root itself.
3435 let first = doc.first_sibling();
3436 assert_eq!(first.name(), "r");
3437 }
3438
3439 #[test]
3440 fn document_is_html_false_without_metadata() {
3441 let b = DocumentBuilder::new();
3442 b.set_root(b.new_element(b.alloc_str("r")));
3443 let doc = b.build();
3444 assert!(!doc.is_html());
3445 }
3446
3447 // ── Document post-build mutation API ────────────────────────
3448
3449 #[test]
3450 fn document_post_build_new_element_and_append_child() {
3451 let b = DocumentBuilder::new();
3452 b.set_root(b.new_element(b.alloc_str("r")));
3453 let doc = b.build();
3454 let root = doc.root();
3455 let added = doc.new_element(doc.bump().alloc_str("late"));
3456 doc.append_child(root, added);
3457 let names: Vec<&str> = root.children().map(|c| c.name()).collect();
3458 assert_eq!(names, vec!["late"]);
3459 }
3460
3461 #[test]
3462 fn document_post_build_append_two_children_threads_siblings() {
3463 let b = DocumentBuilder::new();
3464 b.set_root(b.new_element(b.alloc_str("r")));
3465 let doc = b.build();
3466 let root = doc.root();
3467 let a = doc.new_element(doc.bump().alloc_str("a"));
3468 let bn = doc.new_element(doc.bump().alloc_str("b"));
3469 doc.append_child(root, a);
3470 doc.append_child(root, bn);
3471 // First+second-append both branches in append_child (None / Some(last)).
3472 let names: Vec<&str> = root.children().map(|c| c.name()).collect();
3473 assert_eq!(names, vec!["a", "b"]);
3474 // Sibling links between a and b set up.
3475 assert!(std::ptr::eq(a.next_sibling.get().unwrap(), bn));
3476 assert!(std::ptr::eq(bn.prev_sibling.get().unwrap(), a));
3477 }
3478
3479 #[test]
3480 fn document_post_build_append_attribute_two_threads_links() {
3481 let b = DocumentBuilder::new();
3482 b.set_root(b.new_element(b.alloc_str("r")));
3483 let doc = b.build();
3484 let root = doc.root();
3485 let a1 = doc.new_attribute(doc.bump().alloc_str("id"), doc.bump().alloc_str("1"));
3486 let a2 = doc.new_attribute(doc.bump().alloc_str("class"), doc.bump().alloc_str("c"));
3487 doc.append_attribute(root, a1);
3488 doc.append_attribute(root, a2);
3489 let pairs: Vec<(&str, &str)> = root.attributes()
3490 .map(|a| (a.name(), a.value())).collect();
3491 assert_eq!(pairs, vec![("id", "1"), ("class", "c")]);
3492 }
3493
3494 #[test]
3495 fn document_post_build_allocator_variants() {
3496 // Touch every Document-level allocator to cover their bodies.
3497 let b = DocumentBuilder::new();
3498 b.set_root(b.new_element(b.alloc_str("r")));
3499 let doc = b.build();
3500 let root = doc.root();
3501
3502 let t = doc.new_text(doc.bump().alloc_str("hi"));
3503 let cd = doc.new_cdata(doc.bump().alloc_str("raw"));
3504 let cm = doc.new_comment(doc.bump().alloc_str(" c "));
3505 let pi = doc.new_pi(doc.bump().alloc_str("php"), Some(doc.bump().alloc_str("")));
3506 let er = doc.new_entity_ref(doc.bump().alloc_str("foo"), doc.bump().alloc_str("&foo;"));
3507
3508 doc.append_child(root, t);
3509 doc.append_child(root, cd);
3510 doc.append_child(root, cm);
3511 doc.append_child(root, pi);
3512 doc.append_child(root, er);
3513
3514 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
3515 assert_eq!(kinds, vec![
3516 NodeKind::Text, NodeKind::CData, NodeKind::Comment,
3517 NodeKind::Pi, NodeKind::EntityRef,
3518 ]);
3519 }
3520
3521 #[test]
3522 fn document_bump_accessor() {
3523 let b = DocumentBuilder::new();
3524 b.set_root(b.new_element(b.alloc_str("r")));
3525 let doc = b.build();
3526 let _: &bumpalo::Bump = doc.bump();
3527 }
3528}