use std::fmt;
use zeroize::Zeroizing;
pub const MAX_DEPTH: usize = 16;
pub const MAX_ELEMENTS: usize = 2_000_000;
pub const MAX_ATTRIBUTES: usize = 64;
pub struct Limits<'a> {
pub elements: usize,
pub elements_named: &'a [(&'a str, usize)],
pub attribute_len: fn(element: &str, attribute: &str) -> usize,
pub text_len: fn(element: &str) -> usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct XmlError {
pub line: usize,
pub column: usize,
pub problem: &'static str,
}
impl fmt::Display for XmlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} at line {}, column {}",
self.problem, self.line, self.column
)
}
}
impl std::error::Error for XmlError {}
#[derive(Debug, Default)]
pub struct Element<'a> {
pub name: &'a str,
attributes: Vec<(&'a str, &'a str)>,
text: Vec<&'a str>,
text_len: usize,
text_after_cr: bool,
pub children: Vec<Element<'a>>,
}
impl<'a> Element<'a> {
pub fn attribute(&self, name: &str) -> Option<Zeroizing<String>> {
self.attributes
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| decode([*v].into_iter(), true))
}
pub fn attribute_bounded(
&self,
name: &str,
max: usize,
) -> Result<Option<Zeroizing<String>>, &'static str> {
self.attributes
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| decode_bounded([*v].into_iter(), true, max))
.transpose()
}
pub fn attribute_names(&self) -> impl Iterator<Item = &'a str> + '_ {
self.attributes.iter().map(|(n, _)| *n)
}
pub fn attributes(&self) -> impl Iterator<Item = (&'a str, Zeroizing<String>)> + '_ {
self.attributes
.iter()
.map(|(n, v)| (*n, decode([*v].into_iter(), true)))
}
pub fn text(&self) -> Zeroizing<String> {
decode(self.text.iter().copied(), false)
}
pub fn text_bounded(&self, max: usize) -> Result<Zeroizing<String>, &'static str> {
decode_bounded(self.text.iter().copied(), false, max)
}
pub fn children_named<'s>(&'s self, name: &'s str) -> impl Iterator<Item = &'s Element<'a>> {
self.children.iter().filter(move |c| c.name == name)
}
}
fn decode<'a>(pieces: impl Iterator<Item = &'a str> + Clone, attribute: bool) -> Zeroizing<String> {
decode_bounded(pieces, attribute, usize::MAX).expect("reader checked the values")
}
fn decode_bounded<'a>(
pieces: impl Iterator<Item = &'a str> + Clone,
attribute: bool,
max: usize,
) -> Result<Zeroizing<String>, &'static str> {
let len = decoded_len(pieces.clone(), attribute, max, &mut false)?;
let mut out = Zeroizing::new(String::with_capacity(len));
walk_decoded(pieces, attribute, &mut false, |c| {
out.push(c);
Ok(())
})?;
Ok(out)
}
fn decoded_len<'a>(
pieces: impl Iterator<Item = &'a str>,
attribute: bool,
max: usize,
after_cr: &mut bool,
) -> Result<usize, &'static str> {
let mut len = 0usize;
walk_decoded(pieces, attribute, after_cr, |c| {
if c.len_utf8() > max - len {
return Err("value too long");
}
len += c.len_utf8();
Ok(())
})?;
Ok(len)
}
fn walk_decoded<'a>(
pieces: impl Iterator<Item = &'a str>,
attribute: bool,
after_cr: &mut bool,
mut emit: impl FnMut(char) -> Result<(), &'static str>,
) -> Result<(), &'static str> {
for piece in pieces {
let mut chars = piece.char_indices();
while let Some((i, c)) = chars.next() {
let was_cr = std::mem::replace(after_cr, c == '\r');
match c {
'\n' if was_cr => {}
'\r' | '\n' => emit(if attribute { ' ' } else { '\n' })?,
'\t' if attribute => emit(' ')?,
'&' => {
let rest = &piece[i + 1..];
let semi = rest.find(';').ok_or("unknown entity")?;
emit(entity(&rest[..semi]).ok_or("unknown entity")?)?;
for _ in 0..semi + 1 {
chars.next();
}
}
c => emit(c)?,
}
}
}
Ok(())
}
fn entity(name: &str) -> Option<char> {
match name {
"lt" => Some('<'),
"gt" => Some('>'),
"amp" => Some('&'),
"quot" => Some('"'),
"apos" => Some('\''),
_ => {
let number = name.strip_prefix('#')?;
let (digits, radix) = match number.strip_prefix('x') {
Some(hex) => (hex, 16),
None => (number, 10),
};
if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
return None;
}
char::from_u32(u32::from_str_radix(digits, radix).ok()?).filter(|&c| is_xml_char(c))
}
}
}
fn is_xml_char(c: char) -> bool {
matches!(c, '\t' | '\n' | '\r') || (c >= ' ' && c != '\u{fffe}' && c != '\u{ffff}')
}
fn is_name_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_name(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':')
}
struct Reader<'a> {
input: &'a str,
at: usize,
}
impl<'a> Reader<'a> {
fn error(&self, problem: &'static str) -> XmlError {
let before = &self.input[..self.at];
let line = before.matches('\n').count() + 1;
let column = before.rsplit('\n').next().map_or(0, |l| l.chars().count()) + 1;
XmlError {
line,
column,
problem,
}
}
fn rest(&self) -> &'a str {
&self.input[self.at..]
}
fn skip_space(&mut self) {
let rest = self.rest();
self.at += rest.len() - rest.trim_start_matches([' ', '\t', '\r', '\n']).len();
}
fn past(&mut self, end: &str, problem: &'static str) -> Result<&'a str, XmlError> {
let found = self.rest().find(end).ok_or_else(|| self.error(problem))?;
let skipped = &self.rest()[..found];
self.at += found + end.len();
Ok(skipped)
}
fn name(&mut self) -> Result<&'a str, XmlError> {
let rest = self.rest();
if !rest.starts_with(is_name_start) {
return Err(self.error("a name was expected"));
}
let len = rest.find(|c: char| !is_name(c)).unwrap_or(rest.len());
self.at += len;
Ok(&rest[..len])
}
fn declaration(&mut self) -> Result<(), XmlError> {
if self.rest().find("?>").is_some_and(|end| end > 64 * 1024) {
return Err(self.error("the XML declaration is too long"));
}
self.at += 5; let mut version = false;
let mut encoding = false;
let mut standalone = false;
loop {
let before = self.at;
self.skip_space();
if self.rest().starts_with("?>") && version {
self.at += 2;
return Ok(());
}
if before == self.at {
return Err(self.error("invalid XML declaration"));
}
let name = self
.name()
.map_err(|_| self.error("invalid XML declaration"))?;
self.skip_space();
if !self.rest().starts_with('=') {
return Err(self.error("invalid XML declaration"));
}
self.at += 1;
self.skip_space();
let quote = match self.rest().chars().next() {
Some(q @ ('\'' | '"')) => q,
_ => return Err(self.error("invalid XML declaration")),
};
self.at += 1;
let value = self.past(
if quote == '"' { "\"" } else { "'" },
"the XML declaration does not end",
)?;
match name {
"version" if !version && value == "1.0" => version = true,
"encoding"
if version
&& !encoding
&& !standalone
&& value.eq_ignore_ascii_case("UTF-8") =>
{
encoding = true
}
"standalone" if version && !standalone && matches!(value, "yes" | "no") => {
standalone = true
}
_ => return Err(self.error("invalid XML declaration")),
}
}
}
}
pub fn parse(input: &str) -> Result<Element<'_>, XmlError> {
parse_with_limits(
input,
&Limits {
elements: MAX_ELEMENTS,
elements_named: &[],
attribute_len: |_, _| usize::MAX,
text_len: |_| usize::MAX,
},
)
}
pub fn parse_with_limits<'a>(input: &'a str, limits: &Limits<'_>) -> Result<Element<'a>, XmlError> {
parse_with_processing_instructions(input, limits, false)
}
pub(crate) fn parse_with_processing_instructions<'a>(
input: &'a str,
limits: &Limits<'_>,
allow_pi: bool,
) -> Result<Element<'a>, XmlError> {
let mut r = Reader { input, at: 0 };
if let Some((at, _)) = input.char_indices().find(|(_, c)| !is_xml_char(*c)) {
r.at = at;
return Err(r.error("a character XML does not allow"));
}
if r.rest().starts_with('\u{feff}') {
r.at += '\u{feff}'.len_utf8();
}
if r.rest().starts_with("<?xml")
&& (!allow_pi || r.rest()[5..].starts_with([' ', '\t', '\r', '\n', '?']))
{
r.declaration()?;
}
let mut open: Vec<Element> = Vec::new();
let mut root: Option<Element> = None;
let mut elements = 0usize;
let mut named_counts = vec![0usize; limits.elements_named.len()];
loop {
let text_end = r.rest().find('<').unwrap_or(r.rest().len());
let text = &r.rest()[..text_end];
if text.contains("]]>") {
return Err(r.error("invalid XML character data"));
}
if !text.is_empty() {
match open.last_mut() {
Some(parent) => {
let remaining = (limits.text_len)(parent.name) - parent.text_len;
let len = decoded_len(
[text].into_iter(),
false,
remaining,
&mut parent.text_after_cr,
)
.map_err(|problem| {
r.error(if problem == "value too long" {
"text too long"
} else {
"an unknown entity or a bare &"
})
})?;
parent.text_len += len;
parent.text.push(text);
}
None if text.trim_matches([' ', '\t', '\r', '\n']).is_empty() => {}
None => return Err(r.error("text outside the root element")),
}
r.at += text_end;
}
if r.at == input.len() {
break;
}
let rest = r.rest();
if rest.starts_with("<!--") {
r.at += 4;
let comment = r.past("-->", "a comment does not end")?;
if comment.contains("--") || comment.ends_with('-') {
return Err(r.error("invalid XML comment"));
}
} else if rest.starts_with("<!") {
return Err(r.error("a DTD or CDATA, which TQSL does not write"));
} else if rest.starts_with("<?") {
if !allow_pi {
return Err(r.error("a processing instruction"));
}
r.at += 2;
let target = r.name()?;
if target.eq_ignore_ascii_case("xml") {
return Err(r.error("an XML processing instruction"));
}
if !r.rest().starts_with("?>") && !r.rest().starts_with([' ', '\t', '\r', '\n']) {
return Err(r.error("a malformed processing instruction"));
}
let body = r.past("?>", "a processing instruction does not end")?;
if target.len().saturating_add(body.len()) > 64 * 1024 {
return Err(r.error("a processing instruction is too long"));
}
elements += 1;
if elements > limits.elements.min(MAX_ELEMENTS) {
return Err(r.error("too many elements"));
}
} else if rest.starts_with("</") {
r.at += 2;
let name = r.name()?;
r.skip_space();
if !r.rest().starts_with('>') {
return Err(r.error("an end tag does not end"));
}
r.at += 1;
let element = open
.pop()
.ok_or_else(|| r.error("an end tag with no start"))?;
if element.name != name {
return Err(r.error("an end tag for another element"));
}
match open.last_mut() {
Some(parent) => parent.children.push(element),
None => root = Some(element),
}
} else {
r.at += 1;
if root.is_some() {
return Err(r.error("a second root element"));
}
elements += 1;
if elements > limits.elements.min(MAX_ELEMENTS) {
return Err(r.error("too many elements"));
}
if open.len() == MAX_DEPTH {
return Err(r.error("elements nested too deep"));
}
let mut element = Element {
name: r.name()?,
..Element::default()
};
for ((name, max), count) in limits.elements_named.iter().zip(&mut named_counts) {
if element.name == *name {
if *count == *max {
return Err(r.error("too many named elements"));
}
*count += 1;
}
}
loop {
let before = r.at;
r.skip_space();
let rest = r.rest();
if rest.starts_with("/>") || rest.starts_with('>') {
break;
}
if r.at == before {
return Err(r.error("attributes must be separated by space"));
}
if element.attributes.len() == MAX_ATTRIBUTES {
return Err(r.error("too many attributes"));
}
let name = r.name()?;
r.skip_space();
if !r.rest().starts_with('=') {
return Err(r.error("an attribute with no value"));
}
r.at += 1;
r.skip_space();
let quote = match r.rest().chars().next() {
Some(q @ ('"' | '\'')) => q,
_ => return Err(r.error("an attribute value must be quoted")),
};
r.at += 1;
let value = r.past(
if quote == '"' { "\"" } else { "'" },
"an attribute value does not end",
)?;
if value.contains('<') {
return Err(r.error("an attribute value with < or an unknown entity"));
}
decoded_len(
[value].into_iter(),
true,
(limits.attribute_len)(element.name, name),
&mut false,
)
.map_err(|problem| {
r.error(if problem == "value too long" {
"attribute value too long"
} else {
"an attribute value with < or an unknown entity"
})
})?;
if element.attributes.iter().any(|(n, _)| *n == name) {
return Err(r.error("an attribute given twice"));
}
element.attributes.push((name, value));
}
if r.rest().starts_with("/>") {
r.at += 2;
match open.last_mut() {
Some(parent) => parent.children.push(element),
None => root = Some(element),
}
} else {
r.at += 1;
open.push(element);
}
}
}
if !open.is_empty() {
return Err(r.error("an element does not end"));
}
root.ok_or_else(|| r.error("no root element"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn what_tqsl_writes_is_read() {
let doc = parse(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<A>\n<!-- a note -->\n<B x=\"1\" y='a & b'>te<!-- c -->xt <AB</B>\n<C/>\n</A>\n",
)
.expect("parses");
assert_eq!(doc.name, "A");
assert_eq!(doc.children.len(), 2);
let b = &doc.children[0];
assert_eq!(b.attribute("x").as_deref().map(String::as_str), Some("1"));
assert_eq!(
b.attribute("y").as_deref().map(String::as_str),
Some("a & b")
);
assert_eq!(b.text().as_str(), "text <AB");
assert_eq!(doc.children_named("C").count(), 1);
}
#[test]
fn what_could_expand_or_hide_is_refused() {
for (bad, problem) in [
(
"<!DOCTYPE a [<!ENTITY x \"y\">]><a>&x;</a>",
"a DTD or CDATA, which TQSL does not write",
),
(
"<a><![CDATA[x]]></a>",
"a DTD or CDATA, which TQSL does not write",
),
("<a>&x;</a>", "an unknown entity or a bare &"),
("<a>a & b</a>", "an unknown entity or a bare &"),
(
"<a b=\"&x;\"/>",
"an attribute value with < or an unknown entity",
),
("<a><?pi x?></a>", "a processing instruction"),
("<a b=\"1\" b=\"2\"/>", "an attribute given twice"),
("<a b=1/>", "an attribute value must be quoted"),
("<a></b>", "an end tag for another element"),
("<a>", "an element does not end"),
("<a/><b/>", "a second root element"),
("text<a/>", "text outside the root element"),
("<a><!-- open", "a comment does not end"),
("", "no root element"),
] {
assert_eq!(
parse(bad).map(|_| ()).map_err(|e| e.problem),
Err(problem),
"{bad}"
);
}
}
#[test]
fn limits_are_kept_while_reading() {
let deep = "<a>".repeat(MAX_DEPTH + 1);
assert_eq!(
parse(&deep).err().map(|e| e.problem),
Some("elements nested too deep")
);
let attrs: String = (0..=MAX_ATTRIBUTES)
.map(|i| format!(" a{i}=\"\""))
.collect();
assert_eq!(
parse(&format!("<a{attrs}/>")).err().map(|e| e.problem),
Some("too many attributes")
);
let comment = format!("<a><!--{}--></a>", "<".repeat(1 << 20));
assert!(parse(&comment).is_ok());
let many = format!("<a>{}</a>", "<b/>".repeat(MAX_ELEMENTS));
assert_eq!(
parse(&many).err().map(|e| e.problem),
Some("too many elements")
);
}
#[test]
fn values_are_normalized_as_xml_says() {
let doc = parse("<a n=\"HOME\tA\" m=\"x\r\ny\" r=\"	\">one\r\ntwo\rthree</a>")
.expect("parses");
assert_eq!(
doc.attribute("n").as_deref().map(String::as_str),
Some("HOME A")
);
assert_eq!(
doc.attribute("m").as_deref().map(String::as_str),
Some("x y")
);
assert_eq!(
doc.attribute("r").as_deref().map(String::as_str),
Some("\t")
);
assert_eq!(doc.text().as_str(), "one\ntwo\nthree");
}
#[test]
fn characters_xml_forbids_are_refused() {
for bad in [
"<a>\u{1}</a>",
"<a b=\"\0\"/>",
"<a></a>",
"<a>�</a>",
"<a>&#+65;</a>",
"<a>&#x;</a>",
] {
assert!(parse(bad).is_err(), "{bad:?}");
}
assert!(parse("<a>AB\u{7f}</a>").is_ok());
}
#[test]
fn errors_say_where() {
let e = parse("<a>\n <b>&x;</b>\n</a>").expect_err("refused");
assert_eq!((e.line, e.column), (2, 6));
}
#[test]
fn decoded_text_is_wiped_and_made_at_its_size() {
fn wiped<T: zeroize::ZeroizeOnDrop>(_: &T) {}
let doc = parse("<a>abc&def</a>").expect("parses");
let text = doc.text();
wiped(&text);
assert!(text.capacity() >= text.len() && text.capacity() <= "abc&def".len());
}
#[test]
fn bounded_values_stop_before_decoding_an_oversized_value_whole() {
let limits = Limits {
elements: 3,
elements_named: &[("b", 1)],
attribute_len: |_, _| 4,
text_len: |_| 4,
};
let oversized = "A".repeat(5) + "&unknown;";
assert_eq!(
parse_with_limits(&format!("<a k=\"{oversized}\"/>"), &limits)
.expect_err("attribute cap")
.problem,
"attribute value too long"
);
assert_eq!(
decode_bounded([oversized.as_str()].into_iter(), true, 4),
Err("value too long")
);
assert_eq!(
parse_with_limits("<a>ab<!-- split -->éx&unknown;</a>", &limits)
.expect_err("cumulative UTF-8 text cap")
.problem,
"text too long"
);
let doc = parse_with_limits("<a k=\"ABCD\">ab<!-- -->é</a>", &limits)
.expect("exact limits fit");
let value = doc.attribute_bounded("k", 4).expect("fits").expect("k");
assert_eq!(value.as_str(), "ABCD");
assert_eq!(value.capacity(), 4);
assert_eq!(doc.text_bounded(4).expect("fits").as_str(), "abé");
assert!(doc.attribute_bounded("k", 3).is_err());
assert!(doc.text_bounded(3).is_err());
assert_eq!(
parse_with_limits("<a>ab\r<!-- -->\nc</a>", &limits)
.expect("normalized text fits")
.text()
.as_str(),
"ab\nc"
);
assert_eq!(
parse_with_limits("<a><b/><b invalid", &limits)
.expect_err("named count before attributes")
.problem,
"too many named elements"
);
assert_eq!(
parse_with_limits("<a><b/><c/><d invalid", &limits)
.expect_err("total count before element")
.problem,
"too many elements"
);
assert!(parse_with_limits("<a><b/><c/></a>", &limits).is_ok());
}
}
#[cfg(test)]
mod review_tests {
use super::parse;
#[test]
fn declarations_follow_xml_10_pseudo_attribute_grammar() {
for declaration in [
"<?xml nonsense?>",
"<?xml?>",
"<?xml version='1.0' nonsense='x'?>",
"<?xml version='1.0' version='1.0'?>",
"<?xml encoding='UTF-8' version='1.0'?>",
"<?xml version='1.0' standalone='yes' encoding='UTF-8'?>",
"<?xml version='1.0'encoding='UTF-8'?>",
"<?xml version=1.0?>",
"<?xml version='1.0' standalone='YES'?>",
"<?xml version='1.0' encoding='UTF-16'?>",
"<?xml version='1.0'?>",
"<?xml version='1.0' encoding='UTF-8'?>",
"<?xml version='1.0' standalone='yes'?>",
"<?xml version='1.1'?>",
" <?xml version='1.0'?>",
"<!-- ok --><?xml version='1.0'?>",
] {
assert!(
parse(&format!("{declaration}<A/>")).is_err(),
"{declaration}"
);
}
for declaration in [
"<?xml version='1.0'?>",
"\u{feff}<?xml\nversion = \"1.0\" encoding='utf-8' standalone = 'no' ?>",
"<?xml version='1.0' standalone='yes'?>",
] {
assert!(
parse(&format!("{declaration}<A/>")).is_ok(),
"{declaration}"
);
}
}
#[test]
fn comments_and_character_data_follow_xml_10_grammar() {
for bad in ["<!-- bad -- comment -->", "<!-- bad --->", "<!-- -- -->"] {
for input in [
format!("{bad}<A/>"),
format!("<A>{bad}</A>"),
format!("<A/>{bad}"),
] {
assert!(parse(&input).is_err(), "{input}");
}
}
for good in [
"<!---->",
"<!---good-->",
"<!-- good-comment -->",
"<!-- ]]> -->",
] {
assert!(parse(&format!("<A>{good}</A>")).is_ok(), "{good}");
}
assert!(parse("<A>forbidden ]]></A>").is_err());
assert!(parse("<A>escaped ]]></A>").is_ok());
assert!(parse("<A attr=']]>'/>").is_ok());
}
}