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,357 @@
|
|
|
1
|
+
//! `.ods` spreadsheets: a zip whose `mimetype` entry names the format and
|
|
2
|
+
//! whose `content.xml` holds the tables. Cells carry their
|
|
3
|
+
//! value (`office:value-type` and its attributes) and the text they show;
|
|
4
|
+
//! repeated rows and columns are expanded only where they hold something.
|
|
5
|
+
|
|
6
|
+
#[cfg(test)]
|
|
7
|
+
mod tests;
|
|
8
|
+
|
|
9
|
+
use std::fmt::Write as _;
|
|
10
|
+
use std::ops::Range;
|
|
11
|
+
|
|
12
|
+
use super::Budget;
|
|
13
|
+
use super::content::{Grid, Note, Sheet, Value};
|
|
14
|
+
use super::numbers::push_general;
|
|
15
|
+
use crate::xlsx::DateTime;
|
|
16
|
+
use crate::xml::reader::{Event, Tag, XmlReader};
|
|
17
|
+
use crate::zip::reader::ZipReader;
|
|
18
|
+
use crate::{Error, Result};
|
|
19
|
+
|
|
20
|
+
const SPREADSHEET: &str = "application/vnd.oasis.opendocument.spreadsheet";
|
|
21
|
+
/// Excel's grid, which repeated rows and columns are cut to.
|
|
22
|
+
const MAX_ROWS: u32 = 1_048_576;
|
|
23
|
+
const MAX_COLUMNS: u32 = 16_384;
|
|
24
|
+
|
|
25
|
+
/// Whether a zip is an ODF file, by its `mimetype` entry: `None` when it is
|
|
26
|
+
/// not one, `Some(true)` for a spreadsheet (or its template).
|
|
27
|
+
///
|
|
28
|
+
/// # Errors
|
|
29
|
+
/// As reading the entry.
|
|
30
|
+
pub fn spreadsheet(zip: &ZipReader<'_>, budget: &Budget<'_>) -> Result<Option<bool>> {
|
|
31
|
+
let Some(kind) = zip.read("mimetype", budget)? else { return Ok(None) };
|
|
32
|
+
let kind = String::from_utf8_lossy(&kind);
|
|
33
|
+
let kind = kind.trim();
|
|
34
|
+
if !kind.starts_with("application/vnd.oasis.opendocument.") {
|
|
35
|
+
return Ok(None);
|
|
36
|
+
}
|
|
37
|
+
Ok(Some(kind.strip_suffix("-template").unwrap_or(kind) == SPREADSHEET))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The spreadsheet's tables, in order, with their comments.
|
|
41
|
+
///
|
|
42
|
+
/// # Errors
|
|
43
|
+
/// [`Error::Malformed`] for a missing or broken `content.xml`, and the
|
|
44
|
+
/// limits' errors.
|
|
45
|
+
pub fn content(zip: &ZipReader<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
|
|
46
|
+
let content = zip
|
|
47
|
+
.read("content.xml", budget)?
|
|
48
|
+
.ok_or_else(|| Error::malformed("ODS", "no content.xml"))?;
|
|
49
|
+
let text = String::from_utf8(content)
|
|
50
|
+
.map_err(|_| Error::malformed("ODS", "content.xml is not UTF-8"))?;
|
|
51
|
+
let mut reader = Reader::default();
|
|
52
|
+
let mut xml = XmlReader::new(&text, budget.max_depth());
|
|
53
|
+
while let Some(event) = xml.next()? {
|
|
54
|
+
budget.tick()?;
|
|
55
|
+
reader.event(event, budget)?;
|
|
56
|
+
}
|
|
57
|
+
Ok(reader.sheets)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A cell being read: its value attributes, its repeat, and where its text
|
|
61
|
+
/// starts in the row's buffer.
|
|
62
|
+
#[derive(Default)]
|
|
63
|
+
struct Cell {
|
|
64
|
+
kind: String,
|
|
65
|
+
value: String,
|
|
66
|
+
formula: String,
|
|
67
|
+
repeat: u32,
|
|
68
|
+
paragraphs: usize,
|
|
69
|
+
start: usize,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Default)]
|
|
73
|
+
struct Reader {
|
|
74
|
+
sheets: Vec<Sheet>,
|
|
75
|
+
hidden_styles: Vec<String>,
|
|
76
|
+
style: Option<String>,
|
|
77
|
+
grid: Grid,
|
|
78
|
+
comments: Vec<Note>,
|
|
79
|
+
row: u32,
|
|
80
|
+
column: u32,
|
|
81
|
+
row_repeat: u32,
|
|
82
|
+
/// The row's cells, their text in one buffer: column, text, value.
|
|
83
|
+
row_text: String,
|
|
84
|
+
row_cells: Vec<(u32, Range<usize>, Value)>,
|
|
85
|
+
cell: Option<Cell>,
|
|
86
|
+
note: Option<(String, String)>,
|
|
87
|
+
in_creator: bool,
|
|
88
|
+
/// Text counts inside paragraphs only, outside drawings and dates of
|
|
89
|
+
/// annotations.
|
|
90
|
+
paragraphs: usize,
|
|
91
|
+
skipping: usize,
|
|
92
|
+
scratch: String,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl Reader {
|
|
96
|
+
fn event(&mut self, event: Event<'_>, budget: &Budget<'_>) -> Result<()> {
|
|
97
|
+
match event {
|
|
98
|
+
Event::Start(tag) => self.start(&tag),
|
|
99
|
+
Event::End(name) => self.end(name, budget)?,
|
|
100
|
+
Event::Text(text) => self.text(&text),
|
|
101
|
+
}
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn start(&mut self, tag: &Tag<'_>) {
|
|
106
|
+
let repeat = |name| tag.attr(name).and_then(|n| n.parse::<u32>().ok()).unwrap_or(1).max(1);
|
|
107
|
+
match tag.name() {
|
|
108
|
+
"style" if tag.attr("family").as_deref() == Some("table") => {
|
|
109
|
+
self.style = tag.attr("name").map(std::borrow::Cow::into_owned);
|
|
110
|
+
}
|
|
111
|
+
"table-properties" if tag.attr("display").as_deref() == Some("false") => {
|
|
112
|
+
if let Some(style) = self.style.take() {
|
|
113
|
+
self.hidden_styles.push(style);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
"table" => {
|
|
117
|
+
let hidden = tag
|
|
118
|
+
.attr("style-name")
|
|
119
|
+
.is_some_and(|style| self.hidden_styles.iter().any(|h| *h == style));
|
|
120
|
+
self.sheets.push(Sheet {
|
|
121
|
+
name: tag.attr("name").map(std::borrow::Cow::into_owned).unwrap_or_default(),
|
|
122
|
+
hidden,
|
|
123
|
+
cells: Grid::default(),
|
|
124
|
+
comments: Vec::new(),
|
|
125
|
+
});
|
|
126
|
+
self.row = 0;
|
|
127
|
+
}
|
|
128
|
+
"table-row" => {
|
|
129
|
+
self.column = 0;
|
|
130
|
+
self.row_repeat = repeat("number-rows-repeated");
|
|
131
|
+
}
|
|
132
|
+
"table-cell" | "covered-table-cell" => {
|
|
133
|
+
let attr =
|
|
134
|
+
|name| tag.attr(name).map(std::borrow::Cow::into_owned).unwrap_or_default();
|
|
135
|
+
let kind = attr("value-type");
|
|
136
|
+
let value = match kind.as_str() {
|
|
137
|
+
"date" => attr("date-value"),
|
|
138
|
+
"time" => attr("time-value"),
|
|
139
|
+
"boolean" => attr("boolean-value"),
|
|
140
|
+
_ => attr("value"),
|
|
141
|
+
};
|
|
142
|
+
let repeat = repeat("number-columns-repeated");
|
|
143
|
+
let start = self.row_text.len();
|
|
144
|
+
self.cell = Some(Cell {
|
|
145
|
+
kind,
|
|
146
|
+
value,
|
|
147
|
+
formula: attr("formula"),
|
|
148
|
+
repeat,
|
|
149
|
+
paragraphs: 0,
|
|
150
|
+
start,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
"annotation" => self.note = Some((String::new(), String::new())),
|
|
154
|
+
"creator" if self.note.is_some() => self.in_creator = true,
|
|
155
|
+
"date" if self.note.is_some() => self.skipping += 1,
|
|
156
|
+
"frame" | "custom-shape" | "g" => self.skipping += 1,
|
|
157
|
+
"p" | "h" => {
|
|
158
|
+
// A second paragraph starts on a line of its own.
|
|
159
|
+
let separate = match (&mut self.note, &mut self.cell) {
|
|
160
|
+
(Some((_, text)), _) => !text.is_empty(),
|
|
161
|
+
(None, Some(cell)) => {
|
|
162
|
+
cell.paragraphs += 1;
|
|
163
|
+
cell.paragraphs > 1
|
|
164
|
+
}
|
|
165
|
+
(None, None) => false,
|
|
166
|
+
};
|
|
167
|
+
self.paragraphs += 1;
|
|
168
|
+
if separate {
|
|
169
|
+
self.text("\n");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
"s" => {
|
|
173
|
+
let count = tag
|
|
174
|
+
.attr("c")
|
|
175
|
+
.and_then(|count| count.parse::<usize>().ok())
|
|
176
|
+
.unwrap_or(1)
|
|
177
|
+
.min(1024);
|
|
178
|
+
self.text(&" ".repeat(count));
|
|
179
|
+
}
|
|
180
|
+
"tab" => self.text("\t"),
|
|
181
|
+
"line-break" => self.text("\n"),
|
|
182
|
+
_ => {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn text(&mut self, text: &str) {
|
|
187
|
+
if self.skipping > 0 {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
match &mut self.note {
|
|
191
|
+
Some((author, _)) if self.in_creator => author.push_str(text),
|
|
192
|
+
Some((_, note)) if self.paragraphs > 0 => note.push_str(text),
|
|
193
|
+
None if self.cell.is_some() && self.paragraphs > 0 => self.row_text.push_str(text),
|
|
194
|
+
_ => {}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn end(&mut self, name: &str, budget: &Budget<'_>) -> Result<()> {
|
|
199
|
+
match name {
|
|
200
|
+
"style" => self.style = None,
|
|
201
|
+
"creator" => self.in_creator = false,
|
|
202
|
+
"date" if self.note.is_some() => self.skipping = self.skipping.saturating_sub(1),
|
|
203
|
+
"frame" | "custom-shape" | "g" => self.skipping = self.skipping.saturating_sub(1),
|
|
204
|
+
"p" | "h" => self.paragraphs = self.paragraphs.saturating_sub(1),
|
|
205
|
+
"annotation" => {
|
|
206
|
+
if let Some((author, text)) = self.note.take() {
|
|
207
|
+
let author = (!author.trim().is_empty()).then(|| author.trim().into());
|
|
208
|
+
self.comments.push(Note { row: self.row, column: self.column, author, text });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
"table-cell" | "covered-table-cell" => self.end_cell(),
|
|
212
|
+
"table-row" => self.end_row(budget)?,
|
|
213
|
+
"table" => {
|
|
214
|
+
let grid = std::mem::take(&mut self.grid).finish();
|
|
215
|
+
if let Some(sheet) = self.sheets.last_mut() {
|
|
216
|
+
sheet.cells = grid;
|
|
217
|
+
sheet.comments = std::mem::take(&mut self.comments);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
_ => {}
|
|
221
|
+
}
|
|
222
|
+
Ok(())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// The cell's text, typed by its value; repeated across its columns.
|
|
226
|
+
fn end_cell(&mut self) {
|
|
227
|
+
let Some(cell) = self.cell.take() else { return };
|
|
228
|
+
let start = cell.start;
|
|
229
|
+
let shown = self.row_text.split_off(start);
|
|
230
|
+
let value = self.typed(&cell, &shown);
|
|
231
|
+
let end = self.row_text.len();
|
|
232
|
+
if end > start {
|
|
233
|
+
let repeat = cell.repeat.min(MAX_COLUMNS.saturating_sub(self.column));
|
|
234
|
+
for offset in 0..repeat {
|
|
235
|
+
self.row_cells.push((self.column + offset, start..end, value));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
self.column = self.column.saturating_add(cell.repeat);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Writes the cell's text as shown into the row buffer, returning its
|
|
242
|
+
/// value: numbers keep the text they show, dates and times read as in
|
|
243
|
+
/// XLSX, a formula without a result shows itself.
|
|
244
|
+
fn typed(&mut self, cell: &Cell, shown: &str) -> Value {
|
|
245
|
+
let out = &mut self.row_text;
|
|
246
|
+
match cell.kind.as_str() {
|
|
247
|
+
"float" | "percentage" | "currency" if let Ok(number) = cell.value.parse::<f64>() => {
|
|
248
|
+
if shown.trim().is_empty() {
|
|
249
|
+
push_general(out, number, &mut self.scratch);
|
|
250
|
+
} else {
|
|
251
|
+
out.push_str(shown);
|
|
252
|
+
}
|
|
253
|
+
Value::Number(number)
|
|
254
|
+
}
|
|
255
|
+
"date" if let Some(date) = date(&cell.value) => {
|
|
256
|
+
// Writing to a String cannot fail.
|
|
257
|
+
let _ = write!(out, "{date}");
|
|
258
|
+
Value::Date(date)
|
|
259
|
+
}
|
|
260
|
+
"time" if let Some(seconds) = duration(&cell.value) => {
|
|
261
|
+
let clock = seconds.rem_euclid(86_400);
|
|
262
|
+
// Writing to a String cannot fail.
|
|
263
|
+
let _ = write!(out, "{:02}:{:02}:{:02}", clock / 3600, clock / 60 % 60, clock % 60);
|
|
264
|
+
#[expect(clippy::cast_precision_loss, reason = "seconds of a day")]
|
|
265
|
+
Value::Number(seconds as f64 / 86_400.0)
|
|
266
|
+
}
|
|
267
|
+
"boolean" => {
|
|
268
|
+
let on = cell.value == "true" || cell.value == "1";
|
|
269
|
+
out.push_str(if on { "TRUE" } else { "FALSE" });
|
|
270
|
+
Value::Bool(on)
|
|
271
|
+
}
|
|
272
|
+
"" if !cell.formula.is_empty() && shown.trim().is_empty() => {
|
|
273
|
+
let formula =
|
|
274
|
+
cell.formula.split_once(':').map_or(cell.formula.as_str(), |(_, rest)| rest);
|
|
275
|
+
out.push('=');
|
|
276
|
+
out.push_str(formula.strip_prefix('=').unwrap_or(formula));
|
|
277
|
+
Value::Formula
|
|
278
|
+
}
|
|
279
|
+
_ => {
|
|
280
|
+
out.push_str(shown);
|
|
281
|
+
Value::Text
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// Writes the row's cells, once per repeat, and moves past it.
|
|
287
|
+
fn end_row(&mut self, budget: &Budget<'_>) -> Result<()> {
|
|
288
|
+
let repeat = if self.row_cells.is_empty() {
|
|
289
|
+
0
|
|
290
|
+
} else {
|
|
291
|
+
self.row_repeat.min(MAX_ROWS.saturating_sub(self.row))
|
|
292
|
+
};
|
|
293
|
+
for offset in 0..repeat {
|
|
294
|
+
budget.tick()?;
|
|
295
|
+
budget.inflate(self.row_text.len())?;
|
|
296
|
+
for (column, text, value) in &self.row_cells {
|
|
297
|
+
if text.end > text.start {
|
|
298
|
+
self.grid.buffer().push_str(&self.row_text[text.clone()]);
|
|
299
|
+
self.grid.end_cell(self.row + offset, *column, *value);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
self.row_cells.clear();
|
|
304
|
+
self.row_text.clear();
|
|
305
|
+
self.row = self.row.saturating_add(self.row_repeat);
|
|
306
|
+
Ok(())
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// `2023-03-15` or `2023-03-15T09:30:00.5`.
|
|
311
|
+
fn date(value: &str) -> Option<DateTime> {
|
|
312
|
+
let (day, time) = value.split_once('T').unwrap_or((value, ""));
|
|
313
|
+
let mut parts = day.splitn(3, '-').map(str::parse::<u16>);
|
|
314
|
+
let (year, month, day) = (parts.next()?.ok()?, parts.next()?.ok()?, parts.next()?.ok()?);
|
|
315
|
+
let date = DateTime::date(year, u8::try_from(month).ok()?, u8::try_from(day).ok()?).ok()?;
|
|
316
|
+
if time.is_empty() {
|
|
317
|
+
return Some(date);
|
|
318
|
+
}
|
|
319
|
+
let mut parts = time.splitn(3, ':');
|
|
320
|
+
let (hour, minute, second) = (parts.next()?, parts.next()?, parts.next().unwrap_or("0"));
|
|
321
|
+
let seconds: f64 = second.parse().ok()?;
|
|
322
|
+
#[expect(
|
|
323
|
+
clippy::cast_possible_truncation,
|
|
324
|
+
clippy::cast_sign_loss,
|
|
325
|
+
reason = "under 60, checked by `at`"
|
|
326
|
+
)]
|
|
327
|
+
let (whole, nanosecond) = (seconds.trunc() as u8, (seconds.fract() * 1e9).round() as u32);
|
|
328
|
+
date.at(hour.parse().ok()?, minute.parse().ok()?, whole, nanosecond).ok()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// An ISO 8601 duration such as `PT12H30M00S` (or with days), in seconds.
|
|
332
|
+
fn duration(value: &str) -> Option<i64> {
|
|
333
|
+
let (negative, value) = value.strip_prefix('-').map_or((false, value), |rest| (true, rest));
|
|
334
|
+
let value = value.strip_prefix('P')?;
|
|
335
|
+
let (mut seconds, mut number, mut in_time) = (0_f64, String::new(), false);
|
|
336
|
+
for ch in value.chars() {
|
|
337
|
+
match ch {
|
|
338
|
+
'T' => in_time = true,
|
|
339
|
+
'0'..='9' | '.' => number.push(ch),
|
|
340
|
+
unit => {
|
|
341
|
+
let amount: f64 = number.parse().ok()?;
|
|
342
|
+
number.clear();
|
|
343
|
+
let scale = match (unit, in_time) {
|
|
344
|
+
('D', false) => 86_400.0,
|
|
345
|
+
('H', true) => 3_600.0,
|
|
346
|
+
('M', true) => 60.0,
|
|
347
|
+
('S', true) => 1.0,
|
|
348
|
+
_ => return None,
|
|
349
|
+
};
|
|
350
|
+
seconds = amount.mul_add(scale, seconds);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
#[expect(clippy::cast_possible_truncation, reason = "rounded seconds")]
|
|
355
|
+
let seconds = seconds.round() as i64;
|
|
356
|
+
Some(if negative { -seconds } else { seconds })
|
|
357
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
//! Compound files (MS-CFB): the container legacy Office documents are, a
|
|
2
|
+
//! small FAT file system inside one file. Reading a stream of the root
|
|
3
|
+
//! storage by name is all the readers need.
|
|
4
|
+
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod tests;
|
|
7
|
+
|
|
8
|
+
use crate::read::Budget;
|
|
9
|
+
use crate::{Error, Result};
|
|
10
|
+
|
|
11
|
+
/// The first eight bytes of every compound file.
|
|
12
|
+
pub const SIGNATURE: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
|
|
13
|
+
/// Sector numbers above this are markers: end of chain, free, FAT, DIFAT.
|
|
14
|
+
const MAX_SECTOR: u32 = 0xFFFF_FFFA;
|
|
15
|
+
/// No sibling or child in the directory tree.
|
|
16
|
+
const NO_STREAM: u32 = 0xFFFF_FFFF;
|
|
17
|
+
const MINI_SECTOR: usize = 64;
|
|
18
|
+
const ENTRY: usize = 128;
|
|
19
|
+
|
|
20
|
+
/// A compound file's allocation tables and the root storage's entries.
|
|
21
|
+
pub struct Ole<'a> {
|
|
22
|
+
data: &'a [u8],
|
|
23
|
+
sector: usize,
|
|
24
|
+
fat: Vec<u32>,
|
|
25
|
+
mini_fat: Vec<u32>,
|
|
26
|
+
mini_stream: Vec<u8>,
|
|
27
|
+
mini_cutoff: u64,
|
|
28
|
+
/// The root storage's children: name, whether a stream, start, size.
|
|
29
|
+
entries: Vec<(String, bool, u32, u64)>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn malformed(reason: &str) -> Error {
|
|
33
|
+
Error::malformed("compound file", reason)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// An allocation table's sector numbers.
|
|
37
|
+
fn words(bytes: &[u8]) -> impl Iterator<Item = u32> + '_ {
|
|
38
|
+
bytes.as_chunks::<4>().0.iter().map(|word| u32::from_le_bytes(*word))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
|
|
42
|
+
Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
|
|
46
|
+
Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl<'a> Ole<'a> {
|
|
50
|
+
/// Reads the header, the allocation tables and the directory.
|
|
51
|
+
///
|
|
52
|
+
/// # Errors
|
|
53
|
+
/// [`Error::Malformed`] for a file that is not a compound file or whose
|
|
54
|
+
/// tables point outside it or in circles.
|
|
55
|
+
pub fn open(data: &'a [u8], budget: &Budget<'_>) -> Result<Self> {
|
|
56
|
+
if !data.starts_with(&SIGNATURE) || data.len() < 512 {
|
|
57
|
+
return Err(malformed("no compound file header"));
|
|
58
|
+
}
|
|
59
|
+
let header = |at| u32_at(data, at).ok_or_else(|| malformed("a short header"));
|
|
60
|
+
let sector = match u16_at(data, 0x1E) {
|
|
61
|
+
Some(9) => 512,
|
|
62
|
+
Some(12) => 4096,
|
|
63
|
+
_ => return Err(malformed("an unknown sector size")),
|
|
64
|
+
};
|
|
65
|
+
let mut ole = Self {
|
|
66
|
+
data,
|
|
67
|
+
sector,
|
|
68
|
+
fat: Vec::new(),
|
|
69
|
+
mini_fat: Vec::new(),
|
|
70
|
+
mini_stream: Vec::new(),
|
|
71
|
+
mini_cutoff: u64::from(header(0x38)?),
|
|
72
|
+
entries: Vec::new(),
|
|
73
|
+
};
|
|
74
|
+
// The FAT's own sectors: 109 listed in the header, the rest in a
|
|
75
|
+
// chain of DIFAT sectors, each ending with the next one's number.
|
|
76
|
+
let mut fat_sectors: Vec<u32> = (0..109)
|
|
77
|
+
.filter_map(|index| u32_at(data, 0x4C + index * 4))
|
|
78
|
+
.filter(|&s| s <= MAX_SECTOR)
|
|
79
|
+
.collect();
|
|
80
|
+
let mut next = header(0x44)?;
|
|
81
|
+
let per_sector = sector / 4;
|
|
82
|
+
for _ in 0..=data.len() / sector {
|
|
83
|
+
if next > MAX_SECTOR {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
let bytes = ole.sector_bytes(next)?;
|
|
87
|
+
fat_sectors.extend(
|
|
88
|
+
(0..per_sector - 1)
|
|
89
|
+
.filter_map(|index| u32_at(bytes, index * 4))
|
|
90
|
+
.filter(|&s| s <= MAX_SECTOR),
|
|
91
|
+
);
|
|
92
|
+
next = u32_at(bytes, (per_sector - 1) * 4).unwrap_or(NO_STREAM);
|
|
93
|
+
}
|
|
94
|
+
budget.inflate(fat_sectors.len().saturating_mul(sector))?;
|
|
95
|
+
for number in fat_sectors {
|
|
96
|
+
budget.tick()?;
|
|
97
|
+
let bytes = ole.sector_bytes(number)?;
|
|
98
|
+
ole.fat.extend(words(bytes));
|
|
99
|
+
}
|
|
100
|
+
let directory = ole.chain(header(0x30)?, None, budget)?;
|
|
101
|
+
let first_mini_fat = header(0x3C)?;
|
|
102
|
+
if first_mini_fat <= MAX_SECTOR {
|
|
103
|
+
let table = ole.chain(first_mini_fat, None, budget)?;
|
|
104
|
+
ole.mini_fat = words(&table).collect();
|
|
105
|
+
}
|
|
106
|
+
let root = directory.get(..ENTRY).ok_or_else(|| malformed("no root entry"))?;
|
|
107
|
+
if root[0x42] != 5 {
|
|
108
|
+
return Err(malformed("the first entry is not the root"));
|
|
109
|
+
}
|
|
110
|
+
// The whole chain: some writers give the root the size of the one
|
|
111
|
+
// stream in it, short of the last mini sector it fills.
|
|
112
|
+
let (start, _) = location(root, sector);
|
|
113
|
+
ole.mini_stream =
|
|
114
|
+
if start <= MAX_SECTOR { ole.chain(start, None, budget)? } else { Vec::new() };
|
|
115
|
+
ole.entries = children(&directory, sector, budget)?;
|
|
116
|
+
Ok(ole)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// The bytes of the stream `name` in the root storage, its name
|
|
120
|
+
/// compared regardless of case; `None` when there is none.
|
|
121
|
+
///
|
|
122
|
+
/// # Errors
|
|
123
|
+
/// [`Error::Malformed`] for a stream whose sectors are missing or loop.
|
|
124
|
+
pub fn stream(&self, name: &str, budget: &Budget<'_>) -> Result<Option<Vec<u8>>> {
|
|
125
|
+
let Some(&(_, _, start, size)) = self
|
|
126
|
+
.entries
|
|
127
|
+
.iter()
|
|
128
|
+
.find(|(entry, stream, ..)| *stream && entry.eq_ignore_ascii_case(name))
|
|
129
|
+
else {
|
|
130
|
+
return Ok(None);
|
|
131
|
+
};
|
|
132
|
+
if size < self.mini_cutoff {
|
|
133
|
+
return self.mini_chain(start, size, budget).map(Some);
|
|
134
|
+
}
|
|
135
|
+
self.chain(start, Some(size), budget).map(Some)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn sector_bytes(&self, number: u32) -> Result<&'a [u8]> {
|
|
139
|
+
let start = (number as usize + 1)
|
|
140
|
+
.checked_mul(self.sector)
|
|
141
|
+
.ok_or_else(|| malformed("a sector out of range"))?;
|
|
142
|
+
self.data
|
|
143
|
+
.get(start..start + self.sector)
|
|
144
|
+
.ok_or_else(|| malformed("a sector past the end of the file"))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// The sectors chained from `start`, cut to `size` when given.
|
|
148
|
+
fn chain(&self, start: u32, size: Option<u64>, budget: &Budget<'_>) -> Result<Vec<u8>> {
|
|
149
|
+
let limit = size.map_or(usize::MAX, |size| usize::try_from(size).unwrap_or(usize::MAX));
|
|
150
|
+
if limit != usize::MAX && limit > self.data.len() {
|
|
151
|
+
return Err(malformed("a stream larger than the file"));
|
|
152
|
+
}
|
|
153
|
+
let mut out = Vec::with_capacity(limit.min(self.data.len()));
|
|
154
|
+
let mut seen = vec![false; self.fat.len()];
|
|
155
|
+
let mut next = start;
|
|
156
|
+
while next <= MAX_SECTOR && out.len() < limit {
|
|
157
|
+
budget.tick()?;
|
|
158
|
+
let visited =
|
|
159
|
+
seen.get_mut(next as usize).ok_or_else(|| malformed("a sector beyond the FAT"))?;
|
|
160
|
+
if std::mem::replace(visited, true) {
|
|
161
|
+
return Err(malformed("a sector chain that loops"));
|
|
162
|
+
}
|
|
163
|
+
out.extend_from_slice(self.sector_bytes(next)?);
|
|
164
|
+
next = self.fat[next as usize];
|
|
165
|
+
}
|
|
166
|
+
out.truncate(limit);
|
|
167
|
+
budget.inflate(out.len())?;
|
|
168
|
+
Ok(out)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// A small stream, kept in the root's mini stream in 64-byte sectors.
|
|
172
|
+
fn mini_chain(&self, start: u32, size: u64, budget: &Budget<'_>) -> Result<Vec<u8>> {
|
|
173
|
+
let limit = usize::try_from(size).unwrap_or(usize::MAX);
|
|
174
|
+
let mut out = Vec::with_capacity(limit.min(self.mini_stream.len()));
|
|
175
|
+
let mut seen = vec![false; self.mini_fat.len()];
|
|
176
|
+
let mut next = start;
|
|
177
|
+
while next <= MAX_SECTOR && out.len() < limit {
|
|
178
|
+
budget.tick()?;
|
|
179
|
+
let visited = seen
|
|
180
|
+
.get_mut(next as usize)
|
|
181
|
+
.ok_or_else(|| malformed("a sector beyond the mini FAT"))?;
|
|
182
|
+
if std::mem::replace(visited, true) {
|
|
183
|
+
return Err(malformed("a mini sector chain that loops"));
|
|
184
|
+
}
|
|
185
|
+
let at = next as usize * MINI_SECTOR;
|
|
186
|
+
let bytes = self
|
|
187
|
+
.mini_stream
|
|
188
|
+
.get(at..at + MINI_SECTOR)
|
|
189
|
+
.ok_or_else(|| malformed("a mini sector past the mini stream"))?;
|
|
190
|
+
out.extend_from_slice(bytes);
|
|
191
|
+
next = self.mini_fat[next as usize];
|
|
192
|
+
}
|
|
193
|
+
out.truncate(limit);
|
|
194
|
+
budget.inflate(out.len())?;
|
|
195
|
+
Ok(out)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// An entry's first sector and size; files of 512-byte sectors (version 3)
|
|
200
|
+
/// leave the size's high half undefined.
|
|
201
|
+
fn location(entry: &[u8], sector: usize) -> (u32, u64) {
|
|
202
|
+
let start = u32_at(entry, 0x74).unwrap_or(NO_STREAM);
|
|
203
|
+
let low = u64::from(u32_at(entry, 0x78).unwrap_or(0));
|
|
204
|
+
let high = if sector == 512 { 0 } else { u64::from(u32_at(entry, 0x7C).unwrap_or(0)) };
|
|
205
|
+
(start, high << 32 | low)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// The root storage's children, from the red-black tree under its child
|
|
209
|
+
/// pointer: name, whether a stream, first sector, size.
|
|
210
|
+
fn children(
|
|
211
|
+
directory: &[u8],
|
|
212
|
+
sector: usize,
|
|
213
|
+
budget: &Budget<'_>,
|
|
214
|
+
) -> Result<Vec<(String, bool, u32, u64)>> {
|
|
215
|
+
let entry = |index: u32| directory.get(index as usize * ENTRY..(index as usize + 1) * ENTRY);
|
|
216
|
+
let mut found = Vec::new();
|
|
217
|
+
let mut pending = vec![u32_at(directory, 0x4C).unwrap_or(NO_STREAM)];
|
|
218
|
+
let mut visits = 0_usize;
|
|
219
|
+
while let Some(index) = pending.pop() {
|
|
220
|
+
if index == NO_STREAM {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
visits += 1;
|
|
224
|
+
if visits > directory.len() / ENTRY {
|
|
225
|
+
return Err(malformed("a directory tree that loops"));
|
|
226
|
+
}
|
|
227
|
+
budget.tick()?;
|
|
228
|
+
let Some(bytes) = entry(index) else {
|
|
229
|
+
return Err(malformed("a directory entry past the end"));
|
|
230
|
+
};
|
|
231
|
+
let length = usize::from(u16_at(bytes, 0x40).unwrap_or(0)).min(64).saturating_sub(2);
|
|
232
|
+
let units: Vec<u16> = bytes[..length]
|
|
233
|
+
.as_chunks::<2>()
|
|
234
|
+
.0
|
|
235
|
+
.iter()
|
|
236
|
+
.map(|unit| u16::from_le_bytes(*unit))
|
|
237
|
+
.collect();
|
|
238
|
+
let (start, size) = location(bytes, sector);
|
|
239
|
+
found.push((String::from_utf16_lossy(&units), bytes[0x42] == 2, start, size));
|
|
240
|
+
pending.push(u32_at(bytes, 0x44).unwrap_or(NO_STREAM));
|
|
241
|
+
pending.push(u32_at(bytes, 0x48).unwrap_or(NO_STREAM));
|
|
242
|
+
}
|
|
243
|
+
Ok(found)
|
|
244
|
+
}
|
|
@@ -86,11 +86,6 @@ impl<'a> Package<'a> {
|
|
|
86
86
|
self.text(&part.target)?.map_or_else(|| Ok(T::default()), |text| parse(&text))
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/// The parts' names.
|
|
90
|
-
pub fn parts(&self) -> impl Iterator<Item = &'a str> + '_ {
|
|
91
|
-
self.zip.names()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
89
|
/// A part's bytes, `None` when the package has no such part.
|
|
95
90
|
pub fn part(&self, name: &str) -> Result<Option<Vec<u8>>> {
|
|
96
91
|
self.zip.read(name, self.budget)
|