1use std::borrow::Cow;
20
21use crate::sheet::xml;
22
23pub const DOCTYPE_REFUSED: &str =
25 "it declares a DOCTYPE or an entity, and no entity is ever expanded here";
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Event<'a> {
30 Start {
32 name: &'a str,
33 attrs: &'a str,
34 empty: bool,
35 },
36 End { name: &'a str },
38 Text { text: &'a str, raw: bool },
41}
42
43pub struct Cursor<'a> {
45 text: &'a str,
46 pos: usize,
47}
48
49fn 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 pub fn position(&self) -> usize {
66 self.pos
67 }
68
69 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 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
158pub fn attributes(attrs: &str) -> Attributes<'_> {
161 Attributes {
162 rest: attrs,
163 full: false,
164 }
165}
166
167pub 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
202pub 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
209pub 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
217pub 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
265pub 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
274pub 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
318pub 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 & 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 & _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 let mut cur = Cursor::new("<f>A1&\"_x0041_\"<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 & b < c > d "e" 'f' AB &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}