use quick_xml::encoding::Decoder;
use quick_xml::events::{BytesStart, Event};
use quick_xml::{Reader, XmlVersion};
pub(crate) const MAX_XML_DEPTH: usize = 256;
#[derive(Debug)]
pub(crate) enum XmlError {
Limit(String),
Invalid(String),
}
#[derive(Debug, Default)]
pub(crate) struct Element {
pub name: String,
pub attrs: Vec<(String, String)>,
pub children: Vec<XNode>,
}
#[derive(Debug)]
pub(crate) enum XNode {
Element(Element),
Text(String),
}
impl Element {
pub fn attr(&self, local: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == local)
.map(|(_, v)| v.as_str())
}
pub fn elements(&self) -> impl Iterator<Item = &Element> {
self.children.iter().filter_map(|c| match c {
XNode::Element(e) => Some(e),
XNode::Text(_) => None,
})
}
pub fn child(&self, local: &str) -> Option<&Element> {
self.elements().find(|e| e.name == local)
}
pub fn child_val(&self, local: &str) -> Option<&str> {
self.child(local).and_then(|c| c.attr("val"))
}
pub fn text(&self) -> String {
let mut out = String::new();
let mut pending: Vec<std::slice::Iter<'_, XNode>> = vec![self.children.iter()];
while let Some(iter) = pending.last_mut() {
match iter.next() {
Some(XNode::Text(t)) => out.push_str(t),
Some(XNode::Element(e)) => pending.push(e.children.iter()),
None => {
pending.pop();
}
}
}
out
}
pub fn find(&self, local: &str) -> Option<&Element> {
let mut stack: Vec<&Element> = vec![self];
while let Some(e) = stack.pop() {
if e.name == local {
return Some(e);
}
let kids: Vec<&Element> = e.elements().collect();
stack.extend(kids.into_iter().rev());
}
None
}
}
fn local(name: &[u8]) -> String {
let local = match name.iter().rposition(|&b| b == b':') {
Some(i) => &name[i + 1..],
None => name,
};
String::from_utf8_lossy(local).into_owned()
}
fn element_from(start: &BytesStart<'_>, decoder: Decoder) -> Element {
let mut attrs = Vec::new();
for attr in start.attributes().with_checks(false).flatten() {
let key = local(attr.key.as_ref());
let value = match attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder) {
Ok(v) => v.into_owned(),
Err(_) => String::from_utf8_lossy(&attr.value).into_owned(),
};
attrs.push((key, value));
}
Element {
name: local(start.name().as_ref()),
attrs,
children: Vec::new(),
}
}
fn push_text(parent: &mut Element, text: &str) {
if text.is_empty() {
return;
}
if let Some(XNode::Text(prev)) = parent.children.last_mut() {
prev.push_str(text);
} else {
parent.children.push(XNode::Text(text.to_string()));
}
}
pub(crate) fn parse(bytes: &[u8]) -> Result<Element, XmlError> {
let mut reader = Reader::from_reader(bytes);
reader.config_mut().check_end_names = false;
let mut stack: Vec<Element> = vec![Element::default()];
let mut buf = Vec::new();
loop {
let event = reader
.read_event_into(&mut buf)
.map_err(|e| XmlError::Invalid(format!("malformed XML: {e}")))?;
match event {
Event::Start(s) => {
if stack.len() > MAX_XML_DEPTH {
return Err(XmlError::Limit(format!(
"XML nesting deeper than {MAX_XML_DEPTH}"
)));
}
stack.push(element_from(&s, reader.decoder()));
}
Event::Empty(s) => {
let el = element_from(&s, reader.decoder());
if let Some(top) = stack.last_mut() {
top.children.push(XNode::Element(el));
}
}
Event::End(_) => {
if stack.len() > 1 {
let el = stack.pop().unwrap_or_default();
if let Some(top) = stack.last_mut() {
top.children.push(XNode::Element(el));
}
}
}
Event::Text(t) => {
let text = t
.xml_content(XmlVersion::Implicit1_0)
.map_err(|e| XmlError::Invalid(format!("bad text encoding: {e}")))?;
if let Some(top) = stack.last_mut() {
push_text(top, &text);
}
}
Event::CData(t) => {
let text = String::from_utf8_lossy(&t);
if let Some(top) = stack.last_mut() {
push_text(top, &text);
}
}
Event::GeneralRef(r) => {
let resolved: Option<String> = match r.resolve_char_ref() {
Ok(Some(c)) => Some(c.to_string()),
Ok(None) => {
let name = String::from_utf8_lossy(&r);
quick_xml::escape::resolve_predefined_entity(&name).map(str::to_string)
}
Err(_) => None,
};
if let (Some(text), Some(top)) = (resolved, stack.last_mut()) {
push_text(top, &text);
}
}
Event::Eof => break,
Event::Decl(_) | Event::PI(_) | Event::Comment(_) | Event::DocType(_) => {}
}
buf.clear();
}
while stack.len() > 1 {
let el = stack.pop().unwrap_or_default();
if let Some(top) = stack.last_mut() {
top.children.push(XNode::Element(el));
}
}
Ok(stack.pop().unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_local_names_text_and_refs() {
let root = parse(br#"<a:root x:k="v&w"><b>1 < 2 A</b><c/></a:root>"#).unwrap();
let r = root.child("root").unwrap();
assert_eq!(r.attr("k"), Some("v&w"));
assert_eq!(r.child("b").unwrap().text(), "1 < 2 A");
assert!(r.child("c").is_some());
assert_eq!(r.text(), "1 < 2 A");
assert_eq!(root.find("c").map(|e| e.name.as_str()), Some("c"));
}
#[test]
fn depth_limited() {
let deep = "<a>".repeat(MAX_XML_DEPTH + 10);
assert!(matches!(parse(deep.as_bytes()), Err(XmlError::Limit(_))));
}
#[test]
fn unknown_entities_dropped() {
let root = parse(br#"<!DOCTYPE x [<!ENTITY e "boom">]><x>a&e;b</x>"#).unwrap();
assert_eq!(root.child("x").unwrap().text(), "ab");
}
}