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,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"&", '&'),
|
|
652
|
+
(b"<", '<'),
|
|
653
|
+
(b">", '>'),
|
|
654
|
+
(b""", '"'),
|
|
655
|
+
(b"'", '\''),
|
|
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;
|