zip/
read.rs

1//! Types for reading ZIP archives
2
3#[cfg(feature = "aes-crypto")]
4use crate::aes::{AesReader, AesReaderValid};
5use crate::cfg_if;
6use crate::compression::{CompressionMethod, Decompressor};
7use crate::cp437::FromCp437;
8use crate::crc32::Crc32Reader;
9use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs};
10use crate::result::{invalid, ZipError, ZipResult};
11use crate::spec::{
12    self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod, ZIP64_BYTES_THR,
13};
14use crate::types::{
15    AesMode, AesVendorVersion, DateTime, SimpleFileOptions, System, ZipCentralEntryBlock,
16    ZipFileData, ZipLocalEntryBlock,
17};
18use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
19use core::mem::{replace, size_of};
20use core::ops::{Deref, Range};
21use indexmap::IndexMap;
22use std::borrow::Cow;
23use std::ffi::OsStr;
24use std::io::{self, copy, sink, Read, Seek, SeekFrom, Write};
25use std::path::{Component, Path, PathBuf};
26use std::sync::{Arc, OnceLock};
27
28mod config;
29
30pub use config::{ArchiveOffset, Config};
31
32/// Provides high level API for reading from a stream.
33pub(crate) mod stream;
34
35pub(crate) mod magic_finder;
36
37/// Immutable metadata about a `ZipArchive`.
38#[derive(Debug)]
39pub struct ZipArchiveMetadata {
40    pub(crate) files: IndexMap<Box<str>, ZipFileData>,
41    pub(crate) offset: u64,
42    pub(crate) dir_start: u64,
43    // This isn't yet used anywhere, but it is here for use cases in the future.
44    #[allow(dead_code)]
45    pub(crate) config: Config,
46    pub(crate) comment: Box<[u8]>,
47    pub(crate) zip64_comment: Option<Box<[u8]>>,
48}
49
50pub(crate) mod zip_archive {
51    use crate::read::ZipArchiveMetadata;
52    use indexmap::IndexMap;
53    use std::sync::Arc;
54
55    #[derive(Debug)]
56    pub(crate) struct SharedBuilder {
57        pub(crate) files: Vec<super::ZipFileData>,
58        pub(super) offset: u64,
59        pub(super) dir_start: u64,
60        // This isn't yet used anywhere, but it is here for use cases in the future.
61        #[allow(dead_code)]
62        pub(super) config: super::Config,
63    }
64
65    impl SharedBuilder {
66        pub fn build(
67            self,
68            comment: Box<[u8]>,
69            zip64_comment: Option<Box<[u8]>>,
70        ) -> ZipArchiveMetadata {
71            let mut index_map = IndexMap::with_capacity(self.files.len());
72            self.files.into_iter().for_each(|file| {
73                index_map.insert(file.file_name.clone(), file);
74            });
75            ZipArchiveMetadata {
76                files: index_map,
77                offset: self.offset,
78                dir_start: self.dir_start,
79                config: self.config,
80                comment,
81                zip64_comment,
82            }
83        }
84    }
85
86    /// ZIP archive reader
87    ///
88    /// At the moment, this type is cheap to clone if this is the case for the
89    /// reader it uses. However, this is not guaranteed by this crate and it may
90    /// change in the future.
91    ///
92    /// ```no_run
93    /// use std::io::{Read, Seek};
94    /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
95    ///     use zip::HasZipMetadata;
96    ///     let mut zip = zip::ZipArchive::new(reader)?;
97    ///
98    ///     for i in 0..zip.len() {
99    ///         let mut file = zip.by_index(i)?;
100    ///         println!("Filename: {}", file.name());
101    ///         std::io::copy(&mut file, &mut std::io::stdout())?;
102    ///     }
103    ///
104    ///     Ok(())
105    /// }
106    /// ```
107    #[derive(Clone, Debug)]
108    pub struct ZipArchive<R> {
109        pub(super) reader: R,
110        pub(super) shared: Arc<ZipArchiveMetadata>,
111    }
112}
113
114#[cfg(feature = "aes-crypto")]
115use crate::aes::PWD_VERIFY_LENGTH;
116use crate::extra_fields::UnicodeExtraField;
117use crate::result::ZipError::InvalidPassword;
118use crate::spec::is_dir;
119use crate::types::ffi::{S_IFLNK, S_IFREG};
120use crate::unstable::{path_to_string, LittleEndianReadExt};
121pub use zip_archive::ZipArchive;
122
123#[allow(clippy::large_enum_variant)]
124pub(crate) enum CryptoReader<'a, R: Read + ?Sized> {
125    Plaintext(io::Take<&'a mut R>),
126    ZipCrypto(ZipCryptoReaderValid<io::Take<&'a mut R>>),
127    #[cfg(feature = "aes-crypto")]
128    Aes {
129        reader: AesReaderValid<io::Take<&'a mut R>>,
130        vendor_version: AesVendorVersion,
131    },
132}
133
134impl<R: Read + ?Sized> Read for CryptoReader<'_, R> {
135    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
136        match self {
137            CryptoReader::Plaintext(r) => r.read(buf),
138            CryptoReader::ZipCrypto(r) => r.read(buf),
139            #[cfg(feature = "aes-crypto")]
140            CryptoReader::Aes { reader: r, .. } => r.read(buf),
141        }
142    }
143
144    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
145        match self {
146            CryptoReader::Plaintext(r) => r.read_to_end(buf),
147            CryptoReader::ZipCrypto(r) => r.read_to_end(buf),
148            #[cfg(feature = "aes-crypto")]
149            CryptoReader::Aes { reader: r, .. } => r.read_to_end(buf),
150        }
151    }
152
153    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
154        match self {
155            CryptoReader::Plaintext(r) => r.read_to_string(buf),
156            CryptoReader::ZipCrypto(r) => r.read_to_string(buf),
157            #[cfg(feature = "aes-crypto")]
158            CryptoReader::Aes { reader: r, .. } => r.read_to_string(buf),
159        }
160    }
161}
162
163impl<'a, R: Read + ?Sized> CryptoReader<'a, R> {
164    /// Consumes this decoder, returning the underlying reader.
165    pub fn into_inner(self) -> io::Take<&'a mut R> {
166        match self {
167            CryptoReader::Plaintext(r) => r,
168            CryptoReader::ZipCrypto(r) => r.into_inner(),
169            #[cfg(feature = "aes-crypto")]
170            CryptoReader::Aes { reader: r, .. } => r.into_inner(),
171        }
172    }
173
174    /// Returns `true` if the data is encrypted using AE2.
175    #[allow(clippy::needless_return)] // can't use cfg_if_expr! because const
176    pub const fn is_ae2_encrypted(&self) -> bool {
177        cfg_if! {
178            if #[cfg(feature = "aes-crypto")] {
179                return matches!(
180                    self,
181                    CryptoReader::Aes {
182                        vendor_version: AesVendorVersion::Ae2,
183                        ..
184                    }
185                );
186            } else {
187                return false;
188            }
189        }
190    }
191}
192
193#[cold]
194fn invalid_state<T>() -> io::Result<T> {
195    Err(io::Error::other("ZipFileReader was in an invalid state"))
196}
197
198pub(crate) enum ZipFileReader<'a, R: Read + ?Sized> {
199    NoReader,
200    Raw(io::Take<&'a mut R>),
201    Compressed(Box<Crc32Reader<Decompressor<io::BufReader<CryptoReader<'a, R>>>>>),
202}
203
204impl<R: Read + ?Sized> Read for ZipFileReader<'_, R> {
205    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
206        match self {
207            ZipFileReader::NoReader => invalid_state(),
208            ZipFileReader::Raw(r) => r.read(buf),
209            ZipFileReader::Compressed(r) => r.read(buf),
210        }
211    }
212
213    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
214        match self {
215            ZipFileReader::NoReader => invalid_state(),
216            ZipFileReader::Raw(r) => r.read_exact(buf),
217            ZipFileReader::Compressed(r) => r.read_exact(buf),
218        }
219    }
220
221    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
222        match self {
223            ZipFileReader::NoReader => invalid_state(),
224            ZipFileReader::Raw(r) => r.read_to_end(buf),
225            ZipFileReader::Compressed(r) => r.read_to_end(buf),
226        }
227    }
228
229    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
230        match self {
231            ZipFileReader::NoReader => invalid_state(),
232            ZipFileReader::Raw(r) => r.read_to_string(buf),
233            ZipFileReader::Compressed(r) => r.read_to_string(buf),
234        }
235    }
236}
237
238impl<'a, R: Read + ?Sized> ZipFileReader<'a, R> {
239    fn into_inner(self) -> io::Result<io::Take<&'a mut R>> {
240        match self {
241            ZipFileReader::NoReader => invalid_state(),
242            ZipFileReader::Raw(r) => Ok(r),
243            ZipFileReader::Compressed(r) => {
244                Ok(r.into_inner().into_inner()?.into_inner().into_inner())
245            }
246        }
247    }
248}
249
250/// A struct for reading a zip file
251pub struct ZipFile<'a, R: Read + ?Sized> {
252    pub(crate) data: Cow<'a, ZipFileData>,
253    pub(crate) reader: ZipFileReader<'a, R>,
254}
255
256/// A struct for reading and seeking a zip file
257pub struct ZipFileSeek<'a, R> {
258    data: Cow<'a, ZipFileData>,
259    reader: ZipFileSeekReader<'a, R>,
260}
261
262enum ZipFileSeekReader<'a, R: ?Sized> {
263    Raw(SeekableTake<'a, R>),
264}
265
266struct SeekableTake<'a, R: ?Sized> {
267    inner: &'a mut R,
268    inner_starting_offset: u64,
269    length: u64,
270    current_offset: u64,
271}
272
273impl<'a, R: Seek + ?Sized> SeekableTake<'a, R> {
274    pub fn new(inner: &'a mut R, length: u64) -> io::Result<Self> {
275        let inner_starting_offset = inner.stream_position()?;
276        Ok(Self {
277            inner,
278            inner_starting_offset,
279            length,
280            current_offset: 0,
281        })
282    }
283}
284
285impl<R: Seek + ?Sized> Seek for SeekableTake<'_, R> {
286    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
287        let offset = match pos {
288            SeekFrom::Start(offset) => Some(offset),
289            SeekFrom::End(offset) => self.length.checked_add_signed(offset),
290            SeekFrom::Current(offset) => self.current_offset.checked_add_signed(offset),
291        };
292        match offset {
293            None => Err(io::Error::new(
294                io::ErrorKind::InvalidInput,
295                "invalid seek to a negative or overflowing position",
296            )),
297            Some(offset) => {
298                let clamped_offset = std::cmp::min(self.length, offset);
299                let new_inner_offset = self
300                    .inner
301                    .seek(SeekFrom::Start(self.inner_starting_offset + clamped_offset))?;
302                self.current_offset = new_inner_offset - self.inner_starting_offset;
303                Ok(self.current_offset)
304            }
305        }
306    }
307}
308
309impl<R: Read + ?Sized> Read for SeekableTake<'_, R> {
310    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
311        let written = self
312            .inner
313            .take(self.length - self.current_offset)
314            .read(buf)?;
315        self.current_offset += written as u64;
316        Ok(written)
317    }
318}
319
320pub(crate) fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
321    use std::fs;
322    fs::create_dir_all(outpath.as_ref())?;
323    #[cfg(unix)]
324    {
325        // Dirs must be writable until all normal files are extracted
326        use std::os::unix::fs::PermissionsExt;
327        std::fs::set_permissions(
328            outpath.as_ref(),
329            std::fs::Permissions::from_mode(
330                0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
331            ),
332        )?;
333    }
334    Ok(())
335}
336
337pub(crate) fn find_content<'a, R: Read + Seek + ?Sized>(
338    data: &ZipFileData,
339    reader: &'a mut R,
340) -> ZipResult<io::Take<&'a mut R>> {
341    // TODO: use .get_or_try_init() once stabilized to provide a closure returning a Result!
342    let data_start = data.data_start(reader)?;
343
344    reader.seek(SeekFrom::Start(data_start))?;
345    Ok(reader.take(data.compressed_size))
346}
347
348fn find_content_seek<'a, R: Read + Seek + ?Sized>(
349    data: &ZipFileData,
350    reader: &'a mut R,
351) -> ZipResult<SeekableTake<'a, R>> {
352    // Parse local header
353    let data_start = data.data_start(reader)?;
354    reader.seek(SeekFrom::Start(data_start))?;
355
356    // Explicit Ok and ? are needed to convert io::Error to ZipError
357    Ok(SeekableTake::new(reader, data.compressed_size)?)
358}
359
360pub(crate) fn find_data_start(
361    data: &ZipFileData,
362    reader: &mut (impl Read + Seek + ?Sized),
363) -> Result<u64, ZipError> {
364    // Go to start of data.
365    reader.seek(SeekFrom::Start(data.header_start))?;
366
367    // Parse static-sized fields and check the magic value.
368    let block = ZipLocalEntryBlock::parse(reader)?;
369
370    // Calculate the end of the local header from the fields we just parsed.
371    let variable_fields_len =
372        // Each of these fields must be converted to u64 before adding, as the result may
373        // easily overflow a u16.
374        block.file_name_length as u64 + block.extra_field_length as u64;
375    let data_start =
376        data.header_start + size_of::<ZipLocalEntryBlock>() as u64 + variable_fields_len;
377
378    // Set the value so we don't have to read it again.
379    match data.data_start.set(data_start) {
380        Ok(()) => (),
381        // If the value was already set in the meantime, ensure it matches (this is probably
382        // unnecessary).
383        Err(_) => {
384            debug_assert_eq!(*data.data_start.get().unwrap(), data_start);
385        }
386    }
387
388    Ok(data_start)
389}
390
391#[allow(clippy::too_many_arguments)]
392pub(crate) fn make_crypto_reader<'a, R: Read + ?Sized>(
393    data: &ZipFileData,
394    reader: io::Take<&'a mut R>,
395    password: Option<&[u8]>,
396    aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
397) -> ZipResult<CryptoReader<'a, R>> {
398    #[allow(deprecated)]
399    {
400        if let CompressionMethod::Unsupported(_) = data.compression_method {
401            return unsupported_zip_error("Compression method not supported");
402        }
403    }
404
405    let reader = match (password, aes_info) {
406        #[cfg(not(feature = "aes-crypto"))]
407        (Some(_), Some(_)) => {
408            return Err(ZipError::UnsupportedArchive(
409                "AES encrypted files cannot be decrypted without the aes-crypto feature.",
410            ))
411        }
412        #[cfg(feature = "aes-crypto")]
413        (Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes {
414            reader: AesReader::new(reader, aes_mode, data.compressed_size).validate(password)?,
415            vendor_version,
416        },
417        (Some(password), None) => {
418            let validator = if data.using_data_descriptor {
419                ZipCryptoValidator::InfoZipMsdosTime(
420                    data.last_modified_time.map_or(0, |x| x.timepart()),
421                )
422            } else {
423                ZipCryptoValidator::PkzipCrc32(data.crc32)
424            };
425            CryptoReader::ZipCrypto(ZipCryptoReader::new(reader, password).validate(validator)?)
426        }
427        (None, Some(_)) => return Err(InvalidPassword),
428        (None, None) => CryptoReader::Plaintext(reader),
429    };
430    Ok(reader)
431}
432
433pub(crate) fn make_reader<'a, R: Read + ?Sized>(
434    compression_method: CompressionMethod,
435    uncompressed_size: u64,
436    crc32: u32,
437    reader: CryptoReader<'a, R>,
438    #[cfg(feature = "legacy-zip")] flags: u16,
439) -> ZipResult<ZipFileReader<'a, R>> {
440    let ae2_encrypted = reader.is_ae2_encrypted();
441    #[cfg(not(feature = "legacy-zip"))]
442    let flags = 0;
443    Ok(ZipFileReader::Compressed(Box::new(Crc32Reader::new(
444        Decompressor::new(
445            io::BufReader::new(reader),
446            compression_method,
447            uncompressed_size,
448            flags,
449        )?,
450        crc32,
451        ae2_encrypted,
452    ))))
453}
454
455pub(crate) fn make_symlink<T>(
456    outpath: &Path,
457    target: &[u8],
458    #[cfg_attr(not(windows), allow(unused))] existing_files: &IndexMap<Box<str>, T>,
459) -> ZipResult<()> {
460    #[cfg_attr(not(any(unix, windows)), allow(unused))]
461    let Ok(target_str) = std::str::from_utf8(target) else {
462        return Err(invalid!("Invalid UTF-8 as symlink target"));
463    };
464
465    cfg_if! {
466        if #[cfg(unix)] {
467            std::os::unix::fs::symlink(Path::new(&target_str), outpath)?;
468        } else if #[cfg(windows)] {
469            let target = Path::new(OsStr::new(&target_str));
470            let target_is_dir_from_archive =
471                existing_files.contains_key(target_str) && is_dir(target_str);
472            let target_is_dir = if target_is_dir_from_archive {
473                true
474            } else if let Ok(meta) = std::fs::metadata(target) {
475                meta.is_dir()
476            } else {
477                false
478            };
479            if target_is_dir {
480                std::os::windows::fs::symlink_dir(target, outpath)?;
481            } else {
482                std::os::windows::fs::symlink_file(target, outpath)?;
483            }
484        } else {
485            use std::fs::File;
486            let output = File::create(outpath);
487            output?.write_all(target)?;
488        }
489    }
490
491    Ok(())
492}
493
494#[derive(Debug)]
495pub(crate) struct CentralDirectoryInfo {
496    pub(crate) archive_offset: u64,
497    pub(crate) directory_start: u64,
498    pub(crate) number_of_files: usize,
499    pub(crate) disk_number: u32,
500    pub(crate) disk_with_central_directory: u32,
501}
502
503impl<'a> TryFrom<&'a CentralDirectoryEndInfo> for CentralDirectoryInfo {
504    type Error = ZipError;
505
506    fn try_from(value: &'a CentralDirectoryEndInfo) -> Result<Self, Self::Error> {
507        let (relative_cd_offset, number_of_files, disk_number, disk_with_central_directory) =
508            match &value.eocd64 {
509                Some(DataAndPosition { data: eocd64, .. }) => {
510                    if eocd64.number_of_files_on_this_disk > eocd64.number_of_files {
511                        return Err(invalid!("ZIP64 footer indicates more files on this disk than in the whole archive"));
512                    }
513                    (
514                        eocd64.central_directory_offset,
515                        eocd64.number_of_files as usize,
516                        eocd64.disk_number,
517                        eocd64.disk_with_central_directory,
518                    )
519                }
520                _ => (
521                    value.eocd.data.central_directory_offset as u64,
522                    value.eocd.data.number_of_files_on_this_disk as usize,
523                    value.eocd.data.disk_number as u32,
524                    value.eocd.data.disk_with_central_directory as u32,
525                ),
526            };
527
528        let directory_start = relative_cd_offset
529            .checked_add(value.archive_offset)
530            .ok_or(invalid!("Invalid central directory size or offset"))?;
531
532        Ok(Self {
533            archive_offset: value.archive_offset,
534            directory_start,
535            number_of_files,
536            disk_number,
537            disk_with_central_directory,
538        })
539    }
540}
541
542/// Store all entries which specify a numeric "mode" which is familiar to POSIX operating systems.
543#[cfg(unix)]
544#[derive(Default, Debug)]
545struct UnixFileModes {
546    map: std::collections::BTreeMap<PathBuf, u32>,
547}
548
549#[cfg(unix)]
550impl UnixFileModes {
551    #[cfg_attr(not(debug_assertions), allow(unused))]
552    pub fn add_mode(&mut self, path: PathBuf, mode: u32) {
553        // We don't print a warning or consider it remotely out of the ordinary to receive two
554        // separate modes for the same path: just take the later one.
555        let old_entry = self.map.insert(path, mode);
556        debug_assert_eq!(old_entry, None);
557    }
558
559    // Child nodes will be sorted later lexicographically, so reversing the order puts them first.
560    pub fn all_perms_with_children_first(
561        self,
562    ) -> impl IntoIterator<Item = (PathBuf, std::fs::Permissions)> {
563        use std::os::unix::fs::PermissionsExt;
564        self.map
565            .into_iter()
566            .rev()
567            .map(|(p, m)| (p, std::fs::Permissions::from_mode(m)))
568    }
569}
570
571impl<R> ZipArchive<R> {
572    pub(crate) fn from_finalized_writer(
573        files: IndexMap<Box<str>, ZipFileData>,
574        comment: Box<[u8]>,
575        zip64_comment: Option<Box<[u8]>>,
576        reader: R,
577        central_start: u64,
578    ) -> ZipResult<Self> {
579        let initial_offset = match files.first() {
580            Some((_, file)) => file.header_start,
581            None => central_start,
582        };
583        let shared = Arc::new(ZipArchiveMetadata {
584            files,
585            offset: initial_offset,
586            dir_start: central_start,
587            config: Config {
588                archive_offset: ArchiveOffset::Known(initial_offset),
589            },
590            comment,
591            zip64_comment,
592        });
593        Ok(Self { reader, shared })
594    }
595
596    /// Total size of the files in the archive, if it can be known. Doesn't include directories or
597    /// metadata.
598    pub fn decompressed_size(&self) -> Option<u128> {
599        let mut total = 0u128;
600        for file in self.shared.files.values() {
601            if file.using_data_descriptor {
602                return None;
603            }
604            total = total.checked_add(file.uncompressed_size as u128)?;
605        }
606        Some(total)
607    }
608}
609
610impl<R: Read + Seek> ZipArchive<R> {
611    pub(crate) fn merge_contents<W: Write + Seek>(
612        &mut self,
613        mut w: W,
614    ) -> ZipResult<IndexMap<Box<str>, ZipFileData>> {
615        if self.shared.files.is_empty() {
616            return Ok(IndexMap::new());
617        }
618        let mut new_files = self.shared.files.clone();
619        /* The first file header will probably start at the beginning of the file, but zip doesn't
620         * enforce that, and executable zips like PEX files will have a shebang line so will
621         * definitely be greater than 0.
622         *
623         * assert_eq!(0, new_files[0].header_start); // Avoid this.
624         */
625
626        let first_new_file_header_start = w.stream_position()?;
627
628        /* Push back file header starts for all entries in the covered files. */
629        new_files.values_mut().try_for_each(|f| {
630            /* This is probably the only really important thing to change. */
631            f.header_start = f
632                .header_start
633                .checked_add(first_new_file_header_start)
634                .ok_or(invalid!(
635                    "new header start from merge would have been too large"
636                ))?;
637            /* This is only ever used internally to cache metadata lookups (it's not part of the
638             * zip spec), and 0 is the sentinel value. */
639            f.central_header_start = 0;
640            /* This is an atomic variable so it can be updated from another thread in the
641             * implementation (which is good!). */
642            if let Some(old_data_start) = f.data_start.take() {
643                let new_data_start = old_data_start
644                    .checked_add(first_new_file_header_start)
645                    .ok_or(invalid!(
646                        "new data start from merge would have been too large"
647                    ))?;
648                f.data_start.get_or_init(|| new_data_start);
649            }
650            Ok::<_, ZipError>(())
651        })?;
652
653        /* Rewind to the beginning of the file.
654         *
655         * NB: we *could* decide to start copying from new_files[0].header_start instead, which
656         * would avoid copying over e.g. any pex shebangs or other file contents that start before
657         * the first zip file entry. However, zip files actually shouldn't care about garbage data
658         * in *between* real entries, since the central directory header records the correct start
659         * location of each, and keeping track of that math is more complicated logic that will only
660         * rarely be used, since most zips that get merged together are likely to be produced
661         * specifically for that purpose (and therefore are unlikely to have a shebang or other
662         * preface). Finally, this preserves any data that might actually be useful.
663         */
664        self.reader.rewind()?;
665        /* Find the end of the file data. */
666        let length_to_read = self.shared.dir_start;
667        /* Produce a Read that reads bytes up until the start of the central directory header.
668         * This "as &mut dyn Read" trick is used elsewhere to avoid having to clone the underlying
669         * handle, which it really shouldn't need to anyway. */
670        let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read);
671        /* Copy over file data from source archive directly. */
672        io::copy(&mut limited_raw, &mut w)?;
673
674        /* Return the files we've just written to the data stream. */
675        Ok(new_files)
676    }
677
678    /// Get the directory start offset and number of files. This is done in a
679    /// separate function to ease the control flow design.
680    pub(crate) fn get_metadata(config: Config, reader: &mut R) -> ZipResult<ZipArchiveMetadata> {
681        // End of the probed region, initially set to the end of the file
682        let file_len = reader.seek(io::SeekFrom::End(0))?;
683        let mut end_exclusive = file_len;
684        let mut last_err = None;
685
686        loop {
687            // Find the EOCD and possibly EOCD64 entries and determine the archive offset.
688            let cde = match spec::find_central_directory(
689                reader,
690                config.archive_offset,
691                end_exclusive,
692                file_len,
693            ) {
694                Ok(cde) => cde,
695                Err(e) => {
696                    // return the previous error first (if there is)
697                    return Err(last_err.unwrap_or(e));
698                }
699            };
700
701            // Turn EOCD into internal representation.
702            match CentralDirectoryInfo::try_from(&cde)
703                .and_then(|info| Self::read_central_header(info, config, reader))
704            {
705                Ok(shared) => {
706                    return Ok(shared.build(
707                        cde.eocd.data.zip_file_comment,
708                        cde.eocd64.map(|v| v.data.extensible_data_sector),
709                    ));
710                }
711                Err(e) => {
712                    last_err = Some(e);
713                }
714            };
715            // Something went wrong while decoding the cde, try to find a new one
716            end_exclusive = cde.eocd.position;
717            continue;
718        }
719    }
720
721    fn read_central_header(
722        dir_info: CentralDirectoryInfo,
723        config: Config,
724        reader: &mut R,
725    ) -> Result<zip_archive::SharedBuilder, ZipError> {
726        // If the parsed number of files is greater than the offset then
727        // something fishy is going on and we shouldn't trust number_of_files.
728        let file_capacity = if dir_info.number_of_files > dir_info.directory_start as usize {
729            0
730        } else {
731            dir_info.number_of_files
732        };
733
734        if dir_info.disk_number != dir_info.disk_with_central_directory {
735            return unsupported_zip_error("Support for multi-disk files is not implemented");
736        }
737
738        if file_capacity.saturating_mul(size_of::<ZipFileData>()) > isize::MAX as usize {
739            return unsupported_zip_error("Oversized central directory");
740        }
741
742        let mut files = Vec::with_capacity(file_capacity);
743        reader.seek(SeekFrom::Start(dir_info.directory_start))?;
744        for _ in 0..dir_info.number_of_files {
745            let file = central_header_to_zip_file(reader, &dir_info)?;
746            files.push(file);
747        }
748
749        Ok(zip_archive::SharedBuilder {
750            files,
751            offset: dir_info.archive_offset,
752            dir_start: dir_info.directory_start,
753            config,
754        })
755    }
756
757    /// Returns the verification value and salt for the AES encryption of the file
758    ///
759    /// It fails if the file number is invalid.
760    ///
761    /// # Returns
762    ///
763    /// - None if the file is not encrypted with AES
764    #[cfg(feature = "aes-crypto")]
765    pub fn get_aes_verification_key_and_salt(
766        &mut self,
767        file_number: usize,
768    ) -> ZipResult<Option<AesInfo>> {
769        let (_, data) = self
770            .shared
771            .files
772            .get_index(file_number)
773            .ok_or(ZipError::FileNotFound)?;
774
775        let limit_reader = find_content(data, &mut self.reader)?;
776        match data.aes_mode {
777            None => Ok(None),
778            Some((aes_mode, _, _)) => {
779                let (verification_value, salt) =
780                    AesReader::new(limit_reader, aes_mode, data.compressed_size)
781                        .get_verification_value_and_salt()?;
782                let aes_info = AesInfo {
783                    aes_mode,
784                    verification_value,
785                    salt,
786                };
787                Ok(Some(aes_info))
788            }
789        }
790    }
791
792    /// Read a ZIP archive, collecting the files it contains.
793    ///
794    /// This uses the central directory record of the ZIP file, and ignores local file headers.
795    ///
796    /// A default [`Config`] is used.
797    pub fn new(reader: R) -> ZipResult<ZipArchive<R>> {
798        Self::with_config(Default::default(), reader)
799    }
800
801    /// Get the metadata associated with the ZIP archive.
802    ///
803    /// This can be used with [`Self::unsafe_new_with_metadata`] to create a new reader over the
804    /// same file without needing to reparse the metadata.
805    pub fn metadata(&self) -> Arc<ZipArchiveMetadata> {
806        self.shared.clone()
807    }
808
809    /// Read a ZIP archive using the given `metadata`.
810    ///
811    /// This is useful for creating multiple readers over the same file without
812    /// needing to reparse the metadata.
813    ///
814    /// # Safety
815    /// `unsafe` is used here to indicate that `reader` and `metadata` could
816    /// potentially be incompatible, and it is left to the user to ensure they are.
817    ///
818    /// # Example
819    ///
820    /// ```no_run
821    /// # use std::fs;
822    /// use rayon::prelude::*;
823    ///
824    /// const FILE_NAME: &str = "my_data.zip";
825    ///
826    /// let file = fs::File::open(FILE_NAME).unwrap();
827    /// let mut archive = zip::ZipArchive::new(file).unwrap();
828    ///
829    /// let file_names = (0..archive.len())
830    ///     .into_par_iter()
831    ///     .map_init({
832    ///         let metadata = archive.metadata().clone();
833    ///         move || {
834    ///             let file = fs::File::open(FILE_NAME).unwrap();
835    ///             unsafe { zip::ZipArchive::unsafe_new_with_metadata(file, metadata.clone()) }
836    ///         }},
837    ///         |archive, i| {
838    ///             let mut file = archive.by_index(i).unwrap();
839    ///             file.enclosed_name()
840    ///         }
841    ///     )
842    ///     .filter_map(|name| name)
843    ///     .collect::<Vec<_>>();
844    /// ```
845    pub unsafe fn unsafe_new_with_metadata(reader: R, metadata: Arc<ZipArchiveMetadata>) -> Self {
846        Self {
847            reader,
848            shared: metadata,
849        }
850    }
851
852    /// Read a ZIP archive providing a read configuration, collecting the files it contains.
853    ///
854    /// This uses the central directory record of the ZIP file, and ignores local file headers.
855    pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
856        let shared = Self::get_metadata(config, &mut reader)?;
857
858        Ok(ZipArchive {
859            reader,
860            shared: shared.into(),
861        })
862    }
863
864    /// Extract a Zip archive into a directory, overwriting files if they
865    /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. Symbolic links are only
866    /// created and followed if the target is within the destination directory (this is checked
867    /// conservatively using [`std::fs::canonicalize`]).
868    ///
869    /// Extraction is not atomic. If an error is encountered, some of the files
870    /// may be left on disk. However, on Unix targets, no newly-created directories with part but
871    /// not all of their contents extracted will be readable, writable or usable as process working
872    /// directories by any non-root user except you.
873    ///
874    /// On Unix and Windows, symbolic links are extracted correctly. On other platforms such as
875    /// WebAssembly, symbolic links aren't supported, so they're extracted as normal files
876    /// containing the target path in UTF-8.
877    pub fn extract<P: AsRef<Path>>(&mut self, directory: P) -> ZipResult<()> {
878        self.extract_internal(directory, None::<fn(&Path) -> bool>)
879    }
880
881    /// Extracts a Zip archive into a directory in the same fashion as
882    /// [`ZipArchive::extract`], but detects a "root" directory in the archive
883    /// (a single top-level directory that contains the rest of the archive's
884    /// entries) and extracts its contents directly.
885    ///
886    /// For a sensible default `filter`, you can use [`root_dir_common_filter`].
887    /// For a custom `filter`, see [`RootDirFilter`].
888    ///
889    /// See [`ZipArchive::root_dir`] for more information on how the root
890    /// directory is detected and the meaning of the `filter` parameter.
891    ///
892    /// ## Example
893    ///
894    /// Imagine a Zip archive with the following structure:
895    ///
896    /// ```text
897    /// root/file1.txt
898    /// root/file2.txt
899    /// root/sub/file3.txt
900    /// root/sub/subsub/file4.txt
901    /// ```
902    ///
903    /// If the archive is extracted to `foo` using [`ZipArchive::extract`],
904    /// the resulting directory structure will be:
905    ///
906    /// ```text
907    /// foo/root/file1.txt
908    /// foo/root/file2.txt
909    /// foo/root/sub/file3.txt
910    /// foo/root/sub/subsub/file4.txt
911    /// ```
912    ///
913    /// If the archive is extracted to `foo` using
914    /// [`ZipArchive::extract_unwrapped_root_dir`], the resulting directory
915    /// structure will be:
916    ///
917    /// ```text
918    /// foo/file1.txt
919    /// foo/file2.txt
920    /// foo/sub/file3.txt
921    /// foo/sub/subsub/file4.txt
922    /// ```
923    ///
924    /// ## Example - No Root Directory
925    ///
926    /// Imagine a Zip archive with the following structure:
927    ///
928    /// ```text
929    /// root/file1.txt
930    /// root/file2.txt
931    /// root/sub/file3.txt
932    /// root/sub/subsub/file4.txt
933    /// other/file5.txt
934    /// ```
935    ///
936    /// Due to the presence of the `other` directory,
937    /// [`ZipArchive::extract_unwrapped_root_dir`] will extract this in the same
938    /// fashion as [`ZipArchive::extract`] as there is now no "root directory."
939    pub fn extract_unwrapped_root_dir<P: AsRef<Path>>(
940        &mut self,
941        directory: P,
942        root_dir_filter: impl RootDirFilter,
943    ) -> ZipResult<()> {
944        self.extract_internal(directory, Some(root_dir_filter))
945    }
946
947    fn extract_internal<P: AsRef<Path>>(
948        &mut self,
949        directory: P,
950        root_dir_filter: Option<impl RootDirFilter>,
951    ) -> ZipResult<()> {
952        use std::fs;
953
954        fs::create_dir_all(&directory)?;
955        let directory = directory.as_ref().canonicalize()?;
956
957        let root_dir = root_dir_filter
958            .and_then(|filter| {
959                self.root_dir(&filter)
960                    .transpose()
961                    .map(|root_dir| root_dir.map(|root_dir| (root_dir, filter)))
962            })
963            .transpose()?;
964
965        // If we have a root dir, simplify the path components to be more
966        // appropriate for passing to `safe_prepare_path`
967        let root_dir = root_dir
968            .as_ref()
969            .map(|(root_dir, filter)| {
970                crate::path::simplified_components(root_dir)
971                    .ok_or_else(|| {
972                        // Should be unreachable
973                        debug_assert!(false, "Invalid root dir path");
974
975                        invalid!("Invalid root dir path")
976                    })
977                    .map(|root_dir| (root_dir, filter))
978            })
979            .transpose()?;
980
981        #[cfg(unix)]
982        let mut files_by_unix_mode = UnixFileModes::default();
983
984        for i in 0..self.len() {
985            let mut file = self.by_index(i)?;
986
987            let mut outpath = directory.clone();
988            /* TODO: the control flow of this method call and subsequent expectations about the
989             *       values in this loop is extremely difficult to follow. It also appears to
990             *       perform a nested loop upon extracting every single file entry? Why does it
991             *       accept two arguments that point to the same directory path, one mutable? */
992            file.safe_prepare_path(directory.as_ref(), &mut outpath, root_dir.as_ref())?;
993
994            #[cfg(any(unix, windows))]
995            if file.is_symlink() {
996                let mut target = Vec::with_capacity(file.size() as usize);
997                file.read_to_end(&mut target)?;
998                drop(file);
999                make_symlink(&outpath, &target, &self.shared.files)?;
1000                continue;
1001            } else if file.is_dir() {
1002                crate::read::make_writable_dir_all(&outpath)?;
1003                continue;
1004            }
1005            let mut outfile = fs::File::create(&outpath)?;
1006            io::copy(&mut file, &mut outfile)?;
1007
1008            // Check for real permissions, which we'll set in a second pass.
1009            #[cfg(unix)]
1010            if let Some(mode) = file.unix_mode() {
1011                files_by_unix_mode.add_mode(outpath, mode);
1012            }
1013
1014            // Set original timestamp.
1015            #[cfg(feature = "chrono")]
1016            if let Some(last_modified) = file.last_modified() {
1017                if let Some(t) = datetime_to_systemtime(&last_modified) {
1018                    outfile.set_modified(t)?;
1019                }
1020            }
1021        }
1022
1023        // Ensure we update children's permissions before making a parent unwritable.
1024        #[cfg(unix)]
1025        for (path, perms) in files_by_unix_mode.all_perms_with_children_first() {
1026            std::fs::set_permissions(path, perms)?;
1027        }
1028
1029        Ok(())
1030    }
1031
1032    /// Number of files contained in this zip.
1033    pub fn len(&self) -> usize {
1034        self.shared.files.len()
1035    }
1036
1037    /// Get the starting offset of the zip central directory.
1038    pub fn central_directory_start(&self) -> u64 {
1039        self.shared.dir_start
1040    }
1041
1042    /// Whether this zip archive contains no files
1043    pub fn is_empty(&self) -> bool {
1044        self.len() == 0
1045    }
1046
1047    /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes.
1048    ///
1049    /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size
1050    /// of that prepended data.
1051    pub fn offset(&self) -> u64 {
1052        self.shared.offset
1053    }
1054
1055    /// Get the comment of the zip archive.
1056    pub fn comment(&self) -> &[u8] {
1057        &self.shared.comment
1058    }
1059
1060    /// Get the ZIP64 comment of the zip archive, if it is ZIP64.
1061    pub fn zip64_comment(&self) -> Option<&[u8]> {
1062        self.shared.zip64_comment.as_deref()
1063    }
1064
1065    /// Returns an iterator over all the file and directory names in this archive.
1066    pub fn file_names(&self) -> impl Iterator<Item = &str> {
1067        self.shared.files.keys().map(|s| s.as_ref())
1068    }
1069
1070    /// Returns Ok(true) if any compressed data in this archive belongs to more than one file. This
1071    /// doesn't make the archive invalid, but some programs will refuse to decompress it because the
1072    /// copies would take up space independently in the destination.
1073    pub fn has_overlapping_files(&mut self) -> ZipResult<bool> {
1074        let mut ranges = Vec::<Range<u64>>::with_capacity(self.shared.files.len());
1075        for file in self.shared.files.values() {
1076            if file.compressed_size == 0 {
1077                continue;
1078            }
1079            let start = file.data_start(&mut self.reader)?;
1080            let end = start + file.compressed_size;
1081            if ranges
1082                .iter()
1083                .any(|range| range.start <= end && start <= range.end)
1084            {
1085                return Ok(true);
1086            }
1087            ranges.push(start..end);
1088        }
1089        Ok(false)
1090    }
1091
1092    /// Search for a file entry by name, decrypt with given password
1093    ///
1094    /// # Warning
1095    ///
1096    /// The implementation of the cryptographic algorithms has not
1097    /// gone through a correctness review, and you should assume it is insecure:
1098    /// passwords used with this API may be compromised.
1099    ///
1100    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
1101    /// to check for a 1/256 chance that the password is correct.
1102    /// There are many passwords out there that will also pass the validity checks
1103    /// we are able to perform. This is a weakness of the ZipCrypto algorithm,
1104    /// due to its fairly primitive approach to cryptography.
1105    pub fn by_name_decrypt(&mut self, name: &str, password: &[u8]) -> ZipResult<ZipFile<'_, R>> {
1106        self.by_name_with_optional_password(name, Some(password))
1107    }
1108
1109    /// Search for a file entry by name
1110    pub fn by_name(&mut self, name: &str) -> ZipResult<ZipFile<'_, R>> {
1111        self.by_name_with_optional_password(name, None)
1112    }
1113
1114    /// Get the index of a file entry by name, if it's present.
1115    #[inline(always)]
1116    pub fn index_for_name(&self, name: &str) -> Option<usize> {
1117        self.shared.files.get_index_of(name)
1118    }
1119
1120    /// Search for a file entry by path, decrypt with given password
1121    ///
1122    /// # Warning
1123    ///
1124    /// The implementation of the cryptographic algorithms has not
1125    /// gone through a correctness review, and you should assume it is insecure:
1126    /// passwords used with this API may be compromised.
1127    ///
1128    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
1129    /// to check for a 1/256 chance that the password is correct.
1130    /// There are many passwords out there that will also pass the validity checks
1131    /// we are able to perform. This is a weakness of the ZipCrypto algorithm,
1132    /// due to its fairly primitive approach to cryptography.
1133    pub fn by_path_decrypt<T: AsRef<Path>>(
1134        &mut self,
1135        path: T,
1136        password: &[u8],
1137    ) -> ZipResult<ZipFile<'_, R>> {
1138        self.index_for_path(path)
1139            .ok_or(ZipError::FileNotFound)
1140            .and_then(|index| {
1141                self.by_index_with_options(index, ZipReadOptions::new().password(Some(password)))
1142            })
1143    }
1144
1145    /// Search for a file entry by path
1146    pub fn by_path<T: AsRef<Path>>(&mut self, path: T) -> ZipResult<ZipFile<'_, R>> {
1147        self.index_for_path(path)
1148            .ok_or(ZipError::FileNotFound)
1149            .and_then(|index| self.by_index_with_options(index, ZipReadOptions::new()))
1150    }
1151
1152    /// Get the index of a file entry by path, if it's present.
1153    #[inline(always)]
1154    pub fn index_for_path<T: AsRef<Path>>(&self, path: T) -> Option<usize> {
1155        self.index_for_name(&path_to_string(path))
1156    }
1157
1158    /// Get the name of a file entry, if it's present.
1159    #[inline(always)]
1160    pub fn name_for_index(&self, index: usize) -> Option<&str> {
1161        self.shared
1162            .files
1163            .get_index(index)
1164            .map(|(name, _)| name.as_ref())
1165    }
1166
1167    /// Search for a file entry by name and return a seekable object.
1168    pub fn by_name_seek(&mut self, name: &str) -> ZipResult<ZipFileSeek<'_, R>> {
1169        self.by_index_seek(self.index_for_name(name).ok_or(ZipError::FileNotFound)?)
1170    }
1171
1172    /// Search for a file entry by index and return a seekable object.
1173    pub fn by_index_seek(&mut self, index: usize) -> ZipResult<ZipFileSeek<'_, R>> {
1174        let reader = &mut self.reader;
1175        self.shared
1176            .files
1177            .get_index(index)
1178            .ok_or(ZipError::FileNotFound)
1179            .and_then(move |(_, data)| {
1180                let seek_reader = match data.compression_method {
1181                    CompressionMethod::Stored => {
1182                        ZipFileSeekReader::Raw(find_content_seek(data, reader)?)
1183                    }
1184                    _ => {
1185                        return Err(ZipError::UnsupportedArchive(
1186                            "Seekable compressed files are not yet supported",
1187                        ))
1188                    }
1189                };
1190                Ok(ZipFileSeek {
1191                    reader: seek_reader,
1192                    data: Cow::Borrowed(data),
1193                })
1194            })
1195    }
1196
1197    fn by_name_with_optional_password<'a>(
1198        &'a mut self,
1199        name: &str,
1200        password: Option<&[u8]>,
1201    ) -> ZipResult<ZipFile<'a, R>> {
1202        let Some(index) = self.shared.files.get_index_of(name) else {
1203            return Err(ZipError::FileNotFound);
1204        };
1205        self.by_index_with_options(index, ZipReadOptions::new().password(password))
1206    }
1207
1208    /// Get a contained file by index, decrypt with given password
1209    ///
1210    /// # Warning
1211    ///
1212    /// The implementation of the cryptographic algorithms has not
1213    /// gone through a correctness review, and you should assume it is insecure:
1214    /// passwords used with this API may be compromised.
1215    ///
1216    /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
1217    /// to check for a 1/256 chance that the password is correct.
1218    /// There are many passwords out there that will also pass the validity checks
1219    /// we are able to perform. This is a weakness of the ZipCrypto algorithm,
1220    /// due to its fairly primitive approach to cryptography.
1221    pub fn by_index_decrypt(
1222        &mut self,
1223        file_number: usize,
1224        password: &[u8],
1225    ) -> ZipResult<ZipFile<'_, R>> {
1226        self.by_index_with_options(file_number, ZipReadOptions::new().password(Some(password)))
1227    }
1228
1229    /// Get a contained file by index
1230    pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_, R>> {
1231        self.by_index_with_options(file_number, ZipReadOptions::new())
1232    }
1233
1234    /// Get a contained file by index without decompressing it
1235    pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_, R>> {
1236        let reader = &mut self.reader;
1237        let (_, data) = self
1238            .shared
1239            .files
1240            .get_index(file_number)
1241            .ok_or(ZipError::FileNotFound)?;
1242        Ok(ZipFile {
1243            reader: ZipFileReader::Raw(find_content(data, reader)?),
1244            data: Cow::Borrowed(data),
1245        })
1246    }
1247
1248    /// Get a contained file by index with options.
1249    pub fn by_index_with_options(
1250        &mut self,
1251        file_number: usize,
1252        mut options: ZipReadOptions<'_>,
1253    ) -> ZipResult<ZipFile<'_, R>> {
1254        let (_, data) = self
1255            .shared
1256            .files
1257            .get_index(file_number)
1258            .ok_or(ZipError::FileNotFound)?;
1259
1260        if options.ignore_encryption_flag {
1261            // Always use no password when we're ignoring the encryption flag.
1262            options.password = None;
1263        } else {
1264            // Require and use the password only if the file is encrypted.
1265            match (options.password, data.encrypted) {
1266                (None, true) => {
1267                    return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED))
1268                }
1269                // Password supplied, but none needed! Discard.
1270                (Some(_), false) => options.password = None,
1271                _ => {}
1272            }
1273        }
1274        let limit_reader = find_content(data, &mut self.reader)?;
1275
1276        let crypto_reader =
1277            make_crypto_reader(data, limit_reader, options.password, data.aes_mode)?;
1278
1279        Ok(ZipFile {
1280            data: Cow::Borrowed(data),
1281            reader: make_reader(
1282                data.compression_method,
1283                data.uncompressed_size,
1284                data.crc32,
1285                crypto_reader,
1286                #[cfg(feature = "legacy-zip")]
1287                data.flags,
1288            )?,
1289        })
1290    }
1291
1292    /// Find the "root directory" of an archive if it exists, filtering out
1293    /// irrelevant entries when searching.
1294    ///
1295    /// Our definition of a "root directory" is a single top-level directory
1296    /// that contains the rest of the archive's entries. This is useful for
1297    /// extracting archives that contain a single top-level directory that
1298    /// you want to "unwrap" and extract directly.
1299    ///
1300    /// For a sensible default filter, you can use [`root_dir_common_filter`].
1301    /// For a custom filter, see [`RootDirFilter`].
1302    pub fn root_dir(&self, filter: impl RootDirFilter) -> ZipResult<Option<PathBuf>> {
1303        let mut root_dir: Option<PathBuf> = None;
1304
1305        for i in 0..self.len() {
1306            let (_, file) = self
1307                .shared
1308                .files
1309                .get_index(i)
1310                .ok_or(ZipError::FileNotFound)?;
1311
1312            let path = match file.enclosed_name() {
1313                Some(path) => path,
1314                None => return Ok(None),
1315            };
1316
1317            if !filter(&path) {
1318                continue;
1319            }
1320
1321            macro_rules! replace_root_dir {
1322                ($path:ident) => {
1323                    match &mut root_dir {
1324                        Some(root_dir) => {
1325                            if *root_dir != $path {
1326                                // We've found multiple root directories,
1327                                // abort.
1328                                return Ok(None);
1329                            } else {
1330                                continue;
1331                            }
1332                        }
1333
1334                        None => {
1335                            root_dir = Some($path.into());
1336                            continue;
1337                        }
1338                    }
1339                };
1340            }
1341
1342            // If this entry is located at the root of the archive...
1343            if path.components().count() == 1 {
1344                if file.is_dir() {
1345                    // If it's a directory, it could be the root directory.
1346                    replace_root_dir!(path);
1347                } else {
1348                    // If it's anything else, this archive does not have a
1349                    // root directory.
1350                    return Ok(None);
1351                }
1352            }
1353
1354            // Find the root directory for this entry.
1355            let mut path = path.as_path();
1356            while let Some(parent) = path.parent().filter(|path| *path != Path::new("")) {
1357                path = parent;
1358            }
1359
1360            replace_root_dir!(path);
1361        }
1362
1363        Ok(root_dir)
1364    }
1365
1366    /// Unwrap and return the inner reader object
1367    ///
1368    /// The position of the reader is undefined.
1369    pub fn into_inner(self) -> R {
1370        self.reader
1371    }
1372}
1373
1374/// Holds the AES information of a file in the zip archive
1375#[derive(Debug)]
1376#[cfg(feature = "aes-crypto")]
1377pub struct AesInfo {
1378    /// The AES encryption mode
1379    pub aes_mode: AesMode,
1380    /// The verification key
1381    pub verification_value: [u8; PWD_VERIFY_LENGTH],
1382    /// The salt
1383    pub salt: Vec<u8>,
1384}
1385
1386const fn unsupported_zip_error<T>(detail: &'static str) -> ZipResult<T> {
1387    Err(ZipError::UnsupportedArchive(detail))
1388}
1389
1390/// Parse a central directory entry to collect the information for the file.
1391pub(crate) fn central_header_to_zip_file<R: Read + Seek>(
1392    reader: &mut R,
1393    central_directory: &CentralDirectoryInfo,
1394) -> ZipResult<ZipFileData> {
1395    let central_header_start = reader.stream_position()?;
1396
1397    // Parse central header
1398    let block = ZipCentralEntryBlock::parse(reader)?;
1399
1400    let file = central_header_to_zip_file_inner(
1401        reader,
1402        central_directory.archive_offset,
1403        central_header_start,
1404        block,
1405    )?;
1406
1407    let central_header_end = reader.stream_position()?;
1408
1409    reader.seek(SeekFrom::Start(central_header_end))?;
1410    Ok(file)
1411}
1412
1413#[inline]
1414fn read_variable_length_byte_field<R: Read>(reader: &mut R, len: usize) -> ZipResult<Box<[u8]>> {
1415    let mut data = vec![0; len].into_boxed_slice();
1416    if let Err(e) = reader.read_exact(&mut data) {
1417        if e.kind() == io::ErrorKind::UnexpectedEof {
1418            return Err(invalid!(
1419                "Variable-length field extends beyond file boundary"
1420            ));
1421        }
1422        return Err(e.into());
1423    }
1424    Ok(data)
1425}
1426
1427/// Parse a central directory entry to collect the information for the file.
1428fn central_header_to_zip_file_inner<R: Read>(
1429    reader: &mut R,
1430    archive_offset: u64,
1431    central_header_start: u64,
1432    block: ZipCentralEntryBlock,
1433) -> ZipResult<ZipFileData> {
1434    let ZipCentralEntryBlock {
1435        // magic,
1436        version_made_by,
1437        // version_to_extract,
1438        flags,
1439        compression_method,
1440        last_mod_time,
1441        last_mod_date,
1442        crc32,
1443        compressed_size,
1444        uncompressed_size,
1445        file_name_length,
1446        extra_field_length,
1447        file_comment_length,
1448        // disk_number,
1449        // internal_file_attributes,
1450        external_file_attributes,
1451        offset,
1452        ..
1453    } = block;
1454
1455    let encrypted = flags & 1 == 1;
1456    let is_utf8 = flags & (1 << 11) != 0;
1457    let using_data_descriptor = flags & (1 << 3) != 0;
1458
1459    let file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?;
1460    let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?;
1461    let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?;
1462    let file_name: Box<str> = match is_utf8 {
1463        true => String::from_utf8_lossy(&file_name_raw).into(),
1464        false => file_name_raw.clone().from_cp437(),
1465    };
1466    let file_comment: Box<str> = match is_utf8 {
1467        true => String::from_utf8_lossy(&file_comment_raw).into(),
1468        false => file_comment_raw.from_cp437(),
1469    };
1470
1471    // Construct the result
1472    let mut result = ZipFileData {
1473        system: System::from((version_made_by >> 8) as u8),
1474        /* NB: this strips the top 8 bits! */
1475        version_made_by: version_made_by as u8,
1476        encrypted,
1477        using_data_descriptor,
1478        is_utf8,
1479        compression_method: CompressionMethod::parse_from_u16(compression_method),
1480        compression_level: None,
1481        last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
1482        crc32,
1483        compressed_size: compressed_size.into(),
1484        uncompressed_size: uncompressed_size.into(),
1485        flags,
1486        file_name,
1487        file_name_raw,
1488        extra_field: Some(Arc::new(extra_field.to_vec())),
1489        central_extra_field: None,
1490        file_comment,
1491        header_start: offset.into(),
1492        extra_data_start: None,
1493        central_header_start,
1494        data_start: OnceLock::new(),
1495        external_attributes: external_file_attributes,
1496        large_file: false,
1497        aes_mode: None,
1498        aes_extra_data_start: 0,
1499        extra_fields: Vec::new(),
1500    };
1501    parse_extra_field(&mut result)?;
1502
1503    let aes_enabled = result.compression_method == CompressionMethod::AES;
1504    if aes_enabled && result.aes_mode.is_none() {
1505        return Err(invalid!("AES encryption without AES extra data field"));
1506    }
1507
1508    // Account for shifted zip offsets.
1509    result.header_start = result
1510        .header_start
1511        .checked_add(archive_offset)
1512        .ok_or(invalid!("Archive header is too large"))?;
1513
1514    Ok(result)
1515}
1516
1517pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
1518    let mut extra_field = file.extra_field.clone();
1519    let mut central_extra_field = file.central_extra_field.clone();
1520    for field_group in [&mut extra_field, &mut central_extra_field] {
1521        let Some(extra_field) = field_group else {
1522            continue;
1523        };
1524        let mut modified = false;
1525        let mut processed_extra_field = vec![];
1526        let len = extra_field.len();
1527        let mut reader = io::Cursor::new(&**extra_field);
1528
1529        let mut position = reader.position();
1530        while position < len as u64 {
1531            let old_position = position;
1532            let remove = parse_single_extra_field(file, &mut reader, position, false)?;
1533            position = reader.position();
1534            if remove {
1535                modified = true;
1536            } else {
1537                let field_len = (position - old_position) as usize;
1538                let write_start = processed_extra_field.len();
1539                reader.seek(SeekFrom::Start(old_position))?;
1540                processed_extra_field.extend_from_slice(&vec![0u8; field_len]);
1541                if let Err(e) = reader
1542                    .read_exact(&mut processed_extra_field[write_start..(write_start + field_len)])
1543                {
1544                    if e.kind() == io::ErrorKind::UnexpectedEof {
1545                        return Err(invalid!("Extra field content exceeds declared length"));
1546                    }
1547                    return Err(e.into());
1548                }
1549            }
1550        }
1551        if modified {
1552            *field_group = Some(Arc::new(processed_extra_field));
1553        }
1554    }
1555    file.extra_field = extra_field;
1556    file.central_extra_field = central_extra_field;
1557    Ok(())
1558}
1559
1560pub(crate) fn parse_single_extra_field<R: Read>(
1561    file: &mut ZipFileData,
1562    reader: &mut R,
1563    bytes_already_read: u64,
1564    disallow_zip64: bool,
1565) -> ZipResult<bool> {
1566    let kind = match reader.read_u16_le() {
1567        Ok(kind) => kind,
1568        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(false),
1569        Err(e) => return Err(e.into()),
1570    };
1571    let len = match reader.read_u16_le() {
1572        Ok(len) => len,
1573        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1574            return Err(invalid!("Extra field header truncated"))
1575        }
1576        Err(e) => return Err(e.into()),
1577    };
1578    match kind {
1579        // Zip64 extended information extra field
1580        0x0001 => {
1581            if disallow_zip64 {
1582                return Err(invalid!("Can't write a custom field using the ZIP64 ID"));
1583            }
1584            file.large_file = true;
1585            let mut consumed_len = 0;
1586            if len >= 24 || file.uncompressed_size == spec::ZIP64_BYTES_THR {
1587                file.uncompressed_size = match reader.read_u64_le() {
1588                    Ok(v) => v,
1589                    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1590                        return Err(invalid!("ZIP64 extra field truncated"))
1591                    }
1592                    Err(e) => return Err(e.into()),
1593                };
1594                consumed_len += size_of::<u64>();
1595            }
1596            if len >= 24 || file.compressed_size == spec::ZIP64_BYTES_THR {
1597                file.compressed_size = match reader.read_u64_le() {
1598                    Ok(v) => v,
1599                    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1600                        return Err(invalid!("ZIP64 extra field truncated"))
1601                    }
1602                    Err(e) => return Err(e.into()),
1603                };
1604                consumed_len += size_of::<u64>();
1605            }
1606            if len >= 24 || file.header_start == spec::ZIP64_BYTES_THR {
1607                file.header_start = match reader.read_u64_le() {
1608                    Ok(v) => v,
1609                    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
1610                        return Err(invalid!("ZIP64 extra field truncated"))
1611                    }
1612                    Err(e) => return Err(e.into()),
1613                };
1614                consumed_len += size_of::<u64>();
1615            }
1616            let Some(leftover_len) = (len as usize).checked_sub(consumed_len) else {
1617                return Err(invalid!("ZIP64 extra-data field is the wrong length"));
1618            };
1619            if let Err(e) = reader.read_exact(&mut vec![0u8; leftover_len]) {
1620                if e.kind() == io::ErrorKind::UnexpectedEof {
1621                    return Err(invalid!("ZIP64 extra field truncated"));
1622                }
1623                return Err(e.into());
1624            }
1625            return Ok(true);
1626        }
1627        0x000a => {
1628            // NTFS extra field
1629            file.extra_fields
1630                .push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?));
1631        }
1632        0x9901 => {
1633            // AES
1634            if len != 7 {
1635                return Err(ZipError::UnsupportedArchive(
1636                    "AES extra data field has an unsupported length",
1637                ));
1638            }
1639            let vendor_version = reader.read_u16_le()?;
1640            let vendor_id = reader.read_u16_le()?;
1641            let mut out = [0u8];
1642            if let Err(e) = reader.read_exact(&mut out) {
1643                if e.kind() == io::ErrorKind::UnexpectedEof {
1644                    return Err(invalid!("AES extra field truncated"));
1645                }
1646                return Err(e.into());
1647            }
1648            let aes_mode = out[0];
1649            let compression_method = CompressionMethod::parse_from_u16(reader.read_u16_le()?);
1650
1651            if vendor_id != 0x4541 {
1652                return Err(invalid!("Invalid AES vendor"));
1653            }
1654            let vendor_version = match vendor_version {
1655                0x0001 => AesVendorVersion::Ae1,
1656                0x0002 => AesVendorVersion::Ae2,
1657                _ => return Err(invalid!("Invalid AES vendor version")),
1658            };
1659            match aes_mode {
1660                0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method)),
1661                0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method)),
1662                0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method)),
1663                _ => return Err(invalid!("Invalid AES encryption strength")),
1664            };
1665            file.compression_method = compression_method;
1666            file.aes_extra_data_start = bytes_already_read;
1667        }
1668        0x5455 => {
1669            // extended timestamp
1670            // https://libzip.org/specifications/extrafld.txt
1671
1672            file.extra_fields.push(ExtraField::ExtendedTimestamp(
1673                ExtendedTimestamp::try_from_reader(reader, len)?,
1674            ));
1675        }
1676        0x6375 => {
1677            // Info-ZIP Unicode Comment Extra Field
1678            // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt
1679            file.file_comment = String::from_utf8(
1680                UnicodeExtraField::try_from_reader(reader, len)?
1681                    .unwrap_valid(file.file_comment.as_bytes())?
1682                    .into_vec(),
1683            )?
1684            .into();
1685        }
1686        0x7075 => {
1687            // Info-ZIP Unicode Path Extra Field
1688            // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt
1689            file.file_name_raw = UnicodeExtraField::try_from_reader(reader, len)?
1690                .unwrap_valid(&file.file_name_raw)?;
1691            file.file_name =
1692                String::from_utf8(file.file_name_raw.clone().into_vec())?.into_boxed_str();
1693            file.is_utf8 = true;
1694        }
1695        _ => {
1696            if let Err(e) = reader.read_exact(&mut vec![0u8; len as usize]) {
1697                if e.kind() == io::ErrorKind::UnexpectedEof {
1698                    return Err(invalid!("Extra field content truncated"));
1699                }
1700                return Err(e.into());
1701            }
1702            // Other fields are ignored
1703        }
1704    }
1705    Ok(false)
1706}
1707
1708/// A trait for exposing file metadata inside the zip.
1709pub trait HasZipMetadata {
1710    /// Get the file metadata
1711    fn get_metadata(&self) -> &ZipFileData;
1712}
1713
1714/// Options for reading a file from an archive.
1715#[derive(Default)]
1716pub struct ZipReadOptions<'a> {
1717    /// The password to use when decrypting the file.  This is ignored if not required.
1718    password: Option<&'a [u8]>,
1719
1720    /// Ignore the value of the encryption flag and proceed as if the file were plaintext.
1721    ignore_encryption_flag: bool,
1722}
1723
1724impl<'a> ZipReadOptions<'a> {
1725    /// Create a new set of options with the default values.
1726    #[must_use]
1727    pub fn new() -> Self {
1728        Self::default()
1729    }
1730
1731    /// Set the password, if any, to use.  Return for chaining.
1732    #[must_use]
1733    pub fn password(mut self, password: Option<&'a [u8]>) -> Self {
1734        self.password = password;
1735        self
1736    }
1737
1738    /// Set the ignore encryption flag.  Return for chaining.
1739    #[must_use]
1740    pub fn ignore_encryption_flag(mut self, ignore: bool) -> Self {
1741        self.ignore_encryption_flag = ignore;
1742        self
1743    }
1744}
1745
1746/// Methods for retrieving information on zip files
1747impl<'a, R: Read + ?Sized> ZipFile<'a, R> {
1748    pub(crate) fn take_raw_reader(&mut self) -> io::Result<io::Take<&'a mut R>> {
1749        replace(&mut self.reader, ZipFileReader::NoReader).into_inner()
1750    }
1751
1752    /// Get the version of the file
1753    pub fn version_made_by(&self) -> (u8, u8) {
1754        (
1755            self.get_metadata().version_made_by / 10,
1756            self.get_metadata().version_made_by % 10,
1757        )
1758    }
1759
1760    /// Get the name of the file
1761    ///
1762    /// # Warnings
1763    ///
1764    /// It is dangerous to use this name directly when extracting an archive.
1765    /// It may contain an absolute path (`/etc/shadow`), or break out of the
1766    /// current directory (`../runtime`). Carelessly writing to these paths
1767    /// allows an attacker to craft a ZIP archive that will overwrite critical
1768    /// files.
1769    ///
1770    /// You can use the [`ZipFile::enclosed_name`] method to validate the name
1771    /// as a safe path.
1772    pub fn name(&self) -> &str {
1773        &self.get_metadata().file_name
1774    }
1775
1776    /// Get the name of the file, in the raw (internal) byte representation.
1777    ///
1778    /// The encoding of this data is currently undefined.
1779    pub fn name_raw(&self) -> &[u8] {
1780        &self.get_metadata().file_name_raw
1781    }
1782
1783    /// Get the name of the file in a sanitized form. It truncates the name to the first NULL byte,
1784    /// removes a leading '/' and removes '..' parts.
1785    #[deprecated(
1786        since = "0.5.7",
1787        note = "by stripping `..`s from the path, the meaning of paths can change.
1788                `mangled_name` can be used if this behaviour is desirable"
1789    )]
1790    pub fn sanitized_name(&self) -> PathBuf {
1791        self.mangled_name()
1792    }
1793
1794    /// Rewrite the path, ignoring any path components with special meaning.
1795    ///
1796    /// - Absolute paths are made relative
1797    /// - [`ParentDir`]s are ignored
1798    /// - Truncates the filename at a NULL byte
1799    ///
1800    /// This is appropriate if you need to be able to extract *something* from
1801    /// any archive, but will easily misrepresent trivial paths like
1802    /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
1803    /// [`ZipFile::enclosed_name`] is the better option in most scenarios.
1804    ///
1805    /// [`ParentDir`]: `Component::ParentDir`
1806    pub fn mangled_name(&self) -> PathBuf {
1807        self.get_metadata().file_name_sanitized()
1808    }
1809
1810    /// Ensure the file path is safe to use as a [`Path`].
1811    ///
1812    /// - It can't contain NULL bytes
1813    /// - It can't resolve to a path outside the current directory
1814    ///   > `foo/../bar` is fine, `foo/../../bar` is not.
1815    /// - It can't be an absolute path
1816    ///
1817    /// This will read well-formed ZIP files correctly, and is resistant
1818    /// to path-based exploits. It is recommended over
1819    /// [`ZipFile::mangled_name`].
1820    pub fn enclosed_name(&self) -> Option<PathBuf> {
1821        self.get_metadata().enclosed_name()
1822    }
1823
1824    pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
1825        self.get_metadata().simplified_components()
1826    }
1827
1828    /// Prepare the path for extraction by creating necessary missing directories and checking for symlinks to be contained within the base path.
1829    ///
1830    /// `base_path` parameter is assumed to be canonicalized.
1831    pub(crate) fn safe_prepare_path(
1832        &self,
1833        base_path: &Path,
1834        outpath: &mut PathBuf,
1835        root_dir: Option<&(Vec<&OsStr>, impl RootDirFilter)>,
1836    ) -> ZipResult<()> {
1837        let components = self
1838            .simplified_components()
1839            .ok_or(invalid!("Invalid file path"))?;
1840
1841        let components = match root_dir {
1842            Some((root_dir, filter)) => match components.strip_prefix(&**root_dir) {
1843                Some(components) => components,
1844
1845                // In this case, we expect that the file was not in the root
1846                // directory, but was filtered out when searching for the
1847                // root directory.
1848                None => {
1849                    // We could technically find ourselves at this code
1850                    // path if the user provides an unstable or
1851                    // non-deterministic `filter` function.
1852                    //
1853                    // If debug assertions are on, we should panic here.
1854                    // Otherwise, the safest thing to do here is to just
1855                    // extract as-is.
1856                    debug_assert!(
1857                        !filter(&PathBuf::from_iter(components.iter())),
1858                        "Root directory filter should not match at this point"
1859                    );
1860
1861                    // Extract as-is.
1862                    &components[..]
1863                }
1864            },
1865
1866            None => &components[..],
1867        };
1868
1869        let components_len = components.len();
1870
1871        for (is_last, component) in components
1872            .iter()
1873            .copied()
1874            .enumerate()
1875            .map(|(i, c)| (i == components_len - 1, c))
1876        {
1877            // we can skip the target directory itself because the base path is assumed to be "trusted" (if the user say extract to a symlink we can follow it)
1878            outpath.push(component);
1879
1880            // check if the path is a symlink, the target must be _inherently_ within the directory
1881            for limit in (0..5u8).rev() {
1882                let meta = match std::fs::symlink_metadata(&outpath) {
1883                    Ok(meta) => meta,
1884                    Err(e) if e.kind() == io::ErrorKind::NotFound => {
1885                        if !is_last {
1886                            crate::read::make_writable_dir_all(&outpath)?;
1887                        }
1888                        break;
1889                    }
1890                    Err(e) => return Err(e.into()),
1891                };
1892
1893                if !meta.is_symlink() {
1894                    break;
1895                }
1896
1897                if limit == 0 {
1898                    return Err(invalid!("Extraction followed a symlink too deep"));
1899                }
1900
1901                // note that we cannot accept links that do not inherently resolve to a path inside the directory to prevent:
1902                // - disclosure of unrelated path exists (no check for a path exist and then ../ out)
1903                // - issues with file-system specific path resolution (case sensitivity, etc)
1904                let target = std::fs::read_link(&outpath)?;
1905
1906                if !crate::path::simplified_components(&target)
1907                    .ok_or(invalid!("Invalid symlink target path"))?
1908                    .starts_with(
1909                        &crate::path::simplified_components(base_path)
1910                            .ok_or(invalid!("Invalid base path"))?,
1911                    )
1912                {
1913                    let is_absolute_enclosed = base_path
1914                        .components()
1915                        .map(Some)
1916                        .chain(std::iter::once(None))
1917                        .zip(target.components().map(Some).chain(std::iter::repeat(None)))
1918                        .all(|(a, b)| match (a, b) {
1919                            // both components are normal
1920                            (Some(Component::Normal(a)), Some(Component::Normal(b))) => a == b,
1921                            // both components consumed fully
1922                            (None, None) => true,
1923                            // target consumed fully but base path is not
1924                            (Some(_), None) => false,
1925                            // base path consumed fully but target is not (and normal)
1926                            (None, Some(Component::CurDir | Component::Normal(_))) => true,
1927                            _ => false,
1928                        });
1929
1930                    if !is_absolute_enclosed {
1931                        return Err(invalid!("Symlink is not inherently safe"));
1932                    }
1933                }
1934
1935                outpath.push(target);
1936            }
1937        }
1938        Ok(())
1939    }
1940
1941    /// Get the comment of the file
1942    pub fn comment(&self) -> &str {
1943        &self.get_metadata().file_comment
1944    }
1945
1946    /// Get the compression method used to store the file
1947    pub fn compression(&self) -> CompressionMethod {
1948        self.get_metadata().compression_method
1949    }
1950
1951    /// Get if the files is encrypted or not
1952    pub fn encrypted(&self) -> bool {
1953        self.data.encrypted
1954    }
1955
1956    /// Get the size of the file, in bytes, in the archive
1957    pub fn compressed_size(&self) -> u64 {
1958        self.get_metadata().compressed_size
1959    }
1960
1961    /// Get the size of the file, in bytes, when uncompressed
1962    pub fn size(&self) -> u64 {
1963        self.get_metadata().uncompressed_size
1964    }
1965
1966    /// Get the time the file was last modified
1967    pub fn last_modified(&self) -> Option<DateTime> {
1968        self.data.last_modified_time
1969    }
1970    /// Returns whether the file is actually a directory
1971    pub fn is_dir(&self) -> bool {
1972        is_dir(self.name())
1973    }
1974
1975    /// Returns whether the file is actually a symbolic link
1976    pub fn is_symlink(&self) -> bool {
1977        self.unix_mode()
1978            .is_some_and(|mode| mode & S_IFLNK == S_IFLNK)
1979    }
1980
1981    /// Returns whether the file is a normal file (i.e. not a directory or symlink)
1982    pub fn is_file(&self) -> bool {
1983        !self.is_dir() && !self.is_symlink()
1984    }
1985
1986    /// Get unix mode for the file
1987    pub fn unix_mode(&self) -> Option<u32> {
1988        self.get_metadata().unix_mode()
1989    }
1990
1991    /// Get the CRC32 hash of the original file
1992    pub fn crc32(&self) -> u32 {
1993        self.get_metadata().crc32
1994    }
1995
1996    /// Get the extra data of the zip header for this file
1997    pub fn extra_data(&self) -> Option<&[u8]> {
1998        self.get_metadata()
1999            .extra_field
2000            .as_ref()
2001            .map(|v| v.deref().deref())
2002    }
2003
2004    /// Get the starting offset of the data of the compressed file
2005    pub fn data_start(&self) -> u64 {
2006        *self.data.data_start.get().unwrap()
2007    }
2008
2009    /// Get the starting offset of the zip header for this file
2010    pub fn header_start(&self) -> u64 {
2011        self.get_metadata().header_start
2012    }
2013    /// Get the starting offset of the zip header in the central directory for this file
2014    pub fn central_header_start(&self) -> u64 {
2015        self.get_metadata().central_header_start
2016    }
2017
2018    /// Get the [`SimpleFileOptions`] that would be used to write this file to
2019    /// a new zip archive.
2020    pub fn options(&self) -> SimpleFileOptions {
2021        let mut options = SimpleFileOptions::default()
2022            .large_file(self.compressed_size().max(self.size()) > ZIP64_BYTES_THR)
2023            .compression_method(self.compression())
2024            .unix_permissions(self.unix_mode().unwrap_or(0o644) | S_IFREG)
2025            .last_modified_time(
2026                self.last_modified()
2027                    .filter(|m| m.is_valid())
2028                    .unwrap_or_else(DateTime::default_for_write),
2029            );
2030
2031        options.normalize();
2032        #[cfg(feature = "aes-crypto")]
2033        if let Some(aes) = self.get_metadata().aes_mode {
2034            // Preserve AES metadata in options for downstream writers.
2035            // This is metadata-only and does not trigger encryption.
2036            options.aes_mode = Some(aes);
2037        }
2038        options
2039    }
2040}
2041
2042/// Methods for retrieving information on zip files
2043impl<R: Read> ZipFile<'_, R> {
2044    /// iterate through all extra fields
2045    pub fn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> {
2046        self.data.extra_fields.iter()
2047    }
2048}
2049
2050impl<R: Read + ?Sized> HasZipMetadata for ZipFile<'_, R> {
2051    fn get_metadata(&self) -> &ZipFileData {
2052        self.data.as_ref()
2053    }
2054}
2055
2056impl<R: Read + ?Sized> Read for ZipFile<'_, R> {
2057    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2058        self.reader.read(buf)
2059    }
2060
2061    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
2062        self.reader.read_exact(buf)
2063    }
2064
2065    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
2066        self.reader.read_to_end(buf)
2067    }
2068
2069    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
2070        self.reader.read_to_string(buf)
2071    }
2072}
2073
2074impl<R: Read> Read for ZipFileSeek<'_, R> {
2075    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2076        match &mut self.reader {
2077            ZipFileSeekReader::Raw(r) => r.read(buf),
2078        }
2079    }
2080}
2081
2082impl<R: Seek> Seek for ZipFileSeek<'_, R> {
2083    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
2084        match &mut self.reader {
2085            ZipFileSeekReader::Raw(r) => r.seek(pos),
2086        }
2087    }
2088}
2089
2090impl<R> HasZipMetadata for ZipFileSeek<'_, R> {
2091    fn get_metadata(&self) -> &ZipFileData {
2092        self.data.as_ref()
2093    }
2094}
2095
2096impl<R: Read + ?Sized> Drop for ZipFile<'_, R> {
2097    fn drop(&mut self) {
2098        // self.data is Owned, this reader is constructed by a streaming reader.
2099        // In this case, we want to exhaust the reader so that the next file is accessible.
2100        if let Cow::Owned(_) = self.data {
2101            // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped.
2102            if let Ok(mut inner) = self.take_raw_reader() {
2103                let _ = copy(&mut inner, &mut sink());
2104            }
2105        }
2106    }
2107}
2108
2109/// Read ZipFile structures from a non-seekable reader.
2110///
2111/// This is an alternative method to read a zip file. If possible, use the ZipArchive functions
2112/// as some information will be missing when reading this manner.
2113///
2114/// Reads a file header from the start of the stream. Will return `Ok(Some(..))` if a file is
2115/// present at the start of the stream. Returns `Ok(None)` if the start of the central directory
2116/// is encountered. No more files should be read after this.
2117///
2118/// The Drop implementation of ZipFile ensures that the reader will be correctly positioned after
2119/// the structure is done.
2120///
2121/// Missing fields are:
2122/// * `comment`: set to an empty string
2123/// * `data_start`: set to 0
2124/// * `external_attributes`: `unix_mode()`: will return None
2125pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<ZipFile<'_, R>>> {
2126    // We can't use the typical ::parse() method, as we follow separate code paths depending on the
2127    // "magic" value (since the magic value will be from the central directory header if we've
2128    // finished iterating over all the actual files).
2129    /* TODO: smallvec? */
2130
2131    let mut block = ZipLocalEntryBlock::zeroed();
2132    reader.read_exact(block.as_bytes_mut())?;
2133
2134    match block.magic().from_le() {
2135        spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
2136        spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
2137        _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR),
2138    }
2139
2140    let block = block.from_le();
2141
2142    let mut result = ZipFileData::from_local_block(block, reader)?;
2143
2144    match parse_extra_field(&mut result) {
2145        Ok(..) | Err(ZipError::Io(..)) => {}
2146        Err(e) => return Err(e),
2147    }
2148
2149    let limit_reader = reader.take(result.compressed_size);
2150    let crypto_reader = make_crypto_reader(&result, limit_reader, None, None)?;
2151    let ZipFileData {
2152        crc32,
2153        uncompressed_size,
2154        compression_method,
2155        #[cfg(feature = "legacy-zip")]
2156        flags,
2157        ..
2158    } = result;
2159
2160    Ok(Some(ZipFile {
2161        data: Cow::Owned(result),
2162        reader: make_reader(
2163            compression_method,
2164            uncompressed_size,
2165            crc32,
2166            crypto_reader,
2167            #[cfg(feature = "legacy-zip")]
2168            flags,
2169        )?,
2170    }))
2171}
2172
2173/// A filter that determines whether an entry should be ignored when searching
2174/// for the root directory of a Zip archive.
2175///
2176/// Returns `true` if the entry should be considered, and `false` if it should
2177/// be ignored.
2178///
2179/// See [`root_dir_common_filter`] for a sensible default filter.
2180pub trait RootDirFilter: Fn(&Path) -> bool {}
2181impl<F: Fn(&Path) -> bool> RootDirFilter for F {}
2182
2183/// Common filters when finding the root directory of a Zip archive.
2184///
2185/// This filter is a sensible default for most use cases and filters out common
2186/// system files that are usually irrelevant to the contents of the archive.
2187///
2188/// Currently, the filter ignores:
2189/// - `/__MACOSX/`
2190/// - `/.DS_Store`
2191/// - `/Thumbs.db`
2192///
2193/// **This function is not guaranteed to be stable and may change in future versions.**
2194///
2195/// # Example
2196///
2197/// ```rust
2198/// # use std::path::Path;
2199/// assert!(zip::read::root_dir_common_filter(Path::new("foo.txt")));
2200/// assert!(!zip::read::root_dir_common_filter(Path::new(".DS_Store")));
2201/// assert!(!zip::read::root_dir_common_filter(Path::new("Thumbs.db")));
2202/// assert!(!zip::read::root_dir_common_filter(Path::new("__MACOSX")));
2203/// assert!(!zip::read::root_dir_common_filter(Path::new("__MACOSX/foo.txt")));
2204/// ```
2205pub fn root_dir_common_filter(path: &Path) -> bool {
2206    const COMMON_FILTER_ROOT_FILES: &[&str] = &[".DS_Store", "Thumbs.db"];
2207
2208    if path.starts_with("__MACOSX") {
2209        return false;
2210    }
2211
2212    if path.components().count() == 1
2213        && path.file_name().is_some_and(|file_name| {
2214            COMMON_FILTER_ROOT_FILES
2215                .iter()
2216                .map(OsStr::new)
2217                .any(|cmp| cmp == file_name)
2218        })
2219    {
2220        return false;
2221    }
2222
2223    true
2224}
2225
2226#[cfg(feature = "chrono")]
2227/// Generate a `SystemTime` from a `DateTime`.
2228fn datetime_to_systemtime(datetime: &DateTime) -> Option<std::time::SystemTime> {
2229    if let Some(t) = generate_chrono_datetime(datetime) {
2230        let time = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(t, chrono::Utc);
2231        return Some(time.into());
2232    }
2233    None
2234}
2235
2236#[cfg(feature = "chrono")]
2237/// Generate a `NaiveDateTime` from a `DateTime`.
2238fn generate_chrono_datetime(datetime: &DateTime) -> Option<chrono::NaiveDateTime> {
2239    if let Some(d) = chrono::NaiveDate::from_ymd_opt(
2240        datetime.year().into(),
2241        datetime.month().into(),
2242        datetime.day().into(),
2243    ) {
2244        if let Some(d) = d.and_hms_opt(
2245            datetime.hour().into(),
2246            datetime.minute().into(),
2247            datetime.second().into(),
2248        ) {
2249            return Some(d);
2250        }
2251    }
2252    None
2253}
2254
2255/// Read ZipFile from a non-seekable reader like [read_zipfile_from_stream] does, but assume the
2256/// given compressed size and don't read any further ahead than that.
2257pub fn read_zipfile_from_stream_with_compressed_size<R: io::Read>(
2258    reader: &mut R,
2259    compressed_size: u64,
2260) -> ZipResult<Option<ZipFile<'_, R>>> {
2261    let mut block = ZipLocalEntryBlock::zeroed();
2262    reader.read_exact(block.as_bytes_mut())?;
2263
2264    match block.magic().from_le() {
2265        spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
2266        spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
2267        _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR),
2268    }
2269
2270    let block = block.from_le();
2271
2272    let mut result = ZipFileData::from_local_block(block, reader)?;
2273    result.compressed_size = compressed_size;
2274
2275    if result.encrypted {
2276        return unsupported_zip_error("Encrypted files are not supported");
2277    }
2278
2279    let limit_reader = reader.take(result.compressed_size);
2280    let crypto_reader = make_crypto_reader(&result, limit_reader, None, None)?;
2281    let ZipFileData {
2282        crc32,
2283        compression_method,
2284        uncompressed_size,
2285        #[cfg(feature = "legacy-zip")]
2286        flags,
2287        ..
2288    } = result;
2289
2290    Ok(Some(ZipFile {
2291        data: Cow::Owned(result),
2292        reader: make_reader(
2293            compression_method,
2294            uncompressed_size,
2295            crc32,
2296            crypto_reader,
2297            #[cfg(feature = "legacy-zip")]
2298            flags,
2299        )?,
2300    }))
2301}
2302
2303#[cfg(test)]
2304mod test {
2305    use crate::read::ZipReadOptions;
2306    use crate::result::ZipResult;
2307    use crate::types::SimpleFileOptions;
2308    use crate::CompressionMethod::Stored;
2309    use crate::{ZipArchive, ZipWriter};
2310    use std::io::{Cursor, Read, Write};
2311    use tempfile::TempDir;
2312
2313    #[test]
2314    fn invalid_offset() {
2315        use super::ZipArchive;
2316
2317        let reader = ZipArchive::new(Cursor::new(include_bytes!(
2318            "../tests/data/invalid_offset.zip"
2319        )));
2320        assert!(reader.is_err());
2321    }
2322
2323    #[test]
2324    fn invalid_offset2() {
2325        use super::ZipArchive;
2326
2327        let reader = ZipArchive::new(Cursor::new(include_bytes!(
2328            "../tests/data/invalid_offset2.zip"
2329        )));
2330        assert!(reader.is_err());
2331    }
2332
2333    #[test]
2334    fn zip64_with_leading_junk() {
2335        use super::ZipArchive;
2336
2337        let reader =
2338            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/zip64_demo.zip"))).unwrap();
2339        assert_eq!(reader.len(), 1);
2340    }
2341
2342    #[test]
2343    fn zip_contents() {
2344        use super::ZipArchive;
2345
2346        let mut reader =
2347            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/mimetype.zip"))).unwrap();
2348        assert_eq!(reader.comment(), b"");
2349        assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
2350    }
2351
2352    #[test]
2353    fn zip_read_streaming() {
2354        use super::read_zipfile_from_stream;
2355
2356        let mut reader = Cursor::new(include_bytes!("../tests/data/mimetype.zip"));
2357        loop {
2358            if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
2359                break;
2360            }
2361        }
2362    }
2363
2364    #[test]
2365    fn zip_clone() {
2366        use super::ZipArchive;
2367        use std::io::Read;
2368
2369        let mut reader1 =
2370            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/mimetype.zip"))).unwrap();
2371        let mut reader2 = reader1.clone();
2372
2373        let mut file1 = reader1.by_index(0).unwrap();
2374        let mut file2 = reader2.by_index(0).unwrap();
2375
2376        let t = file1.last_modified().unwrap();
2377        assert_eq!(
2378            (
2379                t.year(),
2380                t.month(),
2381                t.day(),
2382                t.hour(),
2383                t.minute(),
2384                t.second()
2385            ),
2386            (1980, 1, 1, 0, 0, 0)
2387        );
2388
2389        let mut buf1 = [0; 5];
2390        let mut buf2 = [0; 5];
2391        let mut buf3 = [0; 5];
2392        let mut buf4 = [0; 5];
2393
2394        file1.read_exact(&mut buf1).unwrap();
2395        file2.read_exact(&mut buf2).unwrap();
2396        file1.read_exact(&mut buf3).unwrap();
2397        file2.read_exact(&mut buf4).unwrap();
2398
2399        assert_eq!(buf1, buf2);
2400        assert_eq!(buf3, buf4);
2401        assert_ne!(buf1, buf3);
2402    }
2403
2404    #[test]
2405    fn file_and_dir_predicates() {
2406        use super::ZipArchive;
2407
2408        let mut zip = ZipArchive::new(Cursor::new(include_bytes!(
2409            "../tests/data/files_and_dirs.zip"
2410        )))
2411        .unwrap();
2412
2413        for i in 0..zip.len() {
2414            let zip_file = zip.by_index(i).unwrap();
2415            let full_name = zip_file.enclosed_name().unwrap();
2416            let file_name = full_name.file_name().unwrap().to_str().unwrap();
2417            assert!(
2418                (file_name.starts_with("dir") && zip_file.is_dir())
2419                    || (file_name.starts_with("file") && zip_file.is_file())
2420            );
2421        }
2422    }
2423
2424    #[test]
2425    fn zip64_magic_in_filenames() {
2426        let files = vec![
2427            include_bytes!("../tests/data/zip64_magic_in_filename_1.zip").to_vec(),
2428            include_bytes!("../tests/data/zip64_magic_in_filename_2.zip").to_vec(),
2429            include_bytes!("../tests/data/zip64_magic_in_filename_3.zip").to_vec(),
2430            include_bytes!("../tests/data/zip64_magic_in_filename_4.zip").to_vec(),
2431            include_bytes!("../tests/data/zip64_magic_in_filename_5.zip").to_vec(),
2432        ];
2433        // Although we don't allow adding files whose names contain the ZIP64 CDB-end or
2434        // CDB-end-locator signatures, we still read them when they aren't genuinely ambiguous.
2435        for file in files {
2436            ZipArchive::new(Cursor::new(file)).unwrap();
2437        }
2438    }
2439
2440    /// test case to ensure we don't preemptively over allocate based on the
2441    /// declared number of files in the CDE of an invalid zip when the number of
2442    /// files declared is more than the alleged offset in the CDE
2443    #[test]
2444    fn invalid_cde_number_of_files_allocation_smaller_offset() {
2445        use super::ZipArchive;
2446
2447        let reader = ZipArchive::new(Cursor::new(include_bytes!(
2448            "../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
2449        )));
2450        assert!(reader.is_err() || reader.unwrap().is_empty());
2451    }
2452
2453    /// test case to ensure we don't preemptively over allocate based on the
2454    /// declared number of files in the CDE of an invalid zip when the number of
2455    /// files declared is less than the alleged offset in the CDE
2456    #[test]
2457    fn invalid_cde_number_of_files_allocation_greater_offset() {
2458        use super::ZipArchive;
2459
2460        let reader = ZipArchive::new(Cursor::new(include_bytes!(
2461            "../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
2462        )));
2463        assert!(reader.is_err());
2464    }
2465
2466    #[cfg(feature = "deflate64")]
2467    #[test]
2468    fn deflate64_index_out_of_bounds() -> std::io::Result<()> {
2469        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
2470            "../tests/data/raw_deflate64_index_out_of_bounds.zip"
2471        )))?;
2472        std::io::copy(&mut reader.by_index(0)?, &mut std::io::sink()).expect_err("Invalid file");
2473        Ok(())
2474    }
2475
2476    #[cfg(feature = "deflate64")]
2477    #[test]
2478    fn deflate64_not_enough_space() {
2479        ZipArchive::new(Cursor::new(include_bytes!(
2480            "../tests/data/deflate64_issue_25.zip"
2481        )))
2482        .expect_err("Invalid file");
2483    }
2484
2485    #[cfg(feature = "deflate-flate2")]
2486    #[test]
2487    fn test_read_with_data_descriptor() {
2488        use std::io::Read;
2489
2490        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
2491            "../tests/data/data_descriptor.zip"
2492        )))
2493        .unwrap();
2494        let mut decompressed = [0u8; 16];
2495        let mut file = reader.by_index(0).unwrap();
2496        assert_eq!(file.read(&mut decompressed).unwrap(), 12);
2497    }
2498
2499    #[test]
2500    fn test_is_symlink() -> std::io::Result<()> {
2501        let mut reader = ZipArchive::new(Cursor::new(include_bytes!("../tests/data/symlink.zip")))?;
2502        assert!(reader.by_index(0)?.is_symlink());
2503        let tempdir = TempDir::with_prefix("test_is_symlink")?;
2504        reader.extract(&tempdir)?;
2505        assert!(tempdir.path().join("bar").is_symlink());
2506        Ok(())
2507    }
2508
2509    #[test]
2510    #[cfg(feature = "deflate-flate2")]
2511    fn test_utf8_extra_field() {
2512        let mut reader =
2513            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/chinese.zip"))).unwrap();
2514        reader.by_name("七个房间.txt").unwrap();
2515    }
2516
2517    #[test]
2518    fn test_utf8() {
2519        let mut reader =
2520            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/linux-7z.zip"))).unwrap();
2521        reader.by_name("你好.txt").unwrap();
2522    }
2523
2524    #[test]
2525    fn test_utf8_2() {
2526        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
2527            "../tests/data/windows-7zip.zip"
2528        )))
2529        .unwrap();
2530        reader.by_name("你好.txt").unwrap();
2531    }
2532
2533    #[test]
2534    fn test_64k_files() -> ZipResult<()> {
2535        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
2536        let options = SimpleFileOptions {
2537            compression_method: Stored,
2538            ..Default::default()
2539        };
2540        for i in 0..=u16::MAX {
2541            let file_name = format!("{i}.txt");
2542            writer.start_file(&*file_name, options)?;
2543            writer.write_all(i.to_string().as_bytes())?;
2544        }
2545
2546        let mut reader = ZipArchive::new(writer.finish()?)?;
2547        for i in 0..=u16::MAX {
2548            let expected_name = format!("{i}.txt");
2549            let expected_contents = i.to_string();
2550            let expected_contents = expected_contents.as_bytes();
2551            let mut file = reader.by_name(&expected_name)?;
2552            let mut contents = Vec::with_capacity(expected_contents.len());
2553            file.read_to_end(&mut contents)?;
2554            assert_eq!(contents, expected_contents);
2555            drop(file);
2556            contents.clear();
2557            let mut file = reader.by_index(i as usize)?;
2558            file.read_to_end(&mut contents)?;
2559            assert_eq!(contents, expected_contents);
2560        }
2561        Ok(())
2562    }
2563
2564    /// Symlinks being extracted shouldn't be followed out of the destination directory.
2565    #[test]
2566    fn test_cannot_symlink_outside_destination() -> ZipResult<()> {
2567        use std::fs::create_dir;
2568
2569        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
2570        writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
2571        writer.start_file("symlink/dest-file", SimpleFileOptions::default())?;
2572        let mut reader = writer.finish_into_readable()?;
2573        let dest_parent = TempDir::with_prefix("read__test_cannot_symlink_outside_destination")?;
2574        let dest_sibling = dest_parent.path().join("dest-sibling");
2575        create_dir(&dest_sibling)?;
2576        let dest = dest_parent.path().join("dest");
2577        create_dir(&dest)?;
2578        assert!(reader.extract(dest).is_err());
2579        assert!(!dest_sibling.join("dest-file").exists());
2580        Ok(())
2581    }
2582
2583    #[test]
2584    fn test_can_create_destination() -> ZipResult<()> {
2585        let mut reader =
2586            ZipArchive::new(Cursor::new(include_bytes!("../tests/data/mimetype.zip")))?;
2587        let dest = TempDir::with_prefix("read__test_can_create_destination")?;
2588        reader.extract(&dest)?;
2589        assert!(dest.path().join("mimetype").exists());
2590        Ok(())
2591    }
2592
2593    #[test]
2594    fn test_central_directory_not_at_end() -> ZipResult<()> {
2595        let mut reader = ZipArchive::new(Cursor::new(include_bytes!("../tests/data/omni.ja")))?;
2596        let mut file = reader.by_name("chrome.manifest")?;
2597        let mut contents = String::new();
2598        file.read_to_string(&mut contents)?; // ensures valid UTF-8
2599        assert!(!contents.is_empty(), "chrome.manifest should not be empty");
2600        drop(file);
2601        for i in 0..reader.len() {
2602            let mut file = reader.by_index(i)?;
2603            // Attempt to read a small portion or all of each file to ensure it's accessible
2604            let mut buffer = Vec::new();
2605            file.read_to_end(&mut buffer)?;
2606            assert_eq!(
2607                buffer.len(),
2608                file.size() as usize,
2609                "File size mismatch for {}",
2610                file.name()
2611            );
2612        }
2613        Ok(())
2614    }
2615
2616    #[test]
2617    fn test_ignore_encryption_flag() -> ZipResult<()> {
2618        let mut reader = ZipArchive::new(Cursor::new(include_bytes!(
2619            "../tests/data/ignore_encryption_flag.zip"
2620        )))?;
2621
2622        // Get the file entry by ignoring its encryption flag.
2623        let mut file =
2624            reader.by_index_with_options(0, ZipReadOptions::new().ignore_encryption_flag(true))?;
2625        let mut contents = String::new();
2626        assert_eq!(file.name(), "plaintext.txt");
2627
2628        // The file claims it is encrypted, but it is not.
2629        assert!(file.encrypted());
2630        file.read_to_string(&mut contents)?; // ensures valid UTF-8
2631        assert_eq!(contents, "This file is not encrypted.\n");
2632        Ok(())
2633    }
2634}