use quick_xml::events::Event;
use quick_xml::Reader;
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
}
})
}
pub(crate) const WML_NS: &[u8] = b"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
const OOXML_REL_NS: &[u8] = b"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
fn wml_ns_str() -> &'static str {
std::str::from_utf8(WML_NS).expect("WML namespace is valid UTF-8")
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub(crate) struct NodeId(u32);
#[derive(Clone, Copy, PartialEq, Eq)]
enum WmlVMerge {
None,
Restart,
Continue,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WmlRevisionEditPolicy {
Accept,
Reject,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WmlRevisionEditAction {
Keep,
Remove,
Unwrap,
RenameDeletedText,
}
#[derive(Clone, Copy)]
struct WmlActiveVMerge {
col: usize,
span: usize,
cell: NodeId,
}
#[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>,
}
#[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 buf = Vec::new();
loop {
if tree.nodes.len() > node_budget() {
return Err(Error::Docx("xml has too many nodes".into()));
}
let ev = reader
.read_event_into(&mut buf)
.map_err(|e| Error::Docx(format!("xml parse: {e}")))?;
match ev {
Event::Start(e) => {
if stack.len() >= MAX_DEPTH {
return Err(Error::Docx("xml nesting too deep".into()));
}
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::Docx("xml nesting too deep".into()));
}
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(Node::Text(unescape_bytes(&raw)?), Some(parent))?;
}
None => {
tree.push(Node::Raw(raw.into_owned()), 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::Docx(
"xml declaration is only allowed at the start of the document".into(),
));
}
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::Docx("xml ended with unclosed elements".into()));
}
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'>');
}
}
}
}
#[cfg(test)]
pub(crate) fn roots(&self) -> &[NodeId] {
&self.roots
}
#[cfg(test)]
pub(crate) fn node(&self, id: NodeId) -> &Node {
&self.nodes[id.0 as usize].node
}
#[cfg(test)]
pub(crate) fn children(&self, id: NodeId) -> &[NodeId] {
&self.nodes[id.0 as usize].children
}
pub(crate) fn local_name(&self, id: NodeId) -> Option<&[u8]> {
match &self.nodes[id.0 as usize].node {
Node::Element { name, .. } => Some(match name.iter().position(|&b| b == b':') {
Some(i) => &name[i + 1..],
None => name,
}),
_ => None,
}
}
fn push(&mut self, node: Node, parent: Option<NodeId>) -> Result<NodeId> {
self.nodes
.try_reserve(1)
.map_err(|_| Error::Docx("xml: out of memory growing node arena".into()))?;
match parent {
Some(p) => self.nodes[p.0 as usize]
.children
.try_reserve(1)
.map_err(|_| Error::Docx("xml: out of memory growing child list".into()))?,
None => self
.roots
.try_reserve(1)
.map_err(|_| Error::Docx("xml: out of memory growing root list".into()))?,
}
let id = NodeId(self.nodes.len() as u32);
self.nodes.push(NodeData {
node,
children: Vec::new(),
});
match parent {
Some(p) => self.nodes[p.0 as usize].children.push(id),
None => self.roots.push(id),
}
Ok(id)
}
fn parent_child_index(&self, target: NodeId) -> Option<(NodeId, usize)> {
fn rec(t: &XmlTree, parent: NodeId, target: NodeId) -> Option<(NodeId, usize)> {
for (index, &child) in t.nodes[parent.0 as usize].children.iter().enumerate() {
if child == target {
return Some((parent, index));
}
if let Some(found) = rec(t, child, target) {
return Some(found);
}
}
None
}
for &root in &self.roots {
if let Some(found) = rec(self, root, target) {
return Some(found);
}
}
None
}
}
const MAX_ATTRS_PER_ELEMENT: usize = 65_536;
#[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::Docx("element has too many attributes".into()));
}
let a = a.map_err(|err| Error::Docx(format!("xml attr: {err}")))?;
let key = a.key.as_ref().to_vec();
let val = a
.unescape_value()
.map_err(|err| Error::Docx(format!("xml attr value: {err}")))?
.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(|e| Error::Docx(format!("xml text utf-8: {e}")))?;
let c =
quick_xml::escape::unescape(s).map_err(|e| Error::Docx(format!("xml text entity: {e}")))?;
Ok(c.into_owned().into_bytes())
}
fn is_xml_legal_char(c: char) -> bool {
matches!(c, '\t' | '\n' | '\r')
|| matches!(
c as u32,
0x20..=0xD7FF | 0xE000..=0xFFFD | 0x10000..=0x10FFFF
)
}
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),
}
}
}
fn escaped_text(s: &str) -> String {
let mut out = Vec::new();
esc_text_into(s.as_bytes(), &mut out);
String::from_utf8(out).expect("XML text escaping preserves UTF-8")
}
fn escaped_attr(s: &str) -> String {
let mut out = Vec::new();
esc_attr_into(s.as_bytes(), &mut out);
String::from_utf8(out).expect("XML attribute escaping preserves UTF-8")
}
fn wml_text_run_content_xml(text: &str) -> String {
fn flush(out: &mut String, buf: &mut String) {
if buf.is_empty() {
return;
}
let preserve = buf.as_str() != buf.trim_matches([' ', '\t', '\n', '\r']);
let text = escaped_text(buf);
if preserve {
out.push_str(&format!(r#"<w:t xml:space="preserve">{text}</w:t>"#));
} else {
out.push_str(&format!(r#"<w:t>{text}</w:t>"#));
}
buf.clear();
}
let mut out = String::new();
let mut buf = String::new();
for ch in text.chars() {
match ch {
'\t' => {
flush(&mut out, &mut buf);
out.push_str("<w:tab/>");
}
'\n' => {
flush(&mut out, &mut buf);
out.push_str("<w:br/>");
}
'\r' => {}
c if (c as u32) < 0x20 => {}
c => buf.push(c),
}
}
flush(&mut out, &mut buf);
out
}
pub(crate) fn wml_text_run_content_node_count(text: &str) -> Result<usize> {
let xml = wml_text_run_content_xml(text);
if xml.is_empty() {
return Ok(0);
}
XmlTree::parse(xml.as_bytes()).map(|tree| tree.node_count())
}
#[derive(Debug)]
struct FieldScan {
wanted: usize,
seen: usize,
target: Option<Vec<NodeId>>,
complex: Vec<ComplexFieldScan>,
}
#[derive(Debug)]
struct ComplexFieldScan {
result_phase: bool,
result_runs: Vec<NodeId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WmlRunRange {
parent: NodeId,
first_index: usize,
last_index: usize,
}
#[derive(Debug)]
struct WmlCommentTextEditTarget {
first_run: Option<NodeId>,
text_runs: Vec<NodeId>,
}
fn attr_value_local<'a>(attrs: &'a [(Vec<u8>, Vec<u8>)], local: &[u8]) -> Option<&'a [u8]> {
attrs.iter().find_map(|(k, v)| {
let lname = k
.iter()
.position(|&b| b == b':')
.map_or(k.as_slice(), |i| &k[i + 1..]);
(lname == local).then_some(v.as_slice())
})
}
fn trim_ascii_whitespace(mut value: &[u8]) -> &[u8] {
while value.first().is_some_and(u8::is_ascii_whitespace) {
value = &value[1..];
}
while value.last().is_some_and(u8::is_ascii_whitespace) {
value = &value[..value.len() - 1];
}
value
}
fn attr_value_ns_local<'a>(
attrs: &'a [(Vec<u8>, Vec<u8>)],
scope: &[(Vec<u8>, Vec<u8>)],
ns: &[u8],
local: &[u8],
) -> Option<&'a [u8]> {
attrs.iter().find_map(|(k, v)| {
let (prefix, lname): (&[u8], &[u8]) = match k.iter().position(|&b| b == b':') {
Some(i) => (&k[..i], &k[i + 1..]),
None => (b"", k),
};
if lname != local {
return None;
}
let matches_ns = if prefix.is_empty() {
ns.is_empty()
} else {
scope
.iter()
.rev()
.find(|(p, _)| p.as_slice() == prefix)
.map(|(_, u)| u.as_slice())
== Some(ns)
};
matches_ns.then_some(v.as_slice())
})
}
#[cfg(test)]
impl Node {
fn clone_text(&self) -> Option<Vec<u8>> {
match self {
Node::Text(t) => Some(t.clone()),
_ => None,
}
}
}
impl XmlTree {
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::Docx(
"simulated commit-time allocation failure (test seam)".into(),
));
}
if text != text.trim_matches([' ', '\t', '\n', '\r']) {
self.set_attr(id, b"xml:space", b"preserve")?;
} else {
self.remove_attr(id, b"xml:space");
}
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 => {
self.nodes[id.0 as usize].children.clear();
self.push(Node::Text(text.as_bytes().to_vec()), Some(id))?;
}
}
Ok(())
}
pub(crate) fn replace_wml_text_element_with_run_content(
&mut self,
id: NodeId,
text: &str,
) -> Result<()> {
let (parent, index) = self
.parent_child_index(id)
.ok_or_else(|| Error::Docx("wml text node has no parent".into()))?;
let xml = wml_text_run_content_xml(text);
self.nodes[parent.0 as usize].children.remove(index);
if !xml.is_empty() {
self.insert_fragment_at(parent, index, xml.as_bytes())?;
}
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::Docx(
"element has too many attributes to add another".into(),
));
}
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,
}
}
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 node_count(&self) -> usize {
self.nodes.len()
}
pub(crate) fn has_text_carrier(&self, id: NodeId) -> bool {
self.nodes[id.0 as usize]
.children
.iter()
.any(|&c| match &self.nodes[c.0 as usize].node {
Node::Text(_) => true,
Node::Raw(r) => r.starts_with(b"<![CDATA["),
Node::Element { .. } => false,
})
}
pub(crate) fn wml_body_strict(&self) -> Result<NodeId> {
let mut elements = self
.roots
.iter()
.copied()
.filter(|&r| matches!(self.nodes[r.0 as usize].node, Node::Element { .. }));
let root = elements
.next()
.ok_or_else(|| Error::Docx("document.xml has no root element".into()))?;
if elements.next().is_some() {
return Err(Error::Docx(
"document.xml has more than one top-level element".into(),
));
}
for &r in &self.roots {
if let Node::Raw(bytes) = &self.nodes[r.0 as usize].node {
let is_ws = bytes
.iter()
.all(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'));
let allowed_markup = bytes.starts_with(b"<!--") || bytes.starts_with(b"<?");
if !is_ws && !allowed_markup {
return Err(Error::Docx(
"document.xml has invalid content outside the root element".into(),
));
}
}
}
let mut scope: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
self.push_xmlns(root, &mut scope);
if !self.resolves_to(root, WML_NS, b"document", &scope) {
return Err(Error::Docx(
"document.xml root is not a WordprocessingML w:document".into(),
));
}
for &c in &self.nodes[root.0 as usize].children {
let cb = scope.len();
self.push_xmlns(c, &mut scope);
let is_body = self.resolves_to(c, WML_NS, b"body", &scope);
scope.truncate(cb);
if is_body {
return Ok(c);
}
}
Err(Error::Docx("document.xml has no w:body".into()))
}
pub(crate) fn wml_part_root_strict(
&self,
part_name: &str,
root_local: &[u8],
) -> Result<NodeId> {
self.part_root_strict_ns(part_name, WML_NS, root_local, "WordprocessingML w")
}
pub(crate) fn wml_any_part_root_strict(&self, part_name: &str) -> Result<NodeId> {
self.part_root_strict_ns_any_local(part_name, WML_NS, "WordprocessingML w")
}
pub(crate) fn part_root_strict_ns(
&self,
part_name: &str,
root_ns: &[u8],
root_local: &[u8],
expected_prefix: &str,
) -> Result<NodeId> {
let root = self.part_root_strict_ns_any_local(part_name, root_ns, expected_prefix)?;
let mut scope: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
self.push_xmlns(root, &mut scope);
if self.resolves_to(root, root_ns, root_local, &scope) {
Ok(root)
} else {
let expected = String::from_utf8_lossy(root_local);
Err(Error::Docx(format!(
"{part_name} root is not a {expected_prefix}:{expected}"
)))
}
}
fn part_root_strict_ns_any_local(
&self,
part_name: &str,
root_ns: &[u8],
expected_prefix: &str,
) -> Result<NodeId> {
let mut elements = self
.roots
.iter()
.copied()
.filter(|&r| matches!(self.nodes[r.0 as usize].node, Node::Element { .. }));
let root = elements
.next()
.ok_or_else(|| Error::Docx(format!("{part_name} has no root element")))?;
if elements.next().is_some() {
return Err(Error::Docx(format!(
"{part_name} has more than one top-level element"
)));
}
for &r in &self.roots {
if let Node::Raw(bytes) = &self.nodes[r.0 as usize].node {
let is_ws = bytes
.iter()
.all(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'));
let allowed_markup = bytes.starts_with(b"<!--") || bytes.starts_with(b"<?");
if !is_ws && !allowed_markup {
return Err(Error::Docx(format!(
"{part_name} has invalid content outside the root element"
)));
}
}
}
let mut scope: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
self.push_xmlns(root, &mut scope);
if self.resolves_to_ns(root, root_ns, &scope) {
Ok(root)
} else {
Err(Error::Docx(format!(
"{part_name} root is not in the {expected_prefix} namespace"
)))
}
}
fn push_xmlns(&self, id: NodeId, scope: &mut Vec<(Vec<u8>, Vec<u8>)>) {
if let Node::Element { attrs, .. } = &self.nodes[id.0 as usize].node {
for (k, v) in attrs {
if k.as_slice() == b"xmlns" {
scope.push((Vec::new(), v.clone()));
} else if let Some(p) = k.strip_prefix(b"xmlns:".as_slice()) {
scope.push((p.to_vec(), v.clone()));
}
}
}
}
fn resolves_to(
&self,
id: NodeId,
ns: &[u8],
local: &[u8],
scope: &[(Vec<u8>, Vec<u8>)],
) -> bool {
let Node::Element { name, .. } = &self.nodes[id.0 as usize].node else {
return false;
};
let (prefix, lname): (&[u8], &[u8]) = match name.iter().position(|&b| b == b':') {
Some(i) => (&name[..i], &name[i + 1..]),
None => (b"", name),
};
lname == local
&& scope
.iter()
.rev()
.find(|(p, _)| p.as_slice() == prefix)
.map(|(_, u)| u.as_slice())
== Some(ns)
}
fn resolves_to_ns(&self, id: NodeId, ns: &[u8], scope: &[(Vec<u8>, Vec<u8>)]) -> bool {
let Node::Element { name, .. } = &self.nodes[id.0 as usize].node else {
return false;
};
let prefix: &[u8] = match name.iter().position(|&b| b == b':') {
Some(i) => &name[..i],
None => b"",
};
scope
.iter()
.rev()
.find(|(p, _)| p.as_slice() == prefix)
.map(|(_, u)| u.as_slice())
== Some(ns)
}
fn scope_binds_prefix(scope: &[(Vec<u8>, Vec<u8>)], prefix: &[u8], ns: &[u8]) -> bool {
scope
.iter()
.rev()
.find(|(p, _)| p.as_slice() == prefix)
.map(|(_, u)| u.as_slice())
== Some(ns)
}
fn fallback_attr_needed(scope: &[(Vec<u8>, Vec<u8>)], name: &str, value: &str) -> bool {
if name == "xmlns" {
!Self::scope_binds_prefix(scope, b"", value.as_bytes())
} else if let Some(prefix) = name.strip_prefix("xmlns:") {
!Self::scope_binds_prefix(scope, prefix.as_bytes(), value.as_bytes())
} else {
true
}
}
fn ns_scope_at(&self, target: NodeId) -> Vec<(Vec<u8>, Vec<u8>)> {
fn rec(
t: &XmlTree,
id: NodeId,
target: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
) -> bool {
let base = scope.len();
if let Node::Element { attrs, .. } = &t.nodes[id.0 as usize].node {
for (k, v) in attrs {
if k.as_slice() == b"xmlns" {
scope.push((Vec::new(), v.clone()));
} else if let Some(p) = k.strip_prefix(b"xmlns:".as_slice()) {
scope.push((p.to_vec(), v.clone()));
}
}
}
if id == target {
return true; }
for i in 0..t.nodes[id.0 as usize].children.len() {
let c = t.nodes[id.0 as usize].children[i];
if rec(t, c, target, scope) {
return true;
}
}
scope.truncate(base);
false
}
let mut scope = Vec::new();
for r in 0..self.roots.len() {
if rec(self, self.roots[r], target, &mut scope) {
break;
}
}
scope
}
pub(crate) fn wml_text_runs_under(&self, body: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
let mut scope = self.ns_scope_at(body); for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
self.collect_wml_t(c, &mut scope, &mut out);
}
out
}
pub(crate) fn wml_hyperlink_rids_under(&self, body: NodeId) -> Vec<String> {
let mut out = Vec::new();
let mut scope = self.ns_scope_at(body);
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
self.collect_wml_hyperlink_rids(c, &mut scope, &mut out);
}
out
}
pub(crate) fn wml_field_result_runs_under(
&self,
body: NodeId,
field_index: usize,
) -> Option<Vec<NodeId>> {
let mut scan = FieldScan {
wanted: field_index,
seen: 0,
target: None,
complex: Vec::new(),
};
let mut scope = self.ns_scope_at(body);
for i in 0..self.nodes[body.0 as usize].children.len() {
if scan.target.is_some() {
break;
}
let c = self.nodes[body.0 as usize].children[i];
self.collect_wml_field_results(c, &mut scope, &mut scan);
}
scan.target
}
pub(crate) fn wml_content_control_text_runs_by_tag_under(
&self,
body: NodeId,
tag: &str,
) -> Vec<Vec<NodeId>> {
let mut out = Vec::new();
let mut scope = self.ns_scope_at(body);
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
self.collect_wml_content_control_text_by_tag(c, &mut scope, tag.as_bytes(), &mut out);
}
out
}
pub(crate) fn accept_wml_revisions_under(&mut self, body: NodeId) -> usize {
let mut scope = self.ns_scope_at(body);
self.edit_wml_revisions_descendants(body, &mut scope, WmlRevisionEditPolicy::Accept)
}
pub(crate) fn reject_wml_revisions_under(&mut self, body: NodeId) -> usize {
let mut scope = self.ns_scope_at(body);
self.edit_wml_revisions_descendants(body, &mut scope, WmlRevisionEditPolicy::Reject)
}
pub(crate) fn set_wml_comment_text_under(
&mut self,
root: NodeId,
comment_id: &str,
text: &str,
) -> Result<()> {
let target = self
.wml_comment_text_edit_target_under(root, comment_id)
.ok_or_else(|| Error::Docx(format!("comment id {comment_id:?} not found")))?;
let first_run = target
.first_run
.ok_or_else(|| Error::Docx(format!("comment id {comment_id:?} has no visible text")))?;
if target.text_runs.is_empty() {
return Err(Error::Docx(format!(
"comment id {comment_id:?} has no visible text"
)));
}
let old_children = self.nodes[first_run.0 as usize].children.clone();
let xml = wml_text_run_content_xml(text);
self.nodes[first_run.0 as usize].children.clear();
self.insert_fragment_at(first_run, 0, xml.as_bytes())?;
for id in target.text_runs {
if !old_children.contains(&id) {
self.set_element_text(id, "")?;
}
}
Ok(())
}
pub(crate) fn wml_table_cell_text_runs_under(
&self,
body: NodeId,
table_index: usize,
row_index: usize,
cell_index: usize,
) -> Option<Vec<NodeId>> {
let table = self.wml_body_table_at(body, table_index)?;
let mut scope = self.ns_scope_at(table);
self.wml_table_cell_text_runs(table, &mut scope, row_index, cell_index)
}
pub(crate) fn wml_table_cell_has_nested_table_under(
&self,
body: NodeId,
table_index: usize,
row_index: usize,
cell_index: usize,
) -> Option<bool> {
let table = self.wml_body_table_at(body, table_index)?;
let mut scope = self.ns_scope_at(table);
self.wml_table_cell_has_nested_table(table, &mut scope, row_index, cell_index)
}
fn wml_body_table_at(&self, body: NodeId, table_index: usize) -> Option<NodeId> {
let mut seen = 0usize;
let mut scope = self.ns_scope_at(body);
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
if let Some(table) = self.find_wml_body_table(c, &mut scope, table_index, &mut seen) {
return Some(table);
}
}
None
}
fn find_wml_body_table(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
table_index: usize,
seen: &mut usize,
) -> Option<NodeId> {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return None;
}
if self.resolves_to(id, WML_NS, b"tbl", scope) {
let found = (*seen == table_index).then_some(id);
*seen += 1;
scope.truncate(base);
return found;
}
if self.is_wml_body_table_wrapper(id, scope) {
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
if let Some(table) = self.find_wml_body_table(c, scope, table_index, seen) {
scope.truncate(base);
return Some(table);
}
}
}
scope.truncate(base);
None
}
fn is_wml_body_table_wrapper(&self, id: NodeId, scope: &[(Vec<u8>, Vec<u8>)]) -> bool {
[
b"sdt".as_slice(),
b"sdtContent".as_slice(),
b"customXml".as_slice(),
b"smartTag".as_slice(),
b"ins".as_slice(),
b"moveTo".as_slice(),
]
.into_iter()
.any(|local| self.resolves_to(id, WML_NS, local, scope))
}
pub(crate) fn wml_note_text_runs_under(&self, root: NodeId, note_local: &[u8]) -> Vec<NodeId> {
let mut out = Vec::new();
let mut scope = self.ns_scope_at(root);
for i in 0..self.nodes[root.0 as usize].children.len() {
let c = self.nodes[root.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, &mut scope);
if self.resolves_to(c, WML_NS, note_local, &scope) {
let skip = match &self.nodes[c.0 as usize].node {
Node::Element { attrs, .. } => {
attr_value_local(attrs, b"type").is_some_and(|value| {
matches!(
trim_ascii_whitespace(value),
b"separator" | b"continuationSeparator" | b"continuationNotice"
)
})
}
_ => false,
};
if !skip {
self.collect_wml_t(c, &mut scope, &mut out);
}
}
scope.truncate(base);
}
out
}
pub(crate) fn add_wml_comment_anchor_on_text(
&mut self,
body: NodeId,
anchor_text: &str,
comment_id: &str,
) -> Result<()> {
let mut scope = self.ns_scope_at(body);
let mut target = None;
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
if self.find_wml_run_range_with_text(c, &mut scope, anchor_text, &mut target) {
break;
}
}
let Some(range) = target else {
return Err(Error::Docx(format!(
"comment anchor text {anchor_text:?} not found"
)));
};
let id = escaped_attr(comment_id);
let start = format!(
r#"<w:commentRangeStart xmlns:w="{}" w:id="{id}"/>"#,
wml_ns_str()
);
let end = format!(
r#"<w:commentRangeEnd xmlns:w="{}" w:id="{id}"/><w:r xmlns:w="{}"><w:commentReference w:id="{id}"/></w:r>"#,
wml_ns_str(),
wml_ns_str()
);
self.insert_fragment_at(range.parent, range.first_index, start.as_bytes())?;
self.insert_fragment_at(range.parent, range.last_index + 2, end.as_bytes())?;
Ok(())
}
pub(crate) fn add_wml_footnote_reference_on_text(
&mut self,
body: NodeId,
anchor_text: &str,
footnote_id: &str,
) -> Result<()> {
let mut scope = self.ns_scope_at(body);
let mut target = None;
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
if self.find_wml_run_range_with_text(c, &mut scope, anchor_text, &mut target) {
break;
}
}
let Some(range) = target else {
return Err(Error::Docx(format!(
"footnote anchor text {anchor_text:?} not found"
)));
};
let id = escaped_attr(footnote_id);
let reference = format!(
r#"<w:r xmlns:w="{}"><w:footnoteReference w:id="{id}"/></w:r>"#,
wml_ns_str()
);
self.insert_fragment_at(range.parent, range.last_index + 1, reference.as_bytes())
}
pub(crate) fn add_wml_endnote_reference_on_text(
&mut self,
body: NodeId,
anchor_text: &str,
endnote_id: &str,
) -> Result<()> {
let mut scope = self.ns_scope_at(body);
let mut target = None;
for i in 0..self.nodes[body.0 as usize].children.len() {
let c = self.nodes[body.0 as usize].children[i];
if self.find_wml_run_range_with_text(c, &mut scope, anchor_text, &mut target) {
break;
}
}
let Some(range) = target else {
return Err(Error::Docx(format!(
"endnote anchor text {anchor_text:?} not found"
)));
};
let id = escaped_attr(endnote_id);
let reference = format!(
r#"<w:r xmlns:w="{}"><w:endnoteReference w:id="{id}"/></w:r>"#,
wml_ns_str()
);
self.insert_fragment_at(range.parent, range.last_index + 1, reference.as_bytes())
}
pub(crate) fn append_wml_comment(
&mut self,
root: NodeId,
comment_id: &str,
text: &str,
author: &str,
) -> Result<()> {
let id = escaped_attr(comment_id);
let author = escaped_attr(author);
let text = wml_text_run_content_xml(text);
let xml = format!(
r#"<w:comment xmlns:w="{}" w:id="{id}" w:author="{author}"><w:p><w:r>{text}</w:r></w:p></w:comment>"#,
wml_ns_str()
);
let pos = self.nodes[root.0 as usize].children.len();
self.insert_fragment_at(root, pos, xml.as_bytes())
}
pub(crate) fn append_wml_footnote(
&mut self,
root: NodeId,
footnote_id: &str,
text: &str,
) -> Result<()> {
let id = escaped_attr(footnote_id);
let text = wml_text_run_content_xml(text);
let xml = format!(
r#"<w:footnote xmlns:w="{}" w:id="{id}"><w:p><w:r>{text}</w:r></w:p></w:footnote>"#,
wml_ns_str()
);
let pos = self.nodes[root.0 as usize].children.len();
self.insert_fragment_at(root, pos, xml.as_bytes())
}
pub(crate) fn append_wml_endnote(
&mut self,
root: NodeId,
endnote_id: &str,
text: &str,
) -> Result<()> {
let id = escaped_attr(endnote_id);
let text = wml_text_run_content_xml(text);
let xml = format!(
r#"<w:endnote xmlns:w="{}" w:id="{id}"><w:p><w:r>{text}</w:r></w:p></w:endnote>"#,
wml_ns_str()
);
let pos = self.nodes[root.0 as usize].children.len();
self.insert_fragment_at(root, pos, xml.as_bytes())
}
pub(crate) fn set_child_text_ns_local_with_attrs(
&mut self,
root: NodeId,
ns: &[u8],
local: &[u8],
fallback_qname: &str,
fallback_attrs: &[(&str, &str)],
text: &str,
) -> Result<()> {
let mut scope = self.ns_scope_at(root);
for i in 0..self.nodes[root.0 as usize].children.len() {
let c = self.nodes[root.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, &mut scope);
if self.resolves_to(c, ns, local, &scope) {
scope.truncate(base);
for (name, value) in fallback_attrs {
if Self::fallback_attr_needed(&scope, name, value) {
self.set_attr(c, name.as_bytes(), value.as_bytes())?;
}
}
return self.set_element_text(c, text);
}
scope.truncate(base);
}
let prefix = fallback_qname
.split_once(':')
.map(|(prefix, _)| prefix)
.unwrap_or("");
let prefix_bytes = prefix.as_bytes();
let xmlns = if Self::scope_binds_prefix(&scope, prefix_bytes, ns) {
None
} else if prefix.is_empty() {
Some(format!(
r#"xmlns="{}""#,
escaped_attr(&String::from_utf8_lossy(ns))
))
} else {
Some(format!(
r#"xmlns:{prefix}="{}""#,
escaped_attr(&String::from_utf8_lossy(ns))
))
};
let mut attrs = String::new();
if let Some(xmlns) = xmlns {
attrs.push_str(&xmlns);
}
for (name, value) in fallback_attrs {
if !Self::fallback_attr_needed(&scope, name, value) {
continue;
}
if !attrs.is_empty() {
attrs.push(' ');
}
attrs.push_str(name);
attrs.push_str("=\"");
attrs.push_str(&escaped_attr(value));
attrs.push('"');
}
let text = escaped_text(text);
let xml = if attrs.is_empty() {
format!("<{fallback_qname}>{text}</{fallback_qname}>")
} else {
format!("<{fallback_qname} {attrs}>{text}</{fallback_qname}>")
};
let pos = self.nodes[root.0 as usize].children.len();
self.insert_fragment_at(root, pos, xml.as_bytes())
}
#[cfg(test)]
pub(crate) fn wml_text_runs(&self) -> Vec<NodeId> {
let mut out = Vec::new();
let mut scope: Vec<(Vec<u8>, Vec<u8>)> = Vec::new(); for r in 0..self.roots.len() {
self.collect_wml_t(self.roots[r], &mut scope, &mut out);
}
out
}
fn wml_table_cell_text_runs(
&self,
table: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
row_index: usize,
cell_index: usize,
) -> Option<Vec<NodeId>> {
let cell = self.wml_table_cell_at(table, scope, row_index, cell_index)?;
let mut runs = Vec::new();
let mut cell_scope = self.ns_scope_at(cell);
for i in 0..self.nodes[cell.0 as usize].children.len() {
let c = self.nodes[cell.0 as usize].children[i];
self.collect_wml_t(c, &mut cell_scope, &mut runs);
}
Some(runs)
}
fn wml_table_cell_has_nested_table(
&self,
table: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
row_index: usize,
cell_index: usize,
) -> Option<bool> {
let cell = self.wml_table_cell_at(table, scope, row_index, cell_index)?;
let mut cell_scope = self.ns_scope_at(cell);
Some(self.contains_wml_table_descendant(cell, &mut cell_scope))
}
fn wml_table_cell_at(
&self,
table: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
row_index: usize,
cell_index: usize,
) -> Option<NodeId> {
let mut seen = 0usize;
let mut active_vmerges = Vec::new();
for i in 0..self.nodes[table.0 as usize].children.len() {
let c = self.nodes[table.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"tr", scope) {
let result = self.wml_row_cell_at(c, scope, cell_index, &mut active_vmerges);
if seen == row_index {
scope.truncate(base);
return result;
}
seen += 1;
}
scope.truncate(base);
}
None
}
fn wml_row_cell_at(
&self,
row: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
cell_index: usize,
active_vmerges: &mut Vec<WmlActiveVMerge>,
) -> Option<NodeId> {
let previous_vmerges = active_vmerges.clone();
let mut next_vmerges = Vec::new();
let mut found = None;
let mut logical_col = 0usize;
for i in 0..self.nodes[row.0 as usize].children.len() {
let c = self.nodes[row.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"tc", scope) {
let vmerge = self.wml_table_cell_vmerge(c, scope);
let active = previous_vmerges.iter().find(|m| m.col == logical_col);
let span = if vmerge == WmlVMerge::Continue {
active.map_or_else(
|| self.wml_table_cell_grid_span(c, scope),
|m| m.span.max(1),
)
} else {
self.wml_table_cell_grid_span(c, scope)
};
let next_col = logical_col.saturating_add(span);
if (logical_col..next_col).contains(&cell_index) {
found = Some(match vmerge {
WmlVMerge::Continue => active.map_or(c, |m| m.cell),
WmlVMerge::None | WmlVMerge::Restart => c,
});
}
match vmerge {
WmlVMerge::Restart => next_vmerges.push(WmlActiveVMerge {
col: logical_col,
span,
cell: c,
}),
WmlVMerge::Continue => {
if let Some(active) = active {
next_vmerges.push(WmlActiveVMerge {
col: logical_col,
span,
cell: active.cell,
});
}
}
WmlVMerge::None => {}
}
logical_col = next_col;
}
scope.truncate(base);
}
*active_vmerges = next_vmerges;
found
}
fn wml_table_cell_grid_span(&self, cell: NodeId, scope: &mut Vec<(Vec<u8>, Vec<u8>)>) -> usize {
for i in 0..self.nodes[cell.0 as usize].children.len() {
let c = self.nodes[cell.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"tcPr", scope) {
let span = self.wml_tcpr_grid_span(c, scope);
scope.truncate(base);
return span;
}
scope.truncate(base);
}
1
}
fn wml_tcpr_grid_span(&self, tcpr: NodeId, scope: &mut Vec<(Vec<u8>, Vec<u8>)>) -> usize {
let mut span = 1usize;
for i in 0..self.nodes[tcpr.0 as usize].children.len() {
let c = self.nodes[tcpr.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"gridSpan", scope) {
if let Node::Element { attrs, .. } = &self.nodes[c.0 as usize].node {
span = attr_value_local(attrs, b"val")
.map(trim_ascii_whitespace)
.and_then(|v| std::str::from_utf8(v).ok())
.and_then(|v| v.parse::<usize>().ok())
.filter(|&v| v > 0)
.unwrap_or(1);
}
}
scope.truncate(base);
}
span
}
fn wml_table_cell_vmerge(
&self,
cell: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
) -> WmlVMerge {
for i in 0..self.nodes[cell.0 as usize].children.len() {
let c = self.nodes[cell.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"tcPr", scope) {
let vmerge = self.wml_tcpr_vmerge(c, scope);
scope.truncate(base);
return vmerge;
}
scope.truncate(base);
}
WmlVMerge::None
}
fn wml_tcpr_vmerge(&self, tcpr: NodeId, scope: &mut Vec<(Vec<u8>, Vec<u8>)>) -> WmlVMerge {
let mut vmerge = WmlVMerge::None;
for i in 0..self.nodes[tcpr.0 as usize].children.len() {
let c = self.nodes[tcpr.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"vMerge", scope) {
vmerge = if let Node::Element { attrs, .. } = &self.nodes[c.0 as usize].node {
match attr_value_local(attrs, b"val").map(trim_ascii_whitespace) {
Some(b"restart") => WmlVMerge::Restart,
_ => WmlVMerge::Continue,
}
} else {
WmlVMerge::Continue
};
}
scope.truncate(base);
}
vmerge
}
fn contains_wml_table_descendant(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
) -> bool {
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"tbl", scope)
|| self.contains_wml_table_descendant(c, scope)
{
scope.truncate(base);
return true;
}
scope.truncate(base);
}
false
}
fn edit_wml_revisions_descendants(
&mut self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
policy: WmlRevisionEditPolicy,
) -> usize {
let mut changed = 0usize;
let mut i = 0usize;
while i < self.nodes[id.0 as usize].children.len() {
let child = self.nodes[id.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(child, scope);
match self.wml_revision_edit_action(child, scope, policy) {
WmlRevisionEditAction::Remove => {
self.nodes[id.0 as usize].children.remove(i);
changed += 1;
}
WmlRevisionEditAction::Unwrap => {
changed += self.edit_wml_revisions_descendants(child, scope, policy);
let replacement = self.nodes[child.0 as usize].children.clone();
let replacement_len = replacement.len();
self.nodes[id.0 as usize]
.children
.splice(i..=i, replacement);
changed += 1;
i += replacement_len;
}
WmlRevisionEditAction::RenameDeletedText => {
if self.rename_wml_del_text_to_text(child) {
changed += 1;
}
changed += self.edit_wml_revisions_descendants(child, scope, policy);
i += 1;
}
WmlRevisionEditAction::Keep => {
changed += self.edit_wml_revisions_descendants(child, scope, policy);
i += 1;
}
}
scope.truncate(base);
}
changed
}
fn wml_revision_edit_action(
&self,
id: NodeId,
scope: &[(Vec<u8>, Vec<u8>)],
policy: WmlRevisionEditPolicy,
) -> WmlRevisionEditAction {
let is_inserted = self.resolves_to(id, WML_NS, b"ins", scope)
|| self.resolves_to(id, WML_NS, b"moveTo", scope);
let is_deleted = self.resolves_to(id, WML_NS, b"del", scope)
|| self.resolves_to(id, WML_NS, b"moveFrom", scope);
let is_property_change = self.resolves_to(id, WML_NS, b"pPrChange", scope)
|| self.resolves_to(id, WML_NS, b"rPrChange", scope)
|| self.resolves_to(id, WML_NS, b"tblPrChange", scope)
|| self.resolves_to(id, WML_NS, b"trPrChange", scope)
|| self.resolves_to(id, WML_NS, b"tcPrChange", scope)
|| self.resolves_to(id, WML_NS, b"sectPrChange", scope);
if is_property_change {
return WmlRevisionEditAction::Remove;
}
match policy {
WmlRevisionEditPolicy::Accept => {
if is_inserted {
WmlRevisionEditAction::Unwrap
} else if is_deleted {
WmlRevisionEditAction::Remove
} else {
WmlRevisionEditAction::Keep
}
}
WmlRevisionEditPolicy::Reject => {
if is_inserted {
WmlRevisionEditAction::Remove
} else if is_deleted {
WmlRevisionEditAction::Unwrap
} else if self.resolves_to(id, WML_NS, b"delText", scope) {
WmlRevisionEditAction::RenameDeletedText
} else {
WmlRevisionEditAction::Keep
}
}
}
}
fn rename_wml_del_text_to_text(&mut self, id: NodeId) -> bool {
let Node::Element { name, .. } = &mut self.nodes[id.0 as usize].node else {
return false;
};
let local_start = name.iter().position(|&b| b == b':').map_or(0, |i| i + 1);
if &name[local_start..] != b"delText" {
return false;
}
name.truncate(local_start);
name.extend_from_slice(b"t");
true
}
fn collect_wml_t(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
out: &mut Vec<NodeId>,
) {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return;
}
if let Node::Element { name, .. } = &self.nodes[id.0 as usize].node {
let (prefix, lname): (&[u8], &[u8]) = match name.iter().position(|&b| b == b':') {
Some(i) => (&name[..i], &name[i + 1..]),
None => (b"", name),
};
if lname == b"t" {
let uri = scope.iter().rev().find(|(p, _)| p.as_slice() == prefix);
if uri.map(|(_, u)| u.as_slice()) == Some(WML_NS) {
out.push(id);
}
}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_t(c, scope, out);
}
scope.truncate(base);
}
fn collect_wml_content_control_text_by_tag(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
tag: &[u8],
out: &mut Vec<Vec<NodeId>>,
) {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return;
}
if self.resolves_to(id, WML_NS, b"sdt", scope) && self.wml_sdt_tag_matches(id, scope, tag) {
let mut runs = Vec::new();
self.collect_wml_t(id, scope, &mut runs);
out.push(runs);
scope.truncate(base);
return;
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_content_control_text_by_tag(c, scope, tag, out);
}
scope.truncate(base);
}
fn wml_sdt_tag_matches(
&self,
sdt: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
tag: &[u8],
) -> bool {
for i in 0..self.nodes[sdt.0 as usize].children.len() {
let c = self.nodes[sdt.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
if self.resolves_to(c, WML_NS, b"sdtPr", scope) {
let found = self.wml_sdtpr_tag_matches(c, scope, tag);
scope.truncate(base);
return found;
}
scope.truncate(base);
}
false
}
fn wml_sdtpr_tag_matches(
&self,
sdt_pr: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
tag: &[u8],
) -> bool {
for i in 0..self.nodes[sdt_pr.0 as usize].children.len() {
let c = self.nodes[sdt_pr.0 as usize].children[i];
let base = scope.len();
self.push_xmlns(c, scope);
let matches = self.resolves_to(c, WML_NS, b"tag", scope)
&& match &self.nodes[c.0 as usize].node {
Node::Element { attrs, .. } => attr_value_local(attrs, b"val")
.is_some_and(|value| trim_ascii_whitespace(value) == tag),
_ => false,
};
scope.truncate(base);
if matches {
return true;
}
}
false
}
fn collect_wml_hyperlink_rids(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
out: &mut Vec<String>,
) {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return;
}
if self.resolves_to(id, WML_NS, b"hyperlink", scope) {
if let Node::Element { attrs, .. } = &self.nodes[id.0 as usize].node {
if let Some(rid) = attr_value_ns_local(attrs, scope, OOXML_REL_NS, b"id")
.map(trim_ascii_whitespace)
.and_then(|v| std::str::from_utf8(v).ok())
{
out.push(rid.to_string());
}
}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_hyperlink_rids(c, scope, out);
}
scope.truncate(base);
}
fn find_wml_run_range_with_text(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
anchor_text: &str,
out: &mut Option<WmlRunRange>,
) -> bool {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return false;
}
if self.find_wml_child_run_range(id, scope, anchor_text, out) {
scope.truncate(base);
return true;
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
if self.find_wml_run_range_with_text(c, scope, anchor_text, out) {
scope.truncate(base);
return true;
}
}
scope.truncate(base);
false
}
fn find_wml_child_run_range(
&self,
parent: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
anchor_text: &str,
out: &mut Option<WmlRunRange>,
) -> bool {
let children = &self.nodes[parent.0 as usize].children;
for start in 0..children.len() {
let Some(first_text) = self.wml_run_text(children[start], scope) else {
continue;
};
let mut candidate = first_text;
if candidate == anchor_text {
*out = Some(WmlRunRange {
parent,
first_index: start,
last_index: start,
});
return true;
}
if !anchor_text.starts_with(&candidate) {
continue;
}
for (end, &child) in children.iter().enumerate().skip(start + 1) {
let Some(text) = self.wml_run_text(child, scope) else {
break;
};
candidate.push_str(&text);
if candidate == anchor_text {
*out = Some(WmlRunRange {
parent,
first_index: start,
last_index: end,
});
return true;
}
if !anchor_text.starts_with(&candidate) {
break;
}
}
}
false
}
fn wml_run_text(&self, id: NodeId, scope: &mut Vec<(Vec<u8>, Vec<u8>)>) -> Option<String> {
let base = scope.len();
self.push_xmlns(id, scope);
let text = if self.resolves_to(id, WML_NS, b"r", scope) {
let mut runs = Vec::new();
self.collect_wml_t(id, scope, &mut runs);
Some(runs.into_iter().map(|run| self.text_of(run)).collect())
} else {
None
};
scope.truncate(base);
text
}
fn wml_comment_text_edit_target_under(
&self,
root: NodeId,
comment_id: &str,
) -> Option<WmlCommentTextEditTarget> {
let mut out = None;
let mut scope = self.ns_scope_at(root);
for i in 0..self.nodes[root.0 as usize].children.len() {
if out.is_some() {
break;
}
let c = self.nodes[root.0 as usize].children[i];
self.collect_wml_comment_text_edit_target(
c,
&mut scope,
comment_id.as_bytes(),
&mut out,
);
}
out
}
fn collect_wml_comment_text_edit_target(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
comment_id: &[u8],
out: &mut Option<WmlCommentTextEditTarget>,
) {
if out.is_some() {
return;
}
let base = scope.len();
if let Node::Element { attrs, .. } = &self.nodes[id.0 as usize].node {
for (k, v) in attrs {
if k.as_slice() == b"xmlns" {
scope.push((Vec::new(), v.clone()));
} else if let Some(p) = k.strip_prefix(b"xmlns:".as_slice()) {
scope.push((p.to_vec(), v.clone()));
}
}
if self.resolves_to(id, WML_NS, b"comment", scope)
&& attr_value_local(attrs, b"id")
.is_some_and(|value| trim_ascii_whitespace(value) == comment_id)
{
let mut target = WmlCommentTextEditTarget {
first_run: None,
text_runs: Vec::new(),
};
self.collect_wml_comment_text_edit_descendants(id, scope, None, &mut target);
*out = Some(target);
scope.truncate(base);
return;
}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_comment_text_edit_target(c, scope, comment_id, out);
if out.is_some() {
break;
}
}
scope.truncate(base);
}
fn collect_wml_comment_text_edit_descendants(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
current_run: Option<NodeId>,
target: &mut WmlCommentTextEditTarget,
) {
let base = scope.len();
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return;
}
let run = if self.resolves_to(id, WML_NS, b"r", scope) {
Some(id)
} else {
current_run
};
if self.resolves_to(id, WML_NS, b"t", scope) {
target.text_runs.push(id);
if target.first_run.is_none() {
target.first_run = run;
}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_comment_text_edit_descendants(c, scope, run, target);
}
scope.truncate(base);
}
fn collect_wml_field_results(
&self,
id: NodeId,
scope: &mut Vec<(Vec<u8>, Vec<u8>)>,
scan: &mut FieldScan,
) {
if scan.target.is_some() {
return;
}
let base = scope.len();
let mut fld_char_type: Option<Vec<u8>> = None;
self.push_xmlns(id, scope);
if self.wml_revision_edit_action(id, scope, WmlRevisionEditPolicy::Accept)
== WmlRevisionEditAction::Remove
{
scope.truncate(base);
return;
}
if let Node::Element { attrs, .. } = &self.nodes[id.0 as usize].node {
if self.resolves_to(id, WML_NS, b"fldSimple", scope) {
if scan.seen == scan.wanted {
let mut result = Vec::new();
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_t(c, scope, &mut result);
}
scan.target = Some(result);
}
scan.seen += 1;
scope.truncate(base);
return;
}
if self.resolves_to(id, WML_NS, b"fldChar", scope) {
fld_char_type = attr_value_local(attrs, b"fldCharType")
.map(trim_ascii_whitespace)
.map(Vec::from);
} else if self.resolves_to(id, WML_NS, b"t", scope) {
if let Some(complex) = scan.complex.last_mut() {
if complex.result_phase {
complex.result_runs.push(id);
}
}
}
}
match fld_char_type.as_deref() {
Some(b"begin") => {
scan.complex.push(ComplexFieldScan {
result_phase: false,
result_runs: Vec::new(),
});
}
Some(b"separate") => {
if let Some(complex) = scan.complex.last_mut() {
complex.result_phase = true;
}
}
Some(b"end") => {
if let Some(complex) = scan.complex.pop() {
if let Some(parent) = scan.complex.last_mut() {
if parent.result_phase {
parent
.result_runs
.extend(complex.result_runs.iter().copied());
}
}
if scan.seen == scan.wanted {
scan.target = Some(complex.result_runs);
}
scan.seen += 1;
}
scope.truncate(base);
return;
}
_ => {}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
if scan.target.is_some() {
break;
}
let c = self.nodes[id.0 as usize].children[i];
self.collect_wml_field_results(c, scope, scan);
}
scope.truncate(base);
}
pub(crate) fn fresh_drawing_id(&self) -> u32 {
let mut used = std::collections::HashSet::new();
for r in 0..self.roots.len() {
self.collect_drawing_ids(self.roots[r], &mut used);
}
(1..=used.len() as u32 + 1)
.find(|i| !used.contains(i))
.unwrap_or(1)
}
fn collect_drawing_ids(&self, id: NodeId, used: &mut std::collections::HashSet<u32>) {
if matches!(self.local_name(id), Some(b"docPr") | Some(b"cNvPr")) {
if let Node::Element { attrs, .. } = &self.nodes[id.0 as usize].node {
if let Some((_, v)) = attrs.iter().find(|(k, _)| k.as_slice() == b"id") {
if let Some(n) = std::str::from_utf8(v)
.ok()
.and_then(|s| s.parse::<u32>().ok())
{
used.insert(n);
}
}
}
}
for i in 0..self.nodes[id.0 as usize].children.len() {
let c = self.nodes[id.0 as usize].children[i];
self.collect_drawing_ids(c, used);
}
}
pub(crate) fn insert_fragment_before_ns_local(
&mut self,
parent: NodeId,
xml: &[u8],
before_ns: &[u8],
before_local: &[u8],
) -> Result<()> {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Docx(
"simulated commit-time allocation failure (test seam)".into(),
));
}
let scope = self.ns_scope_at(parent);
let children = self.nodes[parent.0 as usize].children.clone();
let pos = (0..children.len()).rev().find(|&i| {
let cid = children[i];
let Node::Element { name, attrs, .. } = &self.nodes[cid.0 as usize].node else {
return false;
};
let (prefix, lname): (&[u8], &[u8]) = match name.iter().position(|&b| b == b':') {
Some(j) => (&name[..j], &name[j + 1..]),
None => (b"", name),
};
if lname != before_local {
return false;
}
let own = attrs.iter().rev().find_map(|(k, v)| {
let binds = (k.as_slice() == b"xmlns" && prefix.is_empty())
|| k.strip_prefix(b"xmlns:".as_slice()) == Some(prefix);
binds.then_some(v.as_slice())
});
let uri = own.or_else(|| {
scope
.iter()
.rev()
.find(|(p, _)| p.as_slice() == prefix)
.map(|(_, u)| u.as_slice())
});
uri == Some(before_ns)
});
let frag = XmlTree::parse(xml)?;
if self.nodes.len().saturating_add(frag.nodes.len()) > node_budget() {
return Err(Error::Docx("edit would exceed the node budget".into()));
}
let added: Vec<NodeId> = frag
.roots
.iter()
.map(|&r| self.graft(&frag, r, parent))
.collect::<Result<_>>()?;
let n = added.len();
let pi = parent.0 as usize;
let head_len = self.nodes[pi].children.len() - n;
if let Some(p) = pos {
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(p + k, id);
}
}
Ok(())
}
fn insert_fragment_at(&mut self, parent: NodeId, index: usize, xml: &[u8]) -> Result<()> {
#[cfg(test)]
if commit_should_fail() {
return Err(Error::Docx(
"simulated commit-time allocation failure (test seam)".into(),
));
}
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::Docx("edit would exceed the node budget".into()));
}
let added: Vec<NodeId> = frag
.roots
.iter()
.map(|&r| self.graft(&frag, r, parent))
.collect::<Result<_>>()?;
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(())
}
fn graft(&mut self, src: &XmlTree, src_id: NodeId, parent: NodeId) -> Result<NodeId> {
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(tree: &XmlTree) -> String {
String::from_utf8(tree.serialize()).unwrap()
}
#[test]
fn round_trips_unknown_elements_and_structure() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="urn:w" xmlns:mc="urn:mc"><w:body><w:sdt><w:sdtContent><w:p><w:r><w:t>keep me</w:t></w:r></w:p></w:sdtContent></w:sdt><w:fldSimple w:instr=" PAGE "><w:r><w:t>1</w:t></w:r></w:fldSimple><mc:AlternateContent><mc:Choice><w:drawing/></mc:Choice></mc:AlternateContent></w:body></w:document>"#;
let tree = XmlTree::parse(xml).unwrap();
let out = s(&tree);
for needle in [
"w:sdt",
"w:sdtContent",
"w:fldSimple",
"w:instr=\" PAGE \"",
"mc:AlternateContent",
"mc:Choice",
"w:drawing/",
"keep me",
"xmlns:mc=\"urn:mc\"",
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
] {
assert!(out.contains(needle), "lost {needle:?} in:\n{out}");
}
}
#[test]
fn preserves_attribute_order() {
let xml = br#"<w:p w:a="1" w:c="3" w:b="2"/>"#;
let out = s(&XmlTree::parse(xml).unwrap());
assert_eq!(out, r#"<w:p w:a="1" w:c="3" w:b="2"/>"#);
}
#[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 text_entities_round_trip() {
let xml = br#"<t>a & b <c> "q"</t>"#;
let tree = XmlTree::parse(xml).unwrap();
let root = tree.roots()[0];
let child = tree.children(root)[0];
assert_eq!(
tree.node(child).clone_text(),
Some(br#"a & b <c> "q""#.to_vec())
);
assert_eq!(s(&tree), r#"<t>a & b <c> "q"</t>"#);
}
#[test]
fn empty_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 carriage_return_escaped_to_survive_normalization() {
let mut t = XmlTree::parse(b"<w:t>x</w:t>").unwrap();
let id = t.roots()[0];
t.set_element_text(id, "a\rb").unwrap();
assert_eq!(s(&t), "<w:t>a b</w:t>");
let re = XmlTree::parse(s(&t).as_bytes()).unwrap();
let child = re.children(re.roots()[0])[0];
assert_eq!(re.node(child).clone_text(), Some(b"a\rb".to_vec()));
}
#[test]
fn prolog_crlf_preserved_verbatim() {
let xml = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<a><b>x</b></a>";
assert_eq!(XmlTree::parse(xml).unwrap().serialize(), xml);
}
#[test]
fn edited_values_drop_xml_forbidden_scalars() {
let mut t = XmlTree::parse(b"<w:t>x</w:t>").unwrap();
let id = t.roots()[0];
t.set_element_text(id, "a\u{FFFE}b\u{FFFF}c").unwrap();
assert_eq!(s(&t), "<w:t>abc</w:t>");
let mut attr = XmlTree::parse(b"<w:t/>").unwrap();
let id = attr.roots()[0];
attr.set_attr(id, b"data-x", "a\u{FFFF}b".as_bytes())
.unwrap();
assert_eq!(s(&attr), r#"<w:t data-x="ab"/>"#);
}
#[test]
fn wml_text_runs_resolves_namespaces() {
let xml = br#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><w:body><w:t>A</w:t><w:drawing><a:t>B</a:t><a:x xmlns="http://schemas.openxmlformats.org/drawingml/2006/main"><t>C</t></a:x></w:drawing></w:body></w:document>"#;
let tree = XmlTree::parse(xml).unwrap();
let runs = tree.wml_text_runs();
let texts: Vec<String> = runs.iter().map(|&id| tree.text_of(id)).collect();
assert_eq!(
texts,
vec!["A".to_string()],
"only the WML w:t should match"
);
}
#[test]
fn wml_body_strict_rejects_top_level_cdata_and_doctype() {
let doc = br#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body/></w:document>"#;
let mut cdata = b"<![CDATA[junk]]>".to_vec();
cdata.extend_from_slice(doc);
assert!(
XmlTree::parse(&cdata).unwrap().wml_body_strict().is_err(),
"top-level CDATA is character data outside the root"
);
let mut doctype = b"<!DOCTYPE w:document>".to_vec();
doctype.extend_from_slice(doc);
assert!(
XmlTree::parse(&doctype).unwrap().wml_body_strict().is_err(),
"document.xml must not accept a top-level DOCTYPE"
);
let mut comment = b"<!--ok--><?ok?>".to_vec();
comment.extend_from_slice(doc);
assert!(
XmlTree::parse(&comment).unwrap().wml_body_strict().is_ok(),
"comments and processing instructions stay allowed outside the root"
);
}
#[test]
fn local_name_strips_prefix() {
let tree = XmlTree::parse(br#"<w:p/>"#).unwrap();
assert_eq!(tree.local_name(tree.roots()[0]), Some(&b"p"[..]));
}
#[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 truncated_xml_with_open_elements_is_rejected() {
assert!(XmlTree::parse(b"<a><b>").is_err());
assert!(XmlTree::parse(b"<w:p><w:r><w:t>OLD").is_err());
assert!(XmlTree::parse(b"<a></a>").is_ok()); }
#[test]
fn too_many_attributes_is_rejected() {
set_test_max_attrs(4);
let r = XmlTree::parse(br#"<w:p a0="" a1="" a2="" a3="" a4=""/>"#);
set_test_max_attrs(MAX_ATTRS_PER_ELEMENT);
assert!(r.is_err());
assert!(XmlTree::parse(br#"<w:p a0="" a1=""/>"#).is_ok());
}
#[test]
fn malformed_entity_refs_are_rejected() {
assert!(XmlTree::parse(b"<w:t>x&bogus;y</w:t>").is_err());
assert!(XmlTree::parse(br#"<w:p w:x="a&bogus;b"/>"#).is_err());
assert!(XmlTree::parse(b"<w:t>x&y</w:t>").is_ok());
assert!(XmlTree::parse(br#"<w:p w:x="a&b"/>"#).is_ok());
}
#[test]
fn node_budget_boundary_matches_edits() {
set_test_node_budget(3);
assert!(XmlTree::parse(b"<a><b/><c/></a>").is_ok()); assert!(XmlTree::parse(b"<a><b/><c/><d/></a>").is_err()); reset_test_node_budget();
}
#[test]
fn set_element_text_reuses_cdata_slot_without_growth() {
let mut t = XmlTree::parse(b"<w:t><![CDATA[OLD]]></w:t>").unwrap();
let before = t.node_count();
let id = t.roots()[0];
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!(String::from_utf8(t.serialize()).unwrap(), "<w:t>NEW</w:t>");
}
#[test]
fn empty_element_respects_depth_cap() {
let nest = |opens: usize| {
let mut s = String::new();
for _ in 0..opens {
s.push_str("<a>");
}
s.push_str("<b/>");
for _ in 0..opens {
s.push_str("</a>");
}
s
};
assert!(
XmlTree::parse(nest(MAX_DEPTH).as_bytes()).is_err(),
"an empty element past MAX_DEPTH must be rejected"
);
assert!(
XmlTree::parse(nest(MAX_DEPTH - 1).as_bytes()).is_ok(),
"an empty element at the cap should still parse"
);
}
#[test]
fn set_attr_enforces_attribute_cap_symmetric_with_parse() {
set_test_max_attrs(2);
let mut t = XmlTree::parse(br#"<w:t a="1" b="2">x</w:t>"#).unwrap();
let id = t.roots()[0];
assert!(!t.can_set_attr(id, b"xml:space"), "should report no room");
assert!(
t.set_element_text(id, " x ").is_err(),
"adding xml:space past the cap must error, not build an un-re-parseable element"
);
assert!(t.can_set_attr(id, b"a"));
t.set_attr(id, b"a", b"9").unwrap();
t.set_element_text(id, "y").unwrap();
assert_eq!(t.text_of(id), "y");
set_test_max_attrs(MAX_ATTRS_PER_ELEMENT);
}
}