sup_xml_core/iterparse.rs
1//! Streaming-iterator wrapper for the SAX reader.
2//!
3//! The lower-level [`crate::XmlReader`] / [`crate::XmlBytesReader`]
4//! deliver events whose borrows tie back to the reader's source
5//! buffer — that's the right shape for zero-copy consumers, but it
6//! prevents the reader from implementing `Iterator` directly
7//! (each `next()` would borrow `&mut self`, so callers can hold at
8//! most one event at a time).
9//!
10//! [`Iterparse`] addresses the common case where the caller wants
11//! an actual `Iterator` and is willing to pay one `String`
12//! allocation per event for owned names and text. Events arrive
13//! alongside the current ancestor path so handlers don't need to
14//! maintain their own depth tracker.
15//!
16//! # Quick example
17//!
18//! ```no_run
19//! use sup_xml_core::iterparse::{Iterparse, IterEvent};
20//!
21//! let xml = b"<catalog><book id='1'/><book id='2'/></catalog>";
22//! for ev in Iterparse::from_bytes(xml).unwrap() {
23//! let ev = ev.unwrap();
24//! if let IterEvent::EndElement { name, path, .. } = ev {
25//! if name == "book" { println!("done with {}", path); }
26//! }
27//! }
28//! ```
29//!
30//! For consumers that need byte-exact zero-copy events (every
31//! payload borrowed from the source buffer), reach for
32//! [`crate::XmlBytesReader`] directly.
33
34use std::collections::HashMap;
35
36use crate::error::XmlError;
37use crate::reader::{Attr, Event, XmlReader};
38use crate::options::ParseOptions;
39
40/// One streamed event. Names, attribute values, and text content
41/// are owned `String`s so the iterator can yield items
42/// independent of the reader's source buffer lifetime.
43#[derive(Debug, Clone)]
44pub enum IterEvent {
45 /// Opening (or empty-element) start tag. Attributes are
46 /// pre-collected; `path` is the current ancestor chain
47 /// *including* this element.
48 StartElement {
49 name: String,
50 attrs: Vec<(String, String)>,
51 path: String,
52 depth: usize,
53 },
54 /// Closing tag — emitted once for each `StartElement`,
55 /// including for empty (`<br/>`) elements. `path` is the
56 /// chain *up to* this element (with the element itself as
57 /// the last segment).
58 EndElement {
59 name: String,
60 path: String,
61 depth: usize,
62 },
63 /// Character data between tags.
64 Text {
65 content: String,
66 path: String,
67 depth: usize,
68 },
69 /// CDATA section.
70 CData {
71 content: String,
72 path: String,
73 depth: usize,
74 },
75 /// Comment (no payload exposed by the path — comments don't
76 /// add a path segment).
77 Comment {
78 content: String,
79 depth: usize,
80 },
81 /// Processing instruction.
82 Pi {
83 target: String,
84 data: String,
85 depth: usize,
86 },
87}
88
89/// Owns an [`XmlReader`] plus the bookkeeping needed to produce
90/// owned iterator items: a path stack, per-frame sibling counters
91/// (so `book[2]` etc. show up in the path), and an attribute
92/// scratch buffer.
93pub struct Iterparse<'src> {
94 reader: XmlReader<'src>,
95 /// Element names on the open-element stack (only Element
96 /// frames; comments / PIs don't push).
97 stack: Vec<StackFrame>,
98 /// Indicates whether the EOF event has been delivered; the
99 /// iterator returns `None` after that.
100 done: bool,
101}
102
103struct StackFrame {
104 name: String,
105 /// Sibling-index counter for child element names, used to build
106 /// `/a/b[2]` style paths.
107 children: HashMap<String, u32>,
108}
109
110impl<'src> Iterparse<'src> {
111 /// Construct from a borrowed byte slice. Defaults to
112 /// [`ParseOptions::default()`] — well-formedness checks on,
113 /// entities resolved, no external loading.
114 pub fn from_bytes(bytes: &'src [u8]) -> Result<Self, XmlError> {
115 let reader = XmlReader::from_bytes(bytes)?;
116 Ok(Self { reader, stack: Vec::new(), done: false })
117 }
118
119 /// Like [`Iterparse::from_bytes`] but consults the supplied
120 /// `ParseOptions` (e.g. `recovery_mode: true` for tolerant
121 /// parsing, or a configured `external_resolver`).
122 pub fn from_bytes_with(bytes: &'src [u8], opts: &ParseOptions) -> Result<Self, XmlError> {
123 let reader = XmlReader::from_bytes(bytes)?.with_options(opts.clone());
124 Ok(Self { reader, stack: Vec::new(), done: false })
125 }
126
127 /// Build the path string from the current open-element stack.
128 /// `/a/b[2]/c` style; sibling indices are emitted only when a
129 /// name repeats so single-occurrence paths stay readable.
130 fn current_path(&self) -> String {
131 if self.stack.is_empty() { return "/".into(); }
132 let mut s = String::new();
133 // Each frame's `children` map counts *its children*, not
134 // its own siblings. To emit `name[n]` for the frame
135 // itself we'd need the parent's counter snapshot at the
136 // moment this frame was pushed — which we stash on the
137 // frame. For simplicity here we re-derive from the
138 // counters: if parent's count for this name is >= 2, we
139 // assume this is the latest of a series. Good enough for
140 // diagnostic paths; the worst that happens on a
141 // mis-classification is a slightly-stale index.
142 let mut prev_children: Option<&HashMap<String, u32>> = None;
143 for f in &self.stack {
144 s.push('/');
145 s.push_str(&f.name);
146 if let Some(counters) = prev_children {
147 if let Some(&n) = counters.get(&f.name) {
148 if n >= 2 {
149 s.push_str(&format!("[{n}]"));
150 }
151 }
152 }
153 prev_children = Some(&f.children);
154 }
155 s
156 }
157}
158
159impl<'src> Iterator for Iterparse<'src> {
160 type Item = Result<IterEvent, XmlError>;
161
162 fn next(&mut self) -> Option<Self::Item> {
163 if self.done { return None; }
164 let depth = self.stack.len();
165 let ev = match self.reader.next() {
166 Ok(ev) => ev,
167 Err(e) => {
168 self.done = true;
169 return Some(Err(e));
170 }
171 };
172 match ev {
173 Event::Eof => {
174 self.done = true;
175 None
176 }
177 Event::StartElement(tag) => {
178 let name = tag.name().to_string();
179 let attrs: Vec<(String, String)> = tag.attrs()
180 .filter_map(|a: Result<Attr<'_>, _>| a.ok())
181 .map(|a| (a.name().to_string(), a.value().to_string()))
182 .collect();
183 // Bump parent's per-name counter so siblings show
184 // up as `name[2]`, `name[3]` etc. in `path`.
185 if let Some(parent) = self.stack.last_mut() {
186 *parent.children.entry(name.clone()).or_insert(0) += 1;
187 }
188 self.stack.push(StackFrame {
189 name: name.clone(),
190 children: HashMap::new(),
191 });
192 let path = self.current_path();
193 let new_depth = self.stack.len();
194 Some(Ok(IterEvent::StartElement {
195 name, attrs, path, depth: new_depth,
196 }))
197 }
198 Event::EndElement(end) => {
199 let path = self.current_path();
200 let name = end.name().to_string();
201 self.stack.pop();
202 Some(Ok(IterEvent::EndElement { name, path, depth }))
203 }
204 Event::Text(t) => {
205 let path = self.current_path();
206 Some(Ok(IterEvent::Text {
207 content: t.as_str().to_string(),
208 path, depth,
209 }))
210 }
211 Event::CData(c) => {
212 let path = self.current_path();
213 Some(Ok(IterEvent::CData {
214 content: c.as_str().to_string(),
215 path, depth,
216 }))
217 }
218 Event::Comment(c) => Some(Ok(IterEvent::Comment {
219 content: c.as_str().to_string(),
220 depth,
221 })),
222 Event::Pi(p) => Some(Ok(IterEvent::Pi {
223 target: p.target().to_string(),
224 data: p.content().to_string(),
225 depth,
226 })),
227 Event::EntityRef(_) => {
228 // Skip — typed streaming consumers don't get a
229 // meaningful payload from a bare reference, and
230 // the default `resolve_entities: true` path
231 // already inlines them as text. Recurse.
232 self.next()
233 }
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn collect(xml: &[u8]) -> Vec<IterEvent> {
243 Iterparse::from_bytes(xml).unwrap()
244 .map(|r| r.unwrap())
245 .collect()
246 }
247
248 #[test]
249 fn yields_start_end_text() {
250 let events = collect(b"<r>hi</r>");
251 assert_eq!(events.len(), 3);
252 assert!(matches!(&events[0],
253 IterEvent::StartElement { name, .. } if name == "r"));
254 assert!(matches!(&events[1],
255 IterEvent::Text { content, .. } if content == "hi"));
256 assert!(matches!(&events[2],
257 IterEvent::EndElement { name, .. } if name == "r"));
258 }
259
260 #[test]
261 fn path_includes_sibling_index() {
262 let events = collect(
263 b"<r><a/><a><b/></a><a/></r>",
264 );
265 // Find the EndElement of the middle <a> — should be
266 // at path /r/a[2] (it's the 2nd <a> child of <r>).
267 let mid_end = events.iter().find_map(|e| match e {
268 IterEvent::EndElement { name, path, .. }
269 if name == "a" && path.contains("[2]") => Some(path.clone()),
270 _ => None,
271 });
272 assert_eq!(mid_end.as_deref(), Some("/r/a[2]"));
273 }
274
275 #[test]
276 fn captures_attrs() {
277 let events = collect(br#"<r a="1" b="2"/>"#);
278 match &events[0] {
279 IterEvent::StartElement { attrs, .. } => {
280 assert_eq!(attrs.len(), 2);
281 assert_eq!(attrs[0], ("a".into(), "1".into()));
282 assert_eq!(attrs[1], ("b".into(), "2".into()));
283 }
284 _ => panic!(),
285 }
286 }
287
288 #[test]
289 fn malformed_input_surfaces_error_then_stops() {
290 let mut it = Iterparse::from_bytes(b"<r><unclosed>").unwrap();
291 let mut saw_err = false;
292 for ev in it.by_ref() {
293 if ev.is_err() { saw_err = true; break; }
294 }
295 assert!(saw_err);
296 assert!(it.next().is_none(), "iterator must stop after first error");
297 }
298
299 #[test]
300 fn comments_and_pis_dont_push_path() {
301 let events = collect(b"<r><!-- c --><?pi data?><a/></r>");
302 let a_start = events.iter().find_map(|e| match e {
303 IterEvent::StartElement { name, path, .. } if name == "a" => Some(path.clone()),
304 _ => None,
305 });
306 assert_eq!(a_start.as_deref(), Some("/r/a"));
307 }
308}