imprimatur 0.1.0 → 0.2.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Cargo.lock +4 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +27 -5
  6. data/ext/imprimatur_native/cli/Cargo.toml +2 -2
  7. data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
  8. data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
  9. data/ext/imprimatur_native/cli/src/cli.rs +5 -5
  10. data/ext/imprimatur_native/cli/src/main.rs +1 -1
  11. data/ext/imprimatur_native/core/Cargo.toml +7 -2
  12. data/ext/imprimatur_native/core/README.md +6 -1
  13. data/ext/imprimatur_native/core/src/archive.rs +292 -0
  14. data/ext/imprimatur_native/core/src/error.rs +1 -1
  15. data/ext/imprimatur_native/core/src/lib.rs +2 -0
  16. data/ext/imprimatur_native/core/src/read/content.rs +16 -1
  17. data/ext/imprimatur_native/core/src/read/json.rs +3 -1
  18. data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
  19. data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +4 -3
  20. data/ext/imprimatur_native/core/src/read/ods.rs +357 -0
  21. data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
  22. data/ext/imprimatur_native/core/src/read/package.rs +0 -5
  23. data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
  24. data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
  25. data/ext/imprimatur_native/core/src/read/xlsx.rs +1 -2
  26. data/ext/imprimatur_native/core/src/read.rs +64 -20
  27. data/ext/imprimatur_native/core/src/test_support.rs +87 -0
  28. data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
  29. data/ext/imprimatur_native/ffi/src/archive.rs +121 -0
  30. data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
  31. data/ext/imprimatur_native/ffi/src/lib.rs +14 -0
  32. data/ext/imprimatur_native/ffi/src/read.rs +1 -3
  33. data/lib/imprimatur/error.rb +4 -3
  34. data/lib/imprimatur/reader.rb +1 -1
  35. data/lib/imprimatur/version.rb +1 -1
  36. data/lib/imprimatur/zip.rb +127 -0
  37. data/lib/imprimatur.rb +3 -2
  38. metadata +13 -4
