use quick_xml::events::{BytesRef, Event};
use quick_xml::{Reader, XmlVersion};
use crate::error::{Error, Result};
const MAX_DEPTH: usize = 256;
const MAX_NODES: usize = 8_000_000;
#[cfg(test)]
thread_local! {
static TEST_NODE_BUDGET: std::cell::Cell<usize> = const { std::cell::Cell::new(MAX_NODES) };
}
#[cfg(test)]
pub(crate) fn set_test_node_budget(n: usize) {
TEST_NODE_BUDGET.with(|c| c.set(n));
}
#[cfg(test)]
pub(crate) fn reset_test_node_budget() {
TEST_NODE_BUDGET.with(|c| c.set(MAX_NODES));
}
pub(crate) fn node_budget() -> usize {
#[cfg(test)]
{
TEST_NODE_BUDGET.with(|c| c.get())
}
#[cfg(not(test))]
{
MAX_NODES
}
}
#[cfg(test)]
thread_local! {
static FAIL_COMMIT_AFTER: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn set_test_fail_commit_after(n: usize) {
FAIL_COMMIT_AFTER.with(|c| c.set(Some(n)));
}
#[cfg(test)]
pub(crate) fn reset_test_fail_commit() {
FAIL_COMMIT_AFTER.with(|c| c.set(None));
}
#[cfg(test)]
fn commit_should_fail() -> bool {
FAIL_COMMIT_AFTER.with(|c| match c.get() {
None => false,
Some(0) => {
c.set(None);
true
}
Some(n) => {
c.set(Some(n - 1));
false
}
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub(crate) struct NodeId(u32);
#[derive(Debug, Clone)]
pub(crate) enum Node {
Element {
name: Vec<u8>,
attrs: Vec<(Vec<u8>, Vec<u8>)>,
self_closing: bool,
},
Text(Vec<u8>),
Raw(Vec<u8>),
}
#[derive(Debug, Clone)]
struct NodeData {
node: Node,
children: Vec<NodeId>,
depth: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct XmlTree {
nodes: Vec<NodeData>,
roots: Vec<NodeId>,
}
impl XmlTree {
pub(crate) fn parse(xml: &[u8]) -> Result<XmlTree> {
let mut reader = Reader::from_reader(xml);
let cfg = reader.config_mut();
cfg.expand_empty_elements = false; cfg.check_end_names = true;
let mut tree = XmlTree {
nodes: Vec::new(),
roots: Vec::new(),
};
let mut stack: Vec<NodeId> = Vec::new();
let mut general_refs = 0usize;
let mut buf = Vec::new();
loop {
if tree.nodes.len() > node_budget() {
return Err(Error::Xml("xml has too many nodes"));
}
let ev = reader
.read_event_into(&mut buf)
.map_err(|_| Error::Xml("malformed xml"))?;
match ev {
Event::Start(e) => {
if stack.len() >= MAX_DEPTH {
return Err(Error::Xml("xml nesting too deep"));
}
let node = element_node(&e, false)?;
let id = tree.push(node, stack.last().copied())?;
stack.push(id);
}
Event::Empty(e) => {
if stack.len() >= MAX_DEPTH {
return Err(Error::Xml("xml nesting too deep"));
}
let node = element_node(&e, true)?;
tree.push(node, stack.last().copied())?;
}
Event::End(_) => {
stack.pop();
}
Event::Text(t) => {
let raw = t.into_inner();
if raw.is_empty() {
continue;
}
match stack.last().copied() {
Some(parent) => {
tree.push_text_fragment(parent, &unescape_bytes(&raw)?)?;
}
None => {
tree.push(Node::Raw(raw.into_owned()), None)?;
}
}
}
Event::GeneralRef(reference) => {
general_refs += 1;
if general_refs > max_general_refs() {
return Err(Error::Xml("xml has too many entity references"));
}
match stack.last().copied() {
Some(parent) => {
push_resolved_general_ref(&mut tree, parent, &reference)?;
}
None => {
let mut raw = Vec::new();
raw.try_reserve(reference.as_ref().len() + 2)
.map_err(|_| Error::Xml("xml: out of memory growing raw text"))?;
raw.push(b'&');
raw.extend_from_slice(reference.as_ref());
raw.push(b';');
tree.push(Node::Raw(raw), None)?;
}
}
}
Event::CData(c) => {
let mut raw = b"<![CDATA[".to_vec();
raw.extend_from_slice(&c.into_inner());
raw.extend_from_slice(b"]]>");
tree.push(Node::Raw(raw), stack.last().copied())?;
}
Event::Comment(c) => {
let mut raw = b"<!--".to_vec();
raw.extend_from_slice(c.as_ref());
raw.extend_from_slice(b"-->");
tree.push(Node::Raw(raw), stack.last().copied())?;
}
Event::PI(p) => {
let mut raw = b"<?".to_vec();
raw.extend_from_slice(p.as_ref());
raw.extend_from_slice(b"?>");
tree.push(Node::Raw(raw), stack.last().copied())?;
}
Event::Decl(d) => {
if !stack.is_empty() || !tree.roots.is_empty() {
return Err(Error::Xml(
"xml declaration is only allowed at the start of the document",
));
}
let mut raw = b"<?".to_vec();
raw.extend_from_slice(d.as_ref());
raw.extend_from_slice(b"?>");
tree.push(Node::Raw(raw), stack.last().copied())?;
}
Event::DocType(d) => {
let mut raw = b"<!DOCTYPE ".to_vec();
raw.extend_from_slice(d.as_ref());
raw.extend_from_slice(b">");
tree.push(Node::Raw(raw), stack.last().copied())?;
}
Event::Eof => {
if !stack.is_empty() {
return Err(Error::Xml("xml ended with unclosed elements"));
}
break;
}
}
buf.clear();
}
Ok(tree)
}
pub(crate) fn serialize(&self) -> Vec<u8> {
let mut out = Vec::new();
for &root in &self.roots {
self.write_node(root, &mut out);
}
out
}
fn write_node(&self, id: NodeId, out: &mut Vec<u8>) {
let data = &self.nodes[id.0 as usize];
match &data.node {
Node::Raw(bytes) => out.extend_from_slice(bytes),
Node::Text(bytes) => esc_text_into(bytes, out),
Node::Element {
name,
attrs,
self_closing,
} => {
out.push(b'<');
out.extend_from_slice(name);
for (k, v) in attrs {
out.push(b' ');
out.extend_from_slice(k);
out.extend_from_slice(b"=\"");
esc_attr_into(v, out);
out.push(b'"');
}
if *self_closing && data.children.is_empty() {
out.extend_from_slice(b"/>");
} else {
out.push(b'>');
for &c in &data.children {
self.write_node(c, out);
}
out.extend_from_slice(b"</");
out.extend_from_slice(name);
out.push(b'>');
}
}
}
}
pub(crate) fn node_count(&self) -> usize {
self.nodes.len()
}
pub(crate) fn root_element(&self) -> Option<NodeId> {
self.roots
.iter()
.copied()
.find(|&id| matches!(self.nodes[id.0 as usize].node, Node::Element { .. }))
}
pub(crate) fn children_of(&self, id: NodeId) -> &[NodeId] {
&self.nodes[id.0 as usize].children
}
pub(crate) fn child_by_name(&self, parent: NodeId, name: &[u8]) -> Option<NodeId> {
self.nodes[parent.0 as usize]
.children
.iter()
.copied()
.find(|&c| match &self.nodes[c.0 as usize].node {
Node::Element { name: n, .. } => n.as_slice() == name,
_ => false,
})
}
pub(crate) fn element_name(&self, id: NodeId) -> Option<&[u8]> {
match &self.nodes[id.0 as usize].node {
Node::Element { name, .. } => Some(name.as_slice()),
_ => None,
}
}
pub(crate) fn attr_value(&self, id: NodeId, key: &[u8]) -> Option<&[u8]> {
match &self.nodes[id.0 as usize].node {
Node::Element { attrs, .. } => attrs
.iter()
.find(|(k, _)| k.as_slice() == key)
.map(|(_, v)| v.as_slice()),
_ => None,
}
}
pub(crate) fn text_of(&self, id: NodeId) -> String {
let mut out = String::new();
self.collect_text(id, &mut out);
out
}
fn collect_text(&self, id: NodeId, out: &mut String) {
match &self.nodes[id.0 as usize].node {
Node::Text(t) => out.push_str(&String::from_utf8_lossy(t)),
Node::Raw(r) => {
if let Some(inner) = r
.strip_prefix(b"<![CDATA[".as_slice())
.and_then(|s| s.strip_suffix(b"]]>".as_slice()))
{
out.push_str(&String::from_utf8_lossy(inner));
}
}
Node::Element { .. } => {
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_text(c, out);
}
}
}
}
pub(crate) fn set_element_text(&mut self, id: NodeId, text: &str) -> Result<()> {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Xml(
"simulated commit-time allocation failure (test seam)",
));
}
let reuse = self.nodes[id.0 as usize]
.children
.iter()
.copied()
.find(|&c| match &self.nodes[c.0 as usize].node {
Node::Text(_) => true,
Node::Raw(r) => r.starts_with(b"<![CDATA["),
Node::Element { .. } => false,
});
match reuse {
Some(tid) => {
self.nodes[tid.0 as usize].node = Node::Text(text.as_bytes().to_vec());
self.nodes[id.0 as usize].children = vec![tid];
}
None => {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Xml(
"simulated commit-time allocation failure (test seam)",
));
}
self.nodes
.try_reserve(1)
.map_err(|_| Error::Xml("xml: out of memory growing node arena"))?;
self.nodes[id.0 as usize]
.children
.try_reserve(1)
.map_err(|_| Error::Xml("xml: out of memory growing child list"))?;
self.nodes[id.0 as usize].children.clear();
self.push(Node::Text(text.as_bytes().to_vec()), Some(id))?;
}
}
Ok(())
}
pub(crate) fn set_attr(&mut self, id: NodeId, key: &[u8], val: &[u8]) -> Result<()> {
if let Node::Element { attrs, .. } = &mut self.nodes[id.0 as usize].node {
match attrs.iter_mut().find(|(k, _)| k.as_slice() == key) {
Some((_, v)) => *v = val.to_vec(),
None => {
if attrs.len() >= max_attrs() {
return Err(Error::Xml("element has too many attributes to add another"));
}
attrs.push((key.to_vec(), val.to_vec()));
}
}
}
Ok(())
}
pub(crate) fn can_set_attr(&self, id: NodeId, key: &[u8]) -> bool {
match &self.nodes[id.0 as usize].node {
Node::Element { attrs, .. } => {
attrs.iter().any(|(k, _)| k.as_slice() == key) || attrs.len() < max_attrs()
}
_ => true,
}
}
pub(crate) fn remove_attr(&mut self, id: NodeId, key: &[u8]) {
if let Node::Element { attrs, .. } = &mut self.nodes[id.0 as usize].node {
attrs.retain(|(k, _)| k.as_slice() != key);
}
}
pub(crate) fn remove_child(&mut self, parent: NodeId, id: NodeId) -> Result<()> {
let children = &mut self.nodes[parent.0 as usize].children;
let pos = children
.iter()
.position(|&c| c == id)
.ok_or(Error::Xml("child not found under parent"))?;
children.remove(pos);
Ok(())
}
pub(crate) fn insert_fragment_at(
&mut self,
parent: NodeId,
index: usize,
xml: &[u8],
) -> Result<NodeId> {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Xml(
"simulated commit-time allocation failure (test seam)",
));
}
let pos = index.min(self.nodes[parent.0 as usize].children.len());
let frag = XmlTree::parse(xml)?;
if self.nodes.len().saturating_add(frag.nodes.len()) > node_budget() {
return Err(Error::Xml("edit would exceed the node budget"));
}
let parent_depth = self.nodes[parent.0 as usize].depth;
let frag_max_depth = frag.nodes.iter().map(|n| n.depth).max().unwrap_or(0);
if parent_depth.saturating_add(frag_max_depth) > MAX_DEPTH {
return Err(Error::Xml(
"xml fragment insert would exceed the maximum nesting depth",
));
}
let pre_nodes_len = self.nodes.len();
let pre_children_len = self.nodes[parent.0 as usize].children.len();
let mut added: Vec<NodeId> = Vec::new();
for &r in &frag.roots {
match self.graft(&frag, r, parent) {
Ok(id) => added.push(id),
Err(e) => {
self.nodes.truncate(pre_nodes_len);
self.nodes[parent.0 as usize]
.children
.truncate(pre_children_len);
return Err(e);
}
}
}
let Some(&first) = added.first() else {
return Err(Error::Xml("xml fragment has no root nodes to insert"));
};
let n = added.len();
let pi = parent.0 as usize;
let head_len = self.nodes[pi].children.len() - n;
let ch = &mut self.nodes[pi].children;
let tail: Vec<NodeId> = ch.split_off(head_len);
for (k, id) in tail.into_iter().enumerate() {
ch.insert(pos + k, id);
}
Ok(first)
}
fn graft(&mut self, src: &XmlTree, src_id: NodeId, parent: NodeId) -> Result<NodeId> {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Xml(
"simulated commit-time allocation failure (test seam)",
));
}
let node = src.nodes[src_id.0 as usize].node.clone();
let new_id = self.push(node, Some(parent))?;
for &c in &src.nodes[src_id.0 as usize].children {
self.graft(src, c, new_id)?;
}
Ok(new_id)
}
fn push(&mut self, node: Node, parent: Option<NodeId>) -> Result<NodeId> {
self.nodes
.try_reserve(1)
.map_err(|_| Error::Xml("xml: out of memory growing node arena"))?;
match parent {
Some(p) => self.nodes[p.0 as usize]
.children
.try_reserve(1)
.map_err(|_| Error::Xml("xml: out of memory growing child list"))?,
None => self
.roots
.try_reserve(1)
.map_err(|_| Error::Xml("xml: out of memory growing root list"))?,
}
let depth = match parent {
Some(p) => self.nodes[p.0 as usize].depth + 1,
None => 1,
};
let id = NodeId(self.nodes.len() as u32);
self.nodes.push(NodeData {
node,
children: Vec::new(),
depth,
});
match parent {
Some(p) => self.nodes[p.0 as usize].children.push(id),
None => self.roots.push(id),
}
Ok(id)
}
fn push_text_fragment(&mut self, parent: NodeId, fragment: &[u8]) -> Result<()> {
if fragment.is_empty() {
return Ok(());
}
let last = self.nodes[parent.0 as usize].children.last().copied();
if let Some(last) = last {
if let Node::Text(text) = &mut self.nodes[last.0 as usize].node {
text.try_reserve(fragment.len())
.map_err(|_| Error::Xml("xml: out of memory growing text node"))?;
text.extend_from_slice(fragment);
return Ok(());
}
}
let mut text = Vec::new();
text.try_reserve(fragment.len())
.map_err(|_| Error::Xml("xml: out of memory creating text node"))?;
text.extend_from_slice(fragment);
self.push(Node::Text(text), Some(parent))?;
Ok(())
}
}
const MAX_ATTRS_PER_ELEMENT: usize = 65_536;
#[cfg(test)]
thread_local! {
static TEST_MAX_GENERAL_REFS: std::cell::Cell<usize> =
const { std::cell::Cell::new(crate::MAX_XML_GENERAL_REFS) };
}
#[cfg(test)]
fn set_test_max_general_refs(n: usize) {
TEST_MAX_GENERAL_REFS.with(|cap| cap.set(n));
}
fn max_general_refs() -> usize {
#[cfg(test)]
{
TEST_MAX_GENERAL_REFS.with(std::cell::Cell::get)
}
#[cfg(not(test))]
{
crate::MAX_XML_GENERAL_REFS
}
}
#[cfg(test)]
thread_local! {
static TEST_MAX_ATTRS: std::cell::Cell<usize> =
const { std::cell::Cell::new(MAX_ATTRS_PER_ELEMENT) };
}
#[cfg(test)]
pub(crate) fn set_test_max_attrs(n: usize) {
TEST_MAX_ATTRS.with(|c| c.set(n));
}
fn max_attrs() -> usize {
#[cfg(test)]
{
TEST_MAX_ATTRS.with(|c| c.get())
}
#[cfg(not(test))]
{
MAX_ATTRS_PER_ELEMENT
}
}
fn element_node(e: &quick_xml::events::BytesStart<'_>, self_closing: bool) -> Result<Node> {
let name = e.name().as_ref().to_vec();
let mut attrs = Vec::new();
let cap = max_attrs();
for a in e.attributes() {
if attrs.len() >= cap {
return Err(Error::Xml("element has too many attributes"));
}
let a = a.map_err(|_| Error::Xml("malformed xml attribute"))?;
let key = a.key.as_ref().to_vec();
let val = a
.decoded_and_normalized_value_with(
XmlVersion::Implicit1_0,
e.decoder(),
1,
quick_xml::escape::resolve_xml_entity,
)
.map_err(|_| Error::Xml("malformed xml attribute value"))?;
if has_xml_illegal_char(&val) {
return Err(Error::Xml(
"xml attribute value contains an xml 1.0 illegal character",
));
}
let val = val.into_owned().into_bytes();
attrs.push((key, val));
}
Ok(Node::Element {
name,
attrs,
self_closing,
})
}
fn unescape_bytes(raw: &[u8]) -> Result<Vec<u8>> {
let s = std::str::from_utf8(raw).map_err(|_| Error::Xml("xml text is not valid utf-8"))?;
let c =
quick_xml::escape::unescape(s).map_err(|_| Error::Xml("malformed xml entity reference"))?;
if has_xml_illegal_char(&c) {
return Err(Error::Xml("xml text contains an xml 1.0 illegal character"));
}
Ok(c.into_owned().into_bytes())
}
fn push_resolved_general_ref(
tree: &mut XmlTree,
parent: NodeId,
reference: &BytesRef<'_>,
) -> Result<()> {
match reference.resolve_char_ref() {
Ok(Some(ch)) => {
if !is_xml_legal_char(ch) {
return Err(Error::Xml("xml text contains an xml 1.0 illegal character"));
}
let mut encoded = [0u8; 4];
tree.push_text_fragment(parent, ch.encode_utf8(&mut encoded).as_bytes())?;
}
Ok(None) => {
let name = reference
.decode()
.map_err(|_| Error::Xml("xml entity reference is not valid utf-8"))?;
let resolved = quick_xml::escape::resolve_xml_entity(&name)
.ok_or(Error::Xml("malformed xml entity reference"))?;
tree.push_text_fragment(parent, resolved.as_bytes())?;
}
Err(_) => return Err(Error::Xml("malformed xml entity reference")),
}
Ok(())
}
fn is_xml_legal_char(c: char) -> bool {
matches!(c, '\t' | '\n' | '\r')
|| matches!(
c as u32,
0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
)
}
fn has_xml_illegal_char(s: &str) -> bool {
s.chars().any(|c| !is_xml_legal_char(c))
}
fn push_char_utf8(c: char, out: &mut Vec<u8>) {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
fn esc_text_into(s: &[u8], out: &mut Vec<u8>) {
for c in String::from_utf8_lossy(s).chars() {
match c {
'&' => out.extend_from_slice(b"&"),
'<' => out.extend_from_slice(b"<"),
'>' => out.extend_from_slice(b">"),
'\r' => out.extend_from_slice(b" "),
_ if !is_xml_legal_char(c) => {}
_ => push_char_utf8(c, out),
}
}
}
fn esc_attr_into(s: &[u8], out: &mut Vec<u8>) {
for c in String::from_utf8_lossy(s).chars() {
match c {
'&' => out.extend_from_slice(b"&"),
'<' => out.extend_from_slice(b"<"),
'>' => out.extend_from_slice(b">"),
'"' => out.extend_from_slice(b"""),
'\t' => out.extend_from_slice(b"	"),
'\n' => out.extend_from_slice(b" "),
'\r' => out.extend_from_slice(b" "),
_ if !is_xml_legal_char(c) => {}
_ => push_char_utf8(c, out),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(tree: &XmlTree) -> String {
String::from_utf8(tree.serialize()).unwrap()
}
#[test]
fn round_trip_preserves_attribute_order_and_namespace_decls() {
let xml = br#"<worksheet xmlns:r="urn:r" c="3" a="1" b="2"><sheetData/></worksheet>"#;
let out = s(&XmlTree::parse(xml).unwrap());
assert_eq!(
out,
r#"<worksheet xmlns:r="urn:r" c="3" a="1" b="2"><sheetData/></worksheet>"#
);
}
#[test]
fn self_closing_vs_explicit_close_preserved() {
let xml = br#"<a><b/><c></c></a>"#;
assert_eq!(s(&XmlTree::parse(xml).unwrap()), r#"<a><b/><c></c></a>"#);
}
#[test]
fn serialize_is_idempotent() {
let xml = br#"<a><b x="1"><c/>txt</b><!-- note --><d>x & y</d></a>"#;
let once = XmlTree::parse(xml).unwrap().serialize();
let twice = XmlTree::parse(&once).unwrap().serialize();
assert_eq!(once, twice, "second round-trip changed bytes");
}
#[test]
fn prolog_and_unknown_markup_round_trips_verbatim() {
let xml = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!--top--><?pi data?><worksheet><sheetData><row><c r=\"A1\"><v>1</v><extLst><ext uri=\"{x}\"/></extLst></c></row></sheetData></worksheet>";
let out = s(&XmlTree::parse(xml).unwrap());
for needle in [
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"<!--top-->",
"<?pi data?>",
"<extLst><ext uri=\"{x}\"/></extLst>",
] {
assert!(out.contains(needle), "lost {needle:?} in:\n{out}");
}
}
#[test]
fn text_entities_round_trip_and_canonicalize_spelling() {
let xml = br#"<t>a & b <c> "q"</t>"#;
let tree = XmlTree::parse(xml).unwrap();
let root = tree.root_element().unwrap();
assert_eq!(tree.text_of(root), r#"a & b <c> "q""#);
assert_eq!(
tree.node_count(),
2,
"entity fragments stay in one text node"
);
assert_eq!(s(&tree), r#"<t>a & b <c> "q"</t>"#);
}
#[test]
fn carriage_return_escaped_to_survive_normalization() {
let mut t = XmlTree::parse(b"<t>x</t>").unwrap();
let id = t.root_element().unwrap();
t.set_element_text(id, "a\rb").unwrap();
assert_eq!(s(&t), "<t>a b</t>");
let re = XmlTree::parse(s(&t).as_bytes()).unwrap();
let rid = re.root_element().unwrap();
assert_eq!(re.text_of(rid), "a\rb");
}
#[test]
fn attribute_value_escapes_quote_and_whitespace_as_char_refs() {
let mut t = XmlTree::parse(b"<c/>").unwrap();
let id = t.root_element().unwrap();
t.set_attr(id, b"t", b"a\"b\tc\nd\re").unwrap();
assert_eq!(s(&t), "<c t=\"a"b	c d e\"/>");
let re = XmlTree::parse(s(&t).as_bytes()).unwrap();
let rid = re.root_element().unwrap();
assert_eq!(re.attr_value(rid, b"t"), Some(&b"a\"b\tc\nd\re"[..]));
}
#[test]
fn edited_values_drop_xml_forbidden_scalars() {
let mut t = XmlTree::parse(b"<t>x</t>").unwrap();
let id = t.root_element().unwrap();
t.set_element_text(id, "a\u{FFFE}b\u{FFFF}c").unwrap();
assert_eq!(s(&t), "<t>abc</t>");
let mut attr = XmlTree::parse(b"<t/>").unwrap();
let id = attr.root_element().unwrap();
attr.set_attr(id, b"data-x", "a\u{FFFF}b".as_bytes())
.unwrap();
assert_eq!(s(&attr), r#"<t data-x="ab"/>"#);
}
#[test]
fn mismatched_end_tag_is_rejected() {
assert!(XmlTree::parse(b"<a></b>").is_err());
assert!(XmlTree::parse(b"<a></a>").is_ok());
}
#[test]
fn truncated_xml_with_open_elements_is_rejected() {
assert!(XmlTree::parse(b"<a><b>").is_err());
assert!(XmlTree::parse(b"<c><f><v>1").is_err());
assert!(XmlTree::parse(b"<a></a>").is_ok()); }
#[test]
fn invalid_utf8_in_text_is_rejected() {
let mut xml = b"<t>".to_vec();
xml.extend_from_slice(&[0xff, 0xfe]);
xml.extend_from_slice(b"</t>");
assert!(XmlTree::parse(&xml).is_err());
}
#[test]
fn malformed_entity_refs_are_rejected_not_canonicalized() {
assert!(XmlTree::parse(b"<t>x&bogus;y</t>").is_err());
assert!(XmlTree::parse(br#"<c x="a&bogus;b"/>"#).is_err());
assert!(XmlTree::parse(b"<t>x&y</t>").is_ok());
assert!(XmlTree::parse(br#"<c x="a&b"/>"#).is_ok());
}
#[test]
fn declared_custom_entities_are_not_expanded() {
let xml = br#"<!DOCTYPE t [<!ENTITY local "secret">]><t>&local;</t>"#;
assert!(XmlTree::parse(xml).is_err());
}
#[test]
fn parsed_attributes_follow_xml_10_whitespace_normalization() {
let tree = XmlTree::parse(b"<t value=\"a\tb\nc	d\"/>").unwrap();
let root = tree.root_element().unwrap();
assert_eq!(tree.attr_value(root, b"value"), Some(&b"a b c\td"[..]));
assert_eq!(s(&tree), "<t value=\"a b c	d\"/>");
}
#[test]
fn second_xml_declaration_is_rejected() {
assert!(XmlTree::parse(b"<?xml version=\"1.0\"?><a/><?xml version=\"1.0\"?>").is_err());
assert!(XmlTree::parse(b"<a><?xml version=\"1.0\"?></a>").is_err());
assert!(XmlTree::parse(b"<?xml version=\"1.0\"?><a/>").is_ok());
}
#[test]
fn garbage_and_deep_nesting_never_panic() {
let _ = XmlTree::parse(&[0xff, 0xfe, 0x00, 0x3c]);
let _ = XmlTree::parse(b"<a><b><c");
let _ = XmlTree::parse(b"plain text no tags");
let deep = "<a>".repeat(5000);
assert!(XmlTree::parse(deep.as_bytes()).is_err());
}
#[test]
fn node_budget_boundary_is_exact() {
set_test_node_budget(3);
let ok = XmlTree::parse(b"<a><b/><c/></a>"); let err = XmlTree::parse(b"<a><b/><c/><d/></a>"); reset_test_node_budget();
assert!(ok.is_ok());
assert!(err.is_err());
}
#[test]
fn general_reference_budget_is_exact() {
set_test_max_general_refs(2);
let at_limit = XmlTree::parse(b"<t>&<</t>");
let over_limit = XmlTree::parse(b"<t>&<></t>");
set_test_max_general_refs(crate::MAX_XML_GENERAL_REFS);
assert!(at_limit.is_ok());
assert!(over_limit.is_err());
}
#[test]
fn empty_element_respects_depth_cap() {
let nest = |opens: usize| {
let mut out = String::new();
for _ in 0..opens {
out.push_str("<a>");
}
out.push_str("<b/>");
for _ in 0..opens {
out.push_str("</a>");
}
out
};
assert!(XmlTree::parse(nest(MAX_DEPTH).as_bytes()).is_err());
assert!(XmlTree::parse(nest(MAX_DEPTH - 1).as_bytes()).is_ok());
}
#[test]
fn too_many_attributes_on_parse_is_rejected() {
set_test_max_attrs(4);
let over = XmlTree::parse(br#"<c a0="" a1="" a2="" a3="" a4=""/>"#);
let under = XmlTree::parse(br#"<c a0="" a1=""/>"#);
set_test_max_attrs(MAX_ATTRS_PER_ELEMENT);
assert!(over.is_err());
assert!(under.is_ok());
}
#[test]
fn fallible_push_does_not_panic_under_node_budget_pressure() {
set_test_node_budget(2);
let mut t = XmlTree::parse(b"<a/>").unwrap();
let id = t.root_element().unwrap();
let r = t.insert_fragment_at(id, 0, b"<b/><c/>");
reset_test_node_budget();
assert!(r.is_err());
}
#[test]
fn set_attr_replaces_in_place_and_appends_preserving_order() {
let mut t = XmlTree::parse(br#"<c a="1" b="2"/>"#).unwrap();
let id = t.root_element().unwrap();
t.set_attr(id, b"a", b"9").unwrap(); assert_eq!(s(&t), r#"<c a="9" b="2"/>"#);
t.set_attr(id, b"z", b"3").unwrap(); assert_eq!(s(&t), r#"<c a="9" b="2" z="3"/>"#);
}
#[test]
fn set_attr_append_is_budget_gated_replace_is_not() {
set_test_max_attrs(2);
let mut t = XmlTree::parse(br#"<c a="1" b="2"/>"#).unwrap();
let id = t.root_element().unwrap();
assert!(t.can_set_attr(id, b"a"));
t.set_attr(id, b"a", b"9").unwrap();
assert!(!t.can_set_attr(id, b"new"));
assert!(t.set_attr(id, b"new", b"1").is_err());
set_test_max_attrs(MAX_ATTRS_PER_ELEMENT);
}
#[test]
fn remove_attr_is_noop_if_absent() {
let mut t = XmlTree::parse(br#"<c a="1"/>"#).unwrap();
let id = t.root_element().unwrap();
t.remove_attr(id, b"missing");
assert_eq!(s(&t), r#"<c a="1"/>"#);
t.remove_attr(id, b"a");
assert_eq!(s(&t), r#"<c/>"#);
}
#[test]
fn text_of_reads_plain_qualified_attrs_and_nested_text() {
let xml = br#"<c r="A1" s="3" t="s"><v>5</v></c>"#;
let t = XmlTree::parse(xml).unwrap();
let id = t.root_element().unwrap();
assert_eq!(t.attr_value(id, b"r"), Some(&b"A1"[..]));
assert_eq!(t.attr_value(id, b"s"), Some(&b"3"[..]));
assert_eq!(t.attr_value(id, b"t"), Some(&b"s"[..]));
assert_eq!(t.attr_value(id, b"missing"), None);
assert_eq!(t.text_of(id), "5");
}
#[test]
fn set_element_text_reuses_text_carrier_without_growing_arena() {
let mut t = XmlTree::parse(b"<t>OLD</t>").unwrap();
let before = t.node_count();
let id = t.root_element().unwrap();
t.set_element_text(id, "NEW").unwrap();
assert_eq!(t.node_count(), before, "reused-carrier edit grew the arena");
assert_eq!(t.text_of(id), "NEW");
}
#[test]
fn set_element_text_reuses_cdata_carrier_without_growing_arena() {
let mut t = XmlTree::parse(b"<t><![CDATA[OLD]]></t>").unwrap();
let before = t.node_count();
let id = t.root_element().unwrap();
t.set_element_text(id, "NEW").unwrap();
assert_eq!(t.node_count(), before, "CDATA edit grew the arena");
assert_eq!(t.text_of(id), "NEW");
assert_eq!(s(&t), "<t>NEW</t>");
}
#[test]
fn set_element_text_allocates_when_no_carrier_present() {
let mut t = XmlTree::parse(b"<t/>").unwrap();
let before = t.node_count();
let id = t.root_element().unwrap();
t.set_element_text(id, "NEW").unwrap();
assert!(t.node_count() > before, "no-carrier edit should allocate");
assert_eq!(s(&t), "<t>NEW</t>");
}
#[test]
fn set_element_text_clears_other_children_besides_the_reused_carrier() {
let mut t = XmlTree::parse(b"<c><extra/>OLD<more/></c>").unwrap();
let id = t.root_element().unwrap();
t.set_element_text(id, "NEW").unwrap();
assert_eq!(s(&t), "<c>NEW</c>");
}
#[test]
fn insert_fragment_at_grafts_at_index_and_returns_first_new_id() {
let mut t = XmlTree::parse(b"<row><c r=\"A1\"/><c r=\"C1\"/></row>").unwrap();
let row = t.root_element().unwrap();
let new_id = t.insert_fragment_at(row, 1, br#"<c r="B1"/>"#).unwrap();
assert_eq!(s(&t), r#"<row><c r="A1"/><c r="B1"/><c r="C1"/></row>"#);
assert_eq!(t.attr_value(new_id, b"r"), Some(&b"B1"[..]));
}
#[test]
fn insert_fragment_at_grafts_multiple_roots_and_nested_children() {
let mut t = XmlTree::parse(b"<row/>").unwrap();
let row = t.root_element().unwrap();
t.insert_fragment_at(row, 0, b"<c><v>1</v></c><c><v>2</v></c>")
.unwrap();
assert_eq!(s(&t), "<row><c><v>1</v></c><c><v>2</v></c></row>");
}
#[test]
fn insert_fragment_at_index_is_clamped_to_child_count() {
let mut t = XmlTree::parse(b"<row><a/></row>").unwrap();
let row = t.root_element().unwrap();
t.insert_fragment_at(row, 999, b"<b/>").unwrap();
assert_eq!(s(&t), "<row><a/><b/></row>");
}
#[test]
fn insert_fragment_at_rejects_over_node_budget_before_committing() {
set_test_node_budget(2);
let mut t = XmlTree::parse(b"<row/>").unwrap();
let before = t.node_count();
let row = t.root_element().unwrap();
let r = t.insert_fragment_at(row, 0, b"<a/><b/>");
reset_test_node_budget();
assert!(r.is_err());
assert_eq!(
t.node_count(),
before,
"rejected insert must not partially commit"
);
}
#[test]
fn self_closing_element_gains_explicit_close_after_graft() {
let mut t = XmlTree::parse(b"<c/>").unwrap();
let id = t.root_element().unwrap();
assert_eq!(s(&t), "<c/>");
t.insert_fragment_at(id, 0, b"<v>1</v>").unwrap();
assert_eq!(s(&t), "<c><v>1</v></c>");
}
#[test]
fn children_of_and_child_by_name() {
let t = XmlTree::parse(b"<row><c r=\"A1\"/><f>SUM</f><c r=\"B1\"/></row>").unwrap();
let row = t.root_element().unwrap();
assert_eq!(t.children_of(row).len(), 3);
let f = t.child_by_name(row, b"f").unwrap();
assert_eq!(t.text_of(f), "SUM");
assert!(t.child_by_name(row, b"missing").is_none());
let first_c = t.child_by_name(row, b"c").unwrap();
assert_eq!(t.attr_value(first_c, b"r"), Some(&b"A1"[..]));
}
#[test]
fn element_name_distinguishes_elements_from_text_and_none_for_non_elements() {
let t = XmlTree::parse(b"<sheets>\n <sheet/>\n <sheet/>\n</sheets>").unwrap();
let root = t.root_element().unwrap();
let kinds: Vec<Option<&[u8]>> = t
.children_of(root)
.iter()
.map(|&c| t.element_name(c))
.collect();
assert_eq!(
kinds,
vec![None, Some(&b"sheet"[..]), None, Some(&b"sheet"[..]), None]
);
}
#[test]
fn remove_child_removes_and_errors_if_not_a_direct_child() {
let mut t = XmlTree::parse(b"<row><c/><f/></row>").unwrap();
let row = t.root_element().unwrap();
let f = t.child_by_name(row, b"f").unwrap();
t.remove_child(row, f).unwrap();
assert_eq!(s(&t), "<row><c/></row>");
assert!(t.remove_child(row, f).is_err());
let c = t.child_by_name(row, b"c").unwrap();
assert!(t.remove_child(c, row).is_err());
}
#[test]
fn root_element_skips_raw_prolog_nodes() {
let xml = b"<?xml version=\"1.0\"?><!--c--><worksheet foo=\"1\"/>";
let t = XmlTree::parse(xml).unwrap();
assert_eq!(s(&t), std::str::from_utf8(xml).unwrap());
let id = t.root_element().unwrap();
assert_eq!(t.attr_value(id, b"foo"), Some(&b"1"[..]));
assert!(t.children_of(id).is_empty());
}
#[test]
fn commit_should_fail_seam_fires_once_then_disarms() {
let mut t = XmlTree::parse(b"<t>x</t>").unwrap();
let id = t.root_element().unwrap();
set_test_fail_commit_after(1);
assert!(
t.set_element_text(id, "a").is_ok(),
"first commit should succeed"
);
assert!(
t.set_element_text(id, "b").is_err(),
"armed commit should fail exactly once"
);
assert!(
t.set_element_text(id, "c").is_ok(),
"seam must self-disarm after firing"
);
reset_test_fail_commit();
set_test_fail_commit_after(0);
let row_xml = b"<row/>";
let mut row_tree = XmlTree::parse(row_xml).unwrap();
let row = row_tree.root_element().unwrap();
assert!(
row_tree.insert_fragment_at(row, 0, b"<c/>").is_err(),
"insert_fragment_at must also honor the commit-failure seam"
);
reset_test_fail_commit();
assert!(row_tree.insert_fragment_at(row, 0, b"<c/>").is_ok());
}
#[test]
fn insert_fragment_at_rejects_chained_inserts_before_exceeding_max_depth() {
let mut t = XmlTree::parse(b"<a/>").unwrap();
let mut cur = t.root_element().unwrap();
let mut successes = 0usize;
let mut node_count_before_last_attempt = t.node_count();
let mut failed = false;
for _ in 0..(MAX_DEPTH + 16) {
node_count_before_last_attempt = t.node_count();
match t.insert_fragment_at(cur, 0, b"<a/>") {
Ok(id) => {
cur = id;
successes += 1;
}
Err(_) => {
failed = true;
break;
}
}
}
assert!(
failed,
"chained inserts must eventually be rejected for depth, not keep \
succeeding indefinitely"
);
assert_eq!(
successes,
MAX_DEPTH - 1,
"depth budget boundary is off (root itself already occupies depth 1)"
);
assert_eq!(
t.node_count(),
node_count_before_last_attempt,
"the rejected insert must not have partially committed"
);
let bytes = t.serialize();
assert!(!bytes.is_empty());
let _ = t.text_of(t.root_element().unwrap());
}
#[test]
fn set_element_text_no_carrier_path_leaves_tree_unchanged_on_simulated_failure() {
let mut t = XmlTree::parse(b"<c><extra/><more/></c>").unwrap(); let id = t.root_element().unwrap();
let before_bytes = s(&t);
let before_count = t.node_count();
set_test_fail_commit_after(1);
let r = t.set_element_text(id, "NEW");
reset_test_fail_commit();
assert!(r.is_err(), "simulated failure must propagate as Err");
assert_eq!(
s(&t),
before_bytes,
"a rejected no-carrier edit must not destroy the element's prior children"
);
assert_eq!(
t.node_count(),
before_count,
"a rejected no-carrier edit must not grow the arena"
);
}
#[test]
fn insert_fragment_at_rolls_back_earlier_roots_when_a_later_root_fails() {
let mut t = XmlTree::parse(b"<row/>").unwrap();
let row = t.root_element().unwrap();
let before_bytes = s(&t);
let before_count = t.node_count();
set_test_fail_commit_after(2);
let r = t.insert_fragment_at(row, 0, b"<a/><b/>");
reset_test_fail_commit();
assert!(
r.is_err(),
"simulated mid-graft failure must propagate as Err"
);
assert_eq!(
s(&t),
before_bytes,
"an earlier fragment root that grafted successfully must be rolled back \
when a later root in the same insert fails"
);
assert_eq!(
t.node_count(),
before_count,
"insert_fragment_at must be all-or-nothing"
);
}
#[test]
fn parse_rejects_xml_illegal_char_from_numeric_entity_in_text() {
assert!(XmlTree::parse(b"<t>ab</t>").is_err());
assert!(XmlTree::parse(b"<t>abc</t>").is_ok());
}
#[test]
fn parse_rejects_xml_illegal_char_from_numeric_entity_in_attribute() {
assert!(XmlTree::parse(br#"<t x="ab"/>"#).is_err());
assert!(XmlTree::parse(br#"<t x="abc"/>"#).is_ok());
}
#[test]
fn prolog_epilog_text_is_raw_passthrough_not_utf8_validated() {
let mut xml = b"<a/> ".to_vec();
xml.extend_from_slice(&[0xff, 0xfe]);
let tree = XmlTree::parse(&xml).unwrap();
assert_eq!(
tree.serialize(),
xml,
"prolog/epilog Raw text must round-trip verbatim, invalid UTF-8 included"
);
}
}