Skip to main content

zip/read/
zip_archive.rs

1//! Code related to `ZipArchive`
2
3use crate::compression::CompressionMethod;
4use crate::format::blocks::{FixedSizeBlock, ZipCentralEntryBlock};
5use crate::format::find_central_directory_end;
6use crate::read::config::Config;
7use crate::read::extract::RootDirFilter;
8use crate::read::readers::{ZipFileReader, ZipFileSeekReader};
9use crate::read::zipfile::{ZipFileEntry, ZipFileEntryWithDataDescriptor};
10use crate::read::{
11    ArchiveOffset, CentralDirectoryInfo, ZipFile, ZipFileSeek, ZipReadOptions,
12    central_header_to_zip_file_inner,
13};
14use crate::result::{ZipError, ZipResult};
15use crate::types::ZipFileData;
16use crate::unstable::path_to_string;
17use core::ops::Range;
18use indexmap::IndexMap;
19use std::borrow::Cow;
20use std::io::{Read, Seek, SeekFrom};
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23/// Immutable metadata about a `ZipArchive`.
24#[derive(Debug)]
25pub struct ZipArchiveMetadata {
26    pub(crate) files: IndexMap<Box<[u8]>, ZipFileData>,
27    pub(crate) offset: u64,
28    pub(crate) dir_start: u64,
29    // This isn't yet used anywhere, but it is here for use cases in the future.
30    #[allow(dead_code)]
31    pub(crate) config: Config,
32    pub(crate) comment: Box<[u8]>,
33    pub(crate) zip64_extensible_data_sector: Option<Box<[u8]>>,
34}
35
36impl ZipArchiveMetadata {
37    /// Number of files contained in the central directory.
38    #[must_use]
39    pub fn len(&self) -> usize {
40        self.files.len()
41    }
42
43    /// Whether the central directory contains no files.
44    #[must_use]
45    pub fn is_empty(&self) -> bool {
46        self.len() == 0
47    }
48
49    /// Get a central directory entry by index
50    pub fn entry(&self, index: usize) -> ZipResult<ZipFileEntry<'_>> {
51        let (file_name_raw, data) = self.files.get_index(index).ok_or(ZipError::FileNotFound)?;
52        Ok(ZipFileEntry {
53            file_name_raw: Cow::Borrowed(file_name_raw),
54            data: Cow::Borrowed(data),
55        })
56    }
57}
58
59#[derive(Debug)]
60pub(crate) struct SharedBuilder {
61    pub(crate) files: Vec<(Box<[u8]>, ZipFileData)>,
62    pub(super) offset: u64,
63    pub(super) dir_start: u64,
64    // This isn't yet used anywhere, but it is here for use cases in the future.
65    #[allow(dead_code)]
66    pub(super) config: Config,
67}
68
69impl SharedBuilder {
70    pub(crate) fn build(
71        self,
72        comment: Box<[u8]>,
73        zip64_extensible_data_sector: Option<Box<[u8]>>,
74    ) -> ZipArchiveMetadata {
75        let mut index_map = IndexMap::with_capacity(self.files.len());
76        self.files.into_iter().for_each(|(file_name_raw, file)| {
77            index_map.insert(file_name_raw, file);
78        });
79        ZipArchiveMetadata {
80            files: index_map,
81            offset: self.offset,
82            dir_start: self.dir_start,
83            config: self.config,
84            comment,
85            zip64_extensible_data_sector,
86        }
87    }
88}
89
90/// ZIP archive reader
91///
92/// At the moment, this type is cheap to clone if this is the case for the
93/// reader it uses. However, this is not guaranteed by this crate and it may
94/// change in the future.
95///
96/// ```
97/// use std::io::{Read, Seek};
98/// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
99///     let mut zip = zip::ZipArchive::new(reader)?;
100///
101///     for i in 0..zip.len() {
102///         let mut file = zip.by_index(i)?;
103///         let name = file.name()?;
104///         println!("Filename: {}", name);
105///         std::io::copy(&mut file, &mut std::io::stdout())?;
106///     }
107///
108///     Ok(())
109/// }
110/// ```
111#[derive(Clone, Debug)]
112pub struct ZipArchive<R> {
113    pub(super) reader: R,
114    pub(crate) shared: Arc<ZipArchiveMetadata>,
115}
116
117impl<R> ZipArchive<R> {
118    pub(crate) fn from_finalized_writer(
119        files: IndexMap<Box<[u8]>, ZipFileData>,
120        comment: Box<[u8]>,
121        zip64_extensible_data_sector: Option<Box<[u8]>>,
122        reader: R,
123        central_start: u64,
124    ) -> Self {
125        let initial_offset = match files.first() {
126            Some((_, file)) => file.header_start,
127            None => central_start,
128        };
129        let shared = Arc::new(ZipArchiveMetadata {
130            files,
131            offset: initial_offset,
132            dir_start: central_start,
133            config: Config {
134                archive_offset: ArchiveOffset::Known(initial_offset),
135            },
136            comment,
137            zip64_extensible_data_sector,
138        });
139        Self { reader, shared }
140    }
141
142    /// Total size of the files in the archive, if it can be known. Doesn't include directories or
143    /// metadata.
144    pub fn decompressed_size(&self) -> Option<u128> {
145        let mut total = 0u128;
146        for file in self.shared.files.values() {
147            if file.is_using_data_descriptor() {
148                return None;
149            }
150            total = total.checked_add(u128::from(file.uncompressed_size))?;
151        }
152        Some(total)
153    }
154}
155
156impl<R: Read + Seek> ZipArchive<R> {
157    /// Get the directory start offset and number of files. This is done in a
158    /// separate function to ease the control flow design.
159    pub(crate) fn get_metadata(config: Config, reader: &mut R) -> ZipResult<ZipArchiveMetadata> {
160        // End of the probed region, initially set to the end of the file
161        let file_len = reader.seek(SeekFrom::End(0))?;
162        let mut end_exclusive = file_len;
163        let mut last_err = None;
164
165        loop {
166            // Find the EOCD and possibly EOCD64 entries and determine the archive offset.
167            let cde = match find_central_directory_end(
168                reader,
169                config.archive_offset,
170                end_exclusive,
171                file_len,
172            ) {
173                Ok(cde) => cde,
174                Err(e) => {
175                    // return the previous error first (if there is)
176                    return Err(last_err.unwrap_or(e));
177                }
178            };
179
180            // Turn EOCD into internal representation.
181            match CentralDirectoryInfo::try_from(&cde)
182                .and_then(|info| Self::read_central_header(&info, config, reader))
183            {
184                Ok(shared) => {
185                    let zip64_extensible_data_sector = if let Some(eocd64) = cde.eocd64 {
186                        eocd64.data.zip64_extensible_data_sector
187                    } else {
188                        None
189                    };
190                    return Ok(
191                        shared.build(cde.eocd.data.zip_file_comment, zip64_extensible_data_sector)
192                    );
193                }
194                Err(e) => {
195                    last_err = Some(e);
196                }
197            }
198            // Something went wrong while decoding the cde, try to find a new one
199            end_exclusive = cde.eocd.position;
200            continue;
201        }
202    }
203
204    fn read_central_header(
205        dir_info: &CentralDirectoryInfo,
206        config: Config,
207        reader: &mut R,
208    ) -> Result<SharedBuilder, ZipError> {
209        // If the parsed number of files is greater than the offset then
210        // something fishy is going on and we shouldn't trust number_of_files.
211        let file_capacity = if dir_info.number_of_files > dir_info.directory_start as usize {
212            0
213        } else {
214            dir_info.number_of_files
215        };
216
217        if dir_info.disk_number != dir_info.disk_with_central_directory {
218            return Err(ZipError::UnsupportedArchive(
219                "Support for multi-disk files is not implemented",
220            ));
221        }
222
223        if file_capacity.saturating_mul(size_of::<ZipFileData>()) > isize::MAX as usize {
224            return Err(ZipError::UnsupportedArchive("Oversized central directory"));
225        }
226
227        let mut files = Vec::with_capacity(file_capacity);
228        reader.seek(SeekFrom::Start(dir_info.directory_start))?;
229        for _ in 0..dir_info.number_of_files {
230            let central_header_start = reader.stream_position()?;
231
232            // Parse central header
233            let block = ZipCentralEntryBlock::parse(reader)?;
234
235            let (file, file_name_raw) = central_header_to_zip_file_inner(
236                reader,
237                dir_info.archive_offset,
238                central_header_start,
239                block,
240            )?;
241            files.push((file_name_raw.into(), file));
242        }
243
244        Ok(SharedBuilder {
245            files,
246            offset: dir_info.archive_offset,
247            dir_start: dir_info.directory_start,
248            config,
249        })
250    }
251
252    /// Returns the verification value and salt for the AES encryption of the file
253    ///
254    /// It fails if the file number is invalid.
255    ///
256    /// # Returns
257    ///
258    /// - None if the file is not encrypted with AES
259    #[cfg(feature = "aes-crypto")]
260    pub fn get_aes_verification_key_and_salt(
261        &mut self,
262        file_number: usize,
263    ) -> ZipResult<Option<crate::aes::AesInfo>> {
264        let (_, data) = self
265            .shared
266            .files
267            .get_index(file_number)
268            .ok_or(ZipError::FileNotFound)?;
269
270        let limit_reader = data.find_content(&mut self.reader)?;
271        match data.aes_settings() {
272            None => Ok(None),
273            Some((aes_mode, _)) => {
274                let (verification_value, salt) =
275                    crate::aes::AesReader::new(limit_reader, aes_mode, data.compressed_size)?
276                        .get_verification_value_and_salt()?;
277                let aes_info = crate::aes::AesInfo {
278                    aes_mode,
279                    verification_value,
280                    salt,
281                };
282                Ok(Some(aes_info))
283            }
284        }
285    }
286
287    /// Read a ZIP archive, collecting the files it contains.
288    ///
289    /// This uses the central directory record of the ZIP file, and ignores local file headers.
290    ///
291    /// A default [`Config`] is used.
292    pub fn new(reader: R) -> ZipResult<ZipArchive<R>> {
293        Self::with_config(Config::default(), reader)
294    }
295
296    /// Get the metadata associated with the ZIP archive.
297    ///
298    /// This can be used with [`Self::unsafe_new_with_metadata`] to create a new reader over the
299    /// same file without needing to reparse the metadata.
300    pub fn metadata(&self) -> Arc<ZipArchiveMetadata> {
301        self.shared.clone()
302    }
303
304    /// Read a ZIP archive using the given `metadata`.
305    ///
306    /// This is useful for creating multiple readers over the same file without
307    /// needing to reparse the metadata.
308    ///
309    /// # Safety
310    /// `unsafe` is used here to indicate that `reader` and `metadata` could
311    /// potentially be incompatible, and it is left to the user to ensure they are.
312    ///
313    /// # Example
314    ///
315    /// ```no_run
316    /// # use std::fs;
317    /// use rayon::prelude::*;
318    ///
319    /// const FILE_NAME: &str = "my_data.zip";
320    ///
321    /// let file = fs::File::open(FILE_NAME).unwrap();
322    /// let mut archive = zip::ZipArchive::new(file).unwrap();
323    ///
324    /// let file_names = (0..archive.len())
325    ///     .into_par_iter()
326    ///     .map_init({
327    ///         let metadata = archive.metadata();
328    ///         move || {
329    ///             let file = fs::File::open(FILE_NAME).unwrap();
330    ///             unsafe { zip::ZipArchive::unsafe_new_with_metadata(file, metadata.clone()) }
331    ///         }},
332    ///         |archive, i| {
333    ///             let mut file = archive.by_index(i).unwrap();
334    ///             file.enclosed_name()
335    ///         }
336    ///     )
337    ///     .filter_map(|name| name)
338    ///     .collect::<Vec<_>>();
339    /// ```
340    pub unsafe fn unsafe_new_with_metadata(reader: R, metadata: Arc<ZipArchiveMetadata>) -> Self {
341        Self {
342            reader,
343            shared: metadata,
344        }
345    }
346
347    /// Read a ZIP archive providing a read configuration, collecting the files it contains.
348    ///
349    /// This uses the central directory record of the ZIP file, and ignores local file headers.
350    pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
351        let shared = Self::get_metadata(config, &mut reader)?;
352
353        Ok(ZipArchive {
354            reader,
355            shared: Arc::new(shared),
356        })
357    }
358
359    /// Number of files contained in this zip.
360    pub fn len(&self) -> usize {
361        self.shared.len()
362    }
363
364    /// Get the starting offset of the zip central directory.
365    pub fn central_directory_start(&self) -> u64 {
366        self.shared.dir_start
367    }
368
369    /// Whether this zip archive contains no files
370    pub fn is_empty(&self) -> bool {
371        self.len() == 0
372    }
373
374    /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes.
375    ///
376    /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size
377    /// of that prepended data.
378    pub fn offset(&self) -> u64 {
379        self.shared.offset
380    }
381
382    /// Get the comment of the zip archive.
383    pub fn comment(&self) -> &[u8] {
384        &self.shared.comment
385    }
386
387    /// Get the ZIP64 extensible_data of the zip archive, if it is ZIP64.
388    pub fn raw_zip64_extensible_data_sector(&self) -> Option<&[u8]> {
389        self.shared.zip64_extensible_data_sector.as_deref()
390    }
391
392    /// Returns an iterator over all the file and directory names in this archive.
393    pub fn file_names(&self) -> impl Iterator<Item = ZipResult<Cow<'_, str>>> {
394        self.shared
395            .files
396            .iter()
397            .map(|(file_name_raw, data)| data.name(file_name_raw))
398    }
399
400    /// Returns Ok(true) if any compressed data in this archive belongs to more than one file. This
401    /// doesn't make the archive invalid, but some programs will refuse to decompress it because m
402    /// copies of a block that decompresses to length n would occupy `m*n` bytes in the destination
403    /// but require only an archive size of O(m + n) bytes.
404    ///
405    /// This will return true on an archive where `zip::write::shallow_copy_file` has been used,
406    /// unless the file being copied has a compressed size of zero.
407    ///
408    /// Doesn't count cases where one file entry's header overlaps with another's header or data.
409    /// To include those cases, use [ZipArchive::has_overlapping_entries] instead.
410    pub fn has_overlapping_files(&mut self) -> ZipResult<bool> {
411        self.has_overlap(false)
412    }
413
414    /// Returns Ok(true) if any byte in this archive belongs to more than one file entry, including
415    /// the header. Similar to [ZipArchive::has_overlapping_files] except that it also checks for
416    /// header-with-header and header-with-data overlaps. These can be used to increase the number
417    /// of files in a ZIP archive so that the extracted files' metadata takes up a larger multiple
418    /// of the archive's size (but still a constant multiple, since seeking readers require a
419    /// central-directory entry for each file and streaming readers process one entry at a time).
420    ///
421    /// This will return true on an archive where `zip::write::shallow_copy_file` has been used,
422    /// even when the file being copied has a size of zero.
423    pub fn has_overlapping_entries(&mut self) -> ZipResult<bool> {
424        self.has_overlap(true)
425    }
426
427    fn has_overlap(&mut self, include_header: bool) -> ZipResult<bool> {
428        let mut ranges = Vec::<Range<u64>>::with_capacity(self.shared.files.len());
429        for file in self.shared.files.values() {
430            if !include_header && file.compressed_size == 0 {
431                continue;
432            }
433            let data_start = file.data_start(&mut self.reader)?;
434            let start = if include_header {
435                file.header_start
436            } else {
437                data_start
438            };
439            let end =
440                data_start
441                    .checked_add(file.compressed_size)
442                    .ok_or(ZipError::InvalidArchive(Cow::Borrowed(
443                        "File data end offset overflows u64",
444                    )))?;
445            ranges.push(start..end);
446        }
447        ranges.sort_unstable_by_key(|r| (r.start, r.end));
448        Ok(ranges.windows(2).any(|w| w[0].end > w[1].start))
449    }
450
451    /// Search for a file entry by name, decrypt with given password
452    ///
453    /// # Warning
454    ///
455    /// The implementation of the cryptographic algorithms has not
456    /// gone through a correctness review, and you should assume it is insecure:
457    /// passwords used with this API may be compromised.
458    ///
459    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
460    /// to check for a 1/256 chance that the password is correct.
461    /// There are many passwords out there that will also pass the validity checks
462    /// we are able to perform. This is a weakness of the `ZipCrypto` algorithm,
463    /// due to its fairly primitive approach to cryptography.
464    pub fn by_name_decrypt(&mut self, name: &str, password: &[u8]) -> ZipResult<ZipFile<'_, R>> {
465        self.by_name_with_optional_password(name, Some(password))
466    }
467
468    /// Search for a file entry by name
469    pub fn by_name(&mut self, name: &str) -> ZipResult<ZipFile<'_, R>> {
470        self.by_name_with_optional_password(name, None)
471    }
472
473    /// Get the index of a file entry by name, if it's present.
474    #[inline]
475    pub fn index_for_name(&self, name: &str) -> Option<usize> {
476        self.shared.files.get_index_of(name.as_bytes())
477    }
478
479    /// Get data and the data descriptor of the file
480    /// This method will work if the zip is using the data descriptor signature (see 4.3.9.3)
481    /// To access the data descriptor, the ZipFileData needs to have a correct `compressed_size` and
482    /// the Data Descriptor should follow the data
483    pub fn by_index_with_data_descriptor(
484        &mut self,
485        index: usize,
486    ) -> ZipResult<ZipFileEntryWithDataDescriptor<'_>> {
487        let (file_name_raw, data) = self
488            .shared
489            .files
490            .get_index(index)
491            .ok_or(ZipError::FileNotFound)?;
492
493        let data_descriptor = data.get_data_descriptor(&mut self.reader)?;
494        Ok(ZipFileEntryWithDataDescriptor {
495            file_name_raw: Cow::Borrowed(file_name_raw),
496            data: Cow::Borrowed(data),
497            data_descriptor,
498        })
499    }
500
501    /// Search for a file entry by path, decrypt with given password
502    ///
503    /// # Warning
504    ///
505    /// The implementation of the cryptographic algorithms has not
506    /// gone through a correctness review, and you should assume it is insecure:
507    /// passwords used with this API may be compromised.
508    ///
509    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
510    /// to check for a 1/256 chance that the password is correct.
511    /// There are many passwords out there that will also pass the validity checks
512    /// we are able to perform. This is a weakness of the `ZipCrypto` algorithm,
513    /// due to its fairly primitive approach to cryptography.
514    pub fn by_path_decrypt<T: AsRef<Path>>(
515        &mut self,
516        path: T,
517        password: &[u8],
518    ) -> ZipResult<ZipFile<'_, R>> {
519        self.index_for_path(path)
520            .ok_or(ZipError::FileNotFound)
521            .and_then(|index| {
522                self.by_index_with_options(index, ZipReadOptions::new().password(Some(password)))
523            })
524    }
525
526    /// Search for a file entry by path
527    pub fn by_path<T: AsRef<Path>>(&mut self, path: T) -> ZipResult<ZipFile<'_, R>> {
528        self.index_for_path(path)
529            .ok_or(ZipError::FileNotFound)
530            .and_then(|index| self.by_index_with_options(index, ZipReadOptions::new()))
531    }
532
533    /// Get the index of a file entry by path, if it's present.
534    #[inline(always)]
535    pub fn index_for_path<T: AsRef<Path>>(&self, path: T) -> Option<usize> {
536        let path_as_string = &path_to_string(path).ok()?;
537        self.index_for_name(path_as_string)
538    }
539
540    /// Get the name of a file entry, if it's present.
541    #[inline(always)]
542    pub fn name_for_index(&self, index: usize) -> Option<ZipResult<Cow<'_, str>>> {
543        self.shared
544            .files
545            .get_index(index)
546            .map(|(file_name_raw, zip_file_data)| zip_file_data.name(file_name_raw))
547    }
548
549    /// Search for a file entry by name and return a seekable object.
550    pub fn by_name_seek(&mut self, name: &str) -> ZipResult<ZipFileSeek<'_, R>> {
551        self.by_index_seek(self.index_for_name(name).ok_or(ZipError::FileNotFound)?)
552    }
553
554    /// Search for a file entry by index and return a seekable object.
555    pub fn by_index_seek(&mut self, index: usize) -> ZipResult<ZipFileSeek<'_, R>> {
556        let reader = &mut self.reader;
557        self.shared
558            .files
559            .get_index(index)
560            .ok_or(ZipError::FileNotFound)
561            .and_then(move |(file_name_raw, data)| {
562                let seek_reader = match data.compression_method {
563                    CompressionMethod::Stored => {
564                        ZipFileSeekReader::Raw(data.find_content_seek(reader)?)
565                    }
566                    _ => {
567                        return Err(ZipError::UnsupportedArchive(
568                            "Seekable compressed files are not yet supported",
569                        ));
570                    }
571                };
572                Ok(ZipFileSeek {
573                    file_name_raw: Cow::Borrowed(file_name_raw),
574                    reader: seek_reader,
575                    data: Cow::Borrowed(data),
576                })
577            })
578    }
579
580    fn by_name_with_optional_password<'a>(
581        &'a mut self,
582        name: &str,
583        password: Option<&[u8]>,
584    ) -> ZipResult<ZipFile<'a, R>> {
585        let Some(index) = self.shared.files.get_index_of(name.as_bytes()) else {
586            return Err(ZipError::FileNotFound);
587        };
588        self.by_index_with_options(index, ZipReadOptions::new().password(password))
589    }
590
591    /// Get a contained file by index, decrypt with given password
592    ///
593    /// # Warning
594    ///
595    /// The implementation of the cryptographic algorithms has not
596    /// gone through a correctness review, and you should assume it is insecure:
597    /// passwords used with this API may be compromised.
598    ///
599    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
600    /// to check for a 1/256 chance that the password is correct.
601    /// There are many passwords out there that will also pass the validity checks
602    /// we are able to perform. This is a weakness of the `ZipCrypto` algorithm,
603    /// due to its fairly primitive approach to cryptography.
604    pub fn by_index_decrypt(
605        &mut self,
606        file_number: usize,
607        password: &[u8],
608    ) -> ZipResult<ZipFile<'_, R>> {
609        self.by_index_with_options(file_number, ZipReadOptions::new().password(Some(password)))
610    }
611
612    /// Get a contained file by index
613    pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_, R>> {
614        self.by_index_with_options(file_number, ZipReadOptions::new())
615    }
616
617    /// Get a contained file by index
618    pub fn by_index_data(&self, file_number: usize) -> ZipResult<ZipFileEntry<'_>> {
619        self.shared.entry(file_number)
620    }
621
622    /// Get a contained file by index without decompressing it
623    pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_, R>> {
624        let reader = &mut self.reader;
625        let (file_name_raw, data) = self
626            .shared
627            .files
628            .get_index(file_number)
629            .ok_or(ZipError::FileNotFound)?;
630        Ok(ZipFile {
631            file_name_raw: Cow::Borrowed(file_name_raw),
632            reader: ZipFileReader::Raw(data.find_content(reader)?),
633            data: Cow::Borrowed(data),
634        })
635    }
636
637    /// Get a contained file by index with options.
638    pub fn by_index_with_options(
639        &mut self,
640        file_number: usize,
641        options: ZipReadOptions<'_>,
642    ) -> ZipResult<ZipFile<'_, R>> {
643        let (file_name_raw, data) = self
644            .shared
645            .files
646            .get_index(file_number)
647            .ok_or(ZipError::FileNotFound)?;
648        let limit_reader = data.find_content(&mut self.reader)?;
649        let reader = data.make_reader(limit_reader, options)?;
650        Ok(ZipFile {
651            file_name_raw: Cow::Borrowed(file_name_raw),
652            data: Cow::Borrowed(data),
653            reader,
654        })
655    }
656
657    /// Find the "root directory" of an archive if it exists, filtering out
658    /// irrelevant entries when searching.
659    ///
660    /// Our definition of a "root directory" is a single top-level directory
661    /// that contains the rest of the archive's entries. This is useful for
662    /// extracting archives that contain a single top-level directory that
663    /// you want to "unwrap" and extract directly.
664    ///
665    /// For a sensible default filter, you can use [`crate::read::root_dir_common_filter`].
666    /// For a custom filter, see [`RootDirFilter`].
667    pub fn root_dir(&self, filter: impl RootDirFilter) -> ZipResult<Option<PathBuf>> {
668        let mut root_dir: Option<PathBuf> = None;
669
670        for i in 0..self.len() {
671            let (file_name_raw, file) = self
672                .shared
673                .files
674                .get_index(i)
675                .ok_or(ZipError::FileNotFound)?;
676            let file_name = match file.name(file_name_raw) {
677                Ok(file_name) => file_name,
678                Err(_) => continue,
679            };
680            let path = match file.enclosed_name(&file_name) {
681                Some(path) => path,
682                None => return Ok(None),
683            };
684
685            if !filter(&path) {
686                continue;
687            }
688
689            macro_rules! replace_root_dir {
690                ($path:ident) => {
691                    match &mut root_dir {
692                        Some(root_dir) => {
693                            if *root_dir != $path {
694                                // We've found multiple root directories,
695                                // abort.
696                                return Ok(None);
697                            }
698                            continue;
699                        }
700
701                        None => {
702                            root_dir = Some($path.into());
703                            continue;
704                        }
705                    }
706                };
707            }
708
709            // If this entry is located at the root of the archive...
710            if path.components().count() == 1 {
711                if file.is_dir(file_name_raw.as_ref()) {
712                    // If it's a directory, it could be the root directory.
713                    replace_root_dir!(path);
714                }
715                // If it's anything else, this archive does not have a
716                // root directory.
717                return Ok(None);
718            }
719
720            // Find the root directory for this entry.
721            let mut path = path.as_path();
722            while let Some(parent) = path.parent().filter(|path| *path != Path::new("")) {
723                path = parent;
724            }
725
726            replace_root_dir!(path);
727        }
728
729        Ok(root_dir)
730    }
731
732    /// Unwrap and return the inner reader object
733    ///
734    /// The position of the reader is undefined.
735    pub fn into_inner(self) -> R {
736        self.reader
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    #[test]
743    fn invalid_offset() {
744        use super::ZipArchive;
745        use std::io::Cursor;
746
747        let reader = ZipArchive::new(Cursor::new(include_bytes!(
748            "../../tests/data/invalid_offset.zip"
749        )));
750        assert!(reader.is_err());
751    }
752
753    #[test]
754    fn invalid_offset2() {
755        use super::ZipArchive;
756        use std::io::Cursor;
757
758        let reader = ZipArchive::new(Cursor::new(include_bytes!(
759            "../../tests/data/invalid_offset2.zip"
760        )));
761        assert!(reader.is_err());
762    }
763
764    #[test]
765    fn zip64_with_leading_junk() {
766        use super::ZipArchive;
767        use std::io::Cursor;
768
769        let reader = ZipArchive::new(Cursor::new(include_bytes!(
770            "../../tests/data/zip64_demo.zip"
771        )))
772        .unwrap();
773        assert_eq!(reader.len(), 1);
774    }
775
776    #[test]
777    fn zip_contents() {
778        use super::ZipArchive;
779        use std::io::Cursor;
780
781        let mut reader =
782            ZipArchive::new(Cursor::new(include_bytes!("../../tests/data/mimetype.zip"))).unwrap();
783        assert_eq!(reader.comment(), b"");
784        assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
785    }
786
787    #[test]
788    fn metadata_entry_matches_by_index() {
789        use super::ZipArchive;
790        use crate::result::ZipError;
791        use std::io::Cursor;
792
793        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
794            "../../tests/data/files_and_dirs.zip"
795        )))
796        .unwrap();
797        let metadata = reader.metadata();
798        assert_eq!(metadata.len(), reader.len());
799        assert!(!metadata.is_empty());
800
801        for i in 0..metadata.len() {
802            let entry = metadata.entry(i).unwrap();
803            let file = reader.by_index(i).unwrap();
804            assert_eq!(entry.name_raw(), file.name_raw());
805            assert_eq!(entry.size(), file.size());
806            assert_eq!(entry.compressed_size(), file.compressed_size());
807            assert_eq!(entry.central_header_start(), file.central_header_start());
808        }
809
810        assert!(matches!(
811            metadata.entry(metadata.len()),
812            Err(ZipError::FileNotFound)
813        ));
814    }
815
816    #[test]
817    fn metadata_empty_archive() {
818        use super::ZipArchive;
819        use crate::result::ZipError;
820        use crate::write::ZipWriter;
821        use std::io::Cursor;
822
823        let writer = ZipWriter::new(Cursor::new(Vec::new()));
824        let reader = ZipArchive::new(writer.finish().unwrap()).unwrap();
825        let metadata = reader.metadata();
826        assert_eq!(metadata.len(), 0);
827        assert!(metadata.is_empty());
828        assert!(matches!(metadata.entry(0), Err(ZipError::FileNotFound)));
829    }
830
831    #[test]
832    fn zip_clone() {
833        use super::ZipArchive;
834        use std::io::Cursor;
835        use std::io::Read;
836
837        let mut reader1 =
838            ZipArchive::new(Cursor::new(include_bytes!("../../tests/data/mimetype.zip"))).unwrap();
839        let mut reader2 = reader1.clone();
840
841        let mut file1 = reader1.by_index(0).unwrap();
842        let mut file2 = reader2.by_index(0).unwrap();
843
844        let t = file1.last_modified().unwrap();
845        assert_eq!(
846            (
847                t.year(),
848                t.month(),
849                t.day(),
850                t.hour(),
851                t.minute(),
852                t.second()
853            ),
854            (1980, 1, 1, 0, 0, 0)
855        );
856
857        let mut buf1 = [0; 5];
858        let mut buf2 = [0; 5];
859        let mut buf3 = [0; 5];
860        let mut buf4 = [0; 5];
861
862        file1.read_exact(&mut buf1).unwrap();
863        file2.read_exact(&mut buf2).unwrap();
864        file1.read_exact(&mut buf3).unwrap();
865        file2.read_exact(&mut buf4).unwrap();
866
867        assert_eq!(buf1, buf2);
868        assert_eq!(buf3, buf4);
869        assert_ne!(buf1, buf3);
870    }
871
872    #[test]
873    fn file_and_dir_predicates() {
874        use super::ZipArchive;
875        use std::io::Cursor;
876
877        let mut zip = ZipArchive::new(Cursor::new(include_bytes!(
878            "../../tests/data/files_and_dirs.zip"
879        )))
880        .unwrap();
881
882        for i in 0..zip.len() {
883            let zip_file = zip.by_index(i).unwrap();
884            let full_name = zip_file.enclosed_name().unwrap();
885            let file_name = full_name.file_name().unwrap().to_str().unwrap();
886            assert!(
887                (file_name.starts_with("dir") && zip_file.is_dir())
888                    || (file_name.starts_with("file") && zip_file.is_file())
889            );
890        }
891    }
892
893    #[test]
894    fn zip64_magic_in_filenames() {
895        use super::ZipArchive;
896        use std::io::Cursor;
897
898        let files = vec![
899            include_bytes!("../../tests/data/zip64_magic_in_filename_1.zip").to_vec(),
900            include_bytes!("../../tests/data/zip64_magic_in_filename_2.zip").to_vec(),
901            include_bytes!("../../tests/data/zip64_magic_in_filename_3.zip").to_vec(),
902            include_bytes!("../../tests/data/zip64_magic_in_filename_4.zip").to_vec(),
903            include_bytes!("../../tests/data/zip64_magic_in_filename_5.zip").to_vec(),
904        ];
905        // Although we don't allow adding files whose names contain the ZIP64 CDB-end or
906        // CDB-end-locator signatures, we still read them when they aren't genuinely ambiguous.
907        for file in files {
908            ZipArchive::new(Cursor::new(file)).unwrap();
909        }
910    }
911
912    /// test case to ensure we don't preemptively over allocate based on the
913    /// declared number of files in the CDE of an invalid zip when the number of
914    /// files declared is more than the alleged offset in the CDE
915    #[test]
916    fn invalid_cde_number_of_files_allocation_smaller_offset() {
917        use super::ZipArchive;
918        use std::io::Cursor;
919
920        let reader = ZipArchive::new(Cursor::new(include_bytes!(
921            "../../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
922        )));
923        assert!(reader.is_err() || reader.unwrap().is_empty());
924    }
925
926    /// test case to ensure we don't preemptively over allocate based on the
927    /// declared number of files in the CDE of an invalid zip when the number of
928    /// files declared is less than the alleged offset in the CDE
929    #[test]
930    fn invalid_cde_number_of_files_allocation_greater_offset() {
931        use super::ZipArchive;
932        use std::io::Cursor;
933
934        let reader = ZipArchive::new(Cursor::new(include_bytes!(
935            "../../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
936        )));
937        assert!(reader.is_err());
938    }
939
940    #[cfg(feature = "deflate64")]
941    #[test]
942    fn deflate64_index_out_of_bounds() {
943        use super::ZipArchive;
944        use std::io::Cursor;
945
946        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
947            "../../tests/data/raw_deflate64_index_out_of_bounds.zip"
948        )))
949        .unwrap();
950        let mut file = reader.by_index(0).unwrap();
951        std::io::copy(&mut file, &mut std::io::sink()).expect_err("Invalid file");
952    }
953
954    #[cfg(feature = "deflate64")]
955    #[test]
956    fn deflate64_not_enough_space() {
957        use super::ZipArchive;
958        use std::io::Cursor;
959
960        ZipArchive::new(Cursor::new(include_bytes!(
961            "../../tests/data/deflate64_issue_25.zip"
962        )))
963        .expect_err("Invalid file");
964    }
965
966    #[cfg(feature = "deflate-flate2")]
967    #[test]
968    fn test_read_with_data_descriptor() {
969        use super::ZipArchive;
970        use std::io::Cursor;
971        use std::io::Read;
972
973        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
974            "../../tests/data/data_descriptor.zip"
975        )))
976        .unwrap();
977        let mut decompressed = [0u8; 16];
978        let mut file = reader.by_index(0).unwrap();
979        assert_eq!(file.read(&mut decompressed).unwrap(), 12);
980    }
981
982    /// Only on little endian because we cannot use fs with miri CI
983    #[cfg(all(target_endian = "little", not(miri)))]
984    #[test]
985    fn test_central_directory_not_at_end() {
986        use super::ZipArchive;
987        use std::io::Cursor;
988        use std::io::Read;
989
990        let mut reader =
991            ZipArchive::new(Cursor::new(include_bytes!("../../tests/data/omni.ja"))).unwrap();
992        let mut file = reader.by_name("chrome.manifest").unwrap();
993        let mut contents = String::new();
994        file.read_to_string(&mut contents).unwrap(); // ensures valid UTF-8
995        assert!(!contents.is_empty(), "chrome.manifest should not be empty");
996        drop(file);
997        for i in 0..reader.len() {
998            let mut file = reader.by_index(i).unwrap();
999            // Attempt to read a small portion or all of each file to ensure it's accessible
1000            let mut buffer = Vec::new();
1001            file.read_to_end(&mut buffer).unwrap();
1002            let file_name = file.name().unwrap();
1003            assert_eq!(
1004                buffer.len(),
1005                file.size() as usize,
1006                "File size mismatch for {}",
1007                file_name
1008            );
1009        }
1010    }
1011
1012    #[test]
1013    fn test_ignore_encryption_flag() {
1014        use super::ZipArchive;
1015        use crate::ZipReadOptions;
1016        use std::io::Cursor;
1017        use std::io::Read;
1018
1019        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
1020            "../../tests/data/ignore_encryption_flag.zip"
1021        )))
1022        .unwrap();
1023
1024        // Get the file entry by ignoring its encryption flag.
1025        let mut file = reader
1026            .by_index_with_options(0, ZipReadOptions::new().ignore_encryption_flag(true))
1027            .unwrap();
1028        let mut contents = String::new();
1029        assert_eq!(file.name().unwrap(), "plaintext.txt");
1030
1031        // The file claims it is encrypted, but it is not.
1032        assert!(file.encrypted());
1033        file.read_to_string(&mut contents).unwrap(); // ensures valid UTF-8
1034        assert_eq!(contents, "This file is not encrypted.\n");
1035    }
1036}