zip/
spec.rs

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