use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::io::{BufRead, Read as _};
use crate::error::{Error, ErrorKind, Result};
use crate::parser::Parser;
#[cfg(feature = "std")]
use crate::parser::Version;
use crate::tree::{Attribute, ExpandedName};
use crate::{Document, Limits};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Event {
StartElement {
name: ExpandedName,
attributes: Vec<(ExpandedName, String)>,
},
EndElement {
name: ExpandedName,
},
Text(String),
Comment(String),
ProcessingInstruction {
target: String,
data: String,
},
}
#[derive(Debug)]
pub struct Reader {
text: String,
carried: Carried,
cursor: Cursor,
done: bool,
pending_end: Option<Event>,
#[cfg(feature = "std")]
source: Source,
consumed: usize,
}
#[cfg(feature = "std")]
enum Source {
Complete,
Incremental(Box<Incoming>),
}
#[cfg(feature = "std")]
struct Incoming {
inner: Box<dyn BufRead>,
at_eof: bool,
version: Version,
partial: Vec<u8>,
pending_cr: bool,
}
#[derive(Debug)]
struct Cursor {
pos: usize,
open: Vec<(String, ExpandedName)>,
started: bool,
seen_root: bool,
}
#[derive(Debug)]
struct Carried {
document: Document,
namespaces: crate::parser::Namespaces,
names: alloc::collections::BTreeMap<String, Vec<u32>>,
version: crate::parser::Version,
limits: Limits,
depth: usize,
dtd: Option<crate::dtd::Dtd>,
standalone: bool,
entity_budget: usize,
entity_cache: alloc::collections::BTreeSet<(String, Vec<(String, String)>)>,
}
#[cfg(feature = "std")]
impl core::fmt::Debug for Source {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Complete => f.write_str("Complete"),
#[cfg(feature = "std")]
Self::Incremental(incoming) => f
.debug_struct("Incremental")
.field("at_eof", &incoming.at_eof)
.finish_non_exhaustive(),
}
}
}
#[cfg(feature = "std")]
fn append_chunk(
text: &mut String,
buf: &[u8],
read: usize,
partial: &mut Vec<u8>,
pending_cr: &mut bool,
version: Version,
at_eof: &mut bool,
) -> Result<bool> {
if read == 0 {
*at_eof = true;
if *pending_cr {
*pending_cr = false;
text.push('\n');
return Ok(true);
}
if !partial.is_empty() {
return Err(Error::new(ErrorKind::MalformedEncoding, text.len()));
}
return Ok(false);
}
let mut bytes = core::mem::take(partial);
bytes.extend_from_slice(&buf[..read]);
let chunk = match core::str::from_utf8(&bytes) {
Ok(s) => s,
Err(e) => {
let valid = e.valid_up_to();
if e.error_len().is_some() {
return Err(Error::new(
ErrorKind::MalformedEncoding,
text.len() + valid,
));
}
*partial = bytes[valid..].to_vec();
match core::str::from_utf8(&bytes[..valid]) {
Ok(s) => s,
Err(_) => {
return Err(Error::new(
ErrorKind::MalformedEncoding,
text.len(),
));
}
}
}
};
let chunk = chunk.to_owned();
if !partial.is_empty() && chunk.is_empty() {
return Ok(true);
}
let before = text.len();
let mut rest = chunk.as_str();
if *pending_cr {
*pending_cr = false;
text.push('\n');
if let Some(stripped) = rest.strip_prefix('\n') {
rest = stripped;
}
}
let (body, hold) = match rest.strip_suffix('\r') {
Some(body) if !*at_eof => (body, true),
_ => (rest, false),
};
*pending_cr = hold;
let normalised = crate::parser::normalize_line_endings(body, version);
text.push_str(&normalised);
crate::parser::check_characters(&text[before..], version, before)?;
Ok(true)
}
#[cfg(feature = "std")]
impl Incoming {
fn pull(&mut self, text: &mut String, version: Version) -> Result<bool> {
if self.at_eof && self.partial.is_empty() && !self.pending_cr {
return Ok(false);
}
let mut buf = [0u8; 8192];
let read = match self.inner.read(&mut buf) {
Ok(n) => n,
Err(e) => {
return Err(Error::new(
ErrorKind::Io(e.to_string()),
text.len(),
));
}
};
append_chunk(
text,
&buf,
read,
&mut self.partial,
&mut self.pending_cr,
version,
&mut self.at_eof,
)
}
}
impl Reader {
pub fn new(input: &str) -> Result<Self> {
Self::with_limits(input, Limits::default())
}
pub fn with_limits(input: &str, limits: Limits) -> Result<Self> {
let (text, version) = crate::parser::prepare(input, limits)?;
let standalone = crate::parser::declared_standalone(&text);
Ok(Self {
text,
carried: Carried {
document: Document::placeholder(),
namespaces: crate::parser::Namespaces::default(),
names: alloc::collections::BTreeMap::new(),
version,
limits,
depth: 0,
dtd: None,
entity_budget: limits.max_entity_expansion,
standalone,
entity_cache: alloc::collections::BTreeSet::new(),
},
cursor: Cursor {
pos: 0,
open: Vec::new(),
started: false,
seen_root: false,
},
done: false,
pending_end: None,
#[cfg(feature = "std")]
source: Source::Complete,
consumed: 0,
})
}
#[cfg(feature = "std")]
pub fn from_reader<R: BufRead + 'static>(reader: R) -> Result<Self> {
Self::from_reader_with(reader, Limits::default())
}
#[cfg(feature = "std")]
pub fn from_reader_with<R: BufRead + 'static>(
reader: R,
limits: Limits,
) -> Result<Self> {
let mut incoming = Incoming {
inner: Box::new(reader),
at_eof: false,
version: Version::V10,
partial: Vec::new(),
pending_cr: false,
};
let mut text = String::new();
while text.len() < 1024 && !incoming.at_eof {
if !incoming.pull(&mut text, Version::V10)? {
break;
}
}
let version = crate::parser::declared_version(&text)?;
incoming.version = version;
crate::parser::check_prolog_shape(&text)?;
if version != Version::V10 {
text = crate::parser::normalize_line_endings(&text, version)
.into_owned();
}
crate::parser::check_characters(&text, version, 0)?;
let standalone = crate::parser::declared_standalone(&text);
Ok(Self {
text,
carried: Carried {
document: Document::placeholder(),
namespaces: crate::parser::Namespaces::default(),
names: alloc::collections::BTreeMap::new(),
version,
limits,
depth: 0,
dtd: None,
entity_budget: limits.max_entity_expansion,
standalone,
entity_cache: alloc::collections::BTreeSet::new(),
},
cursor: Cursor {
pos: 0,
open: Vec::new(),
started: false,
seen_root: false,
},
done: false,
pending_end: None,
source: Source::Incremental(Box::new(incoming)),
consumed: 0,
})
}
#[cfg(feature = "std")]
fn ensure_construct(&mut self) -> Result<()> {
if !matches!(self.source, Source::Incremental(_)) {
return Ok(());
}
loop {
if self.construct_is_whole() {
return Ok(());
}
let Source::Incremental(incoming) = &mut self.source else {
return Ok(());
};
let version = incoming.version;
if !incoming.pull(&mut self.text, version)? {
return Ok(());
}
}
}
#[cfg(feature = "std")]
fn construct_is_whole(&self) -> bool {
construct_is_whole_str(&self.text, self.cursor.pos)
}
#[cfg(feature = "std")]
fn compact(&mut self) {
const THRESHOLD: usize = 8192;
if !matches!(self.source, Source::Incremental(_))
|| self.cursor.pos < THRESHOLD
{
return;
}
let _ = self.text.drain(..self.cursor.pos);
self.consumed += self.cursor.pos;
self.cursor.pos = 0;
}
#[allow(clippy::should_implement_trait)] pub fn next_event(&mut self) -> Result<Option<Event>> {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("oxml.stream.next_event").entered();
if let Some(end) = self.pending_end.take() {
return Ok(Some(end));
}
if self.done {
return Ok(None);
}
#[cfg(feature = "std")]
self.ensure_construct()?;
let outcome =
Self::scan(&self.text, &mut self.carried, &mut self.cursor);
#[cfg(feature = "std")]
self.compact();
let outcome = outcome.map_err(|mut e| {
e.offset += self.consumed;
e
});
match outcome {
Ok(Scanned::Event(event)) => Ok(Some(event)),
Ok(Scanned::SelfClosed(start, end)) => {
self.pending_end = Some(end);
Ok(Some(start))
}
Ok(Scanned::Eof) => {
self.done = true;
Ok(None)
}
Err(e) => {
self.done = true;
Err(e)
}
}
}
}
#[cfg(feature = "std")]
fn construct_is_whole_str(text: &str, pos: usize) -> bool {
let rest = &text[pos.min(text.len())..];
if rest.is_empty() {
return false;
}
if !rest.starts_with('<') {
return rest.contains('<');
}
for (opener, closer) in
[("<!--", "-->"), ("<![CDATA[", "]]>"), ("<?", "?>")]
{
if let Some(body) = rest.strip_prefix(opener) {
return body.contains(closer);
}
}
if rest.starts_with("<!DOCTYPE") {
let mut depth = 0usize;
for c in rest.chars() {
match c {
'[' => depth += 1,
']' => depth = depth.saturating_sub(1),
'>' if depth == 0 => return true,
_ => {}
}
}
return false;
}
let mut quote: Option<char> = None;
for c in rest.chars() {
match (quote, c) {
(Some(q), c) if c == q => quote = None,
(None, '"' | '\'') => quote = Some(c),
(None, '>') => return true,
_ => {}
}
}
false
}
#[cfg(feature = "async")]
pub struct AsyncReader<R> {
inner: R,
at_eof: bool,
version: Version,
partial: Vec<u8>,
pending_cr: bool,
text: String,
carried: Carried,
cursor: Cursor,
done: bool,
pending_end: Option<Event>,
consumed: usize,
}
#[cfg(feature = "async")]
impl<R> core::fmt::Debug for AsyncReader<R> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AsyncReader")
.field("at_eof", &self.at_eof)
.field("version", &self.version)
.field("consumed", &self.consumed)
.finish_non_exhaustive()
}
}
#[cfg(feature = "async")]
impl<R: tokio::io::AsyncBufRead + Unpin> AsyncReader<R> {
pub async fn from_reader(reader: R) -> Result<Self> {
Self::from_reader_with(reader, Limits::default()).await
}
pub async fn from_reader_with(
mut reader: R,
limits: Limits,
) -> Result<Self> {
let mut at_eof = false;
let mut partial = Vec::new();
let mut pending_cr = false;
let mut text = String::new();
let mut buf = [0u8; 1024];
while text.len() < 1024 && !at_eof {
let read = tokio::io::AsyncReadExt::read(&mut reader, &mut buf)
.await
.map_err(|e| {
Error::new(ErrorKind::Io(e.to_string()), text.len())
})?;
if !append_chunk(
&mut text,
&buf,
read,
&mut partial,
&mut pending_cr,
Version::V10,
&mut at_eof,
)? {
break;
}
}
let version = crate::parser::declared_version(&text)?;
crate::parser::check_prolog_shape(&text)?;
if version != Version::V10 {
text = crate::parser::normalize_line_endings(&text, version)
.into_owned();
}
crate::parser::check_characters(&text, version, 0)?;
let standalone = crate::parser::declared_standalone(&text);
Ok(Self {
inner: reader,
at_eof,
version,
partial,
pending_cr,
text,
carried: Carried {
document: Document::placeholder(),
namespaces: crate::parser::Namespaces::default(),
names: alloc::collections::BTreeMap::new(),
version,
limits,
depth: 0,
dtd: None,
entity_budget: limits.max_entity_expansion,
standalone,
entity_cache: alloc::collections::BTreeSet::new(),
},
cursor: Cursor {
pos: 0,
open: Vec::new(),
started: false,
seen_root: false,
},
done: false,
pending_end: None,
consumed: 0,
})
}
pub async fn next_event(&mut self) -> Result<Option<Event>> {
const THRESHOLD: usize = 8192;
#[cfg(feature = "tracing")]
let _span =
tracing::trace_span!("oxml.stream.async_next_event").entered();
if let Some(end) = self.pending_end.take() {
return Ok(Some(end));
}
if self.done {
return Ok(None);
}
loop {
if construct_is_whole_str(&self.text, self.cursor.pos) {
break;
}
if self.at_eof && self.partial.is_empty() && !self.pending_cr {
break;
}
let mut buf = [0u8; 8192];
let read = tokio::io::AsyncReadExt::read(&mut self.inner, &mut buf)
.await
.map_err(|e| {
Error::new(ErrorKind::Io(e.to_string()), self.text.len())
})?;
let version = self.version;
if !append_chunk(
&mut self.text,
&buf,
read,
&mut self.partial,
&mut self.pending_cr,
version,
&mut self.at_eof,
)? {
break;
}
}
let outcome =
Reader::scan(&self.text, &mut self.carried, &mut self.cursor);
if self.cursor.pos >= THRESHOLD {
let _ = self.text.drain(..self.cursor.pos);
self.consumed += self.cursor.pos;
self.cursor.pos = 0;
}
let outcome = outcome.map_err(|mut e| {
e.offset += self.consumed;
e
});
match outcome {
Ok(Scanned::Event(event)) => Ok(Some(event)),
Ok(Scanned::SelfClosed(start, end)) => {
self.pending_end = Some(end);
Ok(Some(start))
}
Ok(Scanned::Eof) => {
self.done = true;
Ok(None)
}
Err(e) => {
self.done = true;
Err(e)
}
}
}
}
impl Reader {
#[allow(clippy::too_many_lines)] fn scan(
text: &str,
carried: &mut Carried,
cursor: &mut Cursor,
) -> Result<Scanned> {
let mut parser = Parser {
input: text,
bytes: text.as_bytes(),
pos: cursor.pos,
doc: core::mem::replace(
&mut carried.document,
Document::placeholder(),
),
name_index: core::mem::take(&mut carried.names),
external: &crate::external::NoExternal,
ns: core::mem::take(&mut carried.namespaces),
depth: carried.depth,
limits: carried.limits,
dtd: carried.dtd.take(),
version: carried.version,
entity_budget: carried.entity_budget,
entity_depth: 0,
standalone: carried.standalone,
entity_cache: core::mem::take(&mut carried.entity_cache),
};
let result = Self::one_event(
&mut parser,
&mut cursor.open,
cursor.started,
&mut cursor.seen_root,
);
cursor.started = true;
cursor.pos = parser.pos;
carried.depth = parser.depth;
carried.document =
core::mem::replace(&mut parser.doc, Document::placeholder());
carried.names = core::mem::take(&mut parser.name_index);
carried.namespaces = core::mem::take(&mut parser.ns);
carried.dtd = parser.dtd.take();
carried.entity_budget = parser.entity_budget;
carried.entity_cache = core::mem::take(&mut parser.entity_cache);
result
}
fn outside_root(
parser: &mut Parser<'_>,
open: &mut Vec<(String, ExpandedName)>,
seen_root: &mut bool,
) -> Result<Scanned> {
loop {
parser.skip_whitespace();
if parser.pos >= parser.bytes.len() {
if !*seen_root {
return Err(Error::new(
ErrorKind::NoRootElement,
parser.pos,
));
}
return Ok(Scanned::Eof);
}
if !parser.peek_is(b'<') {
return Err(Error::new(ErrorKind::TrailingContent, parser.pos));
}
if parser.starts_with("<!--") {
let c = parser.parse_comment()?;
return Ok(Scanned::Event(Event::Comment(
parser.owned(c).to_owned(),
)));
}
if parser.starts_with("<?") {
let (target, data) = parser.parse_pi()?;
return Ok(Scanned::Event(Event::ProcessingInstruction {
target: parser.owned(target).to_owned(),
data: parser.owned(data).to_owned(),
}));
}
if parser.starts_with("<!DOCTYPE") {
if *seen_root || parser.dtd.is_some() {
return Err(Error::new(
ErrorKind::TrailingContent,
parser.pos,
));
}
parser.skip_doctype()?;
continue;
}
if parser.starts_with("</") {
let name = parser.peek_end_tag_name();
return Err(Error::new(
ErrorKind::UnexpectedEndTag(name),
parser.pos,
));
}
if *seen_root {
return Err(Error::new(ErrorKind::TrailingContent, parser.pos));
}
*seen_root = true;
return Self::start_tag(parser, open);
}
}
fn char_data(parser: &mut Parser<'_>) -> Result<Event> {
let mut run = crate::parser::Run::default();
loop {
if parser.starts_with("<![CDATA[") {
parser.parse_cdata(&mut run)?;
} else if parser.pos < parser.bytes.len() && !parser.peek_is(b'<') {
parser.parse_text_run(&mut run)?;
} else {
break;
}
}
if parser.limits.max_text_length.is_some_and(|m| run.len() > m) {
return Err(Error::new(ErrorKind::TextTooLong, parser.pos));
}
Ok(Event::Text(run.as_str(parser.input).to_owned()))
}
fn one_event(
parser: &mut Parser<'_>,
open: &mut Vec<(String, ExpandedName)>,
started: bool,
seen_root: &mut bool,
) -> Result<Scanned> {
if !started {
parser.skip_prolog()?;
}
if open.is_empty() {
return Self::outside_root(parser, open, seen_root);
}
if parser.pos >= parser.bytes.len() {
return Err(Error::new(ErrorKind::UnexpectedEof, parser.pos));
}
if !parser.peek_is(b'<') || parser.starts_with("<![CDATA[") {
return Ok(Scanned::Event(Self::char_data(parser)?));
}
if parser.starts_with("</") {
let start = parser.pos;
parser.pos += 2;
let close = parser.parse_name()?;
parser.skip_whitespace();
if !parser.peek_is(b'>') {
return Err(Error::new(
ErrorKind::Unterminated("end tag"),
start,
));
}
parser.pos += 1;
let (expected, name) = open.pop().expect("inside an element");
if close != expected {
return Err(Error::new(
ErrorKind::MismatchedEndTag {
expected,
found: close.to_owned(),
},
start,
));
}
parser.ns.pop_scope();
parser.depth -= 1;
return Ok(Scanned::Event(Event::EndElement { name }));
}
if parser.starts_with("<!--") {
let c = parser.parse_comment()?;
return Ok(Scanned::Event(Event::Comment(
parser.owned(c).to_owned(),
)));
}
if parser.starts_with("<?") {
let (target, data) = parser.parse_pi()?;
return Ok(Scanned::Event(Event::ProcessingInstruction {
target: parser.owned(target).to_owned(),
data: parser.owned(data).to_owned(),
}));
}
Self::start_tag(parser, open)
}
fn start_tag(
parser: &mut Parser<'_>,
open: &mut Vec<(String, ExpandedName)>,
) -> Result<Scanned> {
if parser.depth >= parser.limits.max_depth {
return Err(Error::new(ErrorKind::DepthLimitExceeded, parser.pos));
}
let tag = parser.scan_start_tag()?;
let at = tag.tag_start;
let mut attributes = Vec::with_capacity(tag.raw_attrs.len());
for (raw, value) in tag.raw_attrs {
if raw == "xmlns" || raw.starts_with("xmlns:") {
continue;
}
let id = parser.intern_qname(raw, false, at)?;
let name = parser
.doc
.name(id)
.cloned()
.unwrap_or_else(|| ExpandedName::local(raw));
if attributes
.iter()
.any(|(seen, _): &(ExpandedName, _)| *seen == name)
{
return Err(Error::new(
ErrorKind::DuplicateAttribute(raw.to_owned()),
at,
));
}
attributes.push((name, value.as_str(parser.input).to_owned()));
}
let id = parser.intern_qname(tag.qname, true, at)?;
let name = parser
.doc
.name(id)
.cloned()
.unwrap_or_else(|| ExpandedName::local(tag.qname));
let start = Event::StartElement {
name: name.clone(),
attributes,
};
if tag.self_closing {
parser.ns.pop_scope();
return Ok(Scanned::SelfClosed(start, Event::EndElement { name }));
}
parser.depth += 1;
open.push((tag.qname.to_owned(), name));
Ok(Scanned::Event(start))
}
}
enum Scanned {
Event(Event),
SelfClosed(Event, Event),
Eof,
}
const _: Option<Attribute> = None;