use std::collections::BTreeMap;
use quick_xml::escape::unescape;
use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
use super::XmlImportError;
#[derive(Debug)]
pub(super) struct Node {
pub name: String,
pub attributes: BTreeMap<String, String>,
pub text: String,
pub children: Vec<Node>,
}
impl Node {
pub fn child(&self, name: &str) -> Option<&Node> {
self.children.iter().find(|child| child.name == name)
}
pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Node> {
self.children.iter().filter(move |child| child.name == name)
}
pub fn text_trimmed(&self) -> Option<&str> {
let value = self.text.trim();
(!value.is_empty()).then_some(value)
}
pub fn child_text(&self, name: &str) -> Option<&str> {
self.child(name).and_then(Node::text_trimmed)
}
pub fn attribute(&self, name: &str) -> Option<&str> {
self.attributes.get(name).map(String::as_str)
}
}
pub(super) fn parse(xml: &str, limits: super::ImportLimits) -> Result<Node, XmlImportError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(false);
let mut stack = Vec::new();
let mut root = None;
let mut nodes = 0usize;
loop {
match reader.read_event() {
Ok(Event::Start(start)) => {
check_node(&limits, &mut nodes, stack.len() + 1)?;
stack.push(new_node(&start)?);
}
Ok(Event::Empty(start)) => {
check_node(&limits, &mut nodes, stack.len() + 1)?;
let node = new_node(&start)?;
append(node, &mut stack, &mut root)?;
}
Ok(Event::Text(text)) => {
if let Some(parent) = stack.last_mut() {
parent.text.push_str(&text);
}
}
Ok(Event::GeneralRef(reference)) => {
let resolved = unescape(&format!("&{};", &*reference))
.map(std::borrow::Cow::into_owned)
.map_err(|error| XmlImportError::Xml(error.to_string()))?;
if let Some(parent) = stack.last_mut() {
parent.text.push_str(&resolved);
}
}
Ok(Event::CData(text)) => {
if let Some(parent) = stack.last_mut() {
parent.text.push_str(&text);
}
}
Ok(Event::End(_)) => {
let node = stack
.pop()
.ok_or_else(|| XmlImportError::Xml("unexpected closing tag".into()))?;
append(node, &mut stack, &mut root)?;
}
Ok(Event::DocType(_)) => {
return Err(XmlImportError::Xml(
"document type declarations are not supported".into(),
));
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(error) => return Err(XmlImportError::Xml(error.to_string())),
}
}
if !stack.is_empty() {
return Err(XmlImportError::Xml("unclosed XML element".into()));
}
root.ok_or(XmlImportError::MissingRoot)
}
fn check_node(
limits: &super::ImportLimits,
nodes: &mut usize,
depth: usize,
) -> Result<(), XmlImportError> {
*nodes += 1;
if *nodes > limits.max_nodes {
return Err(XmlImportError::LimitExceeded {
kind: "nodes",
limit: limits.max_nodes,
});
}
if depth > limits.max_depth {
return Err(XmlImportError::LimitExceeded {
kind: "depth",
limit: limits.max_depth,
});
}
Ok(())
}
fn new_node(start: &BytesStart<'_>) -> Result<Node, XmlImportError> {
let name = local_name(start.name().as_ref());
let mut attributes = BTreeMap::new();
for attribute in start.attributes() {
let attribute = attribute.map_err(|error| XmlImportError::Xml(error.to_string()))?;
let key = local_name(attribute.key.as_ref());
let value =
unescape(&attribute.value).map_err(|error| XmlImportError::Xml(error.to_string()))?;
attributes.insert(key, value.into_owned());
}
Ok(Node {
name,
attributes,
text: String::new(),
children: Vec::new(),
})
}
fn append(node: Node, stack: &mut [Node], root: &mut Option<Node>) -> Result<(), XmlImportError> {
if let Some(parent) = stack.last_mut() {
parent.children.push(node);
} else if root.replace(node).is_some() {
return Err(XmlImportError::MultipleRoots);
}
Ok(())
}
fn local_name(name: &str) -> String {
name.rsplit(':').next().unwrap_or(name).to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unescapes_references_and_keeps_literal_whitespace() {
let xml = "<Root note=\"tab\there\nline\r\nend & <>"' AB\">\
<Text>tab\there\nline\r\nend & <>"' AB</Text>\
<Raw><![CDATA[raw & <kept>]]></Raw></Root>";
let root = parse(xml, super::super::ImportLimits::default()).unwrap();
let expected = "tab\there\nline\r\nend & <>\"' AB";
assert_eq!(root.attribute("note"), Some(expected));
assert_eq!(
root.child("Text").map(|node| node.text.as_str()),
Some(expected)
);
assert_eq!(
root.child("Raw").map(|node| node.text.as_str()),
Some("raw & <kept>")
);
}
#[test]
fn refuses_undeclared_entities() {
for xml in ["<Root a=\"&bogus;\"/>", "<Root>&bogus;</Root>"] {
let error = parse(xml, super::super::ImportLimits::default())
.expect_err("an undeclared entity is refused, not passed through");
assert!(error.to_string().contains("bogus"), "{error}");
}
}
}