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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Cargo.lock +4 -3
- data/Cargo.toml +1 -1
- data/README.md +27 -5
- data/ext/imprimatur_native/cli/Cargo.toml +2 -2
- data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
- data/ext/imprimatur_native/cli/src/cli.rs +5 -5
- data/ext/imprimatur_native/cli/src/main.rs +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +7 -2
- data/ext/imprimatur_native/core/README.md +6 -1
- data/ext/imprimatur_native/core/src/archive.rs +292 -0
- data/ext/imprimatur_native/core/src/error.rs +1 -1
- data/ext/imprimatur_native/core/src/lib.rs +2 -0
- data/ext/imprimatur_native/core/src/read/content.rs +16 -1
- data/ext/imprimatur_native/core/src/read/json.rs +3 -1
- data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
- data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +4 -3
- data/ext/imprimatur_native/core/src/read/ods.rs +357 -0
- data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
- data/ext/imprimatur_native/core/src/read/package.rs +0 -5
- data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
- data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +1 -2
- data/ext/imprimatur_native/core/src/read.rs +64 -20
- data/ext/imprimatur_native/core/src/test_support.rs +87 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +121 -0
- data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +14 -0
- data/ext/imprimatur_native/ffi/src/read.rs +1 -3
- data/lib/imprimatur/error.rb +4 -3
- data/lib/imprimatur/reader.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/zip.rb +127 -0
- data/lib/imprimatur.rb +3 -2
- 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 `
|
|
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
|
|
64
|
-
///
|
|
65
|
-
///
|
|
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 `.
|
|
69
|
-
/// `.
|
|
70
|
-
/// of scanned pages, [`Error::PasswordRequired`] for a
|
|
71
|
-
/// [`Error::LimitExceeded`] and [`Error::Cancelled`] when the
|
|
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
|
-
|
|
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
|
|
100
|
-
/// stores raw become PNG. Stops at the first error
|
|
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
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
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) =
|
|
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(
|
|
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
|
}
|