Skip to main content

sup_xml_core/
reader.rs

1//! `XmlReader` — string-typed streaming SAX-style API.
2//!
3//! Thin wrapper around [`XmlBytesReader`](crate::xml_bytes_reader::XmlBytesReader).
4//! The engine is byte-level (raw `Cow<'src, [u8]>` events with no UTF-8
5//! cast); this module converts each event's payload to `Cow<'src, str>` at
6//! the boundary via `from_utf8_unchecked`.  The bytes are valid UTF-8 by
7//! the Scanner's construction-time invariant, so the cast is a no-op.
8//!
9//! ## Why two readers exist
10//!
11//! Some callers prefer raw bytes — byte-literal tag matching
12//! (`name == b"item"`), hash/digest pipelines, format conversion, byte
13//! forwarding — and would rather not pay for the type system's `&str`
14//! guarantee.  `XmlBytesReader` serves them.  Most callers want validated
15//! strings; `XmlReader` (this module) serves them and is the recommended
16//! default.  The two share a single parser; the only difference is the
17//! payload type each emits.
18
19use std::borrow::Cow;
20
21use memchr::memchr;
22
23use crate::error::Result;
24use crate::options::ParseOptions;
25use crate::xml_bytes_reader::{
26    BytesAttrs, BytesCData, BytesComment, BytesEndTag, BytesEvent, BytesPi, BytesStartTag,
27    BytesText, XmlBytesReader, XmlDeclInfo,
28};
29
30// ── public types ──────────────────────────────────────────────────────────────
31
32/// A single attribute from a start tag, with a zero-copy value when possible.
33#[derive(Debug)]
34pub struct Attr<'src> {
35    /// Source-borrowed attribute name — XML names can't contain entity
36    /// refs, so no allocation is ever required.
37    pub name:  &'src str,
38    /// Attribute value.  Borrowed from source when no entity references
39    /// appeared in the literal; owned otherwise.
40    pub value: Cow<'src, str>,
41}
42
43impl<'src> Attr<'src> {
44    /// Attribute name as a borrowed source slice.  Same as `self.name`.
45    pub fn name(&self)  -> &'src str { self.name }
46    /// Attribute value, borrowing from the source when possible.
47    pub fn value(&self) -> &str      { &self.value }
48}
49
50/// Lazy iterator over the attributes of a start tag.
51///
52/// Returned by [`StartTag::attrs`].  Each call to `.next()` parses one
53/// `name="value"` pair from the source.  Attributes you never iterate
54/// cost nothing — this is the win over the eager
55/// [`XmlReader::next_into`] API.
56///
57/// # Error semantics
58///
59/// If an attribute fails to parse, the iterator yields `Some(Err(_))`
60/// once and returns `None` on every subsequent call.  A malformed
61/// attribute terminates iteration; the caller should bail.
62pub struct Attrs<'r, 'src> {
63    inner: BytesAttrs<'r, 'src>,
64}
65
66impl<'src> Iterator for Attrs<'_, 'src> {
67    type Item = Result<Attr<'src>>;
68
69    fn next(&mut self) -> Option<Self::Item> {
70        self.inner.next().map(|res| res.map(|ba| Attr {
71            // SAFETY: Scanner invariant — name bytes are valid UTF-8.
72            name:  unsafe { std::str::from_utf8_unchecked(ba.name) },
73            value: cow_bytes_to_str(ba.value),
74        }))
75    }
76}
77
78// ── tag types (str-typed wrappers over the bytes-typed BytesXxx) ─────────────
79
80/// A start-tag event.  Source offsets only — no name extraction or
81/// attribute parsing happens until you call a method.
82pub struct StartTag<'r, 'src> {
83    inner: BytesStartTag<'r, 'src>,
84}
85
86impl<'r, 'src> StartTag<'r, 'src> {
87    /// Element name as a string slice.  Borrowed from the source on
88    /// the common path; tied to `&self` for start tags read from
89    /// inside an entity-replacement stream.  Use [`name_cow`] when
90    /// you need a `'src`-lifetime string.
91    ///
92    /// [`name_cow`]: StartTag::name_cow
93    #[inline]
94    pub fn name(&self) -> &str {
95        // SAFETY: Scanner invariant — name bytes are valid UTF-8.
96        unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
97    }
98
99    /// Element name with the `'src` lifetime preserved when possible.
100    /// Source-borrowed names round-trip without copying;
101    /// entity-stream names come back as `Cow::Owned`.
102    pub fn name_cow(&self) -> Cow<'src, str> {
103        match self.inner.name_cow() {
104            // SAFETY: Scanner invariant — name bytes are valid UTF-8.
105            Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
106            Cow::Owned(v)    => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
107        }
108    }
109
110    /// Iterate the attributes (consumes the tag).
111    pub fn attrs(self) -> Attrs<'r, 'src> {
112        Attrs { inner: self.inner.attrs() }
113    }
114
115    /// Raw byte range of the attrs region (between the name and the
116    /// closing `>` / `/>`).
117    #[inline]
118    pub fn attrs_str(&self) -> &'src str {
119        // SAFETY: Scanner invariant — bytes are valid UTF-8.
120        unsafe { std::str::from_utf8_unchecked(self.inner.attrs_bytes()) }
121    }
122}
123
124impl std::fmt::Debug for StartTag<'_, '_> {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("StartTag")
127            .field("name",  &self.name())
128            .field("attrs", &self.attrs_str())
129            .finish()
130    }
131}
132
133/// An end-tag event (`</element>` — or the synthetic close emitted
134/// after every self-closing `<element/>`).
135pub struct EndTag<'src> {
136    inner: BytesEndTag<'src>,
137}
138
139impl<'src> EndTag<'src> {
140    #[inline]
141    pub fn name(&self) -> &'src str {
142        // SAFETY: Scanner invariant — name bytes are valid UTF-8.
143        unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
144    }
145}
146
147impl std::fmt::Debug for EndTag<'_> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("EndTag").field("name", &self.name()).finish()
150    }
151}
152
153/// Character-data text between elements.
154pub struct Text<'src> { inner: BytesText<'src> }
155impl<'src> Text<'src> {
156    #[inline] pub fn as_str(&self) -> &str {
157        // SAFETY: Scanner invariant.
158        unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
159    }
160    pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
161}
162impl std::fmt::Debug for Text<'_> {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        write!(f, "Text({:?})", self.as_str())
165    }
166}
167
168/// A `<![CDATA[…]]>` section.
169pub struct CData<'src> { inner: BytesCData<'src> }
170impl<'src> CData<'src> {
171    #[inline] pub fn as_str(&self) -> &str {
172        unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
173    }
174    pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
175}
176impl std::fmt::Debug for CData<'_> {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        write!(f, "CData({:?})", self.as_str())
179    }
180}
181
182/// An XML comment (`<!-- ... -->`).  The payload is the text strictly
183/// between the delimiters.
184pub struct Comment<'src> { inner: BytesComment<'src> }
185impl<'src> Comment<'src> {
186    #[inline] pub fn as_str(&self) -> &str {
187        unsafe { std::str::from_utf8_unchecked(self.inner.as_bytes()) }
188    }
189    pub fn into_str(self) -> Cow<'src, str> { cow_bytes_to_str(self.inner.into_bytes()) }
190}
191impl std::fmt::Debug for Comment<'_> {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(f, "Comment({:?})", self.as_str())
194    }
195}
196
197/// A processing instruction (`<?target content?>`).
198pub struct Pi<'src> { inner: BytesPi<'src> }
199impl<'src> Pi<'src> {
200    #[inline] pub fn target(&self) -> &str {
201        unsafe { std::str::from_utf8_unchecked(self.inner.target()) }
202    }
203    #[inline] pub fn content(&self) -> &str {
204        unsafe { std::str::from_utf8_unchecked(self.inner.content()) }
205    }
206    pub fn into_parts(self) -> (Cow<'src, str>, Cow<'src, str>) {
207        let (t, c) = self.inner.into_parts();
208        (cow_bytes_to_str(t), cow_bytes_to_str(c))
209    }
210}
211impl std::fmt::Debug for Pi<'_> {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("Pi")
214            .field("target",  &self.target())
215            .field("content", &self.content())
216            .finish()
217    }
218}
219
220/// A streaming XML event with **lazy** access to its payload.
221///
222/// Returned by [`XmlReader::next`].  Each variant wraps a tag struct
223/// whose methods (`name()`, `attrs()`, `as_str()`, etc.) extract data
224/// on demand.  For an eager API that fills a caller-owned buffer see
225/// [`XmlReader::next_into`] which returns [`EventInto`].
226#[derive(Debug)]
227pub enum Event<'r, 'src> {
228    /// An opening (or empty-element) start tag.
229    StartElement(StartTag<'r, 'src>),
230    /// A closing tag.  Emitted once for each `StartElement`, including
231    /// for empty elements (`<br/>` emits `StartElement` then `EndElement`).
232    EndElement(EndTag<'src>),
233    /// Character data between tags.
234    Text(Text<'src>),
235    /// A `<![CDATA[…]]>` section.
236    CData(CData<'src>),
237    /// An XML comment.
238    Comment(Comment<'src>),
239    /// A processing instruction.
240    Pi(Pi<'src>),
241    /// An unresolved entity reference (`&name;`) — emitted only when
242    /// [`ParseOptions::resolve_entities`] is `false`.  Carries the
243    /// entity name without the surrounding `&` / `;`.
244    EntityRef(EntityRef<'src>),
245    /// The document has been fully consumed.
246    Eof,
247}
248
249/// `Event::EntityRef` payload — entity name only.  String-typed
250/// counterpart to [`crate::xml_bytes_reader::BytesEntityRef`].
251pub struct EntityRef<'src> {
252    inner: crate::xml_bytes_reader::BytesEntityRef<'src>,
253}
254
255impl<'src> EntityRef<'src> {
256    /// Entity name as a borrowed source slice (e.g. `"foo"` for
257    /// the reference `&foo;`).
258    #[inline]
259    pub fn name(&self) -> &'src str {
260        // SAFETY: Scanner UTF-8 invariant — name bytes are valid UTF-8.
261        unsafe { std::str::from_utf8_unchecked(self.inner.name()) }
262    }
263}
264
265impl std::fmt::Debug for EntityRef<'_> {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        f.debug_struct("EntityRef")
268            .field("name", &self.name())
269            .finish()
270    }
271}
272
273/// A streaming XML event with **attributes already parsed into a caller-owned
274/// buffer**.
275///
276/// Returned by [`XmlReader::next_into`].  The buffer the caller passes is
277/// cleared on each call and filled with the start tag's attributes, allowing
278/// buffer reuse across many events.  For lazy attribute access without a
279/// buffer see [`XmlReader::next`] which returns [`Event`].
280#[derive(Debug)]
281pub enum EventInto<'src> {
282    /// An opening (or empty-element) start tag.  Attributes are in the
283    /// caller-owned buffer passed to [`XmlReader::next_into`].
284    StartElement { name: Cow<'src, str> },
285    /// A closing tag.  Emitted once for each `StartElement`, including for
286    /// empty elements.
287    EndElement { name: Cow<'src, str> },
288    Text(Cow<'src, str>),
289    CData(Cow<'src, str>),
290    Comment(Cow<'src, str>),
291    Pi { target: Cow<'src, str>, content: Cow<'src, str> },
292    /// An unresolved entity reference (`&name;`) — emitted only when
293    /// [`ParseOptions::resolve_entities`] is `false`.
294    EntityRef { name: Cow<'src, str> },
295    Eof,
296}
297
298// ── reader ────────────────────────────────────────────────────────────────────
299
300/// Streaming XML reader with string-typed events.  Thin wrapper around
301/// [`XmlBytesReader`](crate::xml_bytes_reader::XmlBytesReader) — the engine
302/// runs in raw bytes and we re-label each event's payload as `Cow<'src, str>`
303/// at the boundary.  The conversion is a no-op: bytes are valid UTF-8 by
304/// the Scanner's construction-time invariant.
305pub struct XmlReader<'src> {
306    inner: XmlBytesReader<'src>,
307}
308
309impl<'src> XmlReader<'src> {
310    /// Create a reader from a string slice.  The string must remain alive for
311    /// the lifetime of the reader and all events it produces.
312    #[allow(clippy::should_implement_trait)] // intentional: mirrors `FromStr` but `XmlReader<'src>` borrows
313    pub fn from_str(input: &'src str) -> Self {
314        Self { inner: XmlBytesReader::from_str(input) }
315    }
316
317    /// Create a reader from a byte slice.  Returns an error if the bytes are
318    /// not valid UTF-8.
319    pub fn from_bytes(src: &'src [u8]) -> Result<Self> {
320        XmlBytesReader::from_bytes(src).map(|inner| Self { inner })
321    }
322
323    /// Create a reader from a byte slice, **skipping** the upfront UTF-8
324    /// validation that [`from_bytes`](Self::from_bytes) performs.
325    ///
326    /// # Safety
327    ///
328    /// The bytes pointed to by `src` must be valid UTF-8 for the lifetime of
329    /// the returned reader and for any [`Event`] it emits.  Violating this
330    /// invariant is **undefined behaviour** — `Event` payloads are `&str`
331    /// slices into the input, produced via [`std::str::from_utf8_unchecked`].
332    pub unsafe fn from_bytes_unchecked(src: &'src [u8]) -> Self {
333        Self { inner: unsafe { XmlBytesReader::from_bytes_unchecked(src) } }
334    }
335
336    /// Destructive in-place reader.  The reader is permitted to mutate
337    /// `src` during parsing — used by [`crate::parser::parse_bytes_in_place`].
338    ///
339    /// # Safety
340    ///
341    /// `src` must be valid UTF-8.  Caller transfers exclusive write
342    /// access to `src` for the reader's lifetime.
343    pub unsafe fn from_bytes_in_place_unchecked(src: &'src mut [u8]) -> Self {
344        Self { inner: unsafe { XmlBytesReader::from_bytes_in_place_unchecked(src) } }
345    }
346
347    /// Override the [`ParseOptions`] that the reader was constructed with.
348    /// Use this to lower limits or skip checks for trusted input.
349    pub fn with_options(self, opts: ParseOptions) -> Self {
350        Self { inner: self.inner.with_options(opts) }
351    }
352
353    /// XML declaration fields parsed from the prolog.  Returns `None`
354    /// before the first event has been read, or when the document has
355    /// no `<?xml ... ?>` declaration.  See
356    /// [`XmlBytesReader::xml_decl`] for the underlying definition.
357    pub fn xml_decl(&self) -> Option<&XmlDeclInfo> {
358        self.inner.xml_decl()
359    }
360
361    /// Non-fatal errors logged while parsing with
362    /// `ParseOptions::recovery_mode = true`.  Empty in strict mode (errors
363    /// are returned via `Err` from `next`/`next_into` instead).  See
364    /// [`XmlBytesReader::recovered_errors`] for full semantics.
365    pub fn recovered_errors(&self) -> &[crate::error::XmlError] {
366        self.inner.recovered_errors()
367    }
368
369    /// Current byte offset into the original source.  Use with
370    /// [`line_col`](Self::line_col) (or [`src_bytes`](Self::src_bytes)
371    /// + [`crate::scanner::compute_line_col`]) to attribute
372    /// diagnostics to a source position.
373    ///
374    /// For start-of-element offsets specifically, prefer
375    /// [`last_start_offset`](Self::last_start_offset): it returns the
376    /// `<` of the most recent StartElement, whereas `src_offset` will
377    /// already be past the start tag's closing `>` by the time you
378    /// read it.
379    #[inline]
380    pub fn src_offset(&self) -> usize {
381        self.inner.src_offset()
382    }
383
384    /// Source byte offset of the `<` of the most recently emitted
385    /// StartElement.  `None` before the first start tag, or when the
386    /// start tag was read from inside an entity-replacement stream.
387    /// Pairs with [`line_col_at`](Self::line_col_at) to anchor
388    /// diagnostics at the right source position.
389    #[inline]
390    pub fn last_start_offset(&self) -> Option<usize> {
391        self.inner.last_start_offset()
392    }
393
394    /// The original source bytes the reader is parsing.  Pairs with
395    /// [`src_offset`](Self::src_offset) for translating byte positions
396    /// into line/column.
397    #[inline]
398    pub fn src_bytes(&self) -> &'src [u8] {
399        self.inner.src_bytes()
400    }
401
402    /// Translate the current reader position into a 1-based
403    /// `(line, column)` pair.  Lazy: scans the prefix from byte 0
404    /// to the current offset once per call (cost ~O(offset)).  Cheap
405    /// enough for error paths; not for tight loops.
406    #[inline]
407    pub fn line_col(&self) -> (u32, u32) {
408        crate::scanner::compute_line_col(self.src_bytes(), self.src_offset())
409    }
410
411    /// Translate an arbitrary byte offset (typically captured earlier
412    /// via [`src_offset`](Self::src_offset)) into 1-based
413    /// `(line, column)`.  Same cost model as
414    /// [`line_col`](Self::line_col).
415    #[inline]
416    pub fn line_col_at(&self, offset: usize) -> (u32, u32) {
417        crate::scanner::compute_line_col(self.src_bytes(), offset)
418    }
419
420    /// Read the next event with **lazy** attribute access.
421    ///
422    /// Returns an [`Event`] borrowing the reader for its lifetime.  Start tag
423    /// events carry an [`Attrs`] iterator — iterate it to read attributes,
424    /// ignore it to skip attribute parsing entirely.
425    ///
426    /// For an eager API that fills a caller-owned buffer with parsed
427    /// attributes, see [`next_into`](Self::next_into).
428    #[allow(clippy::should_implement_trait)] // can't impl `Iterator`: events borrow the reader
429    pub fn next(&mut self) -> Result<Event<'_, 'src>> {
430        match self.inner.next()? {
431            BytesEvent::StartElement(t) => Ok(Event::StartElement(StartTag { inner: t })),
432            BytesEvent::EndElement(t)   => Ok(Event::EndElement(EndTag { inner: t })),
433            BytesEvent::Text(t)         => Ok(Event::Text(Text { inner: t })),
434            BytesEvent::CData(s)        => Ok(Event::CData(CData { inner: s })),
435            BytesEvent::Comment(s)      => Ok(Event::Comment(Comment { inner: s })),
436            BytesEvent::Pi(p)           => Ok(Event::Pi(Pi { inner: p })),
437            BytesEvent::EntityRef(e)    => Ok(Event::EntityRef(EntityRef { inner: e })),
438            BytesEvent::Eof             => Ok(Event::Eof),
439        }
440    }
441
442    /// Read the next event, eagerly parsing start-tag attributes into `buf`.
443    ///
444    /// `buf` is cleared on every call.  For `StartElement` events `buf` is
445    /// filled with the element's attributes in source order; for other events
446    /// `buf` is left empty.  Pass the same `Vec` across many calls to reuse
447    /// its allocation.
448    ///
449    /// For lazy attribute access (zero work when you never read attrs), see
450    /// [`next`](Self::next).
451    pub fn next_into(&mut self, buf: &mut Vec<Attr<'src>>) -> Result<EventInto<'src>> {
452        buf.clear();
453        match self.next()? {
454            Event::StartElement(tag) => {
455                let name = tag.name_cow();
456                for attr in tag.attrs() { buf.push(attr?); }
457                Ok(EventInto::StartElement { name })
458            }
459            Event::EndElement(tag) => Ok(EventInto::EndElement {
460                name: Cow::Borrowed(tag.name()),
461            }),
462            Event::Text(t)     => Ok(EventInto::Text(t.into_str())),
463            Event::CData(s)    => Ok(EventInto::CData(s.into_str())),
464            Event::Comment(s)  => Ok(EventInto::Comment(s.into_str())),
465            Event::Pi(p)       => {
466                let (target, content) = p.into_parts();
467                Ok(EventInto::Pi { target, content })
468            }
469            Event::EntityRef(e) => Ok(EventInto::EntityRef {
470                name: Cow::Borrowed(e.name()),
471            }),
472            Event::Eof         => Ok(EventInto::Eof),
473        }
474    }
475}
476
477// ── helpers ──────────────────────────────────────────────────────────────────
478
479/// Zero-cost conversion from `Cow<'src, [u8]>` to `Cow<'src, str>` — the
480/// bytes are valid UTF-8 by the Scanner's invariant.
481///
482/// `&[u8]` ↔ `&str` and `Vec<u8>` ↔ `String` have identical memory layout,
483/// so both arms compile to no-op casts (just type-system relabeling).
484#[inline]
485fn cow_bytes_to_str(c: Cow<'_, [u8]>) -> Cow<'_, str> {
486    match c {
487        // SAFETY: Scanner invariant — slices come from valid-UTF-8 input
488        // (either the original source, which the constructor validated, or
489        // an entity replacement built up by entity expansion which writes
490        // only complete UTF-8 sequences).
491        Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
492        Cow::Owned(v)    => Cow::Owned(unsafe { String::from_utf8_unchecked(v)   }),
493    }
494}
495
496/// Expand the five XML predefined entity references (`&amp;`, `&lt;`,
497/// `&gt;`, `&quot;`, `&apos;`) and numeric character references (`&#NN;`,
498/// `&#xNN;`) inside `s`.  Intended for callers using
499/// [`ParseOptions::skip_entity_expansion`] who want to decode a specific
500/// text payload on demand.
501///
502/// Returns `Cow::Borrowed(s)` when `s` contains no `&` — i.e. the
503/// no-entity case is zero-copy.
504///
505/// **General entities** declared in a DTD (`<!ENTITY foo "...">`) are not
506/// expanded here; the helper has no access to the document's entity table.
507/// If you need DTD-defined entity expansion, don't enable
508/// `skip_entity_expansion` in the first place.
509pub fn unescape(s: &str) -> Cow<'_, str> {
510    if memchr(b'&', s.as_bytes()).is_none() {
511        return Cow::Borrowed(s);
512    }
513    let bytes = s.as_bytes();
514    let mut out = String::with_capacity(s.len());
515    let mut i = 0;
516    while i < bytes.len() {
517        if bytes[i] != b'&' {
518            out.push(bytes[i] as char);
519            i += 1;
520            continue;
521        }
522        // Find the `;`.  If missing or too far away, treat `&` as literal.
523        let rest = &bytes[i + 1..];
524        let semi = match memchr(b';', rest) {
525            Some(n) if n <= 16 => n,
526            _ => { out.push('&'); i += 1; continue; }
527        };
528        let name = &rest[..semi];
529        match name {
530            b"amp"  => { out.push('&'); }
531            b"lt"   => { out.push('<'); }
532            b"gt"   => { out.push('>'); }
533            b"quot" => { out.push('"'); }
534            b"apos" => { out.push('\''); }
535            _ if name.starts_with(b"#") => {
536                let cp: Option<u32> = if name.len() >= 2 && (name[1] == b'x' || name[1] == b'X') {
537                    std::str::from_utf8(&name[2..]).ok()
538                        .and_then(|h| u32::from_str_radix(h, 16).ok())
539                } else {
540                    std::str::from_utf8(&name[1..]).ok()
541                        .and_then(|d| d.parse::<u32>().ok())
542                };
543                match cp.and_then(char::from_u32) {
544                    Some(c) => out.push(c),
545                    None    => {
546                        out.push('&');
547                        out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
548                        out.push(';');
549                    }
550                }
551            }
552            _ => {
553                out.push('&');
554                out.push_str(unsafe { std::str::from_utf8_unchecked(name) });
555                out.push(';');
556            }
557        }
558        i += 1 + semi + 1;
559    }
560    Cow::Owned(out)
561}
562
563// ── tests ─────────────────────────────────────────────────────────────────────
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    fn events(src: &str) -> Vec<String> {
570        let mut r = XmlReader::from_str(src);
571        let mut out = Vec::new();
572        let mut buf = Vec::new();
573        loop {
574            match r.next_into(&mut buf).unwrap() {
575                EventInto::StartElement { name } => {
576                    let a: Vec<_> = buf.iter().map(|a| format!("{}={}", a.name, a.value)).collect();
577                    if a.is_empty() { out.push(format!("<{name}>")); }
578                    else            { out.push(format!("<{name} {}>", a.join(" "))); }
579                }
580                EventInto::EndElement { name }  => out.push(format!("</{name}>")),
581                EventInto::Text(t)              => out.push(format!("T:{t}")),
582                EventInto::CData(s)             => out.push(format!("CD:{s}")),
583                EventInto::Comment(s)           => out.push(format!("C:{s}")),
584                EventInto::Pi { target, .. }    => out.push(format!("PI:{target}")),
585                EventInto::EntityRef { name }   => out.push(format!("E:{name}")),
586                EventInto::Eof                  => break,
587            }
588        }
589        out
590    }
591
592    #[test]
593    fn minimal() {
594        assert_eq!(events("<r/>"), vec!["<r>", "</r>"]);
595    }
596
597    #[test]
598    fn nested() {
599        assert_eq!(
600            events("<a><b>hello</b></a>"),
601            vec!["<a>", "<b>", "T:hello", "</b>", "</a>"],
602        );
603    }
604
605    #[test]
606    fn attributes_borrowed() {
607        let src = r#"<el id="1" class="x"/>"#;
608        let mut r = XmlReader::from_str(src);
609        let mut buf = Vec::new();
610        let ev = r.next_into(&mut buf).unwrap();
611        assert!(matches!(&ev, EventInto::StartElement { name } if name == "el"));
612        assert_eq!(buf.len(), 2);
613        assert!(matches!(buf[0].value, Cow::Borrowed(_)), "no entity → should borrow");
614    }
615
616    #[test]
617    fn attribute_entity_owned() {
618        let src = r#"<el v="a&amp;b"/>"#;
619        let mut r = XmlReader::from_str(src);
620        let mut buf = Vec::new();
621        r.next_into(&mut buf).unwrap();
622        assert_eq!(buf[0].value.as_ref(), "a&b");
623        assert!(matches!(buf[0].value, Cow::Owned(_)), "entity → must allocate");
624    }
625
626    #[test]
627    fn text_borrowed() {
628        let src = "<r>hello world</r>";
629        let mut r = XmlReader::from_str(src);
630        let mut buf = Vec::new();
631        r.next_into(&mut buf).unwrap(); // StartElement
632        let ev = r.next_into(&mut buf).unwrap();
633        assert!(matches!(ev, EventInto::Text(Cow::Borrowed("hello world"))));
634    }
635
636    #[test]
637    fn cdata_borrowed() {
638        let src = "<r><![CDATA[raw <data>]]></r>";
639        assert_eq!(events(src), vec!["<r>", "CD:raw <data>", "</r>"]);
640    }
641
642    #[test]
643    fn empty_element_emits_both_events() {
644        assert_eq!(events("<root><br/></root>"), vec!["<root>", "<br>", "</br>", "</root>"]);
645    }
646
647    #[test]
648    fn buffer_reuse() {
649        let src = "<a x='1'/><b y='2'/>";
650        let src = format!("<root>{src}</root>");
651        let mut r = XmlReader::from_str(&src);
652        let mut buf: Vec<Attr> = Vec::new();
653        let cap_before;
654        loop {
655            match r.next_into(&mut buf).unwrap() {
656                EventInto::StartElement { name } if name == "a" => {
657                    cap_before = buf.capacity();
658                    break;
659                }
660                EventInto::Eof => panic!("unexpected EOF"),
661                _ => {}
662            }
663        }
664        loop {
665            match r.next_into(&mut buf).unwrap() {
666                EventInto::StartElement { name } if name == "b" => {
667                    assert_eq!(buf.capacity(), cap_before, "capacity should not grow for same-size attrs");
668                    break;
669                }
670                EventInto::Eof => panic!("unexpected EOF"),
671                _ => {}
672            }
673        }
674    }
675
676    #[test]
677    fn lazy_attrs_iter() {
678        let src = r#"<el id="1" class="x"/>"#;
679        let mut r = XmlReader::from_str(src);
680        match r.next().unwrap() {
681            Event::StartElement(tag) => {
682                assert_eq!(tag.name(), "el");
683                let pairs: Vec<(String, String)> = tag.attrs()
684                    .map(|a| a.map(|a| (a.name.to_owned(), a.value.into_owned())).unwrap())
685                    .collect();
686                assert_eq!(pairs, vec![("id".into(), "1".into()), ("class".into(), "x".into())]);
687            }
688            _ => panic!("expected StartElement"),
689        }
690    }
691
692    #[test]
693    fn lazy_attrs_skipped_costs_nothing() {
694        let src = r#"<el id="1" class="x"/>"#;
695        let mut r = XmlReader::from_str(src);
696        match r.next().unwrap() {
697            Event::StartElement(tag) => assert_eq!(tag.name(), "el"),
698            _ => panic!(),
699        }
700        match r.next().unwrap() {
701            Event::EndElement(tag) => assert_eq!(tag.name(), "el"),
702            _ => panic!(),
703        }
704        match r.next().unwrap() {
705            Event::Eof => {}
706            _ => panic!(),
707        }
708    }
709
710    #[test]
711    fn debug_impl_shows_contents() {
712        // Custom Debug should print the *content* (lossy-decoded), not
713        // raw byte offsets.  Sanity-check each tag flavour.
714        let mut r = XmlReader::from_str("<a x='1'>hi<!-- c --><![CDATA[cd]]><?p y?></a>");
715        let mut seen: Vec<String> = Vec::new();
716        loop {
717            match r.next().unwrap() {
718                Event::Eof => break,
719                ev => seen.push(format!("{ev:?}")),
720            }
721        }
722        let combined = seen.join("\n");
723        assert!(combined.contains("StartTag"),  "Debug missing StartTag — {combined}");
724        assert!(combined.contains("\"a\""),     "Debug should show element name — {combined}");
725        assert!(combined.contains("Comment(\" c \")"), "Debug for Comment — {combined}");
726        assert!(combined.contains("CData(\"cd\")"),    "Debug for CData — {combined}");
727        assert!(combined.contains("EndTag"),    "Debug missing EndTag — {combined}");
728    }
729
730    #[test]
731    fn text_method_access() {
732        // Text exposes as_str() and into_str().
733        let src = "<r>hello</r>";
734        let mut r = XmlReader::from_str(src);
735        let _ = r.next().unwrap(); // StartElement
736        match r.next().unwrap() {
737            Event::Text(t) => {
738                assert_eq!(t.as_str(), "hello");
739                let owned = t.into_str();
740                assert_eq!(owned, "hello");
741                assert!(matches!(owned, Cow::Borrowed("hello")));
742            }
743            _ => panic!(),
744        }
745    }
746
747    // ── Attr accessors ────────────────────────────────────────────────────
748
749    #[test]
750    fn attr_name_and_value_accessors() {
751        let src = r#"<el id="1" class="x"/>"#;
752        let mut r = XmlReader::from_str(src);
753        match r.next().unwrap() {
754            Event::StartElement(tag) => {
755                let attrs: Vec<_> = tag.attrs().map(|a| a.unwrap()).collect();
756                // .name() and .value() methods (vs direct fields).
757                assert_eq!(attrs[0].name(),  "id");
758                assert_eq!(attrs[0].value(), "1");
759                assert_eq!(attrs[1].name(),  "class");
760                assert_eq!(attrs[1].value(), "x");
761            }
762            _ => panic!(),
763        }
764    }
765
766    // ── from_bytes / from_bytes_unchecked / from_bytes_in_place_unchecked ─
767
768    #[test]
769    fn from_bytes_valid_utf8() {
770        let src = b"<r/>";
771        let mut r = XmlReader::from_bytes(src).expect("valid utf-8");
772        match r.next().unwrap() {
773            Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
774            _ => panic!(),
775        }
776    }
777
778    #[test]
779    fn from_bytes_invalid_utf8_errors() {
780        let bad = &[b'<', 0xFF, 0xFE, b'>'];
781        assert!(XmlReader::from_bytes(bad).is_err());
782    }
783
784    #[test]
785    fn from_bytes_unchecked_skips_validation() {
786        let src = b"<r/>";
787        // SAFETY: the bytes are valid UTF-8.
788        let mut r = unsafe { XmlReader::from_bytes_unchecked(src) };
789        match r.next().unwrap() {
790            Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
791            _ => panic!(),
792        }
793    }
794
795    #[test]
796    fn from_bytes_in_place_unchecked() {
797        let mut src = b"<r/>".to_vec();
798        // SAFETY: bytes are valid UTF-8 and we don't touch `src` until the
799        // reader is dropped.
800        let mut r = unsafe { XmlReader::from_bytes_in_place_unchecked(&mut src) };
801        match r.next().unwrap() {
802            Event::StartElement(tag) => assert_eq!(tag.name(), "r"),
803            _ => panic!(),
804        }
805        drop(r);
806    }
807
808    // ── with_options / xml_decl / recovered_errors ───────────────────────
809
810    #[test]
811    fn with_options_changes_behavior() {
812        // With skip_entity_expansion the parser emits raw text without
813        // decoding &amp;.  Verify the Text payload is "&amp;" not "&".
814        let opts = ParseOptions { skip_entity_expansion: true, ..ParseOptions::default() };
815        let mut r = XmlReader::from_str("<r>&amp;</r>").with_options(opts);
816        let _ = r.next().unwrap();    // StartElement
817        match r.next().unwrap() {
818            Event::Text(t) => assert_eq!(t.as_str(), "&amp;"),
819            ev => panic!("expected Text, got {ev:?}"),
820        }
821    }
822
823    #[test]
824    fn xml_decl_returns_some_after_reading_prolog() {
825        let mut r = XmlReader::from_str(r#"<?xml version="1.0" encoding="UTF-8"?><r/>"#);
826        let _ = r.next().unwrap();    // StartElement — prolog has now been read
827        let decl = r.xml_decl().expect("xml decl");
828        assert_eq!(decl.version, "1.0");
829    }
830
831    #[test]
832    fn xml_decl_returns_none_when_absent() {
833        let mut r = XmlReader::from_str("<r/>");
834        let _ = r.next().unwrap();
835        assert!(r.xml_decl().is_none());
836    }
837
838    #[test]
839    fn recovered_errors_empty_for_well_formed_input() {
840        let mut r = XmlReader::from_str("<r/>");
841        let _ = r.next().unwrap();
842        let _ = r.next().unwrap();
843        assert!(r.recovered_errors().is_empty());
844    }
845
846    // ── EntityRef event surface ──────────────────────────────────────────
847
848    #[test]
849    fn entity_ref_event_via_next() {
850        // EntityRef events are only emitted for *user-defined* entities
851        // and only when resolve_entities = false.  Need a DOCTYPE so
852        // 'foo' is a valid declared entity name.  Run through events
853        // until we either find an EntityRef or hit EOF.
854        let src = r#"<?xml version="1.0"?>
855<!DOCTYPE r [<!ENTITY foo "bar">]>
856<r>&foo;</r>"#;
857        let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
858        let mut r = XmlReader::from_str(src).with_options(opts);
859        let mut found = false;
860        loop {
861            match r.next().unwrap() {
862                Event::EntityRef(e) => {
863                    assert_eq!(e.name(), "foo");
864                    let s = format!("{e:?}");
865                    assert!(s.contains("foo"), "got {s}");
866                    found = true;
867                    break;
868                }
869                Event::Eof => break,
870                _ => continue,
871            }
872        }
873        assert!(found, "EntityRef event was not emitted");
874    }
875
876    #[test]
877    fn entity_ref_event_via_next_into() {
878        let src = r#"<?xml version="1.0"?>
879<!DOCTYPE r [<!ENTITY bar "x">]>
880<r>&bar;</r>"#;
881        let opts = ParseOptions { resolve_entities: false, ..ParseOptions::default() };
882        let mut r = XmlReader::from_str(src).with_options(opts);
883        let mut buf = Vec::new();
884        let mut found = false;
885        loop {
886            match r.next_into(&mut buf).unwrap() {
887                EventInto::EntityRef { name } => {
888                    assert_eq!(name, "bar");
889                    found = true;
890                    break;
891                }
892                EventInto::Eof => break,
893                _ => continue,
894            }
895        }
896        assert!(found, "EntityRef event was not emitted");
897    }
898
899    // ── unescape ─────────────────────────────────────────────────────────
900
901    #[test]
902    fn unescape_no_amp_returns_borrowed() {
903        let s = "no entities here";
904        let out = unescape(s);
905        assert!(matches!(out, Cow::Borrowed(_)));
906        assert_eq!(out, "no entities here");
907    }
908
909    #[test]
910    fn unescape_predefined_entities() {
911        assert_eq!(unescape("&amp;").as_ref(),  "&");
912        assert_eq!(unescape("&lt;").as_ref(),   "<");
913        assert_eq!(unescape("&gt;").as_ref(),   ">");
914        assert_eq!(unescape("&quot;").as_ref(), "\"");
915        assert_eq!(unescape("&apos;").as_ref(), "'");
916    }
917
918    #[test]
919    fn unescape_mixed_text() {
920        assert_eq!(
921            unescape("a &amp; b &lt; c &gt; d").as_ref(),
922            "a & b < c > d",
923        );
924    }
925
926    #[test]
927    fn unescape_numeric_decimal_char_ref() {
928        assert_eq!(unescape("&#65;").as_ref(),    "A");
929        assert_eq!(unescape("&#8364;").as_ref(),  "€");
930    }
931
932    #[test]
933    fn unescape_numeric_hex_char_ref() {
934        assert_eq!(unescape("&#x41;").as_ref(),   "A");
935        assert_eq!(unescape("&#xX41;").as_ref(),  "&#xX41;"); // invalid → literal pass-through
936        assert_eq!(unescape("&#X41;").as_ref(),   "A");
937    }
938
939    #[test]
940    fn unescape_invalid_char_ref_passes_through() {
941        // Unparseable codepoint → keep the original text.
942        assert_eq!(unescape("&#abc;").as_ref(), "&#abc;");
943        // Out-of-range codepoint → keep the original text.
944        assert_eq!(unescape("&#99999999;").as_ref(), "&#99999999;");
945    }
946
947    #[test]
948    fn unescape_unknown_named_entity_passes_through() {
949        // Unknown named entity → keep the original `&name;`.
950        assert_eq!(unescape("&bogus;").as_ref(), "&bogus;");
951    }
952
953    #[test]
954    fn unescape_ampersand_without_semicolon() {
955        // `&` not followed by a `;` within 16 chars → literal '&'.
956        assert_eq!(unescape("&just an ampersand").as_ref(), "&just an ampersand");
957        // `&...` where `;` is too far away — kept as literal.
958        assert_eq!(unescape("&very_long_name_that_exceeds_sixteen_chars_threshold;").as_ref(),
959                   "&very_long_name_that_exceeds_sixteen_chars_threshold;");
960    }
961}