Skip to main content

zip/format/
blocks.rs

1//! Blocks used in zip format
2
3use crate::format::magic::Magic;
4use crate::result::{ZipError, ZipResult, invalid};
5
6use core::any::type_name;
7use core::mem;
8use core::slice;
9use std::io::{self, Read, Write};
10
11/// # Safety
12///
13/// - No padding/uninit bytes
14/// - All bytes patterns must be valid
15/// - No cell, pointers
16///
17/// See `bytemuck::Pod` for more details.
18pub(crate) unsafe trait Pod: Copy + 'static {
19    #[inline]
20    fn zeroed() -> Self {
21        unsafe { mem::zeroed() }
22    }
23
24    #[inline]
25    fn as_bytes(&self) -> &[u8] {
26        unsafe {
27            slice::from_raw_parts(
28                std::ptr::from_ref::<Self>(self).cast::<u8>(),
29                mem::size_of::<Self>(),
30            )
31        }
32    }
33
34    #[inline]
35    fn as_bytes_mut(&mut self) -> &mut [u8] {
36        unsafe {
37            slice::from_raw_parts_mut(
38                std::ptr::from_mut::<Self>(self).cast::<u8>(),
39                mem::size_of::<Self>(),
40            )
41        }
42    }
43}
44
45#[derive(Copy, Clone)]
46#[repr(C, packed)]
47struct BlockWithMagic<T: FixedSizeBlock> {
48    magic: Magic,
49    inner: T,
50}
51unsafe impl<T: FixedSizeBlock> Pod for BlockWithMagic<T> {}
52
53impl<T: FixedSizeBlock> BlockWithMagic<T> {
54    fn to_le(mut self) -> Self {
55        self.magic = self.magic.to_le();
56        self.inner = self.inner.to_le();
57        self
58    }
59}
60
61pub(crate) trait FixedSizeBlock: Pod {
62    const MAGIC: Magic;
63
64    const WRONG_MAGIC_ERROR: ZipError;
65
66    #[allow(clippy::wrong_self_convention)]
67    fn from_le(self) -> Self;
68
69    fn parse<R: Read + ?Sized>(reader: &mut R) -> ZipResult<Self> {
70        let mut block_with_magic = BlockWithMagic::zeroed();
71        if let Err(e) = reader.read_exact(block_with_magic.as_bytes_mut()) {
72            if e.kind() == io::ErrorKind::UnexpectedEof {
73                return Err(invalid!("Unexpected end of {}", type_name::<Self>()));
74            }
75            return Err(e.into());
76        }
77        let BlockWithMagic {
78            magic,
79            inner: block,
80        } = block_with_magic;
81        let magic = Magic::from_le(magic);
82        if magic != Self::MAGIC {
83            return Err(Self::WRONG_MAGIC_ERROR);
84        }
85        let block = Self::from_le(block);
86        Ok(block)
87    }
88
89    fn to_le(self) -> Self;
90
91    fn write<T: Write + ?Sized>(self, writer: &mut T) -> ZipResult<()> {
92        let block = BlockWithMagic {
93            magic: Self::MAGIC,
94            inner: self,
95        };
96        let block = block.to_le();
97        writer.write_all(block.as_bytes())?;
98        Ok(())
99    }
100}
101
102/// Convert all the fields of a struct *from* little-endian representations.
103macro_rules! from_le {
104    ($obj:ident, $field:ident, $type:ty) => {
105        $obj.$field = <$type>::from_le($obj.$field);
106    };
107    ($obj:ident, [($field:ident, $type:ty) $(,)?]) => {
108        from_le![$obj, $field, $type];
109    };
110    ($obj:ident, [($field:ident, $type:ty), $($rest:tt),+ $(,)?]) => {
111        from_le![$obj, $field, $type];
112        from_le!($obj, [$($rest),+]);
113    };
114}
115
116/// Convert all the fields of a struct *into* little-endian representations.
117macro_rules! to_le {
118    ($obj:ident, $field:ident, $type:ty) => {
119        $obj.$field = <$type>::to_le($obj.$field);
120    };
121    ($obj:ident, [($field:ident, $type:ty) $(,)?]) => {
122        to_le![$obj, $field, $type];
123    };
124    ($obj:ident, [($field:ident, $type:ty), $($rest:tt),+ $(,)?]) => {
125        to_le![$obj, $field, $type];
126        to_le!($obj, [$($rest),+]);
127    };
128}
129
130/* TODO: derive macro to generate these fields? */
131/// Implement `from_le()` and `to_le()`, providing the field specification to both macros
132/// and methods.
133macro_rules! to_and_from_le {
134    ($($args:tt),+ $(,)?) => {
135        #[inline(always)]
136        fn from_le(mut self) -> Self {
137            from_le![self, [$($args),+]];
138            self
139        }
140        #[inline(always)]
141        fn to_le(mut self) -> Self {
142            to_le![self, [$($args),+]];
143            self
144        }
145    };
146}
147
148#[derive(Copy, Clone, Debug)]
149#[repr(packed, C)]
150#[allow(missing_docs)]
151pub struct ZipCentralEntryBlock {
152    pub version_made_by: u16,
153    pub version_to_extract: u16,
154    pub flags: u16,
155    pub compression_method: u16,
156    pub last_mod_time: u16,
157    pub last_mod_date: u16,
158    pub crc32: u32,
159    pub compressed_size: u32,
160    pub uncompressed_size: u32,
161    pub file_name_length: u16,
162    pub extra_field_length: u16,
163    pub file_comment_length: u16,
164    pub disk_number: u16,
165    pub internal_file_attributes: u16,
166    pub external_file_attributes: u32,
167    pub offset: u32,
168}
169
170unsafe impl Pod for ZipCentralEntryBlock {}
171
172impl ZipEntryBlock for ZipCentralEntryBlock {
173    fn get_uncompressed_size(&self) -> u32 {
174        self.uncompressed_size
175    }
176    fn get_compressed_size(&self) -> u32 {
177        self.compressed_size
178    }
179    fn get_header_start(&self) -> Option<u32> {
180        Some(self.offset)
181    }
182}
183
184impl FixedSizeBlock for ZipCentralEntryBlock {
185    const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;
186
187    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");
188
189    to_and_from_le![
190        (version_made_by, u16),
191        (version_to_extract, u16),
192        (flags, u16),
193        (compression_method, u16),
194        (last_mod_time, u16),
195        (last_mod_date, u16),
196        (crc32, u32),
197        (compressed_size, u32),
198        (uncompressed_size, u32),
199        (file_name_length, u16),
200        (extra_field_length, u16),
201        (file_comment_length, u16),
202        (disk_number, u16),
203        (internal_file_attributes, u16),
204        (external_file_attributes, u32),
205        (offset, u32),
206    ];
207}
208
209pub(crate) trait ZipEntryBlock {
210    fn get_uncompressed_size(&self) -> u32;
211    fn get_compressed_size(&self) -> u32;
212    fn get_header_start(&self) -> Option<u32>;
213}
214
215#[derive(Copy, Clone, Debug)]
216#[repr(packed, C)]
217#[allow(missing_docs)]
218pub struct ZipLocalEntryBlock {
219    pub version_made_by: u16,
220    pub flags: u16,
221    pub compression_method: u16,
222    pub last_mod_time: u16,
223    pub last_mod_date: u16,
224    pub crc32: u32,
225    pub compressed_size: u32,
226    pub uncompressed_size: u32,
227    pub file_name_length: u16,
228    pub extra_field_length: u16,
229}
230
231unsafe impl Pod for ZipLocalEntryBlock {}
232
233impl ZipEntryBlock for ZipLocalEntryBlock {
234    fn get_uncompressed_size(&self) -> u32 {
235        self.uncompressed_size
236    }
237    fn get_compressed_size(&self) -> u32 {
238        self.compressed_size
239    }
240    fn get_header_start(&self) -> Option<u32> {
241        None
242    }
243}
244
245impl FixedSizeBlock for ZipLocalEntryBlock {
246    const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE;
247
248    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");
249
250    to_and_from_le![
251        (version_made_by, u16),
252        (flags, u16),
253        (compression_method, u16),
254        (last_mod_time, u16),
255        (last_mod_date, u16),
256        (crc32, u32),
257        (compressed_size, u32),
258        (uncompressed_size, u32),
259        (file_name_length, u16),
260        (extra_field_length, u16),
261    ];
262}
263
264#[derive(Copy, Clone, Debug)]
265#[repr(packed, C)]
266#[allow(missing_docs)]
267pub struct Zip32CDEBlock {
268    pub disk_number: u16,
269    pub disk_with_central_directory: u16,
270    pub number_of_files_on_this_disk: u16,
271    pub number_of_files: u16,
272    pub central_directory_size: u32,
273    pub central_directory_offset: u32,
274    pub zip_file_comment_length: u16,
275}
276
277unsafe impl Pod for Zip32CDEBlock {}
278
279impl FixedSizeBlock for Zip32CDEBlock {
280    const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_END_SIGNATURE;
281
282    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory End header");
283
284    to_and_from_le![
285        (disk_number, u16),
286        (disk_with_central_directory, u16),
287        (number_of_files_on_this_disk, u16),
288        (number_of_files, u16),
289        (central_directory_size, u32),
290        (central_directory_offset, u32),
291        (zip_file_comment_length, u16)
292    ];
293}
294
295#[derive(Debug)]
296#[allow(missing_docs)]
297pub struct Zip32CentralDirectoryEnd {
298    pub disk_number: u16,
299    pub disk_with_central_directory: u16,
300    pub number_of_files_on_this_disk: u16,
301    pub number_of_files: u16,
302    pub central_directory_size: u32,
303    pub central_directory_offset: u32,
304    pub zip_file_comment: Box<[u8]>,
305}
306
307impl Zip32CentralDirectoryEnd {
308    fn into_block_and_comment(self) -> (Zip32CDEBlock, Box<[u8]>) {
309        let Self {
310            disk_number,
311            disk_with_central_directory,
312            number_of_files_on_this_disk,
313            number_of_files,
314            central_directory_size,
315            central_directory_offset,
316            zip_file_comment,
317        } = self;
318        let block = Zip32CDEBlock {
319            disk_number,
320            disk_with_central_directory,
321            number_of_files_on_this_disk,
322            number_of_files,
323            central_directory_size,
324            central_directory_offset,
325            zip_file_comment_length: zip_file_comment.len() as u16,
326        };
327
328        (block, zip_file_comment)
329    }
330
331    /// Parse the block
332    pub fn parse<T: Read + ?Sized>(reader: &mut T) -> ZipResult<Zip32CentralDirectoryEnd> {
333        let Zip32CDEBlock {
334            disk_number,
335            disk_with_central_directory,
336            number_of_files_on_this_disk,
337            number_of_files,
338            central_directory_size,
339            central_directory_offset,
340            zip_file_comment_length,
341            ..
342        } = Zip32CDEBlock::parse(reader)?;
343
344        let mut zip_file_comment = vec![0u8; zip_file_comment_length as usize].into_boxed_slice();
345        if let Err(e) = reader.read_exact(&mut zip_file_comment) {
346            if e.kind() == io::ErrorKind::UnexpectedEof {
347                return Err(invalid!("EOCD comment exceeds file boundary"));
348            }
349
350            return Err(e.into());
351        }
352
353        Ok(Zip32CentralDirectoryEnd {
354            disk_number,
355            disk_with_central_directory,
356            number_of_files_on_this_disk,
357            number_of_files,
358            central_directory_size,
359            central_directory_offset,
360            zip_file_comment,
361        })
362    }
363
364    /// Write the block
365    pub fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
366        let (block, comment) = self.into_block_and_comment();
367
368        if comment.len() > u16::MAX as usize {
369            return Err(invalid!("EOCD comment length exceeds u16::MAX"));
370        }
371
372        block.write(writer)?;
373        writer.write_all(&comment)?;
374        Ok(())
375    }
376
377    /// Check if the zip file could be a zip64
378    #[must_use]
379    pub fn may_be_zip64(&self) -> bool {
380        self.number_of_files == u16::MAX
381            || self.central_directory_size == u32::MAX
382            || self.central_directory_offset == u32::MAX
383    }
384}
385
386#[derive(Copy, Clone)]
387#[repr(packed, C)]
388#[allow(missing_docs)]
389pub struct Zip64CDELocatorBlock {
390    pub disk_with_central_directory: u32,
391    pub end_of_central_directory_offset: u64,
392    pub number_of_disks: u32,
393}
394
395unsafe impl Pod for Zip64CDELocatorBlock {}
396
397impl FixedSizeBlock for Zip64CDELocatorBlock {
398    const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE;
399
400    const WRONG_MAGIC_ERROR: ZipError =
401        invalid!("Invalid ZIP64 End of Central Directory Locator header");
402
403    to_and_from_le![
404        (disk_with_central_directory, u32),
405        (end_of_central_directory_offset, u64),
406        (number_of_disks, u32),
407    ];
408}
409
410#[allow(missing_docs)]
411pub struct Zip64CentralDirectoryEndLocator {
412    pub disk_with_central_directory: u32,
413    pub end_of_central_directory_offset: u64,
414    pub number_of_disks: u32,
415}
416
417impl Zip64CentralDirectoryEndLocator {
418    /// Parse
419    pub fn parse<T: Read + ?Sized>(reader: &mut T) -> ZipResult<Zip64CentralDirectoryEndLocator> {
420        let Zip64CDELocatorBlock {
421            disk_with_central_directory,
422            end_of_central_directory_offset,
423            number_of_disks,
424            ..
425        } = Zip64CDELocatorBlock::parse(reader)?;
426
427        Ok(Zip64CentralDirectoryEndLocator {
428            disk_with_central_directory,
429            end_of_central_directory_offset,
430            number_of_disks,
431        })
432    }
433
434    /// Get the block
435    #[must_use]
436    pub fn block(self) -> Zip64CDELocatorBlock {
437        let Self {
438            disk_with_central_directory,
439            end_of_central_directory_offset,
440            number_of_disks,
441        } = self;
442        Zip64CDELocatorBlock {
443            disk_with_central_directory,
444            end_of_central_directory_offset,
445            number_of_disks,
446        }
447    }
448
449    /// Write the block
450    pub fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
451        self.block().write(writer)
452    }
453}
454
455#[derive(Copy, Clone)]
456#[repr(packed, C)]
457#[allow(missing_docs)]
458pub struct Zip64CDEBlock {
459    pub record_size: u64,
460    pub version_made_by: u16,
461    pub version_needed_to_extract: u16,
462    pub disk_number: u32,
463    pub disk_with_central_directory: u32,
464    pub number_of_files_on_this_disk: u64,
465    pub number_of_files: u64,
466    pub central_directory_size: u64,
467    pub central_directory_offset: u64,
468}
469
470unsafe impl Pod for Zip64CDEBlock {}
471
472impl FixedSizeBlock for Zip64CDEBlock {
473    const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE;
474
475    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid ZIP64 Central Directory End header");
476
477    to_and_from_le![
478        (record_size, u64),
479        (version_made_by, u16),
480        (version_needed_to_extract, u16),
481        (disk_number, u32),
482        (disk_with_central_directory, u32),
483        (number_of_files_on_this_disk, u64),
484        (number_of_files, u64),
485        (central_directory_size, u64),
486        (central_directory_offset, u64),
487    ];
488}
489
490#[allow(missing_docs)]
491pub struct Zip64CentralDirectoryEnd {
492    pub record_size: u64,
493    pub version_made_by: u16,
494    pub version_needed_to_extract: u16,
495    pub disk_number: u32,
496    pub disk_with_central_directory: u32,
497    pub number_of_files_on_this_disk: u64,
498    pub number_of_files: u64,
499    pub central_directory_size: u64,
500    pub central_directory_offset: u64,
501    pub(crate) zip64_extensible_data_sector: Option<Box<[u8]>>,
502}
503
504impl Zip64CentralDirectoryEnd {
505    /// Minimum size of the block
506    /// Block - record_size - extensible_data
507    const MIN_SIZE: usize = 2 * size_of::<u16>() + 2 * size_of::<u32>() + 4 * size_of::<u64>();
508    pub(crate) const MIN_FULL_SIZE: usize =
509        2 * size_of::<u16>() + 2 * size_of::<u32>() + 5 * size_of::<u64>();
510    /// Size of ZIP64 EOCD signature + record_size field.
511    pub(crate) const RECORD_OVERHEAD: u64 = (size_of::<Magic>() + size_of::<u64>()) as u64;
512
513    pub(crate) fn parse<T: Read + ?Sized>(
514        reader: &mut T,
515        max_size: u64,
516    ) -> ZipResult<Zip64CentralDirectoryEnd> {
517        let Zip64CDEBlock {
518            record_size,
519            version_made_by,
520            version_needed_to_extract,
521            disk_number,
522            disk_with_central_directory,
523            number_of_files_on_this_disk,
524            number_of_files,
525            central_directory_size,
526            central_directory_offset,
527            ..
528        } = Zip64CDEBlock::parse(reader)?;
529
530        if record_size < (Self::MIN_SIZE - size_of::<Magic>()) as u64 {
531            return Err(invalid!("Low EOCD64 record size"));
532        } else if record_size.saturating_add(Self::RECORD_OVERHEAD) > max_size {
533            return Err(invalid!("EOCD64 extends beyond EOCD64 locator"));
534        }
535
536        let zip64_extensible_data_sector = if record_size > (Self::MIN_SIZE as u64) {
537            let mut extensible_data_sector =
538                vec![0u8; record_size as usize - Self::MIN_SIZE].into_boxed_slice();
539            if let Err(e) = reader.read_exact(&mut extensible_data_sector) {
540                if e.kind() == io::ErrorKind::UnexpectedEof {
541                    return Err(invalid!(
542                        "EOCD64 extensible data sector exceeds file boundary"
543                    ));
544                }
545                return Err(e.into());
546            }
547            Some(extensible_data_sector)
548        } else {
549            None
550        };
551
552        Ok(Self {
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            zip64_extensible_data_sector,
563        })
564    }
565
566    pub(crate) fn into_block_and_extensible_data(self) -> (Zip64CDEBlock, Option<Box<[u8]>>) {
567        let Self {
568            record_size,
569            version_made_by,
570            version_needed_to_extract,
571            disk_number,
572            disk_with_central_directory,
573            number_of_files_on_this_disk,
574            number_of_files,
575            central_directory_size,
576            central_directory_offset,
577            zip64_extensible_data_sector,
578        } = self;
579
580        (
581            Zip64CDEBlock {
582                record_size,
583                version_made_by,
584                version_needed_to_extract,
585                disk_number,
586                disk_with_central_directory,
587                number_of_files_on_this_disk,
588                number_of_files,
589                central_directory_size,
590                central_directory_offset,
591            },
592            zip64_extensible_data_sector,
593        )
594    }
595
596    pub(crate) fn write<T: Write>(self, writer: &mut T) -> ZipResult<()> {
597        let (block, zip64_extensible_data) = self.into_block_and_extensible_data();
598        block.write(writer)?;
599        if let Some(extensible_data) = zip64_extensible_data {
600            writer.write_all(&extensible_data)?;
601        }
602        Ok(())
603    }
604}
605
606pub(crate) struct DataAndPosition<T> {
607    pub data: T,
608    #[allow(dead_code)]
609    pub position: u64,
610}
611
612impl<T> From<(T, u64)> for DataAndPosition<T> {
613    fn from(value: (T, u64)) -> Self {
614        Self {
615            data: value.0,
616            position: value.1,
617        }
618    }
619}
620
621pub(crate) struct CentralDirectoryEndInfo {
622    pub eocd: DataAndPosition<Zip32CentralDirectoryEnd>,
623    pub eocd64: Option<DataAndPosition<Zip64CentralDirectoryEnd>>,
624
625    pub archive_offset: u64,
626}
627
628#[cfg(test)]
629mod tests {
630    use std::io::Cursor;
631
632    use crate::{
633        format::blocks::{FixedSizeBlock, Pod},
634        format::magic::Magic,
635        result::{ZipError, invalid},
636    };
637
638    #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
639    #[repr(packed, C)]
640    pub struct TestBlock {
641        pub file_name_length: u16,
642    }
643
644    unsafe impl Pod for TestBlock {}
645
646    impl FixedSizeBlock for TestBlock {
647        const MAGIC: Magic = Magic::literal(0x01111);
648
649        const WRONG_MAGIC_ERROR: ZipError = invalid!("unreachable");
650
651        to_and_from_le![(file_name_length, u16)];
652    }
653
654    /// Demonstrate that a block object can be safely written to memory and deserialized back out.
655    #[test]
656    fn block_serde() {
657        let block = TestBlock {
658            file_name_length: 3,
659        };
660        let mut c = Cursor::new(Vec::new());
661        block.write(&mut c).unwrap();
662        c.set_position(0);
663        let block2 = TestBlock::parse(&mut c).unwrap();
664        assert_eq!(block, block2);
665    }
666
667    #[test]
668    fn test_size_zip64_central_directory_end() {
669        use super::Zip64CentralDirectoryEnd;
670        assert_eq!(Zip64CentralDirectoryEnd::MIN_SIZE, 44);
671        assert_eq!(Zip64CentralDirectoryEnd::MIN_FULL_SIZE, 52);
672    }
673}