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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +63 -0
- data/Cargo.lock +341 -0
- data/Cargo.toml +7 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/ext/roo_oxide/Cargo.toml +19 -0
- data/ext/roo_oxide/extconf.rb +14 -0
- data/ext/roo_oxide/src/archive.rs +546 -0
- data/ext/roo_oxide/src/convert/cells.rs +516 -0
- data/ext/roo_oxide/src/convert/csv.rs +209 -0
- data/ext/roo_oxide/src/convert.rs +687 -0
- data/ext/roo_oxide/src/lib.rs +306 -0
- data/ext/roo_oxide/src/numeric.rs +174 -0
- data/ext/roo_oxide/src/reader/attributes.rs +199 -0
- data/ext/roo_oxide/src/reader/tests.rs +475 -0
- data/ext/roo_oxide/src/reader.rs +944 -0
- data/ext/roo_oxide/src/shared_strings.rs +684 -0
- data/ext/roo_oxide/src/sheet_parts.rs +312 -0
- data/ext/roo_oxide/src/store/formatters.rs +177 -0
- data/ext/roo_oxide/src/store.rs +894 -0
- data/lib/roo/calamine/version.rb +5 -0
- data/lib/roo/calamine.rb +3 -0
- data/lib/roo/oxide/context.rb +74 -0
- data/lib/roo/oxide/csv.rb +157 -0
- data/lib/roo/oxide/deferred_sheets.rb +284 -0
- data/lib/roo/oxide/excelx.rb +156 -0
- data/lib/roo/oxide/formatters.rb +114 -0
- data/lib/roo/oxide/row_index.rb +93 -0
- data/lib/roo/oxide/row_iteration.rb +33 -0
- data/lib/roo/oxide/shared_strings.rb +46 -0
- data/lib/roo/oxide/sheet.rb +99 -0
- data/lib/roo/oxide/sheet_doc.rb +138 -0
- data/lib/roo/oxide/sheet_parts.rb +78 -0
- data/lib/roo/oxide/version.rb +7 -0
- data/lib/roo/oxide.rb +100 -0
- metadata +110 -0
|
@@ -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=\"s\"/>",
|
|
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&b \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>ab</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="A1"/>"#,
|
|
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 & 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<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
|
+
}
|