@@ -0,0 +1,156 @@
1
+ //! Workbook strings: BIFF8's Latin-1 or UTF-16, BIFF5's Windows-1252
2
+ //! bytes, and BIFF8's shared string table, whose strings may be split
3
+ //! across `CONTINUE` records.
4
+
5
+ use crate::Result;
6
+ use crate::pdf::metrics::WIN_ANSI_HIGH;
7
+ use crate::read::Budget;
8
+
9
+ /// How a workbook's strings are stored.
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub enum Encoding {
12
+ /// BIFF8: each string says whether it is Latin-1 or UTF-16.
13
+ Unicode,
14
+ /// BIFF5: bytes in the workbook's code page.
15
+ Windows1252,
16
+ }
17
+
18
+ /// A string with a 1-byte or 2-byte character count at `at`; `None` past
19
+ /// the end.
20
+ pub fn string(bytes: &[u8], at: usize, wide_count: bool, encoding: Encoding) -> Option<String> {
21
+ let (count, at) = if wide_count {
22
+ (usize::from(u16::from_le_bytes(bytes.get(at..at + 2)?.try_into().ok()?)), at + 2)
23
+ } else {
24
+ (usize::from(*bytes.get(at)?), at + 1)
25
+ };
26
+ if encoding == Encoding::Windows1252 {
27
+ let chars = bytes.get(at..at + count).or_else(|| bytes.get(at..))?;
28
+ return Some(chars.iter().map(|&byte| windows_1252(byte)).collect());
29
+ }
30
+ let high = bytes.get(at)? & 1 != 0;
31
+ let mut out = String::with_capacity(count);
32
+ let mut units = Vec::new();
33
+ let width = if high { 2 } else { 1 };
34
+ let chars = bytes.get(at + 1..at + 1 + count * width).or_else(|| bytes.get(at + 1..))?;
35
+ decode(chars, high, &mut units, &mut out);
36
+ flush(&mut units, &mut out);
37
+ Some(out)
38
+ }
39
+
40
+ /// A Windows-1252 byte: Latin-1, but for the typographic characters at
41
+ /// 0x80 to 0x9F.
42
+ fn windows_1252(byte: u8) -> char {
43
+ match byte {
44
+ 0x80..=0x9F => WIN_ANSI_HIGH[usize::from(byte - 0x80)].unwrap_or_else(|| char::from(byte)),
45
+ _ => char::from(byte),
46
+ }
47
+ }
48
+
49
+ /// Appends characters: Latin-1 bytes at once, UTF-16 units to `units`
50
+ /// (a surrogate pair may span two pieces), decoded by [`flush`].
51
+ fn decode(chars: &[u8], high: bool, units: &mut Vec<u16>, out: &mut String) {
52
+ if high {
53
+ units.extend(chars.as_chunks::<2>().0.iter().map(|unit| u16::from_le_bytes(*unit)));
54
+ } else {
55
+ flush(units, out);
56
+ out.extend(chars.iter().map(|&byte| char::from(byte)));
57
+ }
58
+ }
59
+
60
+ fn flush(units: &mut Vec<u16>, out: &mut String) {
61
+ out.extend(
62
+ char::decode_utf16(units.drain(..)).map(|ch| ch.unwrap_or(char::REPLACEMENT_CHARACTER)),
63
+ );
64
+ }
65
+
66
+ /// The shared string table (`SST`): its record's body, then each
67
+ /// `CONTINUE` body after it.
68
+ pub fn shared(parts: &[&[u8]], budget: &Budget<'_>) -> Result<Vec<String>> {
69
+ let mut reader = Cursor { parts, part: 0, at: 8 };
70
+ let Some(unique) = parts.first().and_then(|first| first.get(4..8)) else {
71
+ return Ok(Vec::new());
72
+ };
73
+ let unique = u32::from_le_bytes(unique.try_into().unwrap_or([0; 4])) as usize;
74
+ let mut strings = Vec::with_capacity(unique.min(1 << 20));
75
+ let mut units = Vec::new();
76
+ while strings.len() < unique {
77
+ budget.tick()?;
78
+ let Some(string) = reader.string(&mut units) else { break };
79
+ strings.push(string);
80
+ }
81
+ Ok(strings)
82
+ }
83
+
84
+ /// A cursor over record bodies read as one stream.
85
+ struct Cursor<'p, 'a> {
86
+ parts: &'p [&'a [u8]],
87
+ part: usize,
88
+ at: usize,
89
+ }
90
+
91
+ impl Cursor<'_, '_> {
92
+ fn byte(&mut self) -> Option<u8> {
93
+ while self.at >= self.parts.get(self.part)?.len() {
94
+ self.part += 1;
95
+ self.at = 0;
96
+ }
97
+ let byte = self.parts[self.part][self.at];
98
+ self.at += 1;
99
+ Some(byte)
100
+ }
101
+
102
+ fn u16(&mut self) -> Option<u16> {
103
+ Some(u16::from_le_bytes([self.byte()?, self.byte()?]))
104
+ }
105
+
106
+ fn u32(&mut self) -> Option<u32> {
107
+ Some(u32::from_le_bytes([self.byte()?, self.byte()?, self.byte()?, self.byte()?]))
108
+ }
109
+
110
+ fn skip(&mut self, mut count: usize) -> Option<()> {
111
+ while count > 0 {
112
+ let left = self.parts.get(self.part)?.len().saturating_sub(self.at);
113
+ if left == 0 {
114
+ self.part += 1;
115
+ self.at = 0;
116
+ continue;
117
+ }
118
+ let step = left.min(count);
119
+ self.at += step;
120
+ count -= step;
121
+ }
122
+ Some(())
123
+ }
124
+
125
+ /// An `XLUnicodeRichExtendedString`: where its characters cross into the
126
+ /// next record, that record starts with their width again.
127
+ fn string(&mut self, units: &mut Vec<u16>) -> Option<String> {
128
+ let count = usize::from(self.u16()?);
129
+ let flags = self.byte()?;
130
+ let runs = if flags & 0x08 != 0 { usize::from(self.u16()?) } else { 0 };
131
+ let extended = if flags & 0x04 != 0 { self.u32()? as usize } else { 0 };
132
+ let mut high = flags & 1 != 0;
133
+ let mut out = String::with_capacity(count);
134
+ let mut left = count;
135
+ while left > 0 {
136
+ let part = self.parts.get(self.part)?;
137
+ if self.at >= part.len() {
138
+ self.part += 1;
139
+ self.at = 1;
140
+ high = self.parts.get(self.part)?.first()? & 1 != 0;
141
+ continue;
142
+ }
143
+ let width = if high { 2 } else { 1 };
144
+ let take = ((part.len() - self.at) / width).min(left);
145
+ if take == 0 {
146
+ return None;
147
+ }
148
+ decode(&part[self.at..self.at + take * width], high, units, &mut out);
149
+ self.at += take * width;
150
+ left -= take;
151
+ }
152
+ flush(units, &mut out);
153
+ self.skip(runs * 4 + extended)?;
154
+ Some(out)
155
+ }
156
+ }
@@ -0,0 +1,342 @@
1
+ //! Legacy Excel workbooks (`.xls`): BIFF8 (Excel 97 to 2003) and BIFF5
2
+ //! (Excel 5.0 and 95), the `Workbook` or `Book` stream of a compound file,
3
+ //! a sequence of records. Cell values come from the cells' records (shared
4
+ //! or inline strings, numbers, RK numbers, booleans, errors, formula
5
+ //! results) and show through their number formats as in XLSX. Comments,
6
+ //! charts and macro sheets are left out.
7
+
8
+ mod strings;
9
+ #[cfg(test)]
10
+ mod tests;
11
+
12
+ use super::Budget;
13
+ use super::content::{Grid, Sheet, Value};
14
+ use super::numbers::{Shown, built_in, classify, push_number};
15
+ use super::ole::Ole;
16
+ use crate::{Error, Result};
17
+ use strings::Encoding;
18
+
19
+ const BOF: u16 = 0x0809;
20
+ const EOF: u16 = 0x000A;
21
+ const CONTINUE: u16 = 0x003C;
22
+ /// BIFF8, the version of Excel 97 and later; BIFF5, of Excel 5.0 and 95.
23
+ const BIFF8: u16 = 0x0600;
24
+ const BIFF5: u16 = 0x0500;
25
+
26
+ fn malformed(reason: &str) -> Error {
27
+ Error::malformed("XLS", reason)
28
+ }
29
+
30
+ /// The workbook's sheets, in order, its chart and macro sheets left out.
31
+ ///
32
+ /// # Errors
33
+ /// [`Error::UnsupportedDocument`] for a compound file that is no workbook,
34
+ /// one older than Excel 5.0 or in a code page other than Western European,
35
+ /// [`Error::PasswordRequired`] for an encrypted one, [`Error::Malformed`]
36
+ /// for a damaged one.
37
+ pub fn content(ole: &Ole<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
38
+ let stream = match ole.stream("Workbook", budget)? {
39
+ Some(stream) => stream,
40
+ None => ole.stream("Book", budget)?.ok_or_else(|| {
41
+ Error::UnsupportedDocument(
42
+ "an Office file that is not a workbook (a legacy .doc?)".to_owned(),
43
+ )
44
+ })?,
45
+ };
46
+ let book = globals(&stream, budget)?;
47
+ let mut scratch = String::new();
48
+ let mut sheets = Vec::with_capacity(book.sheets.len());
49
+ for listed in &book.sheets {
50
+ let cells = cells(&stream, listed.offset, &book, &mut scratch, budget)?;
51
+ sheets.push(Sheet {
52
+ name: listed.name.clone(),
53
+ hidden: listed.hidden,
54
+ cells,
55
+ comments: Vec::new(),
56
+ });
57
+ }
58
+ Ok(sheets)
59
+ }
60
+
61
+ /// One record: its type and its body, `CONTINUE` records left apart.
62
+ fn records(data: &[u8], mut at: usize) -> impl Iterator<Item = (u16, &[u8])> {
63
+ std::iter::from_fn(move || {
64
+ let header = data.get(at..at + 4)?;
65
+ let kind = u16::from_le_bytes([header[0], header[1]]);
66
+ let length = usize::from(u16::from_le_bytes([header[2], header[3]]));
67
+ let body = data.get(at + 4..at + 4 + length)?;
68
+ at += 4 + length;
69
+ Some((kind, body))
70
+ })
71
+ }
72
+
73
+ fn u16_at(body: &[u8], at: usize) -> Option<u16> {
74
+ Some(u16::from_le_bytes(body.get(at..at + 2)?.try_into().ok()?))
75
+ }
76
+
77
+ fn u32_at(body: &[u8], at: usize) -> Option<u32> {
78
+ Some(u32::from_le_bytes(body.get(at..at + 4)?.try_into().ok()?))
79
+ }
80
+
81
+ /// A worksheet the workbook lists.
82
+ struct Listed {
83
+ name: String,
84
+ hidden: bool,
85
+ offset: usize,
86
+ }
87
+
88
+ /// What the workbook's global records say: its sheets, its shared strings,
89
+ /// how each cell format shows numbers, and its date system.
90
+ struct Book {
91
+ sheets: Vec<Listed>,
92
+ strings: Vec<String>,
93
+ shown: Vec<Shown>,
94
+ date1904: bool,
95
+ encoding: Encoding,
96
+ }
97
+
98
+ impl Book {
99
+ /// How the cell format `xf` shows numbers.
100
+ fn shown(&self, xf: Option<u16>) -> Shown {
101
+ xf.and_then(|xf| self.shown.get(usize::from(xf))).copied().unwrap_or(Shown::General)
102
+ }
103
+
104
+ /// A string in the workbook's encoding.
105
+ fn string(&self, bytes: &[u8], at: usize, wide_count: bool) -> String {
106
+ strings::string(bytes, at, wide_count, self.encoding).unwrap_or_default()
107
+ }
108
+ }
109
+
110
+ fn globals(stream: &[u8], budget: &Budget<'_>) -> Result<Book> {
111
+ let mut all = records(stream, 0).peekable();
112
+ let encoding = match all.next() {
113
+ Some((BOF, body)) if u16_at(body, 0) == Some(BIFF8) => Encoding::Unicode,
114
+ Some((BOF, body)) if u16_at(body, 0) == Some(BIFF5) => Encoding::Windows1252,
115
+ Some((BOF, _)) => {
116
+ return Err(Error::UnsupportedDocument(
117
+ "a workbook older than Excel 5.0 (.xls); save it as .xlsx".into(),
118
+ ));
119
+ }
120
+ _ => return Err(malformed("the workbook stream does not start with BOF")),
121
+ };
122
+ let mut book = Book {
123
+ sheets: Vec::new(),
124
+ strings: Vec::new(),
125
+ shown: Vec::new(),
126
+ date1904: false,
127
+ encoding,
128
+ };
129
+ let mut formats: Vec<(u16, Shown)> = Vec::new();
130
+ let mut format_ids = Vec::new();
131
+ while let Some((kind, body)) = all.next() {
132
+ budget.tick()?;
133
+ match kind {
134
+ EOF => break,
135
+ // FILEPASS: the rest is encrypted.
136
+ 0x002F => return Err(Error::PasswordRequired),
137
+ 0x0022 => book.date1904 = u16_at(body, 0) == Some(1),
138
+ // CODEPAGE: BIFF5's strings are bytes of it; Western European
139
+ // (or ASCII) only, rather than misread text.
140
+ 0x0042 if encoding == Encoding::Windows1252 => {
141
+ let page = u16_at(body, 0).unwrap_or(1252);
142
+ if !matches!(page, 1252 | 367 | 0x8001) {
143
+ return Err(Error::UnsupportedDocument(format!(
144
+ "an Excel 5.0/95 workbook in code page {page} (.xls); save it as .xlsx"
145
+ )));
146
+ }
147
+ }
148
+ 0x041E => {
149
+ if let Some(id) = u16_at(body, 0) {
150
+ let code = book.string(body, 2, encoding == Encoding::Unicode);
151
+ formats.push((id, classify(&code)));
152
+ }
153
+ }
154
+ 0x00E0 => format_ids.push(u16_at(body, 2).unwrap_or(0)),
155
+ 0x0085 => {
156
+ let (Some(offset), Some(&state), Some(&kind)) =
157
+ (u32_at(body, 0), body.get(4), body.get(5))
158
+ else {
159
+ continue;
160
+ };
161
+ // Worksheets only: macro sheets, charts and modules hold no table.
162
+ if kind == 0 {
163
+ let name = book.string(body, 6, false);
164
+ book.sheets.push(Listed {
165
+ name,
166
+ hidden: state & 3 != 0,
167
+ offset: offset as usize,
168
+ });
169
+ }
170
+ }
171
+ 0x00FC => {
172
+ let mut parts = vec![body];
173
+ while let Some(&(CONTINUE, more)) = all.peek() {
174
+ parts.push(more);
175
+ all.next();
176
+ }
177
+ book.strings = strings::shared(&parts, budget)?;
178
+ }
179
+ _ => {}
180
+ }
181
+ }
182
+ book.shown = format_ids
183
+ .iter()
184
+ .map(|&id| {
185
+ formats
186
+ .iter()
187
+ .find(|&&(custom, _)| custom == id)
188
+ .map_or_else(|| built_in(u32::from(id)), |&(_, shown)| shown)
189
+ })
190
+ .collect();
191
+ Ok(book)
192
+ }
193
+
194
+ /// An `RK` number: a 30-bit integer or the high bits of a double, either
195
+ /// perhaps divided by 100.
196
+ fn rk(value: u32) -> f64 {
197
+ #[expect(clippy::cast_possible_wrap, reason = "the bits are a signed integer")]
198
+ let number = if value & 2 != 0 {
199
+ f64::from((value as i32) >> 2)
200
+ } else {
201
+ f64::from_bits(u64::from(value & 0xFFFF_FFFC) << 32)
202
+ };
203
+ if value & 1 != 0 { number / 100.0 } else { number }
204
+ }
205
+
206
+ /// An error code as Excel shows it.
207
+ const fn error_text(code: u8) -> &'static str {
208
+ match code {
209
+ 0x00 => "#NULL!",
210
+ 0x07 => "#DIV/0!",
211
+ 0x0F => "#VALUE!",
212
+ 0x17 => "#REF!",
213
+ 0x1D => "#NAME?",
214
+ 0x24 => "#NUM!",
215
+ _ => "#N/A",
216
+ }
217
+ }
218
+
219
+ /// A worksheet's cells, from its BOF at `offset` to its EOF.
220
+ fn cells(
221
+ stream: &[u8],
222
+ offset: usize,
223
+ book: &Book,
224
+ scratch: &mut String,
225
+ budget: &Budget<'_>,
226
+ ) -> Result<Grid> {
227
+ let mut grid = Grid::default();
228
+ let mut all = records(stream, offset);
229
+ if !matches!(all.next(), Some((BOF, _))) {
230
+ return Err(malformed("a sheet that does not start with BOF"));
231
+ }
232
+ // A formula's string result follows it in a STRING record.
233
+ let mut pending: Option<(u32, u32)> = None;
234
+ for (kind, body) in all {
235
+ budget.tick()?;
236
+ let (Some(row), Some(column)) = (u16_at(body, 0), u16_at(body, 2)) else {
237
+ if kind == EOF {
238
+ break;
239
+ }
240
+ continue;
241
+ };
242
+ let (row, column) = (u32::from(row), u32::from(column));
243
+ // NUMBER, RK and numeric FORMULA results, written after the match.
244
+ let number = match kind {
245
+ EOF => break,
246
+ // LABELSST
247
+ 0x00FD => {
248
+ let text = u32_at(body, 6).and_then(|index| book.strings.get(index as usize));
249
+ text_cell(&mut grid, (row, column), text.map_or("", String::as_str));
250
+ None
251
+ }
252
+ // LABEL, RSTRING
253
+ 0x0204 | 0x00D6 => {
254
+ text_cell(&mut grid, (row, column), &book.string(body, 6, true));
255
+ None
256
+ }
257
+ 0x0203 => {
258
+ body.get(6..14).map(|bits| f64::from_le_bytes(bits.try_into().unwrap_or([0; 8])))
259
+ }
260
+ 0x027E => u32_at(body, 6).map(rk),
261
+ // MULRK: (format, RK) pairs from the first column on.
262
+ 0x00BD => {
263
+ let pairs = body.get(4..body.len().saturating_sub(2)).unwrap_or_default();
264
+ for (index, pair) in (0..).zip(pairs.as_chunks::<6>().0) {
265
+ let value = rk(u32::from_le_bytes([pair[2], pair[3], pair[4], pair[5]]));
266
+ let shown = book.shown(Some(u16::from_le_bytes([pair[0], pair[1]])));
267
+ number_cell(
268
+ &mut grid,
269
+ (row, column + index),
270
+ value,
271
+ shown,
272
+ book.date1904,
273
+ scratch,
274
+ );
275
+ }
276
+ None
277
+ }
278
+ // BOOLERR
279
+ 0x0205 => {
280
+ let (Some(&value), Some(&error)) = (body.get(6), body.get(7)) else { continue };
281
+ flag_cell(&mut grid, (row, column), value, error != 0);
282
+ None
283
+ }
284
+ // FORMULA: its cached result.
285
+ 0x0006 => {
286
+ let Some(result) = body.get(6..14) else { continue };
287
+ if result[6..8] == [0xFF, 0xFF] {
288
+ match result[0] {
289
+ 0 => pending = Some((row, column)),
290
+ 1 => flag_cell(&mut grid, (row, column), result[2], false),
291
+ 2 => flag_cell(&mut grid, (row, column), result[2], true),
292
+ _ => {}
293
+ }
294
+ None
295
+ } else {
296
+ Some(f64::from_le_bytes(result.try_into().unwrap_or([0; 8])))
297
+ }
298
+ }
299
+ // STRING: the string result of the formula before it.
300
+ 0x0207 => {
301
+ if let Some(at) = pending.take() {
302
+ text_cell(&mut grid, at, &book.string(body, 0, true));
303
+ }
304
+ None
305
+ }
306
+ _ => None,
307
+ };
308
+ if let Some(number) = number {
309
+ let shown = book.shown(u16_at(body, 4));
310
+ number_cell(&mut grid, (row, column), number, shown, book.date1904, scratch);
311
+ }
312
+ }
313
+ Ok(grid.finish())
314
+ }
315
+
316
+ fn number_cell(
317
+ grid: &mut Grid,
318
+ (row, column): (u32, u32),
319
+ number: f64,
320
+ shown: Shown,
321
+ date1904: bool,
322
+ scratch: &mut String,
323
+ ) {
324
+ let value = push_number(grid.buffer(), number, shown, date1904, scratch);
325
+ grid.end_cell(row, column, value);
326
+ }
327
+
328
+ fn text_cell(grid: &mut Grid, (row, column): (u32, u32), text: &str) {
329
+ grid.buffer().push_str(text);
330
+ grid.end_cell(row, column, Value::Text);
331
+ }
332
+
333
+ /// A boolean, or an error code.
334
+ fn flag_cell(grid: &mut Grid, (row, column): (u32, u32), value: u8, error: bool) {
335
+ let (text, value) = match (error, value) {
336
+ (true, code) => (error_text(code), Value::Error),
337
+ (false, 0) => ("FALSE", Value::Bool(false)),
338
+ (false, _) => ("TRUE", Value::Bool(true)),
339
+ };
340
+ grid.buffer().push_str(text);
341
+ grid.end_cell(row, column, value);
342
+ }
@@ -3,7 +3,6 @@
3
3
  //! digits), formulas without a cached result as `=FORMULA`, then the sheet's
