use rustc_hash::{FxHashMap, FxHashSet};
use crate::error::Result;
use crate::ns_helpers::{
ns_err, validate_qname, validate_xmlns_decl, XML_NS_URI, XMLNS_NS_URI,
};
use crate::options::ParseOptions;
use crate::reader::{Attr, EventInto, XmlReader};
use sup_xml_tree::dom::{Document, DocumentBuilder, Namespace, Node};
pub struct StreamParser<'src> {
reader: XmlReader<'src>,
attr_buf: Vec<Attr<'src>>,
emit: EmitMode,
state: State,
}
pub type EmitPredicate = Box<dyn Fn(&[String]) -> bool + Send + Sync + 'static>;
enum EmitMode {
None,
Depth(u32),
Path(Vec<String>),
When(EmitPredicate),
}
struct State {
sax_depth: u32,
name_stack: Vec<String>,
ns_bindings: Vec<(Option<String>, String)>,
ns_frames: Vec<usize>,
current: Option<Emission>,
}
impl Default for State {
fn default() -> Self {
Self {
sax_depth: 0,
name_stack: Vec::new(),
ns_bindings: vec![
(Some("xml".to_string()), XML_NS_URI.to_string()),
(Some("xmlns".to_string()), XMLNS_NS_URI.to_string()),
],
ns_frames: Vec::new(),
current: None,
}
}
}
struct Emission {
builder: DocumentBuilder,
start_depth: u32,
stack: Vec<*const ()>,
ns_cache: FxHashMap<(Option<String>, String), *const ()>,
}
#[inline] fn erase(node: &Node<'_>) -> *const () {
node as *const Node<'_> as *const ()
}
#[inline] unsafe fn unerase<'a>(p: *const ()) -> &'a Node<'a> {
unsafe { &*(p as *const Node<'a>) }
}
impl<'src> StreamParser<'src> {
pub fn from_str(input: &'src str) -> Self {
Self {
reader: XmlReader::from_str(input),
attr_buf: Vec::new(),
emit: EmitMode::None,
state: State::default(),
}
}
pub fn from_bytes(input: &'src [u8]) -> Result<Self> {
Ok(Self {
reader: XmlReader::from_bytes(input)?,
attr_buf: Vec::new(),
emit: EmitMode::None,
state: State::default(),
})
}
pub fn with_options(mut self, opts: &ParseOptions) -> Self {
self.reader = self.reader.with_options(opts.clone());
self
}
pub fn emit_at_depth(mut self, depth: u32) -> Self {
self.emit = EmitMode::Depth(depth);
self
}
pub fn emit_at_path(mut self, path: &[&str]) -> Self {
self.emit = EmitMode::Path(path.iter().map(|s| (*s).to_owned()).collect());
self
}
pub fn emit_when<F>(mut self, predicate: F) -> Self
where
F: Fn(&[String]) -> bool + Send + Sync + 'static,
{
self.emit = EmitMode::When(Box::new(predicate));
self
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<Document>> {
loop {
let event = self.reader.next_into(&mut self.attr_buf)?;
match event {
EventInto::StartElement { name } => {
let name = name.into_owned();
self.state.sax_depth += 1;
self.state.name_stack.push(name.clone());
validate_qname(&name, "element")?;
self.state.ns_frames.push(self.state.ns_bindings.len());
let attrs: Vec<(String, String)> = self.attr_buf.drain(..)
.map(|a| (a.name.to_owned(), a.value.into_owned()))
.collect();
for (an, av) in &attrs {
if an == "xmlns" {
self.state.ns_bindings.push((None, av.clone()));
} else if let Some(local) = an.strip_prefix("xmlns:") {
validate_xmlns_decl(local, av)?;
if local == "xml" { continue; } self.state.ns_bindings.push((Some(local.to_string()), av.clone()));
}
}
let starts_emission = self.state.current.is_none() && self.element_matches();
if starts_emission {
let mut emis = Emission {
builder: DocumentBuilder::new(),
start_depth: self.state.sax_depth,
stack: Vec::new(),
ns_cache: FxHashMap::default(),
};
let el_ptr = build_element_with_ns(
&mut emis, &name, &attrs, &self.state.ns_bindings,
)?;
emis.stack.push(el_ptr);
self.state.current = Some(emis);
} else if let Some(emis) = self.state.current.as_mut() {
let el_ptr = build_element_with_ns(
emis, &name, &attrs, &self.state.ns_bindings,
)?;
let parent: &Node<'_> = unsafe { unerase(*emis.stack.last().unwrap()) };
let el: &Node<'_> = unsafe { unerase(el_ptr) };
emis.builder.append_child(parent, el);
emis.stack.push(el_ptr);
} else {
validate_element_ns(&name, &attrs, &self.state.ns_bindings)?;
}
}
EventInto::EndElement { name: _ } => {
self.state.name_stack.pop();
if let Some(frame_start) = self.state.ns_frames.pop() {
self.state.ns_bindings.truncate(frame_start);
}
let closed_depth = self.state.sax_depth;
self.state.sax_depth -= 1;
if let Some(emis) = self.state.current.as_mut() {
if emis.start_depth == closed_depth {
let emis = self.state.current.take().unwrap();
let root: &Node<'_> = unsafe { unerase(emis.stack[0]) };
emis.builder.set_root(root);
return Ok(Some(emis.builder.build()));
} else {
emis.stack.pop();
}
}
}
EventInto::Text(t) => {
if let Some(emis) = self.state.current.as_mut() {
let parent_ptr = *emis.stack.last().unwrap();
let s = emis.builder.alloc_str(&t);
let node = emis.builder.new_text(s);
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
emis.builder.append_child(parent, node);
}
}
EventInto::CData(t) => {
if let Some(emis) = self.state.current.as_mut() {
let parent_ptr = *emis.stack.last().unwrap();
let s = emis.builder.alloc_str(&t);
let node = emis.builder.new_cdata(s);
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
emis.builder.append_child(parent, node);
}
}
EventInto::Comment(t) => {
if let Some(emis) = self.state.current.as_mut() {
let parent_ptr = *emis.stack.last().unwrap();
let s = emis.builder.alloc_str(&t);
let node = emis.builder.new_comment(s);
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
emis.builder.append_child(parent, node);
}
}
EventInto::Pi { target, content } => {
if let Some(emis) = self.state.current.as_mut() {
let parent_ptr = *emis.stack.last().unwrap();
let t = emis.builder.alloc_str(&target);
let c = if content.is_empty() { None } else { Some(&*emis.builder.alloc_str(&content)) };
let node = emis.builder.new_pi(t, c);
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
emis.builder.append_child(parent, node);
}
}
EventInto::EntityRef { name } => {
if let Some(emis) = self.state.current.as_mut() {
let parent_ptr = *emis.stack.last().unwrap();
let n = emis.builder.alloc_str(&name);
let lit = format!("&{name};");
let c = emis.builder.alloc_str(&lit);
let node = emis.builder.new_entity_ref(n, c);
let parent: &Node<'_> = unsafe { unerase(parent_ptr) };
emis.builder.append_child(parent, node);
}
}
EventInto::Eof => return Ok(None),
}
}
}
fn element_matches(&self) -> bool {
match &self.emit {
EmitMode::None => false,
EmitMode::Depth(d) => self.state.sax_depth.saturating_sub(1) == *d,
EmitMode::Path(path) => path.len() == self.state.name_stack.len()
&& self.state.name_stack.iter().zip(path.iter())
.all(|(a, b)| a == b),
EmitMode::When(pred) => pred(&self.state.name_stack),
}
}
}
fn build_element_with_ns(
emis: &mut Emission,
name: &str,
attrs: &[(String, String)],
global_ns_bindings: &[(Option<String>, String)],
) -> Result<*const ()> {
let Emission { builder, ns_cache, .. } = emis;
let aname = builder.alloc_str(name);
let el = builder.new_element(aname);
if let Some((p, h)) = resolve_qname(name, global_ns_bindings, false)? {
let ns_ptr = lookup_or_alloc_ns(builder, ns_cache, p, h);
let ns: &Namespace<'_> = unsafe { &*(ns_ptr as *const Namespace<'_>) };
el.namespace.set(Some(ns));
}
let mut seen: FxHashSet<(String, String)> = FxHashSet::default();
for (an, av) in attrs {
let aname = builder.alloc_str(an);
let avalue = builder.alloc_str(av);
let attr = builder.new_attribute(aname, avalue);
builder.append_attribute(el, attr);
if an == "xmlns" || an.starts_with("xmlns:") {
continue;
}
validate_qname(an, "attribute")?;
if let Some((p, h)) = resolve_qname(an, global_ns_bindings, true)? {
let ns_ptr = lookup_or_alloc_ns(builder, ns_cache, p, h);
let ns: &Namespace<'_> = unsafe { &*(ns_ptr as *const Namespace<'_>) };
attr.namespace.set(Some(ns));
}
let ns_uri = attr.namespace.get().map(|n| n.href()).unwrap_or("");
let local = an.rfind(':').map(|i| &an[i+1..]).unwrap_or(an);
if !seen.insert((ns_uri.to_string(), local.to_string())) {
return Err(ns_err(format!(
"duplicate attribute '{local}' in namespace '{ns_uri}' after namespace expansion"
)));
}
}
Ok(erase(el))
}
fn lookup_or_alloc_ns(
builder: &DocumentBuilder,
ns_cache: &mut FxHashMap<(Option<String>, String), *const ()>,
prefix: Option<&str>,
href: &str,
) -> *const () {
let key = (prefix.map(str::to_owned), href.to_owned());
if let Some(&ptr) = ns_cache.get(&key) {
return ptr;
}
let pref_arena = prefix.map(|p| builder.alloc_str(p));
let href_arena = builder.alloc_str(href);
let ns = builder.new_namespace(pref_arena, href_arena);
let ptr = ns as *const Namespace<'_> as *const ();
ns_cache.insert(key, ptr);
ptr
}
fn resolve_qname<'a>(
qname: &'a str,
bindings: &'a [(Option<String>, String)],
is_attribute: bool,
) -> Result<Option<(Option<&'a str>, &'a str)>> {
if let Some(colon) = qname.find(':') {
let prefix = &qname[..colon];
match lookup_prefix(Some(prefix), bindings) {
Some(href) => Ok(Some((Some(prefix), href))),
None => Err(ns_err(format!("undeclared namespace prefix '{prefix}' in '{qname}'"))),
}
} else if is_attribute {
Ok(None)
} else {
match lookup_prefix(None, bindings) {
Some(href) if !href.is_empty() => Ok(Some((None, href))),
_ => Ok(None),
}
}
}
fn lookup_prefix<'a>(
prefix: Option<&str>,
bindings: &'a [(Option<String>, String)],
) -> Option<&'a str> {
for (p, h) in bindings.iter().rev() {
if p.as_deref() == prefix {
return Some(h);
}
}
None
}
fn validate_element_ns(
name: &str,
attrs: &[(String, String)],
bindings: &[(Option<String>, String)],
) -> Result<()> {
validate_qname(name, "element")?;
let _ = resolve_qname(name, bindings, false)?;
for (an, _av) in attrs {
if an == "xmlns" || an.starts_with("xmlns:") { continue; }
validate_qname(an, "attribute")?;
let _ = resolve_qname(an, bindings, true)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use sup_xml_tree::dom::NodeKind;
fn collect_names(mut sp: StreamParser) -> Vec<String> {
let mut out = Vec::new();
while let Some(doc) = sp.next().unwrap() {
out.push(doc.root().name().to_string());
}
out
}
#[test]
fn depth_one_matches_children_of_root() {
let xml = "<r><a/><b/><c/></r>";
let sp = StreamParser::from_str(xml).emit_at_depth(1);
assert_eq!(collect_names(sp), vec!["a", "b", "c"]);
}
#[test]
fn depth_zero_emits_root() {
let xml = "<root><a/><b/></root>";
let mut sp = StreamParser::from_str(xml).emit_at_depth(0);
let doc = sp.next().unwrap().expect("should yield root");
assert_eq!(doc.root().name(), "root");
assert_eq!(doc.root().children().count(), 2);
assert!(sp.next().unwrap().is_none());
}
#[test]
fn depth_two_matches_grandchildren() {
let xml = "<r><a><b/><b/></a><a><b/></a></r>";
let sp = StreamParser::from_str(xml).emit_at_depth(2);
assert_eq!(collect_names(sp), vec!["b", "b", "b"]);
}
#[test]
fn no_emit_mode_yields_nothing() {
let mut sp = StreamParser::from_str("<r><a/></r>");
assert!(sp.next().unwrap().is_none());
}
#[test]
fn path_root_anchored() {
let xml = "<rss><channel>\
<item><id>1</id></item>\
<item><id>2</id><item><id>2a</id></item></item>\
</channel></rss>";
let sp = StreamParser::from_str(xml)
.emit_at_path(&["rss", "channel", "item"]);
assert_eq!(collect_names(sp), vec!["item", "item"]);
}
#[test]
fn path_wrong_root_yields_nothing() {
let xml = "<feed><channel><item/></channel></feed>";
let mut sp = StreamParser::from_str(xml)
.emit_at_path(&["rss", "channel", "item"]);
assert!(sp.next().unwrap().is_none());
}
#[test]
fn path_single_element_emits_root() {
let xml = "<root><a/></root>";
let mut sp = StreamParser::from_str(xml).emit_at_path(&["root"]);
let doc = sp.next().unwrap().unwrap();
assert_eq!(doc.root().name(), "root");
assert!(sp.next().unwrap().is_none());
}
#[test]
fn predicate_matches_by_name_across_nesting_levels() {
let xml = r#"<root>
<section><item>one</item></section>
<wrapper><inner><item>two</item></inner></wrapper>
<thing/>
</root>"#;
let sp = StreamParser::from_str(xml)
.emit_when(|chain| chain.last().map(String::as_str) == Some("item"));
let names: Vec<String> = {
let mut sp = sp;
let mut out = Vec::new();
while let Some(d) = sp.next().unwrap() {
out.push(d.root().text_content().unwrap_or_default().to_string());
}
out
};
assert_eq!(names, vec!["one", "two"],
"predicate should pick both <item>s irrespective of depth");
}
#[test]
fn predicate_can_inspect_ancestor_chain() {
let xml = r#"<archive>
<library><book>L1</book></library>
<library><book>L2</book></library>
<library><book>L3<book>nested</book></book></library>
</archive>"#;
let sp = StreamParser::from_str(xml).emit_when(|chain| {
chain.len() >= 2
&& chain.last().map(String::as_str) == Some("book")
&& chain.get(chain.len() - 2).map(String::as_str) == Some("library")
});
let texts: Vec<String> = {
let mut sp = sp;
let mut out = Vec::new();
while let Some(d) = sp.next().unwrap() {
let t = d.root().text_content().unwrap_or_default().to_string();
out.push(t);
}
out
};
assert_eq!(texts.len(), 3,
"expected exactly the three immediate-child <book>s, got {texts:?}");
}
#[test]
fn predicate_emission_preserves_namespace_resolution() {
let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom">
<entry><title>t1</title></entry>
<entry><title>t2</title></entry>
</feed>"#;
let mut sp = StreamParser::from_str(xml)
.emit_when(|chain| chain.last().map(String::as_str) == Some("entry"));
let entry = sp.next().unwrap().expect("should yield entry");
let ns = entry.root().namespace.get()
.expect("entry must inherit feed's default namespace");
assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
}
#[test]
fn emitted_subtree_contains_children() {
let xml = "<r><item><title>X</title><body>hello</body></item></r>";
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let item = sp.next().unwrap().unwrap();
let root = item.root();
assert_eq!(root.name(), "item");
let title = root.find_child("title").unwrap();
assert_eq!(title.text_content(), Some("X"));
let body = root.find_child("body").unwrap();
assert_eq!(body.text_content(), Some("hello"));
}
#[test]
fn emitted_subtree_contains_attributes() {
let xml = r#"<r><page id="1" lang="en">x</page></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let page = sp.next().unwrap().unwrap();
let pairs: Vec<(&str, &str)> = page.root().attributes()
.map(|a| (a.name(), a.value())).collect();
assert_eq!(pairs, vec![("id", "1"), ("lang", "en")]);
}
#[test]
fn mixed_content_preserved() {
let xml = "<r><item>before<!-- c --><b>x</b><![CDATA[raw]]>after</item></r>";
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let item = sp.next().unwrap().unwrap();
let kinds: Vec<NodeKind> = item.root().children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![
NodeKind::Text, NodeKind::Comment, NodeKind::Element,
NodeKind::CData, NodeKind::Text,
]);
}
#[test]
fn entity_in_attr_and_text_expanded() {
let xml = r#"<r><x v="a&b">c<d</x></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let x = sp.next().unwrap().unwrap();
assert_eq!(x.root().attributes().next().unwrap().value(), "a&b");
assert_eq!(x.root().text_content(), Some("c<d"));
}
#[test]
fn many_emissions_independent_arenas() {
let mut xml = String::from("<r>");
for i in 0..50 { xml.push_str(&format!("<i>{i}</i>")); }
xml.push_str("</r>");
let mut sp = StreamParser::from_str(&xml).emit_at_depth(1);
let mut docs = Vec::new();
while let Some(d) = sp.next().unwrap() { docs.push(d); }
assert_eq!(docs.len(), 50);
for d in &docs {
assert!(d.memory_bytes() > 0);
assert!(d.memory_bytes() < 4096, "single-item arena should be small");
}
let removed = docs.remove(0);
drop(removed);
assert_eq!(docs[0].root().name(), "i");
}
#[test]
fn eof_then_none_idempotent() {
let mut sp = StreamParser::from_str("<r><a/></r>").emit_at_depth(1);
assert!(sp.next().unwrap().is_some());
assert!(sp.next().unwrap().is_none());
assert!(sp.next().unwrap().is_none());
}
#[test]
fn errors_propagate() {
let mut sp = StreamParser::from_str("<r><a></b></r>").emit_at_depth(1);
assert!(sp.next().is_err());
}
#[test]
fn prolog_events_are_silently_discarded() {
let xml = "<?xml version='1.0'?><!-- pre --><?stylesheet?><r><a/></r>";
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let a = sp.next().unwrap().unwrap();
assert_eq!(a.root().name(), "a");
assert!(sp.next().unwrap().is_none());
}
#[test]
fn ns_inherited_from_ancestor_above_emit_boundary() {
let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom"><entry><title>X</title></entry></feed>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let entry = sp.next().unwrap().unwrap();
let ns = entry.root().namespace.get().expect("entry must inherit feed's default ns");
assert!(ns.prefix.is_none());
assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
let title = entry.root().find_child("title").unwrap();
assert_eq!(title.namespace.get().unwrap().href(), "http://www.w3.org/2005/Atom");
}
#[test]
fn ns_prefix_inherited_from_ancestor() {
let xml = r#"<rss xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><dc:title>X</dc:title></channel></rss>"#;
let mut sp = StreamParser::from_str(xml)
.emit_at_path(&["rss", "channel"]);
let channel = sp.next().unwrap().unwrap();
let title = channel.root().find_child("dc:title").unwrap();
let ns = title.namespace.get().unwrap();
assert_eq!(ns.prefix(), Some("dc"));
assert_eq!(ns.href(), "http://purl.org/dc/elements/1.1/");
}
#[test]
fn ns_declared_inside_emission() {
let xml = r#"<feed><entry xmlns:atom="http://www.w3.org/2005/Atom" atom:rel="self"/></feed>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let entry = sp.next().unwrap().unwrap();
let rel = entry.root().attributes().find(|a| a.name() == "atom:rel").unwrap();
let ns = rel.namespace.get().unwrap();
assert_eq!(ns.href(), "http://www.w3.org/2005/Atom");
}
#[test]
fn ns_xml_prefix_resolved_inside_emission() {
let xml = r#"<r><item xml:lang="en"/></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let item = sp.next().unwrap().unwrap();
let lang = item.root().attributes().next().unwrap();
assert_eq!(lang.namespace.get().unwrap().href(),
"http://www.w3.org/XML/1998/namespace");
}
#[test]
fn ns_undeclared_prefix_above_emit_boundary_errors() {
let xml = r#"<r><foo:bad/><item/></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
assert!(sp.next().is_err());
}
#[test]
fn ns_undeclared_prefix_inside_emission_errors() {
let xml = r#"<r><item><bad:thing/></item></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
assert!(sp.next().is_err());
}
#[test]
fn ns_cache_dedups_repeated_prefix_uses() {
let single = r#"<r><item><a><x:c xmlns:x="http://x.com/"/></a></item></r>"#;
let many = r#"<r><item><a xmlns:x="http://x.com/"><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/><x:c/></a></item></r>"#;
let mut sp1 = StreamParser::from_str(single).emit_at_depth(1);
let mut sp2 = StreamParser::from_str(many).emit_at_depth(1);
let d1 = sp1.next().unwrap().unwrap();
let d2 = sp2.next().unwrap().unwrap();
let ratio = d2.memory_bytes() as f64 / d1.memory_bytes() as f64;
assert!(ratio < 4.0, "8× uses shouldn't bloat 8× — got ratio {ratio:.2}");
}
#[test]
fn ns_override_in_emitted_subtree() {
let xml = r#"<root xmlns="http://outer.com/"><item><inner xmlns="http://inner.com/"/></item></root>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let item = sp.next().unwrap().unwrap();
assert_eq!(item.root().namespace.get().unwrap().href(), "http://outer.com/");
let inner = item.root().find_child("inner").unwrap();
assert_eq!(inner.namespace.get().unwrap().href(), "http://inner.com/");
}
#[test]
fn ns_unprefixed_attr_not_in_default_ns() {
let xml = r#"<r xmlns="http://ex.com/"><item id="1"/></r>"#;
let mut sp = StreamParser::from_str(xml).emit_at_depth(1);
let item = sp.next().unwrap().unwrap();
assert!(item.root().namespace.get().is_some());
let id = item.root().attributes().next().unwrap();
assert!(id.namespace.get().is_none(), "default ns must not apply to unprefixed attrs");
}
}