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}