4
4
  //! comments.
5
5
 
6
- mod numbers;
7
6
  #[cfg(test)]
8
7
  mod tests;
9
8
 
@@ -11,12 +10,12 @@ use std::sync::Arc;
11
10
 
12
11
  use super::Budget;
13
12
  use super::content::{Grid, Note, Sheet, Value};
13
+ use super::numbers::{Shown, push_number, styles};
14
14
  use super::package::Package;
15
15
  use crate::Result;
16
16
  use crate::xlsx::parse_cell_reference;
17
17
  use crate::xlsx::text::unescape as unescape_xstring;
18
18
  use crate::xml::reader::{Event, XmlReader};
19
- use numbers::{Shown, push_number, styles};
20
19
 
21
20
  /// A cell's `t` attribute.
22
21
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -21,11 +21,15 @@ pub(crate) mod inflate;
21
21
  #[cfg(feature = "json")]
22
22
  pub mod json;
23
23
  mod markdown;
24
+ mod numbers;
25
+ mod ods;
26
+ mod ole;
24
27
  mod package;
25
28
  mod pdf;
26
29
  mod span;
27
30
  #[cfg(test)]
28
31
  mod tests;
32
+ mod xls;
29
33
  mod xlsx;
30
34
 
31
35
  pub(crate) use budget::Budget;
