//! A small, bounded XML tree on top of `quick-xml`, plus a string-building XML writer. //! //! Element and attribute names are rewritten to *canonical prefixes* by namespace URI (`w:p`, //! `r:id`, `a:blip`…), so the reader doesn't care which prefixes a producer chose. Names in //! unknown namespaces keep their local name behind a `?:` prefix. use quick_xml::XmlVersion; use quick_xml::events::Event; use quick_xml::name::ResolveResult; use quick_xml::reader::NsReader; use crate::DocxError; /// Maximum element nesting accepted by the parser. pub const MAX_DEPTH: usize = 256; /// Maximum number of elements in one part. pub const MAX_ELEMENTS: usize = 8_000_000; /// Known namespaces and the canonical prefix we use for them (read and write). pub const NAMESPACES: &[(&str, &str)] = &[ ("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"), ("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"), ("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"), ("a", "http://schemas.openxmlformats.org/drawingml/2006/main"), ("c", "http://schemas.openxmlformats.org/drawingml/2006/chart"), ("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture"), ("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006"), ("w14", "http://schemas.microsoft.com/office/word/2010/wordml"), ("w15", "http://schemas.microsoft.com/office/word/2012/wordml"), ("wp14", "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"), ("wps", "http://schemas.microsoft.com/office/word/2010/wordprocessingShape"), ("wpg", "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"), ("wpc", "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"), ("dgm", "http://schemas.openxmlformats.org/drawingml/2006/diagram"), ("dsp", "http://schemas.microsoft.com/office/drawing/2008/diagram"), ("m", "http://schemas.openxmlformats.org/officeDocument/2006/math"), ("v", "urn:schemas-microsoft-com:vml"), ("o", "urn:schemas-microsoft-com:office:office"), ("w10", "urn:schemas-microsoft-com:office:word"), ]; /// The `a:ext` URI under which a drawing is marked decorative ([MS-ODRAWXML]), and the /// namespace of its `adec:decorative` element. pub const DECORATIVE_EXT: &str = "{C183D7F6-B498-43B3-948B-1728B52AA6E4}"; pub const DECORATIVE_NS: &str = "http://schemas.microsoft.com/office/drawing/2017/decorative"; /// Namespaces that only appear in non-body parts. const OTHER_NAMESPACES: &[(&str, &str)] = &[ ("rel", "http://schemas.openxmlformats.org/package/2006/relationships"), ("ct", "http://schemas.openxmlformats.org/package/2006/content-types"), ("cp", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"), ("dc", "http://purl.org/dc/elements/1.1/"), ("dcterms", "http://purl.org/dc/terms/"), ("xsi", "http://www.w3.org/2001/XMLSchema-instance"), ("xml", "http://www.w3.org/XML/1998/namespace"), ("ep", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"), ("op", "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"), ("vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"), ]; /// ISO/IEC 29500 Strict namespaces map onto the transitional prefixes. const STRICT: &[(&str, &str)] = &[ ("w", "http://purl.oclc.org/ooxml/wordprocessingml/main"), ("r", "http://purl.oclc.org/ooxml/officeDocument/relationships"), ("wp", "http://purl.oclc.org/ooxml/drawingml/wordprocessingDrawing"), ("a", "http://purl.oclc.org/ooxml/drawingml/main"), ("pic", "http://purl.oclc.org/ooxml/drawingml/picture"), ("m", "http://purl.oclc.org/ooxml/officeDocument/math"), ]; fn prefix_for(uri: &[u8], extra: &[(&'static str, &str)]) -> &'static str { if let Some((p, _)) = extra.iter().find(|(_, u)| u.as_bytes() == uri) { return p; } for (p, u) in NAMESPACES.iter().chain(OTHER_NAMESPACES).chain(STRICT) { if u.as_bytes() == uri { return p; } } "?" } #[derive(Clone, Debug, PartialEq)] pub enum Node { El(El), Text(String), } /// An element with canonical-prefixed name and attributes. #[derive(Clone, Debug, PartialEq, Default)] pub struct El { pub name: String, pub attrs: Vec<(String, String)>, pub kids: Vec, } impl El { /// Attribute by canonical name (`w:val`); falls back to an unqualified attribute with the /// same local name (some producers omit the prefix). pub fn attr(&self, name: &str) -> Option<&str> { if let Some((_, v)) = self.attrs.iter().find(|(k, _)| k == name) { return Some(v); } let local = name.rsplit(':').next().unwrap_or(name); if local != name { return self.attrs.iter().find(|(k, _)| k == local).map(|(_, v)| v.as_str()); } None } /// Child elements. pub fn els(&self) -> impl Iterator { self.kids.iter().filter_map(|n| match n { Node::El(e) => Some(e), Node::Text(_) => None, }) } pub fn child(&self, name: &str) -> Option<&El> { self.els().find(|e| e.name == name) } pub fn children<'a>(&'a self, name: &'a str) -> impl Iterator + 'a { self.els().filter(move |e| e.name == name) } /// `w:val` of a child. pub fn child_val(&self, name: &str) -> Option<&str> { self.child(name).and_then(|c| c.attr("w:val")) } /// Direct text content. pub fn text(&self) -> String { let mut s = String::new(); for n in &self.kids { if let Node::Text(t) = n { s.push_str(t); } } s } /// All descendant text (depth-bounded by the parser). pub fn deep_text(&self) -> String { let mut s = String::new(); fn go(e: &El, s: &mut String, depth: usize) { if depth > MAX_DEPTH { return; } for n in &e.kids { match n { Node::Text(t) => s.push_str(t), Node::El(c) => go(c, s, depth + 1), } } } go(self, &mut s, 0); s } /// First descendant with this name (depth-first, bounded). pub fn find(&self, name: &str) -> Option<&El> { fn go<'a>(e: &'a El, name: &str, depth: usize) -> Option<&'a El> { if depth > MAX_DEPTH { return None; } for c in e.els() { if c.name == name { return Some(c); } if let Some(f) = go(c, name, depth + 1) { return Some(f); } } None } go(self, name, 0) } /// The element as XML with canonical prefixes (for re-emitting kept content). Elements and /// attributes in unknown namespaces are dropped. pub fn to_xml(&self) -> String { fn go(e: &El, s: &mut String, depth: usize) { if depth > MAX_DEPTH || e.name.starts_with("?:") { return; } s.push('<'); s.push_str(&e.name); for (k, v) in &e.attrs { if k.starts_with("?:") { continue; } s.push(' '); s.push_str(k); s.push_str("=\""); s.push_str(&esc(v)); s.push('"'); } if e.kids.is_empty() { s.push_str("/>"); return; } s.push('>'); for n in &e.kids { match n { Node::Text(t) => s.push_str(&esc(t)), Node::El(c) => go(c, s, depth + 1), } } s.push_str("'); } let mut s = String::new(); go(self, &mut s, 0); s } /// Local part of the name. pub fn local(&self) -> &str { self.name.rsplit(':').next().unwrap_or(&self.name) } } /// UTF-16 LE/BE text with a byte-order mark as UTF-8 (`None` without one). Lossy: an unpaired /// surrogate becomes U+FFFD and a trailing odd byte is dropped. The result is at most 1.5× the input. pub(crate) fn utf16_to_utf8(bytes: &[u8]) -> Option { let (rest, le) = match bytes.get(..2) { Some([0xFF, 0xFE]) => (bytes.get(2..)?, true), Some([0xFE, 0xFF]) => (bytes.get(2..)?, false), _ => return None, }; let units = rest.as_chunks::<2>().0.iter().map(|c| if le { u16::from_le_bytes(*c) } else { u16::from_be_bytes(*c) }); Some(char::decode_utf16(units).map(|c| c.unwrap_or(char::REPLACEMENT_CHARACTER)).collect()) } /// Parse XML bytes into the root element. pub fn parse(bytes: &[u8]) -> Result { parse_ns(bytes, &[]) } /// [`parse`] with `extra` namespaces (prefix, URI) mapped to canonical prefixes first, so a /// related vocabulary (WordprocessingML 2003) reads with the prefixes the reader knows. pub fn parse_ns(bytes: &[u8], extra: &[(&'static str, &str)]) -> Result { // UTF-16 (with a BOM) is transcoded to UTF-8; a UTF-8 BOM is stripped. let transcoded = utf16_to_utf8(bytes); let bytes = match &transcoded { Some(s) => s.as_bytes(), None => bytes.strip_prefix(&[0xEF, 0xBB, 0xBF][..]).unwrap_or(bytes), }; let mut r = NsReader::from_reader(bytes); { let c = r.config_mut(); c.trim_text_start = false; c.trim_text_end = false; c.expand_empty_elements = false; c.check_end_names = false; } let mut stack: Vec = Vec::new(); let mut root: Option = None; let mut count = 0usize; let mut buf = Vec::new(); loop { buf.clear(); let (res, ev) = r.read_resolved_event_into(&mut buf).map_err(|e| DocxError::Xml(e.to_string()))?; let el_prefix = match &res { ResolveResult::Bound(ns) => Some(prefix_for(ns.as_ref(), extra)), _ => None, }; match ev { Event::Start(ref e) | Event::Empty(ref e) => { count += 1; if count > MAX_ELEMENTS { return Err(DocxError::Limit("too many XML elements".into())); } let local = String::from_utf8_lossy(e.local_name().as_ref()).into_owned(); let name = match el_prefix { Some(p) => format!("{p}:{local}"), None => local, }; let mut el = El { name, attrs: Vec::new(), kids: Vec::new() }; for a in e.attributes().with_checks(false) { let Ok(a) = a else { continue }; let key = a.key; let raw_key = key.as_ref(); if raw_key == b"xmlns" || raw_key.starts_with(b"xmlns:") { continue; } let (ares, alocal) = r.resolver_mut().resolve_attribute(key); let alocal = String::from_utf8_lossy(alocal.as_ref()).into_owned(); let aname = match ares { ResolveResult::Bound(ns) => format!("{}:{alocal}", prefix_for(ns.as_ref(), extra)), _ => alocal, }; let val = match a.normalized_value(XmlVersion::Explicit1_0) { Ok(v) => v.into_owned(), Err(_) => String::from_utf8_lossy(&a.value).into_owned(), }; el.attrs.push((aname, val)); } if matches!(ev, Event::Start(_)) { if stack.len() >= MAX_DEPTH { return Err(DocxError::Limit("XML nesting too deep".into())); } stack.push(el); } else { attach(&mut stack, &mut root, el); } } Event::End(_) => { if let Some(el) = stack.pop() { attach(&mut stack, &mut root, el); } } Event::Text(t) => { if let Some(top) = stack.last_mut() { let s = t.decode().map_err(|e| DocxError::Xml(e.to_string()))?; push_text(top, &s); } } Event::CData(t) => { if let Some(top) = stack.last_mut() { let s = String::from_utf8_lossy(&t).into_owned(); push_text(top, &s); } } Event::GeneralRef(g) => { if let Some(top) = stack.last_mut() { let ch = match g.resolve_char_ref() { Ok(Some(c)) => Some(c.to_string()), Ok(None) => { let n = g.decode().map_err(|e| DocxError::Xml(e.to_string()))?; quick_xml::escape::resolve_predefined_entity(&n).map(str::to_string) } Err(_) => None, }; if let Some(c) = ch { push_text(top, &c); } } } Event::Eof => break, _ => {} } if root.is_some() && stack.is_empty() { break; } } // Unclosed elements at EOF: close them (lenient). while let Some(el) = stack.pop() { attach(&mut stack, &mut root, el); } root.ok_or_else(|| DocxError::Xml("no root element".into())) } fn push_text(top: &mut El, s: &str) { if let Some(Node::Text(prev)) = top.kids.last_mut() { prev.push_str(s); } else { top.kids.push(Node::Text(s.to_string())); } } fn attach(stack: &mut [El], root: &mut Option, el: El) { match stack.last_mut() { Some(parent) => parent.kids.push(Node::El(el)), None => { if root.is_none() { *root = Some(el); } } } } /// Is `c` allowed in XML 1.0 text? fn xml_char_ok(c: char) -> bool { matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') } /// Escape text for element content or attribute values (drops characters XML can't carry). pub fn esc(s: &str) -> String { let mut o = String::with_capacity(s.len()); for c in s.chars() { match c { '&' => o.push_str("&"), '<' => o.push_str("<"), '>' => o.push_str(">"), '"' => o.push_str("""), '\'' => o.push_str("'"), '\t' => o.push_str(" "), '\n' => o.push_str(" "), '\r' => o.push_str(" "), c if xml_char_ok(c) => o.push(c), _ => {} } } o } /// A string-building XML writer. #[derive(Default)] pub struct W { pub s: String, } impl W { pub fn new() -> W { W { s: String::from("\n") } } fn attrs(&mut self, attrs: &[(&str, &str)]) { for (k, v) in attrs { self.s.push(' '); self.s.push_str(k); self.s.push_str("=\""); self.s.push_str(&esc(v)); self.s.push('"'); } } pub fn open(&mut self, name: &str, attrs: &[(&str, &str)]) { self.s.push('<'); self.s.push_str(name); self.attrs(attrs); self.s.push('>'); } pub fn empty(&mut self, name: &str, attrs: &[(&str, &str)]) { self.s.push('<'); self.s.push_str(name); self.attrs(attrs); self.s.push_str("/>"); } pub fn close(&mut self, name: &str) { self.s.push_str("'); } pub fn text(&mut self, t: &str) { self.s.push_str(&esc(t)); } /// `text`. pub fn leaf(&mut self, name: &str, attrs: &[(&str, &str)], t: &str) { self.open(name, attrs); self.text(t); self.close(name); } /// ``. pub fn val(&mut self, name: &str, v: &str) { self.empty(name, &[("w:val", v)]); } pub fn raw(&mut self, s: &str) { self.s.push_str(s); } pub fn into_bytes(self) -> Vec { self.s.into_bytes() } } /// `xmlns:` declarations for the body-part namespaces, plus `mc:Ignorable`. pub fn body_ns() -> Vec<(String, String)> { let mut v: Vec<(String, String)> = NAMESPACES.iter().map(|(p, u)| (format!("xmlns:{p}"), (*u).to_string())).collect(); v.push(("mc:Ignorable".into(), "w14 w15 wp14".into())); v } #[cfg(test)] mod tests { use super::*; #[test] fn utf16_parts_parse_and_odd_ones_never_panic() { let src = r#""#; for le in [true, false] { let mut b = if le { vec![0xFF, 0xFE] } else { vec![0xFE, 0xFF] }; b.extend(src.encode_utf16().flat_map(|u| if le { u.to_le_bytes() } else { u.to_be_bytes() })); let root = parse(&b).unwrap(); assert_eq!(root.els().next().and_then(|e| e.attr("Target")), Some("café😀.xml"), "le {le}"); } // Odd length, unpaired surrogates, a bare BOM: an error or a tree, never a panic. for b in [&[0xFF, 0xFE][..], &[0xFE, 0xFF, 0x00], &[0xFF, 0xFE, b'<', 0, 0x00, 0xD8, b'a', 0, b'/', 0, b'>', 0], &[0xFF, 0xFE, 0x00, 0xDC, 0x00]] { let _ = parse(b); } assert_eq!(utf16_to_utf8(&[0xFF, 0xFE, b'a', 0, 0x00, 0xD8, b'b']).as_deref(), Some("a\u{FFFD}")); assert_eq!(utf16_to_utf8(b""), None); } #[test] fn parses_with_namespaces() { let x = br#" hi A< "#; let e = parse(x).unwrap(); assert_eq!(e.name, "w:document"); let p = e.child("w:p").unwrap(); assert_eq!(p.attr("r:id"), Some("r1")); assert_eq!(p.attr("w:val"), Some("a&b")); let t = p.child("w:t").unwrap(); assert_eq!(t.text(), " hi A< "); assert_eq!(t.attr("xml:space"), Some("preserve")); } #[test] fn deep_nesting_is_error() { let mut s = String::new(); for _ in 0..1000 { s.push_str(""); } assert!(parse(s.as_bytes()).is_err()); } /// A start tag with thousands of `xmlns:` declarations is refused instead of allocated /// (RUSTSEC-2026-0195: unbounded namespace bindings in the resolver). #[test] fn too_many_namespace_declarations_is_error() { let mut s = String::from(""); assert!(parse(s.as_bytes()).is_err()); } #[test] fn garbage_is_error_not_panic() { assert!(parse(b"").is_err()); assert!(parse(b"\xff\xfe<").is_err()); let _ = parse(b""); let _ = parse(b""); } #[test] fn escapes() { assert_eq!(esc("a