Skip to main content

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}