Skip to main content

rightkit_fs/
zip.rs

1//! A deterministic ZIP writer that stores entries uncompressed.
2//!
3//! [`StoredZipWriter`] writes the *stored* method only (method 0, no compression), which is what a
4//! QA bundle, a test fixture or an evidence archive needs: the same entries in the same order give the
5//! same bytes, on every platform and every run. Nothing varies: the timestamp of every entry is the
6//! DOS epoch (1980-01-01 00:00:00), there are no extra fields and no comments, the host and the
7//! attributes are fixed (Unix `0644` for a file, `0755` for a directory), and entries appear in the
8//! order they were added. There is no compression and no ZIP64: an archive of more than 65 535
9//! entries, or with any entry, local-header offset or central directory beyond 4 GiB - 1, is refused
10//! with `InvalidInput` rather than written wrongly.
11//!
12//! Entry names are checked, since a name is the part of an archive that attacks the program that
13//! unpacks it: a name must be non-empty, relative, use `/` only (no `\`, no drive prefix), have no
14//! empty, `.` or `..` component, no NUL or control character, fit in 65 535 bytes, and be unique
15//! (compared byte for byte). Names are written as UTF-8 and flagged as such (general-purpose bit 11).
16//!
17//! [`crc32`] is the standard IEEE CRC-32 (the one ZIP stores) computed inline; this crate adds no
18//! dependency for it.
19
20use std::collections::HashSet;
21use std::fs::File;
22use std::io::{self, BufWriter, Read, Write};
23use std::path::{Path, PathBuf};
24
25/// The DOS time of every entry: 00:00:00.
26const DOS_TIME: u16 = 0;
27/// The DOS date of every entry: 1980-01-01 (year 0, month 1, day 1).
28const DOS_DATE: u16 = (1 << 5) | 1;
29/// General-purpose flag bit 11: the name is UTF-8.
30const FLAG_UTF8: u16 = 0x0800;
31/// "Version needed to extract": 2.0, the first that supports directories and stored entries alike.
32const VERSION_NEEDED: u16 = 20;
33/// "Version made by": host Unix (3), specification 2.0.
34const VERSION_MADE_BY: u16 = (3 << 8) | 20;
35const FILE_ATTRIBUTES: u32 = 0o100_644 << 16;
36const DIR_ATTRIBUTES: u32 = (0o040_755 << 16) | 0x10;
37const MAX_ENTRIES: usize = 0xFFFF;
38const MAX_U32: u64 = 0xFFFF_FFFF;
39
40const SIG_LOCAL: u32 = 0x0403_4b50;
41const SIG_CENTRAL: u32 = 0x0201_4b50;
42const SIG_END: u32 = 0x0605_4b50;
43
44const CRC_TABLE: [u32; 256] = {
45    let mut table = [0u32; 256];
46    let mut n = 0;
47    while n < 256 {
48        let mut c = n as u32;
49        let mut k = 0;
50        while k < 8 {
51            c = if c & 1 != 0 {
52                0xEDB8_8320 ^ (c >> 1)
53            } else {
54                c >> 1
55            };
56            k += 1;
57        }
58        table[n] = c;
59        n += 1;
60    }
61    table
62};
63
64/// The IEEE CRC-32 of `bytes` (polynomial `0xEDB88320`, initial value and final XOR `0xFFFFFFFF`), the
65/// checksum a ZIP entry stores. `crc32(b"123456789")` is `0xCBF43926`.
66pub fn crc32(bytes: &[u8]) -> u32 {
67    let mut crc = 0xFFFF_FFFFu32;
68    for byte in bytes {
69        crc = CRC_TABLE[((crc ^ u32::from(*byte)) & 0xFF) as usize] ^ (crc >> 8);
70    }
71    !crc
72}
73
74fn invalid(message: impl Into<String>) -> io::Error {
75    io::Error::new(io::ErrorKind::InvalidInput, message.into())
76}
77
78/// Check one entry name; see the module docs for the rules. `directory` names may end in one `/`.
79fn check_name(name: &str, directory: bool) -> io::Result<()> {
80    let body = if directory {
81        name.strip_suffix('/').unwrap_or(name)
82    } else {
83        name
84    };
85    if body.is_empty() {
86        return Err(invalid("a ZIP entry name is empty"));
87    }
88    if name.len() > 0xFFFF {
89        return Err(invalid("a ZIP entry name is longer than 65535 bytes"));
90    }
91    if name.starts_with('/') || name.contains('\\') {
92        return Err(invalid(format!(
93            "ZIP entry name {name:?} is absolute or uses a backslash"
94        )));
95    }
96    if name.chars().any(|c| c.is_control()) {
97        return Err(invalid(format!(
98            "ZIP entry name {name:?} has a control character"
99        )));
100    }
101    let bytes = body.as_bytes();
102    // `C:` or `C:x` as the first component is a drive prefix on Windows.
103    if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
104        return Err(invalid(format!(
105            "ZIP entry name {name:?} has a drive prefix"
106        )));
107    }
108    for component in body.split('/') {
109        if component.is_empty() || component == "." || component == ".." {
110            return Err(invalid(format!(
111                "ZIP entry name {name:?} has an empty, `.` or `..` component"
112            )));
113        }
114    }
115    Ok(())
116}
117
118struct Entry {
119    name: String,
120    crc: u32,
121    size: u32,
122    offset: u32,
123    directory: bool,
124}
125
126/// Writes a stored ZIP archive to `W`. Add entries, then [`finish`](Self::finish). Nothing is
127/// flushed or synced: wrap a file in [`crate::AtomicWriter`] (or write the finished bytes with
128/// [`crate::atomic_write`]) when the archive must appear whole.
129pub struct StoredZipWriter<W: Write> {
130    out: W,
131    written: u64,
132    entries: Vec<Entry>,
133    seen: HashSet<String>,
134}
135
136impl<W: Write> StoredZipWriter<W> {
137    /// A writer that appends to `out` from its current position (the archive starts there).
138    pub fn new(out: W) -> Self {
139        Self {
140            out,
141            written: 0,
142            entries: Vec::new(),
143            seen: HashSet::new(),
144        }
145    }
146
147    fn put(&mut self, bytes: &[u8]) -> io::Result<()> {
148        self.out.write_all(bytes)?;
149        self.written += bytes.len() as u64;
150        Ok(())
151    }
152
153    fn register(&mut self, name: &str, directory: bool) -> io::Result<String> {
154        check_name(name, directory)?;
155        if self.entries.len() >= MAX_ENTRIES {
156            return Err(invalid(
157                "a ZIP archive without ZIP64 holds at most 65535 entries",
158            ));
159        }
160        let stored = if directory && !name.ends_with('/') {
161            format!("{name}/")
162        } else {
163            name.to_string()
164        };
165        if stored.len() > 0xFFFF {
166            return Err(invalid("a ZIP entry name is longer than 65535 bytes"));
167        }
168        // A file `a` and a directory `a/` are different names but cannot both be unpacked.
169        let twin = if directory {
170            stored.trim_end_matches('/').to_string()
171        } else {
172            format!("{stored}/")
173        };
174        if self.seen.contains(&stored) || self.seen.contains(&twin) {
175            return Err(invalid(format!("ZIP entry name {name:?} is already used")));
176        }
177        self.seen.insert(stored.clone());
178        Ok(stored)
179    }
180
181    /// Add a file entry holding `data`.
182    ///
183    /// Errors (`InvalidInput`): a bad or duplicate name, `data` of 4 GiB or more, an archive past the
184    /// ZIP limits, or a local-header offset past 4 GiB. A failed write is the writer's error.
185    pub fn add_file(&mut self, name: &str, data: &[u8]) -> io::Result<()> {
186        let size = u32::try_from(data.len())
187            .map_err(|_| invalid("a ZIP entry without ZIP64 is under 4 GiB"))?;
188        let stored = self.register(name, false)?;
189        self.write_local(stored, data, size, false)
190    }
191
192    /// Add a directory entry (`name` with or without a trailing `/`; the stored name ends in `/`).
193    pub fn add_dir(&mut self, name: &str) -> io::Result<()> {
194        let stored = self.register(name, true)?;
195        self.write_local(stored, &[], 0, true)
196    }
197
198    fn write_local(
199        &mut self,
200        name: String,
201        data: &[u8],
202        size: u32,
203        directory: bool,
204    ) -> io::Result<()> {
205        let offset = u32::try_from(self.written)
206            .map_err(|_| invalid("a ZIP entry starts past 4 GiB, which needs ZIP64"))?;
207        let crc = crc32(data);
208        let name_bytes = name.as_bytes();
209        let mut header = Vec::with_capacity(30 + name_bytes.len());
210        header.extend_from_slice(&SIG_LOCAL.to_le_bytes());
211        header.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
212        header.extend_from_slice(&FLAG_UTF8.to_le_bytes());
213        header.extend_from_slice(&0u16.to_le_bytes()); // method: stored
214        header.extend_from_slice(&DOS_TIME.to_le_bytes());
215        header.extend_from_slice(&DOS_DATE.to_le_bytes());
216        header.extend_from_slice(&crc.to_le_bytes());
217        header.extend_from_slice(&size.to_le_bytes()); // compressed size
218        header.extend_from_slice(&size.to_le_bytes()); // uncompressed size
219        header.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
220        header.extend_from_slice(&0u16.to_le_bytes()); // extra field length
221        header.extend_from_slice(name_bytes);
222        self.put(&header)?;
223        self.put(data)?;
224        self.entries.push(Entry {
225            name,
226            crc,
227            size,
228            offset,
229            directory,
230        });
231        Ok(())
232    }
233
234    /// Write the central directory and end record and return the underlying writer. The archive is
235    /// complete only after this succeeds.
236    pub fn finish(mut self) -> io::Result<W> {
237        let directory_offset = u32::try_from(self.written).map_err(|_| {
238            invalid("the ZIP central directory starts past 4 GiB, which needs ZIP64")
239        })?;
240        let mut central = Vec::new();
241        for entry in &self.entries {
242            let name = entry.name.as_bytes();
243            central.extend_from_slice(&SIG_CENTRAL.to_le_bytes());
244            central.extend_from_slice(&VERSION_MADE_BY.to_le_bytes());
245            central.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
246            central.extend_from_slice(&FLAG_UTF8.to_le_bytes());
247            central.extend_from_slice(&0u16.to_le_bytes()); // method: stored
248            central.extend_from_slice(&DOS_TIME.to_le_bytes());
249            central.extend_from_slice(&DOS_DATE.to_le_bytes());
250            central.extend_from_slice(&entry.crc.to_le_bytes());
251            central.extend_from_slice(&entry.size.to_le_bytes());
252            central.extend_from_slice(&entry.size.to_le_bytes());
253            central.extend_from_slice(&(name.len() as u16).to_le_bytes());
254            central.extend_from_slice(&0u16.to_le_bytes()); // extra field length
255            central.extend_from_slice(&0u16.to_le_bytes()); // comment length
256            central.extend_from_slice(&0u16.to_le_bytes()); // disk number start
257            central.extend_from_slice(&0u16.to_le_bytes()); // internal attributes
258            let attributes = if entry.directory {
259                DIR_ATTRIBUTES
260            } else {
261                FILE_ATTRIBUTES
262            };
263            central.extend_from_slice(&attributes.to_le_bytes());
264            central.extend_from_slice(&entry.offset.to_le_bytes());
265            central.extend_from_slice(name);
266        }
267        let directory_size = u32::try_from(central.len())
268            .ok()
269            .filter(|size| u64::from(*size) + u64::from(directory_offset) <= MAX_U32)
270            .ok_or_else(|| {
271                invalid("the ZIP central directory ends past 4 GiB, which needs ZIP64")
272            })?;
273        let count = self.entries.len() as u16;
274        let mut end = Vec::with_capacity(22);
275        end.extend_from_slice(&SIG_END.to_le_bytes());
276        end.extend_from_slice(&0u16.to_le_bytes()); // this disk
277        end.extend_from_slice(&0u16.to_le_bytes()); // disk with the central directory
278        end.extend_from_slice(&count.to_le_bytes());
279        end.extend_from_slice(&count.to_le_bytes());
280        end.extend_from_slice(&directory_size.to_le_bytes());
281        end.extend_from_slice(&directory_offset.to_le_bytes());
282        end.extend_from_slice(&0u16.to_le_bytes()); // comment length
283        self.put(&central)?;
284        self.put(&end)?;
285        Ok(self.out)
286    }
287}
288
289/// The bytes of a stored ZIP archive holding `entries` in order, each `(name, bytes)` a file; a name
290/// ending in `/` with empty bytes is a directory entry. Same rules and limits as
291/// [`StoredZipWriter`].
292pub fn stored_zip_bytes(entries: &[(&str, &[u8])]) -> io::Result<Vec<u8>> {
293    let mut writer = StoredZipWriter::new(Vec::new());
294    for (name, data) in entries {
295        if name.ends_with('/') {
296            if !data.is_empty() {
297                return Err(invalid(format!("directory entry {name:?} has content")));
298            }
299            writer.add_dir(name)?;
300        } else {
301            writer.add_file(name, data)?;
302        }
303    }
304    writer.finish()
305}
306
307// Archives built from files on disk, byte-compatible with CutRight's former writer (rightkit#467).
308
309/// "Version made by" of [`write_zip`]: host Unix (3), specification 3.0. This is the byte-exact
310/// value CutRight's former `cutright-qa/src/zip.rs` wrote; [`StoredZipWriter`] keeps its own 2.0.
311const FILE_ZIP_MADE_BY: u16 = 0x031E;
312
313fn io_context(context: String, error: io::Error) -> io::Error {
314    io::Error::new(error.kind(), format!("{context}: {error}"))
315}
316
317/// The CRC-32 (IEEE, as [`crc32`]) lookup table, for use with [`crc32_update`].
318pub fn crc_table() -> [u32; 256] {
319    CRC_TABLE
320}
321
322/// Extend CRC-32 with `bytes`; start at zero & pass each returned CRC to the next call.
323pub fn crc32_update(table: &[u32; 256], crc: u32, bytes: &[u8]) -> u32 {
324    let mut c = !crc;
325    for &b in bytes {
326        c = table[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
327    }
328    !c
329}
330
331fn file_crc_size(path: &Path) -> io::Result<(u32, u64)> {
332    let table = crc_table();
333    let mut f =
334        File::open(path).map_err(|e| io_context(format!("opening {}", path.display()), e))?;
335    let mut crc = 0u32;
336    let mut size = 0u64;
337    let mut buf = vec![0u8; 1 << 16];
338    loop {
339        let n = f
340            .read(&mut buf)
341            .map_err(|e| io_context(format!("reading {}", path.display()), e))?;
342        if n == 0 {
343            break;
344        }
345        crc = crc32_update(&table, crc, &buf[..n]);
346        size += n as u64;
347    }
348    Ok((crc, size))
349}
350
351/// Write `entries` (UTF-8 archive name, source file) to `dest`, in caller-supplied order.
352/// Uses STORED entries, ZIP epoch (1980-01-01 00:00:00), Unix creator version 3.0,
353/// fixed regular-file mode 0644, UTF-8 flags, & no extras/comments/data descriptors.
354/// Identical ordered names & file bytes produce byte-identical archives across runs.
355/// Source mtimes, permissions & host platform do not affect output.
356///
357/// ZIP64 is unsupported: fewer than 65535 entries, names at most 65535 UTF-8 bytes,
358/// & sizes/offsets below 0xffffffff. Invalid limits return `InvalidInput`.
359/// A source whose bytes change between the CRC pass and the copy pass (same size or not) is
360/// rejected with `InvalidInput` rather than archived under the wrong CRC. Names are stored
361/// verbatim, including separators & duplicates; callers choose extraction-safe names.
362/// `dest` is created/truncated directly; errors can leave partial output. It must not
363/// alias any source. Success flushes the buffer but does not fsync or publish atomically.
364pub fn write_zip(dest: &Path, entries: &[(String, PathBuf)]) -> io::Result<()> {
365    if entries.len() >= 0xFFFF {
366        return Err(invalid("too many files for a zip"));
367    }
368    for (name, _) in entries {
369        if name.len() > u16::MAX as usize {
370            return Err(invalid("archive name exceeds 65535 UTF-8 bytes"));
371        }
372    }
373    let io = |e| io_context(format!("writing {}", dest.display()), e);
374    let mut out = BufWriter::new(File::create(dest).map_err(io)?);
375    let mut offset: u64 = 0;
376    let mut central: Vec<u8> = Vec::new();
377    for (name, src) in entries {
378        let (crc, size) = file_crc_size(src)?;
379        if size >= 0xFFFF_FFFF || offset >= 0xFFFF_FFFF {
380            return Err(invalid(format!(
381                "{} is too large for a zip (ZIP64 is not written)",
382                src.display()
383            )));
384        }
385        // Reserve both records before narrowing lengths or offsets to ZIP32 fields.
386        let name_bytes = name.as_bytes();
387        checked_zip32(offset + 30 + name_bytes.len() as u64 + size)?;
388        checked_zip32(central.len() as u64 + 46 + name_bytes.len() as u64)?;
389        let mut local = Vec::with_capacity(30 + name_bytes.len());
390        local.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
391        local.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
392        local.extend_from_slice(&FLAG_UTF8.to_le_bytes());
393        local.extend_from_slice(&0u16.to_le_bytes()); // stored
394        local.extend_from_slice(&DOS_TIME.to_le_bytes());
395        local.extend_from_slice(&DOS_DATE.to_le_bytes());
396        local.extend_from_slice(&crc.to_le_bytes());
397        local.extend_from_slice(&(size as u32).to_le_bytes());
398        local.extend_from_slice(&(size as u32).to_le_bytes());
399        local.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
400        local.extend_from_slice(&0u16.to_le_bytes());
401        local.extend_from_slice(name_bytes);
402        out.write_all(&local).map_err(io)?;
403        let mut f =
404            File::open(src).map_err(|e| io_context(format!("opening {}", src.display()), e))?;
405        // Re-hash while copying: a same-size mutation since the CRC pass must be rejected, not
406        // archived under the first pass's CRC (which `read_zip` would then refuse, and other
407        // readers would misreport as mere corruption of good archived bytes).
408        let (copied_crc, copied) = copy_counted_crc(&mut f, &mut out).map_err(io)?;
409        if copied != size || copied_crc != crc {
410            return Err(invalid(format!("{} changed while zipping", src.display())));
411        }
412
413        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
414        central.extend_from_slice(&FILE_ZIP_MADE_BY.to_le_bytes());
415        central.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
416        central.extend_from_slice(&FLAG_UTF8.to_le_bytes());
417        central.extend_from_slice(&0u16.to_le_bytes());
418        central.extend_from_slice(&DOS_TIME.to_le_bytes());
419        central.extend_from_slice(&DOS_DATE.to_le_bytes());
420        central.extend_from_slice(&crc.to_le_bytes());
421        central.extend_from_slice(&(size as u32).to_le_bytes());
422        central.extend_from_slice(&(size as u32).to_le_bytes());
423        central.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
424        central.extend_from_slice(&0u16.to_le_bytes()); // extra
425        central.extend_from_slice(&0u16.to_le_bytes()); // comment
426        central.extend_from_slice(&0u16.to_le_bytes()); // disk
427        central.extend_from_slice(&0u16.to_le_bytes()); // internal attrs
428        central.extend_from_slice(&FILE_ATTRIBUTES.to_le_bytes());
429        central.extend_from_slice(&(offset as u32).to_le_bytes());
430        central.extend_from_slice(name_bytes);
431        offset += local.len() as u64 + size;
432    }
433    let cd_offset = offset;
434    checked_zip32(cd_offset)?;
435    checked_zip32(central.len() as u64)?;
436    out.write_all(&central).map_err(io)?;
437    let mut end = Vec::with_capacity(22);
438    end.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
439    end.extend_from_slice(&0u16.to_le_bytes());
440    end.extend_from_slice(&0u16.to_le_bytes());
441    end.extend_from_slice(&(entries.len() as u16).to_le_bytes());
442    end.extend_from_slice(&(entries.len() as u16).to_le_bytes());
443    end.extend_from_slice(&(central.len() as u32).to_le_bytes());
444    end.extend_from_slice(&(cd_offset as u32).to_le_bytes());
445    end.extend_from_slice(&0u16.to_le_bytes());
446    out.write_all(&end).map_err(io)?;
447    out.flush().map_err(io)?;
448    Ok(())
449}
450
451/// Copy `src` to `out`, returning the CRC-32 and byte count of what was actually copied, so a
452/// caller with an earlier fingerprint of the source can reject a source mutated mid-stream.
453fn copy_counted_crc<R: Read, W: Write>(src: &mut R, out: &mut W) -> io::Result<(u32, u64)> {
454    let table = crc_table();
455    let mut crc = 0u32;
456    let mut size = 0u64;
457    let mut buf = vec![0u8; 1 << 16];
458    loop {
459        let n = src.read(&mut buf)?;
460        if n == 0 {
461            break;
462        }
463        crc = crc32_update(&table, crc, &buf[..n]);
464        out.write_all(&buf[..n])?;
465        size += n as u64;
466    }
467    Ok((crc, size))
468}
469
470fn checked_zip32(value: u64) -> io::Result<()> {
471    if value >= u32::MAX as u64 {
472        Err(invalid("archive exceeds ZIP32 limits"))
473    } else {
474        Ok(())
475    }
476}
477
478/// Read archives produced by [`write_zip`], checking record bounds, sizes & every CRC.
479/// Loads archive & entry contents into memory (about twice the archive size). This verifier
480/// supports only this writer's STORED, UTF-8, no-extra/comment, single-disk format; other formats
481/// return `InvalidData`. Malformed or truncated archives return errors without indexing outside
482/// input.
483///
484/// **Entry names are untrusted.** They are returned exactly as stored: they may be absolute, hold
485/// `..` components, `\` separators, a drive prefix or control characters. Preventing a zip-slip
486/// write is the caller's responsibility; check each name (for example with
487/// [`StoredZipWriter`]'s name rules, or [`crate::contain::contain_lenient`] against the extraction
488/// root) before joining it to a path. For an archive from a source you do not control use
489/// [`read_zip_limited`], which bounds the memory this call takes.
490pub fn read_zip(path: &Path) -> io::Result<Vec<(String, Vec<u8>)>> {
491    let data = std::fs::read(path)?;
492    parse_zip(&data)
493}
494
495/// [`read_zip`] for an archive that may be hostile: refuses with `InvalidData` an archive file
496/// larger than `max_bytes` before holding more than `max_bytes` of it in memory. Because every
497/// entry is STORED, the entries' total size is below the archive's, so this one cap bounds the
498/// total of the returned contents too (memory is at most about `2 * max_bytes`). The names it
499/// returns are untrusted exactly as for [`read_zip`].
500pub fn read_zip_limited(path: &Path, max_bytes: u64) -> io::Result<Vec<(String, Vec<u8>)>> {
501    let file =
502        File::open(path).map_err(|e| io_context(format!("opening {}", path.display()), e))?;
503    let mut data = Vec::new();
504    // Read one byte past the cap so a file that grows after any size check is still caught.
505    file.take(max_bytes.saturating_add(1))
506        .read_to_end(&mut data)
507        .map_err(|e| io_context(format!("reading {}", path.display()), e))?;
508    if data.len() as u64 > max_bytes {
509        return Err(io::Error::new(
510            io::ErrorKind::InvalidData,
511            format!(
512                "{} is larger than the {max_bytes} byte limit for a zip",
513                path.display()
514            ),
515        ));
516    }
517    parse_zip(&data)
518}
519
520fn parse_zip(data: &[u8]) -> io::Result<Vec<(String, Vec<u8>)>> {
521    let bad = || io::Error::new(io::ErrorKind::InvalidData, "invalid STORED zip");
522    let u16at = |o: usize| -> io::Result<usize> {
523        let bytes: [u8; 2] = data
524            .get(o..o.checked_add(2).ok_or_else(bad)?)
525            .ok_or_else(bad)?
526            .try_into()
527            .map_err(|_| bad())?;
528        Ok(u16::from_le_bytes(bytes) as usize)
529    };
530    let u32at = |o: usize| -> io::Result<u32> {
531        let bytes: [u8; 4] = data
532            .get(o..o.checked_add(4).ok_or_else(bad)?)
533            .ok_or_else(bad)?
534            .try_into()
535            .map_err(|_| bad())?;
536        Ok(u32::from_le_bytes(bytes))
537    };
538    let eocd = data.len().checked_sub(22).ok_or_else(bad)?;
539    if u32at(eocd)? != 0x0605_4b50
540        || u16at(eocd + 4)? != 0
541        || u16at(eocd + 6)? != 0
542        || u16at(eocd + 20)? != 0
543    {
544        return Err(bad());
545    }
546    let count = u16at(eocd + 10)?;
547    if count >= 0xffff || u16at(eocd + 8)? != count {
548        return Err(bad());
549    }
550    let central_size = u32at(eocd + 12)? as usize;
551    let central_start = u32at(eocd + 16)? as usize;
552    if central_start.checked_add(central_size) != Some(eocd) {
553        return Err(bad());
554    }
555    let table = crc_table();
556    let mut p = central_start;
557    let mut local_end = 0;
558    let mut out = Vec::new();
559    for _ in 0..count {
560        let header_end = p.checked_add(46).filter(|&n| n <= eocd).ok_or_else(bad)?;
561        if u32at(p)? != 0x0201_4b50
562            || u16at(p + 8)? != 0x0800
563            || u16at(p + 10)? != 0
564            || u16at(p + 30)? != 0
565            || u16at(p + 32)? != 0
566            || u16at(p + 34)? != 0
567        {
568            return Err(bad());
569        }
570        let crc = u32at(p + 16)?;
571        let size = u32at(p + 24)? as usize;
572        if u32at(p + 20)? as usize != size {
573            return Err(bad());
574        }
575        let name_len = u16at(p + 28)?;
576        let next = header_end
577            .checked_add(name_len)
578            .filter(|&n| n <= eocd)
579            .ok_or_else(bad)?;
580        let name_bytes = &data[header_end..next];
581        let name = std::str::from_utf8(name_bytes)
582            .map_err(|_| bad())?
583            .to_owned();
584        let local = u32at(p + 42)? as usize;
585        let start = local
586            .checked_add(30)
587            .and_then(|n| n.checked_add(name_len))
588            .filter(|&n| n <= central_start)
589            .ok_or_else(bad)?;
590        let end = start
591            .checked_add(size)
592            .filter(|&n| n <= central_start)
593            .ok_or_else(bad)?;
594        if local != local_end
595            || u32at(local)? != 0x0403_4b50
596            || u16at(local + 6)? != 0x0800
597            || u16at(local + 8)? != 0
598            || u32at(local + 14)? != crc
599            || u32at(local + 18)? as usize != size
600            || u32at(local + 22)? as usize != size
601            || u16at(local + 26)? != name_len
602            || u16at(local + 28)? != 0
603            || &data[local + 30..start] != name_bytes
604        {
605            return Err(bad());
606        }
607        let bytes = &data[start..end];
608        if crc32_update(&table, 0, bytes) != crc {
609            return Err(bad());
610        }
611        out.push((name, bytes.to_vec()));
612        local_end = end;
613        p = next;
614    }
615    if p != eocd || local_end != central_start {
616        return Err(bad());
617    }
618    Ok(out)
619}