@@ -46,41 +50,62 @@ pub enum Format {
46
50
  Pdf,
47
51
  Xlsx,
48
52
  Docx,
53
+ Ods,
54
+ Xls,
49
55
  }
50
56
 
51
57
  impl Format {
52
- /// `pdf`, `xlsx` or `docx`.
58
+ /// `pdf`, `xlsx`, `docx`, `ods` or `xls`.
53
59
  #[must_use]
54
60
  pub const fn as_str(self) -> &'static str {
55
61
  match self {
56
62
  Self::Pdf => "pdf",
57
63
  Self::Xlsx => "xlsx",
58
64
  Self::Docx => "docx",
65
+ Self::Ods => "ods",
66
+ Self::Xls => "xls",
59
67
  }
60
68
  }
61
69
  }
62
70
 
63
- /// Reads a document, its format told by its content: `%PDF-` in the first
64
- /// kilobyte, or a zip whose main part is a workbook or a Word document
65
- /// (macro-enabled ones and templates included).
71
+ /// Reads a document, its format told by its content.
72
+ ///
73
+ /// That is `%PDF-` in the first kilobyte; a compound file holding a legacy
74
+ /// Excel workbook (`.xls`); a
75
+ /// zip that is an ODF spreadsheet (`.ods`), or whose main part is
76
+ /// a workbook or a Word document (macro-enabled ones and templates
77
+ /// included).
66
78
  ///
