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,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)