use alloc::{string::String, vec::Vec};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Name {
pub ns: String,
pub local: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Node {
pub name: Name,
pub attrs: Vec<(Name, String)>,
pub text: String,
pub children: Vec<Node>,
}
impl Node {
pub fn is(&self, ns: &str, local: &str) -> bool {
self.name.ns == ns && self.name.local == local
}
pub fn attr(&self, ns: &str, local: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(n, _)| n.ns == ns && n.local == local)
.map(|(_, v)| v.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Syntax,
Limit,
Unsupported,
}
pub fn valid_char(c: char) -> bool {
matches!(c as u32, 9 | 10 | 13 | 0x20..=0xd7ff | 0xe000..=0xfffd | 0x10000..=0x10ffff)
}
pub fn escape(s: &str) -> Result<String, Error> {
let mut out = String::new();
for c in s.chars() {
if !valid_char(c) {
return Err(Error::Syntax);
}
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
'\r' => out.push_str(" "),
'\n' => out.push_str(" "),
'\t' => out.push_str("	"),
_ => out.push(c),
}
}
Ok(out)
}
fn unescape(s: &str) -> Result<String, Error> {
if s.contains('<') || s.contains("]]>") {
return Err(Error::Syntax);
}
let mut out = String::new();
let mut rest = s;
while let Some(at) = rest.find('&') {
out.push_str(&rest[..at]);
rest = &rest[at + 1..];
let end = rest.find(';').ok_or(Error::Syntax)?;
let e = &rest[..end];
let c = match e {
"amp" => '&',
"lt" => '<',
"gt" => '>',
"quot" => '"',
"apos" => '\'',
_ => {
let (digits, radix) = if let Some(d) = e.strip_prefix("#x") {
(d, 16)
} else if let Some(d) = e.strip_prefix('#') {
(d, 10)
} else {
return Err(Error::Unsupported);
};
if digits.is_empty()
|| !digits.bytes().all(|b| {
if radix == 16 {
b.is_ascii_hexdigit()
} else {
b.is_ascii_digit()
}
})
{
return Err(Error::Syntax);
}
char::from_u32(u32::from_str_radix(digits, radix).map_err(|_| Error::Syntax)?)
.ok_or(Error::Syntax)?
}
};
if !valid_char(c) {
return Err(Error::Syntax);
}
out.push(c);
rest = &rest[end + 1..];
}
out.push_str(rest);
Ok(out)
}
type Namespaces = Vec<(String, String)>;
fn expand(q: &str, namespaces: &Namespaces, attr: bool) -> Result<Name, Error> {
let (prefix, local) = q.split_once(':').unwrap_or(("", q));
if local.is_empty() || local.contains(':') {
return Err(Error::Syntax);
}
let ns = if prefix.is_empty() && attr {
""
} else if prefix == "xml" {
"http://www.w3.org/XML/1998/namespace"
} else {
match namespaces.iter().rev().find(|(p, _)| p == prefix) {
Some((_, n)) => n.as_str(),
None if prefix.is_empty() => "",
_ => return Err(Error::Syntax),
}
};
Ok(Name {
ns: String::from(ns),
local: String::from(local),
})
}
struct Parser<'a> {
s: &'a str,
pos: usize,
nodes: usize,
}
impl<'a> Parser<'a> {
fn rest(&self) -> &'a str {
&self.s[self.pos..]
}
fn eat(&mut self, s: &str) -> bool {
if self.rest().starts_with(s) {
self.pos += s.len();
true
} else {
false
}
}
fn ws(&mut self) {
while self
.rest()
.as_bytes()
.first()
.is_some_and(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
{
self.pos += 1;
}
}
fn name(&mut self) -> Result<&'a str, Error> {
let start = self.pos;
if !self
.rest()
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
{
return Err(Error::Syntax);
}
while self
.rest()
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_alphanumeric() || b"_:.-".contains(b))
{
self.pos += 1;
}
Ok(&self.s[start..self.pos])
}
fn node(&mut self, ns: &Namespaces, depth: usize) -> Result<Node, Error> {
self.nodes += 1;
if depth > 48 || self.nodes > 30000 {
return Err(Error::Limit);
}
if !self.eat("<") {
return Err(Error::Syntax);
}
if self.rest().starts_with(['!', '?']) {
return Err(Error::Unsupported);
}
let q = self.name()?;
let mut scope = ns.clone();
let mut attrs = Vec::new();
let empty;
loop {
let before = self.pos;
self.ws();
if self.eat("/>") {
empty = true;
break;
}
if self.eat(">") {
empty = false;
break;
}
if before == self.pos || attrs.len() >= 64 {
return Err(Error::Syntax);
}
let name = self.name()?;
self.ws();
if !self.eat("=") {
return Err(Error::Syntax);
}
self.ws();
let quote = if self.eat("\"") {
'"'
} else if self.eat("'") {
'\''
} else {
return Err(Error::Syntax);
};
let end = self.rest().find(quote).ok_or(Error::Syntax)?;
let value = unescape(&self.rest()[..end].replace(['\n', '\t'], " "))?;
self.pos += end + 1;
if attrs.iter().any(|(n, _)| *n == name) {
return Err(Error::Syntax);
}
if name == "xmlns" || name.starts_with("xmlns:") {
let prefix = name.strip_prefix("xmlns:").unwrap_or("");
if prefix == "xmlns"
|| (!prefix.is_empty() && value.is_empty())
|| value == "http://www.w3.org/2000/xmlns/"
|| (prefix == "xml") != (value == "http://www.w3.org/XML/1998/namespace")
{
return Err(Error::Syntax);
}
scope.push((String::from(prefix), value.clone()));
if scope.len() > 64
|| scope.iter().map(|(p, n)| p.len() + n.len()).sum::<usize>() > 4096
{
return Err(Error::Limit);
}
}
attrs.push((name, value));
}
let mut node = Node {
name: expand(q, &scope, false)?,
attrs: Vec::new(),
text: String::new(),
children: Vec::new(),
};
for (q, v) in attrs {
if q == "xmlns" || q.starts_with("xmlns:") {
continue;
}
let n = expand(q, &scope, true)?;
if node.attrs.iter().any(|(a, _)| *a == n) {
return Err(Error::Syntax);
}
node.attrs.push((n, v));
}
if empty {
return Ok(node);
}
loop {
if self.eat("</") {
let end = self.name()?;
self.ws();
if end != q || !self.eat(">") {
return Err(Error::Syntax);
}
break;
}
if self.rest().starts_with('<') {
node.children.push(self.node(&scope, depth + 1)?);
} else {
let end = self.rest().find('<').ok_or(Error::Syntax)?;
node.text.push_str(&unescape(&self.rest()[..end])?);
self.pos += end;
}
}
if !node.children.is_empty() {
if !node.text.trim_matches([' ', '\t', '\r', '\n']).is_empty() {
return Err(Error::Unsupported);
}
node.text.clear();
}
Ok(node)
}
}
pub fn parse(s: &str) -> Result<Node, Error> {
if s.len() > 4 * 1024 * 1024 {
return Err(Error::Limit);
}
if !s.chars().all(valid_char) {
return Err(Error::Syntax);
}
let normalized = s
.strip_prefix('\u{feff}')
.unwrap_or(s)
.replace("\r\n", "\n")
.replace('\r', "\n");
let mut p = Parser {
s: &normalized,
pos: 0,
nodes: 0,
};
if p.eat("<?xml ") {
let end = p.rest().find("?>").ok_or(Error::Syntax)?;
let decl = &p.rest()[..end];
let declaration = alloc::format!("<declaration {decl}/>");
let mut dp = Parser {
s: &declaration,
pos: 0,
nodes: 0,
};
let dn = dp.node(&Vec::new(), 0)?;
if dn.attrs.is_empty()
|| dn.attrs[0].0.local != "version"
|| dn.attrs[0].1 != "1.0"
|| !dp.rest().is_empty()
{
return Err(Error::Unsupported);
}
let mut last = 0;
for (name, value) in &dn.attrs {
let order = match name.local.as_str() {
"version" if value == "1.0" => 1,
"encoding"
if matches!(
value.as_str(),
"UTF-8" | "utf-8" | "UTF-16" | "utf-16" | "UTF-16LE" | "UTF-16BE"
) =>
{
2
}
"standalone" if matches!(value.as_str(), "yes" | "no") => 3,
_ => return Err(Error::Unsupported),
};
if !name.ns.is_empty() || order <= last {
return Err(Error::Syntax);
}
last = order;
}
p.pos += end + 2;
}
p.ws();
let node = p.node(&Vec::new(), 0)?;
p.ws();
if !p.rest().is_empty() {
return Err(Error::Syntax);
}
Ok(node)
}