Skip to main content

zip/read/
zipfile.rs

1//! Code related to zipfile
2
3use crate::CompressionMethod;
4use crate::DateTime;
5use crate::HasZipMetadata;
6use crate::ZIP64_BYTES_THR;
7use crate::ZipReadOptions;
8use crate::format::data_descriptor::ZipDataDescriptor;
9use crate::format::ffi;
10use crate::format::flags::ZipFileFlags;
11use crate::format::system::System;
12use crate::read::ExtraField;
13use crate::read::extract::RootDirFilter;
14use crate::read::readers::{ZipFileReader, ZipFileSeekReader};
15use crate::result::ZipResult;
16use crate::result::invalid;
17use crate::types::ZipFileData;
18use crate::write::options::SimpleFileOptions;
19
20use core::mem::replace;
21use std::borrow::Cow;
22use std::ffi::OsStr;
23use std::io::{self, Cursor, Read, Seek, SeekFrom, copy, sink};
24use std::path::{Path, PathBuf};
25
26/// A struct for reading a zip file
27///
28/// When reading from a `ZipFile` using [`Self::read()`], keep in mind that `read()` **does not guarantee** the buffer will be fully filled in a single call.
29///
30/// If your logic depends on the buffer being completely populated, use [`Self::read_exact()`] instead. It will continue reading until the entire buffer is filled or an error occurs.
31#[derive(Debug)]
32pub struct ZipFile<'a, R: Read + ?Sized> {
33    pub(crate) file_name_raw: Cow<'a, [u8]>,
34    pub(crate) data: Cow<'a, ZipFileData>,
35    pub(crate) reader: ZipFileReader<'a, R>,
36}
37
38/// A zip file Entry with a data descriptor (if present).
39///
40/// The method [`Self::data_descriptor`] can be use to retrieve it
41#[derive(Debug)]
42pub struct ZipFileEntryWithDataDescriptor<'a> {
43    pub(crate) file_name_raw: Cow<'a, [u8]>,
44    pub(crate) data: Cow<'a, ZipFileData>,
45    pub(crate) data_descriptor: Option<ZipDataDescriptor>,
46}
47
48/// A struct for reading a zip file, without a reader
49#[derive(Debug)]
50pub struct ZipFileEntry<'a> {
51    pub(crate) file_name_raw: Cow<'a, [u8]>,
52    pub(crate) data: Cow<'a, ZipFileData>,
53}
54
55/// A struct for reading and seeking a zip file
56pub struct ZipFileSeek<'a, R> {
57    pub(crate) file_name_raw: Cow<'a, [u8]>,
58    pub(crate) data: Cow<'a, ZipFileData>,
59    pub(crate) reader: ZipFileSeekReader<'a, R>,
60}
61
62macro_rules! zip_file_methods {
63    () => {
64        /// Get the name of the file, in the raw (internal) byte representation.
65        ///
66        /// The encoding of this data is currently undefined.
67        pub fn name_raw(&self) -> &[u8] {
68            &self.file_name_raw
69        }
70
71        /// Get the name of the file
72        ///
73        /// # Warnings
74        ///
75        /// It is dangerous to use this name directly when extracting an archive.
76        /// It may contain an absolute path (`/etc/shadow`), or break out of the
77        /// current directory (`../runtime`). Carelessly writing to these paths
78        /// allows an attacker to craft a ZIP archive that will overwrite critical
79        /// files.
80        ///
81        /// You can use the [`ZipFile::enclosed_name`] method to validate the name
82        /// as a safe path.
83        pub fn name(&self) -> ZipResult<Cow<'_, str>> {
84            self.get_metadata().name(&self.file_name_raw)
85        }
86
87        /// Rewrite the path, ignoring any path components with special meaning.
88        ///
89        /// - Absolute paths are made relative
90        /// - [`ParentDir`]s are ignored
91        /// - Truncates the filename at a NULL byte
92        ///
93        /// This is appropriate if you need to be able to extract *something* from
94        /// any archive, but will easily misrepresent trivial paths like
95        /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
96        /// [`ZipFile::enclosed_name`] is the better option in most scenarios.
97        ///
98        /// [`ParentDir`]: `std::path::Component::ParentDir`
99        pub fn mangled_name(&self) -> ZipResult<PathBuf> {
100            let file_name = self.name()?;
101            let sanitized = self.get_metadata().file_name_sanitized(&file_name);
102            Ok(sanitized)
103        }
104
105        /// Ensure the file path is safe to use as a [`Path`].
106        ///
107        /// - It can't contain NULL bytes
108        /// - It can't resolve to a path outside the current directory
109        ///   > `foo/../bar` is fine, `foo/../../bar` is not.
110        /// - It can't be an absolute path
111        ///
112        /// This will read well-formed ZIP files correctly, and is resistant
113        /// to path-based exploits. It is recommended over
114        /// [`ZipFile::mangled_name`].
115        pub fn enclosed_name(&self) -> Option<PathBuf> {
116            self.get_metadata().enclosed_name(&self.name().ok()?)
117        }
118
119        /// Get the starting offset of the zip header for this file
120        pub fn header_start(&self) -> u64 {
121            self.get_metadata().header_start
122        }
123        /// Get the starting offset of the zip header in the central directory for this file
124        pub fn central_header_start(&self) -> u64 {
125            self.get_metadata().central_header_start
126        }
127
128        /// Get the version of the file
129        pub fn version_made_by(&self) -> (u8, u8) {
130            (
131                self.get_metadata().version_made_by / 10,
132                self.get_metadata().version_made_by % 10,
133            )
134        }
135
136        /// PKZIP version needed to open this file (from APPNOTE 4.4.3.2).
137        pub fn version_needed(&self) -> u16 {
138            self.get_metadata().version_needed()
139        }
140
141        /// Get the comment of the file
142        pub fn comment(&self) -> &str {
143            &self.get_metadata().file_comment
144        }
145
146        /// Get the system of the file
147        pub fn system(&self) -> System {
148            self.get_metadata().system
149        }
150
151        /// Get the external attributes of the file
152        pub fn external_attributes(&self) -> u32 {
153            self.get_metadata().external_attributes
154        }
155
156        /// Get the flags of the file
157        pub fn flags(&self) -> ZipFileFlags {
158            self.get_metadata().flags
159        }
160
161        /// Get the compression method used to store the file
162        pub fn compression(&self) -> CompressionMethod {
163            self.get_metadata().compression_method
164        }
165
166        /// Get if the files is encrypted or not
167        pub fn encrypted(&self) -> bool {
168            self.data.is_encrypted()
169        }
170
171        /// Get the size of the file, in bytes, in the archive
172        pub fn compressed_size(&self) -> u64 {
173            self.get_metadata().compressed_size
174        }
175
176        /// Get the size of the file, in bytes, when uncompressed
177        pub fn size(&self) -> u64 {
178            self.get_metadata().uncompressed_size
179        }
180
181        /// Get the time the file was last modified
182        pub fn last_modified(&self) -> Option<DateTime> {
183            self.get_metadata().last_modified_time
184        }
185        /// Returns whether the file is actually a directory
186        pub fn is_dir(&self) -> bool {
187            self.get_metadata().is_dir(&self.file_name_raw)
188        }
189
190        /// Returns whether the file is actually a symbolic link
191        pub fn is_symlink(&self) -> bool {
192            self.unix_mode()
193                .is_some_and(|mode| mode & ffi::S_IFLNK == ffi::S_IFLNK)
194        }
195
196        /// Returns whether the file is a normal file (i.e. not a directory or symlink)
197        pub fn is_file(&self) -> bool {
198            !self.is_dir() && !self.is_symlink()
199        }
200
201        /// Returns whether the file is read-only. (On Unix, this means it's not readable by
202        /// *anyone*.)
203        pub fn is_read_only(&self) -> bool {
204            self.unix_mode().is_some_and(|mode| mode & 0o222 == 0)
205        }
206
207        /// Get unix mode for the file
208        pub fn unix_mode(&self) -> Option<u32> {
209            self.get_metadata().unix_mode()
210        }
211
212        /// Get the CRC32 hash of the original file
213        pub fn crc32(&self) -> u32 {
214            self.get_metadata().crc32
215        }
216    };
217}
218impl<'a> ZipFileEntryWithDataDescriptor<'a> {
219    zip_file_methods!();
220
221    /// Get the data descriptor
222    pub fn data_descriptor(&self) -> &Option<ZipDataDescriptor> {
223        &self.data_descriptor
224    }
225}
226
227impl HasZipMetadata for ZipFileEntryWithDataDescriptor<'_> {
228    fn get_metadata(&self) -> &ZipFileData {
229        self.data.as_ref()
230    }
231}
232
233impl<'a> ZipFileEntry<'a> {
234    zip_file_methods!();
235
236    /// Create a readable
237    pub fn with_reader<R: Read + Seek>(
238        self,
239        reader: &'a mut R,
240        options: ZipReadOptions<'_>,
241    ) -> ZipResult<ZipFile<'a, R>> {
242        let limit_reader = self.data.find_content(reader)?;
243
244        let reader = self.data.make_reader(limit_reader, options)?;
245        Ok(ZipFile {
246            file_name_raw: self.file_name_raw,
247            data: self.data,
248            reader,
249        })
250    }
251}
252
253impl HasZipMetadata for ZipFileEntry<'_> {
254    fn get_metadata(&self) -> &ZipFileData {
255        self.data.as_ref()
256    }
257}
258
259/// Methods for retrieving information on zip files
260impl<'a, R: Read + ?Sized> ZipFile<'a, R> {
261    zip_file_methods!();
262
263    pub(crate) fn take_raw_reader(&mut self) -> io::Result<io::Take<&'a mut R>> {
264        replace(&mut self.reader, ZipFileReader::NoReader).into_inner()
265    }
266
267    /// Prepare the path for extraction by creating necessary missing directories and checking for symlinks to be contained within the base path.
268    ///
269    /// `base_path` parameter is assumed to be canonicalized.
270    pub(crate) fn safe_prepare_path(
271        &self,
272        base_path: &Path,
273        outpath: &mut PathBuf,
274        root_dir: Option<&(Vec<&OsStr>, impl RootDirFilter)>,
275    ) -> ZipResult<()> {
276        let file_name = self.name()?;
277        let components = self
278            .data
279            .simplified_components(&file_name)
280            .ok_or(invalid!("Invalid file path"))?;
281
282        let components = match root_dir {
283            Some((root_dir, filter)) => match components.strip_prefix(&**root_dir) {
284                Some(components) => components,
285
286                // In this case, we expect that the file was not in the root
287                // directory, but was filtered out when searching for the
288                // root directory.
289                None => {
290                    // We could technically find ourselves at this code
291                    // path if the user provides an unstable or
292                    // non-deterministic `filter` function.
293                    //
294                    // If debug assertions are on, we should panic here.
295                    // Otherwise, the safest thing to do here is to just
296                    // extract as-is.
297                    debug_assert!(
298                        !filter(&PathBuf::from_iter(components.iter())),
299                        "Root directory filter should not match at this point"
300                    );
301
302                    // Extract as-is.
303                    &components[..]
304                }
305            },
306
307            None => &components[..],
308        };
309
310        *outpath = crate::path::resolve_enclosed(
311            base_path,
312            base_path,
313            components.iter().map(|c| c.to_os_string()),
314            true, // create intermediate directories
315        )?;
316
317        Ok(())
318    }
319
320    /// Get the extra data of the zip header for this file
321    pub fn extra_data(&self) -> Option<Vec<u8>> {
322        let out_buffer = Vec::new();
323        let mut cursor = Cursor::new(out_buffer);
324        let extra_fields = self.data.extra_fields.local_extra_fields();
325        for one_extra_field in extra_fields {
326            one_extra_field.write(&mut cursor, true).ok()?;
327        }
328        let extra_fields_data = cursor.into_inner();
329        if extra_fields_data.is_empty() {
330            None
331        } else {
332            Some(extra_fields_data)
333        }
334    }
335
336    /// Get the starting offset of the data of the compressed file
337    pub fn data_start(&self) -> Option<u64> {
338        self.data.data_start.get().copied()
339    }
340
341    /// Get the [`SimpleFileOptions`] that would be used to write this file to
342    /// a new zip archive.
343    pub fn options(&self) -> SimpleFileOptions {
344        let mut options = SimpleFileOptions::default()
345            .large_file(self.compressed_size().max(self.size()) >= ZIP64_BYTES_THR)
346            .compression_method(self.compression())
347            .system(self.system())
348            .external_attributes(self.external_attributes())
349            .last_modified_time(
350                self.last_modified()
351                    .filter(DateTime::is_valid)
352                    .unwrap_or_else(DateTime::default_for_write),
353            );
354
355        options.normalize();
356        #[cfg(feature = "aes-crypto")]
357        if let Some((mode, vendor_version)) = self.get_metadata().aes_settings() {
358            use crate::write::options::EncryptWith;
359            // Preserve AES metadata in options for downstream writers.
360            // This is metadata-only and does not trigger encryption.
361            options.encrypt_with = Some(EncryptWith::Aes {
362                mode,
363                vendor_version,
364                salt: None,
365                password: None,
366            });
367        }
368        options
369    }
370}
371
372/// Methods for retrieving information on zip files
373impl<R: Read> ZipFile<'_, R> {
374    /// iterate through all extra fields
375    pub fn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> {
376        self.data.extra_fields.inner.iter()
377    }
378}
379
380impl<R: Read + ?Sized> HasZipMetadata for ZipFile<'_, R> {
381    fn get_metadata(&self) -> &ZipFileData {
382        self.data.as_ref()
383    }
384}
385
386impl<R: Read + ?Sized> Read for ZipFile<'_, R> {
387    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
388        self.reader.read(buf)
389    }
390
391    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
392        self.reader.read_to_end(buf)
393    }
394
395    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
396        self.reader.read_to_string(buf)
397    }
398
399    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
400        self.reader.read_exact(buf)
401    }
402}
403
404impl<R: Read> Read for ZipFileSeek<'_, R> {
405    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
406        match &mut self.reader {
407            ZipFileSeekReader::Raw(r) => r.read(buf),
408        }
409    }
410}
411
412impl<R: Seek> Seek for ZipFileSeek<'_, R> {
413    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
414        match &mut self.reader {
415            ZipFileSeekReader::Raw(r) => r.seek(pos),
416        }
417    }
418}
419
420impl<R> HasZipMetadata for ZipFileSeek<'_, R> {
421    fn get_metadata(&self) -> &ZipFileData {
422        self.data.as_ref()
423    }
424}
425
426impl<'a, R> ZipFileSeek<'a, R> {
427    zip_file_methods!();
428}
429
430impl<R: Read + ?Sized> Drop for ZipFile<'_, R> {
431    fn drop(&mut self) {
432        // self.data is Owned, this reader is constructed by a streaming reader.
433        // In this case, we want to exhaust the reader so that the next file is accessible.
434        if let Cow::Owned(_) = self.data {
435            // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped.
436            if let Ok(mut inner) = self.take_raw_reader() {
437                let _ = copy(&mut inner, &mut sink());
438            }
439        }
440    }
441}