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,475 @@
1
+ use super::*;
2
+
3
+ const NS: &str = r#"xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main""#;
4
+
5
+ fn rows(xml: &str, mode: Mode) -> Vec<RawRow> {
6
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
7
+ let mut out = Vec::new();
8
+ while let Some(row) = reader.next_row().unwrap() {
9
+ out.push(row);
10
+ }
11
+ out
12
+ }
13
+
14
+ /// `(reference, formula, body)` of every cell, with text resolved.
15
+ fn texts(row: &RawRow) -> Vec<(Reference, Option<&str>, Option<&str>)> {
16
+ row.cells
17
+ .iter()
18
+ .map(|c| {
19
+ let body = match &c.body {
20
+ Body::Empty => None,
21
+ Body::Inline(s) | Body::Value(s) => Some(row.text(s)),
22
+ };
23
+ (
24
+ c.reference.clone(),
25
+ c.formula.as_ref().map(|s| row.text(s)),
26
+ body,
27
+ )
28
+ })
29
+ .collect()
30
+ }
31
+
32
+ fn at(row: i64, column: i64) -> Reference {
33
+ Reference::At { row, column }
34
+ }
35
+
36
+ #[test]
37
+ fn reads_values_formulas_and_inline_strings() {
38
+ let xml = format!(
39
+ r#"<?xml version="1.0" encoding="UTF-8"?><worksheet {NS}><sheetData>
40
+ <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>
41
+ </sheetData></worksheet>"#
42
+ );
43
+ let got = rows(&xml, Mode::Document);
44
+ assert_eq!(got.len(), 1);
45
+ assert_eq!(got[0].cells[0].style, Style::Index(2));
46
+ assert_eq!(got[0].cells[0].kind, Kind::ByFormat);
47
+ assert_eq!(got[0].cells[1].kind, Kind::InlineStr);
48
+ assert!(matches!(got[0].cells[1].body, Body::Inline(_)));
49
+ assert_eq!(
50
+ texts(&got[0]),
51
+ vec![
52
+ (at(1, 1), Some("B1+1"), Some("1.0")),
53
+ (at(1, 2), None, Some("ab"))
54
+ ]
55
+ );
56
+ }
57
+
58
+ #[test]
59
+ fn keeps_attributes_ruby_has_to_read() {
60
+ let xml = format!(
61
+ r#"<worksheet {NS}><sheetData><row><c r="1A" s=" 3" t="e"/><c/></row></sheetData></worksheet>"#
62
+ );
63
+ let got = rows(&xml, Mode::Document);
64
+ let cells = &got[0].cells;
65
+ assert_eq!(cells[0].reference, Reference::Invalid("1A".into()));
66
+ assert_eq!(cells[0].style, Style::Other(" 3".into()));
67
+ assert_eq!(cells[0].kind, Kind::ByFormat);
68
+ assert_eq!(cells[1].reference, Reference::Missing);
69
+ assert_eq!(cells[1].style, Style::Index(0));
70
+
71
+ let many: String = (0..12).map(|i| format!(" a{i}=\"\"")).collect();
72
+ let xml = format!(
73
+ "<worksheet {NS}><sheetData><row><c{many}\n r = 'B2'\ts=\"4\" t='s'/></row></sheetData></worksheet>"
74
+ );
75
+ let cell = &rows(&xml, Mode::Document)[0].cells[0];
76
+ assert_eq!(cell.reference, at(2, 2));
77
+ assert_eq!(cell.style, Style::Index(4));
78
+ assert_eq!(cell.kind, Kind::Shared);
79
+
80
+ for tag in [
81
+ "<c r=\"B2\" s=\"4\"\tt='s'/>",
82
+ "<c r=\"B2\" s=\"4\" t=\"s\" />",
83
+ "<c r=\"B2\" s=\"4\" t = \"s\"/>",
84
+ "<c r=\"B2\" s=\"4\" t=\"&#115;\"/>",
85
+ "<c r=\"B2\" s=\"4\" x=\"\" t=\"s\"/>",
86
+ ] {
87
+ let xml = format!("<worksheet {NS}><sheetData><row>{tag}</row></sheetData></worksheet>");
88
+ let cell = &rows(&xml, Mode::Document)[0].cells[0];
89
+ assert_eq!(
90
+ (&cell.reference, &cell.style, cell.kind),
91
+ (&at(2, 2), &Style::Index(4), Kind::Shared),
92
+ "{tag}"
93
+ );
94
+ }
95
+ }
96
+
97
+ #[test]
98
+ fn resolves_references_and_normalizes_line_ends() {
99
+ let xml = format!(
100
+ "<worksheet {NS}><sheetData><row><c r=\"A1\" t=\"str\"><v>a&amp;b&#13;\r\nc<![CDATA[<d>]]></v></c></row></sheetData></worksheet>"
101
+ );
102
+ let got = rows(&xml, Mode::Document);
103
+ assert_eq!(texts(&got[0])[0].2, Some("a&b\r\nc<d>"));
104
+ }
105
+
106
+ #[test]
107
+ fn first_value_wins_and_formula_after_it_is_ignored() {
108
+ let xml = format!(
109
+ "<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>"
110
+ );
111
+ let got = rows(&xml, Mode::Document);
112
+ assert_eq!(
113
+ texts(&got[0]),
114
+ vec![(at(1, 1), None, Some("1")), (at(1, 2), None, None)]
115
+ );
116
+ }
117
+
118
+ #[test]
119
+ fn empty_inline_string_does_not_decide_the_value() {
120
+ let xml = format!(
121
+ "<worksheet {NS}><sheetData><row><c r=\"A1\" t=\"inlineStr\"><is><t></t></is><v>7</v></c></row></sheetData></worksheet>"
122
+ );
123
+ let got = rows(&xml, Mode::Document);
124
+ assert!(matches!(got[0].cells[0].body, Body::Value(_)));
125
+ assert_eq!(texts(&got[0])[0].2, Some("7"));
126
+ }
127
+
128
+ #[test]
129
+ fn document_mode_reads_only_sheet_data_rows_and_c_children() {
130
+ let xml = format!(
131
+ "<worksheet {NS}><sheetPr><row/></sheetPr><sheetData><row r=\"1\"/><row r=\"2\"><x>1</x><c r=\"A2\"/></row></sheetData></worksheet>"
132
+ );
133
+ let got = rows(&xml, Mode::Document);
134
+ assert_eq!(got.len(), 2);
135
+ assert!(got[0].cells.is_empty());
136
+ assert_eq!(texts(&got[1]), vec![(at(2, 1), None, None)]);
137
+ }
138
+
139
+ #[test]
140
+ fn document_mode_matches_prefixed_names_by_local_name() {
141
+ 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>"#;
142
+ let got = rows(xml, Mode::Document);
143
+ assert_eq!(texts(&got[0])[0].2, Some("3"));
144
+ }
145
+
146
+ #[test]
147
+ fn stream_mode_needs_an_unprefixed_row_and_takes_every_child_element() {
148
+ let prefixed = r#"<x:worksheet xmlns:x="urn:x"><x:sheetData><x:row><x:c r="A1"/></x:row></x:sheetData></x:worksheet>"#;
149
+ assert!(rows(prefixed, Mode::Stream).is_empty());
150
+
151
+ let xml = format!(
152
+ "<worksheet {NS}><sheetData><row><c r=\"A1\"/><extra r=\"B1\"><v>1</v></extra></row></sheetData></worksheet>"
153
+ );
154
+ let got = rows(&xml, Mode::Stream);
155
+ assert_eq!(
156
+ texts(&got[0]),
157
+ vec![(at(1, 1), None, None), (at(1, 2), None, Some("1"))]
158
+ );
159
+ }
160
+
161
+ #[test]
162
+ fn rejects_inputs_it_cannot_reproduce() {
163
+ let latin1 = r#"<?xml version="1.0" encoding="ISO-8859-1"?><worksheet/>"#;
164
+ assert!(SheetReader::new(latin1.as_bytes(), Mode::Document)
165
+ .next_row()
166
+ .is_err());
167
+ for body in [
168
+ "<v>&custom;</v>",
169
+ "<v>a&#1;b</v>",
170
+ "<v>a\u{1}b</v>",
171
+ "<v>\u{FFFE}</v>",
172
+ ] {
173
+ let xml =
174
+ format!("<worksheet {NS}><sheetData><row><c>{body}</c></row></sheetData></worksheet>");
175
+ assert!(
176
+ SheetReader::new(xml.as_bytes(), Mode::Document)
177
+ .next_row()
178
+ .is_err(),
179
+ "{body:?}"
180
+ );
181
+ }
182
+ for tag in [
183
+ r#"<c r="A&#2;1"/>"#,
184
+ "<c r=\"A\u{1}1\"/>",
185
+ "<c t=\"\u{FFFE}\"/>",
186
+ ] {
187
+ let xml = format!("<worksheet {NS}><sheetData><row>{tag}</row></sheetData></worksheet>");
188
+ assert!(
189
+ SheetReader::new(xml.as_bytes(), Mode::Document)
190
+ .next_row()
191
+ .is_err(),
192
+ "{tag:?}"
193
+ );
194
+ }
195
+ let truncated = format!("<worksheet {NS}><sheetData><row><c><v>1</v></c></row>");
196
+ let mut reader = SheetReader::new(truncated.as_bytes(), Mode::Document);
197
+ assert!(reader.next_row().unwrap().is_some());
198
+ assert!(
199
+ reader.next_row().is_err(),
200
+ "a sheet that ends with open elements"
201
+ );
202
+ }
203
+
204
+ #[test]
205
+ fn rejects_xml_that_is_not_well_formed() {
206
+ let rows = r#"<sheetData><row r="1"><c r="A1"><v>1</v></c></row></sheetData>"#;
207
+ for (label, xml) in [
208
+ (
209
+ "unquoted value",
210
+ "<worksheet><sheetData><row r=1/></sheetData></worksheet>".to_string(),
211
+ ),
212
+ (
213
+ "no space between attributes",
214
+ r#"<worksheet><sheetData><row r="1"s="2"/></sheetData></worksheet>"#.to_string(),
215
+ ),
216
+ (
217
+ "`<` in a value",
218
+ format!(r#"<worksheet x="a<b">{rows}</worksheet>"#),
219
+ ),
220
+ (
221
+ "unknown entity in a value",
222
+ format!(r#"<worksheet x="&bogus;">{rows}</worksheet>"#),
223
+ ),
224
+ (
225
+ "unknown entity in a row's `r`",
226
+ r#"<worksheet><sheetData><row r="&bogus;"/></sheetData></worksheet>"#.to_string(),
227
+ ),
228
+ (
229
+ "no space before a cell's attribute",
230
+ r#"<worksheet><sheetData><row><c r="A1"xs="2"/></row></sheetData></worksheet>"#
231
+ .to_string(),
232
+ ),
233
+ (
234
+ "`<` in a cell's value",
235
+ r#"<worksheet><sheetData><row><c r="A<1"/></row></sheetData></worksheet>"#.to_string(),
236
+ ),
237
+ (
238
+ "bare `&` in a value",
239
+ format!(r#"<worksheet x="a & b">{rows}</worksheet>"#),
240
+ ),
241
+ (
242
+ "`]]>` in text",
243
+ format!("<worksheet>{rows}a ]]> b</worksheet>"),
244
+ ),
245
+ (
246
+ "second root",
247
+ format!("<worksheet>{rows}</worksheet><worksheet/>"),
248
+ ),
249
+ (
250
+ "text before the root",
251
+ format!("x<worksheet>{rows}</worksheet>"),
252
+ ),
253
+ (
254
+ "doctype",
255
+ format!("<!DOCTYPE worksheet><worksheet>{rows}</worksheet>"),
256
+ ),
257
+ (
258
+ "illegal character in a PI",
259
+ format!("<?pi \u{1}?><worksheet>{rows}</worksheet>"),
260
+ ),
261
+ (
262
+ "`--` in a comment",
263
+ format!("<!-- a -- b --><worksheet>{rows}</worksheet>"),
264
+ ),
265
+ (
266
+ "invalid name",
267
+ format!("<worksheet><1x/>{rows}</worksheet>"),
268
+ ),
269
+ (
270
+ "late declaration",
271
+ format!(r#"<!-- c --><?xml version="1.0"?><worksheet>{rows}</worksheet>"#),
272
+ ),
273
+ ("empty document", String::new()),
274
+ (
275
+ "PI in a value",
276
+ "<worksheet><sheetData><row><c><v>1<? x?></v></c></row></sheetData></worksheet>"
277
+ .to_string(),
278
+ ),
279
+ (
280
+ "non-ASCII attribute name",
281
+ "<worksheet><sheetData><row><c \u{d7}=\"1\"/></row></sheetData></worksheet>"
282
+ .to_string(),
283
+ ),
284
+ (
285
+ "non-ASCII element name",
286
+ "<worksheet><sheetData><row><c><v>1<\u{d7}/>2</v></c></row></sheetData></worksheet>"
287
+ .to_string(),
288
+ ),
289
+ (
290
+ "undeclared element prefix",
291
+ format!("<worksheet><sheetData><x:row>{rows}</x:row></sheetData></worksheet>"),
292
+ ),
293
+ (
294
+ "empty prefix",
295
+ "<worksheet><sheetData><:row/></sheetData></worksheet>".to_string(),
296
+ ),
297
+ (
298
+ "undeclared value prefix",
299
+ "<worksheet><sheetData><row><c><x:v>1</x:v><v>9</v></c></row></sheetData></worksheet>"
300
+ .to_string(),
301
+ ),
302
+ ] {
303
+ for mode in [Mode::Document, Mode::Stream] {
304
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
305
+ let read: Result<Vec<RawRow>> =
306
+ std::iter::from_fn(|| reader.next_row().transpose()).collect();
307
+ assert!(read.is_err(), "{label} ({mode:?})");
308
+ }
309
+ }
310
+ for (label, xml) in [
311
+ (
312
+ "prefixed r",
313
+ r#"<worksheet xmlns:x="urn:x"><sheetData><row><c x:r="B9" r="A1"><v>1</v></c></row></sheetData></worksheet>"#,
314
+ ),
315
+ (
316
+ "prefixed t",
317
+ r#"<worksheet xmlns:x="urn:x"><sheetData><row><c r="A1" x:t="str"><v>1</v></c></row></sheetData></worksheet>"#,
318
+ ),
319
+ ] {
320
+ assert!(
321
+ SheetReader::new(xml.as_bytes(), Mode::Document)
322
+ .next_rows(9)
323
+ .is_err(),
324
+ "{label}"
325
+ );
326
+ assert!(
327
+ SheetReader::new(xml.as_bytes(), Mode::Stream)
328
+ .next_rows(9)
329
+ .is_ok(),
330
+ "{label} streams"
331
+ );
332
+ }
333
+ for (label, xml) in [
334
+ (
335
+ "comment in a row",
336
+ "<worksheet><sheetData><row><!-- c --><c r=\"A1\"/></row></sheetData></worksheet>",
337
+ ),
338
+ (
339
+ "text in a row",
340
+ "<worksheet><sheetData><row>x<c r=\"A1\"/></row></sheetData></worksheet>",
341
+ ),
342
+ ] {
343
+ assert!(
344
+ SheetReader::new(xml.as_bytes(), Mode::Stream)
345
+ .next_rows(9)
346
+ .is_err(),
347
+ "{label}"
348
+ );
349
+ assert!(
350
+ SheetReader::new(xml.as_bytes(), Mode::Document)
351
+ .next_rows(9)
352
+ .is_ok(),
353
+ "{label} in a document"
354
+ );
355
+ }
356
+ for (label, xml) in [
357
+ ("nested t", "<worksheet><sheetData><row><c t=\"inlineStr\"><is><t>a<t>b</t>c</t></is></c></row></sheetData></worksheet>"),
358
+ ("empty prefix namespace", "<worksheet><sheetData><row r=\"1\" xmlns:x=\"\"><x:c r=\"A1\"><v>1</v></x:c></row></sheetData></worksheet>"),
359
+ ("duplicated attribute", "<worksheet><sheetData><row r=\"1\" r=\"2\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
360
+ ("duplicated attribute among many", "<worksheet><sheetData><row a1=\"\" a2=\"\" a3=\"\" a4=\"\" a5=\"\" a6=\"\" a7=\"\" a8=\"\" a9=\"\" a2=\"\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
361
+ ("attribute without a value", "<worksheet><sheetData><row r><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
362
+ ("attribute without `=`", "<worksheet><sheetData><row r \"1\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
363
+ ("attribute without a name", "<worksheet><sheetData><row =\"1\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
364
+ ("`/` before the end of a tag", "<worksheet><sheetData><row r=\"1\" / ><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
365
+ ("attribute name with two prefixes", "<worksheet xmlns:a=\"urn:a\"><sheetData><row a:b:c=\"1\"><c r=\"A1\"><v>1</v></c></row></sheetData></worksheet>"),
366
+ ("unknown entity in skipped content", "<worksheet><sheetData><row><c r=\"A1\"><v>1</v></c></row></sheetData><ext>&bogus;</ext></worksheet>"),
367
+ ] {
368
+ for mode in [Mode::Document, Mode::Stream] {
369
+ let mut reader = SheetReader::new(xml.as_bytes(), mode);
370
+ let read: Result<Vec<RawRow>> = std::iter::from_fn(|| reader.next_row().transpose()).collect();
371
+ assert!(read.is_err(), "{label} ({mode:?})");
372
+ }
373
+ }
374
+ for (label, xml) in [
375
+ ("nested row", "<worksheet><sheetData><row r=\"1\"><c r=\"A1\"><ext><row r=\"2\"/></ext></c></row></sheetData></worksheet>"),
376
+ ("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>"),
377
+ ("preserved whitespace in a row", "<worksheet><sheetData><row r=\"1\" xml:space=\"preserve\"> <c r=\"A1\"/> </row></sheetData></worksheet>"),
378
+ ] {
379
+ assert!(SheetReader::new(xml.as_bytes(), Mode::Stream).next_rows(9).is_err(), "{label}");
380
+ assert!(SheetReader::new(xml.as_bytes(), Mode::Document).next_rows(9).is_ok(), "{label} in a document");
381
+ }
382
+ 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>"#;
383
+ assert_eq!(
384
+ SheetReader::new(declared.as_bytes(), Mode::Document)
385
+ .next_rows(9)
386
+ .unwrap()
387
+ .len(),
388
+ 1
389
+ );
390
+ let scoped = r#"<worksheet><a xmlns:x="urn:x"/><sheetData><x:row/></sheetData></worksheet>"#;
391
+ assert!(
392
+ SheetReader::new(scoped.as_bytes(), Mode::Document)
393
+ .next_rows(9)
394
+ .is_err(),
395
+ "a prefix out of scope"
396
+ );
397
+ let fine = format!(
398
+ r#"<?xml version="1.0"?>
399
+ <!-- c --><?pi ok?><worksheet x="a &amp; b" y='1'>{rows}</worksheet>
400
+ "#
401
+ );
402
+ let mut reader = SheetReader::new(fine.as_bytes(), Mode::Document);
403
+ assert_eq!(reader.next_rows(10).unwrap().len(), 1);
404
+ }
405
+
406
+ #[test]
407
+ fn returns_the_rows_before_an_error_first() {
408
+ let xml = format!(
409
+ "<worksheet {NS}><sheetData><row/><row><c><v>&x;</v></c></row></sheetData></worksheet>"
410
+ );
411
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
412
+ assert_eq!(reader.next_rows(5).unwrap().len(), 1);
413
+ assert!(reader.next_rows(5).is_err());
414
+ }
415
+
416
+ #[test]
417
+ fn batches_rows() {
418
+ let xml = format!("<worksheet {NS}><sheetData><row/><row/><row/></sheetData></worksheet>");
419
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
420
+ assert_eq!(reader.next_rows(2).unwrap().len(), 2);
421
+ assert_eq!(reader.next_rows(2).unwrap().len(), 1);
422
+ assert!(reader.next_rows(2).unwrap().is_empty());
423
+
424
+ let xml = format!(
425
+ "<worksheet {NS}><sheetData><row><c/><c/><c/></row><row><c/></row></sheetData></worksheet>"
426
+ );
427
+ let mut reader = SheetReader::new(xml.as_bytes(), Mode::Document);
428
+ assert_eq!(
429
+ reader.next_rows(2).unwrap().len(),
430
+ 1,
431
+ "a batch ends once it holds enough cells"
432
+ );
433
+ }
434
+
435
+ #[test]
436
+ fn parses_references_like_roo() {
437
+ assert_eq!(parse_reference("A1"), Some((1, 1)));
438
+ assert_eq!(parse_reference("aa10"), Some((10, 27)));
439
+ assert_eq!(parse_reference("XFD1048576"), Some((1048576, 16384)));
440
+ assert_eq!(parse_reference("1A"), None);
441
+ assert_eq!(parse_reference("A"), None);
442
+ assert_eq!(parse_reference("A1B"), None);
443
+ assert_eq!(parse_reference("$A$1"), None);
444
+ }
445
+
446
+ #[test]
447
+ fn parses_only_plain_indexes() {
448
+ assert_eq!(parse_index("12"), Some(12));
449
+ assert_eq!(parse_index("007"), Some(7));
450
+ assert_eq!(parse_index(""), None);
451
+ assert_eq!(parse_index("+1"), None);
452
+ assert_eq!(parse_index(" 1"), None);
453
+ }
454
+
455
+ #[test]
456
+ fn reads_cells_across_buffer_and_parser_boundaries() {
457
+ let xml = "<worksheet><sheetData><row><c r=\"A1\" t=\"inlineStr\"><is><t>part<![CDATA[extra]]></t></is></c><c r=\"B1\" ><f>A1&lt;2</f><v>2</v></c>\n <c r=\"C1\"><v>3</v></c>\n </row></sheetData></worksheet>";
458
+ for mode in [Mode::Document, Mode::Stream] {
459
+ for capacity in 1..=xml.len() {
460
+ let mut reader =
461
+ SheetReader::new(BufReader::with_capacity(capacity, xml.as_bytes()), mode);
462
+ let row = reader.next_row().unwrap().unwrap();
463
+ assert_eq!(
464
+ texts(&row),
465
+ [
466
+ (at(1, 1), None, Some("partextra")),
467
+ (at(1, 2), Some("A1<2"), Some("2")),
468
+ (at(1, 3), None, Some("3")),
469
+ ],
470
+ "{mode:?}, buffer capacity {capacity}"
471
+ );
472
+ assert_eq!(reader.next_row(), Ok(None));
473
+ }
474
+ }
475
+ }