67
79
  /// # Errors
68
- /// [`Error::UnsupportedDocument`] for other formats (legacy `.xls` and
69
- /// `.doc`, binary `.xlsb`, presentations), [`Error::NoTextLayer`] for a PDF
70
- /// of scanned pages, [`Error::PasswordRequired`] for a PDF that needs one,
71
- /// [`Error::LimitExceeded`] and [`Error::Cancelled`] when the [`Limits`]
72
- /// stop the read, and [`Error::Malformed`] for a corrupt document.
80
+ /// [`Error::UnsupportedDocument`] for other formats (legacy `.doc`, binary
81
+ /// `.xlsb`, presentations, ODF text), [`Error::NoTextLayer`] for a
82
+ /// PDF of scanned pages, [`Error::PasswordRequired`] for a document that
83
+ /// needs one, [`Error::LimitExceeded`] and [`Error::Cancelled`] when the
84
+ /// [`Limits`] stop the read, and [`Error::Malformed`] for a corrupt document.
73
85
  pub fn content(document: &[u8], limits: &Limits) -> Result<Content> {
74
86
  let budget = Budget::new(limits, document.len())?;
75
87
  if pdf::is_pdf(document) {
76
88
  return pdf::content(document, &budget).map(Content::Pdf);
77
89
  }
90
+ if document.starts_with(&ole::SIGNATURE) {
91
+ let ole = ole::Ole::open(document, &budget)?;
92
+ return xls::content(&ole, &budget).map(Content::Xls);
93
+ }
78
94
  let zip = zip(document)?;
95
+ match ods::spreadsheet(&zip, &budget)? {
96
+ Some(true) => return ods::content(&zip, &budget).map(Content::Ods),
97
+ Some(false) => {
98
+ return Err(Error::UnsupportedDocument(
99
+ "an OpenDocument file that is not a spreadsheet".to_owned(),
100
+ ));
101
+ }
102
+ None => {}
103
+ }
79
104
  let package = Package::open(&zip, &budget)?;
80
105
  match package.format()? {
81
106
  Format::Xlsx => xlsx::content(&package, &budget).map(Content::Xlsx),
82
107
  Format::Docx => docx::content(&package, &budget).map(Content::Docx),
83
- Format::Pdf => Err(Error::UnsupportedDocument("a PDF inside a zip".to_owned())),
108
+ other => Err(Error::UnsupportedDocument(format!("a {} inside a zip", other.as_str()))),
84
109
  }
85
110
  }
