roo-calamine 0.0.1

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,1182 @@
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
+ use std::fmt;
19
+ use std::fs::File;
20
+ use std::io::{BufRead, BufReader};
21
+ use std::ops::Range;
22
+ use std::path::Path;
23
+
24
+ use quick_xml::events::{BytesRef, BytesStart, Event};
25
+ use quick_xml::XmlVersion;
26
+
27
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
28
+ pub enum Mode {
29
+ Document,
30
+ Stream,
31
+ }
32
+
33
+ /// The `r` attribute.
34
+ #[derive(Clone, Debug, PartialEq, Eq)]
35
+ pub enum Reference {
36
+ Missing,
37
+ /// What `Roo::Utils.extract_coordinate` returns for it.
38
+ At {
39
+ row: i64,
40
+ column: i64,
41
+ },
42
+ /// A value `Roo::Utils.extract_coordinate` raises for.
43
+ Invalid(String),
44
+ }
45
+
46
+ /// The `s` attribute.
47
+ #[derive(Clone, Debug, PartialEq, Eq)]
48
+ pub enum Style {
49
+ /// Absent (index 0) or plain decimal digits.
50
+ Index(i64),
51
+ /// Anything else, for Ruby's `String#to_i` to read.
52
+ Other(String),
53
+ }
54
+
55
+ /// The `t` attribute, as far as `SheetDoc#cell_value_type` tells types apart.
56
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
57
+ pub enum Kind {
58
+ /// Absent or any other value: the style's number format decides.
59
+ ByFormat,
60
+ Shared,
61
+ Boolean,
62
+ Str,
63
+ InlineStr,
64
+ }
65
+
66
+ /// A byte range of [`RawRow::text`].
67
+ pub type Span = Range<usize>;
68
+
69
+ /// The value-bearing part of a cell, as `cell_from_xml` sees it.
70
+ #[derive(Clone, Debug, PartialEq, Eq)]
71
+ pub enum Body {
72
+ /// No `v` child and no `is` child with text.
73
+ Empty,
74
+ /// Text of every `t` element inside the first non-empty `is` child.
75
+ Inline(Span),
76
+ /// Text content of the first `v` child.
77
+ Value(Span),
78
+ }
79
+
80
+ #[derive(Clone, Debug, PartialEq, Eq)]
81
+ pub struct RawCell {
82
+ pub reference: Reference,
83
+ pub style: Style,
84
+ pub kind: Kind,
85
+ pub formula: Option<Span>,
86
+ pub body: Body,
87
+ }
88
+
89
+ /// A row's cells. Their text lives in one buffer, so a row costs a few
90
+ /// allocations rather than a few per cell.
91
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
92
+ pub struct RawRow {
93
+ pub text: String,
94
+ pub cells: Vec<RawCell>,
95
+ }
96
+
97
+ impl RawRow {
98
+ pub fn text(&self, span: &Span) -> &str {
99
+ &self.text[span.clone()]
100
+ }
101
+ }
102
+
103
+ /// The input uses XML that this reader does not reproduce exactly the way
104
+ /// Nokogiri does, so the caller falls back to roo's own parser.
105
+ #[derive(Debug, PartialEq, Eq)]
106
+ pub struct Unsupported(pub String);
107
+
108
+ impl fmt::Display for Unsupported {
109
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110
+ f.write_str(&self.0)
111
+ }
112
+ }
113
+
114
+ impl std::error::Error for Unsupported {}
115
+
116
+ impl From<quick_xml::Error> for Unsupported {
117
+ fn from(error: quick_xml::Error) -> Self {
118
+ Unsupported(error.to_string())
119
+ }
120
+ }
121
+
122
+ impl From<quick_xml::events::attributes::AttrError> for Unsupported {
123
+ fn from(error: quick_xml::events::attributes::AttrError) -> Self {
124
+ Unsupported(error.to_string())
125
+ }
126
+ }
127
+
128
+ type Result<T> = std::result::Result<T, Unsupported>;
129
+
130
+ /// `Roo::Utils.extract_coordinate` for well-formed references. `None` means
131
+ /// roo raises.
132
+ pub fn parse_reference(reference: &str) -> Option<(i64, i64)> {
133
+ let (mut column, mut row) = (0i64, 0i64);
134
+ let mut digits = false;
135
+ for byte in reference.bytes() {
136
+ match byte {
137
+ b'A'..=b'Z' | b'a'..=b'z' if !digits => {
138
+ column = column
139
+ .checked_mul(26)?
140
+ .checked_add(i64::from(byte & 0x1f))?;
141
+ }
142
+ b'0'..=b'9' => {
143
+ digits = true;
144
+ row = row.checked_mul(10)?.checked_add(i64::from(byte - b'0'))?;
145
+ }
146
+ _ => return None,
147
+ }
148
+ }
149
+ (column != 0 && digits).then_some((row, column))
150
+ }
151
+
152
+ /// `String#to_i` for plain decimal digits; `None` leaves it to Ruby.
153
+ pub fn parse_index(text: &str) -> Option<usize> {
154
+ if text.is_empty() || text.len() > 18 || !text.bytes().all(|b| b.is_ascii_digit()) {
155
+ return None;
156
+ }
157
+ text.parse().ok()
158
+ }
159
+
160
+ pub struct SheetReader<R: BufRead> {
161
+ xml: quick_xml::Reader<R>,
162
+ mode: Mode,
163
+ buf: Vec<u8>,
164
+ depth: usize,
165
+ /// Document mode: depth of the open `sheetData` element under the
166
+ /// `worksheet` root, if any.
167
+ sheet_data_depth: Option<usize>,
168
+ root_is_worksheet: bool,
169
+ done: bool,
170
+ /// Whether any event was read yet; an XML declaration must come first.
171
+ started: bool,
172
+ seen_root: bool,
173
+ /// Depth of the open elements, for namespace scopes.
174
+ open_elements: usize,
175
+ /// Namespace prefixes declared on open elements, with the depth they were
176
+ /// declared at.
177
+ prefixes: Vec<(usize, String)>,
178
+ /// `xml:space` values on open elements (`true` for `preserve`), with the
179
+ /// depth they were set at.
180
+ space: Vec<(usize, bool)>,
181
+ /// Stream mode: whether a row is being read.
182
+ in_row: bool,
183
+ /// An error met after some rows of a batch, returned by the next call.
184
+ pending_error: Option<Unsupported>,
185
+ }
186
+
187
+ impl SheetReader<BufReader<File>> {
188
+ pub fn open(path: &Path, mode: Mode) -> Result<Self> {
189
+ let file = File::open(path).map_err(|e| Unsupported(e.to_string()))?;
190
+ Ok(Self::new(BufReader::with_capacity(64 * 1024, file), mode))
191
+ }
192
+ }
193
+
194
+ #[derive(Clone, Copy)]
195
+ enum Child {
196
+ Is,
197
+ F,
198
+ V,
199
+ Other,
200
+ }
201
+
202
+ fn child(e: &BytesStart<'_>) -> Child {
203
+ match e.local_name().as_ref() {
204
+ "is" => Child::Is,
205
+ "f" => Child::F,
206
+ "v" => Child::V,
207
+ _ => Child::Other,
208
+ }
209
+ }
210
+
211
+ impl<R: BufRead> SheetReader<R> {
212
+ pub fn new(input: R, mode: Mode) -> Self {
213
+ let mut xml = quick_xml::Reader::from_reader(input);
214
+ let config = xml.config_mut();
215
+ config.check_end_names = true;
216
+ config.check_comments = true;
217
+ Self {
218
+ xml,
219
+ mode,
220
+ buf: Vec::with_capacity(16 * 1024),
221
+ depth: 0,
222
+ sheet_data_depth: None,
223
+ root_is_worksheet: false,
224
+ done: false,
225
+ started: false,
226
+ seen_root: false,
227
+ open_elements: 0,
228
+ prefixes: Vec::new(),
229
+ space: Vec::new(),
230
+ in_row: false,
231
+ pending_error: None,
232
+ }
233
+ }
234
+
235
+ /// Reads rows until they hold about `cells` cells (an empty row counts as
236
+ /// one). An empty vector means the sheet is done. When an error stops a
237
+ /// batch, the rows before it are returned first and the error on the next
238
+ /// call.
239
+ pub fn next_rows(&mut self, cells: usize) -> Result<Vec<RawRow>> {
240
+ if let Some(error) = self.pending_error.take() {
241
+ return Err(error);
242
+ }
243
+ let mut rows = Vec::new();
244
+ let mut read = 0;
245
+ while read < cells {
246
+ match self.next_row() {
247
+ Ok(Some(row)) => {
248
+ read += row.cells.len().max(1);
249
+ rows.push(row);
250
+ }
251
+ Ok(None) => break,
252
+ Err(error) if rows.is_empty() => return Err(error),
253
+ Err(error) => {
254
+ self.pending_error = Some(error);
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ Ok(rows)
260
+ }
261
+
262
+ pub fn next_row(&mut self) -> Result<Option<RawRow>> {
263
+ let mut buf = std::mem::take(&mut self.buf);
264
+ let row = self.find_row(&mut buf);
265
+ self.buf = buf;
266
+ row
267
+ }
268
+
269
+ fn find_row(&mut self, buf: &mut Vec<u8>) -> Result<Option<RawRow>> {
270
+ if self.done {
271
+ return Ok(None);
272
+ }
273
+ loop {
274
+ buf.clear();
275
+ let event = self.read_event(buf)?;
276
+ if self.depth == 0 {
277
+ self.check_top_level(&event)?;
278
+ }
279
+ match event {
280
+ Event::Start(e) => {
281
+ self.depth += 1;
282
+ if self.is_row(&e) {
283
+ self.in_row = true;
284
+ let row = self.read_row(buf);
285
+ self.in_row = false;
286
+ self.depth -= 1;
287
+ return row.map(Some);
288
+ }
289
+ self.enter(&e);
290
+ }
291
+ Event::Empty(e) => {
292
+ self.depth += 1;
293
+ let is_row = self.is_row(&e);
294
+ self.depth -= 1;
295
+ if is_row {
296
+ return Ok(Some(RawRow::default()));
297
+ }
298
+ }
299
+ Event::End(_) => {
300
+ if self.sheet_data_depth == Some(self.depth) {
301
+ self.sheet_data_depth = None;
302
+ }
303
+ self.depth -= 1;
304
+ }
305
+ Event::Decl(decl) => check_encoding(&decl)?,
306
+ Event::Eof if self.depth != 0 => {
307
+ return Err(Unsupported("unexpected end of the sheet".into()));
308
+ }
309
+ Event::Eof => {
310
+ self.done = true;
311
+ return Ok(None);
312
+ }
313
+ _ => {}
314
+ }
315
+ }
316
+ }
317
+
318
+ /// An element name and its attributes, as far as quick-xml leaves them
319
+ /// unchecked: names and their namespace prefixes, whitespace between
320
+ /// attributes, `<` in values, and the references in values.
321
+ fn check_start(&mut self, e: &BytesStart<'_>) -> Result<()> {
322
+ let name = e.name();
323
+ check_name(name.as_ref())?;
324
+ let raw = e.attributes_raw();
325
+ check_chars(raw)?;
326
+ let mut quote = None;
327
+ let mut after_value = false;
328
+ let mut name_start = true;
329
+ for &b in raw.as_bytes() {
330
+ match quote {
331
+ Some(q) if b == q => {
332
+ quote = None;
333
+ after_value = true;
334
+ }
335
+ Some(_) if b == b'<' => {
336
+ return Err(Unsupported("`<` in an attribute value".into()))
337
+ }
338
+ Some(_) => {}
339
+ None if b.is_ascii_whitespace() => {
340
+ after_value = false;
341
+ name_start = true;
342
+ }
343
+ None if after_value && b != b'/' => {
344
+ return Err(Unsupported(
345
+ "attributes without whitespace between them".into(),
346
+ ));
347
+ }
348
+ None => match b {
349
+ b'"' | b'\'' => quote = Some(b),
350
+ b'=' | b'/' => name_start = true,
351
+ b if b.is_ascii_alphabetic() || b == b'_' || b == b':' => name_start = false,
352
+ b'0'..=b'9' | b'-' | b'.' if !name_start => {}
353
+ _ => return Err(Unsupported("an invalid attribute name".into())),
354
+ },
355
+ }
356
+ }
357
+ if self.mode == Mode::Stream && self.in_row {
358
+ // roo's streaming reader yields a nested `row` as a row of its own,
359
+ // and matches prefixed names in a row by namespace.
360
+ if name.as_ref() == "row" || name.as_ref().contains(':') {
361
+ return Err(Unsupported(format!(
362
+ "<{}> inside a streamed row",
363
+ name.as_ref()
364
+ )));
365
+ }
366
+ }
367
+ let depth = self.open_elements + 1;
368
+ // quick-xml reports duplicated attributes only while iterating.
369
+ for attribute in e.attributes() {
370
+ let attribute = attribute?;
371
+ let key = attribute.key.as_ref();
372
+ if key.contains(':') {
373
+ check_name(key)?;
374
+ }
375
+ if attribute.value.contains('&') {
376
+ check_chars(&attribute.normalized_value(XmlVersion::Implicit1_0)?)?;
377
+ }
378
+ if let Some(prefix) = key.strip_prefix("xmlns:") {
379
+ if attribute.value.is_empty() {
380
+ return Err(Unsupported(format!("an empty namespace for {prefix}")));
381
+ }
382
+ self.prefixes.push((depth, prefix.to_owned()));
383
+ }
384
+ if key == "xml:space" {
385
+ self.space
386
+ .push((depth, attribute.value.as_ref() == "preserve"));
387
+ }
388
+ }
389
+ if raw.contains(':') {
390
+ for attribute in e.attributes() {
391
+ let key = attribute?.key;
392
+ self.check_prefix(key.as_ref())?;
393
+ }
394
+ }
395
+ self.check_prefix(name.as_ref())
396
+ }
397
+
398
+ /// Ends the namespace and `xml:space` scopes of an element at `depth`.
399
+ fn leave(&mut self, depth: usize) {
400
+ self.prefixes.retain(|(declared, _)| *declared != depth);
401
+ self.space.retain(|(declared, _)| *declared != depth);
402
+ }
403
+
404
+ fn preserves_space(&self) -> bool {
405
+ self.space.last().is_some_and(|(_, preserve)| *preserve)
406
+ }
407
+
408
+ /// libxml2 keeps an undeclared prefix as part of the name, so roo would
409
+ /// not see `x:row` as `row`.
410
+ fn check_prefix(&self, name: &str) -> Result<()> {
411
+ match name.split_once(':') {
412
+ Some(("xml" | "xmlns", _)) | None => Ok(()),
413
+ Some((prefix, _)) if self.prefixes.iter().any(|(_, declared)| declared == prefix) => {
414
+ Ok(())
415
+ }
416
+ Some(_) => Err(Unsupported(format!("the undeclared prefix in {name}"))),
417
+ }
418
+ }
419
+
420
+ /// Outside the root element, XML allows only one root, whitespace,
421
+ /// comments, and processing instructions.
422
+ fn check_top_level(&mut self, event: &Event<'_>) -> Result<()> {
423
+ match event {
424
+ Event::Start(_) | Event::Empty(_) if self.seen_root => {
425
+ Err(Unsupported("a second root element".into()))
426
+ }
427
+ Event::Start(_) | Event::Empty(_) => {
428
+ self.seen_root = true;
429
+ Ok(())
430
+ }
431
+ Event::Text(t) if !t.trim_ascii().is_empty() => {
432
+ Err(Unsupported("text outside the root element".into()))
433
+ }
434
+ Event::CData(_) | Event::GeneralRef(_) => {
435
+ Err(Unsupported("text outside the root element".into()))
436
+ }
437
+ Event::Eof if !self.seen_root => Err(Unsupported("no root element".into())),
438
+ _ => Ok(()),
439
+ }
440
+ }
441
+
442
+ /// The next event, rejecting characters XML does not allow, which
443
+ /// Nokogiri does not accept and quick-xml does not check.
444
+ fn read_event<'b>(&mut self, buf: &'b mut Vec<u8>) -> Result<Event<'b>> {
445
+ let event = self.xml.read_event_into(buf)?;
446
+ let first = !std::mem::replace(&mut self.started, true);
447
+ match &event {
448
+ Event::Start(e) => {
449
+ self.check_start(e)?;
450
+ self.open_elements += 1;
451
+ }
452
+ Event::Empty(e) => {
453
+ self.check_start(e)?;
454
+ self.leave(self.open_elements + 1);
455
+ }
456
+ Event::End(_) => {
457
+ self.leave(self.open_elements);
458
+ self.open_elements = self.open_elements.saturating_sub(1);
459
+ }
460
+ Event::Text(t) if t.contains("]]>") => {
461
+ return Err(Unsupported("`]]>` in text".into()));
462
+ }
463
+ Event::Text(t) | Event::Comment(t) => check_chars(t)?,
464
+ Event::CData(t) => check_chars(&t.xml10_content())?,
465
+ Event::PI(pi) => {
466
+ check_name(pi.target())?;
467
+ check_chars(pi)?;
468
+ }
469
+ Event::GeneralRef(r) => match r.resolve_char_ref()? {
470
+ Some(c) => check_chars(c.encode_utf8(&mut [0; 4]))?,
471
+ None => {
472
+ if !matches!(
473
+ r.xml10_content().as_ref(),
474
+ "lt" | "gt" | "amp" | "apos" | "quot"
475
+ ) {
476
+ return Err(Unsupported(
477
+ "an entity reference XML does not define".into(),
478
+ ));
479
+ }
480
+ }
481
+ },
482
+ Event::Decl(_) if !first => {
483
+ return Err(Unsupported("an XML declaration after the start".into()));
484
+ }
485
+ // A DTD can define entities and attribute defaults, which Nokogiri
486
+ // applies and this reader does not.
487
+ Event::DocType(_) => return Err(Unsupported("a document type declaration".into())),
488
+ _ => {}
489
+ }
490
+ Ok(event)
491
+ }
492
+
493
+ /// Tracks the `/worksheet/sheetData` path for document mode. `self.depth`
494
+ /// is already the depth of `e`.
495
+ fn enter(&mut self, e: &BytesStart<'_>) {
496
+ if self.mode != Mode::Document {
497
+ return;
498
+ }
499
+ let name = e.local_name();
500
+ if self.depth == 1 {
501
+ self.root_is_worksheet = name.as_ref() == "worksheet";
502
+ } else if self.depth == 2 && self.root_is_worksheet && name.as_ref() == "sheetData" {
503
+ self.sheet_data_depth = Some(2);
504
+ }
505
+ }
506
+
507
+ /// `self.depth` is already the depth of `e`.
508
+ fn is_row(&self, e: &BytesStart<'_>) -> bool {
509
+ match self.mode {
510
+ Mode::Document => {
511
+ self.sheet_data_depth == Some(self.depth - 1) && e.local_name().as_ref() == "row"
512
+ }
513
+ Mode::Stream => e.name().as_ref() == "row",
514
+ }
515
+ }
516
+
517
+ fn is_cell(&self, e: &BytesStart<'_>) -> bool {
518
+ match self.mode {
519
+ Mode::Document => e.local_name().as_ref() == "c",
520
+ Mode::Stream => true,
521
+ }
522
+ }
523
+
524
+ /// Reads the children of an open `row` up to its end tag.
525
+ fn read_row(&mut self, buf: &mut Vec<u8>) -> Result<RawRow> {
526
+ let mut row = RawRow::default();
527
+ loop {
528
+ buf.clear();
529
+ match self.read_event(buf)? {
530
+ Event::Start(e) => {
531
+ if self.is_cell(&e) {
532
+ let cell = cell_attributes(&e, self.mode)?;
533
+ self.read_cell(buf, &mut row, cell)?;
534
+ } else {
535
+ self.skip(buf)?;
536
+ }
537
+ }
538
+ Event::Empty(e) => {
539
+ if self.is_cell(&e) {
540
+ row.cells.push(cell_attributes(&e, self.mode)?);
541
+ }
542
+ }
543
+ Event::End(_) => return Ok(row),
544
+ Event::Eof => return Err(Unsupported("unexpected end of row".into())),
545
+ // roo's streaming takes every child node of a row as a cell and
546
+ // fails on one that is not an element.
547
+ Event::Text(t)
548
+ if self.mode == Mode::Stream
549
+ && (self.preserves_space() || !t.trim_ascii().is_empty()) =>
550
+ {
551
+ return Err(Unsupported("text in a streamed row".into()));
552
+ }
553
+ Event::Comment(_) | Event::PI(_) | Event::CData(_) | Event::GeneralRef(_)
554
+ if self.mode == Mode::Stream =>
555
+ {
556
+ return Err(Unsupported("a non-element child of a streamed row".into()));
557
+ }
558
+ _ => {}
559
+ }
560
+ }
561
+ }
562
+
563
+ /// Reads the children of an open cell up to its end tag.
564
+ fn read_cell(&mut self, buf: &mut Vec<u8>, row: &mut RawRow, mut cell: RawCell) -> Result<()> {
565
+ let mut decided = false;
566
+ loop {
567
+ buf.clear();
568
+ let (child, empty) = match self.read_event(buf)? {
569
+ Event::Start(e) => (child(&e), false),
570
+ Event::Empty(e) => (child(&e), true),
571
+ Event::End(_) => break,
572
+ Event::Eof => return Err(Unsupported("unexpected end of cell".into())),
573
+ _ => continue,
574
+ };
575
+ if decided {
576
+ if !empty {
577
+ self.skip(buf)?;
578
+ }
579
+ continue;
580
+ }
581
+ let start = row.text.len();
582
+ match (child, empty) {
583
+ (Child::Is, false) => {
584
+ self.text_of_t_descendants(buf, &mut row.text)?;
585
+ if row.text.len() > start {
586
+ cell.body = Body::Inline(start..row.text.len());
587
+ decided = true;
588
+ }
589
+ }
590
+ (Child::F, false) => {
591
+ self.text_content(buf, &mut row.text)?;
592
+ cell.formula = Some(start..row.text.len());
593
+ }
594
+ (Child::F, true) => cell.formula = Some(start..start),
595
+ (Child::V, false) => {
596
+ self.text_content(buf, &mut row.text)?;
597
+ cell.body = Body::Value(start..row.text.len());
598
+ decided = true;
599
+ }
600
+ (Child::V, true) => {
601
+ cell.body = Body::Value(start..start);
602
+ decided = true;
603
+ }
604
+ (_, false) => self.skip(buf)?,
605
+ (_, true) => {}
606
+ }
607
+ }
608
+ if !decided {
609
+ cell.formula = None;
610
+ }
611
+ row.cells.push(cell);
612
+ Ok(())
613
+ }
614
+
615
+ /// Nokogiri's `Node#content` of an open element: every text and CDATA
616
+ /// descendant, with references resolved.
617
+ fn text_content(&mut self, buf: &mut Vec<u8>, out: &mut String) -> Result<()> {
618
+ let mut depth = 0usize;
619
+ loop {
620
+ buf.clear();
621
+ match self.read_event(buf)? {
622
+ Event::Start(_) => depth += 1,
623
+ Event::End(_) if depth == 0 => return Ok(()),
624
+ Event::End(_) => depth -= 1,
625
+ Event::Text(t) => out.push_str(&t.xml10_content()),
626
+ Event::CData(t) => out.push_str(&t.xml10_content()),
627
+ Event::GeneralRef(r) => push_reference(out, &r)?,
628
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
629
+ _ => {}
630
+ }
631
+ }
632
+ }
633
+
634
+ /// `is_element.search("t").map(&:content).join` for an open `is`.
635
+ fn text_of_t_descendants(&mut self, buf: &mut Vec<u8>, out: &mut String) -> Result<()> {
636
+ let mut depth = 0usize;
637
+ let mut t_depth: Option<usize> = None;
638
+ loop {
639
+ buf.clear();
640
+ match self.read_event(buf)? {
641
+ Event::Start(e) => {
642
+ depth += 1;
643
+ if e.local_name().as_ref() == "t" {
644
+ if t_depth.is_some() {
645
+ // roo joins the text of every `t`, the nested one twice.
646
+ return Err(Unsupported("a `t` inside a `t`".into()));
647
+ }
648
+ t_depth = Some(depth);
649
+ }
650
+ }
651
+ Event::End(_) if depth == 0 => return Ok(()),
652
+ Event::End(_) => {
653
+ if t_depth == Some(depth) {
654
+ t_depth = None;
655
+ }
656
+ depth -= 1;
657
+ }
658
+ Event::Text(t) if t_depth.is_some() => out.push_str(&t.xml10_content()),
659
+ Event::CData(t) if t_depth.is_some() => out.push_str(&t.xml10_content()),
660
+ Event::GeneralRef(r) if t_depth.is_some() => push_reference(out, &r)?,
661
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
662
+ _ => {}
663
+ }
664
+ }
665
+ }
666
+
667
+ /// Skips the rest of an open element.
668
+ fn skip(&mut self, buf: &mut Vec<u8>) -> Result<()> {
669
+ let mut depth = 0usize;
670
+ loop {
671
+ buf.clear();
672
+ match self.read_event(buf)? {
673
+ Event::Start(_) => depth += 1,
674
+ Event::End(_) if depth == 0 => return Ok(()),
675
+ Event::End(_) => depth -= 1,
676
+ Event::Eof => return Err(Unsupported("unexpected end of element".into())),
677
+ _ => {}
678
+ }
679
+ }
680
+ }
681
+ }
682
+
683
+ fn cell_attributes(e: &BytesStart<'_>, mode: Mode) -> Result<RawCell> {
684
+ let mut cell = RawCell {
685
+ reference: Reference::Missing,
686
+ style: Style::Index(0),
687
+ kind: Kind::ByFormat,
688
+ formula: None,
689
+ body: Body::Empty,
690
+ };
691
+ for attribute in e.attributes() {
692
+ let attribute = attribute?;
693
+ let value = || attribute.normalized_value(XmlVersion::Implicit1_0);
694
+ match attribute.key.as_ref() {
695
+ "r" => {
696
+ let value = value()?;
697
+ cell.reference = match parse_reference(&value) {
698
+ Some((row, column)) => Reference::At { row, column },
699
+ None => Reference::Invalid(value.into_owned()),
700
+ };
701
+ }
702
+ "s" => {
703
+ let value = value()?;
704
+ cell.style = match parse_index(&value).filter(|i| *i < 1 << 30) {
705
+ Some(index) => Style::Index(index as i64),
706
+ None => Style::Other(value.into_owned()),
707
+ };
708
+ }
709
+ "t" => {
710
+ cell.kind = match value()?.as_ref() {
711
+ "s" => Kind::Shared,
712
+ "b" => Kind::Boolean,
713
+ "str" => Kind::Str,
714
+ "inlineStr" => Kind::InlineStr,
715
+ _ => Kind::ByFormat,
716
+ };
717
+ }
718
+ // roo's document read removes namespaces first, so `x:t` would
719
+ // count as `t` there.
720
+ key if mode == Mode::Document
721
+ && key.split_once(':').is_some_and(|(prefix, local)| {
722
+ prefix != "xmlns" && matches!(local, "r" | "s" | "t")
723
+ }) =>
724
+ {
725
+ return Err(Unsupported(format!("the attribute {key} on a cell")));
726
+ }
727
+ _ => {}
728
+ }
729
+ }
730
+ Ok(cell)
731
+ }
732
+
733
+ /// XML 1.0 `Char`: tab, line feed, carriage return, and U+0020 up, except
734
+ /// U+FFFE and U+FFFF (Rust strings hold no surrogates).
735
+ fn check_chars(text: &str) -> Result<()> {
736
+ let control = text
737
+ .bytes()
738
+ .any(|b| b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r'));
739
+ if control || text.contains(['\u{FFFE}', '\u{FFFF}']) {
740
+ return Err(Unsupported("a character XML does not allow".into()));
741
+ }
742
+ Ok(())
743
+ }
744
+
745
+ /// An ASCII XML name: a letter or `_` first, then those, digits, `-`, and
746
+ /// `.`, with at most one `:` between two non-empty parts. Non-ASCII names are
747
+ /// valid XML but never appear in worksheets, so they are left to roo rather
748
+ /// than checked character by character.
749
+ fn check_name(name: &str) -> Result<()> {
750
+ let part = |part: &str| {
751
+ part.bytes().enumerate().all(|(i, b)| {
752
+ b.is_ascii_alphabetic()
753
+ || b == b'_'
754
+ || (i > 0 && (b.is_ascii_digit() || b == b'-' || b == b'.'))
755
+ }) && !part.is_empty()
756
+ };
757
+ let valid = match name.split_once(':') {
758
+ Some((prefix, local)) => part(prefix) && part(local),
759
+ None => part(name),
760
+ };
761
+ if valid {
762
+ Ok(())
763
+ } else {
764
+ Err(Unsupported(format!("the name {name:?}")))
765
+ }
766
+ }
767
+
768
+ fn push_reference(text: &mut String, reference: &BytesRef<'_>) -> Result<()> {
769
+ if reference.is_char_ref() {
770
+ match reference.resolve_char_ref()? {
771
+ Some(c) => text.push(c),
772
+ None => return Err(Unsupported("invalid character reference".into())),
773
+ }
774
+ return Ok(());
775
+ }
776
+ let resolved = match reference.xml10_content().as_ref() {
777
+ "lt" => '<',
778
+ "gt" => '>',
779
+ "amp" => '&',
780
+ "apos" => '\'',
781
+ "quot" => '"',
782
+ other => return Err(Unsupported(format!("entity reference &{other};"))),
783
+ };
784
+ text.push(resolved);
785
+ Ok(())
786
+ }
787
+
788
+ fn check_encoding(decl: &quick_xml::events::BytesDecl<'_>) -> Result<()> {
789
+ if let Some(encoding) = decl.encoding() {
790
+ let encoding = encoding?;
791
+ if !encoding.eq_ignore_ascii_case("utf-8") {
792
+ return Err(Unsupported(format!("XML encoding {encoding}")));
793
+ }
794
+ }
795
+ Ok(())
796
+ }
797
+
798
+ #[cfg(test)]
799
+ mod tests {
800
+ use super::*;
801
+
802
+ const NS: &str = r#"xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#;
803
+
804
+ fn rows(xml: &str, mode: Mode) -> Vec<RawRow> {
805
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
806
+ let mut out = Vec::new();
807
+ while let Some(row) = reader.next_row().unwrap() {
808
+ out.push(row);
809
+ }
810
+ out
811
+ }
812
+
813
+ /// `(reference, formula, body)` of every cell, with text resolved.
814
+ fn texts(row: &RawRow) -> Vec<(Reference, Option<&str>, Option<&str>)> {
815
+ row.cells
816
+ .iter()
817
+ .map(|c| {
818
+ let body = match &c.body {
819
+ Body::Empty => None,
820
+ Body::Inline(s) | Body::Value(s) => Some(row.text(s)),
821
+ };
822
+ (
823
+ c.reference.clone(),
824
+ c.formula.as_ref().map(|s| row.text(s)),
825
+ body,
826
+ )
827
+ })
828
+ .collect()
829
+ }
830
+
831
+ fn at(row: i64, column: i64) -> Reference {
832
+ Reference::At { row, column }
833
+ }
834
+
835
+ #[test]
836
+ fn reads_values_formulas_and_inline_strings() {
837
+ let xml = format!(
838
+ r#"<?xml version="1.0" encoding="UTF-8"?><worksheet {NS}><sheetData>
839
+ <row r="1"><c r="A1" s="2" t="n"><f>B1+1</f><v>1.0</v></c><c r="B1" t="inlineStr"><is><r><t>a</t></r><rPh><t>b</t></rPh></is></c></row>
840
+ </sheetData></worksheet>"#
841
+ );
842
+ let got = rows(&xml, Mode::Document);
843
+ assert_eq!(got.len(), 1);
844
+ assert_eq!(got[0].cells[0].style, Style::Index(2));
845
+ assert_eq!(got[0].cells[0].kind, Kind::ByFormat);
846
+ assert_eq!(got[0].cells[1].kind, Kind::InlineStr);
847
+ assert!(matches!(got[0].cells[1].body, Body::Inline(_)));
848
+ assert_eq!(
849
+ texts(&got[0]),
850
+ vec![
851
+ (at(1, 1), Some("B1+1"), Some("1.0")),
852
+ (at(1, 2), None, Some("ab"))
853
+ ]
854
+ );
855
+ }
856
+
857
+ #[test]
858
+ fn keeps_attributes_ruby_has_to_read() {
859
+ let xml = format!(
860
+ r#"<worksheet {NS}><sheetData><row><c r="1A" s=" 3" t="e"/><c/></row></sheetData></worksheet>"#
861
+ );
862
+ let got = rows(&xml, Mode::Document);
863
+ let cells = &got[0].cells;
864
+ assert_eq!(cells[0].reference, Reference::Invalid("1A".into()));
865
+ assert_eq!(cells[0].style, Style::Other(" 3".into()));
866
+ assert_eq!(cells[0].kind, Kind::ByFormat);
867
+ assert_eq!(cells[1].reference, Reference::Missing);
868
+ assert_eq!(cells[1].style, Style::Index(0));
869
+ }
870
+
871
+ #[test]
872
+ fn resolves_references_and_normalizes_line_ends() {
873
+ let xml = format!(
874
+ "<worksheet {NS}><sheetData><row><c r=\"A1\" t=\"str\"><v>a&amp;b&#13;\r\nc<![CDATA[<d>]]></v></c></row></sheetData></worksheet>"
875
+ );
876
+ let got = rows(&xml, Mode::Document);
877
+ assert_eq!(texts(&got[0])[0].2, Some("a&b\r\nc<d>"));
878
+ }
879
+
880
+ #[test]
881
+ fn first_value_wins_and_formula_after_it_is_ignored() {
882
+ let xml = format!(
883
+ "<worksheet {NS}><sheetData><row><c r=\"A1\"><v>1</v><f>X</f><v>2</v></c><c r=\"B1\"><f>Y</f></c></row></sheetData></worksheet>"
884
+ );
885
+ let got = rows(&xml, Mode::Document);
886
+ assert_eq!(
887
+ texts(&got[0]),
888
+ vec![(at(1, 1), None, Some("1")), (at(1, 2), None, None)]
889
+ );
890
+ }
891
+
892
+ #[test]
893
+ fn empty_inline_string_does_not_decide_the_value() {
894
+ let xml = format!(
895
+ "<worksheet {NS}><sheetData><row><c r=\"A1\" t=\"inlineStr\"><is><t></t></is><v>7</v></c></row></sheetData></worksheet>"
896
+ );
897
+ let got = rows(&xml, Mode::Document);
898
+ assert!(matches!(got[0].cells[0].body, Body::Value(_)));
899
+ assert_eq!(texts(&got[0])[0].2, Some("7"));
900
+ }
901
+
902
+ #[test]
903
+ fn document_mode_reads_only_sheet_data_rows_and_c_children() {
904
+ let xml = format!(
905
+ "<worksheet {NS}><sheetPr><row/></sheetPr><sheetData><row r=\"1\"/><row r=\"2\"><x>1</x><c r=\"A2\"/></row></sheetData></worksheet>"
906
+ );
907
+ let got = rows(&xml, Mode::Document);
908
+ assert_eq!(got.len(), 2);
909
+ assert!(got[0].cells.is_empty());
910
+ assert_eq!(texts(&got[1]), vec![(at(2, 1), None, None)]);
911
+ }
912
+
913
+ #[test]
914
+ fn document_mode_matches_prefixed_names_by_local_name() {
915
+ let xml = r#"<x:worksheet xmlns:x="urn:x"><x:sheetData><x:row><x:c r="A1"><x:v>3</x:v></x:c></x:row></x:sheetData></x:worksheet>"#;
916
+ let got = rows(xml, Mode::Document);
917
+ assert_eq!(texts(&got[0])[0].2, Some("3"));
918
+ }
919
+
920
+ #[test]
921
+ fn stream_mode_needs_an_unprefixed_row_and_takes_every_child_element() {
922
+ let prefixed = r#"<x:worksheet xmlns:x="urn:x"><x:sheetData><x:row><x:c r="A1"/></x:row></x:sheetData></x:worksheet>"#;
923
+ assert!(rows(prefixed, Mode::Stream).is_empty());
924
+
925
+ let xml = format!(
926
+ "<worksheet {NS}><sheetData><row><c r=\"A1\"/><extra r=\"B1\"><v>1</v></extra></row></sheetData></worksheet>"
927
+ );
928
+ let got = rows(&xml, Mode::Stream);
929
+ assert_eq!(
930
+ texts(&got[0]),
931
+ vec![(at(1, 1), None, None), (at(1, 2), None, Some("1"))]
932
+ );
933
+ }
934
+
935
+ #[test]
936
+ fn rejects_inputs_it_cannot_reproduce() {
937
+ let latin1 = r#"<?xml version="1.0" encoding="ISO-8859-1"?><worksheet/>"#;
938
+ assert!(SheetReader::new(latin1.as_bytes(), Mode::Document)
939
+ .next_row()
940
+ .is_err());
941
+ for body in [
942
+ "<v>&custom;</v>",
943
+ "<v>a&#1;b</v>",
944
+ "<v>a\u{1}b</v>",
945
+ "<v>\u{FFFE}</v>",
946
+ ] {
947
+ let xml = format!(
948
+ "<worksheet {NS}><sheetData><row><c>{body}</c></row></sheetData></worksheet>"
949
+ );
950
+ assert!(
951
+ SheetReader::new(xml.as_bytes(), Mode::Document)
952
+ .next_row()
953
+ .is_err(),
954
+ "{body:?}"
955
+ );
956
+ }
957
+ let attribute = format!(
958
+ r#"<worksheet {NS}><sheetData><row><c r="A&#2;1"/></row></sheetData></worksheet>"#
959
+ );
960
+ assert!(SheetReader::new(attribute.as_bytes(), Mode::Document)
961
+ .next_row()
962
+ .is_err());
963
+ let truncated = format!("<worksheet {NS}><sheetData><row><c><v>1</v></c></row>");
964
+ let mut reader = SheetReader::new(truncated.as_bytes(), Mode::Document);
965
+ assert!(reader.next_row().unwrap().is_some());
966
+ assert!(
967
+ reader.next_row().is_err(),
968
+ "a sheet that ends with open elements"
969
+ );
970
+ }
971
+
972
+ #[test]
973
+ fn rejects_xml_that_is_not_well_formed() {
974
+ let rows = r#"<sheetData><row r="1"><c r="A1"><v>1</v></c></row></sheetData>"#;
975
+ for (label, xml) in [
976
+ (
977
+ "unquoted value",
978
+ "<worksheet><sheetData><row r=1/></sheetData></worksheet>".to_string(),
979
+ ),
980
+ (
981
+ "no space between attributes",
982
+ r#"<worksheet><sheetData><row r="1"s="2"/></sheetData></worksheet>"#.to_string(),
983
+ ),
984
+ (
985
+ "`<` in a value",
986
+ format!(r#"<worksheet x="a<b">{rows}</worksheet>"#),
987
+ ),
988
+ (
989
+ "unknown entity in a value",
990
+ format!(r#"<worksheet x="&bogus;">{rows}</worksheet>"#),
991
+ ),
992
+ (
993
+ "bare `&` in a value",
994
+ format!(r#"<worksheet x="a & b">{rows}</worksheet>"#),
995
+ ),
996
+ (
997
+ "`]]>` in text",
998
+ format!("<worksheet>{rows}a ]]> b</worksheet>"),
999
+ ),
1000
+ (
1001
+ "second root",
1002
+ format!("<worksheet>{rows}</worksheet><worksheet/>"),
1003
+ ),
1004
+ (
1005
+ "text before the root",
1006
+ format!("x<worksheet>{rows}</worksheet>"),
1007
+ ),
1008
+ (
1009
+ "doctype",
1010
+ format!("<!DOCTYPE worksheet><worksheet>{rows}</worksheet>"),
1011
+ ),
1012
+ (
1013
+ "illegal character in a PI",
1014
+ format!("<?pi \u{1}?><worksheet>{rows}</worksheet>"),
1015
+ ),
1016
+ (
1017
+ "`--` in a comment",
1018
+ format!("<!-- a -- b --><worksheet>{rows}</worksheet>"),
1019
+ ),
1020
+ (
1021
+ "invalid name",
1022
+ format!("<worksheet><1x/>{rows}</worksheet>"),
1023
+ ),
1024
+ (
1025
+ "late declaration",
1026
+ format!(r#"<!-- c --><?xml version="1.0"?><worksheet>{rows}</worksheet>"#),
1027
+ ),
1028
+ ("empty document", String::new()),
1029
+ ("PI in a value", "<worksheet><sheetData><row><c><v>1<? x?></v></c></row></sheetData></worksheet>".to_string()),
1030
+ ("non-ASCII attribute name", "<worksheet><sheetData><row><c \u{d7}=\"1\"/></row></sheetData></worksheet>".to_string()),
1031
+ ("non-ASCII element name", "<worksheet><sheetData><row><c><v>1<\u{d7}/>2</v></c></row></sheetData></worksheet>".to_string()),
1032
+ ("undeclared element prefix", format!("<worksheet><sheetData><x:row>{rows}</x:row></sheetData></worksheet>")),
1033
+ ("empty prefix", "<worksheet><sheetData><:row/></sheetData></worksheet>".to_string()),
1034
+ ("undeclared value prefix", "<worksheet><sheetData><row><c><x:v>1</x:v><v>9</v></c></row></sheetData></worksheet>".to_string()),
1035
+ ] {
1036
+ for mode in [Mode::Document, Mode::Stream] {
1037
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
1038
+ let read: Result<Vec<RawRow>> =
1039
+ std::iter::from_fn(|| reader.next_row().transpose()).collect();
1040
+ assert!(read.is_err(), "{label} ({mode:?})");
1041
+ }
1042
+ }
1043
+ for (label, xml) in [
1044
+ (
1045
+ "prefixed r",
1046
+ r#"<worksheet xmlns:x="urn:x"><sheetData><row><c x:r="B9" r="A1"><v>1</v></c></row></sheetData></worksheet>"#,
1047
+ ),
1048
+ (
1049
+ "prefixed t",
1050
+ r#"<worksheet xmlns:x="urn:x"><sheetData><row><c r="A1" x:t="str"><v>1</v></c></row></sheetData></worksheet>"#,
1051
+ ),
1052
+ ] {
1053
+ assert!(
1054
+ SheetReader::new(xml.as_bytes(), Mode::Document)
1055
+ .next_rows(9)
1056
+ .is_err(),
1057
+ "{label}"
1058
+ );
1059
+ assert!(
1060
+ SheetReader::new(xml.as_bytes(), Mode::Stream)
1061
+ .next_rows(9)
1062
+ .is_ok(),
1063
+ "{label} streams"
1064
+ );
1065
+ }
1066
+ for (label, xml) in [
1067
+ (
1068
+ "comment in a row",
1069
+ "<worksheet><sheetData><row><!-- c --><c r=\"A1\"/></row></sheetData></worksheet>",
1070
+ ),
1071
+ (
1072
+ "text in a row",
1073
+ "<worksheet><sheetData><row>x<c r=\"A1\"/></row></sheetData></worksheet>",
1074
+ ),
1075
+ ] {
1076
+ assert!(
1077
+ SheetReader::new(xml.as_bytes(), Mode::Stream)
1078
+ .next_rows(9)
1079
+ .is_err(),
1080
+ "{label}"
1081
+ );
1082
+ assert!(
1083
+ SheetReader::new(xml.as_bytes(), Mode::Document)
1084
+ .next_rows(9)
1085
+ .is_ok(),
1086
+ "{label} in a document"
1087
+ );
1088
+ }
1089
+ for (label, xml) in [
1090
+ ("nested t", "<worksheet><sheetData><row><c t=\"inlineStr\"><is><t>a<t>b</t>c</t></is></c></row></sheetData></worksheet>"),
1091
+ ("empty prefix namespace", "<worksheet><sheetData><row r=\"1\" xmlns:x=\"\"><x:c r=\"A1\"><v>1</v></x:c></row></sheetData></worksheet>"),
1092
+ ("duplicated attribute", "<worksheet><sheetData><row r=\"1\" r=\"2\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
1093
+ ("unknown entity in skipped content", "<worksheet><sheetData><row><c r=\"A1\"><v>1</v></c></row></sheetData><ext>&bogus;</ext></worksheet>"),
1094
+ ] {
1095
+ for mode in [Mode::Document, Mode::Stream] {
1096
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
1097
+ let read: Result<Vec<RawRow>> = std::iter::from_fn(|| reader.next_row().transpose()).collect();
1098
+ assert!(read.is_err(), "{label} ({mode:?})");
1099
+ }
1100
+ }
1101
+ for (label, xml) in [
1102
+ ("nested row", "<worksheet><sheetData><row r=\"1\"><c r=\"A1\"><ext><row r=\"2\"/></ext></c></row></sheetData></worksheet>"),
1103
+ ("prefixed t in a streamed row", "<worksheet><sheetData><row><c t=\"inlineStr\"><is><x:t xmlns:x=\"urn:other\">hi</x:t></is></c></row></sheetData></worksheet>"),
1104
+ ("preserved whitespace in a row", "<worksheet><sheetData><row r=\"1\" xml:space=\"preserve\"> <c r=\"A1\"/> </row></sheetData></worksheet>"),
1105
+ ] {
1106
+ assert!(SheetReader::new(xml.as_bytes(), Mode::Stream).next_rows(9).is_err(), "{label}");
1107
+ assert!(SheetReader::new(xml.as_bytes(), Mode::Document).next_rows(9).is_ok(), "{label} in a document");
1108
+ }
1109
+ let declared = r#"<x:worksheet xmlns:x="urn:x"><x:sheetData><x:row><x:c r="A1"><x:v>3</x:v></x:c></x:row></x:sheetData></x:worksheet>"#;
1110
+ assert_eq!(
1111
+ SheetReader::new(declared.as_bytes(), Mode::Document)
1112
+ .next_rows(9)
1113
+ .unwrap()
1114
+ .len(),
1115
+ 1
1116
+ );
1117
+ let scoped =
1118
+ r#"<worksheet><a xmlns:x="urn:x"/><sheetData><x:row/></sheetData></worksheet>"#;
1119
+ assert!(
1120
+ SheetReader::new(scoped.as_bytes(), Mode::Document)
1121
+ .next_rows(9)
1122
+ .is_err(),
1123
+ "a prefix out of scope"
1124
+ );
1125
+ let fine = format!(
1126
+ r#"<?xml version="1.0"?>
1127
+ <!-- c --><?pi ok?><worksheet x="a &amp; b" y='1'>{rows}</worksheet>
1128
+ "#
1129
+ );
1130
+ let mut reader = SheetReader::new(fine.as_bytes(), Mode::Document);
1131
+ assert_eq!(reader.next_rows(10).unwrap().len(), 1);
1132
+ }
1133
+
1134
+ #[test]
1135
+ fn returns_the_rows_before_an_error_first() {
1136
+ let xml = format!(
1137
+ "<worksheet {NS}><sheetData><row/><row><c><v>&x;</v></c></row></sheetData></worksheet>"
1138
+ );
1139
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
1140
+ assert_eq!(reader.next_rows(5).unwrap().len(), 1);
1141
+ assert!(reader.next_rows(5).is_err());
1142
+ }
1143
+
1144
+ #[test]
1145
+ fn batches_rows() {
1146
+ let xml = format!("<worksheet {NS}><sheetData><row/><row/><row/></sheetData></worksheet>");
1147
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
1148
+ assert_eq!(reader.next_rows(2).unwrap().len(), 2);
1149
+ assert_eq!(reader.next_rows(2).unwrap().len(), 1);
1150
+ assert!(reader.next_rows(2).unwrap().is_empty());
1151
+
1152
+ let xml = format!(
1153
+ "<worksheet {NS}><sheetData><row><c/><c/><c/></row><row><c/></row></sheetData></worksheet>"
1154
+ );
1155
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
1156
+ assert_eq!(
1157
+ reader.next_rows(2).unwrap().len(),
1158
+ 1,
1159
+ "a batch ends once it holds enough cells"
1160
+ );
1161
+ }
1162
+
1163
+ #[test]
1164
+ fn parses_references_like_roo() {
1165
+ assert_eq!(parse_reference("A1"), Some((1, 1)));
1166
+ assert_eq!(parse_reference("aa10"), Some((10, 27)));
1167
+ assert_eq!(parse_reference("XFD1048576"), Some((1048576, 16384)));
1168
+ assert_eq!(parse_reference("1A"), None);
1169
+ assert_eq!(parse_reference("A"), None);
1170
+ assert_eq!(parse_reference("A1B"), None);
1171
+ assert_eq!(parse_reference("$A$1"), None);
1172
+ }
1173
+
1174
+ #[test]
1175
+ fn parses_only_plain_indexes() {
1176
+ assert_eq!(parse_index("12"), Some(12));
1177
+ assert_eq!(parse_index("007"), Some(7));
1178
+ assert_eq!(parse_index(""), None);
1179
+ assert_eq!(parse_index("+1"), None);
1180
+ assert_eq!(parse_index(" 1"), None);
1181
+ }
1182
+ }