use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use quick_xml::Reader;
use quick_xml::events::Event;
use quick_xml::events::attributes::Attribute as QxAttribute;
use quick_xml::name::{PrefixDeclaration, QName as QxQName};
use crate::error::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QName {
pub namespace: Option<String>,
pub local: String,
}
impl QName {
pub fn new(ns: impl Into<Option<String>>, local: impl Into<String>) -> Self {
Self {
namespace: ns.into(),
local: local.into(),
}
}
pub fn local(&self) -> &str {
&self.local
}
pub fn namespace(&self) -> Option<&str> {
self.namespace.as_deref()
}
}
impl fmt::Display for QName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.namespace {
Some(ns) => write!(f, "{{{}}}{}", ns, self.local),
None => f.write_str(&self.local),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct XmlLimits {
pub max_depth: usize,
pub max_total_nodes: usize,
pub max_attribute_count: usize,
pub max_text_length: usize,
}
impl Default for XmlLimits {
fn default() -> Self {
Self {
max_depth: 100,
max_total_nodes: 100_000,
max_attribute_count: 100,
max_text_length: 1_048_576,
}
}
}
impl XmlLimits {
pub const fn aggregate() -> Self {
Self {
max_depth: 100,
max_total_nodes: 50_000_000,
max_attribute_count: 100,
max_text_length: 1_048_576,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ElementId(pub(crate) u32);
#[derive(Debug, Clone)]
pub(crate) struct Element {
pub(crate) qname: QName,
pub(crate) source_prefix: Option<String>,
pub(crate) namespaces_declared_here: Vec<(Option<String>, String)>,
pub(crate) attributes: Vec<Attribute>,
pub(crate) children: Vec<Node>,
pub(crate) id: ElementId,
}
#[derive(Debug, Clone)]
pub(crate) struct Attribute {
pub(crate) qname: QName,
pub(crate) source_prefix: Option<String>,
pub(crate) value: String,
}
#[derive(Debug, Clone)]
pub(crate) enum Node {
Element(Element),
Text(String),
Comment(String),
}
impl Element {
pub(crate) fn qname(&self) -> &QName {
&self.qname
}
pub(crate) fn id(&self) -> ElementId {
self.id
}
pub(crate) fn attribute(&self, namespace: Option<&str>, local: &str) -> Option<&str> {
for attr in &self.attributes {
if attr.qname.local == local && attr.qname.namespace.as_deref() == namespace {
return Some(attr.value.as_str());
}
}
None
}
pub(crate) fn attributes_with_source_prefix(
&self,
) -> impl Iterator<Item = (&QName, Option<&str>, &str)> {
self.attributes.iter().map(|attr| {
(
&attr.qname,
attr.source_prefix.as_deref(),
attr.value.as_str(),
)
})
}
pub(crate) fn children(&self) -> impl Iterator<Item = &Node> {
self.children.iter()
}
pub(crate) fn child_elements(&self) -> impl Iterator<Item = &Element> {
self.children.iter().filter_map(|child| match child {
Node::Element(e) => Some(e),
_ => None,
})
}
pub(crate) fn child_element<'a>(
&'a self,
namespace: Option<&str>,
local: &str,
) -> Option<&'a Element> {
self.child_elements().find(|child| {
child.qname.local == local && child.qname.namespace.as_deref() == namespace
})
}
pub(crate) fn all_child_elements<'a>(
&'a self,
namespace: Option<&str>,
local: &str,
) -> impl Iterator<Item = &'a Element> {
let ns_owned = namespace.map(str::to_owned);
let local_owned = local.to_owned();
self.child_elements().filter(move |child| {
child.qname.local == local_owned && child.qname.namespace == ns_owned
})
}
pub(crate) fn text_content(&self) -> String {
let mut out = String::new();
for child in &self.children {
if let Node::Text(t) = child {
out.push_str(t);
}
}
out
}
pub(crate) fn insert_child(&mut self, index: usize, node: Node) {
self.children.insert(index, node);
}
}
pub(crate) type ElementPath = Vec<u32>;
#[derive(Debug, Clone)]
pub(crate) struct Document {
pub(crate) root: Element,
pub(crate) id_index: HashMap<String, ElementId>,
pub(crate) paths: Vec<ElementPath>,
}
impl Document {
pub(crate) fn parse(xml: &[u8]) -> Result<Document, Error> {
Self::parse_with_limits(xml, XmlLimits::default())
}
pub(crate) fn parse_with_limits(xml: &[u8], limits: XmlLimits) -> Result<Document, Error> {
parse_inner(xml, &limits)
}
pub(crate) fn root(&self) -> &Element {
&self.root
}
pub(crate) fn element(&self, id: ElementId) -> Option<&Element> {
let path = self.paths.get(id.0 as usize)?;
let mut current: &Element = &self.root;
for &idx in path {
let node = current.children.get(idx as usize)?;
match node {
Node::Element(child) => current = child,
_ => return None,
}
}
Some(current)
}
pub(crate) fn element_by_id_attr(&self, id_attr: &str) -> Option<ElementId> {
self.id_index.get(id_attr).copied()
}
#[cfg(test)]
pub(crate) fn find_first<'a>(
&'a self,
namespace: Option<&str>,
local: &str,
) -> Option<&'a Element> {
find_first_in(&self.root, namespace, local)
}
}
#[cfg(test)]
fn find_first_in<'a>(
element: &'a Element,
namespace: Option<&str>,
local: &str,
) -> Option<&'a Element> {
if element.qname.local == local && element.qname.namespace.as_deref() == namespace {
return Some(element);
}
for child in element.child_elements() {
if let Some(found) = find_first_in(child, namespace, local) {
return Some(found);
}
}
None
}
pub(crate) const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
struct StackFrame {
element: Element,
path: ElementPath,
}
#[derive(Default)]
struct NsLayer {
bindings: Vec<(Option<Vec<u8>>, String)>,
}
fn resolve_prefix<'a>(stack: &'a [NsLayer], prefix: Option<&[u8]>) -> Option<&'a str> {
if let Some(p) = prefix
&& p == b"xml"
{
return Some(XML_NS);
}
for layer in stack.iter().rev() {
for (decl_prefix, uri) in layer.bindings.iter().rev() {
if decl_prefix.as_deref() == prefix {
return if uri.is_empty() {
None
} else {
Some(uri.as_str())
};
}
}
}
None
}
pub(crate) fn normalize_line_endings(xml: &[u8]) -> Cow<'_, [u8]> {
if !xml.contains(&b'\r') {
return Cow::Borrowed(xml);
}
let mut out: Vec<u8> = Vec::with_capacity(xml.len());
let mut iter = xml.iter().peekable();
while let Some(&b) = iter.next() {
if b == b'\r' {
out.push(b'\n');
if iter.peek().copied().copied() == Some(b'\n') {
iter.next();
}
} else {
out.push(b);
}
}
Cow::Owned(out)
}
pub(crate) fn unescape_value(raw: &[u8]) -> Result<String, Error> {
let decoded = std::str::from_utf8(raw)
.map_err(|err| Error::XmlParse(format!("non-UTF-8 attribute value: {err}")))?;
quick_xml::escape::unescape(decoded)
.map(Cow::into_owned)
.map_err(|e| Error::XmlParse(format!("attribute value decode: {e}")))
}
pub(crate) fn configure_reader<R>(reader: &mut Reader<R>) {
let cfg = reader.config_mut();
cfg.expand_empty_elements = false;
cfg.trim_text_start = false;
cfg.trim_text_end = false;
cfg.check_end_names = true;
}
pub(crate) fn reject_doctype_or_pi(event: &Event<'_>) -> Option<Error> {
match event {
Event::DocType(_) => Some(Error::XmlParse("DTDs not allowed".to_string())),
Event::PI(_) => Some(Error::XmlParse(
"processing instructions not allowed".to_string(),
)),
_ => None,
}
}
pub(crate) fn collect_namespace_decls(
start: &quick_xml::events::BytesStart<'_>,
) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for attr in start.attributes().with_checks(false).flatten() {
let key = attr.key.into_inner();
let is_ns = matches!(
QxQName(key).as_namespace_binding(),
Some(PrefixDeclaration::Default | PrefixDeclaration::Named(_))
);
if !is_ns {
continue;
}
out.push((key.to_vec(), attr.value.to_vec()));
}
out
}
fn parse_inner(xml: &[u8], limits: &XmlLimits) -> Result<Document, Error> {
let normalized = normalize_line_endings(xml);
let mut reader = Reader::from_reader(normalized.as_ref());
configure_reader(&mut reader);
let mut builder = TreeBuilder::new(*limits);
loop {
let event = reader
.read_event()
.map_err(|e| Error::XmlParse(format!("quick-xml: {e}")))?;
if matches!(event, Event::Eof) {
break;
}
builder.feed(&event)?;
}
builder.finish()
}
pub(crate) struct TreeBuilder {
limits: XmlLimits,
stack: Vec<StackFrame>,
ns_stack: Vec<NsLayer>,
paths: Vec<ElementPath>,
id_index: HashMap<String, ElementId>,
completed_root: Option<Element>,
total_nodes: usize,
pending_text: String,
}
impl TreeBuilder {
pub(crate) fn new(limits: XmlLimits) -> Self {
Self {
limits,
stack: Vec::new(),
ns_stack: Vec::new(),
paths: Vec::new(),
id_index: HashMap::new(),
completed_root: None,
total_nodes: 0,
pending_text: String::new(),
}
}
pub(crate) fn with_seed_scope(
limits: XmlLimits,
seed: &[(Vec<u8>, Vec<u8>)],
) -> Result<Self, Error> {
let mut layer = NsLayer::default();
for (key, value) in seed {
let value_str = unescape_value(value)?;
match QxQName(key).as_namespace_binding() {
Some(PrefixDeclaration::Default) => layer.bindings.push((None, value_str)),
Some(PrefixDeclaration::Named(prefix)) => {
layer.bindings.push((Some(prefix.to_vec()), value_str));
}
None => {}
}
}
let mut builder = Self::new(limits);
builder.ns_stack.push(layer);
Ok(builder)
}
pub(crate) fn feed(&mut self, event: &Event<'_>) -> Result<(), Error> {
match event {
Event::Text(text) => {
let decoded = text
.decode()
.map_err(|e| Error::XmlParse(format!("text decode: {e}")))?;
self.append_text(&decoded)
}
Event::GeneralRef(general_ref) => {
let decoded = general_ref
.decode()
.map_err(|e| Error::XmlParse(format!("entity reference decode: {e}")))?;
let raw = format!("&{decoded};");
let resolved = quick_xml::escape::unescape(&raw)
.map_err(|e| Error::XmlParse(format!("entity reference: {e}")))?;
self.append_text(&resolved)
}
_ => {
self.flush_text()?;
self.feed_structural(event)
}
}
}
fn feed_structural(&mut self, event: &Event<'_>) -> Result<(), Error> {
match event {
Event::Eof => Ok(()),
Event::Decl(_) => {
Ok(())
}
Event::DocType(_) | Event::PI(_) => Err(reject_doctype_or_pi(event)
.unwrap_or_else(|| Error::XmlParse("unexpected event".to_string()))),
Event::Start(start) => {
self.bump_nodes()?;
let (element, path) = open_element(
start,
false,
&self.stack,
&mut self.ns_stack,
&mut self.paths,
&mut self.id_index,
&self.limits,
)?;
self.stack.push(StackFrame { element, path });
if self.stack.len() > self.limits.max_depth {
return Err(Error::XmlParse("max depth exceeded".to_string()));
}
Ok(())
}
Event::Empty(start) => {
self.bump_nodes()?;
let (element, _path) = open_element(
start,
true,
&self.stack,
&mut self.ns_stack,
&mut self.paths,
&mut self.id_index,
&self.limits,
)?;
close_element(element, &mut self.stack, &mut self.completed_root)
}
Event::End(end) => {
let frame = self
.stack
.pop()
.ok_or_else(|| Error::XmlParse("unmatched end tag".to_string()))?;
self.ns_stack.pop();
let expected_local = &frame.element.qname.local;
let actual = end.name();
let actual_local_name = actual.local_name();
let actual_local = std::str::from_utf8(actual_local_name.as_ref())
.map_err(|err| Error::XmlParse(format!("non-UTF-8 element name: {err}")))?;
if actual_local != expected_local.as_str() {
return Err(Error::XmlParse(format!(
"end tag mismatch: expected </{expected_local}>, got </{actual_local}>"
)));
}
close_element(frame.element, &mut self.stack, &mut self.completed_root)
}
Event::Text(_) | Event::GeneralRef(_) => {
Err(Error::XmlParse(
"internal: character data reached structural handler".to_string(),
))
}
Event::CData(cdata) => {
let value = std::str::from_utf8(cdata.as_ref())
.map_err(|err| Error::XmlParse(format!("non-UTF-8 CDATA: {err}")))?
.to_owned();
if value.len() > self.limits.max_text_length {
return Err(Error::XmlParse("max text length exceeded".to_string()));
}
if value.is_empty() {
return Ok(());
}
self.push_text(value)
}
Event::Comment(comment) => {
let value = std::str::from_utf8(comment.as_ref())
.map_err(|err| Error::XmlParse(format!("non-UTF-8 comment: {err}")))?
.to_owned();
if value.len() > self.limits.max_text_length {
return Err(Error::XmlParse("max text length exceeded".to_string()));
}
self.bump_nodes()?;
if let Some(frame) = self.stack.last_mut() {
frame.element.children.push(Node::Comment(value));
}
Ok(())
}
}
}
pub(crate) fn finish(self) -> Result<Document, Error> {
if !self.stack.is_empty() {
return Err(Error::XmlParse("unclosed element at EOF".to_string()));
}
let root = self
.completed_root
.ok_or_else(|| Error::XmlParse("empty document".to_string()))?;
Ok(Document {
root,
id_index: self.id_index,
paths: self.paths,
})
}
pub(crate) fn finish_root(self) -> Result<Element, Error> {
if !self.stack.is_empty() {
return Err(Error::XmlParse("unclosed element at EOF".to_string()));
}
self.completed_root
.ok_or_else(|| Error::XmlParse("empty document".to_string()))
}
fn bump_nodes(&mut self) -> Result<(), Error> {
self.total_nodes = self
.total_nodes
.checked_add(1)
.ok_or_else(|| Error::XmlParse("max nodes exceeded".to_string()))?;
if self.total_nodes > self.limits.max_total_nodes {
return Err(Error::XmlParse("max nodes exceeded".to_string()));
}
Ok(())
}
fn append_text(&mut self, fragment: &str) -> Result<(), Error> {
if self
.pending_text
.len()
.checked_add(fragment.len())
.is_none_or(|total| total > self.limits.max_text_length)
{
return Err(Error::XmlParse("max text length exceeded".to_string()));
}
self.pending_text.push_str(fragment);
Ok(())
}
fn flush_text(&mut self) -> Result<(), Error> {
if self.pending_text.is_empty() {
return Ok(());
}
let value = std::mem::take(&mut self.pending_text);
self.push_text(value)
}
fn push_text(&mut self, value: String) -> Result<(), Error> {
if self.stack.is_empty() {
return Ok(());
}
self.bump_nodes()?;
if let Some(frame) = self.stack.last_mut() {
frame.element.children.push(Node::Text(value));
}
Ok(())
}
}
fn open_element(
start: &quick_xml::events::BytesStart<'_>,
self_closing: bool,
stack: &[StackFrame],
ns_stack: &mut Vec<NsLayer>,
paths: &mut Vec<ElementPath>,
id_index: &mut HashMap<String, ElementId>,
limits: &XmlLimits,
) -> Result<(Element, ElementPath), Error> {
let mut new_layer = NsLayer::default();
let mut declared: Vec<(Option<String>, String)> = Vec::new();
let mut raw_attrs: Vec<(Vec<u8>, String)> = Vec::new();
let mut attribute_count: usize = 0;
for attr_result in start.attributes() {
let attr: QxAttribute<'_> =
attr_result.map_err(|e| Error::XmlParse(format!("attribute: {e}")))?;
attribute_count = attribute_count
.checked_add(1)
.ok_or_else(|| Error::XmlParse("max attributes per element exceeded".to_string()))?;
if attribute_count > limits.max_attribute_count {
return Err(Error::XmlParse(
"max attributes per element exceeded".to_string(),
));
}
let key_bytes = attr.key.into_inner().to_vec();
let value = unescape_value(&attr.value)?;
match QxQName(&key_bytes).as_namespace_binding() {
Some(PrefixDeclaration::Default) => {
declared.push((None, value.clone()));
new_layer.bindings.push((None, value));
}
Some(PrefixDeclaration::Named(prefix_bytes)) => {
let prefix_str = std::str::from_utf8(prefix_bytes)
.map_err(|err| Error::XmlParse(format!("non-UTF-8 namespace prefix: {err}")))?
.to_owned();
declared.push((Some(prefix_str), value.clone()));
new_layer
.bindings
.push((Some(prefix_bytes.to_vec()), value));
}
None => {
raw_attrs.push((key_bytes, value));
}
}
}
ns_stack.push(new_layer);
let raw_name = start.name();
let raw_name_bytes = raw_name.into_inner();
let elem_qname = resolve_qname(raw_name_bytes, ns_stack, false)?;
let elem_source_prefix = extract_source_prefix(raw_name_bytes)?;
let mut resolved_attrs: Vec<Attribute> = Vec::with_capacity(raw_attrs.len());
for (key_bytes, value) in raw_attrs {
let qn = resolve_qname(&key_bytes, ns_stack, true)?;
let source_prefix = extract_source_prefix(&key_bytes)?;
resolved_attrs.push(Attribute {
qname: qn,
source_prefix,
value,
});
}
let path: ElementPath = if let Some(parent) = stack.last() {
let mut p = parent.path.clone();
let child_index = u32::try_from(parent.element.children.len())
.map_err(|_err| Error::XmlParse("element index exceeds u32::MAX".to_string()))?;
p.push(child_index);
p
} else {
Vec::new()
};
let id_value = ElementId(
u32::try_from(paths.len())
.map_err(|_err| Error::XmlParse("element id exceeds u32::MAX".to_string()))?,
);
paths.push(path.clone());
for attr in &resolved_attrs {
let is_id_attr = (attr.qname.namespace.is_none() && attr.qname.local == "ID")
|| (attr.qname.namespace.as_deref() == Some(XML_NS) && attr.qname.local == "id");
if is_id_attr {
if id_index.contains_key(&attr.value) {
return Err(Error::XmlParse("duplicate ID".to_string()));
}
id_index.insert(attr.value.clone(), id_value);
}
}
let element = Element {
qname: elem_qname,
source_prefix: elem_source_prefix,
namespaces_declared_here: declared,
attributes: resolved_attrs,
children: Vec::new(),
id: id_value,
};
if self_closing {
ns_stack.pop();
}
Ok((element, path))
}
fn close_element(
element: Element,
stack: &mut [StackFrame],
completed_root: &mut Option<Element>,
) -> Result<(), Error> {
if let Some(parent) = stack.last_mut() {
parent.element.children.push(Node::Element(element));
Ok(())
} else if completed_root.is_some() {
Err(Error::XmlParse(
"multiple root elements not allowed".to_string(),
))
} else {
*completed_root = Some(element);
Ok(())
}
}
pub(crate) fn raw_local_name(name: &[u8]) -> &[u8] {
QxQName(name).local_name().into_inner()
}
fn extract_source_prefix(name: &[u8]) -> Result<Option<String>, Error> {
let q = QxQName(name);
let (_local, prefix) = q.decompose();
match prefix {
Some(p) => {
let s = std::str::from_utf8(p.as_ref())
.map_err(|err| Error::XmlParse(format!("non-UTF-8 namespace prefix: {err}")))?;
Ok(Some(s.to_owned()))
}
None => Ok(None),
}
}
fn resolve_qname(name: &[u8], ns_stack: &[NsLayer], is_attribute: bool) -> Result<QName, Error> {
let q = QxQName(name);
let (local, prefix) = q.decompose();
let local_str = std::str::from_utf8(local.as_ref())
.map_err(|err| Error::XmlParse(format!("non-UTF-8 local name: {err}")))?
.to_owned();
let namespace = match prefix {
Some(p) => {
let prefix_bytes = p.as_ref();
match resolve_prefix(ns_stack, Some(prefix_bytes)) {
Some(uri) => Some(uri.to_owned()),
None => {
return Err(Error::XmlParse(format!(
"unbound namespace prefix: {}",
std::str::from_utf8(prefix_bytes).unwrap_or("<invalid utf-8>")
)));
}
}
}
None => {
if is_attribute {
None
} else {
resolve_prefix(ns_stack, None).map(str::to_owned)
}
}
};
Ok(QName {
namespace,
local: local_str,
})
}
#[cfg(test)]
mod tests {
use std::fmt::Write as _;
use super::*;
fn parse(xml: &str) -> Document {
Document::parse(xml.as_bytes()).expect("parse should succeed")
}
#[test]
fn parses_simple_document() {
let doc = parse(r"<root><child>hello</child></root>");
assert_eq!(doc.root().qname().local(), "root");
assert_eq!(doc.root().qname().namespace(), None);
let child = doc.root().child_element(None, "child").unwrap();
assert_eq!(child.text_content(), "hello");
}
#[test]
fn round_trip_attributes_and_namespaces() {
let xml = r#"<a:root xmlns:a="urn:a" xmlns="urn:default" a:k="v" plain="p">
<inner/>
</a:root>"#;
let doc = parse(xml);
assert_eq!(doc.root().qname().namespace(), Some("urn:a"));
assert_eq!(doc.root().qname().local(), "root");
assert_eq!(doc.root().attribute(Some("urn:a"), "k"), Some("v"));
assert_eq!(doc.root().attribute(None, "plain"), Some("p"));
let inner = doc
.root()
.child_element(Some("urn:default"), "inner")
.unwrap();
assert_eq!(inner.qname().namespace(), Some("urn:default"));
let declared = doc.root().namespaces_declared_here.clone();
assert!(declared.contains(&(Some("a".to_owned()), "urn:a".to_owned())));
assert!(declared.contains(&(None, "urn:default".to_owned())));
}
#[test]
fn id_attribute_lookup_and_resolution() {
let xml = r#"<Response xmlns="urn:p" ID="abc"><Assertion ID="xyz"/></Response>"#;
let doc = parse(xml);
let response_id = doc.element_by_id_attr("abc").unwrap();
let assertion_id = doc.element_by_id_attr("xyz").unwrap();
assert_ne!(response_id, assertion_id);
let response = doc.element(response_id).unwrap();
assert_eq!(response.qname().local(), "Response");
let assertion = doc.element(assertion_id).unwrap();
assert_eq!(assertion.qname().local(), "Assertion");
}
#[test]
fn xml_id_attribute_is_indexed() {
let xml = r#"<root xml:id="rooted"><x xml:id="inner"/></root>"#;
let doc = parse(xml);
assert_eq!(doc.element_by_id_attr("rooted"), Some(doc.root().id()));
assert!(doc.element_by_id_attr("inner").is_some());
}
#[test]
fn duplicate_id_rejected() {
let xml = r#"<root><a ID="dup"/><b ID="dup"/></root>"#;
let err = Document::parse(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("duplicate ID"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn dtd_rejected() {
let xml = r"<!DOCTYPE foo><root/>";
let err = Document::parse(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("DTDs"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn processing_instruction_rejected() {
let xml = r"<?php evil ?><root/>";
let err = Document::parse(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("processing instruction"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn comments_preserved() {
let xml = r"<root><!-- hello --><child/><!-- world --></root>";
let doc = parse(xml);
let kinds: Vec<&str> = doc
.root()
.children()
.map(|n| match n {
Node::Element(_) => "elem",
Node::Text(_) => "text",
Node::Comment(_) => "comment",
})
.collect();
assert_eq!(kinds, vec!["comment", "elem", "comment"]);
let comments: Vec<String> = doc
.root()
.children()
.filter_map(|n| match n {
Node::Comment(c) => Some(c.clone()),
_ => None,
})
.collect();
assert_eq!(comments, vec![" hello ".to_string(), " world ".to_string()]);
}
#[test]
fn depth_limit_triggers() {
let depth = 50;
let mut xml = String::new();
for _ in 0..depth {
xml.push_str("<a>");
}
for _ in 0..depth {
xml.push_str("</a>");
}
let limits = XmlLimits {
max_depth: 10,
..XmlLimits::default()
};
let err = Document::parse_with_limits(xml.as_bytes(), limits).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("max depth"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn node_count_limit_triggers() {
let mut xml = String::from("<root>");
for _ in 0..20 {
xml.push_str("<x/>");
}
xml.push_str("</root>");
let limits = XmlLimits {
max_total_nodes: 5,
..XmlLimits::default()
};
let err = Document::parse_with_limits(xml.as_bytes(), limits).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("max nodes"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn attribute_count_limit_triggers() {
let mut xml = String::from("<root");
for i in 0..20 {
write!(xml, r#" a{i}="v""#).unwrap();
}
xml.push_str("/>");
let limits = XmlLimits {
max_attribute_count: 5,
..XmlLimits::default()
};
let err = Document::parse_with_limits(xml.as_bytes(), limits).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("max attributes"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn text_length_limit_triggers() {
let big = "x".repeat(2048);
let xml = format!("<root>{big}</root>");
let limits = XmlLimits {
max_text_length: 1024,
..XmlLimits::default()
};
let err = Document::parse_with_limits(xml.as_bytes(), limits).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("max text length"), "got: {msg}"),
_ => panic!("expected XmlParse"),
}
}
#[test]
fn find_first_recursive() {
let xml = r#"<a xmlns="urn:n"><b><c>found</c></b></a>"#;
let doc = parse(xml);
let c = doc.find_first(Some("urn:n"), "c").unwrap();
assert_eq!(c.text_content(), "found");
}
#[test]
fn text_content_preserves_internal_whitespace() {
let xml = "<root>hello world\n</root>";
let doc = parse(xml);
assert_eq!(doc.root().text_content(), "hello world\n");
}
#[test]
fn unbound_prefix_is_rejected() {
let xml = r"<a:root/>";
let err = Document::parse(xml.as_bytes()).unwrap_err();
assert!(matches!(err, Error::XmlParse(_)));
}
#[test]
fn element_handle_resolution_round_trip() {
let xml = r"<root><a/><b><c/></b></root>";
let doc = parse(xml);
let b = doc.root().child_element(None, "b").unwrap();
let c = b.child_element(None, "c").unwrap();
assert_eq!(doc.element(c.id()).unwrap().qname().local(), "c");
assert_eq!(doc.element(b.id()).unwrap().qname().local(), "b");
assert_eq!(
doc.element(doc.root().id()).unwrap().qname().local(),
"root"
);
}
}