86
111
 
@@ -96,12 +121,13 @@ pub fn markdown(document: &[u8], limits: &Limits) -> Result<String> {
96
121
  /// Calls `each` with every image in a document, as a file.
97
122
  ///
98
123
  /// A PDF's images come once each, from the first page that draws them; a
99
- /// Word or Excel package gives its media. A JPEG stays a JPEG; pixels a PDF
100
- /// stores raw become PNG. Stops at the first error `each` returns.
124
+ /// Word, Excel or ODF package gives its media. A JPEG stays a
125
+ /// JPEG; pixels a PDF stores raw become PNG. Stops at the first error
126
+ /// `each` returns.
101
127
  ///
102
128
  /// # Errors
103
- /// As [`content`] (but a PDF without text is no error here), and what
104
- /// `each` returns.
129
+ /// As [`content`] (but a PDF without text is no error here, and a legacy
130
+ /// `.xls` is one), and what `each` returns.
105
131
  pub fn images(
106
132
  document: &[u8],
107
133
  limits: &Limits,
@@ -111,19 +137,35 @@ pub fn images(
111
137
  if pdf::is_pdf(document) {
112
138
  return pdf::images(document, &budget, &mut each);
113
139
  }
140
+ if document.starts_with(&ole::SIGNATURE) {
141
+ return Err(Error::UnsupportedDocument("the images of a legacy Office file".to_owned()));
142
+ }
114
143
  let zip = zip(document)?;
144
+ if ods::spreadsheet(&zip, &budget)?.is_some() {
145
+ return media(&zip, "Pictures/", &[], &budget, &mut each);
146
+ }
115
147
  let package = Package::open(&zip, &budget)?;
116
- let format = package.format()?;
117
- let sheets = match format {
148
+ let sheets = match package.format()? {
118
149
  Format::Xlsx => xlsx::image_sheets(&package, &budget)?,
119
150
  _ => Vec::new(),
120
151
  };
121
152
  let folder = package.main().rsplit_once('/').map_or("", |(folder, _)| folder);
122
- let media = format!("{folder}/media/");
123
- let names: Vec<&str> = package.parts().filter(|name| name.starts_with(&media)).collect();
153
+ media(&zip, &format!("{folder}/media/"), &sheets, &budget, &mut each)
154
+ }
155
+
156
+ /// The package's parts under `folder`, as images; `sheets` says which
157
+ /// sheet shows each, by part.
158
+ fn media(
159
+ zip: &ZipReader<'_>,
160
+ folder: &str,
161
+ sheets: &[(String, String)],
162
+ budget: &Budget<'_>,
163
+ each: &mut impl FnMut(Image) -> Result<()>,
164
+ ) -> Result<()> {
165
+ let names: Vec<&str> = zip.names().filter(|name| name.starts_with(folder)).collect();
124
166
  for name in names {
125
167
  budget.check()?;
126
- let Some(data) = package.part(name)? else { continue };
168
+ let Some(data) = zip.read(name, budget)? else { continue };
127
169
  let format = ImageFormat::sniff(&data, name);
128
170
  let (width, height) = format.dimensions(&data).unzip();
129
171
  let sheet = sheets
@@ -139,7 +181,9 @@ pub fn images(
139
181
  /// A document that is not a PDF, as the zip an Office document is.
140
182
  fn zip(document: &[u8]) -> Result<ZipReader<'_>> {
141
183
  if !document.starts_with(b"PK") {
142
- return Err(Error::UnsupportedDocument("not a PDF, XLSX or DOCX file".to_owned()));
184
+ return Err(Error::UnsupportedDocument(
185
+ "not a PDF, XLSX, XLS, ODS or DOCX file".to_owned(),
186
+ ));
143
187
  }
144
188
  ZipReader::new(document)
145
189
  }