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