Skip to main content

sup_xml_core/
streaming_reader.rs

1//! Streaming XML reader over `io::Read`.
2//!
3//! [`XmlByteStreamReader`] is a thin wrapper around the existing
4//! slurped [`XmlBytesReader`] that owns a rolling `Vec<u8>` buffer
5//! and pulls more bytes from the inner reader on demand.  Used by
6//! the CLI's `lint` subcommand to validate multi-GB XML files in
7//! bounded memory without slurping them.
8//!
9//! # Design
10//!
11//! The wrapper owns:
12//! * `inner: R` — the source of bytes.
13//! * `buf: Vec<u8>` — a rolling buffer holding the not-yet-consumed
14//!   slice of input.  Grows up to `buffer_size`, then refuses to
15//!   grow further (a single XML token bigger than `buffer_size`
16//!   becomes a hard error).
17//! * `reader: XmlBytesReader<'static>` — the slurped reader, with
18//!   its scanner re-bound to point into `buf` after every refill /
19//!   compaction / growth.  The `'static` lifetime is a deliberate
20//!   lie maintained internally: the actual borrow is bounded by
21//!   `self.buf`, and we re-point the scanner via
22//!   [`XmlBytesReader::rebind_scanner`] whenever the buffer might
23//!   have moved.  This is sound because (a) both fields are owned
24//!   by `Self` and outlive each other, (b) the scanner only stores
25//!   a raw pointer into the buffer (no Rust borrow), and (c) we
26//!   never let any `&[u8]` derived from `buf` escape across a
27//!   buffer mutation.
28//!
29//! # Pre-fill model
30//!
31//! [`XmlBytesReader::next`] mutates internal state (depth,
32//! element_stack, …) partway through the call — i.e., it's not
33//! transactional.  We can't retry it after a mid-token refill
34//! without corrupting state.  So the wrapper refills *between*
35//! events, before calling `next`:
36//!
37//! ```text
38//! 1. Before calling reader.next():
39//!      if cur_len - cur_pos < buffer_size, refill.
40//! 2. Call reader.next() — guaranteed to have at least
41//!    buffer_size bytes ahead.  Completes within them or hits
42//!    true EOF.
43//! 3. Repeat.
44//! ```
45//!
46//! This guarantees the inner reader never sees a transient
47//! "ran-off-the-end" condition — its bytes are always there.  The
48//! trade-off is that `buffer_size` is also the maximum size of a
49//! single XML token (text node, attribute value, CDATA section);
50//! anything larger errors out.  Matches libxml2's
51//! `XML_MAX_TEXT_LENGTH` semantics.
52//!
53//! # Reading
54//!
55//! [`XmlByteStreamReader::next_event`] pulls one event at a time,
56//! streaming more bytes from the source as needed.  Each event
57//! borrows the rolling buffer and is valid only until the next pull;
58//! the borrow checker enforces this by tying the event's lifetime to
59//! `&mut self`, so a caller must consume each event before requesting
60//! the next.  This is the same zero-copy contract as
61//! [`XmlBytesReader::next`] and `quick-xml`'s `Reader::read_event`.
62//!
63//! [`XmlByteStreamReader::validate`] is the drive-to-EOF convenience
64//! used by the CLI's `lint` when only the well-formedness verdict
65//! matters — it pulls events to completion and discards them.
66
67use std::io::Read;
68
69use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
70use crate::options::ParseOptions;
71use crate::xml_bytes_reader::{BytesEvent, XmlBytesReader, XmlDeclInfo};
72
73/// Default working-buffer size when none is provided.  Matches
74/// libxml2's `XML_MAX_TEXT_LENGTH` (10 MB) — bigger than any single
75/// text node in the vast majority of real-world XML, small enough
76/// that streaming meaningfully saves memory vs slurping.
77pub const DEFAULT_BUFFER_SIZE: usize = 10 * 1024 * 1024;
78
79/// "Huge" mode buffer size — matches libxml2's `XML_PARSE_HUGE`
80/// (1 GB).  Use for inputs that contain unusually large tokens
81/// (embedded base64 blobs in SVG, OOXML packages, etc.).
82pub const HUGE_BUFFER_SIZE: usize = 1024 * 1024 * 1024;
83
84/// Initial buffer capacity when no size hint is available (e.g.
85/// reading from stdin / a pipe).  Grows dynamically as needed.
86const INITIAL_CAPACITY_WITHOUT_HINT: usize = 64 * 1024;
87
88/// Internal buffer capacity multiplier vs the user-facing
89/// `buffer_size` (which caps single-token size).  We allocate
90/// `BUF_CAPACITY_MULTIPLE * buffer_size` so that after a refill
91/// the scanner has `(BUF_CAPACITY_MULTIPLE - 1) * buffer_size`
92/// bytes of "consumption budget" before the next refill is
93/// triggered (i.e., before runway drops below `buffer_size`).
94/// With 2×, refills cost `2 * buffer_size` memmove each but
95/// amortise over `buffer_size` bytes of consumption — roughly
96/// 2× overhead vs slurped, and bounded.  Raising this trades
97/// memory for fewer refills; 2× is the sweet spot.
98const BUF_CAPACITY_MULTIPLE: usize = 2;
99
100/// Streaming XML reader that pulls bytes from an `io::Read` source
101/// on demand.
102///
103/// Constructed via [`Self::new`] or [`Self::with_size_hint`] and
104/// then driven via [`Self::next_event`] (pull events one at a time)
105/// or [`Self::validate`] (drive to EOF for a well-formedness check).
106pub struct XmlByteStreamReader<R: Read> {
107    inner: R,
108    /// Rolling buffer.  Capacity grows up to `buffer_size`; bytes
109    /// at `0..reader.cur_pos()` have been consumed and are eligible
110    /// for compaction on the next refill.
111    buf: Vec<u8>,
112    /// Maximum buffer size — also the maximum single-token size
113    /// the wrapper will accept.
114    buffer_size: usize,
115    /// `true` once `inner.read(...)` has returned 0 — no more
116    /// bytes will ever come.
117    eof: bool,
118    /// Inner reader with its scanner pointing into `self.buf`.
119    /// The `'static` lifetime is a lie; the actual borrow is
120    /// bounded by `self`.  Maintained sound by rebinding the
121    /// scanner whenever `buf` might have moved (see
122    /// [`Self::refill_and_rebind`]).
123    reader: XmlBytesReader<'static>,
124    /// Set when input begins with a non-UTF-8 BOM.  Streaming
125    /// rejects those at construction with a clear error.
126    _utf8_only_marker: (),
127}
128
129impl<R: Read> XmlByteStreamReader<R> {
130    /// Construct with no size hint (e.g. stdin).  Starts with a
131    /// 64 KiB buffer that grows as needed up to `buffer_size`.
132    pub fn new(inner: R, buffer_size: usize) -> Result<Self> {
133        Self::with_size_hint(inner, None, buffer_size)
134    }
135
136    /// Construct with an optional size hint.  When the input's
137    /// total size is known (file with stat-derived length), pass it
138    /// — the wrapper pre-allocates exactly that much (capped by
139    /// `buffer_size`), avoiding the per-growth memcpy cost.  For
140    /// stdin / pipes, pass `None` and the buffer grows
141    /// incrementally from a 64 KiB seed.
142    pub fn with_size_hint(
143        mut inner:   R,
144        size_hint:   Option<usize>,
145        buffer_size: usize,
146    ) -> Result<Self> {
147        // Internal capacity is `BUF_CAPACITY_MULTIPLE * buffer_size`
148        // so refills (triggered when runway drops below
149        // `buffer_size`) only fire after consuming roughly
150        // `buffer_size` bytes — amortising memmove cost over many
151        // events instead of one per event.  For inputs smaller than
152        // the internal cap, the file_size hint shrinks the initial
153        // allocation so small files don't pay for the full capacity.
154        let internal_cap = buffer_size.saturating_mul(BUF_CAPACITY_MULTIPLE);
155        let initial = size_hint
156            .map(|n| n.min(internal_cap))
157            .unwrap_or(INITIAL_CAPACITY_WITHOUT_HINT.min(internal_cap));
158        let mut buf = Vec::with_capacity(initial);
159
160        // Prime the buffer so we can detect the encoding from the
161        // BOM / decl before constructing the inner reader.  Read up
162        // to the smaller of the file size and the initial capacity
163        // — for stdin this just pulls the first 64 KiB.
164        read_into_vec(&mut inner, &mut buf, initial)?;
165        let eof = buf.len() < initial;
166
167        // UTF-8 sniff.  Streaming v1 supports UTF-8 only; non-UTF-8
168        // BOMs surface as a clear error pointing at the slurped
169        // reader for those inputs.
170        if let Some(bom) = sniff_non_utf8_bom(&buf) {
171            return Err(XmlError::new(
172                ErrorDomain::Encoding,
173                ErrorLevel::Fatal,
174                format!(
175                    "streaming reader: detected {bom} BOM, but streaming \
176                     v1 supports UTF-8 input only.  Use the slurped reader \
177                     for non-UTF-8 encodings."
178                ),
179            ));
180        }
181
182        // Strip leading UTF-8 BOM if present — XmlBytesReader doesn't
183        // skip it for us when we hand it the bytes directly.
184        if buf.starts_with(&[0xEF, 0xBB, 0xBF]) {
185            buf.drain(..3);
186        }
187
188        // Construct the inner reader against an empty static slice,
189        // then rebind its scanner to the actual buffer bytes.  This
190        // sidesteps a chicken-and-egg lifetime problem: we can't
191        // create a `&'static [u8]` referring to `buf` (it's owned
192        // here), but we can construct against the empty slice (which
193        // really IS `'static`) and rebind.
194        //
195        // `stream_owned_names` forces element-stack entries to be
196        // owned `String`s rather than byte ranges into the source —
197        // required because our rolling buffer compacts between
198        // events, and any byte ranges captured at start-tag time
199        // would become stale by end-tag time.  See
200        // `crate::xml_bytes_reader::dispatch_start_element` for the
201        // branch that consults this flag.
202        let mut opts = ParseOptions::default();
203        opts.stream_owned_names = true;
204        const EMPTY: &[u8] = &[];
205        let mut reader = XmlBytesReader::from_bytes(EMPTY)?.with_options(opts);
206        // SAFETY: `buf` is owned by `Self` and outlives `reader`.
207        // The bytes are valid UTF-8 (sniffed above; the BOM was
208        // stripped if present).  No entity stream is active on a
209        // freshly-constructed reader.  We re-call rebind after
210        // every operation that might move `buf`.
211        unsafe {
212            reader.rebind_scanner(buf.as_ptr(), buf.len(), 0);
213        }
214
215        Ok(Self {
216            inner,
217            buf,
218            buffer_size,
219            eof,
220            reader,
221            _utf8_only_marker: (),
222        })
223    }
224
225    /// Override the inner reader's [`ParseOptions`].  See
226    /// [`XmlBytesReader::with_options`] for what's tunable.
227    ///
228    /// Note: `opts.stream_owned_names` is forced to `true`
229    /// regardless of what the caller passes — the streaming
230    /// wrapper requires owned element names to survive buffer
231    /// compaction between events.
232    pub fn with_options(mut self, mut opts: ParseOptions) -> Self {
233        opts.stream_owned_names = true;
234        self.reader = self.reader.with_options(opts);
235        self
236    }
237
238    /// XML declaration fields parsed from the prolog, if any.
239    /// Returns `None` before the first event has been pulled.
240    pub fn xml_decl(&self) -> Option<&XmlDeclInfo> {
241        self.reader.xml_decl()
242    }
243
244    /// Non-fatal errors logged while
245    /// [`ParseOptions::recovery_mode`] is enabled.  Empty otherwise.
246    pub fn recovered_errors(&self) -> &[XmlError] {
247        self.reader.recovered_errors()
248    }
249
250    /// Pull the next parse event, streaming more bytes from the
251    /// source as needed.
252    ///
253    /// The returned [`BytesEvent`] borrows the reader's internal
254    /// rolling buffer and is valid only until the next call to
255    /// `next_event` (or any other `&mut self` method): its lifetime is
256    /// tied to the `&mut self` borrow, so the borrow checker forbids
257    /// pulling the next event while one is still held.  Consume each
258    /// event (copy out what you need) before requesting the next —
259    /// the same zero-copy contract as [`XmlBytesReader::next`].
260    ///
261    /// Yields [`BytesEvent::Eof`] once the document is exhausted; a
262    /// well-formedness violation surfaces as `Err`.
263    pub fn next_event(&mut self) -> Result<BytesEvent<'_, '_>> {
264        // Pre-fill *between* events so the inner reader has at least
265        // `buffer_size` bytes ahead of its cursor before we call
266        // `next()` — it then never sees a mid-token EOF unless EOF is
267        // real.  `ensure_runway` is the only place `buf` moves; it
268        // completes (releasing its borrow) before the event below is
269        // created, and the event's lifetime is bounded by `&mut self`,
270        // so no refill can run while the event is alive.
271        self.ensure_runway()?;
272        self.reader.next()
273    }
274
275    /// Drive the parser to EOF.  Returns `Ok(())` if every event up
276    /// through [`BytesEvent::Eof`] parses without error.  This is the
277    /// CLI's `lint` workload — we don't need the events themselves,
278    /// just the well-formedness verdict.
279    pub fn validate(mut self) -> Result<()> {
280        while !matches!(self.next_event()?, BytesEvent::Eof) {}
281        Ok(())
282    }
283
284    /// Refill / compact / grow the buffer as needed so the inner
285    /// reader has at least `buffer_size` bytes ahead of its cursor
286    /// (or true EOF has been reached).  Called between events; safe
287    /// only when the scanner is not mid-token, which is guaranteed
288    /// at event boundaries.
289    fn ensure_runway(&mut self) -> Result<()> {
290        // Bytes the scanner could still consume from the current
291        // window without needing more from `inner`.  `src_offset`
292        // == `cur_pos` between events (no entity stream is active),
293        // which is the only time we call this.
294        let cur_pos = self.reader.src_offset();
295        let runway = self.buf.len() - cur_pos;
296        // Refill if runway drops below the user-set max-token cap.
297        // Equality is fine — we have exactly enough for any token
298        // up to the cap, and the next event will likely consume
299        // less than the full cap.
300        if runway >= self.buffer_size || self.eof {
301            return Ok(());
302        }
303
304        // Compact: drop bytes the scanner has already passed.
305        if cur_pos > 0 {
306            self.buf.drain(..cur_pos);
307        }
308
309        // Internal target capacity = buffer_size * BUF_CAPACITY_MULTIPLE.
310        // Grow up to this if not already there; never exceed it.
311        let target_cap = self.buffer_size.saturating_mul(BUF_CAPACITY_MULTIPLE);
312        if self.buf.capacity() < target_cap {
313            let additional = target_cap - self.buf.len();
314            self.buf.reserve(additional);
315        }
316
317        // Pull more bytes until buf is full to target_cap or the
318        // inner reader is dry.
319        while self.buf.len() < target_cap && !self.eof {
320            let space = target_cap - self.buf.len();
321            let n = read_chunk(&mut self.inner, &mut self.buf, space)?;
322            if n == 0 {
323                self.eof = true;
324                break;
325            }
326        }
327
328        // Re-point the scanner: buf may have moved (reserve), and
329        // cur_pos is now 0 (we drained the consumed prefix).
330        // SAFETY: buf outlives reader; UTF-8 invariant preserved
331        // because we only ever append bytes that came from inner —
332        // see `read_chunk` for the per-chunk UTF-8 boundary check.
333        unsafe {
334            self.reader.rebind_scanner(self.buf.as_ptr(), self.buf.len(), 0);
335        }
336        Ok(())
337    }
338}
339
340/// Read up to `wanted` bytes into `buf` (extending it).  Returns
341/// the number of bytes actually read.  Handles short reads from the
342/// underlying source by looping; returns 0 only on real EOF.
343fn read_into_vec<R: Read>(reader: &mut R, buf: &mut Vec<u8>, wanted: usize) -> Result<usize> {
344    let start = buf.len();
345    let target = start + wanted;
346    buf.resize(target, 0);
347    let mut filled = start;
348    while filled < target {
349        let n = reader.read(&mut buf[filled..target]).map_err(io_to_xml_err)?;
350        if n == 0 { break; }
351        filled += n;
352    }
353    buf.truncate(filled);
354    Ok(filled - start)
355}
356
357/// Read one chunk (up to `space` bytes) into the tail of `buf`.
358/// Returns the number of bytes appended.  Validates that what we
359/// appended doesn't introduce a UTF-8 boundary violation — but the
360/// last few bytes may be the start of a multi-byte sequence whose
361/// continuation hasn't arrived yet, which is fine: the next chunk
362/// completes it.
363fn read_chunk<R: Read>(reader: &mut R, buf: &mut Vec<u8>, space: usize) -> Result<usize> {
364    let start = buf.len();
365    buf.resize(start + space, 0);
366    let n = reader.read(&mut buf[start..]).map_err(io_to_xml_err)?;
367    buf.truncate(start + n);
368    Ok(n)
369}
370
371fn io_to_xml_err(e: std::io::Error) -> XmlError {
372    XmlError::new(
373        ErrorDomain::Parser,
374        ErrorLevel::Fatal,
375        format!("streaming reader I/O error: {e}"),
376    )
377}
378
379/// Detect the byte-order mark of an encoding the streaming reader
380/// doesn't support in v1.  Returns the encoding name for the error
381/// message, or `None` for "looks like UTF-8 (with or without BOM)".
382fn sniff_non_utf8_bom(buf: &[u8]) -> Option<&'static str> {
383    if buf.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) { return Some("UTF-32 LE"); }
384    if buf.starts_with(&[0x00, 0x00, 0xFE, 0xFF]) { return Some("UTF-32 BE"); }
385    if buf.starts_with(&[0xFF, 0xFE]) { return Some("UTF-16 LE"); }
386    if buf.starts_with(&[0xFE, 0xFF]) { return Some("UTF-16 BE"); }
387    None
388}
389
390// ── tests ───────────────────────────────────────────────────────────────────
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use std::io::Cursor;
396
397    fn validate(bytes: &[u8]) -> Result<()> {
398        XmlByteStreamReader::new(Cursor::new(bytes.to_vec()), DEFAULT_BUFFER_SIZE)?
399            .validate()
400    }
401
402    fn validate_with_buffer(bytes: &[u8], buffer_size: usize) -> Result<()> {
403        XmlByteStreamReader::new(Cursor::new(bytes.to_vec()), buffer_size)?
404            .validate()
405    }
406
407    #[test]
408    fn accepts_well_formed_xml() {
409        assert!(validate(b"<r/>").is_ok());
410        assert!(validate(b"<?xml version=\"1.0\"?><r><a/><b>text</b></r>").is_ok());
411    }
412
413    #[test]
414    fn rejects_malformed_xml() {
415        assert!(validate(b"<a><b></a>").is_err());
416        assert!(validate(b"<unclosed>").is_err());
417        assert!(validate(b"text without root").is_err());
418    }
419
420    #[test]
421    fn handles_tiny_buffer_with_many_refills() {
422        // 1 KiB buffer parsing a doc much larger than the buffer —
423        // exercises the refill/compact/rebind loop heavily.
424        let mut doc = String::from("<root>");
425        for i in 0..200 {
426            doc.push_str(&format!("<item id=\"{i}\">value{i}</item>"));
427        }
428        doc.push_str("</root>");
429        assert!(validate_with_buffer(doc.as_bytes(), 1024).is_ok());
430    }
431
432    #[test]
433    fn text_content_larger_than_buffer_splits_across_events() {
434        // Text content is not atomic — the scanner emits it in
435        // chunks bounded by the current buffer window.  So a 10 KB
436        // text node with a 1 KiB buffer just produces several text
437        // events back-to-back; the parse succeeds.  Document this
438        // behavior: only ATOMIC tokens (element names, attribute
439        // values, comments, PIs) are bounded by buffer_size.
440        let big_text = "x".repeat(10_000);
441        let doc = format!("<r>{big_text}</r>");
442        assert!(validate_with_buffer(doc.as_bytes(), 1024).is_ok());
443    }
444
445    #[test]
446    fn errors_on_element_name_larger_than_buffer() {
447        // Element names ARE atomic — must be parsed in one go.  A
448        // name bigger than the internal buffer (buffer_size *
449        // BUF_CAPACITY_MULTIPLE) can't fit and the parse fails.
450        // We use buffer_size 1024, so the internal cap is 2048;
451        // a 4000-byte name exceeds it.
452        let big_name = "a".repeat(4000);
453        let doc = format!("<{big_name}/>");
454        let result = validate_with_buffer(doc.as_bytes(), 1024);
455        assert!(result.is_err(), "expected error on huge name, got Ok");
456    }
457
458    #[test]
459    fn accepts_text_at_buffer_size_boundary() {
460        // Text under buffer size still succeeds via a single event.
461        let text = "x".repeat(8000);
462        let doc = format!("<r>{text}</r>");
463        assert!(validate_with_buffer(doc.as_bytes(), 16 * 1024).is_ok());
464    }
465
466    #[test]
467    fn rejects_utf16_le_bom_with_clear_error() {
468        let doc = vec![0xFF, 0xFE, 0x3C, 0x00, 0x72, 0x00, 0x2F, 0x00, 0x3E, 0x00];
469        let err = validate(&doc).unwrap_err();
470        assert!(err.message.contains("UTF-16 LE"), "got: {}", err.message);
471        assert!(err.message.contains("streaming"), "got: {}", err.message);
472    }
473
474    #[test]
475    fn rejects_utf16_be_bom_with_clear_error() {
476        let doc = vec![0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x72, 0x00, 0x2F, 0x00, 0x3E];
477        let err = validate(&doc).unwrap_err();
478        assert!(err.message.contains("UTF-16 BE"), "got: {}", err.message);
479    }
480
481    #[test]
482    fn strips_utf8_bom_silently() {
483        // UTF-8 BOM is allowed and should not affect the parse.
484        let mut doc = vec![0xEF, 0xBB, 0xBF];
485        doc.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><r/>");
486        assert!(validate(&doc).is_ok());
487    }
488
489    #[test]
490    fn size_hint_pre_allocates_to_match() {
491        // With a 64-byte doc and a 1 MiB buffer cap, the size hint
492        // should cause us to allocate exactly 64 bytes (not the cap).
493        let doc = b"<r><a>hi</a></r>".to_vec();
494        let r = XmlByteStreamReader::with_size_hint(
495            Cursor::new(doc.clone()),
496            Some(doc.len()),
497            1024 * 1024,
498        ).unwrap();
499        // Capacity may grow slightly past hint due to Vec internals
500        // but should be much less than the 1 MiB cap.
501        assert!(r.buf.capacity() < 1024 * 1024 / 4,
502            "expected capacity ~{} bytes, got {}", doc.len(), r.buf.capacity());
503        // Result still validates.
504        assert!(r.validate().is_ok());
505    }
506
507    #[test]
508    fn empty_input_errors_cleanly() {
509        // No root element — should be a parse error, not a panic.
510        let result = validate(b"");
511        assert!(result.is_err());
512    }
513
514    #[test]
515    fn handles_text_split_across_refills() {
516        // A 4 KiB text node parsed with a 2 KiB buffer that has to
517        // refill mid-text.  But — the entire text must still fit in
518        // a single buffer's worth (since text tokens can't cross
519        // refill).  So with a 2 KiB buffer, 1 KiB of text is the
520        // safe regime.  This is the documented limit.
521        let text = "x".repeat(1000);
522        let doc = format!("<r>{text}</r>");
523        assert!(validate_with_buffer(doc.as_bytes(), 2048).is_ok());
524    }
525
526    #[test]
527    fn many_small_events_with_small_buffer() {
528        // Many small elements — each is well under buffer size, but
529        // the document overall is bigger than the buffer.  Refills
530        // happen between events and everything should validate.
531        let mut doc = String::from("<r>");
532        for _ in 0..1000 {
533            doc.push_str("<x/>");
534        }
535        doc.push_str("</r>");
536        let r = validate_with_buffer(doc.as_bytes(), 512);
537        if let Err(e) = &r {
538            panic!("expected ok; got error: {} (line={:?}, col={:?})",
539                   e.message, e.line, e.column);
540        }
541    }
542
543    // ── next_event: streaming data extraction ──────────────────────────
544
545    #[test]
546    fn next_event_extracts_data_with_small_buffer() {
547        // A document far larger than the buffer, pulled one event at a
548        // time.  We collect the text of every <item> and confirm we
549        // recover all of it — proving streaming *data extraction*
550        // (not just validation) works in bounded memory.
551        const N: usize = 500;
552        let mut doc = String::from("<root>");
553        for i in 0..N {
554            doc.push_str(&format!("<item>value{i}</item>"));
555        }
556        doc.push_str("</root>");
557
558        let mut r = XmlByteStreamReader::new(Cursor::new(doc.into_bytes()), 512).unwrap();
559        let mut values = Vec::new();
560        loop {
561            let eof = {
562                match r.next_event().unwrap() {
563                    BytesEvent::Eof => true,
564                    BytesEvent::Text(t) => {
565                        let s = std::str::from_utf8(t.as_bytes()).unwrap();
566                        if !s.trim().is_empty() {
567                            values.push(s.to_string());
568                        }
569                        false
570                    }
571                    _ => false,
572                }
573            };
574            if eof { break; }
575        }
576
577        assert_eq!(values.len(), N, "should recover every item's text");
578        assert_eq!(values[0], "value0");
579        assert_eq!(values[N - 1], format!("value{}", N - 1));
580    }
581
582    #[test]
583    fn next_event_keeps_memory_bounded() {
584        // The whole point: peak buffer size stays bounded by
585        // buffer_size * BUF_CAPACITY_MULTIPLE no matter how large the
586        // document is.  Drive a doc many times the buffer and assert
587        // the buffer never exceeds the cap after any event.
588        let buffer_size = 1024;
589        let cap = buffer_size * BUF_CAPACITY_MULTIPLE;
590        let mut doc = String::from("<root>");
591        for i in 0..5000 {
592            doc.push_str(&format!("<item id=\"{i}\">value{i}</item>"));
593        }
594        doc.push_str("</root>");
595        assert!(doc.len() > cap * 10, "doc must dwarf the buffer for the test to mean anything");
596
597        let mut r = XmlByteStreamReader::new(Cursor::new(doc.into_bytes()), buffer_size).unwrap();
598        loop {
599            let eof = matches!(r.next_event().unwrap(), BytesEvent::Eof);
600            // Event dropped at the `;` above, so `r` is no longer
601            // borrowed and we can inspect the buffer.
602            assert!(r.buf.len() <= cap,
603                "buffer grew to {} bytes, exceeding cap {cap}", r.buf.len());
604            if eof { break; }
605        }
606    }
607
608    #[test]
609    fn next_event_surfaces_wellformedness_errors() {
610        let mut r = XmlByteStreamReader::new(Cursor::new(b"<a></b>".to_vec()), 1024).unwrap();
611        let mut got_err = false;
612        for _ in 0..10 {
613            match r.next_event() {
614                Ok(BytesEvent::Eof) => break,
615                Ok(_) => {}
616                Err(_) => { got_err = true; break; }
617            }
618        }
619        assert!(got_err, "mismatched end tag should surface as Err from next_event");
620    }
621}