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