1#[cfg(feature = "aes-crypto")]
4use crate::aes::{AesReader, AesReaderValid};
5use crate::compression::{CompressionMethod, Decompressor};
6use crate::cp437::FromCp437;
7use crate::crc32::Crc32Reader;
8use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs};
9use crate::read::zip_archive::{Shared, SharedBuilder};
10use crate::result::invalid;
11use crate::result::{ZipError, ZipResult};
12use crate::spec::{self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod};
13use crate::types::{
14 AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
15 ZipLocalEntryBlock,
16};
17use crate::write::SimpleFileOptions;
18use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
19use crate::ZIP64_BYTES_THR;
20use indexmap::IndexMap;
21use std::borrow::Cow;
22use std::ffi::OsStr;
23use std::fs::create_dir_all;
24use std::io::{self, copy, prelude::*, sink, SeekFrom};
25use std::mem;
26use std::mem::size_of;
27use std::ops::Deref;
28use std::path::{Component, Path, PathBuf};
29use std::sync::{Arc, OnceLock};
30
31mod config;
32
33pub use config::*;
34
35pub(crate) mod stream;
37
38#[cfg(feature = "lzma")]
39pub(crate) mod lzma;
40
41pub(crate) mod magic_finder;
42
43pub(crate) mod zip_archive {
45 use indexmap::IndexMap;
46 use std::sync::Arc;
47
48 #[derive(Debug)]
50 pub(crate) struct Shared {
51 pub(crate) files: IndexMap<Box<str>, super::ZipFileData>,
52 pub(super) offset: u64,
53 pub(super) dir_start: u64,
54 #[allow(dead_code)]
56 pub(super) config: super::Config,
57 pub(crate) comment: Box<[u8]>,
58 pub(crate) zip64_comment: Option<Box<[u8]>>,
59 }
60
61 #[derive(Debug)]
62 pub(crate) struct SharedBuilder {
63 pub(crate) files: Vec<super::ZipFileData>,
64 pub(super) offset: u64,
65 pub(super) dir_start: u64,
66 #[allow(dead_code)]
68 pub(super) config: super::Config,
69 }
70
71 impl SharedBuilder {
72 pub fn build(self, comment: Box<[u8]>, zip64_comment: Option<Box<[u8]>>) -> Shared {
73 let mut index_map = IndexMap::with_capacity(self.files.len());
74 self.files.into_iter().for_each(|file| {
75 index_map.insert(file.file_name.clone(), file);
76 });
77 Shared {
78 files: index_map,
79 offset: self.offset,
80 dir_start: self.dir_start,
81 config: self.config,
82 comment,
83 zip64_comment,
84 }
85 }
86 }
87
88 #[derive(Clone, Debug)]
110 pub struct ZipArchive<R> {
111 pub(super) reader: R,
112 pub(super) shared: Arc<Shared>,
113 }
114}
115
116#[cfg(feature = "aes-crypto")]
117use crate::aes::PWD_VERIFY_LENGTH;
118use crate::extra_fields::UnicodeExtraField;
119use crate::result::ZipError::InvalidPassword;
120use crate::spec::is_dir;
121use crate::types::ffi::{S_IFLNK, S_IFREG};
122use crate::unstable::{path_to_string, LittleEndianReadExt};
123pub use zip_archive::ZipArchive;
124
125#[allow(clippy::large_enum_variant)]
126pub(crate) enum CryptoReader<'a> {
127 Plaintext(io::Take<&'a mut dyn Read>),
128 ZipCrypto(ZipCryptoReaderValid<io::Take<&'a mut dyn Read>>),
129 #[cfg(feature = "aes-crypto")]
130 Aes {
131 reader: AesReaderValid<io::Take<&'a mut dyn Read>>,
132 vendor_version: AesVendorVersion,
133 },
134}
135
136impl Read for CryptoReader<'_> {
137 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
138 match self {
139 CryptoReader::Plaintext(r) => r.read(buf),
140 CryptoReader::ZipCrypto(r) => r.read(buf),
141 #[cfg(feature = "aes-crypto")]
142 CryptoReader::Aes { reader: r, .. } => r.read(buf),
143 }
144 }
145
146 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
147 match self {
148 CryptoReader::Plaintext(r) => r.read_to_end(buf),
149 CryptoReader::ZipCrypto(r) => r.read_to_end(buf),
150 #[cfg(feature = "aes-crypto")]
151 CryptoReader::Aes { reader: r, .. } => r.read_to_end(buf),
152 }
153 }
154
155 fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
156 match self {
157 CryptoReader::Plaintext(r) => r.read_to_string(buf),
158 CryptoReader::ZipCrypto(r) => r.read_to_string(buf),
159 #[cfg(feature = "aes-crypto")]
160 CryptoReader::Aes { reader: r, .. } => r.read_to_string(buf),
161 }
162 }
163}
164
165impl<'a> CryptoReader<'a> {
166 pub fn into_inner(self) -> io::Take<&'a mut dyn Read> {
168 match self {
169 CryptoReader::Plaintext(r) => r,
170 CryptoReader::ZipCrypto(r) => r.into_inner(),
171 #[cfg(feature = "aes-crypto")]
172 CryptoReader::Aes { reader: r, .. } => r.into_inner(),
173 }
174 }
175
176 pub const fn is_ae2_encrypted(&self) -> bool {
178 #[cfg(feature = "aes-crypto")]
179 return matches!(
180 self,
181 CryptoReader::Aes {
182 vendor_version: AesVendorVersion::Ae2,
183 ..
184 }
185 );
186 #[cfg(not(feature = "aes-crypto"))]
187 false
188 }
189}
190
191#[cold]
192fn invalid_state<T>() -> io::Result<T> {
193 Err(io::Error::new(
194 io::ErrorKind::Other,
195 "ZipFileReader was in an invalid state",
196 ))
197}
198
199pub(crate) enum ZipFileReader<'a> {
200 NoReader,
201 Raw(io::Take<&'a mut dyn Read>),
202 Compressed(Box<Crc32Reader<Decompressor<io::BufReader<CryptoReader<'a>>>>>),
203}
204
205impl Read for ZipFileReader<'_> {
206 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
207 match self {
208 ZipFileReader::NoReader => invalid_state(),
209 ZipFileReader::Raw(r) => r.read(buf),
210 ZipFileReader::Compressed(r) => r.read(buf),
211 }
212 }
213
214 fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
215 match self {
216 ZipFileReader::NoReader => invalid_state(),
217 ZipFileReader::Raw(r) => r.read_exact(buf),
218 ZipFileReader::Compressed(r) => r.read_exact(buf),
219 }
220 }
221
222 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
223 match self {
224 ZipFileReader::NoReader => invalid_state(),
225 ZipFileReader::Raw(r) => r.read_to_end(buf),
226 ZipFileReader::Compressed(r) => r.read_to_end(buf),
227 }
228 }
229
230 fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
231 match self {
232 ZipFileReader::NoReader => invalid_state(),
233 ZipFileReader::Raw(r) => r.read_to_string(buf),
234 ZipFileReader::Compressed(r) => r.read_to_string(buf),
235 }
236 }
237}
238
239impl<'a> ZipFileReader<'a> {
240 fn into_inner(self) -> io::Result<io::Take<&'a mut dyn Read>> {
241 match self {
242 ZipFileReader::NoReader => invalid_state(),
243 ZipFileReader::Raw(r) => Ok(r),
244 ZipFileReader::Compressed(r) => {
245 Ok(r.into_inner().into_inner().into_inner().into_inner())
246 }
247 }
248 }
249}
250
251pub struct ZipFile<'a> {
253 pub(crate) data: Cow<'a, ZipFileData>,
254 pub(crate) reader: ZipFileReader<'a>,
255}
256
257pub struct ZipFileSeek<'a, R> {
259 data: Cow<'a, ZipFileData>,
260 reader: ZipFileSeekReader<'a, R>,
261}
262
263enum ZipFileSeekReader<'a, R> {
264 Raw(SeekableTake<'a, R>),
265}
266
267struct SeekableTake<'a, R> {
268 inner: &'a mut R,
269 inner_starting_offset: u64,
270 length: u64,
271 current_offset: u64,
272}
273
274impl<'a, R: Seek> SeekableTake<'a, R> {
275 pub fn new(inner: &'a mut R, length: u64) -> io::Result<Self> {
276 let inner_starting_offset = inner.stream_position()?;
277 Ok(Self {
278 inner,
279 inner_starting_offset,
280 length,
281 current_offset: 0,
282 })
283 }
284}
285
286impl<R: Seek> Seek for SeekableTake<'_, R> {
287 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
288 let offset = match pos {
289 SeekFrom::Start(offset) => Some(offset),
290 SeekFrom::End(offset) => self.length.checked_add_signed(offset),
291 SeekFrom::Current(offset) => self.current_offset.checked_add_signed(offset),
292 };
293 match offset {
294 None => Err(io::Error::new(
295 io::ErrorKind::InvalidInput,
296 "invalid seek to a negative or overflowing position",
297 )),
298 Some(offset) => {
299 let clamped_offset = std::cmp::min(self.length, offset);
300 let new_inner_offset = self
301 .inner
302 .seek(SeekFrom::Start(self.inner_starting_offset + clamped_offset))?;
303 self.current_offset = new_inner_offset - self.inner_starting_offset;
304 Ok(new_inner_offset)
305 }
306 }
307 }
308}
309
310impl<R: Read> Read for SeekableTake<'_, R> {
311 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
312 let written = self
313 .inner
314 .take(self.length - self.current_offset)
315 .read(buf)?;
316 self.current_offset += written as u64;
317 Ok(written)
318 }
319}
320
321pub(crate) fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
322 create_dir_all(outpath.as_ref())?;
323 #[cfg(unix)]
324 {
325 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>(
338 data: &ZipFileData,
339 reader: &'a mut (impl Read + Seek),
340) -> ZipResult<io::Take<&'a mut dyn Read>> {
341 let data_start = match data.data_start.get() {
343 Some(data_start) => *data_start,
344 None => find_data_start(data, reader)?,
345 };
346
347 reader.seek(SeekFrom::Start(data_start))?;
348 Ok((reader as &mut dyn Read).take(data.compressed_size))
349}
350
351fn find_content_seek<'a, R: Read + Seek>(
352 data: &ZipFileData,
353 reader: &'a mut R,
354) -> ZipResult<SeekableTake<'a, R>> {
355 let data_start = find_data_start(data, reader)?;
357 reader.seek(SeekFrom::Start(data_start))?;
358
359 Ok(SeekableTake::new(reader, data.compressed_size)?)
361}
362
363fn find_data_start(
364 data: &ZipFileData,
365 reader: &mut (impl Read + Seek + Sized),
366) -> Result<u64, ZipError> {
367 reader.seek(SeekFrom::Start(data.header_start))?;
369
370 let block = ZipLocalEntryBlock::parse(reader)?;
372
373 let variable_fields_len =
375 block.file_name_length as u64 + block.extra_field_length as u64;
378 let data_start =
379 data.header_start + size_of::<ZipLocalEntryBlock>() as u64 + variable_fields_len;
380
381 match data.data_start.set(data_start) {
383 Ok(()) => (),
384 Err(_) => {
387 debug_assert_eq!(*data.data_start.get().unwrap(), data_start);
388 }
389 }
390
391 Ok(data_start)
392}
393
394#[allow(clippy::too_many_arguments)]
395pub(crate) fn make_crypto_reader<'a>(
396 data: &ZipFileData,
397 reader: io::Take<&'a mut dyn Read>,
398 password: Option<&[u8]>,
399 aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
400) -> ZipResult<CryptoReader<'a>> {
401 #[allow(deprecated)]
402 {
403 if let CompressionMethod::Unsupported(_) = data.compression_method {
404 return unsupported_zip_error("Compression method not supported");
405 }
406 }
407
408 let reader = match (password, aes_info) {
409 #[cfg(not(feature = "aes-crypto"))]
410 (Some(_), Some(_)) => {
411 return Err(ZipError::UnsupportedArchive(
412 "AES encrypted files cannot be decrypted without the aes-crypto feature.",
413 ))
414 }
415 #[cfg(feature = "aes-crypto")]
416 (Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes {
417 reader: AesReader::new(reader, aes_mode, data.compressed_size).validate(password)?,
418 vendor_version,
419 },
420 (Some(password), None) => {
421 let mut last_modified_time = data.last_modified_time;
422 if !data.using_data_descriptor {
423 last_modified_time = None;
424 }
425 let validator = if let Some(last_modified_time) = last_modified_time {
426 ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
427 } else {
428 ZipCryptoValidator::PkzipCrc32(data.crc32)
429 };
430 CryptoReader::ZipCrypto(ZipCryptoReader::new(reader, password).validate(validator)?)
431 }
432 (None, Some(_)) => return Err(InvalidPassword),
433 (None, None) => CryptoReader::Plaintext(reader),
434 };
435 Ok(reader)
436}
437
438pub(crate) fn make_reader(
439 compression_method: CompressionMethod,
440 crc32: u32,
441 reader: CryptoReader,
442) -> ZipResult<ZipFileReader> {
443 let ae2_encrypted = reader.is_ae2_encrypted();
444
445 Ok(ZipFileReader::Compressed(Box::new(Crc32Reader::new(
446 Decompressor::new(io::BufReader::new(reader), compression_method)?,
447 crc32,
448 ae2_encrypted,
449 ))))
450}
451
452pub(crate) fn make_symlink<T>(
453 outpath: &Path,
454 target: &[u8],
455 #[allow(unused)] existing_files: &IndexMap<Box<str>, T>,
456) -> ZipResult<()> {
457 let Ok(target_str) = std::str::from_utf8(target) else {
458 return Err(invalid!("Invalid UTF-8 as symlink target"));
459 };
460
461 #[cfg(not(any(unix, windows)))]
462 {
463 use std::fs::File;
464 let output = File::create(outpath);
465 output?.write_all(target)?;
466 }
467 #[cfg(unix)]
468 {
469 std::os::unix::fs::symlink(Path::new(&target_str), outpath)?;
470 }
471 #[cfg(windows)]
472 {
473 let target = Path::new(OsStr::new(&target_str));
474 let target_is_dir_from_archive =
475 existing_files.contains_key(target_str) && is_dir(target_str);
476 let target_is_dir = if target_is_dir_from_archive {
477 true
478 } else if let Ok(meta) = std::fs::metadata(target) {
479 meta.is_dir()
480 } else {
481 false
482 };
483 if target_is_dir {
484 std::os::windows::fs::symlink_dir(target, outpath)?;
485 } else {
486 std::os::windows::fs::symlink_file(target, outpath)?;
487 }
488 }
489 Ok(())
490}
491
492#[derive(Debug)]
493pub(crate) struct CentralDirectoryInfo {
494 pub(crate) archive_offset: u64,
495 pub(crate) directory_start: u64,
496 pub(crate) number_of_files: usize,
497 pub(crate) disk_number: u32,
498 pub(crate) disk_with_central_directory: u32,
499}
500
501impl<'a> TryFrom<&'a CentralDirectoryEndInfo> for CentralDirectoryInfo {
502 type Error = ZipError;
503
504 fn try_from(value: &'a CentralDirectoryEndInfo) -> Result<Self, Self::Error> {
505 let (relative_cd_offset, number_of_files, disk_number, disk_with_central_directory) =
506 match &value.eocd64 {
507 Some(DataAndPosition { data: eocd64, .. }) => {
508 if eocd64.number_of_files_on_this_disk > eocd64.number_of_files {
509 return Err(invalid!("ZIP64 footer indicates more files on this disk than in the whole archive"));
510 } else if eocd64.version_needed_to_extract > eocd64.version_made_by {
511 return Err(invalid!("ZIP64 footer indicates a new version is needed to extract this archive than the \
512 version that wrote it"));
513 }
514 (
515 eocd64.central_directory_offset,
516 eocd64.number_of_files as usize,
517 eocd64.disk_number,
518 eocd64.disk_with_central_directory,
519 )
520 }
521 _ => (
522 value.eocd.data.central_directory_offset as u64,
523 value.eocd.data.number_of_files_on_this_disk as usize,
524 value.eocd.data.disk_number as u32,
525 value.eocd.data.disk_with_central_directory as u32,
526 ),
527 };
528
529 let directory_start = relative_cd_offset
530 .checked_add(value.archive_offset)
531 .ok_or(invalid!("Invalid central directory size or offset"))?;
532
533 Ok(Self {
534 archive_offset: value.archive_offset,
535 directory_start,
536 number_of_files,
537 disk_number,
538 disk_with_central_directory,
539 })
540 }
541}
542
543impl<R> ZipArchive<R> {
544 pub(crate) fn from_finalized_writer(
545 files: IndexMap<Box<str>, ZipFileData>,
546 comment: Box<[u8]>,
547 zip64_comment: Option<Box<[u8]>>,
548 reader: R,
549 central_start: u64,
550 ) -> ZipResult<Self> {
551 let initial_offset = match files.first() {
552 Some((_, file)) => file.header_start,
553 None => central_start,
554 };
555 let shared = Arc::new(Shared {
556 files,
557 offset: initial_offset,
558 dir_start: central_start,
559 config: Config {
560 archive_offset: ArchiveOffset::Known(initial_offset),
561 },
562 comment,
563 zip64_comment,
564 });
565 Ok(Self { reader, shared })
566 }
567
568 pub fn decompressed_size(&self) -> Option<u128> {
571 let mut total = 0u128;
572 for file in self.shared.files.values() {
573 if file.using_data_descriptor {
574 return None;
575 }
576 total = total.checked_add(file.uncompressed_size as u128)?;
577 }
578 Some(total)
579 }
580}
581
582impl<R: Read + Seek> ZipArchive<R> {
583 pub(crate) fn merge_contents<W: Write + Seek>(
584 &mut self,
585 mut w: W,
586 ) -> ZipResult<IndexMap<Box<str>, ZipFileData>> {
587 if self.shared.files.is_empty() {
588 return Ok(IndexMap::new());
589 }
590 let mut new_files = self.shared.files.clone();
591 let first_new_file_header_start = w.stream_position()?;
599
600 new_files.values_mut().try_for_each(|f| {
602 f.header_start = f
604 .header_start
605 .checked_add(first_new_file_header_start)
606 .ok_or(invalid!(
607 "new header start from merge would have been too large"
608 ))?;
609 f.central_header_start = 0;
612 if let Some(old_data_start) = f.data_start.take() {
615 let new_data_start = old_data_start
616 .checked_add(first_new_file_header_start)
617 .ok_or(invalid!(
618 "new data start from merge would have been too large"
619 ))?;
620 f.data_start.get_or_init(|| new_data_start);
621 }
622 Ok::<_, ZipError>(())
623 })?;
624
625 self.reader.rewind()?;
637 let length_to_read = self.shared.dir_start;
639 let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read);
643 io::copy(&mut limited_raw, &mut w)?;
645
646 Ok(new_files)
648 }
649
650 pub(crate) fn get_metadata(config: Config, reader: &mut R) -> ZipResult<Shared> {
653 let file_len = reader.seek(io::SeekFrom::End(0))?;
655 let mut end_exclusive = file_len;
656
657 loop {
658 let cde = spec::find_central_directory(
660 reader,
661 config.archive_offset,
662 end_exclusive,
663 file_len,
664 )?;
665
666 let Ok(shared) = CentralDirectoryInfo::try_from(&cde)
668 .and_then(|info| Self::read_central_header(info, config, reader))
669 else {
670 end_exclusive = cde.eocd.position;
672 continue;
673 };
674
675 return Ok(shared.build(
676 cde.eocd.data.zip_file_comment,
677 cde.eocd64.map(|v| v.data.extensible_data_sector),
678 ));
679 }
680 }
681
682 fn read_central_header(
683 dir_info: CentralDirectoryInfo,
684 config: Config,
685 reader: &mut R,
686 ) -> Result<SharedBuilder, ZipError> {
687 let file_capacity = if dir_info.number_of_files > dir_info.directory_start as usize {
690 0
691 } else {
692 dir_info.number_of_files
693 };
694
695 if dir_info.disk_number != dir_info.disk_with_central_directory {
696 return unsupported_zip_error("Support for multi-disk files is not implemented");
697 }
698
699 if file_capacity.saturating_mul(size_of::<ZipFileData>()) > isize::MAX as usize {
700 return unsupported_zip_error("Oversized central directory");
701 }
702
703 let mut files = Vec::with_capacity(file_capacity);
704 reader.seek(SeekFrom::Start(dir_info.directory_start))?;
705 for _ in 0..dir_info.number_of_files {
706 let file = central_header_to_zip_file(reader, &dir_info)?;
707 files.push(file);
708 }
709
710 Ok(SharedBuilder {
711 files,
712 offset: dir_info.archive_offset,
713 dir_start: dir_info.directory_start,
714 config,
715 })
716 }
717
718 #[cfg(feature = "aes-crypto")]
726 pub fn get_aes_verification_key_and_salt(
727 &mut self,
728 file_number: usize,
729 ) -> ZipResult<Option<AesInfo>> {
730 let (_, data) = self
731 .shared
732 .files
733 .get_index(file_number)
734 .ok_or(ZipError::FileNotFound)?;
735
736 let limit_reader = find_content(data, &mut self.reader)?;
737 match data.aes_mode {
738 None => Ok(None),
739 Some((aes_mode, _, _)) => {
740 let (verification_value, salt) =
741 AesReader::new(limit_reader, aes_mode, data.compressed_size)
742 .get_verification_value_and_salt()?;
743 let aes_info = AesInfo {
744 aes_mode,
745 verification_value,
746 salt,
747 };
748 Ok(Some(aes_info))
749 }
750 }
751 }
752
753 pub fn new(reader: R) -> ZipResult<ZipArchive<R>> {
759 Self::with_config(Default::default(), reader)
760 }
761
762 pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
766 let shared = Self::get_metadata(config, &mut reader)?;
767
768 Ok(ZipArchive {
769 reader,
770 shared: shared.into(),
771 })
772 }
773
774 pub fn extract<P: AsRef<Path>>(&mut self, directory: P) -> ZipResult<()> {
788 self.extract_internal(directory, None::<fn(&Path) -> bool>)
789 }
790
791 pub fn extract_unwrapped_root_dir<P: AsRef<Path>>(
850 &mut self,
851 directory: P,
852 root_dir_filter: impl RootDirFilter,
853 ) -> ZipResult<()> {
854 self.extract_internal(directory, Some(root_dir_filter))
855 }
856
857 fn extract_internal<P: AsRef<Path>>(
858 &mut self,
859 directory: P,
860 root_dir_filter: Option<impl RootDirFilter>,
861 ) -> ZipResult<()> {
862 use std::fs;
863
864 create_dir_all(&directory)?;
865 let directory = directory.as_ref().canonicalize()?;
866
867 let root_dir = root_dir_filter
868 .and_then(|filter| {
869 self.root_dir(&filter)
870 .transpose()
871 .map(|root_dir| root_dir.map(|root_dir| (root_dir, filter)))
872 })
873 .transpose()?;
874
875 let root_dir = root_dir
878 .as_ref()
879 .map(|(root_dir, filter)| {
880 crate::path::simplified_components(root_dir)
881 .ok_or_else(|| {
882 debug_assert!(false, "Invalid root dir path");
884
885 invalid!("Invalid root dir path")
886 })
887 .map(|root_dir| (root_dir, filter))
888 })
889 .transpose()?;
890
891 #[cfg(unix)]
892 let mut files_by_unix_mode = Vec::new();
893
894 for i in 0..self.len() {
895 let mut file = self.by_index(i)?;
896
897 let mut outpath = directory.clone();
898 file.safe_prepare_path(directory.as_ref(), &mut outpath, root_dir.as_ref())?;
899
900 let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
901 let mut target = Vec::with_capacity(file.size() as usize);
902 file.read_to_end(&mut target)?;
903 Some(target)
904 } else {
905 if file.is_dir() {
906 crate::read::make_writable_dir_all(&outpath)?;
907 continue;
908 }
909 None
910 };
911
912 drop(file);
913
914 if let Some(target) = symlink_target {
915 make_symlink(&outpath, &target, &self.shared.files)?;
916 continue;
917 }
918 let mut file = self.by_index(i)?;
919 let mut outfile = fs::File::create(&outpath)?;
920 io::copy(&mut file, &mut outfile)?;
921 #[cfg(unix)]
922 {
923 if let Some(mode) = file.unix_mode() {
925 files_by_unix_mode.push((outpath.clone(), mode));
926 }
927 }
928 }
929 #[cfg(unix)]
930 {
931 use std::cmp::Reverse;
932 use std::os::unix::fs::PermissionsExt;
933
934 if files_by_unix_mode.len() > 1 {
935 files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone()));
937 }
938 for (path, mode) in files_by_unix_mode.into_iter() {
939 fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
940 }
941 }
942 Ok(())
943 }
944
945 pub fn len(&self) -> usize {
947 self.shared.files.len()
948 }
949
950 pub fn central_directory_start(&self) -> u64 {
952 self.shared.dir_start
953 }
954
955 pub fn is_empty(&self) -> bool {
957 self.len() == 0
958 }
959
960 pub fn offset(&self) -> u64 {
965 self.shared.offset
966 }
967
968 pub fn comment(&self) -> &[u8] {
970 &self.shared.comment
971 }
972
973 pub fn zip64_comment(&self) -> Option<&[u8]> {
975 self.shared.zip64_comment.as_deref()
976 }
977
978 pub fn file_names(&self) -> impl Iterator<Item = &str> {
980 self.shared.files.keys().map(|s| s.as_ref())
981 }
982
983 pub fn by_name_decrypt(&mut self, name: &str, password: &[u8]) -> ZipResult<ZipFile> {
997 self.by_name_with_optional_password(name, Some(password))
998 }
999
1000 pub fn by_name(&mut self, name: &str) -> ZipResult<ZipFile> {
1002 self.by_name_with_optional_password(name, None)
1003 }
1004
1005 #[inline(always)]
1007 pub fn index_for_name(&self, name: &str) -> Option<usize> {
1008 self.shared.files.get_index_of(name)
1009 }
1010
1011 #[inline(always)]
1013 pub fn index_for_path<T: AsRef<Path>>(&self, path: T) -> Option<usize> {
1014 self.index_for_name(&path_to_string(path))
1015 }
1016
1017 #[inline(always)]
1019 pub fn name_for_index(&self, index: usize) -> Option<&str> {
1020 self.shared
1021 .files
1022 .get_index(index)
1023 .map(|(name, _)| name.as_ref())
1024 }
1025
1026 pub fn by_name_seek(&mut self, name: &str) -> ZipResult<ZipFileSeek<R>> {
1028 self.by_index_seek(self.index_for_name(name).ok_or(ZipError::FileNotFound)?)
1029 }
1030
1031 pub fn by_index_seek(&mut self, index: usize) -> ZipResult<ZipFileSeek<R>> {
1033 let reader = &mut self.reader;
1034 self.shared
1035 .files
1036 .get_index(index)
1037 .ok_or(ZipError::FileNotFound)
1038 .and_then(move |(_, data)| {
1039 let seek_reader = match data.compression_method {
1040 CompressionMethod::Stored => {
1041 ZipFileSeekReader::Raw(find_content_seek(data, reader)?)
1042 }
1043 _ => {
1044 return Err(ZipError::UnsupportedArchive(
1045 "Seekable compressed files are not yet supported",
1046 ))
1047 }
1048 };
1049 Ok(ZipFileSeek {
1050 reader: seek_reader,
1051 data: Cow::Borrowed(data),
1052 })
1053 })
1054 }
1055
1056 fn by_name_with_optional_password<'a>(
1057 &'a mut self,
1058 name: &str,
1059 password: Option<&[u8]>,
1060 ) -> ZipResult<ZipFile<'a>> {
1061 let Some(index) = self.shared.files.get_index_of(name) else {
1062 return Err(ZipError::FileNotFound);
1063 };
1064 self.by_index_with_optional_password(index, password)
1065 }
1066
1067 pub fn by_index_decrypt(
1081 &mut self,
1082 file_number: usize,
1083 password: &[u8],
1084 ) -> ZipResult<ZipFile<'_>> {
1085 self.by_index_with_optional_password(file_number, Some(password))
1086 }
1087
1088 pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
1090 self.by_index_with_optional_password(file_number, None)
1091 }
1092
1093 pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
1095 let reader = &mut self.reader;
1096 let (_, data) = self
1097 .shared
1098 .files
1099 .get_index(file_number)
1100 .ok_or(ZipError::FileNotFound)?;
1101 Ok(ZipFile {
1102 reader: ZipFileReader::Raw(find_content(data, reader)?),
1103 data: Cow::Borrowed(data),
1104 })
1105 }
1106
1107 fn by_index_with_optional_password(
1108 &mut self,
1109 file_number: usize,
1110 mut password: Option<&[u8]>,
1111 ) -> ZipResult<ZipFile<'_>> {
1112 let (_, data) = self
1113 .shared
1114 .files
1115 .get_index(file_number)
1116 .ok_or(ZipError::FileNotFound)?;
1117
1118 match (password, data.encrypted) {
1119 (None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)),
1120 (Some(_), false) => password = None, _ => {}
1122 }
1123 let limit_reader = find_content(data, &mut self.reader)?;
1124
1125 let crypto_reader = make_crypto_reader(data, limit_reader, password, data.aes_mode)?;
1126
1127 Ok(ZipFile {
1128 data: Cow::Borrowed(data),
1129 reader: make_reader(data.compression_method, data.crc32, crypto_reader)?,
1130 })
1131 }
1132
1133 pub fn root_dir(&self, filter: impl RootDirFilter) -> ZipResult<Option<PathBuf>> {
1144 let mut root_dir: Option<PathBuf> = None;
1145
1146 for i in 0..self.len() {
1147 let (_, file) = self
1148 .shared
1149 .files
1150 .get_index(i)
1151 .ok_or(ZipError::FileNotFound)?;
1152
1153 let path = match file.enclosed_name() {
1154 Some(path) => path,
1155 None => return Ok(None),
1156 };
1157
1158 if !filter(&path) {
1159 continue;
1160 }
1161
1162 macro_rules! replace_root_dir {
1163 ($path:ident) => {
1164 match &mut root_dir {
1165 Some(root_dir) => {
1166 if *root_dir != $path {
1167 return Ok(None);
1170 } else {
1171 continue;
1172 }
1173 }
1174
1175 None => {
1176 root_dir = Some($path.into());
1177 continue;
1178 }
1179 }
1180 };
1181 }
1182
1183 if path.components().count() == 1 {
1185 if file.is_dir() {
1186 replace_root_dir!(path);
1188 } else {
1189 return Ok(None);
1192 }
1193 }
1194
1195 let mut path = path.as_path();
1197 while let Some(parent) = path.parent().filter(|path| *path != Path::new("")) {
1198 path = parent;
1199 }
1200
1201 replace_root_dir!(path);
1202 }
1203
1204 Ok(root_dir)
1205 }
1206
1207 pub fn into_inner(self) -> R {
1211 self.reader
1212 }
1213}
1214
1215#[derive(Debug)]
1217#[cfg(feature = "aes-crypto")]
1218pub struct AesInfo {
1219 pub aes_mode: AesMode,
1221 pub verification_value: [u8; PWD_VERIFY_LENGTH],
1223 pub salt: Vec<u8>,
1225}
1226
1227const fn unsupported_zip_error<T>(detail: &'static str) -> ZipResult<T> {
1228 Err(ZipError::UnsupportedArchive(detail))
1229}
1230
1231pub(crate) fn central_header_to_zip_file<R: Read + Seek>(
1233 reader: &mut R,
1234 central_directory: &CentralDirectoryInfo,
1235) -> ZipResult<ZipFileData> {
1236 let central_header_start = reader.stream_position()?;
1237
1238 let block = ZipCentralEntryBlock::parse(reader)?;
1240
1241 let file = central_header_to_zip_file_inner(
1242 reader,
1243 central_directory.archive_offset,
1244 central_header_start,
1245 block,
1246 )?;
1247
1248 let central_header_end = reader.stream_position()?;
1249
1250 if file.header_start >= central_directory.directory_start {
1251 return Err(invalid!(
1252 "A local file entry can't start after the central directory"
1253 ));
1254 }
1255
1256 let data_start = find_data_start(&file, reader)?;
1257
1258 if data_start > central_directory.directory_start {
1259 return Err(invalid!(
1260 "File data can't start after the central directory"
1261 ));
1262 }
1263
1264 reader.seek(SeekFrom::Start(central_header_end))?;
1265 Ok(file)
1266}
1267
1268#[inline]
1269fn read_variable_length_byte_field<R: Read>(reader: &mut R, len: usize) -> io::Result<Box<[u8]>> {
1270 let mut data = vec![0; len].into_boxed_slice();
1271 reader.read_exact(&mut data)?;
1272 Ok(data)
1273}
1274
1275fn central_header_to_zip_file_inner<R: Read>(
1277 reader: &mut R,
1278 archive_offset: u64,
1279 central_header_start: u64,
1280 block: ZipCentralEntryBlock,
1281) -> ZipResult<ZipFileData> {
1282 let ZipCentralEntryBlock {
1283 version_made_by,
1285 flags,
1287 compression_method,
1288 last_mod_time,
1289 last_mod_date,
1290 crc32,
1291 compressed_size,
1292 uncompressed_size,
1293 file_name_length,
1294 extra_field_length,
1295 file_comment_length,
1296 external_file_attributes,
1299 offset,
1300 ..
1301 } = block;
1302
1303 let encrypted = flags & 1 == 1;
1304 let is_utf8 = flags & (1 << 11) != 0;
1305 let using_data_descriptor = flags & (1 << 3) != 0;
1306
1307 let file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?;
1308 let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?;
1309 let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?;
1310 let file_name: Box<str> = match is_utf8 {
1311 true => String::from_utf8_lossy(&file_name_raw).into(),
1312 false => file_name_raw.clone().from_cp437(),
1313 };
1314 let file_comment: Box<str> = match is_utf8 {
1315 true => String::from_utf8_lossy(&file_comment_raw).into(),
1316 false => file_comment_raw.from_cp437(),
1317 };
1318
1319 let mut result = ZipFileData {
1321 system: System::from((version_made_by >> 8) as u8),
1322 version_made_by: version_made_by as u8,
1324 encrypted,
1325 using_data_descriptor,
1326 is_utf8,
1327 compression_method: CompressionMethod::parse_from_u16(compression_method),
1328 compression_level: None,
1329 last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
1330 crc32,
1331 compressed_size: compressed_size.into(),
1332 uncompressed_size: uncompressed_size.into(),
1333 file_name,
1334 file_name_raw,
1335 extra_field: Some(Arc::new(extra_field.to_vec())),
1336 central_extra_field: None,
1337 file_comment,
1338 header_start: offset.into(),
1339 extra_data_start: None,
1340 central_header_start,
1341 data_start: OnceLock::new(),
1342 external_attributes: external_file_attributes,
1343 large_file: false,
1344 aes_mode: None,
1345 aes_extra_data_start: 0,
1346 extra_fields: Vec::new(),
1347 };
1348 match parse_extra_field(&mut result) {
1349 Ok(stripped_extra_field) => {
1350 result.extra_field = stripped_extra_field;
1351 }
1352 Err(ZipError::Io(..)) => {}
1353 Err(e) => return Err(e),
1354 }
1355
1356 let aes_enabled = result.compression_method == CompressionMethod::AES;
1357 if aes_enabled && result.aes_mode.is_none() {
1358 return Err(invalid!("AES encryption without AES extra data field"));
1359 }
1360
1361 result.header_start = result
1363 .header_start
1364 .checked_add(archive_offset)
1365 .ok_or(invalid!("Archive header is too large"))?;
1366
1367 Ok(result)
1368}
1369
1370pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<Option<Arc<Vec<u8>>>> {
1371 let Some(ref extra_field) = file.extra_field else {
1372 return Ok(None);
1373 };
1374 let extra_field = extra_field.clone();
1375 let mut processed_extra_field = extra_field.clone();
1376 let len = extra_field.len();
1377 let mut reader = io::Cursor::new(&**extra_field);
1378
1379 let mut position = reader.position() as usize;
1381 while (position) < len {
1382 let old_position = position;
1383 let remove = parse_single_extra_field(file, &mut reader, position as u64, false)?;
1384 position = reader.position() as usize;
1385 if remove {
1386 let remaining = len - (position - old_position);
1387 if remaining == 0 {
1388 return Ok(None);
1389 }
1390 let mut new_extra_field = Vec::with_capacity(remaining);
1391 new_extra_field.extend_from_slice(&extra_field[0..old_position]);
1392 new_extra_field.extend_from_slice(&extra_field[position..]);
1393 processed_extra_field = Arc::new(new_extra_field);
1394 }
1395 }
1396 Ok(Some(processed_extra_field))
1397}
1398
1399pub(crate) fn parse_single_extra_field<R: Read>(
1400 file: &mut ZipFileData,
1401 reader: &mut R,
1402 bytes_already_read: u64,
1403 disallow_zip64: bool,
1404) -> ZipResult<bool> {
1405 let kind = reader.read_u16_le()?;
1406 let len = reader.read_u16_le()?;
1407 match kind {
1408 0x0001 => {
1410 if disallow_zip64 {
1411 return Err(invalid!("Can't write a custom field using the ZIP64 ID"));
1412 }
1413 file.large_file = true;
1414 let mut consumed_len = 0;
1415 if len >= 24 || file.uncompressed_size == spec::ZIP64_BYTES_THR {
1416 file.uncompressed_size = reader.read_u64_le()?;
1417 consumed_len += size_of::<u64>();
1418 }
1419 if len >= 24 || file.compressed_size == spec::ZIP64_BYTES_THR {
1420 file.compressed_size = reader.read_u64_le()?;
1421 consumed_len += size_of::<u64>();
1422 }
1423 if len >= 24 || file.header_start == spec::ZIP64_BYTES_THR {
1424 file.header_start = reader.read_u64_le()?;
1425 consumed_len += size_of::<u64>();
1426 }
1427 let Some(leftover_len) = (len as usize).checked_sub(consumed_len) else {
1428 return Err(invalid!("ZIP64 extra-data field is the wrong length"));
1429 };
1430 reader.read_exact(&mut vec![0u8; leftover_len])?;
1431 return Ok(true);
1432 }
1433 0x000a => {
1434 file.extra_fields
1436 .push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?));
1437 }
1438 0x9901 => {
1439 if len != 7 {
1441 return Err(ZipError::UnsupportedArchive(
1442 "AES extra data field has an unsupported length",
1443 ));
1444 }
1445 let vendor_version = reader.read_u16_le()?;
1446 let vendor_id = reader.read_u16_le()?;
1447 let mut out = [0u8];
1448 reader.read_exact(&mut out)?;
1449 let aes_mode = out[0];
1450 let compression_method = CompressionMethod::parse_from_u16(reader.read_u16_le()?);
1451
1452 if vendor_id != 0x4541 {
1453 return Err(invalid!("Invalid AES vendor"));
1454 }
1455 let vendor_version = match vendor_version {
1456 0x0001 => AesVendorVersion::Ae1,
1457 0x0002 => AesVendorVersion::Ae2,
1458 _ => return Err(invalid!("Invalid AES vendor version")),
1459 };
1460 match aes_mode {
1461 0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method)),
1462 0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method)),
1463 0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method)),
1464 _ => return Err(invalid!("Invalid AES encryption strength")),
1465 };
1466 file.compression_method = compression_method;
1467 file.aes_extra_data_start = bytes_already_read;
1468 }
1469 0x5455 => {
1470 file.extra_fields.push(ExtraField::ExtendedTimestamp(
1474 ExtendedTimestamp::try_from_reader(reader, len)?,
1475 ));
1476 }
1477 0x6375 => {
1478 file.file_comment = String::from_utf8(
1481 UnicodeExtraField::try_from_reader(reader, len)?
1482 .unwrap_valid(file.file_comment.as_bytes())?
1483 .into_vec(),
1484 )?
1485 .into();
1486 }
1487 0x7075 => {
1488 file.file_name_raw = UnicodeExtraField::try_from_reader(reader, len)?
1491 .unwrap_valid(&file.file_name_raw)?;
1492 file.file_name =
1493 String::from_utf8(file.file_name_raw.clone().into_vec())?.into_boxed_str();
1494 file.is_utf8 = true;
1495 }
1496 _ => {
1497 reader.read_exact(&mut vec![0u8; len as usize])?;
1498 }
1500 }
1501 Ok(false)
1502}
1503
1504pub trait HasZipMetadata {
1506 fn get_metadata(&self) -> &ZipFileData;
1508}
1509
1510impl<'a> ZipFile<'a> {
1512 pub(crate) fn take_raw_reader(&mut self) -> io::Result<io::Take<&'a mut dyn Read>> {
1513 mem::replace(&mut self.reader, ZipFileReader::NoReader).into_inner()
1514 }
1515
1516 pub fn version_made_by(&self) -> (u8, u8) {
1518 (
1519 self.get_metadata().version_made_by / 10,
1520 self.get_metadata().version_made_by % 10,
1521 )
1522 }
1523
1524 pub fn name(&self) -> &str {
1537 &self.get_metadata().file_name
1538 }
1539
1540 pub fn name_raw(&self) -> &[u8] {
1544 &self.get_metadata().file_name_raw
1545 }
1546
1547 #[deprecated(
1550 since = "0.5.7",
1551 note = "by stripping `..`s from the path, the meaning of paths can change.
1552 `mangled_name` can be used if this behaviour is desirable"
1553 )]
1554 pub fn sanitized_name(&self) -> PathBuf {
1555 self.mangled_name()
1556 }
1557
1558 pub fn mangled_name(&self) -> PathBuf {
1571 self.get_metadata().file_name_sanitized()
1572 }
1573
1574 pub fn enclosed_name(&self) -> Option<PathBuf> {
1585 self.get_metadata().enclosed_name()
1586 }
1587
1588 pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
1589 self.get_metadata().simplified_components()
1590 }
1591
1592 pub(crate) fn safe_prepare_path(
1596 &self,
1597 base_path: &Path,
1598 outpath: &mut PathBuf,
1599 root_dir: Option<&(Vec<&OsStr>, impl RootDirFilter)>,
1600 ) -> ZipResult<()> {
1601 let components = self
1602 .simplified_components()
1603 .ok_or(invalid!("Invalid file path"))?;
1604
1605 let components = match root_dir {
1606 Some((root_dir, filter)) => match components.strip_prefix(&**root_dir) {
1607 Some(components) => components,
1608
1609 None => {
1613 debug_assert!(
1621 !filter(&PathBuf::from_iter(components.iter())),
1622 "Root directory filter should not match at this point"
1623 );
1624
1625 &components[..]
1627 }
1628 },
1629
1630 None => &components[..],
1631 };
1632
1633 let components_len = components.len();
1634
1635 for (is_last, component) in components
1636 .iter()
1637 .copied()
1638 .enumerate()
1639 .map(|(i, c)| (i == components_len - 1, c))
1640 {
1641 outpath.push(component);
1643
1644 for limit in (0..5u8).rev() {
1646 let meta = match std::fs::symlink_metadata(&outpath) {
1647 Ok(meta) => meta,
1648 Err(e) if e.kind() == io::ErrorKind::NotFound => {
1649 if !is_last {
1650 crate::read::make_writable_dir_all(&outpath)?;
1651 }
1652 break;
1653 }
1654 Err(e) => return Err(e.into()),
1655 };
1656
1657 if !meta.is_symlink() {
1658 break;
1659 }
1660
1661 if limit == 0 {
1662 return Err(invalid!("Extraction followed a symlink too deep"));
1663 }
1664
1665 let target = std::fs::read_link(&outpath)?;
1669
1670 if !crate::path::simplified_components(&target)
1671 .ok_or(invalid!("Invalid symlink target path"))?
1672 .starts_with(
1673 &crate::path::simplified_components(base_path)
1674 .ok_or(invalid!("Invalid base path"))?,
1675 )
1676 {
1677 let is_absolute_enclosed = base_path
1678 .components()
1679 .map(Some)
1680 .chain(std::iter::once(None))
1681 .zip(target.components().map(Some).chain(std::iter::repeat(None)))
1682 .all(|(a, b)| match (a, b) {
1683 (Some(Component::Normal(a)), Some(Component::Normal(b))) => a == b,
1685 (None, None) => true,
1687 (Some(_), None) => false,
1689 (None, Some(Component::CurDir | Component::Normal(_))) => true,
1691 _ => false,
1692 });
1693
1694 if !is_absolute_enclosed {
1695 return Err(invalid!("Symlink is not inherently safe"));
1696 }
1697 }
1698
1699 outpath.push(target);
1700 }
1701 }
1702 Ok(())
1703 }
1704
1705 pub fn comment(&self) -> &str {
1707 &self.get_metadata().file_comment
1708 }
1709
1710 pub fn compression(&self) -> CompressionMethod {
1712 self.get_metadata().compression_method
1713 }
1714
1715 pub fn encrypted(&self) -> bool {
1717 self.data.encrypted
1718 }
1719
1720 pub fn compressed_size(&self) -> u64 {
1722 self.get_metadata().compressed_size
1723 }
1724
1725 pub fn size(&self) -> u64 {
1727 self.get_metadata().uncompressed_size
1728 }
1729
1730 pub fn last_modified(&self) -> Option<DateTime> {
1732 self.data.last_modified_time
1733 }
1734 pub fn is_dir(&self) -> bool {
1736 is_dir(self.name())
1737 }
1738
1739 pub fn is_symlink(&self) -> bool {
1741 self.unix_mode()
1742 .is_some_and(|mode| mode & S_IFLNK == S_IFLNK)
1743 }
1744
1745 pub fn is_file(&self) -> bool {
1747 !self.is_dir() && !self.is_symlink()
1748 }
1749
1750 pub fn unix_mode(&self) -> Option<u32> {
1752 self.get_metadata().unix_mode()
1753 }
1754
1755 pub fn crc32(&self) -> u32 {
1757 self.get_metadata().crc32
1758 }
1759
1760 pub fn extra_data(&self) -> Option<&[u8]> {
1762 self.get_metadata()
1763 .extra_field
1764 .as_ref()
1765 .map(|v| v.deref().deref())
1766 }
1767
1768 pub fn data_start(&self) -> u64 {
1770 *self.data.data_start.get().unwrap()
1771 }
1772
1773 pub fn header_start(&self) -> u64 {
1775 self.get_metadata().header_start
1776 }
1777 pub fn central_header_start(&self) -> u64 {
1779 self.get_metadata().central_header_start
1780 }
1781
1782 pub fn options(&self) -> SimpleFileOptions {
1785 let mut options = SimpleFileOptions::default()
1786 .large_file(self.compressed_size().max(self.size()) > ZIP64_BYTES_THR)
1787 .compression_method(self.compression())
1788 .unix_permissions(self.unix_mode().unwrap_or(0o644) | S_IFREG)
1789 .last_modified_time(
1790 self.last_modified()
1791 .filter(|m| m.is_valid())
1792 .unwrap_or_else(DateTime::default_for_write),
1793 );
1794
1795 options.normalize();
1796 options
1797 }
1798}
1799
1800impl ZipFile<'_> {
1802 pub fn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> {
1804 self.data.extra_fields.iter()
1805 }
1806}
1807
1808impl HasZipMetadata for ZipFile<'_> {
1809 fn get_metadata(&self) -> &ZipFileData {
1810 self.data.as_ref()
1811 }
1812}
1813
1814impl Read for ZipFile<'_> {
1815 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1816 self.reader.read(buf)
1817 }
1818
1819 fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
1820 self.reader.read_exact(buf)
1821 }
1822
1823 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
1824 self.reader.read_to_end(buf)
1825 }
1826
1827 fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
1828 self.reader.read_to_string(buf)
1829 }
1830}
1831
1832impl<R: Read> Read for ZipFileSeek<'_, R> {
1833 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1834 match &mut self.reader {
1835 ZipFileSeekReader::Raw(r) => r.read(buf),
1836 }
1837 }
1838}
1839
1840impl<R: Seek> Seek for ZipFileSeek<'_, R> {
1841 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1842 match &mut self.reader {
1843 ZipFileSeekReader::Raw(r) => r.seek(pos),
1844 }
1845 }
1846}
1847
1848impl<R> HasZipMetadata for ZipFileSeek<'_, R> {
1849 fn get_metadata(&self) -> &ZipFileData {
1850 self.data.as_ref()
1851 }
1852}
1853
1854impl Drop for ZipFile<'_> {
1855 fn drop(&mut self) {
1856 if let Cow::Owned(_) = self.data {
1859 if let Ok(mut inner) = self.take_raw_reader() {
1861 let _ = copy(&mut inner, &mut sink());
1862 }
1863 }
1864 }
1865}
1866
1867pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<ZipFile<'_>>> {
1884 let mut block = ZipLocalEntryBlock::zeroed();
1890 reader.read_exact(block.as_bytes_mut())?;
1891
1892 match block.magic().from_le() {
1893 spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
1894 spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
1895 _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR),
1896 }
1897
1898 let block = block.from_le();
1899
1900 let mut result = ZipFileData::from_local_block(block, reader)?;
1901
1902 match parse_extra_field(&mut result) {
1903 Ok(..) | Err(ZipError::Io(..)) => {}
1904 Err(e) => return Err(e),
1905 }
1906
1907 let limit_reader = (reader as &mut dyn Read).take(result.compressed_size);
1908
1909 let result_crc32 = result.crc32;
1910 let result_compression_method = result.compression_method;
1911 let crypto_reader = make_crypto_reader(&result, limit_reader, None, None)?;
1912
1913 Ok(Some(ZipFile {
1914 data: Cow::Owned(result),
1915 reader: make_reader(result_compression_method, result_crc32, crypto_reader)?,
1916 }))
1917}
1918
1919pub trait RootDirFilter: Fn(&Path) -> bool {}
1927impl<F: Fn(&Path) -> bool> RootDirFilter for F {}
1928
1929pub fn root_dir_common_filter(path: &Path) -> bool {
1952 const COMMON_FILTER_ROOT_FILES: &[&str] = &[".DS_Store", "Thumbs.db"];
1953
1954 if path.starts_with("__MACOSX") {
1955 return false;
1956 }
1957
1958 if path.components().count() == 1
1959 && path.file_name().is_some_and(|file_name| {
1960 COMMON_FILTER_ROOT_FILES
1961 .iter()
1962 .map(OsStr::new)
1963 .any(|cmp| cmp == file_name)
1964 })
1965 {
1966 return false;
1967 }
1968
1969 true
1970}
1971
1972#[cfg(test)]
1973mod test {
1974 use crate::result::ZipResult;
1975 use crate::write::SimpleFileOptions;
1976 use crate::CompressionMethod::Stored;
1977 use crate::{ZipArchive, ZipWriter};
1978 use std::io::{Cursor, Read, Write};
1979 use tempfile::TempDir;
1980
1981 #[test]
1982 fn invalid_offset() {
1983 use super::ZipArchive;
1984
1985 let mut v = Vec::new();
1986 v.extend_from_slice(include_bytes!("../tests/data/invalid_offset.zip"));
1987 let reader = ZipArchive::new(Cursor::new(v));
1988 assert!(reader.is_err());
1989 }
1990
1991 #[test]
1992 fn invalid_offset2() {
1993 use super::ZipArchive;
1994
1995 let mut v = Vec::new();
1996 v.extend_from_slice(include_bytes!("../tests/data/invalid_offset2.zip"));
1997 let reader = ZipArchive::new(Cursor::new(v));
1998 assert!(reader.is_err());
1999 }
2000
2001 #[test]
2002 fn zip64_with_leading_junk() {
2003 use super::ZipArchive;
2004
2005 let mut v = Vec::new();
2006 v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip"));
2007 let reader = ZipArchive::new(Cursor::new(v)).unwrap();
2008 assert_eq!(reader.len(), 1);
2009 }
2010
2011 #[test]
2012 fn zip_contents() {
2013 use super::ZipArchive;
2014
2015 let mut v = Vec::new();
2016 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
2017 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2018 assert_eq!(reader.comment(), b"");
2019 assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
2020 }
2021
2022 #[test]
2023 fn zip_read_streaming() {
2024 use super::read_zipfile_from_stream;
2025
2026 let mut v = Vec::new();
2027 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
2028 let mut reader = Cursor::new(v);
2029 loop {
2030 if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
2031 break;
2032 }
2033 }
2034 }
2035
2036 #[test]
2037 fn zip_clone() {
2038 use super::ZipArchive;
2039 use std::io::Read;
2040
2041 let mut v = Vec::new();
2042 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
2043 let mut reader1 = ZipArchive::new(Cursor::new(v)).unwrap();
2044 let mut reader2 = reader1.clone();
2045
2046 let mut file1 = reader1.by_index(0).unwrap();
2047 let mut file2 = reader2.by_index(0).unwrap();
2048
2049 let t = file1.last_modified().unwrap();
2050 assert_eq!(
2051 (
2052 t.year(),
2053 t.month(),
2054 t.day(),
2055 t.hour(),
2056 t.minute(),
2057 t.second()
2058 ),
2059 (1980, 1, 1, 0, 0, 0)
2060 );
2061
2062 let mut buf1 = [0; 5];
2063 let mut buf2 = [0; 5];
2064 let mut buf3 = [0; 5];
2065 let mut buf4 = [0; 5];
2066
2067 file1.read_exact(&mut buf1).unwrap();
2068 file2.read_exact(&mut buf2).unwrap();
2069 file1.read_exact(&mut buf3).unwrap();
2070 file2.read_exact(&mut buf4).unwrap();
2071
2072 assert_eq!(buf1, buf2);
2073 assert_eq!(buf3, buf4);
2074 assert_ne!(buf1, buf3);
2075 }
2076
2077 #[test]
2078 fn file_and_dir_predicates() {
2079 use super::ZipArchive;
2080
2081 let mut v = Vec::new();
2082 v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip"));
2083 let mut zip = ZipArchive::new(Cursor::new(v)).unwrap();
2084
2085 for i in 0..zip.len() {
2086 let zip_file = zip.by_index(i).unwrap();
2087 let full_name = zip_file.enclosed_name().unwrap();
2088 let file_name = full_name.file_name().unwrap().to_str().unwrap();
2089 assert!(
2090 (file_name.starts_with("dir") && zip_file.is_dir())
2091 || (file_name.starts_with("file") && zip_file.is_file())
2092 );
2093 }
2094 }
2095
2096 #[test]
2097 fn zip64_magic_in_filenames() {
2098 let files = vec![
2099 include_bytes!("../tests/data/zip64_magic_in_filename_1.zip").to_vec(),
2100 include_bytes!("../tests/data/zip64_magic_in_filename_2.zip").to_vec(),
2101 include_bytes!("../tests/data/zip64_magic_in_filename_3.zip").to_vec(),
2102 include_bytes!("../tests/data/zip64_magic_in_filename_4.zip").to_vec(),
2103 include_bytes!("../tests/data/zip64_magic_in_filename_5.zip").to_vec(),
2104 ];
2105 for file in files {
2108 ZipArchive::new(Cursor::new(file)).unwrap();
2109 }
2110 }
2111
2112 #[test]
2116 fn invalid_cde_number_of_files_allocation_smaller_offset() {
2117 use super::ZipArchive;
2118
2119 let mut v = Vec::new();
2120 v.extend_from_slice(include_bytes!(
2121 "../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
2122 ));
2123 let reader = ZipArchive::new(Cursor::new(v));
2124 assert!(reader.is_err() || reader.unwrap().is_empty());
2125 }
2126
2127 #[test]
2131 fn invalid_cde_number_of_files_allocation_greater_offset() {
2132 use super::ZipArchive;
2133
2134 let mut v = Vec::new();
2135 v.extend_from_slice(include_bytes!(
2136 "../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
2137 ));
2138 let reader = ZipArchive::new(Cursor::new(v));
2139 assert!(reader.is_err());
2140 }
2141
2142 #[cfg(feature = "deflate64")]
2143 #[test]
2144 fn deflate64_index_out_of_bounds() -> std::io::Result<()> {
2145 let mut v = Vec::new();
2146 v.extend_from_slice(include_bytes!(
2147 "../tests/data/raw_deflate64_index_out_of_bounds.zip"
2148 ));
2149 let mut reader = ZipArchive::new(Cursor::new(v))?;
2150 std::io::copy(&mut reader.by_index(0)?, &mut std::io::sink()).expect_err("Invalid file");
2151 Ok(())
2152 }
2153
2154 #[cfg(feature = "deflate64")]
2155 #[test]
2156 fn deflate64_not_enough_space() {
2157 let mut v = Vec::new();
2158 v.extend_from_slice(include_bytes!("../tests/data/deflate64_issue_25.zip"));
2159 ZipArchive::new(Cursor::new(v)).expect_err("Invalid file");
2160 }
2161
2162 #[cfg(feature = "_deflate-any")]
2163 #[test]
2164 fn test_read_with_data_descriptor() {
2165 use std::io::Read;
2166
2167 let mut v = Vec::new();
2168 v.extend_from_slice(include_bytes!("../tests/data/data_descriptor.zip"));
2169 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2170 let mut decompressed = [0u8; 16];
2171 let mut file = reader.by_index(0).unwrap();
2172 assert_eq!(file.read(&mut decompressed).unwrap(), 12);
2173 }
2174
2175 #[test]
2176 fn test_is_symlink() -> std::io::Result<()> {
2177 let mut v = Vec::new();
2178 v.extend_from_slice(include_bytes!("../tests/data/symlink.zip"));
2179 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2180 assert!(reader.by_index(0).unwrap().is_symlink());
2181 let tempdir = TempDir::with_prefix("test_is_symlink")?;
2182 reader.extract(&tempdir).unwrap();
2183 assert!(tempdir.path().join("bar").is_symlink());
2184 Ok(())
2185 }
2186
2187 #[test]
2188 #[cfg(feature = "_deflate-any")]
2189 fn test_utf8_extra_field() {
2190 let mut v = Vec::new();
2191 v.extend_from_slice(include_bytes!("../tests/data/chinese.zip"));
2192 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2193 reader.by_name("七个房间.txt").unwrap();
2194 }
2195
2196 #[test]
2197 fn test_utf8() {
2198 let mut v = Vec::new();
2199 v.extend_from_slice(include_bytes!("../tests/data/linux-7z.zip"));
2200 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2201 reader.by_name("你好.txt").unwrap();
2202 }
2203
2204 #[test]
2205 fn test_utf8_2() {
2206 let mut v = Vec::new();
2207 v.extend_from_slice(include_bytes!("../tests/data/windows-7zip.zip"));
2208 let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
2209 reader.by_name("你好.txt").unwrap();
2210 }
2211
2212 #[test]
2213 fn test_64k_files() -> ZipResult<()> {
2214 let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
2215 let options = SimpleFileOptions {
2216 compression_method: Stored,
2217 ..Default::default()
2218 };
2219 for i in 0..=u16::MAX {
2220 let file_name = format!("{i}.txt");
2221 writer.start_file(&*file_name, options)?;
2222 writer.write_all(i.to_string().as_bytes())?;
2223 }
2224
2225 let mut reader = ZipArchive::new(writer.finish()?)?;
2226 for i in 0..=u16::MAX {
2227 let expected_name = format!("{i}.txt");
2228 let expected_contents = i.to_string();
2229 let expected_contents = expected_contents.as_bytes();
2230 let mut file = reader.by_name(&expected_name)?;
2231 let mut contents = Vec::with_capacity(expected_contents.len());
2232 file.read_to_end(&mut contents)?;
2233 assert_eq!(contents, expected_contents);
2234 drop(file);
2235 contents.clear();
2236 let mut file = reader.by_index(i as usize)?;
2237 file.read_to_end(&mut contents)?;
2238 assert_eq!(contents, expected_contents);
2239 }
2240 Ok(())
2241 }
2242
2243 #[test]
2245 fn test_cannot_symlink_outside_destination() -> ZipResult<()> {
2246 use std::fs::create_dir;
2247
2248 let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
2249 writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
2250 writer.start_file("symlink/dest-file", SimpleFileOptions::default())?;
2251 let mut reader = writer.finish_into_readable()?;
2252 let dest_parent =
2253 TempDir::with_prefix("read__test_cannot_symlink_outside_destination").unwrap();
2254 let dest_sibling = dest_parent.path().join("dest-sibling");
2255 create_dir(&dest_sibling)?;
2256 let dest = dest_parent.path().join("dest");
2257 create_dir(&dest)?;
2258 assert!(reader.extract(dest).is_err());
2259 assert!(!dest_sibling.join("dest-file").exists());
2260 Ok(())
2261 }
2262
2263 #[test]
2264 fn test_can_create_destination() -> ZipResult<()> {
2265 let mut v = Vec::new();
2266 v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
2267 let mut reader = ZipArchive::new(Cursor::new(v))?;
2268 let dest = TempDir::with_prefix("read__test_can_create_destination").unwrap();
2269 reader.extract(&dest)?;
2270 assert!(dest.path().join("mimetype").exists());
2271 Ok(())
2272 }
2273}