Skip to main content

pith_zip/
xml.rs

1//! A minimal XML tokenizer, sized for the documents the suite actually
2//! meets: `word/document.xml` inside DOCX, `content.xml` inside ODF —
3//! well-formed XML produced by real office software, not hostile DTDs.
4//!
5//! What it emits: start tags with attributes, end tags (self-closing
6//! elements surface as `Start` followed by `End`), text with entities
7//! decoded, and CDATA content as text. What it skips silently: the XML
8//! declaration, other processing instructions, comments, and `<!DOCTYPE>`
9//! declarations (internal subset included). What it refuses: unknown
10//! entities, malformed names, mismatched end tags, a second root element.
11//!
12//! Namespaces are *not* resolved: `w:document` arrives as the literal
13//! name `w:document` and `xmlns:w="…"` as an ordinary attribute, because
14//! the consumers match element names verbatim and a half-resolution would
15//! be worse than none.
16//!
17//! The reader is an [`Iterator`] of [`Result`]`<`[`XmlEvent`]`>`. Once it
18//! returns `Err` or `None` it is done; a poisoned reader keeps returning
19//! `None` rather than resuming a corrupt position.
20
21use pith_digest::{Error, Result};
22
23/// One token of the document.
24#[derive(Clone, Debug, PartialEq)]
25pub enum XmlEvent<'a> {
26    /// `<name …>` — attributes in document order, values with entities
27    /// already decoded.
28    Start {
29        /// The element name, verbatim (`prefix:local` included).
30        name: &'a str,
31        /// `(name, decoded value)` pairs, document order preserved.
32        attrs: Vec<(&'a str, String)>,
33    },
34    /// `</name>` or the implied close of a self-closing `<name/>`.
35    End {
36        /// The element name being closed.
37        name: &'a str,
38    },
39    /// Character data or CDATA content. Entities are already decoded in
40    /// character data; CDATA content arrives verbatim.
41    Text(String),
42}
43
44/// A pull tokenizer over an XML document.
45///
46/// `XmlReader::new("<w:t>hi</w:t>")` hands out `Start(w:t)`,
47/// `Text("hi")`, `End(w:t)` and then `None`.
48pub struct XmlReader<'a> {
49    /// Unconsumed input.
50    input: &'a str,
51    /// Open element names, innermost last.
52    stack: Vec<&'a str>,
53    /// A `Start` from a self-closing tag produces its `End` on the next
54    /// call; at most one can be outstanding because `End` carries no
55    /// fields that would need a queue.
56    deferred_end: Option<&'a str>,
57    /// Whether the (single) root element has been seen already.
58    root_seen: bool,
59    /// Set after an error or a completed document: the stream is over.
60    done: bool,
61}
62
63impl<'a> XmlReader<'a> {
64    /// Starts reading a document. Nothing is consumed yet; the first
65    /// event appears on [`next_event`](Self::next_event).
66    pub fn new(input: &'a str) -> Self {
67        XmlReader {
68            input,
69            stack: Vec::new(),
70            deferred_end: None,
71            root_seen: false,
72            done: false,
73        }
74    }
75
76    /// The next token, `None` at end of document, or a named [`Error`].
77    /// Errors are sticky: every call after the first `Err` returns `None`.
78    pub fn next_event(&mut self) -> Result<Option<XmlEvent<'a>>> {
79        if self.done {
80            return Ok(None);
81        }
82        match self.step() {
83            Ok(e) => Ok(e),
84            Err(e) => {
85                self.done = true;
86                Err(e)
87            }
88        }
89    }
90
91    /// The walking state machine behind [`next_event`](Self::next_event).
92    fn step(&mut self) -> Result<Option<XmlEvent<'a>>> {
93        if let Some(name) = self.deferred_end.take() {
94            return Ok(Some(XmlEvent::End { name }));
95        }
96        if self.input.is_empty() {
97            if self.stack.is_empty() {
98                return Ok(None);
99            }
100            return Err(Error::truncated("xml document", 1, 0));
101        }
102        if let Some(rest) = self.input.strip_prefix('<') {
103            self.input = rest;
104            self.markup()
105        } else {
106            self.take_text()
107        }
108    }
109
110    /// Consumes whatever follows the `<` already stripped: a tag, a
111    /// comment/PI/doctype to skip and recurse over, or a CDATA section.
112    fn markup(&mut self) -> Result<Option<XmlEvent<'a>>> {
113        if self.input.starts_with("!--") {
114            self.input = &self.input[3..];
115            self.skip_past("-->", "xml comment")?;
116            return self.step();
117        }
118        if self.input.starts_with('?') {
119            self.input = &self.input[1..];
120            self.skip_past("?>", "xml processing instruction")?;
121            return self.step();
122        }
123        if self.input.starts_with("![CDATA[") {
124            self.input = &self.input[8..];
125            let end = self.input.find("]]>").ok_or_else(|| {
126                Error::truncated("xml cdata", self.input.len() + 3, self.input.len())
127            })?;
128            let text = self.input[..end].to_string();
129            self.input = &self.input[end + 3..];
130            return Ok(Some(XmlEvent::Text(text)));
131        }
132        if self.input.starts_with('!') {
133            // <!DOCTYPE …> and friends: skip to the `>` that closes the
134            // declaration, tracking an internal subset's […] and quotes.
135            self.input = &self.input[1..];
136            self.skip_declaration()?;
137            return self.step();
138        }
139        if self.input.starts_with('/') {
140            self.input = &self.input[1..];
141            return self.end_tag();
142        }
143        self.start_tag()
144    }
145
146    /// Consumes an end tag after `</` and checks it against the innermost
147    /// open element.
148    fn end_tag(&mut self) -> Result<Option<XmlEvent<'a>>> {
149        let name = self.scan_name("xml end tag name")?;
150        self.skip_ws();
151        match self.take_char() {
152            Some('>') => {}
153            Some(_) => return Err(Error::BadValue("xml end tag")),
154            None => return Err(Error::truncated("xml end tag", 1, 0)),
155        }
156        match self.stack.last() {
157            Some(&open) if open == name => {
158                self.stack.pop();
159                Ok(Some(XmlEvent::End { name }))
160            }
161            _ => Err(Error::BadValue("xml end tag mismatch")),
162        }
163    }
164
165    /// Consumes a start tag: name, attributes, the `>` or `/>`.
166    fn start_tag(&mut self) -> Result<Option<XmlEvent<'a>>> {
167        if self.stack.is_empty() && self.root_seen {
168            return Err(Error::BadValue("xml second root element"));
169        }
170        let name = self.scan_name("xml tag name")?;
171        let mut attrs: Vec<(&'a str, String)> = Vec::new();
172        let self_closing;
173        loop {
174            self.skip_ws();
175            match self.input.as_bytes().first() {
176                Some(b'>') => {
177                    self.input = &self.input[1..];
178                    self_closing = false;
179                    break;
180                }
181                Some(b'/') => {
182                    self.input = &self.input[1..];
183                    match self.take_char() {
184                        Some('>') => {
185                            self_closing = true;
186                            break;
187                        }
188                        Some(_) => return Err(Error::BadValue("xml self-closing tag")),
189                        None => return Err(Error::truncated("xml tag", 1, 0)),
190                    }
191                }
192                Some(_) => {
193                    let attr_name = self.scan_name("xml attribute name")?;
194                    self.skip_ws();
195                    match self.take_char() {
196                        Some('=') => {}
197                        Some(_) => return Err(Error::BadValue("xml attribute")),
198                        None => return Err(Error::truncated("xml attribute", 1, 0)),
199                    }
200                    self.skip_ws();
201                    let quote = match self.take_char() {
202                        Some('"') => '"',
203                        Some('\'') => '\'',
204                        Some(_) => return Err(Error::BadValue("xml attribute quote")),
205                        None => return Err(Error::truncated("xml attribute", 1, 0)),
206                    };
207                    let value = self.take_attr_value(quote)?;
208                    if attrs.iter().any(|(n, _)| *n == attr_name) {
209                        return Err(Error::BadValue("xml duplicate attribute"));
210                    }
211                    attrs.push((attr_name, value));
212                }
213                None => return Err(Error::truncated("xml tag", 1, 0)),
214            }
215        }
216        if self_closing {
217            self.deferred_end = Some(name);
218        } else {
219            self.stack.push(name);
220        }
221        if self.stack.len() == 1 && !self_closing {
222            self.root_seen = true;
223        }
224        if self_closing && self.stack.is_empty() {
225            // A self-closing document element is still the root.
226            self.root_seen = true;
227        }
228        Ok(Some(XmlEvent::Start { name, attrs }))
229    }
230
231    /// Consumes a run of character data up to the next markup.
232    fn take_text(&mut self) -> Result<Option<XmlEvent<'a>>> {
233        if self.stack.is_empty() {
234            // Outside the root only whitespace may appear; anything else
235            // is a malformed document, not a text node.
236            if self.input[..self.text_run_end()].trim().is_empty() {
237                let run = self.text_run_end();
238                self.input = &self.input[run..];
239                return self.step();
240            }
241            return Err(Error::BadValue("xml text outside root element"));
242        }
243        let run = self.text_run_end();
244        let chunk = &self.input[..run];
245        let mut out = String::with_capacity(chunk.len());
246        let mut rest = chunk;
247        while let Some(amp) = rest.find('&') {
248            out.push_str(&rest[..amp]);
249            rest = &rest[amp..];
250            self.decode_entity(&mut rest, &mut out)?;
251        }
252        out.push_str(rest);
253        self.input = &self.input[run..];
254        Ok(Some(XmlEvent::Text(out)))
255    }
256
257    /// How many bytes before the next markup boundary (`<`) or EOF. Text
258    /// may contain `&`, so the run stops at `<` alone.
259    fn text_run_end(&self) -> usize {
260        self.input.find('<').unwrap_or(self.input.len())
261    }
262
263    /// Consumes an attribute value after its opening quote, decoding
264    /// entities. `<` inside a value is malformed XML.
265    fn take_attr_value(&mut self, quote: char) -> Result<String> {
266        let mut out = String::new();
267        let mut rest = self.input;
268        loop {
269            let next = rest.find([quote, '<', '&']);
270            match next {
271                None => {
272                    return Err(Error::truncated(
273                        "xml attribute value",
274                        rest.len() + 1,
275                        rest.len(),
276                    ));
277                }
278                Some(i) => {
279                    out.push_str(&rest[..i]);
280                    match rest.as_bytes()[i] {
281                        b'<' => return Err(Error::BadValue("xml '<' in attribute value")),
282                        b'&' => {
283                            rest = &rest[i..];
284                            self.decode_entity(&mut rest, &mut out)?;
285                        }
286                        _ => {
287                            self.input = &rest[i + 1..];
288                            return Ok(out);
289                        }
290                    }
291                }
292            }
293        }
294    }
295
296    /// Decodes `&…;` at the head of `*rest` (which starts at the `&`)
297    /// into `out` and advances `*rest` past the `;`.
298    fn decode_entity(&self, rest: &mut &'a str, out: &mut String) -> Result<()> {
299        let semi = rest
300            .find(';')
301            .ok_or(Error::BadValue("xml unterminated entity"))?;
302        let body = &rest[1..semi];
303        if body.is_empty() || body.contains(|c: char| c.is_whitespace() || c == '<' || c == '&') {
304            return Err(Error::BadValue("xml entity"));
305        }
306        let decoded: char = match body {
307            "amp" => '&',
308            "lt" => '<',
309            "gt" => '>',
310            "quot" => '"',
311            "apos" => '\'',
312            _ if body.starts_with("#x") || body.starts_with("#X") => {
313                let digits = &body[2..];
314                let v = u32::from_str_radix(digits, 16)
315                    .map_err(|_| Error::BadValue("xml numeric character reference"))?;
316                char::from_u32(v).ok_or(Error::BadValue("xml numeric character reference"))?
317            }
318            _ if body.starts_with('#') => {
319                let digits = &body[1..];
320                let v = digits
321                    .parse::<u32>()
322                    .map_err(|_| Error::BadValue("xml numeric character reference"))?;
323                char::from_u32(v).ok_or(Error::BadValue("xml numeric character reference"))?
324            }
325            _ => return Err(Error::BadValue("xml unknown entity")),
326        };
327        out.push(decoded);
328        *rest = &rest[semi + 1..];
329        Ok(())
330    }
331
332    /// Scans an XML Name: a non-empty run of name characters, terminated
333    /// by anything that is not one.
334    fn scan_name(&mut self, what: &'static str) -> Result<&'a str> {
335        let end = self
336            .input
337            .find(|c: char| !(c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':'))
338            .unwrap_or(self.input.len());
339        if end == 0 {
340            if self.input.is_empty() {
341                return Err(Error::truncated(what, 1, 0));
342            }
343            return Err(Error::BadValue(what));
344        }
345        let (name, rest) = self.input.split_at(end);
346        self.input = rest;
347        Ok(name)
348    }
349
350    /// Skips whitespace in place.
351    fn skip_ws(&mut self) {
352        self.input = self.input.trim_start();
353    }
354
355    /// Takes the first char of the input, or `None` at EOF. Char-based,
356    /// not byte-based: slicing off a fixed byte width would panic on a
357    /// multi-byte character.
358    fn take_char(&mut self) -> Option<char> {
359        let c = self.input.chars().next()?;
360        self.input = &self.input[c.len_utf8()..];
361        Some(c)
362    }
363
364    /// Skips to just past the first `needle`, or [`Error::Truncated`].
365    fn skip_past(&mut self, needle: &str, what: &'static str) -> Result<()> {
366        match self.input.find(needle) {
367            Some(i) => {
368                self.input = &self.input[i + needle.len()..];
369                Ok(())
370            }
371            None => Err(Error::truncated(
372                what,
373                self.input.len() + needle.len(),
374                self.input.len(),
375            )),
376        }
377    }
378
379    /// Skips a `<!…>` declaration: a `<!DOCTYPE` internal subset can hide
380    /// `>` inside `[…]` or quoted strings, so the scan tracks bracket
381    /// depth and quoting rather than stopping at the first `>`.
382    fn skip_declaration(&mut self) -> Result<()> {
383        let mut depth = 0usize;
384        let mut quote: Option<char> = None;
385        for (i, c) in self.input.char_indices() {
386            match quote {
387                Some(q) => {
388                    if c == q {
389                        quote = None;
390                    }
391                }
392                None => match c {
393                    '"' | '\'' => quote = Some(c),
394                    '[' => depth += 1,
395                    ']' => depth = depth.saturating_sub(1),
396                    '>' if depth == 0 => {
397                        self.input = &self.input[i + 1..];
398                        return Ok(());
399                    }
400                    _ => {}
401                },
402            }
403        }
404        Err(Error::truncated(
405            "xml declaration",
406            self.input.len() + 1,
407            self.input.len(),
408        ))
409    }
410}
411
412impl<'a> Iterator for XmlReader<'a> {
413    type Item = Result<XmlEvent<'a>>;
414
415    fn next(&mut self) -> Option<Self::Item> {
416        self.next_event().transpose()
417    }
418}