pub const MAX_DEPTH: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct XmlError {
pub message: String,
pub offset: usize,
}
impl core::fmt::Display for XmlError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} (at byte {})", self.message, self.offset)
}
}
#[derive(Clone, Debug)]
pub struct Attribute {
pub prefix: String,
pub name: String,
pub value: String,
}
#[derive(Clone, Debug)]
pub struct StartTag {
pub namespace: String,
pub name: String,
pub attributes: Vec<Attribute>,
pub empty: bool,
}
impl StartTag {
pub fn attribute(&self, name: &str) -> Option<&str> {
self.attributes
.iter()
.find(|attribute| attribute.prefix.is_empty() && attribute.name == name)
.map(|attribute| attribute.value.as_str())
}
}
#[derive(Clone, Debug)]
pub enum Event {
Start(StartTag),
End { name: String },
Eof,
}
struct Scope {
bindings: Vec<(String, String)>,
depth_marks: Vec<usize>,
}
pub struct XmlReader<'a> {
data: &'a [u8],
at: usize,
scope: Scope,
open: Vec<(String, String)>,
}
impl<'a> XmlReader<'a> {
pub fn new(text: &'a str) -> Self {
Self {
data: text.as_bytes(),
at: 0,
scope: Scope { bindings: Vec::new(), depth_marks: Vec::new() },
open: Vec::new(),
}
}
fn error<T>(&self, message: impl Into<String>) -> Result<T, XmlError> {
Err(XmlError { message: message.into(), offset: self.at })
}
fn peek(&self) -> Option<u8> {
self.data.get(self.at).copied()
}
fn starts_with(&self, text: &str) -> bool {
self.data[self.at..].starts_with(text.as_bytes())
}
fn skip_whitespace(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
self.at += 1;
}
}
fn expect(&mut self, text: &str) -> Result<(), XmlError> {
if self.starts_with(text) {
self.at += text.len();
Ok(())
} else {
self.error(format!("expected '{text}'"))
}
}
pub fn next(&mut self) -> Result<Event, XmlError> {
loop {
while let Some(byte) = self.peek() {
if byte == b'<' {
break;
}
self.at += 1;
}
let Some(_) = self.peek() else {
if let Some((_, name)) = self.open.last() {
return self.error(format!("the document ends inside <{name}>"));
}
return Ok(Event::Eof);
};
if self.starts_with("<!--") {
self.at += 4;
let Some(end) = find(self.data, self.at, b"-->") else {
return self.error("an XML comment is never closed");
};
self.at = end + 3;
continue;
}
if self.starts_with("<![CDATA[") {
self.at += 9;
let Some(end) = find(self.data, self.at, b"]]>") else {
return self.error("a CDATA section is never closed");
};
self.at = end + 3;
continue;
}
if self.starts_with("<!DOCTYPE") {
return self.error(
"a DOCTYPE declaration is refused: this reader expands no entities",
);
}
if self.starts_with("<!") {
return self.error("unsupported XML declaration");
}
if self.starts_with("<?") {
self.at += 2;
let Some(end) = find(self.data, self.at, b"?>") else {
return self.error("an XML processing instruction is never closed");
};
self.at = end + 2;
continue;
}
if self.starts_with("</") {
self.at += 2;
let name = self.read_name()?;
self.skip_whitespace();
self.expect(">")?;
let Some((open_prefix, open_name)) = self.open.pop() else {
return self.error(format!("</{name}> closes nothing"));
};
let (prefix, local) = split_name(&name);
if prefix != open_prefix || local != open_name {
return self
.error(format!("</{name}> does not close the open element"));
}
self.pop_scope();
return Ok(Event::End { name: local });
}
self.at += 1;
let name = self.read_name()?;
let (prefix, local) = split_name(&name);
let mut attributes = Vec::new();
let mut declared = 0usize;
loop {
self.skip_whitespace();
if self.starts_with("/>") {
self.at += 2;
self.scope.depth_marks.push(declared);
let namespace = self.resolve(&prefix)?;
self.pop_scope();
return Ok(Event::Start(StartTag {
namespace,
name: local,
attributes,
empty: true,
}));
}
if self.starts_with(">") {
self.at += 1;
self.scope.depth_marks.push(declared);
if self.open.len() + 1 > MAX_DEPTH {
return self
.error(format!("XML nesting deeper than {MAX_DEPTH} elements"));
}
let namespace = self.resolve(&prefix)?;
self.open.push((prefix, local.clone()));
return Ok(Event::Start(StartTag {
namespace,
name: local,
attributes,
empty: false,
}));
}
let attribute_name = self.read_name()?;
self.skip_whitespace();
self.expect("=")?;
self.skip_whitespace();
let value = self.read_attribute_value()?;
let (attribute_prefix, attribute_local) = split_name(&attribute_name);
if attribute_name == "xmlns" {
self.scope.bindings.push((String::new(), value));
declared += 1;
} else if attribute_prefix == "xmlns" {
self.scope.bindings.push((attribute_local, value));
declared += 1;
} else {
attributes.push(Attribute {
prefix: attribute_prefix,
name: attribute_local,
value,
});
}
}
}
}
pub fn skip_subtree(&mut self, tag: &StartTag) -> Result<(), XmlError> {
if tag.empty {
return Ok(());
}
let mut depth = 1usize;
loop {
match self.next()? {
Event::Start(inner) if !inner.empty => depth += 1,
Event::Start(_) => {}
Event::End { .. } => {
depth -= 1;
if depth == 0 {
return Ok(());
}
}
Event::Eof => return self.error("the document ends inside a skipped element"),
}
}
}
fn pop_scope(&mut self) {
if let Some(declared) = self.scope.depth_marks.pop() {
let keep = self.scope.bindings.len().saturating_sub(declared);
self.scope.bindings.truncate(keep);
}
}
fn resolve(&self, prefix: &str) -> Result<String, XmlError> {
if prefix == "xml" {
return Ok("http://www.w3.org/XML/1998/namespace".into());
}
match self
.scope
.bindings
.iter()
.rev()
.find(|(bound, _)| bound == prefix)
{
Some((_, uri)) => Ok(uri.clone()),
None if prefix.is_empty() => Ok(String::new()),
None => Err(XmlError {
message: format!("namespace prefix '{prefix}' is not declared"),
offset: self.at,
}),
}
}
fn read_name(&mut self) -> Result<String, XmlError> {
let start = self.at;
while let Some(byte) = self.peek() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':') {
self.at += 1;
} else {
break;
}
}
if self.at == start {
return self.error("expected an element or attribute name");
}
core::str::from_utf8(&self.data[start..self.at])
.map(str::to_owned)
.map_err(|_| XmlError { message: "a name is not UTF-8".into(), offset: start })
}
fn read_attribute_value(&mut self) -> Result<String, XmlError> {
let quote = match self.peek() {
Some(byte @ (b'"' | b'\'')) => byte,
_ => return self.error("an attribute value must be quoted"),
};
self.at += 1;
let start = self.at;
while let Some(byte) = self.peek() {
if byte == quote {
let raw = core::str::from_utf8(&self.data[start..self.at]).map_err(|_| {
XmlError { message: "an attribute value is not UTF-8".into(), offset: start }
})?;
let value = unescape(raw).ok_or(XmlError {
message: "an attribute value has an unknown entity reference".into(),
offset: start,
})?;
self.at += 1;
return Ok(value);
}
if byte == b'<' {
return self.error("'<' is not allowed inside an attribute value");
}
self.at += 1;
}
self.error("an attribute value is never closed")
}
}
fn find(data: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
if from >= data.len() || needle.is_empty() {
return None;
}
data[from..]
.windows(needle.len())
.position(|window| window == needle)
.map(|at| at + from)
}
fn split_name(name: &str) -> (String, String) {
match name.split_once(':') {
Some((prefix, local)) => (prefix.to_owned(), local.to_owned()),
None => (String::new(), name.to_owned()),
}
}
fn unescape(text: &str) -> Option<String> {
if !text.contains('&') {
return Some(text.to_owned());
}
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find('&') {
out.push_str(&rest[..at]);
let tail = &rest[at + 1..];
let end = tail.find(';')?;
let reference = &tail[..end];
match reference {
"amp" => out.push('&'),
"lt" => out.push('<'),
"gt" => out.push('>'),
"quot" => out.push('"'),
"apos" => out.push('\''),
_ => {
let code = if let Some(hex) = reference.strip_prefix("#x") {
u32::from_str_radix(hex, 16).ok()?
} else if let Some(decimal) = reference.strip_prefix('#') {
decimal.parse::<u32>().ok()?
} else {
return None;
};
out.push(char::from_u32(code)?);
}
}
rest = &tail[end + 1..];
}
out.push_str(rest);
Some(out)
}