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 #[cfg(feature = "c-abi")]
839 let line: u16 = raw_line.min(u16::MAX as u32) as u16;
840 #[cfg(not(feature = "c-abi"))]
841 let line: u32 = raw_line;
842 el.line = line;
843 // Keep the uncapped line for files past 65535 lines;
844 // `xmlGetLineNo` returns it in preference to the
845 // saturated `line`.
846 #[cfg(feature = "c-abi")]
847 {
848 el.full_line = raw_line;
849 }
850 }
851 // Entity-stream start tags carry their attrs pre-
852 // parsed (the lazy iterator can't surface bytes that
853 // don't live in `src`). Take them out first; the
854 // lazy `attrs()` path returns nothing in that case.
855 let entity_attr_pairs: Option<Vec<(Vec<u8>, Vec<u8>)>> =
856 tag.entity_attrs().map(|v| v.to_vec());
857
858 // Skip the attribute iterator entirely when there's
859 // nothing between the name and the closing `>` — a
860 // single empty-slice check is cheaper than constructing
861 // a Scanner just to have its `next()` immediately return
862 // None. Trailing whitespace inside the start tag still
863 // falls through (rare in practice; attrs() short-
864 // circuits on the first .next() either way).
865 if !tag.attrs_bytes().is_empty() {
866 for a in tag.attrs() { attr_buf.push(a?); }
867 }
868
869 // Splice pre-parsed entity-stream attrs into the buffer
870 // so the existing ns-aware / ns-blind loops below see
871 // them uniformly. Bytes are arena-copied; the
872 // lifetime cast launders the arena's lifetime to the
873 // attr_buf slot, sound because the arena outlives the
874 // BytesAttr.
875 if let Some(pairs) = entity_attr_pairs {
876 for (n_bytes, v_bytes) in pairs {
877 let n_arena: &str = b.alloc_str(unsafe {
878 std::str::from_utf8_unchecked(&n_bytes)
879 });
880 let v_arena: &str = b.alloc_str(unsafe {
881 std::str::from_utf8_unchecked(&v_bytes)
882 });
883 let n_slice: &[u8] = n_arena.as_bytes();
884 let v_slice: &[u8] = v_arena.as_bytes();
885 let n_ext: &[u8] = unsafe { &*(n_slice as *const [u8]) };
886 let v_ext: &[u8] = unsafe { &*(v_slice as *const [u8]) };
887 attr_buf.push(BytesAttr {
888 name: n_ext,
889 value: std::borrow::Cow::Borrowed(v_ext),
890 });
891 }
892 }
893
894 if !ns_aware {
895 // Fastest path — namespace-blind, mirrors legacy parse_bytes.
896 for a in attr_buf.drain(..) {
897 // Attr name is always &'src [u8] (no entity decode for
898 // names per XML 1.0 § 2.3) so we can always borrow it.
899 let aname = alloc_name_bytes_as_str(b, a.name);
900 let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
901 // XML 1.0 § 3.3.3 non-CDATA normalization — same
902 // helper the ns-aware branch uses. No-op when
903 // no ATTLIST covers this attribute.
904 let avalue: &str =
905 dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
906 let attr = b.new_attribute(aname, avalue);
907 b.append_attribute(el, attr);
908 }
909 } else {
910 // ── namespace-aware path ──────────────────────────────────
911 validate_qname(name, "element")?;
912 ns_frames.push(ns_bindings.len());
913 let mut new_bindings_this_frame = 0u32;
914 let mut any_attr_prefixed = false;
915 // Prefixed attributes paired with their original QName.
916 // Namespace resolution must wait until every `xmlns`
917 // declaration on this element has been seen, so collect
918 // them and resolve in the pass below. In c-abi the
919 // stored `name` is already reduced to the local part
920 // (libxml2 convention); the prefix is recovered from the
921 // QName recorded here.
922 let mut prefixed_attrs: Vec<(&Attribute<'_>, &str)> = Vec::new();
923
924 for a in attr_buf.drain(..) {
925 let aname = alloc_name_bytes_as_str(b, a.name);
926 let raw_avalue = alloc_cow_bytes_as_str(b, a.value);
927
928 // Namespace declarations (`xmlns` / `xmlns:foo`)
929 // do NOT belong in the element's attribute
930 // list under the c-abi layout — libxml2's
931 // `_xmlNode::properties` holds only real
932 // attributes; xmlns declarations live on the
933 // separate `nsDef` chain. In the lean
934 // (non-c-abi) build we keep them in both
935 // places for backwards-compatibility with
936 // existing API consumers that iterate
937 // `attributes()` expecting xmlns decls there.
938 let is_xmlns_decl =
939 aname == "xmlns" || aname.starts_with("xmlns:");
940
941 // XML 1.0 § 3.3.3 attribute-value normalization.
942 // Applied uniformly: if an `<!ATTLIST>` declared
943 // this attribute as non-CDATA (ID, IDREF,
944 // IDREFS, ENTITY/ENTITIES, NMTOKEN/NMTOKENS,
945 // NOTATION, enumeration), strip leading/trailing
946 // whitespace and collapse internal runs to a
947 // single space. Without this, ID equality,
948 // IDREF resolution, and enumeration matching
949 // would all silently disagree with the spec on
950 // values like `id=" abc "`. Helper no-ops when
951 // no ATTLIST covers the pair, so non-DTD docs
952 // pay nothing.
953 let avalue: &str =
954 dtd_normalize_attr_value(b, reader.dtd(), name, aname, raw_avalue);
955
956 #[cfg(not(feature = "c-abi"))]
957 let always_attach = true;
958 #[cfg(feature = "c-abi")]
959 let always_attach = false;
960 let is_prefixed = !is_xmlns_decl
961 && memchr::memchr(b':', aname.as_bytes()).is_some();
962 if always_attach || !is_xmlns_decl {
963 // c-abi follows libxml2: `name` holds the local
964 // part only, with the prefix carried on
965 // `attr->ns` after resolution. Mirror the
966 // element-name reduction so a prefixed attribute
967 // doesn't serialize as `p:p:name` and `keys()`
968 // reports the bare local name. The lean build
969 // keeps the raw QName (its serializer never
970 // re-prepends a prefix).
971 #[cfg(feature = "c-abi")]
972 let stored_name = if ns_aware && is_prefixed {
973 let i = memchr::memchr(b':', aname.as_bytes()).unwrap();
974 &aname[i + 1..]
975 } else {
976 aname
977 };
978 #[cfg(not(feature = "c-abi"))]
979 let stored_name = aname;
980 let attr: &Attribute<'_> = b.new_attribute(stored_name, avalue);
981 b.append_attribute(el, attr);
982 if ns_aware && is_prefixed {
983 prefixed_attrs.push((attr, aname));
984 }
985 }
986
987 if aname == "xmlns" {
988 let ns = b.new_namespace(None, avalue);
989 ns_bindings.push((None, erase_ns(ns)));
990 #[cfg(feature = "c-abi")]
991 { b.append_ns_def(el, ns); }
992 new_bindings_this_frame += 1;
993 } else if let Some(local) = aname.strip_prefix("xmlns:") {
994 validate_xmlns_decl(local, avalue)?;
995 if local == "xml" { continue; }
996 let ns = b.new_namespace(Some(local), avalue);
997 ns_bindings.push((Some(local), erase_ns(ns)));
998 #[cfg(feature = "c-abi")]
999 { b.append_ns_def(el, ns); }
1000 new_bindings_this_frame += 1;
1001 } else if memchr::memchr(b':', aname.as_bytes()).is_some() {
1002 any_attr_prefixed = true;
1003 }
1004 }
1005 active_user_bindings += new_bindings_this_frame;
1006
1007 // ── Element QName resolution ──
1008 // Fast path: no prefix and no user bindings ever in scope → no namespace.
1009 let elem_has_colon = memchr::memchr(b':', name.as_bytes()).is_some();
1010 if elem_has_colon || active_user_bindings > 0 {
1011 let el_ns = resolve_qname(name, &ns_bindings, /*is_attribute=*/ false)?;
1012 el.namespace.set(el_ns);
1013 }
1014
1015 // ── Attribute QName resolution + dup-by-expanded-name check ──
1016 // Skip the FxHashSet allocation entirely when no attribute is prefixed
1017 // (no namespace = no collision possible after expansion).
1018 if any_attr_prefixed {
1019 // Resolve each prefixed attribute's namespace now that
1020 // every xmlns declaration on this element is in scope.
1021 // `resolve_qname` takes the original prefixed QName
1022 // recorded at creation; the stored `name` may already
1023 // be the local part (c-abi).
1024 for &(attr, qname) in &prefixed_attrs {
1025 validate_qname(qname, "attribute")?;
1026 let ns = resolve_qname(qname, &ns_bindings, true)?;
1027 attr.namespace.set(ns);
1028 }
1029 // XML NS § 6.3: no two attributes may share an expanded
1030 // name (namespace-uri, local-name). The local part is
1031 // `name` after the colon — which equals `name` itself
1032 // once reduced (c-abi).
1033 let mut seen: FxHashSet<(&str, &str)> = FxHashSet::default();
1034 let mut attr_cur = el.first_attribute.get();
1035 while let Some(attr) = attr_cur {
1036 if attr.name() != "xmlns" && !attr.name().starts_with("xmlns:") {
1037 let ns_uri = attr.namespace.get().map(|n| n.href()).unwrap_or("");
1038 let local = attr.name().rfind(':')
1039 .map(|i| &attr.name()[i + 1..])
1040 .unwrap_or(attr.name());
1041 if !seen.insert((ns_uri, local)) {
1042 return Err(ns_err(if ns_uri.is_empty() {
1043 format!("duplicate attribute '{local}' after namespace expansion")
1044 } else {
1045 format!("duplicate attribute '{local}' in namespace '{ns_uri}' after namespace expansion")
1046 }));
1047 }
1048 }
1049 attr_cur = attr.next.get();
1050 }
1051 }
1052 // Stash the per-frame binding count in ns_frames is not enough — we
1053 // also need to know how many to subtract from active_user_bindings
1054 // on EndElement. Pack the count into the frame marker by using a
1055 // sentinel: separate Vec for the counts. (Simpler: piggyback on
1056 // ns_frames by recording `new_bindings_this_frame` in a parallel Vec.)
1057 // For now we track via a side Vec.
1058 ns_frame_count_stack.push(new_bindings_this_frame);
1059 }
1060
1061 // Attach to parent (if any) — first StartElement becomes the root.
1062 if let Some(&parent_ptr) = stack.last() {
1063 // SAFETY: parent_ptr points into `b`, still alive here.
1064 let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
1065 b.append_child(parent, el);
1066 } else {
1067 root = Some(erase(el));
1068 }
1069 stack.push(erase(el));
1070 }
1071 BytesEvent::EndElement(_) => {
1072 stack.pop().expect("EndElement without StartElement — XmlBytesReader invariant");
1073 if ns_aware {
1074 if let Some(frame_start) = ns_frames.pop() {
1075 ns_bindings.truncate(frame_start);
1076 }
1077 if let Some(count) = ns_frame_count_stack.pop() {
1078 active_user_bindings -= count;
1079 }
1080 }
1081 }
1082 BytesEvent::Text(t) => attach_leaf(b, &stack, b.new_text (alloc_cow_bytes_as_str(b, t.into_bytes())), root.is_some()),
1083 BytesEvent::CData(s) => {
1084 // `cdata_as_text` (libxml2 NOCDATA / lxml strip_cdata)
1085 // delivers CDATA content as a plain text node.
1086 let content = alloc_cow_bytes_as_str(b, s.into_bytes());
1087 let leaf = if opts.cdata_as_text { b.new_text(content) } else { b.new_cdata(content) };
1088 attach_leaf(b, &stack, leaf, root.is_some());
1089 }
1090 BytesEvent::Comment(s) => {
1091 // `remove_comments` (lxml NULLs the SAX comment callback):
1092 // skip building the node entirely.
1093 if !opts.remove_comments {
1094 attach_leaf(b, &stack, b.new_comment(alloc_cow_bytes_as_str(b, s.into_bytes())), root.is_some());
1095 }
1096 }
1097 BytesEvent::Pi(p) => {
1098 if !opts.remove_pis {
1099 let (t, c) = p.into_parts();
1100 let target = alloc_cow_bytes_as_str(b, t);
1101 // A PI with no data section serializes without the
1102 // trailing space; libxml2 marks that as NULL content.
1103 let content = if c.is_empty() { None } else { Some(alloc_cow_bytes_as_str(b, c)) };
1104 attach_leaf(b, &stack, b.new_pi(target, content), root.is_some());
1105 }
1106 }
1107 BytesEvent::EntityRef(e) => {
1108 // `resolve_entities: false` left an `&name;` literal in
1109 // the source; materialize it as a dedicated
1110 // `NodeKind::EntityRef` node so the tree round-trips
1111 // back to source and lxml's `tag == Entity` /
1112 // `text == "&name;"` semantics work.
1113 let name_bytes = e.name();
1114 // SAFETY: Scanner UTF-8 invariant — entity-name bytes
1115 // are valid UTF-8 (NameChar+).
1116 let name: &str = unsafe { std::str::from_utf8_unchecked(name_bytes) };
1117 let name = b.alloc_str(name);
1118 // Literal source form `&name;` for the serializer.
1119 let lit = format!("&{name};");
1120 let content = b.alloc_str(&lit);
1121 attach_leaf(b, &stack, b.new_entity_ref(name, content), root.is_some());
1122 }
1123 BytesEvent::Eof => break,
1124 }
1125 }
1126
1127 let root_ptr = root.ok_or_else(|| XmlError::new(
1128 ErrorDomain::Parser, ErrorLevel::Fatal,
1129 "document has no root element",
1130 ))?;
1131 // SAFETY: root_ptr was erased from a node allocated in `b`; `b` is still
1132 // alive at this line.
1133 let root_ref: &Node<'_> = unsafe { unerase(root_ptr) };
1134 b.set_root(root_ref);
1135
1136 // Plumb XML declaration fields from the reader's prolog state into the
1137 // Document. When the document had no `<?xml ... ?>` declaration the
1138 // builder's defaults ("1.0" / "UTF-8" / None) are kept.
1139 if let Some(decl) = reader.xml_decl() {
1140 b.set_version(decl.version.clone());
1141 if let Some(enc) = &decl.encoding {
1142 b.set_encoding(enc.clone());
1143 }
1144 b.set_standalone(decl.standalone);
1145 }
1146
1147 Ok(())
1148}
1149
1150// ── namespace helpers ───────────────────────────────────────────────────────
1151
1152#[inline] fn erase_ns(ns: &Namespace<'_>) -> *const () {
1153 ns as *const Namespace<'_> as *const ()
1154}
1155
1156/// # Safety
1157///
1158/// `p` must have been produced by `erase_ns` from a live `&Namespace`,
1159/// and the caller-chosen lifetime `'a` must not outlive that namespace's
1160/// arena.
1161#[inline] unsafe fn unerase_ns<'a>(p: *const ()) -> &'a Namespace<'a> {
1162 unsafe { &*(p as *const Namespace<'a>) }
1163}
1164
1165/// Find the innermost binding for `prefix` (None = default namespace).
1166/// Returns `None` if no binding is in scope.
1167fn lookup_ns<'a>(
1168 prefix: Option<&str>,
1169 bindings: &[(Option<&'a str>, *const ())],
1170) -> Option<&'a Namespace<'a>> {
1171 for &(p, ns_ptr) in bindings.iter().rev() {
1172 if p == prefix {
1173 // SAFETY: ns_ptr was minted by erase_ns from a Namespace allocated
1174 // in the builder's arena, which is still alive while this function
1175 // runs (called from inside drive() which owns the arena).
1176 return Some(unsafe { unerase_ns(ns_ptr) });
1177 }
1178 }
1179 None
1180}
1181
1182/// Resolve a QName against the namespace scope. Unprefixed elements use the
1183/// default namespace; unprefixed attributes never do (XML Namespaces § 6.2).
1184fn resolve_qname<'a>(
1185 qname: &'a str,
1186 bindings: &[(Option<&'a str>, *const ())],
1187 is_attribute: bool,
1188) -> Result<Option<&'a Namespace<'a>>> {
1189 if let Some(colon) = qname.find(':') {
1190 let prefix = &qname[..colon];
1191 match lookup_ns(Some(prefix), bindings) {
1192 Some(ns) => Ok(Some(ns)),
1193 None => Err(ns_err(format!("undeclared namespace prefix '{prefix}' in '{qname}'"))),
1194 }
1195 } else if is_attribute {
1196 Ok(None)
1197 } else {
1198 // Unprefixed element: default namespace if declared with non-empty URI.
1199 match lookup_ns(None, bindings) {
1200 Some(ns) if !ns.href().is_empty() => Ok(Some(ns)),
1201 _ => Ok(None),
1202 }
1203 }
1204}
1205
1206
1207/// Attach a freshly-allocated leaf node to the top-of-stack element.
1208/// When the stack is empty (prolog before the root or epilogue
1209/// after `</root>`) the leaf is recorded as a document-level
1210/// orphan instead of dropped; [`DocumentBuilder::build`] later
1211/// links it as a sibling of the root so consumers see comments /
1212/// PIs that appeared outside the document element.
1213///
1214/// `after_root` tells the builder whether this orphan goes in the
1215/// prolog (false: root not yet seen) or the epilogue (true: root
1216/// element has opened, regardless of whether it has also closed).
1217fn attach_leaf<'a>(
1218 b: &'a DocumentBuilder,
1219 stack: &[ErasedNodePtr],
1220 node: &'a Node<'a>,
1221 after_root: bool,
1222) {
1223 if let Some(&parent_ptr) = stack.last() {
1224 // SAFETY: stack invariant — pointer points into the live `b.bump`,
1225 // which is the same arena `node` lives in, so unifying lifetimes is sound.
1226 let parent: &'a Node<'a> = unsafe { unerase(parent_ptr) };
1227 b.append_child(parent, node);
1228 } else if after_root {
1229 b.attach_epilogue_orphan(node);
1230 } else {
1231 b.attach_prolog_orphan(node);
1232 }
1233}
1234
1235// Silence the unused-`ErasedAttrPtr` lint until we extend to namespace tables.
1236#[allow(dead_code)] type _AttrAlias = ErasedAttrPtr;
1237
1238// ── tests ───────────────────────────────────────────────────────────────────
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243 use sup_xml_tree::dom::NodeKind;
1244
1245 fn parse(xml: &str) -> Document {
1246 // Default `ParseOptions` has `namespace_aware: false` — most tests here
1247 // exercise the structural shape (children, attrs, kinds), so it doesn't
1248 // matter. Namespace-specific tests use `parse_ns` instead.
1249 parse_str(xml, &ParseOptions::default()).expect("parse")
1250 }
1251
1252 /// Namespace-aware parse helper for the ns-resolution tests.
1253 fn parse_ns(xml: &str) -> Document {
1254 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
1255 parse_str(xml, &opts).expect("parse")
1256 }
1257
1258 #[test]
1259 fn empty_element() {
1260 let doc = parse("<r/>");
1261 assert_eq!(doc.root().name(), "r");
1262 assert!(doc.root().children().next().is_none());
1263 }
1264
1265 // ── XML 1.0 § 3.3.3 attribute-value normalization for xmlns ──
1266
1267 /// `xmlns:b` declared as NMTOKEN: leading/trailing whitespace
1268 /// in the URI literal MUST be stripped before the URI binds.
1269 /// CDATA attributes get no such treatment. Without normalization
1270 /// `xmlns:a="urn:x"` and `xmlns:b=" urn:x "` would bind to two
1271 /// distinct URIs even though the spec treats them as one — the
1272 /// downstream "Unique Att Spec" check would then miss real
1273 /// namespace collisions like W3C rmt-ns10-012.
1274 #[test]
1275 fn xmlns_nmtoken_value_is_stripped_before_binding() {
1276 let xml = r#"<?xml version="1.0"?>
1277<!DOCTYPE foo [
1278<!ELEMENT foo ANY>
1279<!ATTLIST foo xmlns:a CDATA #IMPLIED
1280 xmlns:b NMTOKEN #IMPLIED>
1281]>
1282<foo xmlns:a="urn:x" xmlns:b=" urn:x "/>"#;
1283 let doc = parse_ns(xml);
1284 let root = doc.root();
1285 // Both prefixes should now bind to the same URI string.
1286 // Walk the namespace defs and check.
1287 let nsdefs: Vec<(Option<&str>, &str)> = {
1288 #[cfg(feature = "c-abi")]
1289 { root.ns_declarations().collect() }
1290 #[cfg(not(feature = "c-abi"))]
1291 {
1292 // Without c-abi we exposed xmlns decls as attributes
1293 // instead of nsDef. Pull from the attribute list.
1294 let mut out = Vec::new();
1295 let mut a = root.first_attribute.get();
1296 while let Some(attr) = a {
1297 let name = attr.name();
1298 if name == "xmlns" {
1299 out.push((None, attr.value()));
1300 } else if let Some(p) = name.strip_prefix("xmlns:") {
1301 out.push((Some(p), attr.value()));
1302 }
1303 a = attr.next.get();
1304 }
1305 out
1306 }
1307 };
1308 let a_uri = nsdefs.iter().find(|(p, _)| *p == Some("a")).map(|(_, u)| *u);
1309 let b_uri = nsdefs.iter().find(|(p, _)| *p == Some("b")).map(|(_, u)| *u);
1310 assert_eq!(a_uri, Some("urn:x"));
1311 assert_eq!(b_uri, Some("urn:x"),
1312 "NMTOKEN normalization should strip whitespace; got {b_uri:?}");
1313 }
1314
1315 /// CDATA-typed xmlns (the default when no ATTLIST covers it):
1316 /// whitespace inside the value is preserved. This is the spec
1317 /// behaviour — without an `<!ATTLIST>` redeclaring the type the
1318 /// raw value goes through. Guards against over-normalization.
1319 #[test]
1320 fn xmlns_cdata_value_is_not_stripped() {
1321 let xml = r#"<r xmlns:b=" urn:x "/>"#;
1322 let doc = parse_ns(xml);
1323 let root = doc.root();
1324 let b_uri = {
1325 #[cfg(feature = "c-abi")]
1326 { root.ns_declarations().find(|(p, _)| *p == Some("b")).map(|(_, u)| u) }
1327 #[cfg(not(feature = "c-abi"))]
1328 {
1329 let mut a = root.first_attribute.get();
1330 let mut found = None;
1331 while let Some(attr) = a {
1332 if attr.name() == "xmlns:b" { found = Some(attr.value()); break; }
1333 a = attr.next.get();
1334 }
1335 found
1336 }
1337 };
1338 assert_eq!(b_uri, Some(" urn:x "),
1339 "no ATTLIST → no non-CDATA normalization, URI must stay verbatim");
1340 }
1341
1342 // ── XML Namespaces 1.0 § 6.3 "Unique Att Spec" ──
1343
1344 /// Two prefixes binding to the SAME namespace URI on the same
1345 /// element make `a:attr` and `b:attr` resolve to the same
1346 /// expanded name `{URI}attr` — must be rejected. Catches the
1347 /// straightforward (same-element) collision.
1348 #[test]
1349 fn duplicate_expanded_attribute_name_rejected() {
1350 let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
1351 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1352 .expect_err("two prefixes binding the same URI with same local must be rejected");
1353 let msg = err.to_string().to_lowercase();
1354 assert!(msg.contains("duplicate") && (msg.contains("namespace") || msg.contains("attribute")),
1355 "expected duplicate-attribute message, got: {err}");
1356 }
1357
1358 /// The same collision but the xmlns declarations live on a
1359 /// parent element — collision still detected because the
1360 /// namespace resolver walks the full in-scope binding stack.
1361 /// Mirrors W3C rmt-ns10-012's structural shape (xmlns on
1362 /// outer, prefixed attrs on inner) without the DTD-driven
1363 /// normalization layer.
1364 #[test]
1365 fn duplicate_expanded_attribute_name_rejected_across_elements() {
1366 let xml = r#"<f xmlns:a="urn:x" xmlns:b="urn:x"><g a:attr="1" b:attr="2"/></f>"#;
1367 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1368 .expect_err("inner-element collision must be caught through inherited bindings");
1369 let msg = err.to_string().to_lowercase();
1370 assert!(msg.contains("duplicate"),
1371 "expected duplicate-attribute message, got: {err}");
1372 }
1373
1374 /// The combined case — DTD-aware normalization + namespace-
1375 /// aware uniqueness must both fire to catch this. This is
1376 /// exactly W3C rmt-ns10-012's input shape, distilled to the
1377 /// minimum needed to flip the test verdict. Drops the W3C
1378 /// suite's external machinery so the failure mode is testable
1379 /// from inside the core crate without the harness.
1380 #[test]
1381 fn nmtoken_normalization_unlocks_ns_uniqueness_collision() {
1382 let xml = r#"<?xml version="1.0"?>
1383<!DOCTYPE foo [
1384<!ELEMENT foo ANY>
1385<!ATTLIST foo xmlns:a CDATA #IMPLIED
1386 xmlns:b NMTOKEN #IMPLIED>
1387<!ELEMENT bar ANY>
1388<!ATTLIST bar a:attr CDATA #IMPLIED
1389 b:attr CDATA #IMPLIED>
1390]>
1391<foo xmlns:a="urn:x" xmlns:b=" urn:x ">
1392<bar a:attr="1" b:attr="2"/>
1393</foo>"#;
1394 let err = parse_str(xml, &ParseOptions { namespace_aware: true, ..ParseOptions::default() })
1395 .expect_err("rmt-ns10-012 minimum repro must be rejected once NMTOKEN normalization \
1396 strips the whitespace from xmlns:b's value");
1397 let msg = err.to_string().to_lowercase();
1398 assert!(msg.contains("duplicate"),
1399 "expected duplicate-after-expansion error, got: {err}");
1400 }
1401
1402 /// XML 1.0 § 3.3.3 attribute-value normalization for a regular
1403 /// (non-xmlns) NMTOKEN attribute: leading/trailing whitespace
1404 /// stripped, internal runs collapsed. Confirms the normalization
1405 /// helper applies to ALL non-CDATA attributes, not just xmlns:*.
1406 #[test]
1407 fn nmtoken_attribute_value_is_normalized() {
1408 let xml = r#"<?xml version="1.0"?>
1409<!DOCTYPE r [
1410<!ELEMENT r EMPTY>
1411<!ATTLIST r kind NMTOKEN #IMPLIED>
1412]>
1413<r kind=" alpha "/>"#;
1414 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1415 let kind = doc.root().attributes()
1416 .find(|a| a.name() == "kind")
1417 .map(|a| a.value());
1418 assert_eq!(kind, Some("alpha"),
1419 "NMTOKEN value should be stripped; got {kind:?}");
1420 }
1421
1422 /// Same rule for ID-typed attributes — internal whitespace
1423 /// collapses too. Without normalization, two `id` values that
1424 /// differ only by whitespace would compare unequal under
1425 /// `getElementById` etc.
1426 #[test]
1427 fn id_attribute_value_is_normalized_collapsed() {
1428 let xml = r#"<?xml version="1.0"?>
1429<!DOCTYPE r [
1430<!ELEMENT r EMPTY>
1431<!ATTLIST r tag ID #IMPLIED>
1432]>
1433<r tag=" a b c "/>"#;
1434 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1435 let tag = doc.root().attributes()
1436 .find(|a| a.name() == "tag")
1437 .map(|a| a.value());
1438 assert_eq!(tag, Some("a b c"),
1439 "ID value should strip + collapse; got {tag:?}");
1440 }
1441
1442 /// CDATA-typed attribute (the default when no ATTLIST covers it)
1443 /// preserves whitespace verbatim — guards against
1444 /// over-normalization of CDATA-shaped values.
1445 #[test]
1446 fn cdata_attribute_value_is_not_stripped() {
1447 // No ATTLIST → defaults to CDATA → no non-CDATA stripping.
1448 let xml = r#"<r tag=" a b "/>"#;
1449 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1450 let tag = doc.root().attributes()
1451 .find(|a| a.name() == "tag")
1452 .map(|a| a.value());
1453 assert_eq!(tag, Some(" a b "),
1454 "CDATA-defaulted value must stay verbatim; got {tag:?}");
1455 }
1456
1457 /// Enumeration-typed attribute: stripped before matching the
1458 /// enum. Without this, `<r kind=" yes ">` would silently fail
1459 /// even though the declared enum is `(yes|no)` — and we'd quietly
1460 /// accept the un-matched value instead of validating against it.
1461 #[test]
1462 fn enumeration_attribute_value_is_normalized() {
1463 let xml = r#"<?xml version="1.0"?>
1464<!DOCTYPE r [
1465<!ELEMENT r EMPTY>
1466<!ATTLIST r kind (yes|no) #IMPLIED>
1467]>
1468<r kind=" yes "/>"#;
1469 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1470 let kind = doc.root().attributes()
1471 .find(|a| a.name() == "kind")
1472 .map(|a| a.value());
1473 assert_eq!(kind, Some("yes"));
1474 }
1475
1476 // ── resolve_entities = false (EntityRef node kind) ──
1477
1478 /// Default: entity references expand inline into Text. Confirms
1479 /// the baseline behaviour the new flag opts out of.
1480 #[test]
1481 fn resolve_entities_default_expands_inline() {
1482 let xml = r#"<?xml version="1.0"?>
1483<!DOCTYPE r [<!ENTITY hi "hello">]>
1484<r>&hi;</r>"#;
1485 let doc = parse_str(xml, &ParseOptions::default()).expect("parse");
1486 let root = doc.root();
1487 // Should have a single Text child "hello", no EntityRef.
1488 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1489 assert_eq!(kinds, vec![NodeKind::Text]);
1490 let text = root.children().next().unwrap().content();
1491 assert_eq!(text, "hello");
1492 }
1493
1494 /// `resolve_entities = false` preserves user-defined references
1495 /// as `NodeKind::EntityRef` nodes whose `name` is the entity
1496 /// name and whose `content` round-trips to `&name;` source.
1497 #[test]
1498 fn resolve_entities_false_emits_entity_ref_node() {
1499 let xml = r#"<?xml version="1.0"?>
1500<!DOCTYPE r [<!ENTITY hi "hello">]>
1501<r>before &hi; after</r>"#;
1502 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1503 let doc = parse_str(xml, &opts).expect("parse");
1504 let root = doc.root();
1505 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1506 assert_eq!(
1507 kinds,
1508 vec![NodeKind::Text, NodeKind::EntityRef, NodeKind::Text],
1509 "expected Text-EntityRef-Text triple, got {kinds:?}"
1510 );
1511 // The middle child carries the entity name + literal form.
1512 let ref_node = root.children().nth(1).unwrap();
1513 assert_eq!(ref_node.name(), "hi");
1514 assert_eq!(ref_node.content(), "&hi;");
1515 }
1516
1517 /// Predefined entities always expand regardless of the flag —
1518 /// they're part of the character data production, not the
1519 /// entity-reference machinery.
1520 #[test]
1521 fn resolve_entities_false_still_expands_predefined() {
1522 let xml = r#"<r>a & b < c</r>"#;
1523 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1524 let doc = parse_str(xml, &opts).expect("parse");
1525 let root = doc.root();
1526 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1527 assert_eq!(kinds, vec![NodeKind::Text],
1528 "predefined entities must expand inline, got {kinds:?}");
1529 assert_eq!(root.children().next().unwrap().content(), "a & b < c");
1530 }
1531
1532 /// Numeric character references always expand inline too.
1533 #[test]
1534 fn resolve_entities_false_still_expands_numeric() {
1535 let xml = r#"<r>before A after</r>"#;
1536 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1537 let doc = parse_str(xml, &opts).expect("parse");
1538 let root = doc.root();
1539 let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
1540 assert_eq!(kinds, vec![NodeKind::Text]);
1541 assert_eq!(root.children().next().unwrap().content(), "before A after");
1542 }
1543
1544 /// Round-trip: serialize a tree with EntityRef nodes back to
1545 /// source. The literal `&name;` form is preserved.
1546 #[test]
1547 fn resolve_entities_false_round_trips_through_serializer() {
1548 let xml = r#"<r>x &hi; y</r>"#;
1549 let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
1550 let doc = parse_str(xml, &opts).expect("parse");
1551 let out = crate::serialize_to_string(&doc);
1552 assert!(out.contains("&hi;"),
1553 "EntityRef should serialize as `&hi;`, got: {out}");
1554 }
1555
1556 /// Edition guard: without `namespace_aware`, lexical names rule.
1557 /// `a:attr` and `b:attr` are different names → accepted. Same
1558 /// input as the previous tests, but the namespace pass doesn't
1559 /// run. Confirms the new check is properly gated.
1560 #[test]
1561 fn duplicate_expanded_name_accepted_when_namespace_blind() {
1562 let xml = r#"<r xmlns:a="urn:x" xmlns:b="urn:x" a:attr="1" b:attr="2"/>"#;
1563 // namespace_aware = false (the default).
1564 parse_str(xml, &ParseOptions::default())
1565 .expect("namespace-blind parse must accept lexically-distinct attribute names");
1566 }
1567
1568 #[test]
1569 fn line_numbers_basic() {
1570 // line 1: <r>
1571 // line 2: <a/>
1572 // line 3: <b/>
1573 // line 4: </r>
1574 let doc = parse("<r>\n <a/>\n <b/>\n</r>");
1575 let root = doc.root();
1576 let a = root.children().find(|n| n.is_element()).unwrap();
1577 let b = a.next_sibling.get().and_then(|n|
1578 if n.is_element() { Some(n) } else { n.next_sibling.get() }
1579 ).unwrap();
1580
1581 assert_eq!(root.line, 1, "root <r> on line 1");
1582 assert_eq!(a.line, 2, "<a/> on line 2");
1583 assert_eq!(b.line, 3, "<b/> on line 3");
1584 }
1585
1586 /// Regression guard: a previous implementation called
1587 /// `scanner::compute_line_col` once per StartElement, which rescans
1588 /// `src[0..name_offset]` from byte 0 each call — O(N × file_size).
1589 /// On docs with many elements this slowed the parser by 10×–100×.
1590 /// The current implementation maintains an incremental cursor in
1591 /// `drive()`; this test exercises it across many lines to make sure
1592 /// the cursor stays in sync.
1593 #[test]
1594 fn line_numbers_many_elements() {
1595 // 300 elements, one per line. Each <e i="N"/> on line N+1
1596 // (the <root> opener is line 1).
1597 let mut src = String::from("<root>\n");
1598 let n = 300u32;
1599 for i in 0..n {
1600 src.push_str(&format!(" <e i=\"{i}\"/>\n"));
1601 }
1602 src.push_str("</root>\n");
1603
1604 let doc = parse(&src);
1605 assert_eq!(doc.root().line, 1);
1606
1607 let mut expected: u32 = 2;
1608 let mut walked = 0u32;
1609 for child in doc.root().children().filter(|n| n.is_element()) {
1610 // line is u16 in c-abi mode, u32 in lean — both fit < 65535 here.
1611 assert_eq!(child.line as u32, expected,
1612 "child #{walked}: expected line {expected}, got {}", child.line);
1613 walked += 1;
1614 expected += 1;
1615 }
1616 assert_eq!(walked, n);
1617 }
1618
1619 #[test]
1620 fn nested_elements() {
1621 let doc = parse("<a><b><c/></b></a>");
1622 let a = doc.root();
1623 assert_eq!(a.name(), "a");
1624 let b = a.children().next().unwrap();
1625 assert_eq!(b.name(), "b");
1626 let c = b.children().next().unwrap();
1627 assert_eq!(c.name(), "c");
1628 // parent pointers
1629 assert!(std::ptr::eq(c.parent.get().unwrap(), b));
1630 assert!(std::ptr::eq(b.parent.get().unwrap(), a));
1631 }
1632
1633 #[test]
1634 fn attributes_in_order() {
1635 let doc = parse(r#"<el id="1" class="x" data-y="42"/>"#);
1636 let pairs: Vec<(&str, &str)> = doc.root().attributes()
1637 .map(|a| (a.name(), a.value()))
1638 .collect();
1639 assert_eq!(pairs, vec![("id", "1"), ("class", "x"), ("data-y", "42")]);
1640 }
1641
1642 #[test]
1643 fn mixed_content() {
1644 let doc = parse("<r>before<!-- c --><b>x</b><![CDATA[<raw>]]>after</r>");
1645 let kinds: Vec<NodeKind> = doc.root().children().map(|c| c.kind).collect();
1646 assert_eq!(kinds, vec![
1647 NodeKind::Text, NodeKind::Comment, NodeKind::Element,
1648 NodeKind::CData, NodeKind::Text,
1649 ]);
1650 }
1651
1652 #[test]
1653 fn pi_inside_element() {
1654 let doc = parse(r#"<r><?xml-stylesheet href="s.xsl"?></r>"#);
1655 let pi = doc.root().children().next().unwrap();
1656 assert_eq!(pi.kind, NodeKind::Pi);
1657 assert_eq!(pi.name(), "xml-stylesheet");
1658 assert_eq!(pi.content(), r#"href="s.xsl""#);
1659 }
1660
1661 #[test]
1662 fn entity_in_text_is_expanded() {
1663 let doc = parse("<r>a&b<c</r>");
1664 let t = doc.root().children().next().unwrap();
1665 assert_eq!(t.kind, NodeKind::Text);
1666 assert_eq!(t.content(), "a&b<c");
1667 }
1668
1669 #[test]
1670 fn entity_in_attr_is_expanded() {
1671 let doc = parse(r#"<r v="a&b"/>"#);
1672 let v = doc.root().attributes().next().unwrap();
1673 assert_eq!(v.value(), "a&b");
1674 }
1675
1676 #[test]
1677 fn deeply_nested_doc() {
1678 let mut xml = String::new();
1679 for _ in 0..50 { xml.push_str("<n>"); }
1680 xml.push_str("hello");
1681 for _ in 0..50 { xml.push_str("</n>"); }
1682 let doc = parse(&xml);
1683 let mut cur = doc.root();
1684 let mut depth = 1;
1685 while let Some(c) = cur.children().next() {
1686 if c.kind == NodeKind::Element { cur = c; depth += 1; } else { break; }
1687 }
1688 assert_eq!(depth, 50);
1689 assert_eq!(cur.children().next().unwrap().content(), "hello");
1690 }
1691
1692 #[test]
1693 fn root_lifetime_keeps_tree_alive() {
1694 let doc = parse("<r><a><b>x</b></a></r>");
1695 let a = doc.root().children().next().unwrap();
1696 let b = a.children().next().unwrap();
1697 assert_eq!(b.text_content(), Some("x"));
1698 }
1699
1700 #[test]
1701 fn errors_propagate_from_reader() {
1702 let err = parse_str("<r><a></b></r>", &ParseOptions::default());
1703 assert!(err.is_err());
1704 }
1705
1706 #[test]
1707 fn missing_root_returns_error() {
1708 // An empty document fails earlier (XML decl alone) — use whitespace-only.
1709 let r = parse_str(" ", &ParseOptions::default());
1710 assert!(r.is_err());
1711 }
1712
1713 // ── namespace tests ────────────────────────────────────────────────
1714
1715 #[test]
1716 fn no_namespaces_unchanged() {
1717 let doc = parse_ns("<root><child/></root>");
1718 assert!(doc.root().namespace.get().is_none());
1719 }
1720
1721 #[test]
1722 fn default_namespace_applied_to_element() {
1723 let doc = parse_ns(r#"<root xmlns="http://example.com/"/>"#);
1724 let ns = doc.root().namespace.get().unwrap();
1725 assert!(ns.prefix.is_none());
1726 assert_eq!(ns.href(), "http://example.com/");
1727 }
1728
1729 #[test]
1730 fn default_namespace_not_applied_to_attr() {
1731 let doc = parse_ns(r#"<root xmlns="http://example.com/" id="1"/>"#);
1732 let id_attr = doc.root().attributes().find(|a| a.name() == "id").unwrap();
1733 assert!(id_attr.namespace.get().is_none(), "default ns must not apply to unprefixed attrs");
1734 }
1735
1736 #[test]
1737 fn prefixed_element_resolved() {
1738 let doc = parse_ns(r#"<dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">X</dc:title>"#);
1739 let ns = doc.root().namespace.get().unwrap();
1740 assert_eq!(ns.prefix(), Some("dc"));
1741 assert_eq!(ns.href(), "http://purl.org/dc/elements/1.1/");
1742 }
1743
1744 #[test]
1745 fn prefixed_attribute_resolved() {
1746 let doc = parse_ns(
1747 r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>"#
1748 );
1749 // Match by (local-name, prefix): `name()` is the full QName on
1750 // the lean build but the local part under c-abi.
1751 let nil = doc.root().attributes()
1752 .find(|a| a.local_name() == "nil"
1753 && a.namespace.get().and_then(|n| n.prefix()) == Some("xsi"))
1754 .unwrap();
1755 assert_eq!(nil.namespace.get().unwrap().href(),
1756 "http://www.w3.org/2001/XMLSchema-instance");
1757 }
1758
1759 #[test]
1760 fn xml_prefix_builtin() {
1761 let doc = parse_ns(r#"<root xml:lang="en"/>"#);
1762 let lang = doc.root().attributes()
1763 .find(|a| a.local_name() == "lang"
1764 && a.namespace.get().and_then(|n| n.prefix()) == Some("xml"))
1765 .unwrap();
1766 assert_eq!(lang.namespace.get().unwrap().href(),
1767 "http://www.w3.org/XML/1998/namespace");
1768 }
1769
1770 #[test]
1771 fn nested_prefix_scope_inherits_outer() {
1772 let doc = parse_ns(r#"
1773 <root xmlns:a="http://a.com/">
1774 <a:child xmlns:b="http://b.com/">
1775 <b:leaf/>
1776 </a:child>
1777 </root>
1778 "#);
1779 let root = doc.root();
1780 assert!(root.namespace.get().is_none());
1781 // c-abi mode stores `name` as the local part only (libxml2
1782 // convention); the lean build keeps the full QName.
1783 #[cfg(feature = "c-abi")]
1784 let child_local = "child";
1785 #[cfg(not(feature = "c-abi"))]
1786 let child_local = "a:child";
1787 let child = root.children().find(|c| c.is_element() && c.name() == child_local).unwrap();
1788 assert_eq!(child.namespace.get().unwrap().href(), "http://a.com/");
1789 let leaf = child.children().find(|c| c.is_element()).unwrap();
1790 assert_eq!(leaf.namespace.get().unwrap().href(), "http://b.com/");
1791 }
1792
1793 #[test]
1794 fn undeclared_prefix_is_error() {
1795 let r = parse_str("<dc:title>X</dc:title>", &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
1796 let msg = r.unwrap_err().to_string();
1797 assert!(msg.contains("undeclared") || msg.contains("prefix"), "{msg}");
1798 }
1799
1800 #[test]
1801 fn default_namespace_override_in_child() {
1802 let doc = parse_ns(r#"
1803 <root xmlns="http://outer.com/">
1804 <inner xmlns="http://inner.com/"/>
1805 </root>
1806 "#);
1807 assert_eq!(doc.root().namespace.get().unwrap().href(), "http://outer.com/");
1808 let inner = doc.root().children().find(|c| c.is_element()).unwrap();
1809 assert_eq!(inner.namespace.get().unwrap().href(), "http://inner.com/");
1810 }
1811
1812 #[test]
1813 fn undeclare_default_namespace() {
1814 let doc = parse_ns(r#"
1815 <root xmlns="http://example.com/">
1816 <child xmlns=""/>
1817 </root>
1818 "#);
1819 assert!(doc.root().namespace.get().is_some());
1820 let child = doc.root().children().find(|c| c.is_element()).unwrap();
1821 assert!(child.namespace.get().is_none(), "xmlns='' should clear the default namespace");
1822 }
1823
1824 #[test]
1825 fn xmlns_prefix_cannot_be_declared() {
1826 let r = parse_str(
1827 r#"<r xmlns:xmlns="http://x.com/"/>"#,
1828 &ParseOptions { namespace_aware: true, ..ParseOptions::default() },
1829 );
1830 assert!(r.is_err());
1831 }
1832
1833 #[test]
1834 fn duplicate_attribute_after_ns_expansion() {
1835 // Two prefixed attrs that expand to the same (ns, local) pair.
1836 let r = parse_str(
1837 r#"<r xmlns:a="http://x.com/" xmlns:b="http://x.com/" a:id="1" b:id="2"/>"#,
1838 &ParseOptions { namespace_aware: true, ..ParseOptions::default() },
1839 );
1840 assert!(r.is_err(), "duplicate expanded attribute name must be rejected");
1841 }
1842
1843 #[test]
1844 fn deep_nesting_pops_scope_on_close() {
1845 // After nesting popped back to root level, an inner prefix becomes undeclared.
1846 let r = parse_str(r#"
1847 <root>
1848 <a xmlns:p="http://x.com/"><p:leaf/></a>
1849 <p:bad/>
1850 </root>
1851 "#, &ParseOptions { namespace_aware: true, ..ParseOptions::default() });
1852 assert!(r.is_err());
1853 }
1854
1855 #[test]
1856 fn xml_decl_fields_are_captured() {
1857 let doc = parse(r#"<?xml version="1.1" encoding="ISO-8859-1" standalone="yes"?><r/>"#);
1858 assert_eq!(doc.version, "1.1");
1859 assert_eq!(doc.encoding, "ISO-8859-1");
1860 assert_eq!(doc.standalone, Some(true));
1861 }
1862
1863 #[test]
1864 fn xml_decl_defaults_when_absent() {
1865 // No `<?xml … ?>` declaration → encoding stays empty
1866 // (matches libxml2's NULL doc->encoding) so serializers
1867 // omit the encoding attribute on output.
1868 let doc = parse("<r/>");
1869 assert_eq!(doc.version, "1.0");
1870 assert_eq!(doc.encoding, "");
1871 assert_eq!(doc.standalone, None);
1872 }
1873
1874 #[test]
1875 fn xml_decl_partial_keeps_encoding_default() {
1876 // Only version present — encoding stays empty (no
1877 // declaration to copy), standalone absent.
1878 let doc = parse(r#"<?xml version="1.0"?><r/>"#);
1879 assert_eq!(doc.version, "1.0");
1880 assert_eq!(doc.encoding, "");
1881 assert_eq!(doc.standalone, None);
1882 }
1883
1884 #[test]
1885 fn xml_decl_standalone_no_is_captured() {
1886 // Note: standalone without encoding is rejected by both legacy
1887 // and arena parsers (pre-existing behaviour) — include encoding.
1888 let doc = parse(r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><r/>"#);
1889 assert_eq!(doc.standalone, Some(false));
1890 }
1891
1892 #[test]
1893 fn many_siblings_preserve_order() {
1894 let mut xml = String::from("<r>");
1895 for i in 0..100 {
1896 xml.push_str(&format!("<i>{i}</i>"));
1897 }
1898 xml.push_str("</r>");
1899 let doc = parse(&xml);
1900 let texts: Vec<&str> = doc.root().children()
1901 .filter(|c| c.kind == NodeKind::Element)
1902 .map(|c| c.text_content().unwrap_or(""))
1903 .collect();
1904 assert_eq!(texts.len(), 100);
1905 assert_eq!(texts[0], "0");
1906 assert_eq!(texts[99], "99");
1907 }
1908
1909 // ── parse_bytes_in_place — entry point + call-time gates ──────
1910
1911 #[test]
1912 fn in_place_parses_basic_document() {
1913 let buf = b"<root><child id=\"1\">hello</child></root>".to_vec();
1914 let doc = parse_bytes_in_place(buf, &ParseOptions::default())
1915 .expect("in-place parse should succeed on well-formed input");
1916 assert_eq!(doc.root().name(), "root");
1917 let child = doc.root().children().next().expect("child element");
1918 assert_eq!(child.name(), "child");
1919 assert_eq!(child.attributes().next().unwrap().value(), "1");
1920 }
1921
1922 #[test]
1923 fn in_place_rejects_recovery_mode_at_call_time() {
1924 let buf = b"<r/>".to_vec();
1925 let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
1926 let err = parse_bytes_in_place(buf, &opts)
1927 .expect_err("recovery_mode=true must be rejected up front");
1928 assert!(
1929 err.message.contains("recovery_mode"),
1930 "error should mention recovery_mode, got: {}", err.message,
1931 );
1932 }
1933
1934 #[test]
1935 fn in_place_transcodes_non_utf8_when_auto_transcode_on() {
1936 // ISO-8859-1 with `<?xml encoding="ISO-8859-1"?>` and one non-ASCII
1937 // byte (0xE9 = 'é'). Transcoded to UTF-8 upfront, then parsed.
1938 let buf: Vec<u8> =
1939 b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xe9</r>".to_vec();
1940 let opts = ParseOptions { auto_transcode: true, ..ParseOptions::default() };
1941 let doc = parse_bytes_in_place(buf, &opts).expect("transcoded parse should succeed");
1942 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
1943 assert_eq!(text, "café");
1944 }
1945
1946 #[test]
1947 fn in_place_rejects_invalid_utf8_when_auto_transcode_off() {
1948 let buf: Vec<u8> = b"<r>\xff</r>".to_vec();
1949 let opts = ParseOptions { auto_transcode: false, ..ParseOptions::default() };
1950 let err = parse_bytes_in_place(buf, &opts).expect_err("invalid UTF-8 must be rejected");
1951 assert_eq!(err.domain, crate::error::ErrorDomain::Encoding);
1952 }
1953
1954 // ── simdutf8 ⇄ std::str::from_utf8 equivalence ───────────────
1955 //
1956 // The input-validation gates (`parse_bytes_in_place`,
1957 // `transcode_and_validate`, `XmlBytesReader::from_bytes`) use
1958 // `simdutf8::compat::from_utf8` as a drop-in for `std::str::from_utf8`.
1959 // The whole correctness claim is that it is *behaviorally identical*:
1960 // same accept/reject verdict, and on rejection the same `valid_up_to()`
1961 // and `error_len()` — the parser pins error line/col to `valid_up_to()`,
1962 // so any divergence would silently shift reported error positions.
1963 // These tests pin that equivalence against `std` as the oracle.
1964
1965 /// Reduce a validation outcome to a comparable shape: `Ok(())` when
1966 /// valid, or the error's `(valid_up_to, error_len)` when not.
1967 fn std_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
1968 std::str::from_utf8(b)
1969 .map(|_| ())
1970 .map_err(|e| (e.valid_up_to(), e.error_len()))
1971 }
1972
1973 fn simd_verdict(b: &[u8]) -> std::result::Result<(), (usize, Option<usize>)> {
1974 simdutf8::compat::from_utf8(b)
1975 .map(|_| ())
1976 .map_err(|e| (e.valid_up_to(), e.error_len()))
1977 }
1978
1979 #[test]
1980 fn simdutf8_matches_std_at_chunk_boundaries() {
1981 // SIMD validators process vector-width chunks (16/32/64 bytes) then a
1982 // scalar tail; bugs hide where a malformed byte straddles that seam.
1983 // Sweep a lone 0xFF (never valid UTF-8) across every offset of buffers
1984 // sized around the common vector widths, plus the clean buffer.
1985 for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129] {
1986 let base = vec![b'a'; len];
1987 assert_eq!(std_verdict(&base), simd_verdict(&base), "clean buffer len {len}");
1988 for pos in 0..len {
1989 let mut bad = base.clone();
1990 bad[pos] = 0xFF;
1991 assert_eq!(
1992 std_verdict(&bad), simd_verdict(&bad),
1993 "diverged: lone 0xFF at offset {pos} in len {len}",
1994 );
1995 }
1996 }
1997 }
1998
1999 #[test]
2000 fn simdutf8_matches_std_on_adversarial_utf8() {
2001 // The classic failure modes for hand-rolled UTF-8 validators: truncated
2002 // multibyte sequences, overlong encodings, surrogate code points, lone
2003 // continuation bytes, and valid→invalid transitions mid-stream.
2004 let cases: &[&[u8]] = &[
2005 b"",
2006 b"hello",
2007 &[0xC3, 0xA9], // é — valid 2-byte
2008 &[0xC3], // truncated 2-byte lead
2009 &[0xE2, 0x82, 0xAC], // € — valid 3-byte
2010 &[0xE2, 0x82], // truncated 3-byte
2011 &[0xF0, 0x9F, 0x98, 0x80], // 😀 — valid 4-byte
2012 &[0xF0, 0x9F, 0x98], // truncated 4-byte
2013 &[0x80], // lone continuation
2014 &[0xBF], // lone continuation
2015 &[0xC0, 0x80], // overlong NUL
2016 &[0xE0, 0x80, 0x80], // overlong 3-byte
2017 &[0xF0, 0x80, 0x80, 0x80], // overlong 4-byte
2018 &[0xED, 0xA0, 0x80], // lone high surrogate U+D800
2019 &[0xED, 0xBF, 0xBF], // lone low surrogate U+DFFF
2020 &[0xF4, 0x90, 0x80, 0x80], // above U+10FFFF
2021 &[0xFF],
2022 &[0xFE],
2023 b"caf\xe9", // Latin-1 é — invalid as UTF-8
2024 &[b'o', b'k', 0xC3, 0xA9, 0xFF, b'x'], // valid run then invalid byte
2025 ];
2026 for c in cases {
2027 assert_eq!(std_verdict(c), simd_verdict(c), "diverged on {c:02x?}");
2028 }
2029 }
2030
2031 #[test]
2032 fn simdutf8_matches_std_on_random_bytes() {
2033 // Deterministic xorshift corpus — no rng/clock dependency, so failures
2034 // reproduce exactly. Bias one byte in four into the 0x80..=0xFF range
2035 // so lead/continuation logic gets exercised, not just ASCII runs.
2036 let mut state = 0x9E3779B97F4A7C15u64;
2037 let mut next = move || {
2038 state ^= state << 13;
2039 state ^= state >> 7;
2040 state ^= state << 17;
2041 state
2042 };
2043 for _ in 0..20_000 {
2044 let len = (next() % 70) as usize;
2045 let mut buf = Vec::with_capacity(len);
2046 for _ in 0..len {
2047 let r = next();
2048 let byte = if r & 3 == 0 {
2049 (r >> 8) as u8 | 0x80
2050 } else {
2051 (r >> 8) as u8 & 0x7F
2052 };
2053 buf.push(byte);
2054 }
2055 assert_eq!(std_verdict(&buf), simd_verdict(&buf), "diverged on {buf:02x?}");
2056 }
2057 }
2058
2059 #[test]
2060 fn in_place_decodes_builtin_entities() {
2061 // Slow-path exercise: text content with `&` triggers entity
2062 // expansion in the reader. In-place mode mutates the source
2063 // buffer (overwriting `&` with `&`) and emits the text as
2064 // Cow::Borrowed pointing into the now-mutated buffer. The arena
2065 // parser then takes the alloc_str_borrow zero-copy path.
2066 let buf = b"<r>tom & jerry</r>".to_vec();
2067 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2068 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2069 assert_eq!(text, "tom & jerry");
2070 }
2071
2072 #[test]
2073 fn in_place_decodes_multiple_builtin_entities() {
2074 let buf = b"<r><hello> & "hi"</r>".to_vec();
2075 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2076 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2077 assert_eq!(text, r#"<hello> & "hi""#);
2078 }
2079
2080 #[test]
2081 fn in_place_text_without_entities_works() {
2082 // Fast path (Cow::Borrowed straight from reader, no slow path).
2083 let buf = b"<r>plain text with no entities</r>".to_vec();
2084 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2085 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2086 assert_eq!(text, "plain text with no entities");
2087 }
2088
2089 #[test]
2090 fn in_place_accepts_user_entity_that_shrinks() {
2091 // `&x;` (4 source bytes) expands to "AB" (2 bytes) — fits.
2092 let buf = br#"<!DOCTYPE r [<!ENTITY x "AB">]><r>&x;</r>"#.to_vec();
2093 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2094 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2095 assert_eq!(text, "AB");
2096 }
2097
2098 #[test]
2099 fn in_place_rejects_user_entity_that_grows() {
2100 // `&hi;` (5 source bytes) tries to expand to "Hello, World!" (13 bytes).
2101 // Doesn't fit → error at use site with a clear message.
2102 let buf = br#"<!DOCTYPE r [<!ENTITY hi "Hello, World!">]><r>&hi;</r>"#.to_vec();
2103 let err = parse_bytes_in_place(buf, &ParseOptions::default()).expect_err(
2104 "expansion bigger than reference must be rejected in in-place mode",
2105 );
2106 assert!(
2107 err.message.contains("exceeds source span")
2108 || err.message.contains("expansion"),
2109 "error should explain the expansion mismatch, got: {}", err.message,
2110 );
2111 }
2112
2113 #[test]
2114 fn in_place_numeric_char_refs_work() {
2115 // `É` (6 source bytes) → `É` (2 UTF-8 bytes) — fits.
2116 // `𝐀` (9 source bytes) → `𝐀` (4 UTF-8 bytes) — fits.
2117 let buf = "<r>cafÉ and 𝐀</r>".as_bytes().to_vec();
2118 let doc = parse_bytes_in_place(buf, &ParseOptions::default()).expect("parse");
2119 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2120 assert_eq!(text, "cafÉ and 𝐀");
2121 }
2122
2123 #[test]
2124 fn in_place_rejects_recursive_entity() {
2125 // Direct recursion — same well-formedness error as parse_bytes
2126 // (XML 1.0 § 4.1). The expansion stack catches it before any
2127 // in-place mutation happens.
2128 let buf = br#"<!DOCTYPE r [<!ENTITY a "&a;">]><r>&a;</r>"#.to_vec();
2129 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2130 .expect_err("recursive entity must be rejected");
2131 let msg = err.message.to_lowercase();
2132 assert!(
2133 msg.contains("recurs") || msg.contains("cycle"),
2134 "error should mention recursion/cycle, got: {}", err.message,
2135 );
2136 }
2137
2138 // ── parse_bytes_in_place — flag honoring ────────────────────────────────
2139 //
2140 // These tests pin down a behavioral contract: `parse_bytes_in_place`
2141 // honors every flag on the caller's `ParseOptions` as-is. It does
2142 // NOT silently flip the `skip_*` validation flags on (that override
2143 // used to exist; was removed deliberately so callers control the
2144 // validation / speed tradeoff). Each test pair below verifies one
2145 // flag:
2146 // - default `ParseOptions` (flag is `false`) → input rejected
2147 // - the same input with the flag set `true` → input accepted
2148 // If either half of a pair flips, something has reintroduced the
2149 // override or weakened the validator.
2150
2151 /// XML 1.0 § 2.3 [4]: names start with a letter, `_`, or `:` (or
2152 /// non-ASCII NameStartChar) — never a digit. `<1foo>` is invalid.
2153 #[test]
2154 fn in_place_default_opts_reject_bad_name_start() {
2155 let buf = b"<1foo/>".to_vec();
2156 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2157 .expect_err("default opts must reject name starting with digit");
2158 assert!(
2159 err.message.contains("name-start") || err.message.contains("name start"),
2160 "error should mention name-start, got: {}", err.message,
2161 );
2162 // libxml2-compatible code: XML_ERR_NAME_REQUIRED = 68.
2163 // Validates the Slice 5a contract that consumer-checked codes
2164 // round-trip via `err.code as i32`.
2165 assert_eq!(err.code, crate::error::ErrorCode::NameRequired);
2166 assert_eq!(err.code as i32, 68);
2167 }
2168
2169 #[test]
2170 fn in_place_skip_name_validation_accepts_bad_name_start() {
2171 // Same malformed name as above; with skip_name_validation the
2172 // name is accepted as-is and parsing reaches Eof cleanly.
2173 let buf = b"<1foo/>".to_vec();
2174 let opts = ParseOptions { skip_name_validation: true, ..ParseOptions::default() };
2175 let doc = parse_bytes_in_place(buf, &opts)
2176 .expect("skip_name_validation=true must accept malformed name");
2177 assert_eq!(doc.root().name(), "1foo");
2178 }
2179
2180 /// XML 1.0 § 3.1 [STag] WFC "Unique Att Spec": no element may have
2181 /// two attributes with the same name. `<r a="1" a="2"/>` is
2182 /// rejected when `skip_attr_validation` is false (the default).
2183 #[test]
2184 fn in_place_default_opts_reject_duplicate_attribute() {
2185 let buf = br#"<r a="1" a="2"/>"#.to_vec();
2186 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2187 .expect_err("default opts must reject duplicate attribute");
2188 assert!(
2189 err.message.to_lowercase().contains("duplicate") || err.message.contains("Unique Att Spec"),
2190 "error should mention duplicate-attribute, got: {}", err.message,
2191 );
2192 }
2193
2194 #[test]
2195 fn in_place_skip_attr_validation_accepts_duplicate_attribute() {
2196 let buf = br#"<r a="1" a="2"/>"#.to_vec();
2197 let opts = ParseOptions { skip_attr_validation: true, ..ParseOptions::default() };
2198 let doc = parse_bytes_in_place(buf, &opts)
2199 .expect("skip_attr_validation=true must accept duplicate attr");
2200 // Both attributes end up on the element when validation is off —
2201 // we don't dedup at the structural layer.
2202 let n_attrs = doc.root().attributes().count();
2203 assert_eq!(n_attrs, 2, "both duplicate attrs should be present");
2204 }
2205
2206 /// XML 1.0 § 3.1 [STag/ETag]: end tag name must match the start
2207 /// tag name. `<a></b>` is a well-formedness error.
2208 #[test]
2209 fn in_place_default_opts_reject_end_tag_mismatch() {
2210 let buf = b"<a></b>".to_vec();
2211 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2212 .expect_err("default opts must reject mismatched end tag");
2213 let msg = err.message.to_lowercase();
2214 assert!(
2215 msg.contains("mismatched") || msg.contains("expected"),
2216 "error should mention mismatched end tag, got: {}", err.message,
2217 );
2218 }
2219
2220 #[test]
2221 fn in_place_skip_end_tag_check_accepts_end_tag_mismatch() {
2222 let buf = b"<a></b>".to_vec();
2223 let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
2224 let doc = parse_bytes_in_place(buf, &opts)
2225 .expect("skip_end_tag_check=true must accept mismatched end tag");
2226 assert_eq!(doc.root().name(), "a");
2227 }
2228
2229 /// XML 1.0 § 2.2 [2]: only specific Unicode code points are valid
2230 /// XML characters. 0x01 (control character, not in the allowed
2231 /// set) is rejected when `skip_xml_char_validation` is false.
2232 #[test]
2233 fn in_place_default_opts_reject_invalid_xml_char() {
2234 // Text content containing 0x01 — valid UTF-8 (single ASCII byte)
2235 // but invalid as an XML 1.0 character.
2236 let buf = b"<r>hello\x01world</r>".to_vec();
2237 let err = parse_bytes_in_place(buf, &ParseOptions::default())
2238 .expect_err("default opts must reject invalid XML char");
2239 // Validator's actual wording — check for either the section
2240 // citation or any mention of "character" / "valid".
2241 let msg = err.message.to_lowercase();
2242 assert!(
2243 msg.contains("xml 1.0") || msg.contains("char") || msg.contains("invalid"),
2244 "error should mention invalid XML char, got: {}", err.message,
2245 );
2246 }
2247
2248 #[test]
2249 fn in_place_skip_xml_char_validation_accepts_invalid_xml_char() {
2250 let buf = b"<r>hello\x01world</r>".to_vec();
2251 let opts = ParseOptions { skip_xml_char_validation: true, ..ParseOptions::default() };
2252 let doc = parse_bytes_in_place(buf, &opts)
2253 .expect("skip_xml_char_validation=true must accept 0x01 in text");
2254 let text = doc.root().children().find_map(|n| n.text_content()).unwrap_or("");
2255 assert_eq!(text.as_bytes(), b"hello\x01world");
2256 }
2257
2258 /// Combined "fast path" — all four skips enabled. Exercises the
2259 /// path callers actually reach for when they want maximum speed.
2260 /// Verifies it works on a well-formed doc (the more interesting
2261 /// cases are covered individually above; this is a smoke test).
2262 #[test]
2263 fn in_place_with_all_skips_parses_well_formed_doc() {
2264 let buf = b"<root><a id=\"1\">hello</a></root>".to_vec();
2265 let opts = ParseOptions {
2266 skip_xml_char_validation: true,
2267 skip_name_validation: true,
2268 skip_attr_validation: true,
2269 skip_end_tag_check: true,
2270 ..ParseOptions::default()
2271 };
2272 let doc = parse_bytes_in_place(buf, &opts).expect("all-skips parse");
2273 assert_eq!(doc.root().name(), "root");
2274 let a = doc.root().children().next().expect("element child");
2275 assert_eq!(a.name(), "a");
2276 assert_eq!(a.attributes().next().unwrap().value(), "1");
2277 }
2278}