sup_xml_core/parser.rs
1//! SAX-driven arena DOM parser.
2//!
3//! Produces a [`sup_xml_tree::dom::Document`] by feeding [`XmlReader`]
4//! events into a [`DocumentBuilder`]. Compared to the existing
5//! [`parse_str`](crate::parse_str) / [`parse_bytes`](crate::parse_bytes) DOM
6//! parsers — which build a per-node-heap-allocated tree — this version
7//! allocates the entire tree in a single [`bumpalo::Bump`]. Per-node alloc
8//! cost drops to a pointer bump; drop is free per node.
9//!
10//! # Why a separate entry point
11//!
12//! The arena DOM has a different type ([`arena::Document`]) than the legacy
13//! tree. We expose this as a parallel API so consumers can migrate one at a
14//! time. Once everything's ported, the legacy entry points can become thin
15//! wrappers (or get deleted).
16//!
17//! # Implementation
18//!
19//! Wraps [`XmlReader`] — the SAX layer. All XML correctness (well-formedness
20//! checks, entity expansion, encoding handling, recovery mode) comes from the
21//! reader. This module is pure tree assembly: pop/push elements on a stack,
22//! attach leaf nodes to the current top, copy strings into the arena.
23//!
24//! # Example
25//!
26//! ```
27//! use sup_xml_core::{parse_str, ParseOptions};
28//! let doc = parse_str("<r><a id='1'/></r>", &ParseOptions::default()).unwrap();
29//! let root = doc.root();
30//! assert_eq!(root.name(), "r");
31//! let a = root.children().next().unwrap();
32//! assert_eq!(a.name(), "a");
33//! assert_eq!(a.attributes().next().unwrap().value(), "1");
34//! ```
35
36use crate::encoding;
37use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
38use crate::ns_helpers::{
39 ns_err, validate_qname, validate_xmlns_decl, XML_NS_URI, XMLNS_NS_URI,
40};
41use crate::options::ParseOptions;
42use crate::xml_bytes_reader::{BytesAttr, BytesEvent, XmlBytesReader};
43use rustc_hash::FxHashSet;
44use sup_xml_tree::dom::{Attribute, Document, DocumentBuilder, Namespace, Node};
45
46/// Parse `input` into an arena-allocated [`Document`]. Uses default
47/// [`ParseOptions`].
48pub fn parse_str(input: &str, opts: &ParseOptions) -> Result<Document> {
49 let source: Box<[u8]> = input.as_bytes().to_vec().into_boxed_slice();
50 parse_owned_bytes(source, opts)
51}
52
53/// Recovery-mode sibling of [`parse_str`]. Returns the (best-effort)
54/// parsed [`Document`] along with the list of non-fatal errors that
55/// recovery mode forgave. When [`ParseOptions::recovery_mode`] is `false`
56/// the second element is always empty and the first is the same `Result`
57/// as [`parse_str`] would have produced.
58pub fn parse_str_with_recovered(
59 input: &str,
60 opts: &ParseOptions,
61) -> (Result<Document>, Vec<XmlError>) {
62 let source: Box<[u8]> = input.as_bytes().to_vec().into_boxed_slice();
63 parse_owned_bytes_with_recovered(source, opts)
64}
65
66/// Byte-slice sibling of [`parse_str`].
67///
68/// With [`ParseOptions::auto_transcode`] true (the default), non-UTF-8
69/// input is detected and converted to UTF-8 before parsing — see that
70/// field's docs for the supported set. Set `auto_transcode = false` to
71/// require UTF-8 input and reject anything else with
72/// [`ErrorDomain::Encoding`].
73pub fn parse_bytes(input: &[u8], opts: &ParseOptions) -> Result<Document> {
74 let source = transcode_and_validate(input, opts)?;
75 parse_owned_bytes(source, opts)
76}
77
78/// Variant of [`parse_bytes`] that also returns the DTD captured from
79/// the internal subset. Returns an empty [`Dtd`](crate::dtd::Dtd)
80/// when the document had no `<!DOCTYPE … [ … ]>` or no
81/// `<!ELEMENT>` / `<!ATTLIST>` declarations.
82pub fn parse_bytes_with_dtd(
83 input: &[u8],
84 opts: &ParseOptions,
85) -> Result<(Document, crate::dtd::Dtd)> {
86 let source = transcode_and_validate(input, opts)?;
87 let b = DocumentBuilder::new();
88 b.set_source(source);
89 let src_bytes: &[u8] = b.source().expect("source just set");
90 // SAFETY: transcode_and_validate guarantees UTF-8.
91 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
92 .with_options(opts.clone());
93 drive(&b, &mut reader, opts)?;
94 let dtd = reader.take_dtd();
95 Ok((b.build(), dtd))
96}
97
98/// Parse a standalone external DTD subset into a [`Dtd`](crate::dtd::Dtd).
99///
100/// `input` is the raw bytes of a DTD — the markup declarations a `.dtd`
101/// file (or an `etree.DTD(...)` source) contains, with no surrounding
102/// document or `<!DOCTYPE>` wrapper. Conditional sections and
103/// top-level parameter-entity references are permitted, per the XML 1.0
104/// § 2.8 external-subset grammar (the internal subset forbids both).
105///
106/// Returns the captured declarations. A fatal malformation returns
107/// `Err`; recoverable issues are tolerated (a non-validating DTD parse
108/// is best-effort, matching libxml2's `xmlParseDTD`).
109pub fn parse_external_subset(
110 input: &[u8],
111 opts: &ParseOptions,
112) -> Result<crate::dtd::Dtd> {
113 let bytes = transcode_and_validate(input, opts)?;
114 // SAFETY: transcode_and_validate guarantees the buffer is UTF-8.
115 let text = unsafe { String::from_utf8_unchecked(bytes.into_vec()) };
116 // The subset is read from a pushed entity-stream frame; the reader's
117 // own source is the empty (static) slice, which is a valid origin.
118 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(&[]) }
119 .with_options(opts.clone());
120 reader.parse_standalone_external_subset(text)?;
121 Ok(reader.take_dtd())
122}
123
124/// Variant of [`parse_bytes_with_dtd`] that interns names through
125/// a caller-supplied refcounted dict instead of an internal one.
126/// Use when the resulting document needs to share name canonicals
127/// with another consumer that owns the same dict (e.g. a C-ABI
128/// parser context whose `ctxt->dict` already points to a thread-
129/// shared interner).
130///
131/// The dict's refcount is bumped for the new document's reference;
132/// the caller's own reference remains independent.
133///
134/// # Safety
135///
136/// `dict` must be a valid pointer returned by
137/// [`crate::dict::Dict::new_refcounted`] (or otherwise refcount-
138/// managed), with at least one outstanding reference.
139#[cfg(feature = "c-abi")]
140pub unsafe fn parse_bytes_with_dtd_and_dict(
141 input: &[u8],
142 opts: &ParseOptions,
143 dict: *mut sup_xml_tree::dict::Dict,
144) -> Result<(Document, crate::dtd::Dtd)> {
145 let source = transcode_and_validate(input, opts)?;
146 // SAFETY: caller asserts `dict` is live with positive refcount.
147 let b = unsafe { DocumentBuilder::new_with_dict(dict) };
148 b.set_source(source);
149 let src_bytes: &[u8] = b.source().expect("source just set");
150 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
151 .with_options(opts.clone());
152 drive(&b, &mut reader, opts)?;
153 let dtd = reader.take_dtd();
154 Ok((b.build(), dtd))
155}
156
157/// Variant of [`parse_bytes_with_dtd_and_dict`] that also adopts an
158/// externally-supplied [`Bump`] arena (shared via `Arc`). Used by
159/// C-ABI consumers that route every per-thread parse through a
160/// single shared arena — node memory then outlives any individual
161/// document and cross-doc graft operations are safe by construction.
162///
163/// # Safety
164///
165/// `dict` must be a valid refcount-managed [`crate::dict::Dict`].
166/// `arena` may be cloned from any source; it becomes one of the
167/// document's references.
168#[cfg(feature = "c-abi")]
169pub unsafe fn parse_bytes_with_dtd_dict_arena(
170 input: &[u8],
171 opts: &ParseOptions,
172 dict: *mut sup_xml_tree::dict::Dict,
173 arena: std::sync::Arc<bumpalo::Bump>,
174) -> Result<(Document, crate::dtd::Dtd)> {
175 let source = transcode_and_validate(input, opts)?;
176 // SAFETY: caller asserts `dict` is live with positive refcount.
177 let b = unsafe { DocumentBuilder::new_with_dict_and_arena(dict, arena) };
178 b.set_source(source);
179 let src_bytes: &[u8] = b.source().expect("source just set");
180 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
181 .with_options(opts.clone());
182 drive(&b, &mut reader, opts)?;
183 let dtd = reader.take_dtd();
184 Ok((b.build(), dtd))
185}
186
187/// Recovery-mode sibling of [`parse_bytes`]. See
188/// [`parse_str_with_recovered`] for semantics.
189pub fn parse_bytes_with_recovered(
190 input: &[u8],
191 opts: &ParseOptions,
192) -> (Result<Document>, Vec<XmlError>) {
193 let source = match transcode_and_validate(input, opts) {
194 Ok(s) => s,
195 Err(e) => return (Err(e), Vec::new()),
196 };
197 parse_owned_bytes_with_recovered(source, opts)
198}
199
200/// Byte-slice version that skips the upfront UTF-8 validation. Mirrors
201/// [`parse_bytes_unchecked`](crate::parse_bytes_unchecked).
202///
203/// # Safety
204///
205/// `input` must be valid UTF-8.
206pub unsafe fn parse_bytes_unchecked(input: &[u8], opts: &ParseOptions) -> Result<Document> {
207 // SAFETY: caller asserts UTF-8 (per the function contract).
208 let source: Box<[u8]> = input.to_vec().into_boxed_slice();
209 parse_owned_bytes(source, opts)
210}
211
212/// Destructive-parse fast path. Takes ownership of `buf`, mutates it
213/// in place during parsing, and returns a [`Document`] whose strings
214/// point directly into the (now-mutated) buffer. The Document keeps
215/// the buffer alive for its lifetime.
216///
217/// **The speedup vs [`parse_bytes`] is workload-dependent and depends
218/// on what flags you pass in `opts`.** On entity-heavy documents the
219/// structural in-place mechanism (in-place entity decode, zero string
220/// copy) wins ~20-30% even with full XML 1.0 validation enabled. On
221/// documents that contain few or no entities (most "data" XML —
222/// swiss_prot, OSM, sitemaps, RSS) the structural win is small —
223/// often within run-to-run noise — because the validation cost
224/// dominates and both paths pay it equally. In that regime, the
225/// bigger lever is the four `skip_*` validation flags: passing all
226/// four `true` reaches roughly half of pugixml's throughput. See
227/// "When to use this" below for which combination matches your needs.
228///
229/// # Why it's faster
230///
231/// Two structural advantages over [`parse_bytes`], independent of any
232/// validation flags:
233///
234/// 1. **No string copy into the arena.** Element names, attribute
235/// values, and text content slices point directly at bytes inside
236/// `buf`. [`parse_bytes`] copies them into a fresh arena (or, when
237/// borrowing succeeds, holds the source separately).
238/// 2. **In-place entity decode.** Builtin entities (`&`, `<`,
239/// `>`, `'`, `"`), numeric character references, and
240/// newline normalization are decoded by mutating the source bytes
241/// in place — no scratch buffer per text chunk. User-defined
242/// entities with replacement text smaller than the `&name;`
243/// reference also fit in place; larger ones are rejected (see
244/// Errors below).
245///
246/// **The skip-all validation flags are NOT applied automatically.**
247/// If you want the maximum speed shown in the benchmarks (about ~30%
248/// on top of the structural wins above), build `ParseOptions` with
249/// `skip_xml_char_validation`, `skip_name_validation`,
250/// `skip_attr_validation`, and `skip_end_tag_check` set to `true`.
251/// Otherwise the parser still performs full XML 1.0 validation while
252/// it parses destructively.
253///
254/// ```
255/// use sup_xml_core::{parse_bytes_in_place, ParseOptions};
256///
257/// let buf: Vec<u8> = b"<root><child id=\"1\"/></root>".to_vec();
258///
259/// // Full XML 1.0 validation + destructive parse:
260/// let _doc = parse_bytes_in_place(buf.clone(), &ParseOptions::default())?;
261///
262/// // Trust-the-input maximum-speed:
263/// let fast_opts = ParseOptions {
264/// skip_xml_char_validation: true,
265/// skip_name_validation: true,
266/// skip_attr_validation: true,
267/// skip_end_tag_check: true,
268/// ..ParseOptions::default()
269/// };
270/// let _doc = parse_bytes_in_place(buf, &fast_opts)?;
271/// # Ok::<(), sup_xml_core::XmlError>(())
272/// ```
273///
274/// # When to use this vs [`parse_bytes`]
275///
276/// Pick **`parse_bytes_in_place`** when:
277/// - You own the input buffer and don't need to preserve its original
278/// bytes (round-trip-byte-identical serialization isn't a goal).
279/// - Your inputs use only the 5 XML 1.0 builtin entities, OR any
280/// user-defined `<!ENTITY>` declarations have replacement text whose
281/// byte length is ≤ the corresponding `&name;` reference.
282/// - You do NOT need [`ParseOptions::recovery_mode`].
283///
284/// Pick **[`parse_bytes`]** when:
285/// - You need lossless round-trip (preserve the input bytes verbatim).
286/// - You need [`ParseOptions::recovery_mode`].
287/// - You don't own the buffer or can't have it consumed.
288///
289/// # Errors
290///
291/// Returns `Err` immediately (before any mutation) for:
292/// - `opts.recovery_mode == true` — recovery is incompatible with
293/// destructive parsing (we can't unmutate after the fact).
294///
295/// Returns `Err` during parsing for:
296/// - User `<!ENTITY>` whose expansion exceeds its reference length —
297/// the bytes don't fit in place.
298/// - Cyclic entity references — well-formedness error, same as
299/// [`parse_bytes`].
300/// - Any other XML 1.0 well-formedness violation.
301///
302/// On any error, `buf` is consumed and dropped (it has been partially
303/// mutated by the time most errors fire; handing it back would be
304/// misleading).
305///
306/// # Buffer ownership
307///
308/// `buf` is consumed unconditionally — successful parse returns a
309/// [`Document`] that owns the buffer; failed parse drops it. If you
310/// might need to fall back to [`parse_bytes`], use that entry point
311/// from the start; speculative pre-cloning defeats the performance
312/// benefit this function exists for.
313pub fn parse_bytes_in_place(buf: Vec<u8>, opts: &ParseOptions) -> Result<Document> {
314 // `parse_bytes_in_place` honors every flag on `opts` as-is. In
315 // particular it does NOT silently flip the four `skip_*` validation
316 // flags on — callers who want the fastest possible path build their
317 // own `ParseOptions` with `skip_xml_char_validation`,
318 // `skip_name_validation`, `skip_attr_validation`, and
319 // `skip_end_tag_check` all set to `true`. Callers who want
320 // destructive parsing PLUS full validation (e.g. content from a
321 // semi-trusted source, but they own the buffer and want the
322 // in-place perf win on entity decode + zero string copy) pass
323 // `ParseOptions::default()` and get that.
324
325 // Up-front: recovery + in-place is fundamentally incompatible.
326 if opts.recovery_mode {
327 return Err(XmlError::new(
328 ErrorDomain::Parser,
329 ErrorLevel::Fatal,
330 "parse_bytes_in_place does not support ParseOptions::recovery_mode \
331 — destructive parsing can't unwind after a partial mutation. \
332 Use parse_bytes if you need recovery."
333 .to_string(),
334 ));
335 }
336
337 // Encoding: if auto-transcode is on and the input isn't UTF-8, we
338 // transcode upfront into a fresh Vec<u8>. That's one copy; from
339 // there, all subsequent string handling is in-place against the
340 // transcoded buffer.
341 let source: Box<[u8]> = if let Some(enc) = opts.forced_encoding.clone() {
342 // Explicit encoding overrides auto-detection (BOM / declaration).
343 encoding::transcode_to_utf8_as(&buf, enc)?
344 .into_owned()
345 .into_boxed_slice()
346 } else if opts.auto_transcode {
347 encoding::transcode_to_utf8(&buf)?
348 .into_owned()
349 .into_boxed_slice()
350 } else {
351 // Without auto_transcode the caller asserts the input is already
352 // UTF-8 (or any byte sequence we should just try to parse).
353 // We still validate before parsing to keep the unsafe boundary
354 // tight; the arena's name/text fields are `&str`, so we must
355 // know the bytes are valid UTF-8 before we point at them.
356 simdutf8::compat::from_utf8(&buf).map_err(|e| {
357 XmlError::new(
358 ErrorDomain::Encoding,
359 ErrorLevel::Fatal,
360 format!("invalid UTF-8: {e}"),
361 )
362 })?;
363 buf.into_boxed_slice()
364 };
365
366 parse_owned_bytes_inplace(source, opts)
367}
368
369/// In-place variant of `parse_owned_bytes`. Mirrors that function but
370/// constructs the reader via `XmlReader::from_bytes_in_place_unchecked`
371/// so the SAX layer can mutate the source buffer during entity decoding,
372/// newline normalization, etc. See `crate::scanner::Scanner::compact_at`.
373fn parse_owned_bytes_inplace(source: Box<[u8]>, opts: &ParseOptions) -> Result<Document> {
374 let b = DocumentBuilder::new();
375 b.set_source(source);
376 // SAFETY: the builder owns the source buffer (we just gave it via
377 // `set_source`); we hand mutable access to the reader for the
378 // duration of parsing. The reader is dropped before `b.build()` is
379 // called, so no outstanding `&mut` exists when ownership of the
380 // bytes transfers to the resulting `Document`.
381 let src_bytes: &mut [u8] = {
382 // Reconstruct a `&mut [u8]` from the builder's leaked pointer.
383 // The builder's `Drop` (or `build()`'s ownership transfer) is
384 // the sole other potential consumer; neither runs concurrently
385 // with the reader.
386 let (ptr, len) = (b.source_ptr_for_inplace(), b.source_len_for_inplace());
387 unsafe { std::slice::from_raw_parts_mut(ptr, len) }
388 };
389 let mut reader = unsafe { XmlBytesReader::from_bytes_in_place_unchecked(src_bytes) }
390 .with_options(opts.clone());
391 drive(&b, &mut reader, opts)?;
392 Ok(b.build())
393}
394
395/// Transcode if needed and validate UTF-8; return an owned byte buffer
396/// suitable for stashing on the builder.
397fn transcode_and_validate(input: &[u8], opts: &ParseOptions) -> Result<Box<[u8]>> {
398 let owned: Vec<u8> = if let Some(enc) = opts.forced_encoding.clone() {
399 // Explicit encoding overrides auto-detection (BOM / declaration).
400 encoding::transcode_to_utf8_as(input, enc)?.into_owned()
401 } else if opts.auto_transcode {
402 // transcode_to_utf8 returns Cow; only re-owns if it transcoded.
403 // Either way we end up with an owned Vec we can box for stashing.
404 encoding::transcode_to_utf8(input)?.into_owned()
405 } else {
406 input.to_vec()
407 };
408 simdutf8::compat::from_utf8(&owned).map_err(|e| {
409 // `valid_up_to` is the byte index of the first ill-formed
410 // sequence in the post-transcode buffer — identical to the
411 // caller's input byte index when input was already UTF-8.
412 // Compute line/col against the prefix that *is* valid UTF-8
413 // (compute_line_col only inspects newline bytes, so a partial
414 // UTF-8 buffer is safe to feed).
415 let off = e.valid_up_to();
416 let (line, col) = crate::scanner::compute_line_col(&owned, off);
417 XmlError::new(ErrorDomain::Encoding, ErrorLevel::Fatal, format!("invalid UTF-8: {e}"))
418 .at("<input>", line, col, off as u64)
419 })?;
420 Ok(owned.into_boxed_slice())
421}
422
423/// Drive the arena parser over an already-owned source buffer. Used
424/// by both `parse_str` and `parse_bytes` after they've
425/// produced (and possibly transcoded) a UTF-8 byte buffer. The source
426/// is stashed on the builder so the resulting [`Document`] keeps the
427/// bytes alive — letting arena strings borrow into them.
428fn parse_owned_bytes(source: Box<[u8]>, opts: &ParseOptions) -> Result<Document> {
429 let b = DocumentBuilder::new();
430 b.set_source(source);
431 // `b.source()` returns a `&[u8]` pointing at the leaked-Box bytes. The
432 // bytes live at a stable heap address until the builder (or, after
433 // `build`, the resulting Document) drops them — so the slice is valid
434 // for the entire parse.
435 let src_bytes: &[u8] = b.source().expect("source just set");
436 // SAFETY: `transcode_and_validate` (and the str → bytes conversion in
437 // `parse_str`) guarantees UTF-8.
438 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
439 .with_options(opts.clone());
440 drive(&b, &mut reader, opts)?;
441 let dtd = reader.take_dtd();
442 let mut doc = b.build();
443 if !dtd.unparsed_entities.is_empty() {
444 doc.set_unparsed_entities(dtd.unparsed_entities.clone());
445 }
446 // XML 1.0 §3.3.2 — apply ATTLIST-declared default / #FIXED
447 // attribute values to elements that didn't supply them
448 // explicitly. This was previously gated on `validating: true`,
449 // but XSLT 1.0 §3.4 expects the source tree to carry defaults
450 // (id() / xsl:copy / etc. rely on them) regardless of whether
451 // the caller asked for validation.
452 if !dtd.is_empty() {
453 let _ = crate::dtd::inject::inject_defaults(&doc, &dtd);
454 // Snapshot DTD-declared ID attributes (`<!ATTLIST e a ID>`)
455 // onto the document so XPath's `id()` can find them. Stored
456 // as element local-name → set of attribute local-names; the
457 // DTD model is element-name + attr-name keyed so we keep the
458 // same shape.
459 let id_map = crate::dtd::collect_id_attrs(&dtd);
460 if !id_map.is_empty() {
461 doc.set_id_attributes(id_map);
462 }
463 let idref_map = crate::dtd::collect_idref_attrs(&dtd);
464 if !idref_map.is_empty() {
465 doc.set_idref_attributes(idref_map);
466 }
467 }
468 Ok(doc)
469}
470
471fn parse_owned_bytes_with_recovered(
472 source: Box<[u8]>,
473 opts: &ParseOptions,
474) -> (Result<Document>, Vec<XmlError>) {
475 let b = DocumentBuilder::new();
476 b.set_source(source);
477 let src_bytes: &[u8] = b.source().expect("source just set");
478 let mut reader = unsafe { XmlBytesReader::from_bytes_unchecked(src_bytes) }
479 .with_options(opts.clone());
480 let drive_result = drive(&b, &mut reader, opts);
481 let recovered = reader.recovered_errors().to_vec();
482 let unparsed = reader.take_dtd().unparsed_entities;
483 let result = drive_result.map(|()| {
484 let mut d = b.build();
485 if !unparsed.is_empty() { d.set_unparsed_entities(unparsed); }
486 d
487 });
488 (result, recovered)
489}
490
491/// Parse `input` with XML Namespaces 1.0 processing enabled — resolves
492/// `xmlns` declarations, fills the `namespace` field on every element and
493/// prefixed attribute, validates QName syntax, and rejects undeclared
494/// prefixes. Convenience wrapper over [`parse_str`] with
495/// `namespace_aware: true`.
496///
497/// Arena-DOM equivalent of [`parse_ns_str`](crate::parse_ns_str).
498pub fn parse_ns_str(input: &str) -> Result<Document> {
499 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
500 parse_str(input, &opts)
501}
502
503/// Byte-slice sibling of [`parse_ns_str`]. Validates UTF-8 (or
504/// auto-transcodes if `auto_transcode` is on) before parsing.
505pub fn parse_ns_bytes(input: &[u8]) -> Result<Document> {
506 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
507 parse_bytes(input, &opts)
508}
509
510// ── driver ──────────────────────────────────────────────────────────────────
511
512/// Type-erased pointer to a `Node` living in the builder's arena. Stored on
513/// the construction stack; re-bound to `&'a Node<'a>` only at deref time, in
514/// short scopes where the builder borrow is also active.
515///
516/// We don't use `*const Node<'static>` directly: `Node<'doc>` is invariant
517/// over `'doc`, so casts between `*const Node<'a>` and `*const Node<'static>`
518/// are not free at the type-checker level. An untyped pointer sidesteps the
519/// invariance dance entirely.
520type ErasedNodePtr = *const ();
521type ErasedAttrPtr = *const ();
522
523/// Convert a typed node reference into a type-erased pointer.
524#[inline] fn erase(node: &Node<'_>) -> ErasedNodePtr {
525 node as *const Node<'_> as *const ()
526}
527
528/// Re-type an erased pointer back to a node reference with lifetime `'a`.
529///
530/// # Safety
531///
532/// The pointer must have been obtained from [`erase`] on a node allocated
533/// in an arena that is still alive at the call site (with lifetime ≥ `'a`).
534#[inline] unsafe fn unerase<'a>(p: ErasedNodePtr) -> &'a Node<'a> {
535 unsafe { &*(p as *const Node<'a>) }
536}
537
538/// Consume events from `reader` and build the arena Document.
539///
540/// Namespace handling is gated on [`ParseOptions::namespace_aware`]. When
541/// false (the default for `ParseOptions::default()`), this matches the legacy
542/// `parse_bytes` path — no QName validation, no xmlns scanning, no per-element
543/// namespace assignment. When true, full XML Namespaces 1.0 resolution runs
544/// inline with a per-element fast-path that skips the work for elements +
545/// attributes that have no `:` in their names *and* are not inside a default-
546/// namespace scope.
547fn drive(
548 b: &DocumentBuilder,
549 reader: &mut XmlBytesReader<'_>,
550 opts: &ParseOptions,
551) -> Result<()> {
552 // Every XML document parse funnels through here; the license gate
553 // verifies once per process (cached) and is a no-op thereafter.
554 crate::license_gate::ensure_licensed()?;
555
556 // Pre-reserve typical capacities so the first few pushes don't trigger
557 // grow-doublings on the hot path. XML element nesting rarely exceeds
558 // ~16 (deeper than that is unusual); per-element attrs in
559 // attribute-heavy formats (OSM, SVG) regularly hit 8-16.
560 let mut stack: Vec<ErasedNodePtr> = Vec::with_capacity(16);
561 let mut attr_buf: Vec<BytesAttr<'_>> = Vec::with_capacity(16);
562 let mut root: Option<ErasedNodePtr> = None;
563
564 // Incremental line-number cursor for `node.line` / `Element.sourceline`.
565 // The naive approach — call `compute_line_col` per StartElement — rescans
566 // `src[0..name_offset]` each time, which is O(N × file_size) ≈ O(N²)
567 // across the whole parse and dominates throughput on docs with many
568 // elements (10× — 100× slowdown observed). Instead we keep a `(offset,
569 // line)` cursor that only ever moves forward: each StartElement scans
570 // newlines in `src[cursor.0 .. tag.name_offset]`, then bumps the cursor.
571 // Total scanning work over the parse is O(file_size).
572 let mut line_cursor: (usize, u32) = (0, 1);
573
574 // The source URI (when the caller supplied one) becomes the
575 // document node's base URI — `fn:base-uri()`/`fn:document-uri()`
576 // resolve against it (XPath 2.0 §2.5).
577 if opts.base_url.is_some() {
578 b.set_base_url(opts.base_url.clone());
579 }
580
581 let ns_aware = opts.namespace_aware;
582
583 // Namespace scope state — only populated when `ns_aware` is true. Kept
584 // as a flat Vec of (prefix, &Namespace) bindings with per-element frame
585 // markers; type-erased pointers sidestep `Namespace<'_>` invariance.
586 let mut ns_bindings: Vec<(Option<&str>, *const ())> = Vec::new();
587 let mut ns_frames: Vec<usize> = Vec::new();
588 // Per-frame count of `xmlns`/`xmlns:foo` bindings added by that element,
589 // so EndElement can decrement `active_user_bindings` correctly.
590 let mut ns_frame_count_stack: Vec<u32> = Vec::new();
591 // Cached "is there a non-None binding in scope right now?" — set when
592 // any `xmlns` or `xmlns:foo` is declared; cleared as frames pop. This
593 // is the fast-path gate: when false, no prefix can resolve (built-in
594 // xml/xmlns aside) and no default namespace applies, so elements and
595 // attributes without `:` need no resolution work at all.
596 let mut active_user_bindings: u32 = 0;
597
598 if ns_aware {
599 let xml_ns = b.new_namespace(Some("xml"), XML_NS_URI);
600 let xmlns_ns = b.new_namespace(Some("xmlns"), XMLNS_NS_URI);
601 ns_bindings.push((Some("xml"), erase_ns(xml_ns)));
602 ns_bindings.push((Some("xmlns"), erase_ns(xmlns_ns)));
603 }
604
605 // Borrow-from-source: when a `BytesEvent` payload arrives as
606 // `Cow::Borrowed`, the bytes live in the input buffer (now owned by the
607 // builder via `set_source`). Stash a `&str` slice directly via
608 // `alloc_str_borrow` — zero copy. When the payload is `Cow::Owned`, the
609 // reader had to materialize it (entity decode, char ref, encoding
610 // conversion, etc.) — we copy into the bump.
611 //
612 // SAFETY: the input lifetime — which the borrowed Cow slices come from —
613 // is the lifetime of the builder's pinned `source` buffer. That buffer
614 // moves into the `Document` on `build()` and stays alive for the
615 // document's lifetime, so the `&'doc str`s produced here outlive their
616 // referents only if the builder retains the source. All public entry
617 // points (`parse_str`, `parse_bytes*`) install the source
618 // before calling `drive`.
619 //
620 // We accept `Cow<'_, [u8]>` (not `Cow<'_, str>`) so we can take payloads
621 // straight from `XmlBytesReader` without bouncing through `XmlReader`'s
622 // `&str`-typed wrappers (Lever 5 — see commit history). The Scanner's
623 // UTF-8 invariant means `from_utf8_unchecked` is sound at every borrow.
624 #[inline]
625 fn alloc_cow_bytes_as_str<'b>(
626 b: &'b DocumentBuilder,
627 c: std::borrow::Cow<'_, [u8]>,
628 ) -> &'b str {
629 match c {
630 std::borrow::Cow::Borrowed(bytes) => {
631 // SAFETY: Scanner UTF-8 invariant — every `Cow::Borrowed`
632 // payload is a slice of the original source buffer, which
633 // was validated as UTF-8 by the entry point.
634 let s: &str = unsafe { std::str::from_utf8_unchecked(bytes) };
635 // SAFETY: extend `'src` (the reader's input lifetime, which
636 // is really `'b` — the builder's `source` buffer) to `'b`.
637 // Sound because: caller installed the same buffer on the
638 // builder via `set_source` before constructing the reader.
639 let extended: &'b str = unsafe { &*(s as *const str) };
640 unsafe { b.alloc_str_borrow(extended) }
641 }
642 std::borrow::Cow::Owned(v) => {
643 // SAFETY: Owned payloads come from entity expansion / char
644 // refs / encoding transcode — all of which write only
645 // complete UTF-8 sequences into the temp buffer.
646 let s: &str = unsafe { std::str::from_utf8_unchecked(&v) };
647 b.alloc_str(s)
648 }
649 }
650 }
651
652 // Borrow an element-name byte slice as a `&'src str` in the arena.
653 // Names never contain entity refs (XML 1.0 § 2.3), so they're always a
654 // direct source slice — no `Cow::Owned` arm needed.
655 #[inline]
656 fn alloc_name_bytes_as_str<'b>(b: &'b DocumentBuilder, bytes: &[u8]) -> &'b str {
657 // SAFETY: Scanner UTF-8 invariant + lifetime extension as in
658 // `alloc_cow_bytes_as_str`'s Borrowed arm.
659 let s: &str = unsafe { std::str::from_utf8_unchecked(bytes) };
660 let extended: &'b str = unsafe { &*(s as *const str) };
661 unsafe { b.alloc_str_borrow(extended) }
662 }
663
664 // Look up the AttType the DTD's `<!ATTLIST>` declared for the
665 // given (element, attribute). Returns `None` if no decl
666 // covers it — falls back to CDATA semantics (no
667 // normalization).
668 fn dtd_att_type<'d>(
669 dtd: &'d crate::dtd::Dtd,
670 elem_name: &str,
671 attr_name: &str,
672 ) -> Option<&'d crate::dtd::AttType> {
673 let attlist = dtd.attlists.get(elem_name)?;
674 attlist.iter().find(|d| d.name == attr_name).map(|d| &d.att_type)
675 }
676
677 // Normalize an attribute value according to the DTD-declared
678 // type when present. Returns a `&'b str` interned into the
679 // builder's bump so the lifetime matches the rest of the tree.
680 //
681 // No-op when no `<!ATTLIST>` covers the (element, attr) pair —
682 // attributes default to CDATA semantics, which need no
683 // additional normalization at this layer (the byte reader has
684 // already done its part). Generic across xmlns and regular
685 // attributes; the spec rules don't distinguish.
686 fn dtd_normalize_attr_value<'b>(
687 b: &'b DocumentBuilder,
688 dtd: &crate::dtd::Dtd,
689 elem_name: &str,
690 attr_name: &str,
691 raw_value: &'b str,
692 ) -> &'b str {
693 use crate::dtd::AttType;
694 // W3C `xml:id` Recommendation §4 — the attribute's value is
695 // assigned ID type regardless of DTD typing, with the same
696 // non-CDATA normalization (strip + collapse whitespace).
697 // Fast-path the literal-name check before consulting the
698 // DTD: every element pays one byte comparison, no DTD lookup
699 // needed for the common case of "no xml:id, no DTD typing."
700 let is_xml_id = attr_name == "xml:id";
701 let needs_non_cdata = is_xml_id || {
702 let att_type = dtd_att_type(dtd, elem_name, attr_name);
703 matches!(
704 att_type,
705 Some(AttType::Id | AttType::IdRef | AttType::IdRefs
706 | AttType::Entity | AttType::Entities
707 | AttType::Nmtoken | AttType::Nmtokens
708 | AttType::Notation(_) | AttType::Enumeration(_))
709 )
710 };
711 if !needs_non_cdata { return raw_value; }
712 let normalized = normalize_non_cdata(raw_value);
713 b.alloc_str(&normalized)
714 }
715
716 // Capture the source bytes for line-number translation at
717 // StartElement time. The returned slice has the input's
718 // lifetime (`'src` on the reader), so it's safe to use across
719 // subsequent mutable borrows of the reader.
720 let src_bytes: &[u8] = reader.src_bytes();
721
722 // XML 1.0 § 3.3.3 attribute-value normalization.
723 //
724 // The reader returns attribute values with no normalization
725 // applied (raw bytes between the quotes). When an ATTLIST
726 // declares the attribute as non-CDATA (NMTOKEN, ID, IDREF, …),
727 // the spec requires line-break and whitespace handling beyond
728 // the CDATA default: strip leading/trailing spaces and collapse
729 // internal runs to a single space. Applied at the xmlns-
730 // binding site so namespace URIs feed the "Unique Att Spec"
731 // check after normalization — without it,
732 // `xmlns:b=" urn:x "` would bind to a URI that doesn't compare
733 // equal to another `xmlns:a="urn:x"`, missing a real collision.
734 fn normalize_non_cdata(value: &str) -> String {
735 // `value` has already been through §3.3.3 CDATA-default
736 // normalization, which folds every *literal* whitespace byte
737 // (space, tab, CR, LF) to a single `#x20`. Any `#x9` / `#xA`
738 // / `#xD` still present therefore arrived via a character
739 // reference (`	` / `
` / `
`), and §3.3.3 forbids
740 // rewriting those. The tokenized-type step layered on top
741 // accordingly collapses runs of — and trims leading/trailing —
742 // `#x20` ONLY, leaving character-reference whitespace intact.
743 let bytes = value.as_bytes();
744 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
745 let mut in_run = true; // leading-trim by treating start as in-run
746 for &b in bytes {
747 if b == b' ' {
748 if !in_run {
749 out.push(b' ');
750 in_run = true;
751 }
752 } else {
753 // Every non-space byte (ASCII or a UTF-8 lead /
754 // continuation byte) is copied verbatim — `#x20` never
755 // occurs inside a multi-byte sequence, so this can't
756 // split one.
757 out.push(b);
758 in_run = false;
759 }
760 }
761 if out.last() == Some(&b' ') { out.pop(); }
762 // SAFETY: `value` was valid UTF-8 and we only dropped or
763 // relocated standalone `#x20` bytes, never touching the bytes
764 // of a multi-byte sequence.
765 unsafe { String::from_utf8_unchecked(out) }
766 }
767 loop {
768 // attr_buf is drained inside the StartElement arm below, so it
769 // re-enters the loop empty. No `clear()` is needed at the top.
770 debug_assert!(attr_buf.is_empty());
771 match reader.next()? {
772 BytesEvent::StartElement(tag) => {
773 // Element name: borrow from source on the common
774 // path; copy into the arena when the tag came from
775 // an entity-replacement stream (those bytes are
776 // owned by the tag and die at end-of-match-arm, so
777 // a borrow would dangle).
778 let name: &str = match tag.name_cow() {
779 std::borrow::Cow::Borrowed(bytes) => {
780 alloc_name_bytes_as_str(b, bytes)
781 }
782 std::borrow::Cow::Owned(bytes) => {
783 b.alloc_str(unsafe { std::str::from_utf8_unchecked(&bytes) })
784 }
785 };
786 // In c-abi mode + namespace-aware parsing, `Node::name`
787 // follows libxml2's convention: the local part only.
788 // The prefix lives on `node.ns->prefix` after namespace
789 // resolution. Consumers (lxml, libxslt) read names as
790 // local strings and combine with `ns->href` to form
791 // the expanded tag; keeping the prefix here breaks tag
792 // equality, attribute lookup, and namespacedNameFromNsName.
793 //
794 // Without namespace awareness, names stay raw (with any
795 // prefix) — internal regression tests and tools that
796 // process documents pre-namespace-resolution rely on
797 // that.
798 #[cfg(feature = "c-abi")]
799 let elem_name = if ns_aware {
800 match memchr::memchr(b':', name.as_bytes()) {
801 Some(idx) => &name[idx + 1..],
802 None => name,
803 }
804 } else {
805 name
806 };
807 #[cfg(not(feature = "c-abi"))]
808 let elem_name = name;
809 let el = b.new_element(elem_name);
810 // Record the source line number so consumers can
811 // ask for `node.line` / lxml's `Element.sourceline`.
812 // libxml2 caps the field at u16 (65535) — values
813 // beyond that get the special "encoded line" trick
814 // (`(extra >> 16)`); we keep things simple and
815 // saturate.
816 {
817 let name_offset = (tag.name_offset() as usize).min(src_bytes.len());
818 // Advance the line cursor through any new bytes since
819 // the last StartElement. Element name offsets are
820 // monotonically non-decreasing through the parse, so the
821 // cursor only ever moves forward; on the rare chance it
822 // would move backward (shouldn't happen in practice) we
823 // leave the cursor in place and the recorded line is the
824 // last seen value — never worse than the old quadratic
825 // path that would just produce the same number with
826 // more work.
827 if name_offset > line_cursor.0 {
828 let slice = &src_bytes[line_cursor.0..name_offset];
829 line_cursor.1 += memchr::memchr_iter(b'\n', slice).count() as u32;
830 line_cursor.0 = name_offset;
831 }
832 let raw_line = line_cursor.1;
833 // The `line` field is `u16` in c-abi mode
834 // (matching libxml2's 16-bit slot) and `u32`
835 // in the lean build (no ABI constraint, so we can
836 // handle large files better).
837 // Saturate when narrowing to u16.
838 // c-abi stores the line in a saturating `Cell<u16>`;
839 // the lean build keeps a plain `u32`. `el` is the
840 // freshly-built `&mut Node`, so either write is sound.
841 #[cfg(feature = "c-abi")]
842 el.line.set(raw_line.min(u16::MAX as u32) as u16);
843 #[cfg(not(feature = "c-abi"))]
844 {
845 el.line = raw_line;
846 }
847 // Keep the uncapped line for files past 65535 lines;
848 // `xmlGetLineNo` returns it in preference to the
849 // saturated `line`.
850 #[cfg(feature = "c-abi")]
851 {
852 el.full_line.set(raw_line);
853 }
854 // Byte offset of the element name — the ground truth
855 // from which line/column are derived on demand
856 // (`scanner::compute_line_col`). Saturates at u32.
857 el.source_offset = name_offset.min(u32::MAX as usize) as u32;
858 }
859 // Entity-stream start tags carry their attrs pre-
860 // parsed (the lazy iterator can't surface bytes that
861 // don't live in `src`). Take them out first; the
862 // lazy `attrs()` path returns nothing in that case.
863 let entity_attr_pairs: Option<Vec<(Vec<u8>, Vec<u8>)>> =
864 tag.entity_attrs().map(|v| v.to_vec());
865
866 // Skip the attribute iterator entirely when there's
867 // nothing between the name and the closing `>` — a
868 // single empty-slice check is cheaper than constructing
869 // a Scanner just to have its `next()` immediately return
870 // None. Trailing whitespace inside the start tag still
871 // falls through (rare in practice; attrs() short-
872 // circuits on the first .next() either way).
873 if !tag.attrs_bytes().is_empty() {
874 for a in tag.attrs() { attr_buf.push(a?); }
875 }
876
877 // Splice pre-parsed entity-stream attrs into the buffer
878 // so the existing ns-aware / ns-blind loops below see
879 // them uniformly. Bytes are arena-copied; the
880 // lifetime cast launders the arena's lifetime to the
881 // attr_buf slot, sound because the arena outlives the
882 // BytesAttr.
883 if let Some(pairs) = entity_attr_pairs {
884 for (n_bytes, v_bytes) in pairs {
885 let n_arena: &str = b.alloc_str(unsafe {
886 std::str::from_utf8_unchecked(&n_bytes)
887 });
888 let v_arena: &str = b.alloc_str(unsafe {
889 std::str::from_utf8_unchecked(&v_bytes)
890 });
891 let n_slice: &[u8] = n_arena.as_bytes();
892 let v_slice: &[u8] = v_arena.as_bytes();
893 let n_ext: &[u8] = unsafe { &*(n_slice as *const [u8]) };
894 let v_ext: &[u8] = unsafe { &*(v_slice as *const [u8]) };
895 attr_buf.push(BytesAttr {
896 name: n_ext,
897 value: std::borrow::Cow::Borrowed(v_ext),
898 });
899 }
900 }
901
902 if !ns_aware {
903 // Fastest path — namespace-blind, mirrors legacy parse_bytes.
904 for a in attr_buf.drain(..) {
905 // Attr name is always &'src [u8] (no entity decode for
906 // names per XML 1.0 § 2.3) so we can always borrow it.
907 let aname = alloc_name_bytes_as_str(b, a.name);
908 let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
909 // XML 1.0 § 3.3.3 non-CDATA normalization — same
910 // helper the ns-aware branch uses. No-op when
911 // no ATTLIST covers this attribute.
912 let avalue: &str =
913 dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
914 let attr = b.new_attribute(aname, avalue);
915 b.append_attribute(el, attr);
916 }
917 } else {
918 // ── namespace-aware path ──────────────────────────────────
919 validate_qname(name, "element")?;
920 ns_frames.push(ns_bindings.len());
921 let mut new_bindings_this_frame = 0u32;
922 let mut any_attr_prefixed = false;
923 // Prefixed attributes paired with their original QName.
924 // Namespace resolution must wait until every `xmlns`
925 // declaration on this element has been seen, so collect
926 // them and resolve in the pass below. In c-abi the
927 // stored `name` is already reduced to the local part
928 // (libxml2 convention); the prefix is recovered from the
929 // QName recorded here.
930 let mut prefixed_attrs: Vec<(&Attribute<'_>, &str)> = Vec::new();
931
932 for a in attr_buf.drain(..) {
933 let aname = alloc_name_bytes_as_str(b, a.name);
934 let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
935
936 // Namespace declarations (`xmlns` / `xmlns:foo`)
937 // do NOT belong in the element's attribute
938 // list under the c-abi layout — libxml2's
939 // `_xmlNode::properties` holds only real
940 // attributes; xmlns declarations live on the
941 // separate `nsDef` chain. In the lean
942 // (non-c-abi) build we keep them in both
943 // places for backwards-compatibility with
944 // existing API consumers that iterate
945 // `attributes()` expecting xmlns decls there.
946 let is_xmlns_decl =
947 aname == "xmlns" || aname.starts_with("xmlns:");
948
949 // XML 1.0 § 3.3.3 attribute-value normalization.
950 // Applied uniformly: if an `<!ATTLIST>` declared
951 // this attribute as non-CDATA (ID, IDREF,
952 // IDREFS, ENTITY/ENTITIES, NMTOKEN/NMTOKENS,
953 // NOTATION, enumeration), strip leading/trailing
954 // whitespace and collapse internal runs to a
955 // single space. Without this, ID equality,
956 // IDREF resolution, and enumeration matching
957 // would all silently disagree with the spec on
958 // values like `id=" abc "`. Helper no-ops when
959 // no ATTLIST covers the pair, so non-DTD docs
960 // pay nothing.
961 let avalue: &str =
962 dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
963
964 #[cfg(not(feature = "c-abi"))]
965 let always_attach = true;
966 #[cfg(feature = "c-abi")]
967 let always_attach = false;
968 let is_prefixed = !is_xmlns_decl
969 && memchr::memchr(b':', aname.as_bytes()).is_some();
970 if always_attach || !is_xmlns_decl {
971 // c-abi follows libxml2: `name` holds the local
972 // part only, with the prefix carried on
973 // `attr->ns` after resolution. Mirror the
974 // element-name reduction so a prefixed attribute
975 // doesn't serialize as `p:p:name` and `keys()`
976 // reports the bare local name. The lean build
977 // keeps the raw QName (its serializer never
978 // re-prepends a prefix).
979 #[cfg(feature = "c-abi")]
980 let stored_name = if ns_aware && is_prefixed {
981 let i = memchr::memchr(b':', aname.as_bytes()).unwrap();
982 &aname[i + 1..]
983 } else {
984 aname
985 };
986 #[cfg(not(feature = "c-abi"))]
987 let stored_name = aname;
988 let attr: &Attribute<'_> = b.new_attribute(stored_name, avalue);
989 b.append_attribute(el, attr);
990 if ns_aware && is_prefixed {
991 prefixed_attrs.push((attr, aname));
992 }
993 }
994
995 if aname == "xmlns" {
996 let ns = b.new_namespace(None, avalue);
997 ns_bindings.push((None, erase_ns(ns)));
998 #[cfg(feature = "c-abi")]
999 { b.append_ns_def(el, ns); }
1000 new_bindings_this_frame += 1;
1001 } else if let Some(local) = aname.strip_prefix("xmlns:") {
1002 validate_xmlns_decl(local, avalue)?;
1003 if local == "xml" { continue; }
1004 let ns = b.new_namespace(Some(local), avalue);
1005 ns_bindings.push((Some(local), erase_ns(ns)));
1006 #[cfg(feature = "c-abi")]
1007 { b.append_ns_def(el, ns); }
1008 new_bindings_this_frame += 1;
1009 } else if memchr::memchr(b':', aname.as_bytes()).is_some() {
1010 any_attr_prefixed = true;
1011 }
1012 }
1013 active_user_bindings += new_bindings_this_frame;
1014
1015 // ── Element QName resolution ──
1016 // Fast path: no prefix and no user bindings ever in scope → no namespace.
1017 let elem_has_colon = memchr::memchr(b':', name.as_bytes()).is_some();
1018 if elem_has_colon || active_user_bindings > 0 {
1019 let el_ns = resolve_qname(name, &ns_bindings, /*is_attribute=*/ false)?;
1020 el.namespace.set(el_ns);
1021 }
1022
1023 // ── Attribute QName resolution + dup-by-expanded-name check ──
1024 // Skip the FxHashSet allocation entirely when no attribute is prefixed
1025 // (no namespace = no collision possible after expansion).
1026 if any_attr_prefixed {
1027 // Resolve each prefixed attribute's namespace now that
1028 // every xmlns declaration on this element is in scope.
1029 // `resolve_qname` takes the original prefixed QName
1030 // recorded at creation; the stored `name` may already
1031 // be the local part (c-abi).
1032 for &(attr, qname) in &prefixed_attrs {
1033 validate_qname(qname, "attribute")?;
1034 let ns = resolve_qname(qname, &ns_bindings, true)?;
1035 attr.namespace.set(ns);
1036 }
1037 // XML NS § 6.3: no two attributes may share an expanded
1038 // name (namespace-uri, local-name). The local part is
1039 // `name` after the colon — which equals `name` itself
1040 // once reduced (c-abi).
1041 let mut seen: FxHashSet<(&str, &str)> = FxHashSet::default();
1042 let mut attr_cur = el.first_attribute.get();
1043 while let Some(attr) = attr_cur {
1044 if attr.name() != "xmlns" && !attr.name().starts_with("xmlns:") {
1045 let ns_uri = attr.namespace.get().map(|n| n.href()).unwrap_or("");
1046 let local = attr.name().rfind(':')
1047 .map(|i| &attr.name()[i + 1..])
1048 .unwrap_or(attr.name());
1049 if !seen.insert((ns_uri, local)) {
1050 return Err(ns_err(if ns_uri.is_empty() {
1051 format!("duplicate attribute '{local}' after namespace expansion")
1052 } else {
1053 format!("duplicate attribute '{local}' in namespace '{ns_uri}' after namespace expansion")
1054 }));
1055 }
1056 }
1057 attr_cur = attr.next.get();
1058 }
1059 }
1060 // Stash the per-frame binding count in ns_frames is not enough — we
1061 // also need to know how many to subtract from active_user_bindings
1062 // on EndElement. Pack the count into the frame marker by using a
1063 // sentinel: separate Vec for the counts. (Simpler: piggyback on
1064 // ns_frames by recording `new_bindings_this_frame` in a parallel Vec.)
1065 // For now we track via a side Vec.
1066 ns_frame_count_stack.push(new_bindings_this_frame);
1067 }
1068
1069 // Attach to parent (if any) — first StartElement becomes the root.
1070 if let Some(&parent_ptr) = stack.last() {
1071 // SAFETY: parent_ptr points into `b`, still alive here.
1072 let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
1073 b.append_child(parent, el);
1074 } else {
1075 root = Some(erase(el));
1076 }
1077 stack.push(erase(el));
1078 }
1079 BytesEvent::EndElement(_) => {
1080 stack.pop().expect("EndElement without StartElement — XmlBytesReader invariant");
1081 if ns_aware {
1082 if let Some(frame_start) = ns_frames.pop() {
1083 ns_bindings.truncate(frame_start);
1084 }
1085 if let Some(count) = ns_frame_count_stack.pop() {
1086 active_user_bindings -= count;
1087 }
1088 }
1089 }
1090 BytesEvent::Text(t) => attach_leaf(b, &stack, b.new_text (alloc_cow_bytes_as_str(b, t.into_bytes())), root.is_some()),
1091 BytesEvent::CData(s) => {
1092 // `cdata_as_text` (libxml2 NOCDATA / lxml strip_cdata)
1093 // delivers CDATA content as a plain text node.
1094 let content = alloc_cow_bytes_as_str(b, s.into_bytes());
1095 let leaf = if opts.cdata_as_text { b.new_text(content) } else { b.new_cdata(content) };
1096 attach_leaf(b, &stack, leaf, root.is_some());
1097 }
1098 BytesEvent::Comment(s) => {
1099 // `remove_comments` (lxml NULLs the SAX comment callback):
1100 // skip building the node entirely.
1101 if !opts.remove_comments {
1102 attach_leaf(b, &stack, b.new_comment(alloc_cow_bytes_as_str(b, s.into_bytes())), root.is_some());
1103 }
1104 }
1105 BytesEvent::Pi(p) => {
1106 if !opts.remove_pis {
1107 let (t, c) = p.into_parts();
1108 let target = alloc_cow_bytes_as_str(b, t);
1109 // A PI with no data section serializes without the
1110 // trailing space; libxml2 marks that as NULL content.
1111 let content = if c.is_empty() { None } else { Some(alloc_cow_bytes_as_str(b, c)) };
1112 attach_leaf(b, &stack, b.new_pi(target, content), root.is_some());
1113 }
1114 }
1115 BytesEvent::EntityRef(e) => {
1116 // `resolve_entities: false` left an `&name;` literal in
1117 // the source; materialize it as a dedicated
1118 // `NodeKind::EntityRef` node so the tree round-trips
1119 // back to source and lxml's `tag == Entity` /
1120 // `text == "&name;"` semantics work.
1121 let name_bytes = e.name();
1122 // SAFETY: Scanner UTF-8 invariant — entity-name bytes
1123 // are valid UTF-8 (NameChar+).
1124 let name: &str = unsafe { std::str::from_utf8_unchecked(name_bytes) };
1125 let name = b.alloc_str(name);
1126 // Literal source form `&name;` for the serializer.
1127 let lit = format!("&{name};");
1128 let content = b.alloc_str(&lit);
1129 attach_leaf(b, &stack, b.new_entity_ref(name, content), root.is_some());
1130 }
1131 BytesEvent::Eof => break,
1132 }
1133 }
1134
1135 let root_ptr = root.ok_or_else(|| XmlError::new(
1136 ErrorDomain::Parser, ErrorLevel::Fatal,
1137 "document has no root element",
1138 ))?;
1139 // SAFETY: root_ptr was erased from a node allocated in `b`; `b` is still
1140 // alive at this line.
1141 let root_ref: &Node<'_> = unsafe { unerase(root_ptr) };
1142 b.set_root(root_ref);
1143
1144 // Plumb XML declaration fields from the reader's prolog state into the
1145 // Document. When the document had no `<?xml ... ?>` declaration the
1146 // builder's defaults ("1.0" / "UTF-8" / None) are kept.
1147 if let Some(decl) = reader.xml_decl() {
1148 b.set_version(decl.version.clone());
1149 if let Some(enc) = &decl.encoding {
1150 b.set_encoding(enc.clone());
1151 }
1152 b.set_standalone(decl.standalone);
1153 }
1154
1155 Ok(())
1156}
1157
1158// ── namespace helpers ───────────────────────────────────────────────────────
1159
1160#[inline] fn erase_ns(ns: &Namespace<'_>) -> *const () {
1161 ns as *const Namespace<'_> as *const ()
1162}
1163
1164/// # Safety
1165///
1166/// `p` must have been produced by `erase_ns` from a live `&Namespace`,
1167/// and the caller-chosen lifetime `'a` must not outlive that namespace's
1168/// arena.
1169#[inline] unsafe fn unerase_ns<'a>(p: *const ()) -> &'a Namespace<'a> {
1170 unsafe { &*(p as *const Namespace<'a>) }
1171}
1172
1173/// Find the innermost binding for `prefix` (None = default namespace).
1174/// Returns `None` if no binding is in scope.
1175fn lookup_ns<'a>(
1176 prefix: Option<&str>,
1177 bindings: &[(Option<&'a str>, *const ())],
1178) -> Option<&'a Namespace<'a>> {
1179 for &(p, ns_ptr) in bindings.iter().rev() {
1180 if p == prefix {
1181 // SAFETY: ns_ptr was minted by erase_ns from a Namespace allocated
1182 // in the builder's arena, which is still alive while this function
1183 // runs (called from inside drive() which owns the arena).
1184 return Some(unsafe { unerase_ns(ns_ptr) });
1185 }
1186 }
1187 None
1188}
1189
1190/// Resolve a QName against the namespace scope. Unprefixed elements use the
1191/// default namespace; unprefixed attributes never do (XML Namespaces § 6.2).
1192fn resolve_qname<'a>(
1193 qname: &'a str,
1194 bindings: &[(Option<&'a str>, *const ())],
1195 is_attribute: bool,
1196) -> Result<Option<&'a Namespace<'a>>> {
1197 if let Some(colon) = qname.find(':') {
1198 let prefix = &qname[..colon];
1199 match lookup_ns(Some(prefix), bindings) {
1200 Some(ns) => Ok(Some(ns)),
1201 None => Err(ns_err(format!("undeclared namespace prefix '{prefix}' in '{qname}'"))),
1202 }
1203 } else if is_attribute {
1204 Ok(None)
1205 } else {
1206 // Unprefixed element: default namespace if declared with non-empty URI.
1207 match lookup_ns(None, bindings) {
1208 Some(ns) if !ns.href().is_empty() => Ok(Some(ns)),
1209 _ => Ok(None),
1210 }
1211 }
1212}
1213
1214
1215/// Attach a freshly-allocated leaf node to the top-of-stack element.
1216/// When the stack is empty (prolog before the root or epilogue
1217/// after `</root>`) the leaf is recorded as a document-level
1218/// orphan instead of dropped; [`DocumentBuilder::build`] later
1219/// links it as a sibling of the root so consumers see comments /
1220/// PIs that appeared outside the document element.
1221///
1222/// `after_root` tells the builder whether this orphan goes in the
1223/// prolog (false: root not yet seen) or the epilogue (true: root
1224/// element has opened, regardless of whether it has also closed).
1225fn attach_leaf<'a>(
1226 b: &'a DocumentBuilder,
1227 stack: &[ErasedNodePtr],
1228 node: &'a Node<'a>,
1229 after_root: bool,
1230) {
1231 if let Some(&parent_ptr) = stack.last() {
1232 // SAFETY: stack invariant — pointer points into the live `b.bump`,
1233 // which is the same arena `node` lives in, so unifying lifetimes is sound.
1234 let parent: &'a Node<'a> = unsafe { unerase(parent_ptr) };
1235 b.append_child(parent, node);
1236 } else if after_root {
1237 b.attach_epilogue_orphan(node);
1238 } else {
1239 b.attach_prolog_orphan(node);
1240 }
1241}
1242
1243// Silence the unused-`ErasedAttrPtr` lint until we extend to namespace tables.
1244#[allow(dead_code)] type _AttrAlias = ErasedAttrPtr;
1245
1246// ── tests ───────────────────────────────────────────────────────────────────
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251 use sup_xml_tree::dom::NodeKind;
1252
1253 fn parse(xml: &str) -> Document {
1254 // Default `ParseOptions` has `namespace_aware: false` — most tests here
1255 // exercise the structural shape (children, attrs, kinds), so it doesn't
1256 // matter. Namespace-specific tests use `parse_ns` instead.
1257 parse_str(xml, &ParseOptions::default()).expect("parse")
1258 }
1259
1260 /// Namespace-aware parse helper for the ns-resolution tests.
1261 fn parse_ns(xml: &str) -> Document {
1262 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
1263 parse_str(xml, &opts).expect("parse")
1264 }
1265
1266 #[test]
1267 fn empty_element() {
1268 let doc = parse("<r/>");
1269 assert_eq!(doc.root().name(), "r");
1270 assert!(doc.root().children().next().is_none());
1271 }
1272
1273 #[test]
1274 fn element_records_source_offset_for_line_col_derivation() {
1275 // `<b>` opens at byte 9 (after "<a>\n <b>" — the name `b` is
1276 // at offset 9). Line/column derive from that offset via
1277 // `compute_line_col`, the single source of truth.
1278 let xml = "<a>\n <b/></a>";
1279 let doc = parse(xml);
1280 let b = doc.root().children()
1281 .find(|n| n.is_element() && n.name() == "b")
1282 .expect("found <b>");
1283 assert_eq!(b.source_offset, 7, "byte offset of `b` name");
1284 let (line, col) = crate::compute_line_col(xml.as_bytes(), b.source_offset as usize);
1285 assert_eq!((line, col), (2, 4), "derived line/col");
1286 }
1287
1288 // ── XML 1.0 § 3.3.3 attribute-value normalization for xmlns ──
1289
1290 /// `xmlns:b` declared as NMTOKEN: leading/trailing whitespace
1291 /// in the URI literal MUST be stripped before the URI binds.
1292 /// CDATA attributes get no such treatment. Without normalization
1293 /// `xmlns:a="urn:x"` and `xmlns:b=" urn:x "` would bind to two
1294 /// distinct URIs even though the spec treats them as one — the
1295 /// downstream "Unique Att Spec" check would then miss real
1296 /// namespace collisions like W3C rmt-ns10-012.
1297 #[test]
1298 fn xmlns_nmtoken_value_is_stripped_before_binding() {
1299 let xml = r#"<?xml version="1.0"?>
1300<!DOCTYPE foo [
1301<!ELEMENT foo ANY>
1302<!ATTLIST foo xmlns:a CDATA #IMPLIED
1303 xmlns:b NMTOKEN #IMPLIED>
1304]>
1305<foo xmlns:a="urn:x" xmlns:b=" urn:x "/>"#;
1306 let doc = parse_ns(xml);
1307 let root = doc.root();
1308 // Both prefixes should now bind to the same URI string.
1309 // Walk the namespace defs and check.
1310 let nsdefs: Vec<(Option<&str>, &str)> = {
1311 #[cfg(feature = "c-abi")]
1312 { root.ns_declarations().collect() }
1313 #[cfg(not(feature = "c-abi"))]
1314 {
1315 // Without c-abi we exposed xmlns decls as attributes
1316 // instead of nsDef. Pull from the attribute list.
1317 let mut out = Vec::new();
1318 let mut a = root.first_attribute.get();
1319 while let Some(attr) = a {
1320 let name = attr.name();
1321 if name == "xmlns" {
1322 out.push((None, attr.value()));
1323 } else if let Some(p) = name.strip_prefix("xmlns:") {
1324 out.push((Some(p), attr.value()));
1325 }
1326 a = attr.next.get();
1327 }
1328 out
1329 }
1330 };
1331 let a_uri = nsdefs.iter().find(|(p, _)| *p == Some("a")).map(|(_, u)| *u);
1332 let b_uri = nsdefs.iter().find(|(p, _)| *p == Some("b")).map(|(_, u)| *u);
1333 assert_eq!(a_uri, Some("urn:x"));
1334 assert_eq!(b_uri, Some("urn:x"),
1335 "NMTOKEN normalization should strip whitespace; got {b_uri:?}");
1336 }
1337
1338 /// CDATA-typed xmlns (the default when no ATTLIST covers it):
1339 /// whitespace inside the value is preserved. This is the spec
1340 /// behaviour — without an `<!ATTLIST>` redeclaring the type the
1341 /// raw value goes through. Guards against over-normalization.
1342 #[test]
1343 fn xmlns_cdata_value_is_not_stripped() {
1344 let xml = r#"<r xmlns:b=" urn:x "/>"#;
1345 let doc = parse_ns(xml);
1346 let root = doc.root();
1347 let b_uri = {
1348 #[cfg(feature = "c-abi")]
1349 { root.ns_declarations().find(|(p, _)| *p == Some("b")).map(|(_, u)| u) }
1350 #[cfg(not(feature = "c-abi"))]
1351 {
1352 let mut a = root.first_attribute.get();
1353 let mut found = None;
1354 while let Some(attr) = a {
1355 if attr.name() == "xmlns:b" { found = Some(attr.value()); break; }
1356 a = attr.next.get();
1357 }
1358 found
1359 }
1360 };
1361 assert_eq!(b_uri, Some(" urn:x "),
1362 "no ATTLIST → no non-CDATA normalization, URI must stay verbatim");
1363 }
1364
1365 // ── XML Namespaces 1.0 § 6.3 "Unique Att Spec" ──
1366
1367 /// Two prefixes binding to the SAME namespace URI on the same
1368 /// element make `a:attr` and `b:attr` resolve to the same
1369 /// expanded name `{URI}attr` — must be rejected. Catches the
1370 /// straightforward (same-element) collision.
1371 #[test]
1372 fn duplicate_expanded_attribute_name_rejected() {
1373 let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
1374 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1375 .expect_err("two prefixes binding the same URI with same local must be rejected");
1376 let msg = err.to_string().to_lowercase();
1377 assert!(msg.contains("duplicate") && (msg.contains("namespace") || msg.contains("attribute")),
1378 "expected duplicate-attribute message, got: {err}");
1379 }
1380
1381 /// The same collision but the xmlns declarations live on a
1382 /// parent element — collision still detected because the
1383 /// namespace resolver walks the full in-scope binding stack.
1384 /// Mirrors W3C rmt-ns10-012's structural shape (xmlns on
1385 /// outer, prefixed attrs on inner) without the DTD-driven
1386 /// normalization layer.
1387 #[test]
1388 fn duplicate_expanded_attribute_name_rejected_across_elements() {
1389 let xml = r#"<f xmlns:a="urn:x" xmlns:b="urn:x"><g a:attr="1" b:attr="2"/></f>"#;
1390 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1391 .expect_err("inner-element collision must be caught through inherited bindings");
1392 let msg = err.to_string().to_lowercase();
1393 assert!(msg.contains("duplicate"),
1394 "expected duplicate-attribute message, got: {err}");
1395 }
1396
1397 /// The combined case — DTD-aware normalization + namespace-
1398 /// aware uniqueness must both fire to catch this. This is
1399 /// exactly W3C rmt-ns10-012's input shape, distilled to the
1400 /// minimum needed to flip the test verdict. Drops the W3C
1401 /// suite's external machinery so the failure mode is testable
1402 /// from inside the core crate without the harness.
1403 #[test]
1404 fn nmtoken_normalization_unlocks_ns_uniqueness_collision() {
1405 let xml = r#"<?xml version="1.0"?>
1406<!DOCTYPE foo [
1407<!ELEMENT foo ANY>
1408<!ATTLIST foo xmlns:a CDATA #IMPLIED
1409 xmlns:b NMTOKEN #IMPLIED>
1410<!ELEMENT bar ANY>
1411<!ATTLIST bar a:attr CDATA #IMPLIED
1412 b:attr CDATA #IMPLIED>
1413]>
1414<foo xmlns:a="urn:x" xmlns:b=" urn:x ">
1415<bar a:attr="1" b:attr="2"/>
1416</foo>"#;
1417 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1418 .expect_err("rmt-ns10-012 minimum repro must be rejected once NMTOKEN normalization \
1419 strips the whitespace from xmlns:b's value");
1420 let msg = err.to_string().to_lowercase();
1421 assert!(msg.contains("duplicate"),
1422 "expected duplicate-after-expansion error, got: {err}");
1423 }
1424
1425 /// XML 1.0 § 3.3.3 attribute-value normalization for a regular
1426 /// (non-xmlns) NMTOKEN attribute: leading/trailing whitespace
1427 /// stripped, internal runs collapsed. Confirms the normalization
1428 /// helper applies to ALL non-CDATA attributes, not just xmlns:*.
1429 #[test]
1430 fn nmtoken_attribute_value_is_normalized() {
1431 let xml = r#"<?xml version="1.0"?>
1432<!DOCTYPE r [
1433<!ELEMENT r EMPTY>
1434<!ATTLIST r kind NMTOKEN #IMPLIED>
1435]>
1436<r kind=" alpha "/>"#;
1437 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1438 let kind = doc.root().attributes()
1439 .find(|a| a.name() == "kind")
1440 .map(|a| a.value());
1441 assert_eq!(kind, Some("alpha"),
1442 "NMTOKEN value should be stripped; got {kind:?}");
1443 }
1444
1445 /// Same rule for ID-typed attributes — internal whitespace
1446 /// collapses too. Without normalization, two `id` values that
1447 /// differ only by whitespace would compare unequal under
1448 /// `getElementById` etc.
1449 #[test]
1450 fn id_attribute_value_is_normalized_collapsed() {
1451 let xml = r#"<?xml version="1.0"?>
1452<!DOCTYPE r [
1453<!ELEMENT r EMPTY>
1454<!ATTLIST r tag ID #IMPLIED>
1455]>
1456<r tag=" a b c "/>"#;
1457 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1458 let tag = doc.root().attributes()
1459 .find(|a| a.name() == "tag")
1460 .map(|a| a.value());
1461 assert_eq!(tag, Some("a b c"),
1462 "ID value should strip + collapse; got {tag:?}");
1463 }
1464
1465 /// CDATA-typed attribute (the default when no ATTLIST covers it)
1466 /// preserves whitespace verbatim — guards against
1467 /// over-normalization of CDATA-shaped values.
1468 #[test]
1469 fn cdata_attribute_value_is_not_stripped() {
1470 // No ATTLIST → defaults to CDATA → no non-CDATA stripping.
1471 let xml = r#"<r tag=" a b "/>"#;
1472 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1473 let tag = doc.root().attributes()
1474 .find(|a| a.name() == "tag")
1475 .map(|a| a.value());
1476 assert_eq!(tag, Some(" a b "),
1477 "CDATA-defaulted value must stay verbatim; got {tag:?}");
1478 }
1479
1480 /// Enumeration-typed attribute: stripped before matching the
1481 /// enum. Without this, `<r kind=" yes ">` would silently fail
1482 /// even though the declared enum is `(yes|no)` — and we'd quietly
1483 /// accept the un-matched value instead of validating against it.
1484 #[test]
1485 fn enumeration_attribute_value_is_normalized() {
1486 let xml = r#"<?xml version="1.0"?>
1487<!DOCTYPE r [
1488<!ELEMENT r EMPTY>
1489<!ATTLIST r kind (yes|no) #IMPLIED>
1490]>
1491<r kind=" yes "/>"#;
1492 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1493 let kind = doc.root().attributes()
1494 .find(|a| a.name() == "kind")
1495 .map(|a| a.value());
1496 assert_eq!(kind, Some("yes"));
1497 }
1498
1499 // ── resolve_entities = false (EntityRef node kind) ──
1500
1501 /// Default: entity references expand inline into Text. Confirms
1502 /// the baseline behaviour the new flag opts out of.
1503 #[test]
1504 fn resolve_entities_default_expands_inline() {
1505 let xml = r#"<?xml version="1.0"?>
1506<!DOCTYPE r [<!ENTITY hi "hello">]>
1507<r>&hi;</r>"#;
1508 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1509 let root = doc.root();
1510 // Should have a single Text child "hello", no EntityRef.
1511 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1512 assert_eq!(kinds, vec![NodeKind::Text]);
1513 let text = root.children().next().unwrap().content();
1514 assert_eq!(text, "hello");
1515 }
1516
1517 /// `resolve_entities = false` preserves user-defined references
1518 /// as `NodeKind::EntityRef` nodes whose `name` is the entity
1519 /// name and whose `content` round-trips to `&name;` source.
1520 #[test]
1521 fn resolve_entities_false_emits_entity_ref_node() {
1522 let xml = r#"<?xml version="1.0"?>
1523<!DOCTYPE r [<!ENTITY hi "hello">]>
1524<r>before &hi; after</r>"#;
1525 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1526 let doc = parse_str(xml, &opts).expect("parse");
1527 let root = doc.root();
1528 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1529 assert_eq!(
1530 kinds,
1531 vec![NodeKind::Text, NodeKind::EntityRef, NodeKind::Text],
1532 "expected Text-EntityRef-Text triple, got {kinds:?}"
1533 );
1534 // The middle child carries the entity name + literal form.
1535 let ref_node = root.children().nth(1).unwrap();
1536 assert_eq!(ref_node.name(), "hi");
1537 assert_eq!(ref_node.content(), "&hi;");
1538 }
1539
1540 /// Predefined entities always expand regardless of the flag —
1541 /// they're part of the character data production, not the
1542 /// entity-reference machinery.
1543 #[test]
1544 fn resolve_entities_false_still_expands_predefined() {
1545 let xml = r#"<r>a & b < c</r>"#;
1546 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1547 let doc = parse_str(xml, &opts).expect("parse");
1548 let root = doc.root();
1549 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1550 assert_eq!(kinds, vec![NodeKind::Text],
1551 "predefined entities must expand inline, got {kinds:?}");
1552 assert_eq!(root.children().next().unwrap().content(), "a & b < c");
1553 }
1554
1555 /// Numeric character references always expand inline too.
1556 #[test]
1557 fn resolve_entities_false_still_expands_numeric() {
1558 let xml = r#"<r>before A after</r>"#;
1559 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1560 let doc = parse_str(xml, &opts).expect("parse");
1561 let root = doc.root();
1562 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1563 assert_eq!(kinds, vec![NodeKind::Text]);
1564 assert_eq!(root.children().next().unwrap().content(), "before A after");
1565 }
1566
1567 /// Round-trip: serialize a tree with EntityRef nodes back to
1568 /// source. The literal `&name;` form is preserved.
1569 #[test]
1570 fn resolve_entities_false_round_trips_through_serializer() {
1571 let xml = r#"<r>x &hi; y</r>"#;
1572 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1573 let doc = parse_str(xml, &opts).expect("parse");
1574 let out = crate::serialize_to_string(&doc);
1575 assert!(out.contains("&hi;"),
1576 "EntityRef should serialize as `&hi;`, got: {out}");
1577 }
1578
1579 /// Edition guard: without `namespace_aware`, lexical names rule.
1580 /// `a:attr` and `b:attr` are different names → accepted. Same
1581 /// input as the previous tests, but the namespace pass doesn't
1582 /// run. Confirms the new check is properly gated.
1583 #[test]
1584 fn duplicate_expanded_name_accepted_when_namespace_blind() {
1585 let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
1586 // namespace_aware = false (the default).
1587 parse_str(xml, &ParseOptions::default())
1588 .expect("namespace-blind parse must accept lexically-distinct attribute names");
1589 }
1590
1591 #[test]
1592 fn line_numbers_basic() {
1593 // line 1: <r>
1594 // line 2: <a/>
1595 // line 3: <b/>
1596 // line 4: </r>
1597 let doc = parse("<r>\n <a/>\n <b/>\n</r>");
1598 let root = doc.root();
1599 let a = root.children().find(|n| n.is_element()).unwrap();
1600 let b = a.next_sibling.get().and_then(|n|
1601 if n.is_element() { Some(n) } else { n.next_sibling.get() }
1602 ).unwrap();
1603
1604 assert_eq!(root.line_no(), 1, "root <r> on line 1");
1605 assert_eq!(a.line_no(), 2, "<a/> on line 2");
1606 assert_eq!(b.line_no(), 3, "<b/> on line 3");
1607 }
1608
1609 /// Regression guard: a previous implementation called
1610 /// `scanner::compute_line_col` once per StartElement, which rescans
1611 /// `src[0..name_offset]` from byte 0 each call — O(N × file_size).
1612 /// On docs with many elements this slowed the parser by 10×–100×.
1613 /// The current implementation maintains an incremental cursor in
1614 /// `drive()`; this test exercises it across many lines to make sure
1615 /// the cursor stays in sync.
1616 #[test]
1617 fn line_numbers_many_elements() {
1618 // 300 elements, one per line. Each <e i="N"/> on line N+1
1619 // (the <root> opener is line 1).
1620 let mut src = String::from("<root>\n");
1621 let n = 300u32;
1622 for i in 0..n {
1623 src.push_str(&format!(" <e i=\"{i}\"/>\n"));
1624 }
1625 src.push_str("</root>\n");
1626
1627 let doc = parse(&src);
1628 assert_eq!(doc.root().line_no(), 1);
1629
1630 let mut expected: u32 = 2;
1631 let mut walked = 0u32;
1632 for child in doc.root().children().filter(|n| n.is_element()) {
1633 // line is u16 in c-abi mode, u32 in lean — both fit < 65535 here.
1634 assert_eq!(child.line_no(), expected,
1635 "child #{walked}: expected line {expected}, got {}", child.line_no());
1636 walked += 1;
1637 expected += 1;
1638 }
1639 assert_eq!(walked, n);
1640 }
1641
1642 #[test]
1643 fn nested_elements() {
1644 let doc = parse("<a><b><c/></b></a>");
1645 let a = doc.root();
1646 assert_eq!(a.name(), "a");
1647 let b = a.children().next().unwrap();
1648 assert_eq!(b.name(), "b");
1649 let c = b.children().next().unwrap();
1650 assert_eq!(c.name(), "c");
1651 // parent pointers
1652 assert!(std::ptr::eq(c.parent.get().unwrap(), b));
1653 assert!(std::ptr::eq(b.parent.get().unwrap(), a));
1654 }
1655
1656 #[test]
1657 fn attributes_in_order() {
1658 let doc = parse(r#"<el id="1" class="x" data-y="42"/>"#);
1659 let pairs: Vec<(&str, &str)> = doc.root().attributes()
1660 .map(|a| (a.name(), a.value()))
1661 .collect();
1662 assert_eq!(pairs, vec![("id", "1"), ("class", "x"), ("data-y", "42")]);
1663 }
1664
1665 #[test]
1666 fn mixed_content() {
1667 let doc = parse("<r>before<!-- c --><b>x</b><![CDATA[<raw>]]>after</r>");
1668 let kinds: Vec<NodeKind> = doc.root().children().map(|c| c.kind).collect();
1669 assert_eq!(kinds, vec![
1670 NodeKind::Text, NodeKind::Comment, NodeKind::Element,
1671 NodeKind::CData, NodeKind::Text,
1672 ]);
1673 }
1674
1675 #[test]
1676 fn pi_inside_element() {
1677 let doc = parse(r#"<r><?xml-stylesheet href="s.xsl"?></r>"#);
1678 let pi = doc.root().children().next().unwrap();
1679 assert_eq!(pi.kind, NodeKind::Pi);
1680 assert_eq!(pi.name(), "xml-stylesheet");
1681 assert_eq!(pi.content(), r#"href="s.xsl""#);
1682 }
1683
1684 #[test]
1685 fn entity_in_text_is_expanded() {
1686 let doc = parse("<r>a&b<c</r>");
1687 let t = doc.root().children().next().unwrap();
1688 assert_eq!(t.kind, NodeKind::Text);
1689 assert_eq!(t.content(), "a&b<c");
1690 }
1691
1692 #[test]
1693 fn entity_in_attr_is_expanded() {
1694 let doc = parse(r#"<r v="a&b"/>"#);
1695 let v = doc.root().attributes().next().unwrap();
1696 assert_eq!(v.value(), "a&b");
1697 }
1698
1699 #[test]
1700 fn deeply_nested_doc() {
1701 let mut xml = String::new();
1702 for _ in 0..50 { xml.push_str("<n>"); }
1703 xml.push_str("hello");
1704 for _ in 0..50 { xml.push_str("</n>"); }
1705 let doc = parse(&xml);
1706 let mut cur = doc.root();
1707 let mut depth = 1;
1708 while let Some(c) = cur.children().next() {
1709 if c.kind == NodeKind::Element { cur = c; depth += 1; } else { break; }
1710 }
1711 assert_eq!(depth, 50);
1712 assert_eq!(cur.children().next().unwrap().content(), "hello");
1713 }
1714
1715 #[test]
1716 fn root_lifetime_keeps_tree_alive() {
1717 let doc = parse("<r><a><b>x</b></a></r>");
1718 let a = doc.root().children().next().unwrap();
1719 let b = a.children().next().unwrap();
1720 assert_eq!(b.text_content(), Some("x"));
1721 }
1722
1723 #[test]
1724 fn errors_propagate_from_reader() {
1725 let err = parse_str("<r><a></b></r>", &ParseOptions::default());
1726 assert!(err.is_err());
1727 }
1728
1729 #[test]
1730 fn missing_root_returns_error() {
1731 // An empty document fails earlier (XML decl alone) — use whitespace-only.
1732 let r = parse_str(" ", &ParseOptions::default());
1733 assert!(r.is_err());
1734 }
1735
1736 // ── namespace tests ────────────────────────────────────────────────
1737
1738 #[test]
1739 fn no_namespaces_unchanged() {
1740 let doc = parse_ns("<root><child/></root>");
1741 assert!(doc.root().namespace.get().is_none());
1742 }
1743
1744 #[test]
1745 fn default_namespace_applied_to_element() {
1746 let doc = parse_ns(r#"<root xmlns="http://example.com/"/>"#);
1747 let ns = doc.root().namespace.get().unwrap();
1748 assert!(ns.prefix.is_none());
1749 assert_eq!(ns.href(), "http://example.com/");
1750 }
1751
1752 #[test]
1753 fn default_namespace_not_applied_to_attr() {
1754 let doc = parse_ns(r#"<root xmlns="http://example.com/" id="1"/>"#);
1755 let id_attr = doc.root().attributes().find(|a| a.name() == "id").unwrap();
1756 assert!(id_attr.namespace.get().is_none(), "default ns must not apply to unprefixed attrs");
1757 }
1758
1759 #[test]
1760 fn prefixed_element_resolved() {
1761 let doc = parse_ns(r#"<dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">X</dc:title>"#);
1762 let ns = doc.root().namespace.get().unwrap();
1763 assert_eq!(ns.prefix(), Some("dc"));
1764 assert_eq!(ns.href(), "http://purl.org/dc/elements/1.1/");
1765 }
1766
1767 #[test]
1768 fn prefixed_attribute_resolved() {
1769 let doc = parse_ns(
1770 r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>"#
1771 );
1772 // Match by (local-name, prefix): `name()` is the full QName on
1773 // the lean build but the local part under c-abi.
1774 let nil = doc.root().attributes()
1775 .find(|a| a.local_name() == "nil"
1776 && a.namespace.get().and_then(|n| n.prefix()) == Some("xsi"))
1777 .unwrap();
1778 assert_eq!(nil.namespace.get().unwrap().href(),
1779 "http://www.w3.org/2001/XMLSchema-instance");
1780 }
1781
1782 #[test]
1783 fn xml_prefix_builtin() {
1784 let doc = parse_ns(r#"<root xml:lang="en"/>"#);
1785 let lang = doc.root().attributes()
1786 .find(|a| a.local_name() == "lang"
1787 && a.namespace.get().and_then(|n| n.prefix()) == Some("xml"))
1788 .unwrap();
1789 assert_eq!(lang.namespace.get().unwrap().href(),
1790 "http://www.w3.org/XML/1998/namespace");
1791 }
1792
1793 #[test]
1794 fn nested_prefix_scope_inherits_outer() {
1795 let doc = parse_ns(r#"
1796 <root xmlns:a="http://a.com/">
1797 <a:child xmlns:b="http://b.com/">
1798 <b:leaf/>
1799 </a:child>
1800 </root>
1801 "#);
1802 let root = doc.root();
1803 assert!(root.namespace.get().is_none());
1804 // c-abi mode stores `name` as the local part only (libxml2
1805 // convention); the lean build keeps the full QName.
1806 #[cfg(feature = "c-abi")]
1807 let child_local = "child";
1808 #[cfg(not(feature = "c-abi"))]
1809 let child_local = "a:child";
1810 let child = root.children().find(|c| c.is_element() && c.name() == child_local).unwrap();
1811 assert_eq!(child.namespace.get().unwrap().href(), "http://a.com/");
1812 let leaf = child.children().find(|c| c.is_element()).unwrap();
1813 assert_eq!(leaf.namespace.get().unwrap().href(), "http://b.com/");
1814 }
1815
1816 #[test]
1817 fn undeclared_prefix_is_error() {
1818 let r = parse_str("<dc:title>X</dc:title>", &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
1819 let msg = r.unwrap_err().to_string();
1820 assert!(msg.contains("undeclared") || msg.contains("prefix"), "{msg}");
1821 }
1822
1823 #[test]
1824 fn default_namespace_override_in_child() {
1825 let doc = parse_ns(r#"
1826 <root xmlns="http://outer.com/">
1827 <inner xmlns="http://inner.com/"/>
1828 </root>
1829 "#);
1830 assert_eq!(doc.root().namespace.get().unwrap().href(), "http://outer.com/");
1831 let inner = doc.root().children().find(|c| c.is_element()).unwrap();
1832 assert_eq!(inner.namespace.get().unwrap().href(), "http://inner.com/");
1833 }
1834
1835 #[test]
1836 fn undeclare_default_namespace() {
1837 let doc = parse_ns(r#"
1838 <root xmlns="http://example.com/">
1839 <child xmlns=""/>
1840 </root>
1841 "#);
1842 assert!(doc.root().namespace.get().is_some());
1843 let child = doc.root().children().find(|c| c.is_element()).unwrap();
1844 assert!(child.namespace.get().is_none(), "xmlns='' should clear the default namespace");
1845 }
1846
1847 #[test]
1848 fn xmlns_prefix_cannot_be_declared() {
1849 let r = parse_str(
1850 r#"<r xmlns:xmlns="http://x.com/"/>"#,
1851 &ParseOptions { namespace_aware: true, ..ParseOptions::default() },
1852 );
1853 assert!(r.is_err());
1854 }
1855
1856 #[test]
1857 fn duplicate_attribute_after_ns_expansion() {
1858 // Two prefixed attrs that expand to the same (ns, local) pair.
1859 let r = parse_str(
1860 r#"<r xmlns:a="http://x.com/" xmlns:b="http://x.com/" a:id="1" b:id="2"/>"#,
1861 &ParseOptions { namespace_aware: true, ..ParseOptions::default() },
1862 );
1863 assert!(r.is_err(), "duplicate expanded attribute name must be rejected");
1864 }
1865
1866 #[test]
1867 fn deep_nesting_pops_scope_on_close() {
1868 // After nesting popped back to root level, an inner prefix becomes undeclared.
1869 let r = parse_str(r#"
1870 <root>
1871 <a xmlns:p="http://x.com/"><p:leaf/></a>
1872 <p:bad/>
1873 </root>
1874 "#, &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
1875 assert!(r.is_err());
1876 }
1877
1878 #[test]
1879 fn xml_decl_fields_are_captured() {
1880 let doc = parse(r#"<?xml version="1.1" encoding="ISO-8859-1" standalone="yes"?><r/>"#);
1881 assert_eq!(doc.version, "1.1");
1882 assert_eq!(doc.encoding, "ISO-8859-1");
1883 assert_eq!(doc.standalone, Some(true));
1884 }
1885
1886 #[test]
1887 fn xml_decl_defaults_when_absent() {
1888 // No `<?xml … ?>` declaration → encoding stays empty
1889 // (matches libxml2's NULL doc->encoding) so serializers
1890 // omit the encoding attribute on output.
1891 let doc = parse("<r/>");
1892 assert_eq!(doc.version, "1.0");
1893 assert_eq!(doc.encoding, "");
1894 assert_eq!(doc.standalone, None);
1895 }
1896
1897 #[test]
1898 fn xml_decl_partial_keeps_encoding_default() {
1899 // Only version present — encoding stays empty (no
1900 // declaration to copy), standalone absent.
1901 let doc = parse(r#"<?xml version="1.0"?><r/>"#);
1902 assert_eq!(doc.version, "1.0");
1903 assert_eq!(doc.encoding, "");
1904 assert_eq!(doc.standalone, None);
1905 }
1906
1907 #[test]
1908 fn xml_decl_standalone_no_is_captured() {
1909 // Note: standalone without encoding is rejected by both legacy
1910 // and arena parsers (pre-existing behaviour) — include encoding.
1911 let doc = parse(r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><r/>"#);
1912 assert_eq!(doc.standalone, Some(false));
1913 }
1914
1915 #[test]
1916 fn many_siblings_preserve_order() {
1917 let mut xml = String::from("<r>");
1918 for i in 0..100 {
1919 xml.push_str(&format!("<i>{i}</i>"));
1920 }
1921 xml.push_str("</r>");
1922 let doc = parse(&xml);
1923 let texts: Vec<&str> = doc.root().children()
1924 .filter(|c| c.kind == NodeKind::Element)
1925 .map(|c| c.text_content().unwrap_or(""))
1926 .collect();
1927 assert_eq!(texts.len(), 100);
1928 assert_eq!(texts[0], "0");
1929 assert_eq!(texts[99], "99");
1930 }
1931
1932 // ── parse_bytes_in_place — entry point + call-time gates ──────
1933
1934 #[test]
1935 fn in_place_parses_basic_document() {
1936 let buf = b"<root><child id=\"1\">hello</child></root>".to_vec();
1937 let doc = parse_bytes_in_place(buf, &ParseOptions::default())
1938 .expect("in-place parse should succeed on well-formed input");
1939 assert_eq!(doc.root().name(), "root");
1940 let child = doc.root().children().next().expect("child element");
1941 assert_eq!(child.name(), "child");
1942 assert_eq!(child.attributes().next().unwrap().value(), "1");
1943 }
1944
1945 #[test]
1946 fn in_place_rejects_recovery_mode_at_call_time() {
1947 let buf = b"<r/>".to_vec();
1948 let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
1949 let err = parse_bytes_in_place(buf, &opts)
1950 .expect_err("recovery_mode=true must be rejected up front");
1951 assert!(
1952 err.message.contains("recovery_mode"),
1953 "error should mention recovery_mode, got: {}", err.message,
1954 );
1955 }
1956
1957 #[test]
1958 fn in_place_transcodes_non_utf8_when_auto_transcode_on() {
1959 // ISO-8859-1 with `<?xml encoding="ISO-8859-1"?>` and one non-ASCII
1960 // byte (0xE9 = 'é'). Transcoded to UTF-8 upfront, then parsed.
1961 let buf: Vec<u8> =
1962 b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xe9</r>".to_vec();
1963 let opts = ParseOptions { auto_transcode: true, ..ParseOptions::default() };
1964 let doc = parse_bytes_in_place(buf, &opts).expect("transcoded parse should succeed");
1965 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
1966 assert_eq!(text, "café");
1967 }
1968
1969 #[test]
1970 fn in_place_rejects_invalid_utf8_when_auto_transcode_off() {
1971 let buf: Vec<u8> = b"<r>\xff</r>".to_vec();
1972 let opts = ParseOptions { auto_transcode: false, ..ParseOptions::default() };
1973 let err = parse_bytes_in_place(buf, &opts).expect_err("invalid UTF-8 must be rejected");
1974 assert_eq!(err.domain, crate::error::ErrorDomain::Encoding);
1975 }
1976
1977 // ── simdutf8 ⇄ std::str::from_utf8 equivalence ───────────────
1978 //
1979 // The input-validation gates (`parse_bytes_in_place`,
1980 // `transcode_and_validate`, `XmlBytesReader::from_bytes`) use
1981 // `simdutf8::compat::from_utf8` as a drop-in for `std::str::from_utf8`.
1982 // The whole correctness claim is that it is *behaviorally identical*:
1983 // same accept/reject verdict, and on rejection the same `valid_up_to()`
1984 // and `error_len()` — the parser pins error line/col to `valid_up_to()`,
1985 // so any divergence would silently shift reported error positions.
1986 // These tests pin that equivalence against `std` as the oracle.
1987
1988 /// Reduce a validation outcome to a comparable shape: `Ok(())` when
1989 /// valid, or the error's `(valid_up_to, error_len)` when not.
1990 fn std_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
1991 std::str::from_utf8(b)
1992 .map(|_| ())
1993 .map_err(|e| (e.valid_up_to(), e.error_len()))
1994 }
1995
1996 fn simd_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
1997 simdutf8::compat::from_utf8(b)
1998 .map(|_| ())
1999 .map_err(|e| (e.valid_up_to(), e.error_len()))
2000 }
2001
2002 #[test]
2003 fn simdutf8_matches_std_at_chunk_boundaries() {
2004 // SIMD validators process vector-width chunks (16/32/64 bytes) then a
2005 // scalar tail; bugs hide where a malformed byte straddles that seam.
2006 // Sweep a lone 0xFF (never valid UTF-8) across every offset of buffers
2007 // sized around the common vector widths, plus the clean buffer.
2008 for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129] {
2009 let base = vec![b'a'; len];
2010 assert_eq!(std_verdict(&base), simd_verdict(&base), "clean buffer len {len}");
2011 for pos in 0..len {
2012 let mut bad = base.clone();
2013 bad[pos] = 0xFF;
2014 assert_eq!(
2015 std_verdict(&bad), simd_verdict(&bad),
2016 "diverged: lone 0xFF at offset {pos} in len {len}",
2017 );
2018 }
2019 }
2020 }
2021
2022 #[test]
2023 fn simdutf8_matches_std_on_adversarial_utf8() {
2024 // The classic failure modes for hand-rolled UTF-8 validators: truncated
2025 // multibyte sequences, overlong encodings, surrogate code points, lone
2026 // continuation bytes, and valid→invalid transitions mid-stream.
2027 let cases: &[&[u8]] = &[
2028 b"",
2029 b"hello",
2030 &[0xC3, 0xA9], // é — valid 2-byte
2031 &[0xC3], // truncated 2-byte lead
2032 &[0xE2, 0x82, 0xAC], // € — valid 3-byte
2033 &[0xE2, 0x82], // truncated 3-byte
2034 &[0xF0, 0x9F, 0x98, 0x80], // 😀 — valid 4-byte
2035 &[0xF0, 0x9F, 0x98], // truncated 4-byte
2036 &[0x80], // lone continuation
2037 &[0xBF], // lone continuation
2038 &[0xC0, 0x80], // overlong NUL
2039 &[0xE0, 0x80, 0x80], // overlong 3-byte
2040 &[0xF0, 0x80, 0x80, 0x80], // overlong 4-byte
2041 &[0xED, 0xA0, 0x80], // lone high surrogate U+D800
2042 &[0xED, 0xBF, 0xBF], // lone low surrogate U+DFFF
2043 &[0xF4, 0x90, 0x80, 0x80], // above U+10FFFF
2044 &[0xFF],
2045 &[0xFE],
2046 b"caf\xe9", // Latin-1 é — invalid as UTF-8
2047 &[b'o', b'k', 0xC3, 0xA9, 0xFF, b'x'], // valid run then invalid byte
2048 ];
2049 for c in cases {
2050 assert_eq!(std_verdict(c), simd_verdict(c), "diverged on {c:02x?}");
2051 }
2052 }
2053
2054 #[test]
2055 fn simdutf8_matches_std_on_random_bytes() {
2056 // Deterministic xorshift corpus — no rng/clock dependency, so failures
2057 // reproduce exactly. Bias one byte in four into the 0x80..=0xFF range
2058 // so lead/continuation logic gets exercised, not just ASCII runs.
2059 let mut state = 0x9E3779B97F4A7C15u64;
2060 let mut next = move || {
2061 state ^= state << 13;
2062 state ^= state >> 7;
2063 state ^= state << 17;
2064 state
2065 };
2066 for _ in 0..20_000 {
2067 let len = (next() % 70) as usize;
2068 let mut buf = Vec::with_capacity(len);
2069 for _ in 0..len {
2070 let r = next();
2071 let byte = if r & 3 == 0 {
2072 (r >> 8) as u8 | 0x80
2073 } else {
2074 (r >> 8) as u8 & 0x7F
2075 };
2076 buf.push(byte);
2077 }
2078 assert_eq!(std_verdict(&buf), simd_verdict(&buf), "diverged on {buf:02x?}");
2079 }
2080 }
2081
2082 #[test]
2083 fn in_place_decodes_builtin_entities() {
2084 // Slow-path exercise: text content with `&` triggers entity
2085 // expansion in the reader. In-place mode mutates the source
2086 // buffer (overwriting `&` with `&`) and emits the text as
2087 // Cow::Borrowed pointing into the now-mutated buffer. The arena
2088 // parser then takes the alloc_str_borrow zero-copy path.
2089 let buf = b"<r>tom & jerry</r>".to_vec();
2090 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2091 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2092 assert_eq!(text, "tom & jerry");
2093 }
2094
2095 #[test]
2096 fn in_place_decodes_multiple_builtin_entities() {
2097 let buf = b"<r><hello> & "hi"</r>".to_vec();
2098 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2099 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2100 assert_eq!(text, r#"<hello> & "hi""#);
2101 }
2102
2103 #[test]
2104 fn in_place_text_without_entities_works() {
2105 // Fast path (Cow::Borrowed straight from reader, no slow path).
2106 let buf = b"<r>plain text with no entities</r>".to_vec();
2107 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2108 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2109 assert_eq!(text, "plain text with no entities");
2110 }
2111
2112 #[test]
2113 fn in_place_accepts_user_entity_that_shrinks() {
2114 // `&x;` (4 source bytes) expands to "AB" (2 bytes) — fits.
2115 let buf = br#"<!DOCTYPE r [<!ENTITY x "AB">]><r>&x;</r>"#.to_vec();
2116 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2117 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2118 assert_eq!(text, "AB");
2119 }
2120
2121 #[test]
2122 fn in_place_rejects_user_entity_that_grows() {
2123 // `&hi;` (5 source bytes) tries to expand to "Hello, World!" (13 bytes).
2124 // Doesn't fit → error at use site with a clear message.
2125 let buf = br#"<!DOCTYPE r [<!ENTITY hi "Hello, World!">]><r>&hi;</r>"#.to_vec();
2126 let err = parse_bytes_in_place(buf, &ParseOptions::default()).expect_err(
2127 "expansion bigger than reference must be rejected in in-place mode",
2128 );
2129 assert!(
2130 err.message.contains("exceeds source span")
2131 || err.message.contains("expansion"),
2132 "error should explain the expansion mismatch, got: {}", err.message,
2133 );
2134 }
2135
2136 #[test]
2137 fn in_place_numeric_char_refs_work() {
2138 // `É` (6 source bytes) → `É` (2 UTF-8 bytes) — fits.
2139 // `𝐀` (9 source bytes) → `𝐀` (4 UTF-8 bytes) — fits.
2140 let buf = "<r>cafÉ and 𝐀</r>".as_bytes().to_vec();
2141 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2142 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2143 assert_eq!(text, "cafÉ and 𝐀");
2144 }
2145
2146 #[test]
2147 fn in_place_rejects_recursive_entity() {
2148 // Direct recursion — same well-formedness error as parse_bytes
2149 // (XML 1.0 § 4.1). The expansion stack catches it before any
2150 // in-place mutation happens.
2151 let buf = br#"<!DOCTYPE r [<!ENTITY a "&a;">]><r>&a;</r>"#.to_vec();
2152 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2153 .expect_err("recursive entity must be rejected");
2154 let msg = err.message.to_lowercase();
2155 assert!(
2156 msg.contains("recurs") || msg.contains("cycle"),
2157 "error should mention recursion/cycle, got: {}", err.message,
2158 );
2159 }
2160
2161 // ── parse_bytes_in_place — flag honoring ────────────────────────────────
2162 //
2163 // These tests pin down a behavioral contract: `parse_bytes_in_place`
2164 // honors every flag on the caller's `ParseOptions` as-is. It does
2165 // NOT silently flip the `skip_*` validation flags on (that override
2166 // used to exist; was removed deliberately so callers control the
2167 // validation / speed tradeoff). Each test pair below verifies one
2168 // flag:
2169 // - default `ParseOptions` (flag is `false`) → input rejected
2170 // - the same input with the flag set `true` → input accepted
2171 // If either half of a pair flips, something has reintroduced the
2172 // override or weakened the validator.
2173
2174 /// XML 1.0 § 2.3 [4]: names start with a letter, `_`, or `:` (or
2175 /// non-ASCII NameStartChar) — never a digit. `<1foo>` is invalid.
2176 #[test]
2177 fn in_place_default_opts_reject_bad_name_start() {
2178 let buf = b"<1foo/>".to_vec();
2179 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2180 .expect_err("default opts must reject name starting with digit");
2181 assert!(
2182 err.message.contains("name-start") || err.message.contains("name start"),
2183 "error should mention name-start, got: {}", err.message,
2184 );
2185 // libxml2-compatible code: XML_ERR_NAME_REQUIRED = 68.
2186 // Validates the Slice 5a contract that consumer-checked codes
2187 // round-trip via `err.code as i32`.
2188 assert_eq!(err.code, crate::error::ErrorCode::NameRequired);
2189 assert_eq!(err.code as i32, 68);
2190 }
2191
2192 #[test]
2193 fn in_place_skip_name_validation_accepts_bad_name_start() {
2194 // Same malformed name as above; with skip_name_validation the
2195 // name is accepted as-is and parsing reaches Eof cleanly.
2196 let buf = b"<1foo/>".to_vec();
2197 let opts = ParseOptions { skip_name_validation: true, ..ParseOptions::default() };
2198 let doc = parse_bytes_in_place(buf, &opts)
2199 .expect("skip_name_validation=true must accept malformed name");
2200 assert_eq!(doc.root().name(), "1foo");
2201 }
2202
2203 /// XML 1.0 § 3.1 [STag] WFC "Unique Att Spec": no element may have
2204 /// two attributes with the same name. `<r a="1" a="2"/>` is
2205 /// rejected when `skip_attr_validation` is false (the default).
2206 #[test]
2207 fn in_place_default_opts_reject_duplicate_attribute() {
2208 let buf = br#"<r a="1" a="2"/>"#.to_vec();
2209 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2210 .expect_err("default opts must reject duplicate attribute");
2211 assert!(
2212 err.message.to_lowercase().contains("duplicate") || err.message.contains("Unique Att Spec"),
2213 "error should mention duplicate-attribute, got: {}", err.message,
2214 );
2215 }
2216
2217 #[test]
2218 fn in_place_skip_attr_validation_accepts_duplicate_attribute() {
2219 let buf = br#"<r a="1" a="2"/>"#.to_vec();
2220 let opts = ParseOptions { skip_attr_validation: true, ..ParseOptions::default() };
2221 let doc = parse_bytes_in_place(buf, &opts)
2222 .expect("skip_attr_validation=true must accept duplicate attr");
2223 // Both attributes end up on the element when validation is off —
2224 // we don't dedup at the structural layer.
2225 let n_attrs = doc.root().attributes().count();
2226 assert_eq!(n_attrs, 2, "both duplicate attrs should be present");
2227 }
2228
2229 /// XML 1.0 § 3.1 [STag/ETag]: end tag name must match the start
2230 /// tag name. `<a></b>` is a well-formedness error.
2231 #[test]
2232 fn in_place_default_opts_reject_end_tag_mismatch() {
2233 let buf = b"<a></b>".to_vec();
2234 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2235 .expect_err("default opts must reject mismatched end tag");
2236 let msg = err.message.to_lowercase();
2237 assert!(
2238 msg.contains("mismatched") || msg.contains("expected"),
2239 "error should mention mismatched end tag, got: {}", err.message,
2240 );
2241 }
2242
2243 #[test]
2244 fn in_place_skip_end_tag_check_accepts_end_tag_mismatch() {
2245 let buf = b"<a></b>".to_vec();
2246 let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
2247 let doc = parse_bytes_in_place(buf, &opts)
2248 .expect("skip_end_tag_check=true must accept mismatched end tag");
2249 assert_eq!(doc.root().name(), "a");
2250 }
2251
2252 /// XML 1.0 § 2.2 [2]: only specific Unicode code points are valid
2253 /// XML characters. 0x01 (control character, not in the allowed
2254 /// set) is rejected when `skip_xml_char_validation` is false.
2255 #[test]
2256 fn in_place_default_opts_reject_invalid_xml_char() {
2257 // Text content containing 0x01 — valid UTF-8 (single ASCII byte)
2258 // but invalid as an XML 1.0 character.
2259 let buf = b"<r>hello\x01world</r>".to_vec();
2260 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2261 .expect_err("default opts must reject invalid XML char");
2262 // Validator's actual wording — check for either the section
2263 // citation or any mention of "character" / "valid".
2264 let msg = err.message.to_lowercase();
2265 assert!(
2266 msg.contains("xml 1.0") || msg.contains("char") || msg.contains("invalid"),
2267 "error should mention invalid XML char, got: {}", err.message,
2268 );
2269 }
2270
2271 #[test]
2272 fn in_place_skip_xml_char_validation_accepts_invalid_xml_char() {
2273 let buf = b"<r>hello\x01world</r>".to_vec();
2274 let opts = ParseOptions { skip_xml_char_validation: true, ..ParseOptions::default() };
2275 let doc = parse_bytes_in_place(buf, &opts)
2276 .expect("skip_xml_char_validation=true must accept 0x01 in text");
2277 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2278 assert_eq!(text.as_bytes(), b"hello\x01world");
2279 }
2280
2281 /// Combined "fast path" — all four skips enabled. Exercises the
2282 /// path callers actually reach for when they want maximum speed.
2283 /// Verifies it works on a well-formed doc (the more interesting
2284 /// cases are covered individually above; this is a smoke test).
2285 #[test]
2286 fn in_place_with_all_skips_parses_well_formed_doc() {
2287 let buf = b"<root><a id=\"1\">hello</a></root>".to_vec();
2288 let opts = ParseOptions {
2289 skip_xml_char_validation: true,
2290 skip_name_validation: true,
2291 skip_attr_validation: true,
2292 skip_end_tag_check: true,
2293 ..ParseOptions::default()
2294 };
2295 let doc = parse_bytes_in_place(buf, &opts).expect("all-skips parse");
2296 assert_eq!(doc.root().name(), "root");
2297 let a = doc.root().children().next().expect("element child");
2298 assert_eq!(a.name(), "a");
2299 assert_eq!(a.attributes().next().unwrap().value(), "1");
2300 }
2301}