Skip to main content

frazaro_core/reflect/
cursor.rs

1//! A cursor over a part's XML (PORT.8, slice 8a): the subset sheet XML
2//! needs and no more.
3//!
4//! New ground, said plainly: the reference reads a workbook through Excel's
5//! object model and never parses a part. What is here is a forward scan over
6//! a part's text that yields start tags, end tags and the text between them,
7//! with comments and processing instructions skipped, a CDATA section read
8//! as text, entity references undone, and element and attribute names
9//! matched by their local part, so that a writer that prefixes the main
10//! namespace reads the same. It builds no tree: the walkers in `ooxml.rs`
11//! take the events as they come and hold only what a relation row needs,
12//! which is what lets a sheet part be walked once and dropped.
13//!
14//! Refused outright: a `<!DOCTYPE` or `<!ENTITY` declaration anywhere, since
15//! no entity is ever expanded here (docs/THREAT_MODEL.md, section 5), and a
16//! tag or section that never closes. Everything the cursor does not
17//! understand is passed over unread.
18
19use std::borrow::Cow;
20
21use crate::sheet::xml;
22
23/// Why a declaration is refused, in the words the catalogue's `{why}` takes.
24pub const DOCTYPE_REFUSED: &str =
25    "it declares a DOCTYPE or an entity, and no entity is ever expanded here";
26
27/// One event of the scan. Names are local names, without their prefix.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Event<'a> {
30    /// `<name attrs>` or, with `empty`, `<name attrs/>`.
31    Start {
32        name: &'a str,
33        attrs: &'a str,
34        empty: bool,
35    },
36    /// `</name>`.
37    End { name: &'a str },
38    /// Character data between tags, as written; `raw` for a CDATA section,
39    /// in which nothing is an entity reference.
40    Text { text: &'a str, raw: bool },
41}
42
43/// The scan's position in a part's text.
44pub struct Cursor<'a> {
45    text: &'a str,
46    pos: usize,
47}
48
49/// The name after its prefix: `c` for `x:c`.
50fn local(name: &str) -> &str {
51    match name.rfind(':') {
52        Some(i) => &name[i + 1..],
53        None => name,
54    }
55}
56
57impl<'a> Cursor<'a> {
58    pub fn new(text: &'a str) -> Cursor<'a> {
59        Cursor { text, pos: 0 }
60    }
61
62    /// Where the scan stands: the offset just past the last event, so that
63    /// a reader can remember where an element began (the offset before the
64    /// call that returned its start tag) and where it ended.
65    pub fn position(&self) -> usize {
66        self.pos
67    }
68
69    /// The next event, or `None` at the end of the text. An error names
70    /// what the markup does that this reader refuses.
71    pub fn next_event(&mut self) -> Result<Option<Event<'a>>, &'static str> {
72        let text: &'a str = self.text;
73        loop {
74            let rest = &text[self.pos..];
75            if rest.is_empty() {
76                return Ok(None);
77            }
78            if !rest.starts_with('<') {
79                let end = rest.find('<').unwrap_or(rest.len());
80                self.pos += end;
81                return Ok(Some(Event::Text {
82                    text: &rest[..end],
83                    raw: false,
84                }));
85            }
86            if let Some(after) = rest.strip_prefix("<!--") {
87                let end = after.find("-->").ok_or("a comment never closes")?;
88                self.pos += 4 + end + 3;
89                continue;
90            }
91            if let Some(after) = rest.strip_prefix("<![CDATA[") {
92                let end = after.find("]]>").ok_or("a CDATA section never closes")?;
93                self.pos += 9 + end + 3;
94                return Ok(Some(Event::Text {
95                    text: &after[..end],
96                    raw: true,
97                }));
98            }
99            if rest.starts_with("<!") {
100                return Err(DOCTYPE_REFUSED);
101            }
102            if let Some(after) = rest.strip_prefix("<?") {
103                let end = after
104                    .find("?>")
105                    .ok_or("a processing instruction never closes")?;
106                self.pos += 2 + end + 2;
107                continue;
108            }
109            if let Some(after) = rest.strip_prefix("</") {
110                let end = after.find('>').ok_or("an end tag never closes")?;
111                self.pos += 2 + end + 1;
112                return Ok(Some(Event::End {
113                    name: local(after[..end].trim_end()),
114                }));
115            }
116            // A start tag: the name, then the attributes to the `>` that
117            // is outside every quoted value.
118            let after = &rest[1..];
119            let name_end = after
120                .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
121                .ok_or("a tag never closes")?;
122            if name_end == 0 {
123                return Err("a tag has no name");
124            }
125            let bytes = after.as_bytes();
126            let mut quote: Option<u8> = None;
127            let mut j = name_end;
128            let close = loop {
129                let Some(&b) = bytes.get(j) else {
130                    return Err("a tag never closes");
131                };
132                match quote {
133                    Some(q) => {
134                        if b == q {
135                            quote = None;
136                        }
137                    }
138                    None => match b {
139                        b'"' | b'\'' => quote = Some(b),
140                        b'>' => break j,
141                        _ => {}
142                    },
143                }
144                j += 1;
145            };
146            let empty = close > name_end && bytes[close - 1] == b'/';
147            let attrs_end = if empty { close - 1 } else { close };
148            self.pos += 1 + close + 1;
149            return Ok(Some(Event::Start {
150                name: local(&after[..name_end]),
151                attrs: &after[name_end..attrs_end],
152                empty,
153            }));
154        }
155    }
156}
157
158/// The attributes of a start tag, each as its local name and its value as
159/// written, entities not yet undone.
160pub fn attributes(attrs: &str) -> Attributes<'_> {
161    Attributes {
162        rest: attrs,
163        full: false,
164    }
165}
166
167/// The attributes of a start tag with their names as written, prefix and
168/// all, for the one case two namespaces share a local name (OpenDocument's
169/// `office:value-type` beside `calcext:value-type`).
170pub fn attributes_full(attrs: &str) -> Attributes<'_> {
171    Attributes {
172        rest: attrs,
173        full: true,
174    }
175}
176
177pub struct Attributes<'a> {
178    rest: &'a str,
179    full: bool,
180}
181
182impl<'a> Iterator for Attributes<'a> {
183    type Item = (&'a str, &'a str);
184
185    fn next(&mut self) -> Option<Self::Item> {
186        let s = self.rest.trim_start();
187        let eq = s.find('=')?;
188        let name = s[..eq].trim_end();
189        let after = s[eq + 1..].trim_start();
190        let q = *after.as_bytes().first()?;
191        if q != b'"' && q != b'\'' {
192            self.rest = "";
193            return None;
194        }
195        let end = after[1..].find(q as char)?;
196        let value = &after[1..1 + end];
197        self.rest = &after[1 + end + 1..];
198        Some((if self.full { name } else { local(name) }, value))
199    }
200}
201
202/// An attribute's value by its local name, entities undone.
203pub fn attr<'a>(attrs: &'a str, name: &str) -> Option<Cow<'a, str>> {
204    attributes(attrs)
205        .find(|(n, _)| *n == name)
206        .map(|(_, v)| unescape_entities(v))
207}
208
209/// An attribute's value by its name as written, prefix and all, entities
210/// undone.
211pub fn attr_full<'a>(attrs: &'a str, name: &str) -> Option<Cow<'a, str>> {
212    attributes_full(attrs)
213        .find(|(n, _)| *n == name)
214        .map(|(_, v)| unescape_entities(v))
215}
216
217/// Entity references undone: the five named ones and numeric character
218/// references; a reference that does not parse is kept as written. No
219/// allocation when there is none.
220pub fn unescape_entities(s: &str) -> Cow<'_, str> {
221    if !s.contains('&') {
222        return Cow::Borrowed(s);
223    }
224    let mut out = String::with_capacity(s.len());
225    let mut rest = s;
226    while let Some(i) = rest.find('&') {
227        out.push_str(&rest[..i]);
228        let tail = &rest[i..];
229        let decoded = match tail.find(';') {
230            Some(end) if end <= 12 => {
231                let entity = &tail[1..end];
232                let c = match entity {
233                    "amp" => Some('&'),
234                    "lt" => Some('<'),
235                    "gt" => Some('>'),
236                    "quot" => Some('"'),
237                    "apos" => Some('\''),
238                    e => e
239                        .strip_prefix('#')
240                        .and_then(|n| match n.strip_prefix('x') {
241                            Some(h) => u32::from_str_radix(h, 16).ok(),
242                            None => n.parse::<u32>().ok(),
243                        })
244                        .and_then(char::from_u32),
245                };
246                c.map(|c| (c, end + 1))
247            }
248            _ => None,
249        };
250        match decoded {
251            Some((c, len)) => {
252                out.push(c);
253                rest = &tail[len..];
254            }
255            None => {
256                out.push('&');
257                rest = &tail[1..];
258            }
259        }
260    }
261    out.push_str(rest);
262    Cow::Owned(out)
263}
264
265/// A string's or a cell's text: entities and Excel's `_xHHHH_` spelling
266/// undone (`sheet::xml::unescape`); no allocation when there is neither.
267pub fn unescape_text(s: &str) -> Cow<'_, str> {
268    if !s.contains('&') && !s.contains("_x") {
269        return Cow::Borrowed(s);
270    }
271    Cow::Owned(xml::unescape(s))
272}
273
274/// The text of the element a start event (not an empty one) just opened,
275/// to its end tag: nested elements' text included, except under an element
276/// named in `skip`; entities undone, and Excel's `_xHHHH_` too when
277/// `excel_escapes` says the text is a string of the kind Excel spells that
278/// way (a shared or inline string), not a formula or a value.
279pub fn element_text(
280    cursor: &mut Cursor,
281    skip: &[&str],
282    excel_escapes: bool,
283) -> Result<String, &'static str> {
284    let mut out = String::new();
285    let mut depth = 0u32;
286    loop {
287        match cursor.next_event()? {
288            None => return Err("an element never closes"),
289            Some(Event::Start { name, empty, .. }) => {
290                if empty {
291                    continue;
292                }
293                if skip.contains(&name) {
294                    skip_element(cursor)?;
295                } else {
296                    depth += 1;
297                }
298            }
299            Some(Event::End { .. }) => {
300                if depth == 0 {
301                    return Ok(out);
302                }
303                depth -= 1;
304            }
305            Some(Event::Text { text, raw }) => {
306                if raw {
307                    out.push_str(text);
308                } else if excel_escapes {
309                    out.push_str(&unescape_text(text));
310                } else {
311                    out.push_str(&unescape_entities(text));
312                }
313            }
314        }
315    }
316}
317
318/// Past the element a start event (not an empty one) just opened, to its
319/// end tag, whatever is nested inside.
320pub fn skip_element(cursor: &mut Cursor) -> Result<(), &'static str> {
321    let mut depth = 1u32;
322    loop {
323        match cursor.next_event()? {
324            None => return Err("an element never closes"),
325            Some(Event::Start { empty: false, .. }) => depth += 1,
326            Some(Event::End { .. }) => {
327                depth -= 1;
328                if depth == 0 {
329                    return Ok(());
330                }
331            }
332            _ => {}
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    fn events(text: &str) -> Vec<String> {
342        let mut cur = Cursor::new(text);
343        let mut out = Vec::new();
344        while let Some(ev) = cur.next_event().unwrap() {
345            out.push(match ev {
346                Event::Start { name, attrs, empty } => {
347                    format!("<{name}|{attrs}|{}>", if empty { "/" } else { "" })
348                }
349                Event::End { name } => format!("</{name}>"),
350                Event::Text { text, raw } => format!("{}{text}", if raw { "!" } else { "" }),
351            });
352        }
353        out
354    }
355
356    #[test]
357    fn the_events_of_a_part_come_in_order_with_local_names() {
358        assert_eq!(
359            events(
360                "<?xml version=\"1.0\"?>\n<x:worksheet xmlns:x=\"u\"><!-- c --><sheetData><row r=\"1\"><c r=\"A1\" t=\"s\"><v>0</v></c><c r='B1'/></row></sheetData></x:worksheet>"
361            ),
362            [
363                "\n",
364                "<worksheet| xmlns:x=\"u\"|>",
365                "<sheetData||>",
366                "<row| r=\"1\"|>",
367                "<c| r=\"A1\" t=\"s\"|>",
368                "<v||>",
369                "0",
370                "</v>",
371                "</c>",
372                "<c| r='B1'|/>",
373                "</row>",
374                "</sheetData>",
375                "</worksheet>",
376            ]
377        );
378        assert_eq!(
379            events("<a b=\"x>y\"><![CDATA[1 < 2]]></a>"),
380            ["<a| b=\"x>y\"|>", "!1 < 2", "</a>"]
381        );
382    }
383
384    #[test]
385    fn attributes_read_by_local_name_in_either_quote() {
386        let attrs = " name=\"Q1 &amp; Q2\" sheetId='3' r:id=\"rId7\" state=\"hidden\"";
387        assert_eq!(attr(attrs, "name").as_deref(), Some("Q1 & Q2"));
388        assert_eq!(attr(attrs, "sheetId").as_deref(), Some("3"));
389        assert_eq!(attr(attrs, "id").as_deref(), Some("rId7"));
390        assert_eq!(attr(attrs, "state").as_deref(), Some("hidden"));
391        assert_eq!(attr(attrs, "missing"), None);
392        assert_eq!(attributes(attrs).count(), 4);
393        assert_eq!(attributes("").count(), 0);
394    }
395
396    #[test]
397    fn what_is_refused_or_malformed_is_named() {
398        let mut cur = Cursor::new("<!DOCTYPE x [<!ENTITY a \"b\">]><a/>");
399        assert_eq!(cur.next_event(), Err(DOCTYPE_REFUSED));
400        let mut cur = Cursor::new("<a><b attr=\"1\"");
401        assert_eq!(
402            cur.next_event(),
403            Ok(Some(Event::Start {
404                name: "a",
405                attrs: "",
406                empty: false
407            }))
408        );
409        assert_eq!(cur.next_event(), Err("a tag never closes"));
410        let mut cur = Cursor::new("<!-- open");
411        assert_eq!(cur.next_event(), Err("a comment never closes"));
412        let mut cur = Cursor::new("<a>");
413        cur.next_event().unwrap();
414        assert_eq!(
415            element_text(&mut cur, &[], false),
416            Err("an element never closes")
417        );
418    }
419
420    #[test]
421    fn element_text_gathers_runs_and_skips_phonetics() {
422        let text = "<si><r><rPr><b/></rPr><t>Rate </t></r><r><t xml:space=\"preserve\">applies &amp; _x000D_</t></r><rPh sb=\"0\" eb=\"1\"><t>ignored</t></rPh><phoneticPr fontId=\"1\"/></si>";
423        let mut cur = Cursor::new(text);
424        cur.next_event().unwrap();
425        assert_eq!(
426            element_text(&mut cur, &["rPh"], true).unwrap(),
427            "Rate applies & \r"
428        );
429        assert_eq!(cur.next_event(), Ok(None));
430        // A formula's text undoes entities and nothing else.
431        let mut cur = Cursor::new("<f>A1&amp;\"_x0041_\"&lt;B1</f>");
432        cur.next_event().unwrap();
433        assert_eq!(
434            element_text(&mut cur, &[], false).unwrap(),
435            "A1&\"_x0041_\"<B1"
436        );
437    }
438
439    #[test]
440    fn entities_undone_and_the_rest_kept() {
441        assert_eq!(unescape_entities("plain"), "plain");
442        assert!(matches!(unescape_entities("plain"), Cow::Borrowed(_)));
443        assert_eq!(
444            unescape_entities(
445                "a &amp; b &lt; c &gt; d &quot;e&quot; &apos;f&apos; &#65;&#x42; &unknown; & g"
446            ),
447            "a & b < c > d \"e\" 'f' AB &unknown; & g"
448        );
449        assert_eq!(unescape_text("x_x000A_y"), "x\ny");
450        assert!(matches!(unescape_text("plain"), Cow::Borrowed(_)));
451    }
452
453    #[test]
454    fn skipping_an_element_passes_what_is_nested() {
455        let mut cur = Cursor::new("<a><b><c/><d>t</d></b><e/></a>");
456        assert!(matches!(
457            cur.next_event(),
458            Ok(Some(Event::Start { name: "a", .. }))
459        ));
460        assert!(matches!(
461            cur.next_event(),
462            Ok(Some(Event::Start { name: "b", .. }))
463        ));
464        skip_element(&mut cur).unwrap();
465        assert!(matches!(
466            cur.next_event(),
467            Ok(Some(Event::Start {
468                name: "e",
469                empty: true,
470                ..
471            }))
472        ));
473    }
474}