Skip to main content

pptxboss_core/
zip.rs

1//! ZIP container reader for OPC packages (ECMA-376 Part 2, Annex B; PKWARE
2//! APPNOTE 6.3).
3//!
4//! The central directory is parsed once at open. Entry data is then read
5//! on demand through a [`Source`] with positioned reads, so opening a
6//! 40 MB deck to extract its text costs the central directory plus the XML
7//! parts actually touched, never the embedded media.
8//!
9//! Reading is lenient where the archive is still unambiguous: junk before
10//! the first local header is detected and compensated, data descriptors
11//! with or without their signature are accepted, Zip64 records are
12//! followed, and a truncated central directory yields the entries that
13//! were readable. What was tolerated is recorded in [`Layout`] so a
14//! verifier can report it.
15
16use std::fs::File;
17use std::io;
18use std::path::Path;
19use std::sync::Arc;
20
21use memchr::memmem;
22
23use crate::crc32::crc32;
24use crate::error::{Error, Result};
25use crate::hash::FastMap;
26
27const LOCAL_HEADER_SIG: [u8; 4] = 0x0403_4b50u32.to_le_bytes();
28const CENTRAL_HEADER_SIG: [u8; 4] = 0x0201_4b50u32.to_le_bytes();
29const END_RECORD_SIG: [u8; 4] = 0x0605_4b50u32.to_le_bytes();
30const ZIP64_END_RECORD_SIG: [u8; 4] = 0x0606_4b50u32.to_le_bytes();
31const ZIP64_LOCATOR_SIG: [u8; 4] = 0x0706_4b50u32.to_le_bytes();
32
33const END_RECORD_LEN: usize = 22;
34const ZIP64_LOCATOR_LEN: usize = 20;
35const ZIP64_END_RECORD_LEN: usize = 56;
36const CENTRAL_HEADER_LEN: usize = 46;
37const LOCAL_HEADER_LEN: usize = 30;
38/// Files up to this size are read whole in one call and kept.
39const WHOLE_FILE_LEN: u64 = 256 * 1024;
40/// For larger files, the bytes read from the end in one call and kept: the
41/// end records (a comment can be 65535 bytes long) and the central directory.
42const TAIL_LEN: u64 = 66 * 1024;
43const ZIP64_EXTRA_ID: u16 = 0x0001;
44
45/// Compression method 0: the data is stored as-is.
46pub const METHOD_STORED: u16 = 0;
47/// Compression method 8: raw DEFLATE.
48pub const METHOD_DEFLATE: u16 = 8;
49
50/// General purpose bit 0: the entry is encrypted (Annex B forbids it).
51pub const FLAG_ENCRYPTED: u16 = 1;
52/// General purpose bit 3: CRC and sizes follow the data in a descriptor.
53pub const FLAG_DATA_DESCRIPTOR: u16 = 1 << 3;
54/// General purpose bit 11: the file name is UTF-8.
55pub const FLAG_UTF8_NAMES: u16 = 1 << 11;
56
57/// Random access to the bytes of an archive.
58pub trait Source: Send + Sync {
59    /// Total length in bytes.
60    fn len(&self) -> u64;
61    fn is_empty(&self) -> bool {
62        self.len() == 0
63    }
64    /// Fills `buf` from `offset`; fails if the range is out of bounds.
65    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
66    /// The whole content when it already sits in memory, so reads can borrow it.
67    fn as_bytes(&self) -> Option<&[u8]> {
68        None
69    }
70}
71
72impl Source for Vec<u8> {
73    fn len(&self) -> u64 {
74        Vec::len(self) as u64
75    }
76
77    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
78        let start = usize::try_from(offset).map_err(|_| out_of_bounds())?;
79        let end = start.checked_add(buf.len()).ok_or_else(out_of_bounds)?;
80        let slice = self.get(start..end).ok_or_else(out_of_bounds)?;
81        buf.copy_from_slice(slice);
82        Ok(())
83    }
84
85    fn as_bytes(&self) -> Option<&[u8]> {
86        Some(self)
87    }
88}
89
90/// A file read with positioned reads; no seeking, so shared across threads.
91pub struct FileSource {
92    file: File,
93    len: u64,
94}
95
96impl FileSource {
97    pub fn new(file: File) -> io::Result<Self> {
98        let len = file.metadata()?.len();
99        Ok(Self { file, len })
100    }
101
102    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
103        Self::new(File::open(path)?)
104    }
105}
106
107impl Source for FileSource {
108    fn len(&self) -> u64 {
109        self.len
110    }
111
112    #[cfg(unix)]
113    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
114        use std::os::unix::fs::FileExt;
115        self.file.read_exact_at(buf, offset)
116    }
117
118    #[cfg(windows)]
119    fn read_at(&self, offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
120        use std::os::windows::fs::FileExt;
121        let mut offset = offset;
122        while !buf.is_empty() {
123            let read = self.file.seek_read(buf, offset)?;
124            if read == 0 {
125                return Err(out_of_bounds());
126            }
127            buf = &mut buf[read..];
128            offset += read as u64;
129        }
130        Ok(())
131    }
132}
133
134fn out_of_bounds() -> io::Error {
135    io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of archive")
136}
137
138/// One item of the central directory, exactly as recorded.
139#[derive(Clone, Debug)]
140pub struct Entry {
141    /// The item name decoded as UTF-8, with invalid sequences replaced.
142    pub name: String,
143    /// The item name bytes as written.
144    pub raw_name: Vec<u8>,
145    pub method: u16,
146    pub flags: u16,
147    pub version_needed: u16,
148    pub crc32: u32,
149    pub compressed_size: u64,
150    pub uncompressed_size: u64,
151    /// Local header offset as recorded (before any shift the archive needed).
152    pub header_offset: u64,
153    /// Offset of this entry's central directory header.
154    pub central_offset: u64,
155    /// Header ids of the extra fields present in the central header.
156    pub extra_ids: Vec<u16>,
157    /// True when the central header's comment length was nonzero.
158    pub has_comment: bool,
159}
160
161impl Entry {
162    pub fn is_directory(&self) -> bool {
163        self.raw_name.last() == Some(&b'/')
164    }
165
166    pub fn is_encrypted(&self) -> bool {
167        self.flags & FLAG_ENCRYPTED != 0
168    }
169
170    pub fn has_data_descriptor(&self) -> bool {
171        self.flags & FLAG_DATA_DESCRIPTOR != 0
172    }
173
174    pub fn names_are_utf8(&self) -> bool {
175        self.flags & FLAG_UTF8_NAMES != 0
176    }
177
178    pub fn has_zip64_extra(&self) -> bool {
179        self.extra_ids.contains(&ZIP64_EXTRA_ID)
180    }
181}
182
183/// The fixed fields of an entry's local file header.
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct LocalHeader {
186    pub version_needed: u16,
187    pub flags: u16,
188    pub method: u16,
189    pub crc32: u32,
190    pub compressed_size: u32,
191    pub uncompressed_size: u32,
192    pub raw_name: Vec<u8>,
193    pub extra_len: u16,
194    /// Absolute offset of the first data byte.
195    pub data_offset: u64,
196}
197
198/// What the reader found and tolerated about the archive's structure.
199#[derive(Clone, Debug, Default)]
200pub struct Layout {
201    /// Total length of the source in bytes.
202    pub len: u64,
203    /// Absolute offset of the end of central directory record.
204    pub end_record_offset: u64,
205    /// Whether Zip64 end records were present and used.
206    pub zip64: bool,
207    /// Bytes to add to every recorded offset; nonzero when junk precedes
208    /// the archive.
209    pub offset_shift: u64,
210    /// Central directory offset as recorded.
211    pub central_offset: u64,
212    /// Central directory size as recorded.
213    pub central_size: u64,
214    /// Entry count as recorded.
215    pub declared_entries: u64,
216    /// True when fewer central headers were readable than declared.
217    pub truncated_central: bool,
218    /// The archive comment.
219    pub comment: Vec<u8>,
220    /// Bytes between the end of the last central header and the end
221    /// records, or between the end record and the end of the source after
222    /// the comment; zero for a well-formed archive.
223    pub trailing_garbage: u64,
224    /// True when no central directory was found and the entry list was
225    /// rebuilt by scanning for local file headers.
226    pub reconstructed: bool,
227}
228
229/// A parsed archive: central directory in memory, entry data on demand.
230pub struct Archive {
231    source: Arc<dyn Source>,
232    /// The tail of the file as read at open; empty for in-memory sources.
233    cache: Box<[u8]>,
234    cache_start: u64,
235    entries: Vec<Entry>,
236    index: FastMap<String, usize>,
237    duplicates: Vec<usize>,
238    layout: Layout,
239}
240
241/// Where a requested byte range was found.
242#[derive(Clone, Copy)]
243enum Located {
244    /// At this index of the source's in-memory bytes.
245    Bytes(usize),
246    /// At this index of the tail cache.
247    Cache(usize),
248    /// Read into the caller's scratch buffer.
249    Scratch,
250}
251
252/// Serves `len` bytes at `offset` from in-memory bytes or `cache` when
253/// possible, reading into `scratch` otherwise.
254fn locate_range(
255    source: &dyn Source,
256    cache: &[u8],
257    cache_start: u64,
258    offset: u64,
259    len: usize,
260    scratch: &mut Vec<u8>,
261) -> io::Result<Located> {
262    let end = offset.checked_add(len as u64).ok_or_else(out_of_bounds)?;
263    if let Some(bytes) = source.as_bytes() {
264        if end > bytes.len() as u64 {
265            return Err(out_of_bounds());
266        }
267        return Ok(Located::Bytes(offset as usize));
268    }
269    if offset >= cache_start && end <= cache_start + cache.len() as u64 {
270        return Ok(Located::Cache((offset - cache_start) as usize));
271    }
272    scratch.clear();
273    scratch.resize(len, 0);
274    source.read_at(offset, scratch)?;
275    Ok(Located::Scratch)
276}
277
278fn view_range<'a>(
279    source: &'a dyn Source,
280    cache: &'a [u8],
281    located: Located,
282    len: usize,
283    scratch: &'a [u8],
284) -> &'a [u8] {
285    match located {
286        Located::Bytes(start) => &source.as_bytes().unwrap_or_default()[start..start + len],
287        Located::Cache(start) => &cache[start..start + len],
288        Located::Scratch => &scratch[..len],
289    }
290}
291
292impl Archive {
293    /// Parses the central directory of `source`.
294    pub fn open(source: Arc<dyn Source>) -> Result<Self> {
295        let len = source.len();
296        let tail_len = match len <= WHOLE_FILE_LEN {
297            true => len,
298            false => TAIL_LEN,
299        };
300        let tail_len = usize::try_from(tail_len).map_err(|_| Error::NotZip)?;
301        let tail_start = len - tail_len as u64;
302        let cache: Box<[u8]> = match source.as_bytes() {
303            Some(_) => Box::default(),
304            None => {
305                let mut tail = vec![0u8; tail_len];
306                source.read_at(tail_start, &mut tail)?;
307                tail.into_boxed_slice()
308            }
309        };
310        let cache_start = tail_start;
311        let tail: &[u8] = match source.as_bytes() {
312            Some(bytes) => &bytes[tail_start as usize..],
313            None => &cache,
314        };
315        let Some(end_pos) = find_end_record(tail) else {
316            return Self::reconstruct(source, len);
317        };
318        let end = &tail[end_pos..];
319        let end_record_offset = tail_start + end_pos as u64;
320        let comment_len = usize::from(u16_at(end, 20));
321        let comment = end[END_RECORD_LEN..]
322            .get(..comment_len)
323            .unwrap_or(&end[END_RECORD_LEN..])
324            .to_vec();
325        let after_comment = end.len().saturating_sub(END_RECORD_LEN + comment_len) as u64;
326
327        let mut layout = Layout {
328            len,
329            end_record_offset,
330            comment,
331            trailing_garbage: after_comment,
332            ..Layout::default()
333        };
334        let mut declared_entries = u64::from(u16_at(end, 10));
335        let mut central_size = u64::from(u32_at(end, 12));
336        let mut central_offset = u64::from(u32_at(end, 16));
337        let mut records_start = end_record_offset;
338
339        let locator_pos = end_pos.checked_sub(ZIP64_LOCATOR_LEN);
340        let has_locator = locator_pos.is_some_and(|pos| tail[pos..pos + 4] == ZIP64_LOCATOR_SIG);
341        let needs_zip64 = declared_entries == 0xffff
342            || central_size == 0xffff_ffff
343            || central_offset == 0xffff_ffff;
344        if has_locator && (needs_zip64 || true) {
345            let locator = &tail[locator_pos.unwrap_or(0)..];
346            let declared_z64 = u64_at(locator, 8);
347            let locator_offset = end_record_offset - ZIP64_LOCATOR_LEN as u64;
348            let (z64_pos, shift) = locate_zip64_end(&source, declared_z64, locator_offset)?;
349            let mut record = [0u8; ZIP64_END_RECORD_LEN];
350            source.read_at(z64_pos, &mut record)?;
351            layout.zip64 = true;
352            layout.offset_shift = shift;
353            declared_entries = u64_at(&record, 32);
354            central_size = u64_at(&record, 40);
355            central_offset = u64_at(&record, 48);
356            records_start = z64_pos;
357        } else if needs_zip64 {
358            return Err(Error::zip(
359                end_record_offset,
360                "end record needs a Zip64 record that is missing",
361            ));
362        }
363
364        layout.declared_entries = declared_entries;
365        layout.central_size = central_size;
366        layout.central_offset = central_offset;
367
368        let central_start = match locate_central(
369            &source,
370            central_offset,
371            central_size,
372            records_start,
373            layout.offset_shift,
374        ) {
375            Ok(start) => start,
376            Err(_) => return Self::reconstruct(source, len),
377        };
378        layout.offset_shift = central_start.wrapping_sub(central_offset);
379        let readable = records_start.saturating_sub(central_start);
380        let central_len = usize::try_from(central_size.min(readable))
381            .map_err(|_| Error::zip(central_start, "central directory too large"))?;
382        let mut scratch = Vec::new();
383        let located = locate_range(
384            source.as_ref(),
385            &cache,
386            cache_start,
387            central_start,
388            central_len,
389            &mut scratch,
390        )?;
391        let central = view_range(source.as_ref(), &cache, located, central_len, &scratch);
392        layout.trailing_garbage += readable.saturating_sub(central_size);
393
394        let mut entries =
395            Vec::with_capacity(usize::try_from(declared_entries).unwrap_or(0).min(1 << 16));
396        let mut cursor = 0usize;
397        while cursor + CENTRAL_HEADER_LEN <= central.len()
398            && (entries.len() as u64) < declared_entries
399        {
400            let header = &central[cursor..];
401            if header[..4] != CENTRAL_HEADER_SIG {
402                break;
403            }
404            let name_len = usize::from(u16_at(header, 28));
405            let extra_len = usize::from(u16_at(header, 30));
406            let comment_len = usize::from(u16_at(header, 32));
407            let total = CENTRAL_HEADER_LEN + name_len + extra_len + comment_len;
408            if cursor + total > central.len() {
409                break;
410            }
411            let raw_name = header[CENTRAL_HEADER_LEN..CENTRAL_HEADER_LEN + name_len].to_vec();
412            let extra =
413                &header[CENTRAL_HEADER_LEN + name_len..CENTRAL_HEADER_LEN + name_len + extra_len];
414            let mut entry = Entry {
415                name: String::from_utf8_lossy(&raw_name).into_owned(),
416                raw_name,
417                method: u16_at(header, 10),
418                flags: u16_at(header, 8),
419                version_needed: u16_at(header, 6),
420                crc32: u32_at(header, 16),
421                compressed_size: u64::from(u32_at(header, 20)),
422                uncompressed_size: u64::from(u32_at(header, 24)),
423                header_offset: u64::from(u32_at(header, 42)),
424                central_offset: central_start + cursor as u64,
425                extra_ids: Vec::new(),
426                has_comment: comment_len > 0,
427            };
428            apply_extras(&mut entry, extra);
429            entries.push(entry);
430            cursor += total;
431        }
432        layout.truncated_central = (entries.len() as u64) < declared_entries;
433
434        let mut index = FastMap::default();
435        index.reserve(entries.len());
436        let mut duplicates = Vec::new();
437        for (i, entry) in entries.iter().enumerate() {
438            if index.contains_key(&entry.name) {
439                duplicates.push(i);
440                continue;
441            }
442            index.insert(entry.name.clone(), i);
443        }
444
445        Ok(Self {
446            source,
447            cache,
448            cache_start,
449            entries,
450            index,
451            duplicates,
452            layout,
453        })
454    }
455
456    /// Rebuilds the entry list from local file headers when the central
457    /// directory is missing or unreadable (a truncated download, a file
458    /// cut before its directory). Entries with a data descriptor take their
459    /// sizes from the descriptor found before the next header.
460    fn reconstruct(source: Arc<dyn Source>, len: u64) -> Result<Self> {
461        let cache: Box<[u8]> = Box::default();
462        let cache_start = 0;
463        let total = usize::try_from(len).map_err(|_| Error::NotZip)?;
464        let mut data = vec![0u8; total];
465        source.read_at(0, &mut data)?;
466        let positions: Vec<usize> = memmem::find_iter(&data, &LOCAL_HEADER_SIG).collect();
467        if positions.is_empty() {
468            return Err(Error::NotZip);
469        }
470        let mut entries = Vec::with_capacity(positions.len());
471        for (i, &pos) in positions.iter().enumerate() {
472            let Some(header) = data.get(pos..pos + LOCAL_HEADER_LEN) else {
473                break;
474            };
475            let name_len = usize::from(u16_at(header, 26));
476            let extra_len = usize::from(u16_at(header, 28));
477            let data_offset = pos + LOCAL_HEADER_LEN + name_len + extra_len;
478            let Some(raw_name) =
479                data.get(pos + LOCAL_HEADER_LEN..pos + LOCAL_HEADER_LEN + name_len)
480            else {
481                break;
482            };
483            if raw_name.is_empty() || raw_name.contains(&0) || data_offset > data.len() {
484                continue;
485            }
486            let flags = u16_at(header, 6);
487            let next = positions.get(i + 1).copied().unwrap_or_else(|| {
488                memmem::find(&data[data_offset..], &CENTRAL_HEADER_SIG)
489                    .map_or(data.len(), |rel| data_offset + rel)
490            });
491            let (crc, compressed_size, uncompressed_size) = match flags & FLAG_DATA_DESCRIPTOR != 0
492            {
493                false => (
494                    u32_at(header, 14),
495                    u64::from(u32_at(header, 18)),
496                    u64::from(u32_at(header, 22)),
497                ),
498                true => match descriptor_before(&data, data_offset, next) {
499                    Some(descriptor) => descriptor,
500                    None => continue,
501                },
502            };
503            entries.push(Entry {
504                name: String::from_utf8_lossy(raw_name).into_owned(),
505                raw_name: raw_name.to_vec(),
506                method: u16_at(header, 8),
507                flags,
508                version_needed: u16_at(header, 4),
509                crc32: crc,
510                compressed_size,
511                uncompressed_size,
512                header_offset: pos as u64,
513                central_offset: 0,
514                extra_ids: Vec::new(),
515                has_comment: false,
516            });
517        }
518        if entries.is_empty() {
519            return Err(Error::NotZip);
520        }
521        let mut index = FastMap::default();
522        let mut duplicates = Vec::new();
523        for (i, entry) in entries.iter().enumerate() {
524            if index.contains_key(&entry.name) {
525                duplicates.push(i);
526                continue;
527            }
528            index.insert(entry.name.clone(), i);
529        }
530        let layout = Layout {
531            len,
532            declared_entries: entries.len() as u64,
533            reconstructed: true,
534            ..Layout::default()
535        };
536        Ok(Self {
537            source,
538            cache,
539            cache_start,
540            entries,
541            index,
542            duplicates,
543            layout,
544        })
545    }
546
547    /// Opens an archive held entirely in memory.
548    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
549        Self::open(Arc::new(bytes))
550    }
551
552    /// Opens a file with positioned reads.
553    pub fn open_path(path: impl AsRef<Path>) -> Result<Self> {
554        Self::open(Arc::new(FileSource::open(path)?))
555    }
556
557    pub fn source(&self) -> &Arc<dyn Source> {
558        &self.source
559    }
560
561    pub fn entries(&self) -> &[Entry] {
562        &self.entries
563    }
564
565    /// The first entry with this exact name.
566    pub fn entry(&self, name: &str) -> Option<&Entry> {
567        self.index.get(name).map(|&i| &self.entries[i])
568    }
569
570    pub fn get(&self, index: usize) -> Option<&Entry> {
571        self.entries.get(index)
572    }
573
574    /// Indexes of entries whose name repeats an earlier entry's name.
575    pub fn duplicates(&self) -> &[usize] {
576        &self.duplicates
577    }
578
579    pub fn layout(&self) -> &Layout {
580        &self.layout
581    }
582
583    /// Serves `len` bytes at `offset`, borrowing them from memory when the
584    /// source or the tail cache holds them and reading into `scratch` otherwise.
585    fn locate(&self, offset: u64, len: usize, scratch: &mut Vec<u8>) -> Result<Located> {
586        locate_range(
587            self.source.as_ref(),
588            &self.cache,
589            self.cache_start,
590            offset,
591            len,
592            scratch,
593        )
594        .map_err(|_| Error::zip(offset, "read past the end of the archive"))
595    }
596
597    fn view<'a>(&'a self, located: Located, len: usize, scratch: &'a [u8]) -> &'a [u8] {
598        view_range(self.source.as_ref(), &self.cache, located, len, scratch)
599    }
600
601    /// The compressed bytes of `entry`: one read covering the local header
602    /// and the data when the sizes are known, a second read otherwise.
603    fn raw_slice<'a>(&'a self, entry: &Entry, scratch: &'a mut Vec<u8>) -> Result<&'a [u8]> {
604        let offset = entry.header_offset.wrapping_add(self.layout.offset_shift);
605        let available = self
606            .layout
607            .len
608            .checked_sub(offset)
609            .filter(|available| *available >= LOCAL_HEADER_LEN as u64)
610            .ok_or_else(|| Error::zip(offset, "local header out of bounds"))?;
611        let size_hint = match entry.compressed_size > 0 || entry.uncompressed_size == 0 {
612            true => entry.compressed_size,
613            false => 0,
614        };
615        let want = (LOCAL_HEADER_LEN + entry.raw_name.len() + 64) as u64 + size_hint;
616        let want = usize::try_from(want.min(available))
617            .map_err(|_| Error::zip(offset, "entry too large"))?;
618        let first = self.locate(offset, want, scratch)?;
619        let (start, size, fits) = {
620            let block = self.view(first, want, scratch);
621            if block[..4] != LOCAL_HEADER_SIG {
622                return Err(Error::zip(offset, "bad local header signature"));
623            }
624            let start =
625                LOCAL_HEADER_LEN + usize::from(u16_at(block, 26)) + usize::from(u16_at(block, 28));
626            let size = self.compressed_len(entry, offset + start as u64)?;
627            (start, size, start + size <= block.len())
628        };
629        if fits {
630            return Ok(&self.view(first, want, scratch)[start..start + size]);
631        }
632        let second = self.locate(offset + start as u64, size, scratch)?;
633        Ok(self.view(second, size, scratch))
634    }
635
636    /// Reads and checks the local header of `entry`.
637    pub fn local_header(&self, entry: &Entry) -> Result<LocalHeader> {
638        let offset = entry.header_offset.wrapping_add(self.layout.offset_shift);
639        let mut scratch = Vec::new();
640        let located = self
641            .locate(offset, LOCAL_HEADER_LEN, &mut scratch)
642            .map_err(|_| Error::zip(offset, "local header out of bounds"))?;
643        let mut fixed = [0u8; LOCAL_HEADER_LEN];
644        fixed.copy_from_slice(self.view(located, LOCAL_HEADER_LEN, &scratch));
645        if fixed[..4] != LOCAL_HEADER_SIG {
646            return Err(Error::zip(offset, "bad local header signature"));
647        }
648        let name_len = usize::from(u16_at(&fixed, 26));
649        let extra_len = u16_at(&fixed, 28);
650        let located = self.locate(offset + LOCAL_HEADER_LEN as u64, name_len, &mut scratch)?;
651        let raw_name = self.view(located, name_len, &scratch).to_vec();
652        Ok(LocalHeader {
653            version_needed: u16_at(&fixed, 4),
654            flags: u16_at(&fixed, 6),
655            method: u16_at(&fixed, 8),
656            crc32: u32_at(&fixed, 14),
657            compressed_size: u32_at(&fixed, 18),
658            uncompressed_size: u32_at(&fixed, 22),
659            raw_name,
660            extra_len,
661            data_offset: offset + (LOCAL_HEADER_LEN + name_len + usize::from(extra_len)) as u64,
662        })
663    }
664
665    /// Reads the compressed bytes of `entry` into `out` (cleared first).
666    pub fn read_raw(&self, entry: &Entry, out: &mut Vec<u8>) -> Result<()> {
667        COMPRESSED.with(|cell| {
668            let mut scratch = cell.borrow_mut();
669            let raw = self.raw_slice(entry, &mut scratch)?;
670            out.clear();
671            out.extend_from_slice(raw);
672            Ok(())
673        })
674    }
675
676    /// Reads and decompresses `entry` into `out` (cleared first).
677    pub fn read(&self, entry: &Entry, out: &mut Vec<u8>) -> Result<()> {
678        if entry.is_encrypted() {
679            return Err(Error::Unsupported(format!(
680                "encrypted zip entry {}",
681                entry.name
682            )));
683        }
684        match entry.method {
685            METHOD_STORED => self.read_raw(entry, out),
686            METHOD_DEFLATE => COMPRESSED.with(|cell| {
687                let mut scratch = cell.borrow_mut();
688                let raw = self.raw_slice(entry, &mut scratch)?;
689                out.clear();
690                let expected = usize::try_from(entry.uncompressed_size).unwrap_or(0);
691                inflate(raw, expected, out)
692            }),
693            other => Err(Error::Unsupported(format!(
694                "zip compression method {other} for {}",
695                entry.name
696            ))),
697        }
698    }
699
700    pub fn read_to_vec(&self, entry: &Entry) -> Result<Vec<u8>> {
701        let mut out = Vec::new();
702        self.read(entry, &mut out)?;
703        Ok(out)
704    }
705
706    /// Whether `data` (the decompressed bytes) matches the recorded CRC-32.
707    pub fn crc_matches(entry: &Entry, data: &[u8]) -> bool {
708        crc32(data) == entry.crc32
709    }
710
711    fn compressed_len(&self, entry: &Entry, data_offset: u64) -> Result<usize> {
712        let recorded = entry.compressed_size;
713        let known = recorded > 0 || entry.uncompressed_size == 0;
714        let size = match known {
715            true => recorded,
716            false => self.next_boundary(entry).saturating_sub(data_offset),
717        };
718        let end = data_offset
719            .checked_add(size)
720            .filter(|end| *end <= self.layout.len);
721        if end.is_none() {
722            return Err(Error::zip(
723                data_offset,
724                format!("data of {} runs past the end of the archive", entry.name),
725            ));
726        }
727        usize::try_from(size).map_err(|_| Error::zip(data_offset, "entry too large"))
728    }
729
730    fn next_boundary(&self, entry: &Entry) -> u64 {
731        let shift = self.layout.offset_shift;
732        let start = entry.header_offset.wrapping_add(shift);
733        self.entries
734            .iter()
735            .map(|other| other.header_offset.wrapping_add(shift))
736            .filter(|offset| *offset > start)
737            .min()
738            .unwrap_or(self.layout.central_offset.wrapping_add(shift))
739    }
740}
741
742/// Reads the data descriptor that ends just before `next`, with or without
743/// its signature, returning `(crc, compressed size, uncompressed size)`
744/// when the compressed size is consistent with the span it describes.
745fn descriptor_before(data: &[u8], data_offset: usize, next: usize) -> Option<(u32, u64, u64)> {
746    for (len, signed) in [(16usize, true), (12, false)] {
747        let Some(start) = next.checked_sub(len) else {
748            continue;
749        };
750        if start < data_offset {
751            continue;
752        }
753        let descriptor = &data[start..next];
754        let body = match signed {
755            true if descriptor[..4] == 0x0807_4b50u32.to_le_bytes() => &descriptor[4..],
756            true => continue,
757            false => descriptor,
758        };
759        let compressed = u64::from(u32_at(body, 4));
760        if compressed == (start - data_offset) as u64 {
761            return Some((u32_at(body, 0), compressed, u64::from(u32_at(body, 8))));
762        }
763    }
764    None
765}
766
767fn find_end_record(tail: &[u8]) -> Option<usize> {
768    let mut candidates =
769        memmem::rfind_iter(tail, &END_RECORD_SIG).filter(|pos| pos + END_RECORD_LEN <= tail.len());
770    let mut fallback = None;
771    for pos in candidates.by_ref() {
772        let comment_len = usize::from(u16_at(&tail[pos..], 20));
773        if pos + END_RECORD_LEN + comment_len == tail.len() {
774            return Some(pos);
775        }
776        fallback.get_or_insert(pos);
777    }
778    fallback
779}
780
781fn locate_zip64_end(
782    source: &Arc<dyn Source>,
783    declared: u64,
784    locator_offset: u64,
785) -> Result<(u64, u64)> {
786    let mut sig = [0u8; 4];
787    if source.read_at(declared, &mut sig).is_ok() && sig == ZIP64_END_RECORD_SIG {
788        return Ok((declared, 0));
789    }
790    let candidate = locator_offset.saturating_sub(ZIP64_END_RECORD_LEN as u64);
791    source.read_at(candidate, &mut sig)?;
792    if sig != ZIP64_END_RECORD_SIG {
793        return Err(Error::zip(
794            declared,
795            "zip64 end of central directory record not found",
796        ));
797    }
798    Ok((candidate, candidate.wrapping_sub(declared)))
799}
800
801fn locate_central(
802    source: &Arc<dyn Source>,
803    declared: u64,
804    size: u64,
805    records_start: u64,
806    shift: u64,
807) -> Result<u64> {
808    let mut sig = [0u8; 4];
809    let shifted = declared.wrapping_add(shift);
810    if size == 0 {
811        return Ok(shifted);
812    }
813    if source.read_at(shifted, &mut sig).is_ok() && sig == CENTRAL_HEADER_SIG {
814        return Ok(shifted);
815    }
816    let candidate = records_start.saturating_sub(size);
817    source
818        .read_at(candidate, &mut sig)
819        .map_err(|_| Error::zip(declared, "central directory out of bounds"))?;
820    if sig != CENTRAL_HEADER_SIG {
821        return Err(Error::zip(declared, "central directory not found"));
822    }
823    Ok(candidate)
824}
825
826fn apply_extras(entry: &mut Entry, mut extra: &[u8]) {
827    while extra.len() >= 4 {
828        let id = u16_at(extra, 0);
829        let len = usize::from(u16_at(extra, 2));
830        let body = extra.get(4..4 + len).unwrap_or(&extra[4..]);
831        entry.extra_ids.push(id);
832        if id == ZIP64_EXTRA_ID {
833            let mut fields = body
834                .as_chunks::<8>()
835                .0
836                .iter()
837                .map(|chunk| u64::from_le_bytes(*chunk));
838            if entry.uncompressed_size == 0xffff_ffff {
839                entry.uncompressed_size = fields.next().unwrap_or(entry.uncompressed_size);
840            }
841            if entry.compressed_size == 0xffff_ffff {
842                entry.compressed_size = fields.next().unwrap_or(entry.compressed_size);
843            }
844            if entry.header_offset == 0xffff_ffff {
845                entry.header_offset = fields.next().unwrap_or(entry.header_offset);
846            }
847        }
848        extra = extra.get(4 + len..).unwrap_or(&[]);
849    }
850}
851
852/// Inflates a raw DEFLATE stream, pre-sizing `out` to `expected` bytes.
853pub fn inflate(input: &[u8], expected: usize, out: &mut Vec<u8>) -> Result<()> {
854    out.clear();
855    crate::inflate::inflate(input, expected, out)
856}
857
858thread_local! {
859    /// Scratch buffer for compressed bytes read from the source.
860    static COMPRESSED: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
861}
862
863#[inline]
864fn u16_at(bytes: &[u8], offset: usize) -> u16 {
865    u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
866}
867
868#[inline]
869fn u32_at(bytes: &[u8], offset: usize) -> u32 {
870    u32::from_le_bytes([
871        bytes[offset],
872        bytes[offset + 1],
873        bytes[offset + 2],
874        bytes[offset + 3],
875    ])
876}
877
878#[inline]
879fn u64_at(bytes: &[u8], offset: usize) -> u64 {
880    let mut chunk = [0u8; 8];
881    chunk.copy_from_slice(&bytes[offset..offset + 8]);
882    u64::from_le_bytes(chunk)
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use pptxboss_testkit::ZipBuilder;
889
890    fn names(archive: &Archive) -> Vec<&str> {
891        archive
892            .entries()
893            .iter()
894            .map(|entry| entry.name.as_str())
895            .collect()
896    }
897
898    #[test]
899    fn stored_and_deflated_entries_round_trip() {
900        let bytes = ZipBuilder::new()
901            .stored("a.txt", b"hello")
902            .deflated("dir/b.xml", &[b'x'; 5000])
903            .build();
904        let archive = Archive::from_bytes(bytes).unwrap();
905        assert_eq!(names(&archive), ["a.txt", "dir/b.xml"]);
906        let a = archive.entry("a.txt").unwrap();
907        assert_eq!(archive.read_to_vec(a).unwrap(), b"hello");
908        let b = archive.entry("dir/b.xml").unwrap();
909        assert_eq!(b.method, METHOD_DEFLATE);
910        assert_eq!(archive.read_to_vec(b).unwrap(), vec![b'x'; 5000]);
911        assert!(Archive::crc_matches(b, &[b'x'; 5000]));
912        assert_eq!(archive.layout().offset_shift, 0);
913        assert!(!archive.layout().zip64);
914    }
915
916    #[test]
917    fn data_descriptors_with_and_without_signature_are_read() {
918        for builder in [
919            ZipBuilder::new().with_data_descriptors(),
920            ZipBuilder::new().with_descriptor_signature(),
921        ] {
922            let bytes = builder
923                .deflated("a", b"alpha")
924                .deflated("b", b"beta")
925                .build();
926            let archive = Archive::from_bytes(bytes).unwrap();
927            assert!(archive.entry("a").unwrap().has_data_descriptor());
928            assert_eq!(
929                archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
930                b"alpha"
931            );
932            assert_eq!(
933                archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
934                b"beta"
935            );
936        }
937    }
938
939    #[test]
940    fn zip64_records_are_followed() {
941        let bytes = ZipBuilder::new()
942            .with_zip64()
943            .deflated("one", b"1")
944            .stored("two", b"22")
945            .build();
946        let archive = Archive::from_bytes(bytes).unwrap();
947        assert!(archive.layout().zip64);
948        assert_eq!(archive.layout().declared_entries, 2);
949        assert!(archive.entry("two").unwrap().has_zip64_extra());
950        assert_eq!(
951            archive.read_to_vec(archive.entry("one").unwrap()).unwrap(),
952            b"1"
953        );
954        assert_eq!(
955            archive.read_to_vec(archive.entry("two").unwrap()).unwrap(),
956            b"22"
957        );
958    }
959
960    #[test]
961    fn junk_before_the_archive_is_compensated() {
962        let bytes = ZipBuilder::new()
963            .with_prefix(b"JUNKJUNKJUNK")
964            .stored("a", b"A")
965            .deflated("b", b"BB")
966            .build();
967        let archive = Archive::from_bytes(bytes).unwrap();
968        assert_eq!(archive.layout().offset_shift, 12);
969        assert_eq!(
970            archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
971            b"A"
972        );
973        assert_eq!(
974            archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
975            b"BB"
976        );
977    }
978
979    #[test]
980    fn junk_before_a_zip64_archive_is_compensated() {
981        let bytes = ZipBuilder::new()
982            .with_zip64()
983            .with_prefix(b"xx")
984            .stored("a", b"A")
985            .build();
986        let archive = Archive::from_bytes(bytes).unwrap();
987        assert_eq!(archive.layout().offset_shift, 2);
988        assert_eq!(
989            archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
990            b"A"
991        );
992    }
993
994    #[test]
995    fn the_archive_comment_is_kept_and_does_not_hide_the_end_record() {
996        let comment = b"a comment containing PK\x05\x06 the signature bytes";
997        let bytes = ZipBuilder::new()
998            .with_comment(comment)
999            .stored("a", b"A")
1000            .build();
1001        let archive = Archive::from_bytes(bytes).unwrap();
1002        assert_eq!(archive.layout().comment, comment);
1003        assert_eq!(archive.layout().trailing_garbage, 0);
1004        assert_eq!(names(&archive), ["a"]);
1005    }
1006
1007    #[test]
1008    fn duplicate_names_keep_the_first_and_record_the_rest() {
1009        let bytes = ZipBuilder::new()
1010            .stored("a", b"first")
1011            .stored("a", b"second")
1012            .stored("b", b"")
1013            .build();
1014        let archive = Archive::from_bytes(bytes).unwrap();
1015        assert_eq!(archive.duplicates(), &[1]);
1016        assert_eq!(
1017            archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
1018            b"first"
1019        );
1020    }
1021
1022    #[test]
1023    fn not_a_zip_is_reported() {
1024        assert!(matches!(
1025            Archive::from_bytes(b"<?xml version=\"1.0\"?><x/>".to_vec()),
1026            Err(Error::NotZip)
1027        ));
1028        assert!(matches!(
1029            Archive::from_bytes(Vec::new()),
1030            Err(Error::NotZip)
1031        ));
1032        assert!(matches!(
1033            Archive::from_bytes(b"PK\x03\x04 and then nothing useful at all".to_vec()),
1034            Err(Error::NotZip)
1035        ));
1036    }
1037
1038    #[test]
1039    fn a_missing_central_directory_is_rebuilt_from_local_headers() {
1040        for builder in [
1041            ZipBuilder::new(),
1042            ZipBuilder::new().with_data_descriptors(),
1043            ZipBuilder::new().with_descriptor_signature(),
1044        ] {
1045            let bytes = builder
1046                .deflated("a.xml", b"<a>alpha</a>")
1047                .stored("b.bin", b"BB")
1048                .deflated("c.xml", &[b'c'; 4000])
1049                .build();
1050            let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1051            let truncated = bytes[..central + 7].to_vec();
1052            let archive = Archive::from_bytes(truncated).unwrap();
1053            assert!(archive.layout().reconstructed);
1054            assert_eq!(names(&archive), ["a.xml", "b.bin", "c.xml"]);
1055            assert_eq!(
1056                archive
1057                    .read_to_vec(archive.entry("a.xml").unwrap())
1058                    .unwrap(),
1059                b"<a>alpha</a>"
1060            );
1061            assert_eq!(
1062                archive
1063                    .read_to_vec(archive.entry("b.bin").unwrap())
1064                    .unwrap(),
1065                b"BB"
1066            );
1067            assert_eq!(
1068                archive
1069                    .read_to_vec(archive.entry("c.xml").unwrap())
1070                    .unwrap(),
1071                vec![b'c'; 4000]
1072            );
1073            assert!(Archive::crc_matches(
1074                archive.entry("c.xml").unwrap(),
1075                &[b'c'; 4000]
1076            ));
1077        }
1078    }
1079
1080    #[test]
1081    fn a_file_cut_inside_its_last_entry_still_yields_the_earlier_ones() {
1082        let bytes = ZipBuilder::new()
1083            .deflated("a.xml", b"<a/>")
1084            .deflated("b.xml", &[b'b'; 3000])
1085            .build();
1086        let second = memmem::find_iter(&bytes, &LOCAL_HEADER_SIG).nth(1).unwrap();
1087        let archive = Archive::from_bytes(bytes[..second + 40].to_vec()).unwrap();
1088        assert!(archive.layout().reconstructed);
1089        assert_eq!(
1090            archive
1091                .read_to_vec(archive.entry("a.xml").unwrap())
1092                .unwrap(),
1093            b"<a/>"
1094        );
1095        assert!(archive
1096            .read_to_vec(archive.entry("b.xml").unwrap())
1097            .is_err());
1098    }
1099
1100    #[test]
1101    fn unsupported_method_and_encryption_are_refused_per_entry() {
1102        let bytes = ZipBuilder::new()
1103            .with_method_code(12)
1104            .stored("a", b"A")
1105            .build();
1106        let archive = Archive::from_bytes(bytes).unwrap();
1107        assert!(matches!(
1108            archive.read_to_vec(archive.entry("a").unwrap()),
1109            Err(Error::Unsupported(_))
1110        ));
1111        let mut bytes = ZipBuilder::new().stored("e", b"E").build();
1112        let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1113        bytes[central + 8] |= FLAG_ENCRYPTED as u8;
1114        let archive = Archive::from_bytes(bytes).unwrap();
1115        assert!(archive.entry("e").unwrap().is_encrypted());
1116        assert!(matches!(
1117            archive.read_to_vec(archive.entry("e").unwrap()),
1118            Err(Error::Unsupported(_))
1119        ));
1120    }
1121
1122    #[test]
1123    fn a_truncated_central_directory_yields_the_readable_entries() {
1124        let bytes = ZipBuilder::new()
1125            .stored("a", b"A")
1126            .stored("b", b"B")
1127            .stored("c", b"C")
1128            .build();
1129        let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1130        let end = memmem::rfind(&bytes, &END_RECORD_SIG).unwrap();
1131        let third = memmem::find_iter(&bytes[central..end], &CENTRAL_HEADER_SIG)
1132            .nth(2)
1133            .unwrap()
1134            + central;
1135        let mut damaged = bytes[..third].to_vec();
1136        damaged.extend_from_slice(&bytes[end..]);
1137        let archive = Archive::from_bytes(damaged).unwrap();
1138        assert_eq!(names(&archive), ["a", "b"]);
1139        assert!(archive.layout().truncated_central);
1140        assert_eq!(
1141            archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
1142            b"B"
1143        );
1144    }
1145
1146    #[test]
1147    fn local_header_reports_the_recorded_fields() {
1148        let bytes = ZipBuilder::new()
1149            .with_utf8_flag()
1150            .deflated("ppt/slides/slide1.xml", b"<p:sld/>")
1151            .build();
1152        let archive = Archive::from_bytes(bytes).unwrap();
1153        let entry = archive.entry("ppt/slides/slide1.xml").unwrap();
1154        assert!(entry.names_are_utf8());
1155        let header = archive.local_header(entry).unwrap();
1156        assert_eq!(header.raw_name, b"ppt/slides/slide1.xml");
1157        assert_eq!(header.method, METHOD_DEFLATE);
1158        assert_eq!(header.crc32, entry.crc32);
1159        assert_eq!(header.data_offset, 30 + 21);
1160    }
1161
1162    #[test]
1163    fn a_file_source_reads_the_same_bytes() {
1164        let bytes = ZipBuilder::new().deflated("a", b"from a file").build();
1165        let path = std::env::temp_dir().join(format!("pptxboss-zip-{}.zip", std::process::id()));
1166        std::fs::write(&path, &bytes).unwrap();
1167        let archive = Archive::open_path(&path).unwrap();
1168        assert_eq!(
1169            archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
1170            b"from a file"
1171        );
1172        std::fs::remove_file(&path).unwrap();
1173    }
1174
1175    #[test]
1176    fn corrupt_deflate_data_is_an_inflate_error() {
1177        let mut bytes = ZipBuilder::new().deflated("a", &[b'z'; 3000]).build();
1178        let data_start = 30 + 1;
1179        for byte in &mut bytes[data_start + 2..data_start + 12] {
1180            *byte = 0xff;
1181        }
1182        let archive = Archive::from_bytes(bytes).unwrap();
1183        assert!(matches!(
1184            archive.read_to_vec(archive.entry("a").unwrap()),
1185            Err(Error::Inflate(_))
1186        ));
1187    }
1188
1189    #[test]
1190    fn missing_compressed_size_falls_back_to_the_next_header() {
1191        let mut bytes = ZipBuilder::new()
1192            .with_data_descriptors()
1193            .deflated("a", b"alpha alpha alpha")
1194            .stored("b", b"beta")
1195            .build();
1196        let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1197        for byte in &mut bytes[central + 20..central + 24] {
1198            *byte = 0;
1199        }
1200        let archive = Archive::from_bytes(bytes).unwrap();
1201        let a = archive.entry("a").unwrap();
1202        assert_eq!(a.compressed_size, 0);
1203        assert_eq!(archive.read_to_vec(a).unwrap(), b"alpha alpha alpha");
1204    }
1205}