Skip to main content

xml_core/
reader.rs

1//! Low-level XML reading.
2
3use quick_xml::events::Event;
4
5use crate::error::{Error, Result};
6
7/// Reads a sequence of generic XML events from an in-memory UTF-8 string.
8///
9/// This is a thin wrapper around [`quick_xml::Reader`], scoped to `xml-core`'s
10/// own error type so that callers never need to depend on `quick_xml`
11/// directly.
12///
13/// # Whitespace handling
14///
15/// Unlike some XML tooling, this reader does **not** trim whitespace from
16/// text nodes automatically. Whether leading/trailing whitespace inside an
17/// element is significant depends on the vocabulary being parsed (for
18/// example, `xml:space="preserve"` in WordprocessingML) — a decision that
19/// belongs to the higher-level crates, not to this generic layer.
20///
21/// # Scope
22///
23/// This first version only supports reading from an in-memory `&str`. Real
24/// OOXML parts are read fully into memory from their ZIP entry before
25/// parsing anyway, so this covers the toolkit's actual needs; streaming
26/// from an arbitrary [`std::io::Read`] source can be added later if needed.
27pub struct Reader<'a> {
28    inner: quick_xml::Reader<&'a [u8]>,
29}
30
31impl<'a> Reader<'a> {
32    /// Creates a new reader over the given XML string.
33    pub fn from_xml_str(xml: &'a str) -> Self {
34        Self {
35            inner: quick_xml::Reader::from_str(xml),
36        }
37    }
38
39    /// Reads the next XML event.
40    ///
41    /// Returns `Ok(Event::Eof)` once the end of the input has been reached.
42    pub fn read_event(&mut self) -> Result<Event<'a>> {
43        self.inner.read_event().map_err(Error::from)
44    }
45}
46
47/// Decodes an XML text event's content: character-encoding decoding (e.g.
48/// UTF-16 to UTF-8) via [`quick_xml::events::BytesText::decode`], plus a
49/// defensive `unescape()` pass in case any literal `&...;` survived in the
50/// event's own bytes (see below for why that's normally not needed).
51///
52/// # Why a `Text` event alone isn't the whole story
53///
54/// Despite what a quick read of `BytesText::decode`'s doc comment might
55/// suggest, a `quick_xml` `Reader` does **not** hand back one `Event::Text`
56/// containing an entire text node's escaped content verbatim. Starting
57/// with the version this project depends on (`quick_xml` 0.41), any
58/// `&entity;`/`&#NNN;` reference found in text content is split out into
59/// its own dedicated event, [`quick_xml::events::Event::GeneralRef`] —
60/// confirmed by reading `quick_xml`'s own `Event` enum documentation after
61/// a real round-trip test (an apostrophe in a footnote's text) came back
62/// not as a literal `&apos;` and not restored to `'`, but *entirely
63/// missing*, which a pure "forgot to unescape" bug wouldn't explain (that
64/// would leave the entity text verbatim, not delete it). The surrounding
65/// plain-text fragments arrive as ordinary (already-unescaped-looking)
66/// `Text` events; the reference itself must be resolved separately via
67/// [`decode_general_ref`] and appended in between — see `word-ooxml`'s
68/// `parse_paragraph` for the accumulation loop that does this. Callers
69/// that only ever call `.decode()` (or this function) on `Text` events and
70/// silently ignore `GeneralRef` — as this crate's readers did before this
71/// was diagnosed — see every entity vanish, not survive unescaped.
72///
73/// This function's own `unescape()` call is consequently closer to a
74/// no-op/safety-net for stray literal `&` sequences than the primary fix;
75/// kept anyway since it's harmless and cheap. See the CHANGELOG for the
76/// full story.
77pub fn decode_text(text: &quick_xml::events::BytesText<'_>) -> Result<String> {
78    let decoded = text.decode()?;
79    let unescaped = quick_xml::escape::unescape(&decoded)?;
80    Ok(unescaped.into_owned())
81}
82
83/// Resolves a `&entity;`/`&#NNN;` general reference
84/// ([`quick_xml::events::Event::GeneralRef`], `BytesRef`) found in text
85/// content to the character(s) it represents — see [`decode_text`]'s doc
86/// comment for why this is a *separate* event from `Text` in `quick_xml`
87/// 0.41, and therefore a separate, necessary step, not an edge case.
88///
89/// Handles the two kinds of reference this toolkit's own `Writer` (and any
90/// real Word-authored document) can produce: a numeric character reference
91/// (`&#65;`/`&#x41;`, resolved via [`quick_xml::events::BytesRef::resolve_char_ref`])
92/// and the five predefined XML entities (`&amp;`/`&lt;`/`&gt;`/`&apos;`/
93/// `&quot;`, resolved via [`quick_xml::escape::resolve_xml_entity`]).
94/// Returns `Ok(None)` for anything else (a custom, DTD-declared entity —
95/// this crate has no DTD support to resolve one) — callers drop it rather
96/// than failing the whole document, the same forgiving policy applied to
97/// other malformed/unsupported constructs throughout this toolkit's
98/// readers.
99pub fn decode_general_ref(reference: &quick_xml::events::BytesRef<'_>) -> Result<Option<String>> {
100    if let Some(ch) = reference.resolve_char_ref()? {
101        return Ok(Some(ch.to_string()));
102    }
103    let name = reference.decode()?;
104    Ok(quick_xml::escape::resolve_xml_entity(&name).map(str::to_string))
105}
106
107/// Decodes an XML attribute's raw value, undoing XML entity escaping —
108/// mirrors [`decode_text`] for attribute values. `quick_xml`'s own
109/// `Attribute::from((&str, &str))` (used throughout this toolkit's writers
110/// via `BytesStart::push_attribute`) escapes the value the exact same way
111/// `BytesText::new` does, so this is needed for the same reason.
112///
113/// Returns `Ok(None)` if `value` isn't valid UTF-8, rather than an error —
114/// this crate's readers treat a malformed attribute as simply absent
115/// rather than failing the whole document (see callers). An `Err` here
116/// specifically means the bytes *were* valid UTF-8 but contained a
117/// malformed or unknown entity reference, a stronger signal of a genuinely
118/// broken document.
119pub fn decode_attribute_value(value: &[u8]) -> Result<Option<String>> {
120    let Ok(text) = std::str::from_utf8(value) else {
121        return Ok(None);
122    };
123    let unescaped = quick_xml::escape::unescape(text)?;
124    Ok(Some(unescaped.into_owned()))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn reads_a_simple_element_with_text() {
133        let mut reader = Reader::from_xml_str("<a>hello</a>");
134
135        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
136
137        match reader.read_event().unwrap() {
138            Event::Text(text) => assert_eq!(text.decode().unwrap(), "hello"),
139            other => panic!("expected a Text event, got {other:?}"),
140        }
141
142        assert!(matches!(reader.read_event().unwrap(), Event::End(_)));
143        assert!(matches!(reader.read_event().unwrap(), Event::Eof));
144    }
145
146    #[test]
147    fn a_text_node_with_entities_arrives_as_interleaved_text_and_generalref_events() {
148        // Demonstrates the mechanism `decode_text`/`decode_general_ref`'s
149        // doc comments describe: a single logical text node containing
150        // entity references is NOT handed back as one `Event::Text` with
151        // the entities left escaped inside it — each reference is its own
152        // `Event::GeneralRef`, interleaved with plain `Event::Text`
153        // fragments for whatever surrounds it.
154        let mut reader = Reader::from_xml_str(
155            "<a>Bells &amp; whistles, &apos;quoted&apos; and &lt;tagged&gt;</a>",
156        );
157        reader.read_event().unwrap(); // consume <a>
158
159        let mut reconstructed = String::new();
160        loop {
161            match reader.read_event().unwrap() {
162                Event::Text(text) => reconstructed.push_str(&decode_text(&text).unwrap()),
163                Event::GeneralRef(reference) => {
164                    if let Some(resolved) = decode_general_ref(&reference).unwrap() {
165                        reconstructed.push_str(&resolved);
166                    }
167                }
168                Event::End(_) => break,
169                other => panic!("unexpected event: {other:?}"),
170            }
171        }
172
173        assert_eq!(reconstructed, "Bells & whistles, 'quoted' and <tagged>");
174    }
175
176    #[test]
177    fn decode_general_ref_resolves_numeric_character_references() {
178        let mut reader = Reader::from_xml_str("<a>&#65;&#x42;</a>");
179        reader.read_event().unwrap(); // consume <a>
180
181        let mut resolved = String::new();
182        loop {
183            match reader.read_event().unwrap() {
184                Event::GeneralRef(reference) => {
185                    resolved.push_str(&decode_general_ref(&reference).unwrap().unwrap())
186                }
187                Event::End(_) => break,
188                other => panic!("unexpected event: {other:?}"),
189            }
190        }
191
192        assert_eq!(resolved, "AB");
193    }
194
195    #[test]
196    fn decode_attribute_value_unescapes_entities() {
197        let mut reader = Reader::from_xml_str(r#"<a k="Bells &amp; whistles"/>"#);
198
199        match reader.read_event().unwrap() {
200            Event::Empty(start) => {
201                let attribute = start.attributes().next().unwrap().unwrap();
202                assert_eq!(attribute.value.as_ref(), b"Bells &amp; whistles");
203                assert_eq!(
204                    decode_attribute_value(attribute.value.as_ref())
205                        .unwrap()
206                        .as_deref(),
207                    Some("Bells & whistles")
208                );
209            }
210            other => panic!("expected an Empty event, got {other:?}"),
211        }
212    }
213
214    #[test]
215    fn reads_attributes_on_an_empty_element() {
216        let mut reader = Reader::from_xml_str(r#"<a k="v"/>"#);
217
218        match reader.read_event().unwrap() {
219            Event::Empty(start) => {
220                let attribute = start.attributes().next().unwrap().unwrap();
221                assert_eq!(attribute.key.as_ref(), b"k");
222                assert_eq!(attribute.value.as_ref(), b"v");
223            }
224            other => panic!("expected an Empty event, got {other:?}"),
225        }
226    }
227
228    #[test]
229    fn reports_malformed_xml_as_an_error() {
230        let mut reader = Reader::from_xml_str("<a><b></a>");
231
232        // First event is the opening `<a>` tag.
233        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
234        // Second event is the opening `<b>` tag.
235        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
236        // The mismatched closing tag `</a>` must surface as an `Error`, not a panic.
237        assert!(reader.read_event().is_err());
238    }
239}