1#[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
32pub(crate) mod stream;
34
35pub(crate) mod magic_finder;
36
37#[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 #[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 #[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 #[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 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 #[allow(clippy::needless_return)] 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
250pub struct ZipFile<'a, R: Read + ?Sized> {
252 pub(crate) data: Cow<'a, ZipFileData>,
253 pub(crate) reader: ZipFileReader<'a, R>,
254}
255
256pub 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 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 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 let data_start = data.data_start(reader)?;
354 reader.seek(SeekFrom::Start(data_start))?;
355
356 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 reader.seek(SeekFrom::Start(data.header_start))?;
366
367 let block = ZipLocalEntryBlock::parse(reader)?;
369
370 let variable_fields_len =
372 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 match data.data_start.set(data_start) {
380 Ok(()) => (),
381 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#[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 let old_entry = self.map.insert(path, mode);
556 debug_assert_eq!(old_entry, None);
557 }
558
559 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 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 let first_new_file_header_start = w.stream_position()?;
627
628 new_files.values_mut().try_for_each(|f| {
630 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 f.central_header_start = 0;
640 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 self.reader.rewind()?;
665 let length_to_read = self.shared.dir_start;
667 let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read);
671 io::copy(&mut limited_raw, &mut w)?;
673
674 Ok(new_files)
676 }
677
678 pub(crate) fn get_metadata(config: Config, reader: &mut R) -> ZipResult<ZipArchiveMetadata> {
681 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 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 Err(last_err.unwrap_or(e));
698 }
699 };
700
701 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 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 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 #[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 pub fn new(reader: R) -> ZipResult<ZipArchive<R>> {
798 Self::with_config(Default::default(), reader)
799 }
800
801 pub fn metadata(&self) -> Arc<ZipArchiveMetadata> {
806 self.shared.clone()
807 }
808
809 pub unsafe fn unsafe_new_with_metadata(reader: R, metadata: Arc<ZipArchiveMetadata>) -> Self {
846 Self {
847 reader,
848 shared: metadata,
849 }
850 }
851
852 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 pub fn extract<P: AsRef<Path>>(&mut self, directory: P) -> ZipResult<()> {
878 self.extract_internal(directory, None::<fn(&Path) -> bool>)
879 }
880
881 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 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 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 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 #[cfg(unix)]
1010 if let Some(mode) = file.unix_mode() {
1011 files_by_unix_mode.add_mode(outpath, mode);
1012 }
1013
1014 #[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 #[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 pub fn len(&self) -> usize {
1034 self.shared.files.len()
1035 }
1036
1037 pub fn central_directory_start(&self) -> u64 {
1039 self.shared.dir_start
1040 }
1041
1042 pub fn is_empty(&self) -> bool {
1044 self.len() == 0
1045 }
1046
1047 pub fn offset(&self) -> u64 {
1052 self.shared.offset
1053 }
1054
1055 pub fn comment(&self) -> &[u8] {
1057 &self.shared.comment
1058 }
1059
1060 pub fn zip64_comment(&self) -> Option<&[u8]> {
1062 self.shared.zip64_comment.as_deref()
1063 }
1064
1065 pub fn file_names(&self) -> impl Iterator<Item = &str> {
1067 self.shared.files.keys().map(|s| s.as_ref())
1068 }
1069
1070 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 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 pub fn by_name(&mut self, name: &str) -> ZipResult<ZipFile<'_, R>> {
1111 self.by_name_with_optional_password(name, None)
1112 }
1113
1114 #[inline(always)]
1116 pub fn index_for_name(&self, name: &str) -> Option<usize> {
1117 self.shared.files.get_index_of(name)
1118 }
1119
1120 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 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 #[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 #[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 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 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 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 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 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 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 options.password = None;
1263 } else {
1264 match (options.password, data.encrypted) {
1266 (None, true) => {
1267 return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED))
1268 }
1269 (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 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 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 path.components().count() == 1 {
1344 if file.is_dir() {
1345 replace_root_dir!(path);
1347 } else {
1348 return Ok(None);
1351 }
1352 }
1353
1354 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 pub fn into_inner(self) -> R {
1370 self.reader
1371 }
1372}
1373
1374#[derive(Debug)]
1376#[cfg(feature = "aes-crypto")]
1377pub struct AesInfo {
1378 pub aes_mode: AesMode,
1380 pub verification_value: [u8; PWD_VERIFY_LENGTH],
1382 pub salt: Vec<u8>,
1384}
1385
1386const fn unsupported_zip_error<T>(detail: &'static str) -> ZipResult<T> {
1387 Err(ZipError::UnsupportedArchive(detail))
1388}
1389
1390pub(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 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
1427fn 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 version_made_by,
1437 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 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 let mut result = ZipFileData {
1473 system: System::from((version_made_by >> 8) as u8),
1474 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 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 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 file.extra_fields
1630 .push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?));
1631 }
1632 0x9901 => {
1633 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 file.extra_fields.push(ExtraField::ExtendedTimestamp(
1673 ExtendedTimestamp::try_from_reader(reader, len)?,
1674 ));
1675 }
1676 0x6375 => {
1677 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 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 }
1704 }
1705 Ok(false)
1706}
1707
1708pub trait HasZipMetadata {
1710 fn get_metadata(&self) -> &ZipFileData;
1712}
1713
1714#[derive(Default)]
1716pub struct ZipReadOptions<'a> {
1717 password: Option<&'a [u8]>,
1719
1720 ignore_encryption_flag: bool,
1722}
1723
1724impl<'a> ZipReadOptions<'a> {
1725 #[must_use]
1727 pub fn new() -> Self {
1728 Self::default()
1729 }
1730
1731 #[must_use]
1733 pub fn password(mut self, password: Option<&'a [u8]>) -> Self {
1734 self.password = password;
1735 self
1736 }
1737
1738 #[must_use]
1740 pub fn ignore_encryption_flag(mut self, ignore: bool) -> Self {
1741 self.ignore_encryption_flag = ignore;
1742 self
1743 }
1744}
1745
1746impl<'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 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 pub fn name(&self) -> &str {
1773 &self.get_metadata().file_name
1774 }
1775
1776 pub fn name_raw(&self) -> &[u8] {
1780 &self.get_metadata().file_name_raw
1781 }
1782
1783 #[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 pub fn mangled_name(&self) -> PathBuf {
1807 self.get_metadata().file_name_sanitized()
1808 }
1809
1810 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 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 None => {
1849 debug_assert!(
1857 !filter(&PathBuf::from_iter(components.iter())),
1858 "Root directory filter should not match at this point"
1859 );
1860
1861 &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 outpath.push(component);
1879
1880 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 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 (Some(Component::Normal(a)), Some(Component::Normal(b))) => a == b,
1921 (None, None) => true,
1923 (Some(_), None) => false,
1925 (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 pub fn comment(&self) -> &str {
1943 &self.get_metadata().file_comment
1944 }
1945
1946 pub fn compression(&self) -> CompressionMethod {
1948 self.get_metadata().compression_method
1949 }
1950
1951 pub fn encrypted(&self) -> bool {
1953 self.data.encrypted
1954 }
1955
1956 pub fn compressed_size(&self) -> u64 {
1958 self.get_metadata().compressed_size
1959 }
1960
1961 pub fn size(&self) -> u64 {
1963 self.get_metadata().uncompressed_size
1964 }
1965
1966 pub fn last_modified(&self) -> Option<DateTime> {
1968 self.data.last_modified_time
1969 }
1970 pub fn is_dir(&self) -> bool {
1972 is_dir(self.name())
1973 }
1974
1975 pub fn is_symlink(&self) -> bool {
1977 self.unix_mode()
1978 .is_some_and(|mode| mode & S_IFLNK == S_IFLNK)
1979 }
1980
1981 pub fn is_file(&self) -> bool {
1983 !self.is_dir() && !self.is_symlink()
1984 }
1985
1986 pub fn unix_mode(&self) -> Option<u32> {
1988 self.get_metadata().unix_mode()
1989 }
1990
1991 pub fn crc32(&self) -> u32 {
1993 self.get_metadata().crc32
1994 }
1995
1996 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 pub fn data_start(&self) -> u64 {
2006 *self.data.data_start.get().unwrap()
2007 }
2008
2009 pub fn header_start(&self) -> u64 {
2011 self.get_metadata().header_start
2012 }
2013 pub fn central_header_start(&self) -> u64 {
2015 self.get_metadata().central_header_start
2016 }
2017
2018 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 options.aes_mode = Some(aes);
2037 }
2038 options
2039 }
2040}
2041
2042impl<R: Read> ZipFile<'_, R> {
2044 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 if let Cow::Owned(_) = self.data {
2101 if let Ok(mut inner) = self.take_raw_reader() {
2103 let _ = copy(&mut inner, &mut sink());
2104 }
2105 }
2106 }
2107}
2108
2109pub fn read_zipfile_from_stream<R: Read>(reader: &mut R) -> ZipResult<Option<ZipFile<'_, R>>> {
2126 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
2173pub trait RootDirFilter: Fn(&Path) -> bool {}
2181impl<F: Fn(&Path) -> bool> RootDirFilter for F {}
2182
2183pub 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")]
2227fn 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")]
2237fn 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
2255pub 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 for file in files {
2436 ZipArchive::new(Cursor::new(file)).unwrap();
2437 }
2438 }
2439
2440 #[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]
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 #[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)?; 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 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 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 assert!(file.encrypted());
2630 file.read_to_string(&mut contents)?; assert_eq!(contents, "This file is not encrypted.\n");
2632 Ok(())
2633 }
2634}