Skip to main content

zip/read/
mod.rs

1//! Types for reading ZIP archives
2
3use crate::compression::CompressionMethod;
4use crate::cp437::FromCp437;
5use crate::datetime::DateTime;
6use crate::extra_fields::{ExtraField, ExtraFields};
7use crate::format::blocks::{CentralDirectoryEndInfo, DataAndPosition, ZipCentralEntryBlock};
8use crate::format::flags::{ZipFileFlags, ZipFlags};
9use crate::format::system::System;
10use crate::result::{ZipError, ZipResult, invalid};
11use crate::types::ZipFileData;
12use indexmap::IndexMap;
13use std::io::{self, Read, Seek};
14use std::sync::OnceLock;
15
16mod config;
17pub use config::{ArchiveOffset, Config};
18
19pub(crate) mod extract;
20pub use extract::{ExtractOptions, RootDirFilter, SymlinkExtractAction, root_dir_common_filter};
21
22/// Provides high level API for reading from a stream.
23pub(crate) mod stream;
24pub use stream::{
25    read_zipfile_from_stream, read_zipfile_from_stream_with_compressed_size,
26    read_zipfile_from_stream_with_options,
27};
28
29pub(crate) mod magic_finder;
30pub(crate) mod readers;
31
32pub(crate) mod zipfile;
33pub use zipfile::{ZipFile, ZipFileEntry, ZipFileEntryWithDataDescriptor, ZipFileSeek};
34
35pub(crate) mod zip_archive;
36pub use zip_archive::{ZipArchive, ZipArchiveMetadata};
37
38#[cfg(feature = "aes-crypto")]
39pub use crate::aes::AesInfo;
40
41#[derive(Debug)]
42pub(crate) struct CentralDirectoryInfo {
43    pub(crate) archive_offset: u64,
44    pub(crate) directory_start: u64,
45    pub(crate) number_of_files: usize,
46    pub(crate) disk_number: u32,
47    pub(crate) disk_with_central_directory: u32,
48}
49
50impl<'a> TryFrom<&'a CentralDirectoryEndInfo> for CentralDirectoryInfo {
51    type Error = ZipError;
52
53    fn try_from(value: &'a CentralDirectoryEndInfo) -> Result<Self, Self::Error> {
54        let (relative_cd_offset, number_of_files, disk_number, disk_with_central_directory) =
55            match &value.eocd64 {
56                Some(DataAndPosition { data: eocd64, .. }) => {
57                    if eocd64.number_of_files_on_this_disk > eocd64.number_of_files {
58                        return Err(invalid!(
59                            "ZIP64 footer indicates more files on this disk than in the whole archive"
60                        ));
61                    }
62                    (
63                        eocd64.central_directory_offset,
64                        eocd64.number_of_files as usize,
65                        eocd64.disk_number,
66                        eocd64.disk_with_central_directory,
67                    )
68                }
69                _ => (
70                    u64::from(value.eocd.data.central_directory_offset),
71                    value.eocd.data.number_of_files_on_this_disk as usize,
72                    u32::from(value.eocd.data.disk_number),
73                    u32::from(value.eocd.data.disk_with_central_directory),
74                ),
75            };
76
77        let directory_start = relative_cd_offset
78            .checked_add(value.archive_offset)
79            .ok_or(invalid!("Invalid central directory size or offset"))?;
80
81        Ok(Self {
82            archive_offset: value.archive_offset,
83            directory_start,
84            number_of_files,
85            disk_number,
86            disk_with_central_directory,
87        })
88    }
89}
90
91impl<R: Read + Seek> ZipArchive<R> {
92    pub(crate) fn merge_contents<W: std::io::Write + Seek>(
93        &mut self,
94        mut w: W,
95    ) -> ZipResult<IndexMap<Box<[u8]>, ZipFileData>> {
96        if self.shared.files.is_empty() {
97            return Ok(IndexMap::new());
98        }
99        let mut new_files = self.shared.files.clone();
100        /* The first file header will probably start at the beginning of the file, but zip doesn't
101         * enforce that, and executable zips like PEX files will have a shebang line so will
102         * definitely be greater than 0.
103         *
104         * assert_eq!(0, new_files[0].header_start); // Avoid this.
105         */
106
107        let first_new_file_header_start = w.stream_position()?;
108
109        /* Push back file header starts for all entries in the covered files. */
110        new_files.values_mut().try_for_each(|f| {
111            /* This is probably the only really important thing to change. */
112            f.header_start = f
113                .header_start
114                .checked_add(first_new_file_header_start)
115                .ok_or(invalid!(
116                    "new header start from merge would have been too large"
117                ))?;
118            /* This is only ever used internally to cache metadata lookups (it's not part of the
119             * zip spec), and 0 is the sentinel value. */
120            f.central_header_start = 0;
121            /* This is an atomic variable so it can be updated from another thread in the
122             * implementation (which is good!). */
123            if let Some(old_data_start) = f.data_start.take() {
124                let new_data_start = old_data_start
125                    .checked_add(first_new_file_header_start)
126                    .ok_or(invalid!(
127                        "new data start from merge would have been too large"
128                    ))?;
129                f.data_start.get_or_init(|| new_data_start);
130            }
131            Ok::<_, ZipError>(())
132        })?;
133
134        /* Rewind to the beginning of the file.
135         *
136         * NB: we *could* decide to start copying from new_files[0].header_start instead, which
137         * would avoid copying over e.g. any pex shebangs or other file contents that start before
138         * the first zip file entry. However, zip files actually shouldn't care about garbage data
139         * in *between* real entries, since the central directory header records the correct start
140         * location of each, and keeping track of that math is more complicated logic that will only
141         * rarely be used, since most zips that get merged together are likely to be produced
142         * specifically for that purpose (and therefore are unlikely to have a shebang or other
143         * preface). Finally, this preserves any data that might actually be useful.
144         */
145        self.reader.rewind()?;
146        /* Find the end of the file data. */
147        let length_to_read = self.shared.dir_start;
148        /* Produce a Read that reads bytes up until the start of the central directory header.
149         * This "as &mut dyn Read" trick is used elsewhere to avoid having to clone the underlying
150         * handle, which it really shouldn't need to anyway. */
151        let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read);
152        /* Copy over file data from source archive directly. */
153        io::copy(&mut limited_raw, &mut w)?;
154
155        /* Return the files we've just written to the data stream. */
156        Ok(new_files)
157    }
158}
159
160#[inline]
161fn read_variable_length_byte_field<R: Read>(reader: &mut R, len: usize) -> ZipResult<Vec<u8>> {
162    let mut data = vec![0; len];
163    if let Err(e) = reader.read_exact(&mut data) {
164        if e.kind() == io::ErrorKind::UnexpectedEof {
165            return Err(invalid!(
166                "Variable-length field extends beyond file boundary"
167            ));
168        }
169        return Err(e.into());
170    }
171    Ok(data)
172}
173
174/// Parse a central directory entry to collect the information for the file.
175fn central_header_to_zip_file_inner<R: Read>(
176    reader: &mut R,
177    archive_offset: u64,
178    central_header_start: u64,
179    block: ZipCentralEntryBlock,
180) -> ZipResult<(ZipFileData, Vec<u8>)> {
181    let ZipCentralEntryBlock {
182        // magic,
183        version_made_by,
184        // version_to_extract,
185        flags,
186        compression_method,
187        last_mod_time,
188        last_mod_date,
189        crc32,
190        compressed_size,
191        uncompressed_size,
192        file_name_length,
193        extra_field_length,
194        file_comment_length,
195        // disk_number,
196        // internal_file_attributes,
197        external_file_attributes,
198        offset,
199        ..
200    } = block;
201
202    let is_utf8 = ZipFlags::matching(flags, ZipFlags::LanguageEncoding);
203
204    let mut file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?;
205    let extra_fields_raw = read_variable_length_byte_field(reader, extra_field_length as usize)?;
206    let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?;
207    let file_comment: Box<str> = if is_utf8 {
208        String::from_utf8_lossy(&file_comment_raw).into()
209    } else {
210        file_comment_raw.from_cp437()?.into()
211    };
212
213    let (version_made_by, system) = System::extract_bytes(version_made_by);
214    let extra_fields = ExtraFields::parse(&extra_fields_raw, &block, false)?;
215    // Construct the result
216    let mut result = ZipFileData {
217        system,
218        version_made_by,
219        compression_method: CompressionMethod::parse_from_u16(compression_method),
220        last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
221        crc32,
222        compressed_size: compressed_size.into(),
223        uncompressed_size: uncompressed_size.into(),
224        flags: ZipFileFlags(flags),
225        file_comment,
226        header_start: offset.into(),
227        central_header_start,
228        data_start: OnceLock::new(),
229        external_attributes: external_file_attributes,
230        large_file: false,
231        extra_fields,
232    };
233    result.apply_extra_fields(&mut file_name_raw)?;
234
235    // Account for shifted zip offsets.
236    result.header_start = result
237        .header_start
238        .checked_add(archive_offset)
239        .ok_or(invalid!("Archive header is too large"))?;
240
241    Ok((result, file_name_raw))
242}
243
244/// A trait for exposing file metadata inside the zip.
245pub trait HasZipMetadata {
246    /// Get the file metadata
247    fn get_metadata(&self) -> &ZipFileData;
248}
249
250/// Options for reading a file from an archive.
251#[derive(Default)]
252#[non_exhaustive]
253pub struct ZipReadOptions<'a> {
254    /// The password to use when decrypting the file.  This is ignored if not required.
255    pub(crate) password: Option<&'a [u8]>,
256    /// Ignore the value of the encryption flag and proceed as if the file were plaintext.
257    pub(crate) ignore_encryption_flag: bool,
258    /// Ignore the crc32 of the file
259    pub(crate) ignore_crc: bool,
260    /// override the compressed_size for stream read
261    force_compressed_size: Option<u64>,
262    /// override the uncompressed_size for stream read
263    force_uncompressed_size: Option<u64>,
264    /// override the checksum for stream read
265    force_crc: Option<u32>,
266    /// The uncompressed size is not known (streamed entry with a data descriptor), so it
267    /// can't bound the output.
268    pub(crate) size_unknown: bool,
269}
270
271impl<'a> ZipReadOptions<'a> {
272    /// Create a new set of options with the default values.
273    #[must_use]
274    pub fn new() -> Self {
275        Self::default()
276    }
277
278    /// Set the password, if any, to use.  Return for chaining.
279    #[must_use]
280    pub fn password(mut self, password: Option<&'a [u8]>) -> Self {
281        self.password = password;
282        self
283    }
284
285    /// Set the ignore encryption flag.  Return for chaining.
286    #[must_use]
287    pub fn ignore_encryption_flag(mut self, ignore: bool) -> Self {
288        self.ignore_encryption_flag = ignore;
289        self
290    }
291
292    /// Ignore the CRC32 of the file
293    #[must_use]
294    pub fn ignore_crc32(mut self, should_ignore: bool) -> Self {
295        self.ignore_crc = should_ignore;
296        self
297    }
298
299    /// Override the compressed_size
300    #[must_use]
301    pub fn override_compressed_size(mut self, comp_size: u64) -> Self {
302        self.force_compressed_size = Some(comp_size);
303        self
304    }
305
306    /// Override the uncompressed_size
307    #[must_use]
308    pub fn override_uncompressed_size(mut self, uncomp_size: u64) -> Self {
309        self.force_uncompressed_size = Some(uncomp_size);
310        self
311    }
312
313    /// Override the checksum
314    #[must_use]
315    pub fn override_crc(mut self, crc: u32) -> Self {
316        self.force_crc = Some(crc);
317        self
318    }
319}