roo-oxide 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,944 @@
1
+ //! Streaming reader for worksheet XML that yields the raw cell records roo
2
+ //! builds its cells from.
3
+ //!
4
+ //! roo reads a worksheet in two ways, and each one selects rows and cells
5
+ //! differently:
6
+ //!
7
+ //! * [`Mode::Document`] mirrors `Roo::Excelx::SheetDoc#extract_cells`: the
8
+ //! XPath `/worksheet/sheetData/row` and then `c` children, on a document
9
+ //! whose namespaces were removed, so names compare by local name.
10
+ //! * [`Mode::Stream`] mirrors `Roo::Utils.each_element(path, "row")` plus
11
+ //! `SheetDoc#each_cell`: every element whose qualified name is `row`, at any
12
+ //! depth, and every element child of that row.
13
+ //!
14
+ //! Inside a cell both modes follow `SheetDoc#cell_from_xml`: the first `v`
15
+ //! child, or the first `is` child with non-empty text, decides the value, and
16
+ //! an `f` child seen before it is the formula.
17
+
18
+ mod attributes;
19
+
20
+ use std::fmt;
21
+ use std::fs::File;
22
+ use std::io::{BufRead, BufReader};
23
+ use std::ops::Range;
24
+ use std::path::Path;
25
+
26
+ use quick_xml::events::{BytesRef, BytesStart, Event};
27
+
28
+ use attributes::TagAttributes;
29
+
30
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
31
+ pub enum Mode {
32
+ Document,
33
+ Stream,
34
+ }
35
+
36
+ /// The `r` attribute.
37
+ #[derive(Clone, Debug, PartialEq, Eq)]
38
+ pub enum Reference {
39
+ Missing,
40
+ /// What `Roo::Utils.extract_coordinate` returns for it.
41
+ At {
42
+ row: i64,
43
+ column: i64,
44
+ },
45
+ /// A value `Roo::Utils.extract_coordinate` raises for.
46
+ Invalid(String),
47
+ }
48
+
49
+ /// The `s` attribute.
50
+ #[derive(Clone, Debug, PartialEq, Eq)]
51
+ pub enum Style {
52
+ /// Absent (index 0) or plain decimal digits.
53
+ Index(i64),
54
+ /// Anything else, for Ruby's `String#to_i` to read.
55
+ Other(String),
56
+ }
57
+
58
+ /// The `t` attribute, as far as `SheetDoc#cell_value_type` tells types apart.
59
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
60
+ pub enum Kind {
61
+ /// Absent or any other value: the style's number format decides.
62
+ ByFormat,
63
+ Shared,
64
+ Boolean,
65
+ Str,
66
+ InlineStr,
67
+ }
68
+
69
+ /// A byte range of [`RawRow::text`].
70
+ pub type Span = Range<usize>;
71
+
72
+ /// The value-bearing part of a cell, as `cell_from_xml` sees it.
73
+ #[derive(Clone, Debug, PartialEq, Eq)]
74
+ pub enum Body {
75
+ /// No `v` child and no `is` child with text.
76
+ Empty,
77
+ /// Text of every `t` element inside the first non-empty `is` child.
78
+ Inline(Span),
79
+ /// Text content of the first `v` child.
80
+ Value(Span),
81
+ }
82
+
83
+ #[derive(Clone, Debug, PartialEq, Eq)]
84
+ pub struct RawCell {
85
+ pub reference: Reference,
86
+ pub style: Style,
87
+ pub kind: Kind,
88
+ pub formula: Option<Span>,
89
+ pub body: Body,
90
+ }
91
+
92
+ impl RawCell {
93
+ /// The `[row, column]` key of roo's document-mode `cells` hash for this
94
+ /// cell, the `index`-th of the `row_number`-th row (both 1-based), or
95
+ /// `None` when roo raises for its `r`.
96
+ pub fn position(&self, row_number: usize, index: usize) -> Option<(i64, i64)> {
97
+ match self.reference {
98
+ Reference::At { row, column } => Some((row, column)),
99
+ Reference::Missing => Some((row_number as i64, index as i64)),
100
+ Reference::Invalid(_) => None,
101
+ }
102
+ }
103
+ }
104
+
105
+ /// A row's cells. Their text lives in one buffer, so a row costs a few
106
+ /// allocations rather than a few per cell.
107
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
108
+ pub struct RawRow {
109
+ pub text: String,
110
+ pub cells: Vec<RawCell>,
111
+ }
112
+
113
+ impl RawRow {
114
+ pub fn text(&self, span: &Span) -> &str {
115
+ &self.text[span.clone()]
116
+ }
117
+ }
118
+
119
+ /// The input uses XML that this reader does not reproduce exactly the way
120
+ /// Nokogiri does, so the caller falls back to roo's own parser.
121
+ #[derive(Debug, PartialEq, Eq)]
122
+ pub struct Unsupported(pub String);
123
+
124
+ impl fmt::Display for Unsupported {
125
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126
+ f.write_str(&self.0)
127
+ }
128
+ }
129
+
130
+ impl std::error::Error for Unsupported {}
131
+
132
+ impl From<quick_xml::Error> for Unsupported {
133
+ fn from(error: quick_xml::Error) -> Self {
134
+ Unsupported(error.to_string())
135
+ }
136
+ }
137
+
138
+ impl From<quick_xml::events::attributes::AttrError> for Unsupported {
139
+ fn from(error: quick_xml::events::attributes::AttrError) -> Self {
140
+ Unsupported(error.to_string())
141
+ }
142
+ }
143
+
144
+ type Result<T> = std::result::Result<T, Unsupported>;
145
+
146
+ /// `Roo::Utils.extract_coordinate` for well-formed references. `None` means
147
+ /// roo raises.
148
+ pub fn parse_reference(reference: &str) -> Option<(i64, i64)> {
149
+ let (mut column, mut row) = (0i64, 0i64);
150
+ let mut digits = false;
151
+ for byte in reference.bytes() {
152
+ match byte {
153
+ b'A'..=b'Z' | b'a'..=b'z' if !digits => {
154
+ column = column
155
+ .checked_mul(26)?
156
+ .checked_add(i64::from(byte & 0x1f))?;
157
+ }
158
+ b'0'..=b'9' => {
159
+ digits = true;
160
+ row = row.checked_mul(10)?.checked_add(i64::from(byte - b'0'))?;
161
+ }
162
+ _ => return None,
163
+ }
164
+ }
165
+ (column != 0 && digits).then_some((row, column))
166
+ }
167
+
168
+ /// `String#to_i` for plain decimal digits; `None` leaves it to Ruby.
169
+ pub fn parse_index(text: &str) -> Option<usize> {
170
+ if text.is_empty() || text.len() > 18 || !text.bytes().all(|b| b.is_ascii_digit()) {
171
+ return None;
172
+ }
173
+ text.parse().ok()
174
+ }
175
+
176
+ pub struct SheetReader<R: BufRead> {
177
+ xml: quick_xml::Reader<R>,
178
+ mode: Mode,
179
+ buf: Vec<u8>,
180
+ depth: usize,
181
+ /// Document mode: depth of the open `sheetData` element under the
182
+ /// `worksheet` root, if any.
183
+ sheet_data_depth: Option<usize>,
184
+ root_is_worksheet: bool,
185
+ done: bool,
186
+ /// Whether any event was read yet; an XML declaration must come first.
187
+ started: bool,
188
+ seen_root: bool,
189
+ /// Depth of the open elements, for namespace scopes.
190
+ open_elements: usize,
191
+ /// Namespace prefixes declared on open elements, with the depth they were
192
+ /// declared at.
193
+ prefixes: Vec<(usize, String)>,
194
+ /// `xml:space` values on open elements (`true` for `preserve`), with the
195
+ /// depth they were set at.
196
+ space: Vec<(usize, bool)>,
197
+ /// Stream mode: whether a row is being read.
198
+ in_row: bool,
199
+ /// The attributes of the last start tag read.
200
+ attributes: TagAttributes,
201
+ /// The text length and cell count of the last row read, which the next
202
+ /// row likely has too.
203
+ row_size: (usize, usize),
204
+ /// An error met after some rows of a batch, returned by the next call.
205
+ pending_error: Option<Unsupported>,
206
+ }
207
+
208
+ impl SheetReader<BufReader<File>> {
209
+ pub fn open(path: &Path, mode: Mode) -> Result<Self> {
210
+ let file = File::open(path).map_err(|e| Unsupported(e.to_string()))?;
211
+ Ok(Self::new(BufReader::with_capacity(64 * 1024, file), mode))
212
+ }
213
+ }
214
+
215
+ #[derive(Clone, Copy)]
216
+ enum Child {
217
+ Is,
218
+ F,
219
+ V,
220
+ Other,
221
+ }
222
+
223
+ fn child(e: &BytesStart<'_>) -> Child {
224
+ match e.local_name().as_ref() {
225
+ "is" => Child::Is,
226
+ "f" => Child::F,
227
+ "v" => Child::V,
228
+ _ => Child::Other,
229
+ }
230
+ }
231
+
232
+ impl<R: BufRead> SheetReader<R> {
233
+ pub fn new(input: R, mode: Mode) -> Self {
234
+ let mut xml = quick_xml::Reader::from_reader(input);
235
+ let config = xml.config_mut();
236
+ config.check_end_names = true;
237
+ config.check_comments = true;
238
+ Self {
239
+ xml,
240
+ mode,
241
+ buf: Vec::with_capacity(16 * 1024),
242
+ depth: 0,
243
+ sheet_data_depth: None,
244
+ root_is_worksheet: false,
245
+ done: false,
246
+ started: false,
247
+ seen_root: false,
248
+ open_elements: 0,
249
+ prefixes: Vec::new(),
250
+ space: Vec::new(),
251
+ in_row: false,
252
+ attributes: TagAttributes::default(),
253
+ row_size: (0, 0),
254
+ pending_error: None,
255
+ }
256
+ }
257
+
258
+ /// Reads rows until they hold about `cells` cells (an empty row counts as
259
+ /// one). An empty vector means the sheet is done. When an error stops a
260
+ /// batch, the rows before it are returned first and the error on the next
261
+ /// call.
262
+ pub fn next_rows(&mut self, cells: usize) -> Result<Vec<RawRow>> {
263
+ if let Some(error) = self.pending_error.take() {
264
+ return Err(error);
265
+ }
266
+ let mut rows = Vec::new();
267
+ let mut read = 0;
268
+ while read < cells {
269
+ match self.next_row() {
270
+ Ok(Some(row)) => {
271
+ read += row.cells.len().max(1);
272
+ rows.push(row);
273
+ }
274
+ Ok(None) => break,
275
+ Err(error) if rows.is_empty() => return Err(error),
276
+ Err(error) => {
277
+ self.pending_error = Some(error);
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ Ok(rows)
283
+ }
284
+
285
+ pub fn next_row(&mut self) -> Result<Option<RawRow>> {
286
+ let mut buf = std::mem::take(&mut self.buf);
287
+ let row = self.find_row(&mut buf);
288
+ self.buf = buf;
289
+ row
290
+ }
291
+
292
+ /// Reads a validated XML event without building worksheet rows.
293
+ pub(crate) fn next_xml_event<'b>(&mut self, buf: &'b mut Vec<u8>) -> Result<Event<'b>> {
294
+ let depth = self.open_elements;
295
+ let event = self.read_event(buf)?;
296
+ if depth == 0 {
297
+ self.check_top_level(&event)?;
298
+ }
299
+ match &event {
300
+ Event::Decl(decl) => check_encoding(decl)?,
301
+ Event::Eof if depth != 0 => {
302
+ return Err(Unsupported("unexpected end of the document".into()));
303
+ }
304
+ _ => {}
305
+ }
306
+ Ok(event)
307
+ }
308
+
309
+ fn find_row(&mut self, buf: &mut Vec<u8>) -> Result<Option<RawRow>> {
310
+ if self.done {
311
+ return Ok(None);
312
+ }
313
+ loop {
314
+ buf.clear();
315
+ let event = self.read_event(buf)?;
316
+ if self.depth == 0 {
317
+ self.check_top_level(&event)?;
318
+ }
319
+ match event {
320
+ Event::Start(e) => {
321
+ self.depth += 1;
322
+ if self.is_row(&e) {
323
+ self.in_row = true;
324
+ let row = self.read_row(buf);
325
+ self.in_row = false;
326
+ self.depth -= 1;
327
+ return row.map(Some);
328
+ }
329
+ self.enter(&e);
330
+ }
331
+ Event::Empty(e) => {
332
+ self.depth += 1;
333
+ let is_row = self.is_row(&e);
334
+ self.depth -= 1;
335
+ if is_row {
336
+ return Ok(Some(RawRow::default()));
337
+ }
338
+ }
339
+ Event::End(_) => {
340
+ if self.sheet_data_depth == Some(self.depth) {
341
+ self.sheet_data_depth = None;
342
+ }
343
+ self.depth -= 1;
344
+ }
345
+ Event::Decl(decl) => check_encoding(&decl)?,
346
+ Event::Eof if self.depth != 0 => {
347
+ return Err(Unsupported("unexpected end of the sheet".into()));
348
+ }
349
+ Event::Eof => {
350
+ self.done = true;
351
+ return Ok(None);
352
+ }
353
+ _ => {}
354
+ }
355
+ }
356
+ }
357
+
358
+ /// An element name and its attributes, as far as quick-xml leaves them
359
+ /// unchecked: names and their namespace prefixes, the attribute list, and
360
+ /// the references in values.
361
+ fn check_start(&mut self, e: &BytesStart<'_>) -> Result<()> {
362
+ let name = e.name();
363
+ check_name(name.as_ref())?;
364
+ let raw = e.attributes_raw();
365
+ if self.mode == Mode::Stream && self.in_row {
366
+ // roo's streaming reader yields a nested `row` as a row of its own,
367
+ // and matches prefixed names in a row by namespace.
368
+ if name.as_ref() == "row" || name.as_ref().contains(':') {
369
+ return Err(Unsupported(format!(
370
+ "<{}> inside a streamed row",
371
+ name.as_ref()
372
+ )));
373
+ }
374
+ }
375
+ self.attributes.read(raw)?;
376
+ let prefixed = self.attributes.prefixed();
377
+ if !prefixed && !self.attributes.references() {
378
+ return self.check_prefix(name.as_ref());
379
+ }
380
+ let depth = self.open_elements + 1;
381
+ for attribute in self.attributes.iter(raw) {
382
+ let key = attribute.key;
383
+ if attribute.value.contains('&') {
384
+ check_chars(&attribute.normalized_value()?)?;
385
+ }
386
+ if let Some(prefix) = key.strip_prefix("xmlns:") {
387
+ if attribute.value.is_empty() {
388
+ return Err(Unsupported(format!("an empty namespace for {prefix}")));
389
+ }
390
+ self.prefixes.push((depth, prefix.to_owned()));
391
+ }
392
+ if key == "xml:space" {
393
+ self.space.push((depth, attribute.value == "preserve"));
394
+ }
395
+ }
396
+ // A prefix may be declared after the attribute that uses it.
397
+ if prefixed {
398
+ for attribute in self.attributes.iter(raw) {
399
+ self.check_prefix(attribute.key)?;
400
+ }
401
+ }
402
+ self.check_prefix(name.as_ref())
403
+ }
404
+
405
+ /// Ends the namespace and `xml:space` scopes of an element at `depth`.
406
+ fn leave(&mut self, depth: usize) {
407
+ while self.prefixes.last().is_some_and(|(at, _)| *at == depth) {
408
+ self.prefixes.pop();
409
+ }
410
+ while self.space.last().is_some_and(|(at, _)| *at == depth) {
411
+ self.space.pop();
412
+ }
413
+ }
414
+
415
+ fn preserves_space(&self) -> bool {
416
+ self.space.last().is_some_and(|(_, preserve)| *preserve)
417
+ }
418
+
419
+ /// libxml2 keeps an undeclared prefix as part of the name, so roo would
420
+ /// not see `x:row` as `row`.
421
+ fn check_prefix(&self, name: &str) -> Result<()> {
422
+ match name.split_once(':') {
423
+ Some(("xml" | "xmlns", _)) | None => Ok(()),
424
+ Some((prefix, _)) if self.prefixes.iter().any(|(_, declared)| declared == prefix) => {
425
+ Ok(())
426
+ }
427
+ Some(_) => Err(Unsupported(format!("the undeclared prefix in {name}"))),
428
+ }
429
+ }
430
+
431
+ /// Outside the root element, XML allows only one root, whitespace,
432
+ /// comments, and processing instructions.
433
+ fn check_top_level(&mut self, event: &Event<'_>) -> Result<()> {
434
+ match event {
435
+ Event::Start(_) | Event::Empty(_) if self.seen_root => {
436
+ Err(Unsupported("a second root element".into()))
437
+ }
438
+ Event::Start(_) | Event::Empty(_) => {
439
+ self.seen_root = true;
440
+ Ok(())
441
+ }
442
+ Event::Text(t) if !t.trim_ascii().is_empty() => {
443
+ Err(Unsupported("text outside the root element".into()))
444
+ }
445
+ Event::CData(_) | Event::GeneralRef(_) => {
446
+ Err(Unsupported("text outside the root element".into()))
447
+ }
448
+ Event::Eof if !self.seen_root => Err(Unsupported("no root element".into())),
449
+ _ => Ok(()),
450
+ }
451
+ }
452
+
453
+ /// The next event, rejecting characters XML does not allow, which
454
+ /// Nokogiri does not accept and quick-xml does not check.
455
+ fn read_event<'b>(&mut self, buf: &'b mut Vec<u8>) -> Result<Event<'b>> {
456
+ let event = self.xml.read_event_into(buf)?;
457
+ let first = !std::mem::replace(&mut self.started, true);
458
+ match &event {
459
+ Event::Start(e) => {
460
+ self.check_start(e)?;
461
+ self.open_elements += 1;
462
+ }
463
+ Event::Empty(e) => {
464
+ self.check_start(e)?;
465
+ self.leave(self.open_elements + 1);
466
+ }
467
+ Event::End(_) => {
468
+ self.leave(self.open_elements);
469
+ self.open_elements = self.open_elements.saturating_sub(1);
470
+ }
471
+ Event::Text(t) if t.contains("]]>") => {
472
+ return Err(Unsupported("`]]>` in text".into()));
473
+ }
474
+ Event::Text(t) | Event::Comment(t) => check_chars(t)?,
475
+ Event::CData(t) => check_chars(&t.xml10_content())?,
476
+ Event::PI(pi) => {
477
+ check_name(pi.target())?;
478
+ check_chars(pi)?;
479
+ }
480
+ Event::GeneralRef(r) => match r.resolve_char_ref()? {
481
+ Some(c) => check_chars(c.encode_utf8(&mut [0; 4]))?,
482
+ None => {
483
+ if !matches!(
484
+ r.xml10_content().as_ref(),
485
+ "lt" | "gt" | "amp" | "apos" | "quot"
486
+ ) {
487
+ return Err(Unsupported(
488
+ "an entity reference XML does not define".into(),
489
+ ));
490
+ }
491
+ }
492
+ },
493
+ Event::Decl(_) if !first => {
494
+ return Err(Unsupported("an XML declaration after the start".into()));
495
+ }
496
+ // A DTD can define entities and attribute defaults, which Nokogiri
497
+ // applies and this reader does not.
498
+ Event::DocType(_) => return Err(Unsupported("a document type declaration".into())),
499
+ _ => {}
500
+ }
501
+ Ok(event)
502
+ }
503
+
504
+ /// Tracks the `/worksheet/sheetData` path for document mode. `self.depth`
505
+ /// is already the depth of `e`.
506
+ fn enter(&mut self, e: &BytesStart<'_>) {
507
+ if self.mode != Mode::Document {
508
+ return;
509
+ }
510
+ let name = e.local_name();
511
+ if self.depth == 1 {
512
+ self.root_is_worksheet = name.as_ref() == "worksheet";
513
+ } else if self.depth == 2 && self.root_is_worksheet && name.as_ref() == "sheetData" {
514
+ self.sheet_data_depth = Some(2);
515
+ }
516
+ }
517
+
518
+ /// `self.depth` is already the depth of `e`.
519
+ fn is_row(&self, e: &BytesStart<'_>) -> bool {
520
+ match self.mode {
521
+ Mode::Document => {
522
+ self.sheet_data_depth == Some(self.depth - 1) && e.local_name().as_ref() == "row"
523
+ }
524
+ Mode::Stream => e.name().as_ref() == "row",
525
+ }
526
+ }
527
+
528
+ fn is_cell(&self, e: &BytesStart<'_>) -> bool {
529
+ match self.mode {
530
+ Mode::Document => e.local_name().as_ref() == "c",
531
+ Mode::Stream => true,
532
+ }
533
+ }
534
+
535
+ /// Reads the children of an open `row` up to its end tag.
536
+ fn read_row(&mut self, buf: &mut Vec<u8>) -> Result<RawRow> {
537
+ let (text, cells) = self.row_size;
538
+ let mut row = RawRow {
539
+ text: String::with_capacity(text),
540
+ cells: Vec::with_capacity(cells),
541
+ };
542
+ // quick-xml expects markup after a text event; bypass it only after tags.
543
+ // https://docs.rs/quick-xml/0.42.0/src/quick_xml/reader/mod.rs.html#350-392
544
+ let mut after_tag = true;
545
+ loop {
546
+ if after_tag && self.read_plain_cell(&mut row)? {
547
+ continue;
548
+ }
549
+ buf.clear();
550
+ after_tag = true;
551
+ match self.read_event(buf)? {
552
+ Event::Start(e) => {
553
+ if self.is_cell(&e) {
554
+ let cell =
555
+ Self::cell_attributes(&self.attributes, e.attributes_raw(), self.mode)?;
556
+ self.read_cell(buf, &mut row, cell)?;
557
+ } else {
558
+ self.skip(buf)?;
559
+ }
560
+ }
561
+ Event::Empty(e) => {
562
+ if self.is_cell(&e) {
563
+ row.cells.push(Self::cell_attributes(
564
+ &self.attributes,
565
+ e.attributes_raw(),
566
+ self.mode,
567
+ )?);
568
+ }
569
+ }
570
+ Event::End(_) => {
571
+ self.row_size = (row.text.len(), row.cells.len());
572
+ return Ok(row);
573
+ }
574
+ Event::Eof => return Err(Unsupported("unexpected end of row".into())),
575
+ // roo's streaming takes every child node of a row as a cell and
576
+ // fails on one that is not an element.
577
+ Event::Text(t)
578
+ if self.mode == Mode::Stream
579
+ && (self.preserves_space() || !t.trim_ascii().is_empty()) =>
580
+ {
581
+ return Err(Unsupported("text in a streamed row".into()));
582
+ }
583
+ Event::Comment(_) | Event::PI(_) | Event::CData(_) | Event::GeneralRef(_)
584
+ if self.mode == Mode::Stream =>
585
+ {
586
+ return Err(Unsupported("a non-element child of a streamed row".into()));
587
+ }
588
+ _ => after_tag = false,
589
+ }
590
+ }
591
+ }
592
+
593
+ /// Reads a whole cell written the plain way straight from the input,
594
+ /// without the event parser: `<c` with only `r`, `s`, and `t` attributes,
595
+ /// then `<f>`, `<v>`, or `<is><t>` children that hold only text and the
596
+ /// five references XML defines, then `</c>`. Reads nothing and returns
597
+ /// `false` for any other bytes, and for a cell that the input buffer cuts
598
+ /// short.
599
+ fn read_plain_cell(&mut self, row: &mut RawRow) -> Result<bool> {
600
+ let Ok(input) = self.xml.get_mut().fill_buf() else {
601
+ return Ok(false);
602
+ };
603
+ let Some(tag) = input.strip_prefix(b"<c") else {
604
+ return Ok(false);
605
+ };
606
+ let Some(tag_end) = tag.iter().position(|&b| b == b'>') else {
607
+ return Ok(false);
608
+ };
609
+ let Ok(raw) = std::str::from_utf8(&tag[..tag_end]) else {
610
+ return Ok(false);
611
+ };
612
+ // XML allows whitespace before the `>` of a start tag.
613
+ let raw = raw.trim_end_matches(' ');
614
+ if !self.attributes.read_common(raw) {
615
+ return Ok(false);
616
+ }
617
+ let mut cell = Self::cell_attributes(&self.attributes, raw, self.mode)?;
618
+ let text_start = row.text.len();
619
+ let mut rest = &tag[tag_end + 1..];
620
+ let mut decided = false;
621
+ loop {
622
+ if let Some(after) = rest.strip_prefix(b"</c>") {
623
+ rest = after;
624
+ break;
625
+ }
626
+ let (child, open, close): (Child, &[u8], &[u8]) = match rest {
627
+ [b'<', b'v', b'>', ..] => (Child::V, b"<v>", b"</v>"),
628
+ [b'<', b'f', b'>', ..] => (Child::F, b"<f>", b"</f>"),
629
+ _ if rest.starts_with(b"<is><t>") => (Child::Is, b"<is><t>", b"</t></is>"),
630
+ _ => return Self::not_plain(row, text_start),
631
+ };
632
+ let start = row.text.len();
633
+ let mut content = &rest[open.len()..];
634
+ rest = loop {
635
+ let length = content
636
+ .iter()
637
+ .position(|&b| b < b' ' || matches!(b, b'<' | b'>' | b'&'))
638
+ .unwrap_or(content.len());
639
+ let Ok(text) = std::str::from_utf8(&content[..length]) else {
640
+ return Self::not_plain(row, text_start);
641
+ };
642
+ if check_chars(text).is_err() {
643
+ return Self::not_plain(row, text_start);
644
+ }
645
+ row.text.push_str(text);
646
+ content = &content[length..];
647
+ if let Some(after) = content.strip_prefix(close) {
648
+ break after;
649
+ }
650
+ let references: [(&[u8], char); 5] = [
651
+ (b"&amp;", '&'),
652
+ (b"&lt;", '<'),
653
+ (b"&gt;", '>'),
654
+ (b"&quot;", '"'),
655
+ (b"&apos;", '\''),
656
+ ];
657
+ let Some((reference, resolved)) = references
658
+ .into_iter()
659
+ .find(|(reference, _)| content.starts_with(reference))
660
+ else {
661
+ return Self::not_plain(row, text_start);
662
+ };
663
+ row.text.push(resolved);
664
+ content = &content[reference.len()..];
665
+ };
666
+ if decided {
667
+ row.text.truncate(start);
668
+ continue;
669
+ }
670
+ match child {
671
+ Child::V => {
672
+ cell.body = Body::Value(start..row.text.len());
673
+ decided = true;
674
+ }
675
+ Child::Is if row.text.len() > start => {
676
+ cell.body = Body::Inline(start..row.text.len());
677
+ decided = true;
678
+ }
679
+ Child::F => cell.formula = Some(start..row.text.len()),
680
+ Child::Is | Child::Other => {}
681
+ }
682
+ }
683
+ if !decided {
684
+ cell.formula = None;
685
+ }
686
+ let used = input.len() - rest.len();
687
+ row.cells.push(cell);
688
+ self.xml.get_mut().consume(used);
689
+ Ok(true)
690
+ }
691
+
692
+ fn not_plain(row: &mut RawRow, text_start: usize) -> Result<bool> {
693
+ row.text.truncate(text_start);
694
+ Ok(false)
695
+ }
696
+
697
+ /// Reads the children of an open cell up to its end tag.
698
+ fn read_cell(&mut self, buf: &mut Vec<u8>, row: &mut RawRow, mut cell: RawCell) -> Result<()> {
699
+ let mut decided = false;
700
+ loop {
701
+ buf.clear();
702
+ let (child, empty) = match self.read_event(buf)? {
703
+ Event::Start(e) => (child(&e), false),
704
+ Event::Empty(e) => (child(&e), true),
705
+ Event::End(_) => break,
706
+ Event::Eof => return Err(Unsupported("unexpected end of cell".into())),
707
+ _ => continue,
708
+ };
709
+ if decided {
710
+ if !empty {
711
+ self.skip(buf)?;
712
+ }
713
+ continue;
714
+ }
715
+ let start = row.text.len();
716
+ match (child, empty) {
717
+ (Child::Is, false) => {
718
+ self.text_of_t_descendants(buf, &mut row.text)?;
719
+ if row.text.len() > start {
720
+ cell.body = Body::Inline(start..row.text.len());
721
+ decided = true;
722
+ }
723
+ }
724
+ (Child::F, false) => {
725
+ self.text_content(buf, &mut row.text)?;
726
+ cell.formula = Some(start..row.text.len());
727
+ }
728
+ (Child::F, true) => cell.formula = Some(start..start),
729
+ (Child::V, false) => {
730
+ self.text_content(buf, &mut row.text)?;
731
+ cell.body = Body::Value(start..row.text.len());
732
+ decided = true;
733
+ }
734
+ (Child::V, true) => {
735
+ cell.body = Body::Value(start..start);
736
+ decided = true;
737
+ }
738
+ (_, false) => self.skip(buf)?,
739
+ (_, true) => {}
740
+ }
741
+ }
742
+ if !decided {
743
+ cell.formula = None;
744
+ }
745
+ row.cells.push(cell);
746
+ Ok(())
747
+ }
748
+
749
+ /// Nokogiri's `Node#content` of an open element: every text and CDATA
750
+ /// descendant, with references resolved.
751
+ fn text_content(&mut self, buf: &mut Vec<u8>, out: &mut String) -> Result<()> {
752
+ let mut depth = 0usize;
753
+ loop {
754
+ buf.clear();
755
+ match self.read_event(buf)? {
756
+ Event::Start(_) => depth += 1,
757
+ Event::End(_) if depth == 0 => return Ok(()),
758
+ Event::End(_) => depth -= 1,
759
+ Event::Text(t) => out.push_str(&t.xml10_content()),
760
+ Event::CData(t) => out.push_str(&t.xml10_content()),
761
+ Event::GeneralRef(r) => push_reference(out, &r)?,
762
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
763
+ _ => {}
764
+ }
765
+ }
766
+ }
767
+
768
+ /// `is_element.search("t").map(&:content).join` for an open `is`.
769
+ fn text_of_t_descendants(&mut self, buf: &mut Vec<u8>, out: &mut String) -> Result<()> {
770
+ let mut depth = 0usize;
771
+ let mut t_depth: Option<usize> = None;
772
+ loop {
773
+ buf.clear();
774
+ match self.read_event(buf)? {
775
+ Event::Start(e) => {
776
+ depth += 1;
777
+ if e.local_name().as_ref() == "t" {
778
+ if t_depth.is_some() {
779
+ // roo joins the text of every `t`, the nested one twice.
780
+ return Err(Unsupported("a `t` inside a `t`".into()));
781
+ }
782
+ t_depth = Some(depth);
783
+ }
784
+ }
785
+ Event::End(_) if depth == 0 => return Ok(()),
786
+ Event::End(_) => {
787
+ if t_depth == Some(depth) {
788
+ t_depth = None;
789
+ }
790
+ depth -= 1;
791
+ }
792
+ Event::Text(t) if t_depth.is_some() => out.push_str(&t.xml10_content()),
793
+ Event::CData(t) if t_depth.is_some() => out.push_str(&t.xml10_content()),
794
+ Event::GeneralRef(r) if t_depth.is_some() => push_reference(out, &r)?,
795
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
796
+ _ => {}
797
+ }
798
+ }
799
+ }
800
+
801
+ /// Skips the rest of an open element.
802
+ fn skip(&mut self, buf: &mut Vec<u8>) -> Result<()> {
803
+ let mut depth = 0usize;
804
+ loop {
805
+ buf.clear();
806
+ match self.read_event(buf)? {
807
+ Event::Start(_) => depth += 1,
808
+ Event::End(_) if depth == 0 => return Ok(()),
809
+ Event::End(_) => depth -= 1,
810
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
811
+ _ => {}
812
+ }
813
+ }
814
+ }
815
+
816
+ fn cell_attributes(attributes: &TagAttributes, raw: &str, mode: Mode) -> Result<RawCell> {
817
+ let mut cell = RawCell {
818
+ reference: Reference::Missing,
819
+ style: Style::Index(0),
820
+ kind: Kind::ByFormat,
821
+ formula: None,
822
+ body: Body::Empty,
823
+ };
824
+ for attribute in attributes.iter(raw) {
825
+ let value = || attribute.normalized_value();
826
+ match attribute.key {
827
+ "r" => {
828
+ let value = value()?;
829
+ cell.reference = match parse_reference(&value) {
830
+ Some((row, column)) => Reference::At { row, column },
831
+ None => Reference::Invalid(value.into_owned()),
832
+ };
833
+ }
834
+ "s" => {
835
+ let value = value()?;
836
+ cell.style = match parse_index(&value).filter(|i| *i < 1 << 30) {
837
+ Some(index) => Style::Index(index as i64),
838
+ None => Style::Other(value.into_owned()),
839
+ };
840
+ }
841
+ "t" => {
842
+ cell.kind = match value()?.as_ref() {
843
+ "s" => Kind::Shared,
844
+ "b" => Kind::Boolean,
845
+ "str" => Kind::Str,
846
+ "inlineStr" => Kind::InlineStr,
847
+ _ => Kind::ByFormat,
848
+ };
849
+ }
850
+ // roo's document read removes namespaces first, so `x:t` would
851
+ // count as `t` there.
852
+ key if mode == Mode::Document
853
+ && key.split_once(':').is_some_and(|(prefix, local)| {
854
+ prefix != "xmlns" && matches!(local, "r" | "s" | "t")
855
+ }) =>
856
+ {
857
+ return Err(Unsupported(format!("the attribute {key} on a cell")));
858
+ }
859
+ _ => {}
860
+ }
861
+ }
862
+ Ok(cell)
863
+ }
864
+ }
865
+
866
+ /// XML 1.0 `Char`: tab, line feed, carriage return, and U+0020 up, except
867
+ /// U+FFFE and U+FFFF (Rust strings hold no surrogates).
868
+ fn check_chars(text: &str) -> Result<()> {
869
+ let bytes = text.as_bytes();
870
+ // U+FFFE and U+FFFF are EF BF BE and EF BF BF in UTF-8.
871
+ let suspect = bytes
872
+ .iter()
873
+ .fold(false, |suspect, &b| suspect | (b < 0x20) | (b == 0xEF));
874
+ if !suspect {
875
+ return Ok(());
876
+ }
877
+ let control = bytes
878
+ .iter()
879
+ .any(|&b| b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r'));
880
+ let noncharacter = bytes
881
+ .windows(3)
882
+ .any(|w| matches!(w, [0xEF, 0xBF, 0xBE | 0xBF]));
883
+ if control || noncharacter {
884
+ return Err(Unsupported("a character XML does not allow".into()));
885
+ }
886
+ Ok(())
887
+ }
888
+
889
+ /// An ASCII XML name: a letter or `_` first, then those, digits, `-`, and
890
+ /// `.`, with at most one `:` between two non-empty parts. Non-ASCII names are
891
+ /// valid XML but never appear in worksheets, so they are left to roo rather
892
+ /// than checked character by character.
893
+ fn check_name(name: &str) -> Result<()> {
894
+ let mut part_start = true;
895
+ let mut prefixed = false;
896
+ for &b in name.as_bytes() {
897
+ match b {
898
+ b'A'..=b'Z' | b'a'..=b'z' | b'_' => part_start = false,
899
+ b'0'..=b'9' | b'-' | b'.' if !part_start => {}
900
+ b':' if !part_start && !prefixed => {
901
+ part_start = true;
902
+ prefixed = true;
903
+ }
904
+ _ => return Err(Unsupported(format!("the name {name:?}"))),
905
+ }
906
+ }
907
+ if part_start {
908
+ return Err(Unsupported(format!("the name {name:?}")));
909
+ }
910
+ Ok(())
911
+ }
912
+
913
+ fn push_reference(text: &mut String, reference: &BytesRef<'_>) -> Result<()> {
914
+ if reference.is_char_ref() {
915
+ match reference.resolve_char_ref()? {
916
+ Some(c) => text.push(c),
917
+ None => return Err(Unsupported("invalid character reference".into())),
918
+ }
919
+ return Ok(());
920
+ }
921
+ let resolved = match reference.xml10_content().as_ref() {
922
+ "lt" => '<',
923
+ "gt" => '>',
924
+ "amp" => '&',
925
+ "apos" => '\'',
926
+ "quot" => '"',
927
+ other => return Err(Unsupported(format!("entity reference &{other};"))),
928
+ };
929
+ text.push(resolved);
930
+ Ok(())
931
+ }
932
+
933
+ fn check_encoding(decl: &quick_xml::events::BytesDecl<'_>) -> Result<()> {
934
+ if let Some(encoding) = decl.encoding() {
935
+ let encoding = encoding?;
936
+ if !encoding.eq_ignore_ascii_case("utf-8") {
937
+ return Err(Unsupported(format!("XML encoding {encoding}")));
938
+ }
939
+ }
940
+ Ok(())
941
+ }
942
+
943
+ #[cfg(test)]
944
+ mod tests;