zip/
spec.rs

1#![macro_use]
2
3use crate::read::magic_finder::{Backwards, Forward, MagicFinder, OptimisticMagicFinder};
4use crate::read::ArchiveOffset;
5use crate::result::{ZipError, ZipResult};
6use core::mem;
7use std::io;
8use std::io::prelude::*;
9use std::slice;
10
11/// "Magic" header values used in the zip spec to locate metadata records.
12///
13/// These values currently always take up a fixed four bytes, so we can parse and wrap them in this
14/// struct to enforce some small amount of type safety.
15#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
16#[repr(transparent)]
17pub(crate) struct Magic(u32);
18
19impl Magic {
20    pub const fn literal(x: u32) -> Self {
21        Self(x)
22    }
23
24    #[inline(always)]
25    #[allow(dead_code)]
26    pub const fn from_le_bytes(bytes: [u8; 4]) -> Self {
27        Self(u32::from_le_bytes(bytes))
28    }
29
30    #[inline(always)]
31    pub const fn to_le_bytes(self) -> [u8; 4] {
32        self.0.to_le_bytes()
33    }
34
35    #[allow(clippy::wrong_self_convention)]
36    #[inline(always)]
37    pub fn from_le(self) -> Self {
38        Self(u32::from_le(self.0))
39    }
40
41    #[allow(clippy::wrong_self_convention)]
42    #[inline(always)]
43    pub fn to_le(self) -> Self {
44        Self(u32::to_le(self.0))
45    }
46
47    pub const LOCAL_FILE_HEADER_SIGNATURE: Self = Self::literal(0x04034b50);
48    pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: Self = Self::literal(0x02014b50);
49    pub const CENTRAL_DIRECTORY_END_SIGNATURE: Self = Self::literal(0x06054b50);
50    pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: Self = Self::literal(0x06064b50);
51    pub const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: Self = Self::literal(0x07064b50);
52}
53
54/// Similar to [`Magic`], but used for extra field tags as per section 4.5.3 of APPNOTE.TXT.
55#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
56#[repr(transparent)]
57pub(crate) struct ExtraFieldMagic(u16);
58
59/* TODO: maybe try to use this for parsing extra fields as well as writing them? */
60#[allow(dead_code)]
61impl ExtraFieldMagic {
62    pub const fn literal(x: u16) -> Self {
63        Self(x)
64    }
65
66    #[inline(always)]
67    pub const fn from_le_bytes(bytes: [u8; 2]) -> Self {
68        Self(u16::from_le_bytes(bytes))
69    }
70
71    #[inline(always)]
72    pub const fn to_le_bytes(self) -> [u8; 2] {
73        self.0.to_le_bytes()
74    }
75
76    #[allow(clippy::wrong_self_convention)]
77    #[inline(always)]
78    pub fn from_le(self) -> Self {
79        Self(u16::from_le(self.0))
80    }
81
82    #[allow(clippy::wrong_self_convention)]
83    #[inline(always)]
84    pub fn to_le(self) -> Self {
85        Self(u16::to_le(self.0))
86    }
87
88    pub const ZIP64_EXTRA_FIELD_TAG: Self = Self::literal(0x0001);
89}
90
91/// The file size at which a ZIP64 record becomes necessary.
92///
93/// If a file larger than this threshold attempts to be written, compressed or uncompressed, and
94/// [`FileOptions::large_file()`](crate::write::FileOptions) was not true, then [`ZipWriter`] will
95/// raise an [`io::Error`] with [`io::ErrorKind::Other`].
96///
97/// If the zip file itself is larger than this value, then a zip64 central directory record will be
98/// written to the end of the file.
99///
100///```
101/// # fn main() -> Result<(), zip::result::ZipError> {
102/// # #[cfg(target_pointer_width = "64")]
103/// # {
104/// use std::io::{self, Cursor, prelude::*};
105/// use std::error::Error;
106/// use zip::{ZipWriter, write::SimpleFileOptions};
107///
108/// let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
109/// // Writing an extremely large file for this test is faster without compression.
110/// let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
111///
112/// let big_len: usize = (zip::ZIP64_BYTES_THR as usize) + 1;
113/// let big_buf = vec![0u8; big_len];
114/// zip.start_file("zero.dat", options)?;
115/// // This is too big!
116/// let res = zip.write_all(&big_buf[..]).err().unwrap();
117/// assert_eq!(res.kind(), io::ErrorKind::Other);
118/// let description = format!("{}", &res);
119/// assert_eq!(description, "Large file option has not been set");
120/// // Attempting to write anything further to the same zip will still succeed, but the previous
121/// // failing entry has been removed.
122/// zip.start_file("one.dat", options)?;
123/// let zip = zip.finish_into_readable()?;
124/// let names: Vec<_> = zip.file_names().collect();
125/// assert_eq!(&names, &["one.dat"]);
126///
127/// // Create a new zip output.
128/// let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
129/// // This time, create a zip64 record for the file.
130/// let options = options.large_file(true);
131/// zip.start_file("zero.dat", options)?;
132/// // This succeeds because we specified that it could be a large file.
133/// assert!(zip.write_all(&big_buf[..]).is_ok());
134/// # }
135/// # Ok(())
136/// # }
137///```
138pub const ZIP64_BYTES_THR: u64 = u32::MAX as u64;
139/// The number of entries within a single zip necessary to allocate a zip64 central
140/// directory record.
141///
142/// If more than this number of entries is written to a [`ZipWriter`], then [`ZipWriter::finish()`]
143/// will write out extra zip64 data to the end of the zip file.
144pub const ZIP64_ENTRY_THR: usize = u16::MAX as usize;
145
146/// # Safety
147///
148/// - No padding/uninit bytes
149/// - All bytes patterns must be valid
150/// - No cell, pointers
151///
152/// See `bytemuck::Pod` for more details.
153pub(crate) unsafe trait Pod: Copy + 'static {
154    #[inline]
155    fn zeroed() -> Self {
156        unsafe { mem::zeroed() }
157    }
158
159    #[inline]
160    fn as_bytes(&self) -> &[u8] {
161        unsafe { slice::from_raw_parts(self as *const Self as *const u8, mem::size_of::<Self>()) }
162    }
163
164    #[inline]
165    fn as_bytes_mut(&mut self) -> &mut [u8] {
166        unsafe { slice::from_raw_parts_mut(self as *mut Self as *mut u8, mem::size_of::<Self>()) }
167    }
168}
169
170pub(crate) trait FixedSizeBlock: Pod {
171    const MAGIC: Magic;
172
173    fn magic(self) -> Magic;
174
175    const WRONG_MAGIC_ERROR: ZipError;
176
177    #[allow(clippy::wrong_self_convention)]
178    fn from_le(self) -> Self;
179
180    fn parse<R: Read>(reader: &mut R) -> ZipResult<Self> {
181        let mut block = Self::zeroed();
182        reader.read_exact(block.as_bytes_mut())?;
183        let block = Self::from_le(block);
184
185        if block.magic() != Self::MAGIC {
186            return Err(Self::WRONG_MAGIC_ERROR);
187        }
188        Ok(block)
189    }
190
191    fn to_le(self) -> Self;
192
193    fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
194        let block = self.to_le();
195        writer.write_all(block.as_bytes())?;
196        Ok(())
197    }
198}
199
200/// Convert all the fields of a struct *from* little-endian representations.
201macro_rules! from_le {
202    ($obj:ident, $field:ident, $type:ty) => {
203        $obj.$field = <$type>::from_le($obj.$field);
204    };
205    ($obj:ident, [($field:ident, $type:ty) $(,)?]) => {
206        from_le![$obj, $field, $type];
207    };
208    ($obj:ident, [($field:ident, $type:ty), $($rest:tt),+ $(,)?]) => {
209        from_le![$obj, $field, $type];
210        from_le!($obj, [$($rest),+]);
211    };
212}
213
214/// Convert all the fields of a struct *into* little-endian representations.
215macro_rules! to_le {
216    ($obj:ident, $field:ident, $type:ty) => {
217        $obj.$field = <$type>::to_le($obj.$field);
218    };
219    ($obj:ident, [($field:ident, $type:ty) $(,)?]) => {
220        to_le![$obj, $field, $type];
221    };
222    ($obj:ident, [($field:ident, $type:ty), $($rest:tt),+ $(,)?]) => {
223        to_le![$obj, $field, $type];
224        to_le!($obj, [$($rest),+]);
225    };
226}
227
228/* TODO: derive macro to generate these fields? */
229/// Implement `from_le()` and `to_le()`, providing the field specification to both macros
230/// and methods.
231macro_rules! to_and_from_le {
232    ($($args:tt),+ $(,)?) => {
233        #[inline(always)]
234        fn from_le(mut self) -> Self {
235            from_le![self, [$($args),+]];
236            self
237        }
238        #[inline(always)]
239        fn to_le(mut self) -> Self {
240            to_le![self, [$($args),+]];
241            self
242        }
243    };
244}
245
246#[derive(Copy, Clone, Debug)]
247#[repr(packed, C)]
248pub(crate) struct Zip32CDEBlock {
249    magic: Magic,
250    pub disk_number: u16,
251    pub disk_with_central_directory: u16,
252    pub number_of_files_on_this_disk: u16,
253    pub number_of_files: u16,
254    pub central_directory_size: u32,
255    pub central_directory_offset: u32,
256    pub zip_file_comment_length: u16,
257}
258
259unsafe impl Pod for Zip32CDEBlock {}
260
261impl FixedSizeBlock for Zip32CDEBlock {
262    const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_END_SIGNATURE;
263
264    #[inline(always)]
265    fn magic(self) -> Magic {
266        self.magic
267    }
268
269    const WRONG_MAGIC_ERROR: ZipError =
270        ZipError::InvalidArchive("Invalid digital signature header");
271
272    to_and_from_le![
273        (magic, Magic),
274        (disk_number, u16),
275        (disk_with_central_directory, u16),
276        (number_of_files_on_this_disk, u16),
277        (number_of_files, u16),
278        (central_directory_size, u32),
279        (central_directory_offset, u32),
280        (zip_file_comment_length, u16)
281    ];
282}
283
284#[derive(Debug)]
285pub(crate) struct Zip32CentralDirectoryEnd {
286    pub disk_number: u16,
287    pub disk_with_central_directory: u16,
288    pub number_of_files_on_this_disk: u16,
289    pub number_of_files: u16,
290    pub central_directory_size: u32,
291    pub central_directory_offset: u32,
292    pub zip_file_comment: Box<[u8]>,
293}
294
295impl Zip32CentralDirectoryEnd {
296    fn into_block_and_comment(self) -> (Zip32CDEBlock, Box<[u8]>) {
297        let Self {
298            disk_number,
299            disk_with_central_directory,
300            number_of_files_on_this_disk,
301            number_of_files,
302            central_directory_size,
303            central_directory_offset,
304            zip_file_comment,
305        } = self;
306        let block = Zip32CDEBlock {
307            magic: Zip32CDEBlock::MAGIC,
308            disk_number,
309            disk_with_central_directory,
310            number_of_files_on_this_disk,
311            number_of_files,
312            central_directory_size,
313            central_directory_offset,
314            zip_file_comment_length: zip_file_comment.len() as u16,
315        };
316
317        (block, zip_file_comment)
318    }
319
320    pub fn parse<T: Read>(reader: &mut T) -> ZipResult<Zip32CentralDirectoryEnd> {
321        let Zip32CDEBlock {
322            // magic,
323            disk_number,
324            disk_with_central_directory,
325            number_of_files_on_this_disk,
326            number_of_files,
327            central_directory_size,
328            central_directory_offset,
329            zip_file_comment_length,
330            ..
331        } = Zip32CDEBlock::parse(reader)?;
332
333        let mut zip_file_comment = vec![0u8; zip_file_comment_length as usize].into_boxed_slice();
334        if let Err(e) = reader.read_exact(&mut zip_file_comment) {
335            if e.kind() == io::ErrorKind::UnexpectedEof {
336                return Err(ZipError::InvalidArchive(
337                    "EOCD comment exceeds file boundary",
338                ));
339            }
340
341            return Err(e.into());
342        }
343
344        Ok(Zip32CentralDirectoryEnd {
345            disk_number,
346            disk_with_central_directory,
347            number_of_files_on_this_disk,
348            number_of_files,
349            central_directory_size,
350            central_directory_offset,
351            zip_file_comment,
352        })
353    }
354
355    pub fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
356        let (block, comment) = self.into_block_and_comment();
357
358        if comment.len() > u16::MAX as usize {
359            return Err(ZipError::InvalidArchive(
360                "EOCD comment length exceeds u16::MAX",
361            ));
362        }
363
364        block.write(writer)?;
365        writer.write_all(&comment)?;
366        Ok(())
367    }
368
369    pub fn may_be_zip64(&self) -> bool {
370        self.number_of_files == u16::MAX || self.central_directory_offset == u32::MAX
371    }
372}
373
374#[derive(Copy, Clone)]
375#[repr(packed, C)]
376pub(crate) struct Zip64CDELocatorBlock {
377    magic: Magic,
378    pub disk_with_central_directory: u32,
379    pub end_of_central_directory_offset: u64,
380    pub number_of_disks: u32,
381}
382
383unsafe impl Pod for Zip64CDELocatorBlock {}
384
385impl FixedSizeBlock for Zip64CDELocatorBlock {
386    const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE;
387
388    #[inline(always)]
389    fn magic(self) -> Magic {
390        self.magic
391    }
392
393    const WRONG_MAGIC_ERROR: ZipError =
394        ZipError::InvalidArchive("Invalid zip64 locator digital signature header");
395
396    to_and_from_le![
397        (magic, Magic),
398        (disk_with_central_directory, u32),
399        (end_of_central_directory_offset, u64),
400        (number_of_disks, u32),
401    ];
402}
403
404pub(crate) struct Zip64CentralDirectoryEndLocator {
405    pub disk_with_central_directory: u32,
406    pub end_of_central_directory_offset: u64,
407    pub number_of_disks: u32,
408}
409
410impl Zip64CentralDirectoryEndLocator {
411    pub fn parse<T: Read>(reader: &mut T) -> ZipResult<Zip64CentralDirectoryEndLocator> {
412        let Zip64CDELocatorBlock {
413            // magic,
414            disk_with_central_directory,
415            end_of_central_directory_offset,
416            number_of_disks,
417            ..
418        } = Zip64CDELocatorBlock::parse(reader)?;
419
420        Ok(Zip64CentralDirectoryEndLocator {
421            disk_with_central_directory,
422            end_of_central_directory_offset,
423            number_of_disks,
424        })
425    }
426
427    pub fn block(self) -> Zip64CDELocatorBlock {
428        let Self {
429            disk_with_central_directory,
430            end_of_central_directory_offset,
431            number_of_disks,
432        } = self;
433        Zip64CDELocatorBlock {
434            magic: Zip64CDELocatorBlock::MAGIC,
435            disk_with_central_directory,
436            end_of_central_directory_offset,
437            number_of_disks,
438        }
439    }
440
441    pub fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
442        self.block().write(writer)
443    }
444}
445
446#[derive(Copy, Clone)]
447#[repr(packed, C)]
448pub(crate) struct Zip64CDEBlock {
449    magic: Magic,
450    pub record_size: u64,
451    pub version_made_by: u16,
452    pub version_needed_to_extract: u16,
453    pub disk_number: u32,
454    pub disk_with_central_directory: u32,
455    pub number_of_files_on_this_disk: u64,
456    pub number_of_files: u64,
457    pub central_directory_size: u64,
458    pub central_directory_offset: u64,
459}
460
461unsafe impl Pod for Zip64CDEBlock {}
462
463impl FixedSizeBlock for Zip64CDEBlock {
464    const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE;
465
466    fn magic(self) -> Magic {
467        self.magic
468    }
469
470    const WRONG_MAGIC_ERROR: ZipError =
471        ZipError::InvalidArchive("Invalid digital signature header");
472
473    to_and_from_le![
474        (magic, Magic),
475        (record_size, u64),
476        (version_made_by, u16),
477        (version_needed_to_extract, u16),
478        (disk_number, u32),
479        (disk_with_central_directory, u32),
480        (number_of_files_on_this_disk, u64),
481        (number_of_files, u64),
482        (central_directory_size, u64),
483        (central_directory_offset, u64),
484    ];
485}
486
487pub(crate) struct Zip64CentralDirectoryEnd {
488    pub record_size: u64,
489    pub version_made_by: u16,
490    pub version_needed_to_extract: u16,
491    pub disk_number: u32,
492    pub disk_with_central_directory: u32,
493    pub number_of_files_on_this_disk: u64,
494    pub number_of_files: u64,
495    pub central_directory_size: u64,
496    pub central_directory_offset: u64,
497    pub extensible_data_sector: Box<[u8]>,
498}
499
500impl Zip64CentralDirectoryEnd {
501    pub fn parse<T: Read>(reader: &mut T, max_size: u64) -> ZipResult<Zip64CentralDirectoryEnd> {
502        let Zip64CDEBlock {
503            record_size,
504            version_made_by,
505            version_needed_to_extract,
506            disk_number,
507            disk_with_central_directory,
508            number_of_files_on_this_disk,
509            number_of_files,
510            central_directory_size,
511            central_directory_offset,
512            ..
513        } = Zip64CDEBlock::parse(reader)?;
514
515        if record_size < 44 {
516            return Err(ZipError::InvalidArchive("Low EOCD64 record size"));
517        } else if record_size.saturating_add(12) > max_size {
518            return Err(ZipError::InvalidArchive(
519                "EOCD64 extends beyond EOCD64 locator",
520            ));
521        }
522
523        let mut zip_file_comment = vec![0u8; record_size as usize - 44].into_boxed_slice();
524        reader.read_exact(&mut zip_file_comment)?;
525
526        Ok(Self {
527            record_size,
528            version_made_by,
529            version_needed_to_extract,
530            disk_number,
531            disk_with_central_directory,
532            number_of_files_on_this_disk,
533            number_of_files,
534            central_directory_size,
535            central_directory_offset,
536            extensible_data_sector: zip_file_comment,
537        })
538    }
539
540    pub fn into_block_and_comment(self) -> (Zip64CDEBlock, Box<[u8]>) {
541        let Self {
542            record_size,
543            version_made_by,
544            version_needed_to_extract,
545            disk_number,
546            disk_with_central_directory,
547            number_of_files_on_this_disk,
548            number_of_files,
549            central_directory_size,
550            central_directory_offset,
551            extensible_data_sector,
552        } = self;
553
554        (
555            Zip64CDEBlock {
556                magic: Zip64CDEBlock::MAGIC,
557                record_size,
558                version_made_by,
559                version_needed_to_extract,
560                disk_number,
561                disk_with_central_directory,
562                number_of_files_on_this_disk,
563                number_of_files,
564                central_directory_size,
565                central_directory_offset,
566            },
567            extensible_data_sector,
568        )
569    }
570
571    pub fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
572        let (block, comment) = self.into_block_and_comment();
573        block.write(writer)?;
574        writer.write_all(&comment)?;
575        Ok(())
576    }
577}
578
579pub(crate) struct DataAndPosition<T> {
580    pub data: T,
581    #[allow(dead_code)]
582    pub position: u64,
583}
584
585impl<T> From<(T, u64)> for DataAndPosition<T> {
586    fn from(value: (T, u64)) -> Self {
587        Self {
588            data: value.0,
589            position: value.1,
590        }
591    }
592}
593
594pub(crate) struct CentralDirectoryEndInfo {
595    pub eocd: DataAndPosition<Zip32CentralDirectoryEnd>,
596    pub eocd64: Option<DataAndPosition<Zip64CentralDirectoryEnd>>,
597
598    pub archive_offset: u64,
599}
600
601/// Finds the EOCD and possibly the EOCD64 block and determines the archive offset.
602///
603/// In the best case scenario (no prepended junk), this function will not backtrack
604/// in the reader.
605pub(crate) fn find_central_directory<R: Read + Seek>(
606    reader: &mut R,
607    archive_offset: ArchiveOffset,
608    end_exclusive: u64,
609    file_len: u64,
610) -> ZipResult<CentralDirectoryEndInfo> {
611    const EOCD_SIG_BYTES: [u8; mem::size_of::<Magic>()] =
612        Magic::CENTRAL_DIRECTORY_END_SIGNATURE.to_le_bytes();
613
614    const EOCD64_SIG_BYTES: [u8; mem::size_of::<Magic>()] =
615        Magic::ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE.to_le_bytes();
616
617    const CDFH_SIG_BYTES: [u8; mem::size_of::<Magic>()] =
618        Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE.to_le_bytes();
619
620    // Instantiate the mandatory finder
621    let mut eocd_finder = MagicFinder::<Backwards<'static>>::new(&EOCD_SIG_BYTES, 0, end_exclusive);
622    let mut subfinder: Option<OptimisticMagicFinder<Forward<'static>>> = None;
623
624    // Keep the last errors for cases of improper EOCD instances.
625    let mut parsing_error = None;
626
627    while let Some(eocd_offset) = eocd_finder.next(reader)? {
628        // Attempt to parse the EOCD block
629        let eocd = match Zip32CentralDirectoryEnd::parse(reader) {
630            Ok(eocd) => eocd,
631            Err(e) => {
632                if parsing_error.is_none() {
633                    parsing_error = Some(e);
634                }
635                continue;
636            }
637        };
638
639        // ! Relaxed (inequality) due to garbage-after-comment Python files
640        // Consistency check: the EOCD comment must terminate before the end of file
641        if eocd.zip_file_comment.len() as u64 + eocd_offset + 22 > file_len {
642            parsing_error = Some(ZipError::InvalidArchive("Invalid EOCD comment length"));
643            continue;
644        }
645
646        let zip64_metadata = if eocd.may_be_zip64() {
647            fn try_read_eocd64_locator(
648                reader: &mut (impl Read + Seek),
649                eocd_offset: u64,
650            ) -> ZipResult<(u64, Zip64CentralDirectoryEndLocator)> {
651                if eocd_offset < mem::size_of::<Zip64CDELocatorBlock>() as u64 {
652                    return Err(ZipError::InvalidArchive(
653                        "EOCD64 Locator does not fit in file",
654                    ));
655                }
656
657                let locator64_offset = eocd_offset - mem::size_of::<Zip64CDELocatorBlock>() as u64;
658
659                reader.seek(io::SeekFrom::Start(locator64_offset))?;
660                Ok((
661                    locator64_offset,
662                    Zip64CentralDirectoryEndLocator::parse(reader)?,
663                ))
664            }
665
666            try_read_eocd64_locator(reader, eocd_offset).ok()
667        } else {
668            None
669        };
670
671        let Some((locator64_offset, locator64)) = zip64_metadata else {
672            // Branch out for zip32
673            let relative_cd_offset = eocd.central_directory_offset as u64;
674
675            // If the archive is empty, there is nothing more to be checked, the archive is correct.
676            if eocd.number_of_files == 0 {
677                return Ok(CentralDirectoryEndInfo {
678                    eocd: (eocd, eocd_offset).into(),
679                    eocd64: None,
680                    archive_offset: eocd_offset.saturating_sub(relative_cd_offset),
681                });
682            }
683
684            // Consistency check: the CD relative offset cannot be after the EOCD
685            if relative_cd_offset >= eocd_offset {
686                parsing_error = Some(ZipError::InvalidArchive("Invalid CDFH offset in EOCD"));
687                continue;
688            }
689
690            // Attempt to find the first CDFH
691            let subfinder = subfinder
692                .get_or_insert_with(OptimisticMagicFinder::new_empty)
693                .repurpose(
694                    &CDFH_SIG_BYTES,
695                    // The CDFH must be before the EOCD and after the relative offset,
696                    // because prepended junk can only move it forward.
697                    (relative_cd_offset, eocd_offset),
698                    match archive_offset {
699                        ArchiveOffset::Known(n) => {
700                            Some((relative_cd_offset.saturating_add(n).min(eocd_offset), true))
701                        }
702                        _ => Some((relative_cd_offset, false)),
703                    },
704                );
705
706            // Consistency check: find the first CDFH
707            if let Some(cd_offset) = subfinder.next(reader)? {
708                // The first CDFH will define the archive offset
709                let archive_offset = cd_offset - relative_cd_offset;
710
711                return Ok(CentralDirectoryEndInfo {
712                    eocd: (eocd, eocd_offset).into(),
713                    eocd64: None,
714                    archive_offset,
715                });
716            }
717
718            parsing_error = Some(ZipError::InvalidArchive("No CDFH found"));
719            continue;
720        };
721
722        // Consistency check: the EOCD64 offset must be before EOCD64 Locator offset */
723        if locator64.end_of_central_directory_offset >= locator64_offset {
724            parsing_error = Some(ZipError::InvalidArchive("Invalid EOCD64 Locator CD offset"));
725            continue;
726        }
727
728        if locator64.number_of_disks > 1 {
729            parsing_error = Some(ZipError::InvalidArchive(
730                "Multi-disk ZIP files are not supported",
731            ));
732            continue;
733        }
734
735        // This was hidden inside a function to collect errors in a single place.
736        // Once try blocks are stabilized, this can go away.
737        fn try_read_eocd64<R: Read + Seek>(
738            reader: &mut R,
739            locator64: &Zip64CentralDirectoryEndLocator,
740            expected_length: u64,
741        ) -> ZipResult<Zip64CentralDirectoryEnd> {
742            let z64 = Zip64CentralDirectoryEnd::parse(reader, expected_length)?;
743
744            // Consistency check: EOCD64 locator should agree with the EOCD64
745            if z64.disk_with_central_directory != locator64.disk_with_central_directory {
746                return Err(ZipError::InvalidArchive(
747                    "Invalid EOCD64: inconsistency with Locator data",
748                ));
749            }
750
751            // Consistency check: the EOCD64 must have the expected length
752            if z64.record_size + 12 != expected_length {
753                return Err(ZipError::InvalidArchive(
754                    "Invalid EOCD64: inconsistent length",
755                ));
756            }
757
758            Ok(z64)
759        }
760
761        // Attempt to find the EOCD64 with an initial guess
762        let subfinder = subfinder
763            .get_or_insert_with(OptimisticMagicFinder::new_empty)
764            .repurpose(
765                &EOCD64_SIG_BYTES,
766                (locator64.end_of_central_directory_offset, locator64_offset),
767                match archive_offset {
768                    ArchiveOffset::Known(n) => Some((
769                        locator64
770                            .end_of_central_directory_offset
771                            .saturating_add(n)
772                            .min(locator64_offset),
773                        true,
774                    )),
775                    _ => Some((locator64.end_of_central_directory_offset, false)),
776                },
777            );
778
779        // Consistency check: Find the EOCD64
780        let mut local_error = None;
781        while let Some(eocd64_offset) = subfinder.next(reader)? {
782            let archive_offset = eocd64_offset - locator64.end_of_central_directory_offset;
783
784            match try_read_eocd64(
785                reader,
786                &locator64,
787                locator64_offset.saturating_sub(eocd64_offset),
788            ) {
789                Ok(eocd64) => {
790                    if eocd64_offset
791                        < eocd64
792                            .number_of_files
793                            .saturating_mul(
794                                mem::size_of::<crate::types::ZipCentralEntryBlock>() as u64
795                            )
796                            .saturating_add(eocd64.central_directory_offset)
797                    {
798                        local_error = Some(ZipError::InvalidArchive(
799                            "Invalid EOCD64: inconsistent number of files",
800                        ));
801                        continue;
802                    }
803
804                    return Ok(CentralDirectoryEndInfo {
805                        eocd: (eocd, eocd_offset).into(),
806                        eocd64: Some((eocd64, eocd64_offset).into()),
807                        archive_offset,
808                    });
809                }
810                Err(e) => {
811                    local_error = Some(e);
812                }
813            }
814        }
815
816        parsing_error = local_error.or(Some(ZipError::InvalidArchive("Could not find EOCD64")));
817    }
818
819    Err(parsing_error.unwrap_or(ZipError::InvalidArchive("Could not find EOCD")))
820}
821
822pub(crate) fn is_dir(filename: &str) -> bool {
823    filename
824        .chars()
825        .next_back()
826        .is_some_and(|c| c == '/' || c == '\\')
827}
828
829#[cfg(test)]
830mod test {
831    use super::*;
832    use std::io::Cursor;
833
834    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
835    #[repr(packed, C)]
836    pub struct TestBlock {
837        magic: Magic,
838        pub file_name_length: u16,
839    }
840
841    unsafe impl Pod for TestBlock {}
842
843    impl FixedSizeBlock for TestBlock {
844        const MAGIC: Magic = Magic::literal(0x01111);
845
846        fn magic(self) -> Magic {
847            self.magic
848        }
849
850        const WRONG_MAGIC_ERROR: ZipError = ZipError::InvalidArchive("unreachable");
851
852        to_and_from_le![(magic, Magic), (file_name_length, u16)];
853    }
854
855    /// Demonstrate that a block object can be safely written to memory and deserialized back out.
856    #[test]
857    fn block_serde() {
858        let block = TestBlock {
859            magic: TestBlock::MAGIC,
860            file_name_length: 3,
861        };
862        let mut c = Cursor::new(Vec::new());
863        block.write(&mut c).unwrap();
864        c.set_position(0);
865        let block2 = TestBlock::parse(&mut c).unwrap();
866        assert_eq!(block, block2);
867    }
868}