#[cfg(feature = "aes-crypto")]
use crate::aes::{AesReader, AesReaderValid};
use crate::compression::{CompressionMethod, Decompressor};
use crate::cp437::FromCp437;
use crate::crc32::Crc32Reader;
use crate::extra_fields::{ExtendedTimestamp, ExtraField};
use crate::read::zip_archive::{Shared, SharedBuilder};
use crate::result::{ZipError, ZipResult};
use crate::spec::{
self, FixedSizeBlock, Pod, Zip32CentralDirectoryEnd, Zip64CDELocatorBlock,
Zip64CentralDirectoryEnd, ZIP64_ENTRY_THR,
};
use crate::types::{
AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
ZipLocalEntryBlock,
};
use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
use indexmap::IndexMap;
use std::borrow::Cow;
use std::ffi::OsString;
use std::fs::create_dir_all;
use std::io::{self, copy, prelude::*, sink, SeekFrom};
use std::mem;
use std::mem::size_of;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
mod config;
pub use config::*;
pub(crate) mod stream;
#[cfg(feature = "lzma")]
pub(crate) mod lzma;
#[cfg(feature = "xz")]
pub(crate) mod xz;
pub(crate) mod zip_archive {
use indexmap::IndexMap;
use std::sync::Arc;
#[derive(Debug)]
pub(crate) struct Shared {
pub(crate) files: IndexMap<Box<str>, super::ZipFileData>,
pub(super) offset: u64,
pub(super) dir_start: u64,
#[allow(dead_code)]
pub(super) config: super::Config,
}
#[derive(Debug)]
pub(crate) struct SharedBuilder {
pub(crate) files: Vec<super::ZipFileData>,
pub(super) offset: u64,
pub(super) dir_start: u64,
#[allow(dead_code)]
pub(super) config: super::Config,
}
impl SharedBuilder {
pub fn build(self) -> Shared {
let mut index_map = IndexMap::with_capacity(self.files.len());
self.files.into_iter().for_each(|file| {
index_map.insert(file.file_name.clone(), file);
});
Shared {
files: index_map,
offset: self.offset,
dir_start: self.dir_start,
config: self.config,
}
}
}
#[derive(Clone, Debug)]
pub struct ZipArchive<R> {
pub(super) reader: R,
pub(super) shared: Arc<Shared>,
pub(super) comment: Arc<[u8]>,
}
}
#[cfg(feature = "aes-crypto")]
use crate::aes::PWD_VERIFY_LENGTH;
use crate::extra_fields::UnicodeExtraField;
use crate::result::ZipError::{InvalidArchive, InvalidPassword};
use crate::spec::is_dir;
use crate::types::ffi::S_IFLNK;
use crate::unstable::{path_to_string, LittleEndianReadExt};
pub use zip_archive::ZipArchive;
#[allow(clippy::large_enum_variant)]
pub(crate) enum CryptoReader<'a> {
Plaintext(io::Take<&'a mut dyn Read>),
ZipCrypto(ZipCryptoReaderValid<io::Take<&'a mut dyn Read>>),
#[cfg(feature = "aes-crypto")]
Aes {
reader: AesReaderValid<io::Take<&'a mut dyn Read>>,
vendor_version: AesVendorVersion,
},
}
impl<'a> Read for CryptoReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
CryptoReader::Plaintext(r) => r.read(buf),
CryptoReader::ZipCrypto(r) => r.read(buf),
#[cfg(feature = "aes-crypto")]
CryptoReader::Aes { reader: r, .. } => r.read(buf),
}
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
match self {
CryptoReader::Plaintext(r) => r.read_to_end(buf),
CryptoReader::ZipCrypto(r) => r.read_to_end(buf),
#[cfg(feature = "aes-crypto")]
CryptoReader::Aes { reader: r, .. } => r.read_to_end(buf),
}
}
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
match self {
CryptoReader::Plaintext(r) => r.read_to_string(buf),
CryptoReader::ZipCrypto(r) => r.read_to_string(buf),
#[cfg(feature = "aes-crypto")]
CryptoReader::Aes { reader: r, .. } => r.read_to_string(buf),
}
}
}
impl<'a> CryptoReader<'a> {
pub fn into_inner(self) -> io::Take<&'a mut dyn Read> {
match self {
CryptoReader::Plaintext(r) => r,
CryptoReader::ZipCrypto(r) => r.into_inner(),
#[cfg(feature = "aes-crypto")]
CryptoReader::Aes { reader: r, .. } => r.into_inner(),
}
}
pub const fn is_ae2_encrypted(&self) -> bool {
#[cfg(feature = "aes-crypto")]
return matches!(
self,
CryptoReader::Aes {
vendor_version: AesVendorVersion::Ae2,
..
}
);
#[cfg(not(feature = "aes-crypto"))]
false
}
}
#[cold]
fn invalid_state<T>() -> io::Result<T> {
Err(io::Error::new(
io::ErrorKind::Other,
"ZipFileReader was in an invalid state",
))
}
pub(crate) enum ZipFileReader<'a> {
NoReader,
Raw(io::Take<&'a mut dyn Read>),
Compressed(Box<Crc32Reader<Decompressor<io::BufReader<CryptoReader<'a>>>>>),
}
impl<'a> Read for ZipFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
ZipFileReader::NoReader => invalid_state(),
ZipFileReader::Raw(r) => r.read(buf),
ZipFileReader::Compressed(r) => r.read(buf),
}
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
match self {
ZipFileReader::NoReader => invalid_state(),
ZipFileReader::Raw(r) => r.read_exact(buf),
ZipFileReader::Compressed(r) => r.read_exact(buf),
}
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
match self {
ZipFileReader::NoReader => invalid_state(),
ZipFileReader::Raw(r) => r.read_to_end(buf),
ZipFileReader::Compressed(r) => r.read_to_end(buf),
}
}
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
match self {
ZipFileReader::NoReader => invalid_state(),
ZipFileReader::Raw(r) => r.read_to_string(buf),
ZipFileReader::Compressed(r) => r.read_to_string(buf),
}
}
}
impl<'a> ZipFileReader<'a> {
fn into_inner(self) -> io::Result<io::Take<&'a mut dyn Read>> {
match self {
ZipFileReader::NoReader => invalid_state(),
ZipFileReader::Raw(r) => Ok(r),
ZipFileReader::Compressed(r) => {
Ok(r.into_inner().into_inner().into_inner().into_inner())
}
}
}
}
pub struct ZipFile<'a> {
pub(crate) data: Cow<'a, ZipFileData>,
pub(crate) reader: ZipFileReader<'a>,
}
pub struct ZipFileSeek<'a, R> {
data: Cow<'a, ZipFileData>,
reader: ZipFileSeekReader<'a, R>,
}
enum ZipFileSeekReader<'a, R> {
Raw(SeekableTake<'a, R>),
}
struct SeekableTake<'a, R> {
inner: &'a mut R,
inner_starting_offset: u64,
length: u64,
current_offset: u64,
}
impl<'a, R: Seek> SeekableTake<'a, R> {
pub fn new(inner: &'a mut R, length: u64) -> io::Result<Self> {
let inner_starting_offset = inner.stream_position()?;
Ok(Self {
inner,
inner_starting_offset,
length,
current_offset: 0,
})
}
}
impl<'a, R: Seek> Seek for SeekableTake<'a, R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let offset = match pos {
SeekFrom::Start(offset) => Some(offset),
SeekFrom::End(offset) => self.length.checked_add_signed(offset),
SeekFrom::Current(offset) => self.current_offset.checked_add_signed(offset),
};
match offset {
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a negative or overflowing position",
)),
Some(offset) => {
let clamped_offset = std::cmp::min(self.length, offset);
let new_inner_offset = self
.inner
.seek(SeekFrom::Start(self.inner_starting_offset + clamped_offset))?;
self.current_offset = new_inner_offset - self.inner_starting_offset;
Ok(new_inner_offset)
}
}
}
}
impl<'a, R: Read> Read for SeekableTake<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let written = self
.inner
.take(self.length - self.current_offset)
.read(buf)?;
self.current_offset += written as u64;
Ok(written)
}
}
pub(crate) fn find_content<'a>(
data: &ZipFileData,
reader: &'a mut (impl Read + Seek),
) -> ZipResult<io::Take<&'a mut dyn Read>> {
let data_start = match data.data_start.get() {
Some(data_start) => *data_start,
None => find_data_start(data, reader)?,
};
reader.seek(SeekFrom::Start(data_start))?;
Ok((reader as &mut dyn Read).take(data.compressed_size))
}
fn find_content_seek<'a, R: Read + Seek>(
data: &ZipFileData,
reader: &'a mut R,
) -> ZipResult<SeekableTake<'a, R>> {
let data_start = find_data_start(data, reader)?;
reader.seek(SeekFrom::Start(data_start))?;
Ok(SeekableTake::new(reader, data.compressed_size)?)
}
fn find_data_start(
data: &ZipFileData,
reader: &mut (impl Read + Seek + Sized),
) -> Result<u64, ZipError> {
reader.seek(SeekFrom::Start(data.header_start))?;
let block = ZipLocalEntryBlock::parse(reader)?;
let variable_fields_len =
block.file_name_length as u64 + block.extra_field_length as u64;
let data_start =
data.header_start + size_of::<ZipLocalEntryBlock>() as u64 + variable_fields_len;
match data.data_start.set(data_start) {
Ok(()) => (),
Err(_) => {
debug_assert_eq!(*data.data_start.get().unwrap(), data_start);
}
}
Ok(data_start)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn make_crypto_reader<'a>(
data: &ZipFileData,
reader: io::Take<&'a mut dyn Read>,
password: Option<&[u8]>,
aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
) -> ZipResult<CryptoReader<'a>> {
#[allow(deprecated)]
{
if let CompressionMethod::Unsupported(_) = data.compression_method {
return unsupported_zip_error("Compression method not supported");
}
}
let reader = match (password, aes_info) {
#[cfg(not(feature = "aes-crypto"))]
(Some(_), Some(_)) => {
return Err(ZipError::UnsupportedArchive(
"AES encrypted files cannot be decrypted without the aes-crypto feature.",
))
}
#[cfg(feature = "aes-crypto")]
(Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes {
reader: AesReader::new(reader, aes_mode, data.compressed_size).validate(password)?,
vendor_version,
},
(Some(password), None) => {
let mut last_modified_time = data.last_modified_time;
if !data.using_data_descriptor {
last_modified_time = None;
}
let validator = if let Some(last_modified_time) = last_modified_time {
ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
} else {
ZipCryptoValidator::PkzipCrc32(data.crc32)
};
CryptoReader::ZipCrypto(ZipCryptoReader::new(reader, password).validate(validator)?)
}
(None, Some(_)) => return Err(InvalidPassword),
(None, None) => CryptoReader::Plaintext(reader),
};
Ok(reader)
}
pub(crate) fn make_reader(
compression_method: CompressionMethod,
crc32: u32,
reader: CryptoReader,
) -> ZipResult<ZipFileReader> {
let ae2_encrypted = reader.is_ae2_encrypted();
Ok(ZipFileReader::Compressed(Box::new(Crc32Reader::new(
Decompressor::new(io::BufReader::new(reader), compression_method)?,
crc32,
ae2_encrypted,
))))
}
#[derive(Debug)]
pub(crate) struct CentralDirectoryInfo {
pub(crate) archive_offset: u64,
pub(crate) directory_start: u64,
pub(crate) cde_position: u64,
pub(crate) number_of_files: usize,
pub(crate) disk_number: u32,
pub(crate) disk_with_central_directory: u32,
pub(crate) is_zip64: bool,
}
impl<R> ZipArchive<R> {
pub(crate) fn from_finalized_writer(
files: IndexMap<Box<str>, ZipFileData>,
comment: Box<[u8]>,
reader: R,
central_start: u64,
) -> ZipResult<Self> {
let initial_offset = match files.first() {
Some((_, file)) => file.header_start,
None => central_start,
};
let shared = Arc::new(Shared {
files,
offset: initial_offset,
dir_start: central_start,
config: Config {
archive_offset: ArchiveOffset::Known(initial_offset),
},
});
Ok(Self {
reader,
shared,
comment: comment.into(),
})
}
pub fn decompressed_size(&self) -> Option<u128> {
let mut total = 0u128;
for file in self.shared.files.values() {
if file.using_data_descriptor {
return None;
}
total = total.checked_add(file.uncompressed_size as u128)?;
}
Some(total)
}
}
impl<R: Read + Seek> ZipArchive<R> {
pub(crate) fn merge_contents<W: Write + Seek>(
&mut self,
mut w: W,
) -> ZipResult<IndexMap<Box<str>, ZipFileData>> {
if self.shared.files.is_empty() {
return Ok(IndexMap::new());
}
let mut new_files = self.shared.files.clone();
let first_new_file_header_start = w.stream_position()?;
new_files.values_mut().try_for_each(|f| {
f.header_start = f
.header_start
.checked_add(first_new_file_header_start)
.ok_or(InvalidArchive(
"new header start from merge would have been too large",
))?;
f.central_header_start = 0;
if let Some(old_data_start) = f.data_start.take() {
let new_data_start = old_data_start
.checked_add(first_new_file_header_start)
.ok_or(InvalidArchive(
"new data start from merge would have been too large",
))?;
f.data_start.get_or_init(|| new_data_start);
}
Ok::<_, ZipError>(())
})?;
self.reader.rewind()?;
let length_to_read = self.shared.dir_start;
let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read);
io::copy(&mut limited_raw, &mut w)?;
Ok(new_files)
}
fn get_directory_info_zip32(
config: &Config,
reader: &mut R,
footer: &Zip32CentralDirectoryEnd,
cde_start_pos: u64,
) -> ZipResult<CentralDirectoryInfo> {
let archive_offset = match config.archive_offset {
ArchiveOffset::Known(n) => n,
ArchiveOffset::FromCentralDirectory | ArchiveOffset::Detect => {
let mut offset = cde_start_pos
.checked_sub(footer.central_directory_size as u64)
.and_then(|x| x.checked_sub(footer.central_directory_offset as u64))
.ok_or(InvalidArchive("Invalid central directory size or offset"))?;
if config.archive_offset == ArchiveOffset::Detect {
reader.seek(SeekFrom::Start(
offset + footer.central_directory_offset as u64,
))?;
let mut buf = [0; 4];
reader.read_exact(&mut buf)?;
if spec::Magic::from_le_bytes(buf)
!= spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE
{
offset = 0;
}
}
offset
}
};
let directory_start = footer.central_directory_offset as u64 + archive_offset;
let number_of_files = footer.number_of_files_on_this_disk as usize;
Ok(CentralDirectoryInfo {
archive_offset,
directory_start,
number_of_files,
disk_number: footer.disk_number as u32,
disk_with_central_directory: footer.disk_with_central_directory as u32,
cde_position: cde_start_pos,
is_zip64: false,
})
}
const fn order_lower_upper_bounds(a: u64, b: u64) -> (u64, u64) {
if a > b {
(b, a)
} else {
(a, b)
}
}
fn get_directory_info_zip64(
config: &Config,
reader: &mut R,
cde_start_pos: u64,
) -> ZipResult<Vec<ZipResult<CentralDirectoryInfo>>> {
reader.seek(SeekFrom::Start(
cde_start_pos
.checked_sub(size_of::<Zip64CDELocatorBlock>() as u64)
.ok_or(InvalidArchive(
"No room for ZIP64 locator before central directory end",
))?,
))?;
let locator64 = spec::Zip64CentralDirectoryEndLocator::parse(reader)?;
let search_upper_bound = cde_start_pos
.checked_sub(
(size_of::<Zip64CentralDirectoryEnd>()
+ size_of::<spec::Zip64CentralDirectoryEndLocator>()) as u64,
)
.ok_or(InvalidArchive(
"File cannot contain ZIP64 central directory end",
))?;
let (lower, upper) = Self::order_lower_upper_bounds(
locator64.end_of_central_directory_offset,
search_upper_bound,
);
let search_results = Zip64CentralDirectoryEnd::find_and_parse(reader, lower, upper)?;
let results: Vec<ZipResult<CentralDirectoryInfo>> =
search_results.into_iter().map(|(footer64, archive_offset)| {
let archive_offset = match config.archive_offset {
ArchiveOffset::Known(n) => n,
ArchiveOffset::FromCentralDirectory => archive_offset,
ArchiveOffset::Detect => {
archive_offset.checked_add(footer64.central_directory_offset)
.and_then(|start| {
reader.seek(SeekFrom::Start(start)).ok()?;
let mut buf = [0; 4];
reader.read_exact(&mut buf).ok()?;
if spec::Magic::from_le_bytes(buf) != spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE {
None
} else {
Some(archive_offset)
}
})
.unwrap_or(0)
}
};
let directory_start = footer64
.central_directory_offset
.checked_add(archive_offset)
.ok_or(InvalidArchive(
"Invalid central directory size or offset",
))?;
if directory_start > search_upper_bound {
Err(InvalidArchive(
"Invalid central directory size or offset",
))
} else if footer64.number_of_files_on_this_disk > footer64.number_of_files {
Err(InvalidArchive(
"ZIP64 footer indicates more files on this disk than in the whole archive",
))
} else if footer64.version_needed_to_extract > footer64.version_made_by {
Err(InvalidArchive(
"ZIP64 footer indicates a new version is needed to extract this archive than the \
version that wrote it",
))
} else {
Ok(CentralDirectoryInfo {
archive_offset,
directory_start,
number_of_files: footer64.number_of_files as usize,
disk_number: footer64.disk_number,
disk_with_central_directory: footer64.disk_with_central_directory,
cde_position: cde_start_pos,
is_zip64: true,
})
}
}).collect();
Ok(results)
}
pub(crate) fn get_metadata(
config: Config,
reader: &mut R,
) -> ZipResult<(Zip32CentralDirectoryEnd, Shared)> {
let mut invalid_errors_32 = Vec::new();
let mut unsupported_errors_32 = Vec::new();
let mut invalid_errors_64 = Vec::new();
let mut unsupported_errors_64 = Vec::new();
let mut ok_results = Vec::new();
let cde_locations = Zip32CentralDirectoryEnd::find_and_parse(reader)?;
cde_locations
.into_vec()
.into_iter()
.for_each(|(footer, cde_start_pos)| {
let zip32_result =
Self::get_directory_info_zip32(&config, reader, &footer, cde_start_pos);
Self::sort_result(
zip32_result,
&mut invalid_errors_32,
&mut unsupported_errors_32,
&mut ok_results,
&footer,
);
let mut inner_results = Vec::with_capacity(1);
let zip64_vec_result =
Self::get_directory_info_zip64(&config, reader, cde_start_pos);
Self::sort_result(
zip64_vec_result,
&mut invalid_errors_64,
&mut unsupported_errors_64,
&mut inner_results,
&(),
);
inner_results.into_iter().for_each(|(_, results)| {
results.into_iter().for_each(|result| {
Self::sort_result(
result,
&mut invalid_errors_64,
&mut unsupported_errors_64,
&mut ok_results,
&footer,
);
});
});
});
ok_results.sort_by_key(|(_, result)| {
(
u64::MAX - result.cde_position, !result.is_zip64, )
});
let mut best_result = None;
for (footer, result) in ok_results {
let mut inner_result = Vec::with_capacity(1);
let is_zip64 = result.is_zip64;
Self::sort_result(
Self::read_central_header(result, config, reader),
if is_zip64 {
&mut invalid_errors_64
} else {
&mut invalid_errors_32
},
if is_zip64 {
&mut unsupported_errors_64
} else {
&mut unsupported_errors_32
},
&mut inner_result,
&(),
);
if let Some((_, shared)) = inner_result.into_iter().next() {
if shared.files.len() == footer.number_of_files as usize
|| (is_zip64 && footer.number_of_files == ZIP64_ENTRY_THR as u16)
{
best_result = Some((footer, shared));
break;
} else {
if is_zip64 {
&mut invalid_errors_64
} else {
&mut invalid_errors_32
}
.push(InvalidArchive("wrong number of files"))
}
}
}
let Some((footer, shared)) = best_result else {
return Err(unsupported_errors_32
.into_iter()
.chain(unsupported_errors_64)
.chain(invalid_errors_32)
.chain(invalid_errors_64)
.next()
.unwrap());
};
reader.seek(SeekFrom::Start(shared.dir_start))?;
Ok((Rc::try_unwrap(footer).unwrap(), shared.build()))
}
fn read_central_header(
dir_info: CentralDirectoryInfo,
config: Config,
reader: &mut R,
) -> Result<SharedBuilder, ZipError> {
let file_capacity = if dir_info.number_of_files > dir_info.directory_start as usize {
0
} else {
dir_info.number_of_files
};
if dir_info.disk_number != dir_info.disk_with_central_directory {
return unsupported_zip_error("Support for multi-disk files is not implemented");
}
let mut files = Vec::with_capacity(file_capacity);
reader.seek(SeekFrom::Start(dir_info.directory_start))?;
for _ in 0..dir_info.number_of_files {
let file = central_header_to_zip_file(reader, dir_info.archive_offset)?;
files.push(file);
}
Ok(SharedBuilder {
files,
offset: dir_info.archive_offset,
dir_start: dir_info.directory_start,
config,
})
}
fn sort_result<T, U: Clone>(
result: ZipResult<T>,
invalid_errors: &mut Vec<ZipError>,
unsupported_errors: &mut Vec<ZipError>,
ok_results: &mut Vec<(U, T)>,
footer: &U,
) {
match result {
Err(ZipError::UnsupportedArchive(e)) => {
unsupported_errors.push(ZipError::UnsupportedArchive(e))
}
Err(e) => invalid_errors.push(e),
Ok(o) => ok_results.push((footer.clone(), o)),
}
}
#[cfg(feature = "aes-crypto")]
pub fn get_aes_verification_key_and_salt(
&mut self,
file_number: usize,
) -> ZipResult<Option<AesInfo>> {
let (_, data) = self
.shared
.files
.get_index(file_number)
.ok_or(ZipError::FileNotFound)?;
let limit_reader = find_content(data, &mut self.reader)?;
match data.aes_mode {
None => Ok(None),
Some((aes_mode, _, _)) => {
let (verification_value, salt) =
AesReader::new(limit_reader, aes_mode, data.compressed_size)
.get_verification_value_and_salt()?;
let aes_info = AesInfo {
aes_mode,
verification_value,
salt,
};
Ok(Some(aes_info))
}
}
}
pub fn new(reader: R) -> ZipResult<ZipArchive<R>> {
Self::with_config(Default::default(), reader)
}
pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
reader.seek(SeekFrom::Start(0))?;
if let Ok((footer, shared)) = Self::get_metadata(config, &mut reader) {
return Ok(ZipArchive {
reader,
shared: shared.into(),
comment: footer.zip_file_comment.into(),
});
}
Err(InvalidArchive("No valid central directory found"))
}
pub fn extract<P: AsRef<Path>>(&mut self, directory: P) -> ZipResult<()> {
use std::fs;
#[cfg(unix)]
let mut files_by_unix_mode = Vec::new();
for i in 0..self.len() {
let mut file = self.by_index(i)?;
let filepath = file
.enclosed_name()
.ok_or(InvalidArchive("Invalid file path"))?;
let outpath = directory.as_ref().join(filepath);
if file.is_dir() {
Self::make_writable_dir_all(&outpath)?;
continue;
}
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
Some(target)
} else {
None
};
drop(file);
if let Some(p) = outpath.parent() {
Self::make_writable_dir_all(p)?;
}
if let Some(target) = symlink_target {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
let target = OsString::from_vec(target);
std::os::unix::fs::symlink(&target, outpath.as_path())?;
}
#[cfg(windows)]
{
let Ok(target) = String::from_utf8(target) else {
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
};
let target = target.into_boxed_str();
let target_is_dir_from_archive =
self.shared.files.contains_key(&target) && is_dir(&target);
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
let target_is_dir = if target_is_dir_from_archive {
true
} else if let Ok(meta) = std::fs::metadata(&target_path) {
meta.is_dir()
} else {
false
};
if target_is_dir {
std::os::windows::fs::symlink_dir(target_path, outpath.as_path())?;
} else {
std::os::windows::fs::symlink_file(target_path, outpath.as_path())?;
}
}
continue;
}
let mut file = self.by_index(i)?;
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
#[cfg(unix)]
{
if let Some(mode) = file.unix_mode() {
files_by_unix_mode.push((outpath.clone(), mode));
}
}
}
#[cfg(unix)]
{
use std::cmp::Reverse;
use std::os::unix::fs::PermissionsExt;
if files_by_unix_mode.len() > 1 {
files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone()));
}
for (path, mode) in files_by_unix_mode.into_iter() {
fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
}
}
Ok(())
}
fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
create_dir_all(outpath.as_ref())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
outpath.as_ref(),
std::fs::Permissions::from_mode(
0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
),
)?;
}
Ok(())
}
pub fn len(&self) -> usize {
self.shared.files.len()
}
pub fn central_directory_start(&self) -> u64 {
self.shared.dir_start
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn offset(&self) -> u64 {
self.shared.offset
}
pub fn comment(&self) -> &[u8] {
&self.comment
}
pub fn file_names(&self) -> impl Iterator<Item = &str> {
self.shared.files.keys().map(|s| s.as_ref())
}
pub fn by_name_decrypt(&mut self, name: &str, password: &[u8]) -> ZipResult<ZipFile> {
self.by_name_with_optional_password(name, Some(password))
}
pub fn by_name(&mut self, name: &str) -> ZipResult<ZipFile> {
self.by_name_with_optional_password(name, None)
}
#[inline(always)]
pub fn index_for_name(&self, name: &str) -> Option<usize> {
self.shared.files.get_index_of(name)
}
#[inline(always)]
pub fn index_for_path<T: AsRef<Path>>(&self, path: T) -> Option<usize> {
self.index_for_name(&path_to_string(path))
}
#[inline(always)]
pub fn name_for_index(&self, index: usize) -> Option<&str> {
self.shared
.files
.get_index(index)
.map(|(name, _)| name.as_ref())
}
pub fn by_name_seek(&mut self, name: &str) -> ZipResult<ZipFileSeek<R>> {
self.by_index_seek(self.index_for_name(name).ok_or(ZipError::FileNotFound)?)
}
pub fn by_index_seek(&mut self, index: usize) -> ZipResult<ZipFileSeek<R>> {
let reader = &mut self.reader;
self.shared
.files
.get_index(index)
.ok_or(ZipError::FileNotFound)
.and_then(move |(_, data)| {
let seek_reader = match data.compression_method {
CompressionMethod::Stored => {
ZipFileSeekReader::Raw(find_content_seek(data, reader)?)
}
_ => {
return Err(ZipError::UnsupportedArchive(
"Seekable compressed files are not yet supported",
))
}
};
Ok(ZipFileSeek {
reader: seek_reader,
data: Cow::Borrowed(data),
})
})
}
fn by_name_with_optional_password<'a>(
&'a mut self,
name: &str,
password: Option<&[u8]>,
) -> ZipResult<ZipFile<'a>> {
let Some(index) = self.shared.files.get_index_of(name) else {
return Err(ZipError::FileNotFound);
};
self.by_index_with_optional_password(index, password)
}
pub fn by_index_decrypt(
&mut self,
file_number: usize,
password: &[u8],
) -> ZipResult<ZipFile<'_>> {
self.by_index_with_optional_password(file_number, Some(password))
}
pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
self.by_index_with_optional_password(file_number, None)
}
pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
let reader = &mut self.reader;
let (_, data) = self
.shared
.files
.get_index(file_number)
.ok_or(ZipError::FileNotFound)?;
Ok(ZipFile {
reader: ZipFileReader::Raw(find_content(data, reader)?),
data: Cow::Borrowed(data),
})
}
fn by_index_with_optional_password(
&mut self,
file_number: usize,
mut password: Option<&[u8]>,
) -> ZipResult<ZipFile<'_>> {
let (_, data) = self
.shared
.files
.get_index(file_number)
.ok_or(ZipError::FileNotFound)?;
match (password, data.encrypted) {
(None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)),
(Some(_), false) => password = None, _ => {}
}
let limit_reader = find_content(data, &mut self.reader)?;
let crypto_reader = make_crypto_reader(data, limit_reader, password, data.aes_mode)?;
Ok(ZipFile {
data: Cow::Borrowed(data),
reader: make_reader(data.compression_method, data.crc32, crypto_reader)?,
})
}
pub fn into_inner(self) -> R {
self.reader
}
}
#[derive(Debug)]
#[cfg(feature = "aes-crypto")]
pub struct AesInfo {
pub aes_mode: AesMode,
pub verification_value: [u8; PWD_VERIFY_LENGTH],
pub salt: Vec<u8>,
}
const fn unsupported_zip_error<T>(detail: &'static str) -> ZipResult<T> {
Err(ZipError::UnsupportedArchive(detail))
}
pub(crate) fn central_header_to_zip_file<R: Read + Seek>(
reader: &mut R,
archive_offset: u64,
) -> ZipResult<ZipFileData> {
let central_header_start = reader.stream_position()?;
let block = ZipCentralEntryBlock::parse(reader)?;
let file =
central_header_to_zip_file_inner(reader, archive_offset, central_header_start, block)?;
let central_header_end = reader.stream_position()?;
let data_start = find_data_start(&file, reader)?;
if data_start > central_header_start {
return Err(InvalidArchive(
"A file can't start after its central-directory header",
));
}
reader.seek(SeekFrom::Start(central_header_end))?;
Ok(file)
}
#[inline]
fn read_variable_length_byte_field<R: Read>(reader: &mut R, len: usize) -> io::Result<Box<[u8]>> {
let mut data = vec![0; len].into_boxed_slice();
reader.read_exact(&mut data)?;
Ok(data)
}
fn central_header_to_zip_file_inner<R: Read>(
reader: &mut R,
archive_offset: u64,
central_header_start: u64,
block: ZipCentralEntryBlock,
) -> ZipResult<ZipFileData> {
let ZipCentralEntryBlock {
version_made_by,
flags,
compression_method,
last_mod_time,
last_mod_date,
crc32,
compressed_size,
uncompressed_size,
file_name_length,
extra_field_length,
file_comment_length,
external_file_attributes,
offset,
..
} = block;
let encrypted = flags & 1 == 1;
let is_utf8 = flags & (1 << 11) != 0;
let using_data_descriptor = flags & (1 << 3) != 0;
let file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?;
let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?;
let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?;
let file_name: Box<str> = match is_utf8 {
true => String::from_utf8_lossy(&file_name_raw).into(),
false => file_name_raw.clone().from_cp437(),
};
let file_comment: Box<str> = match is_utf8 {
true => String::from_utf8_lossy(&file_comment_raw).into(),
false => file_comment_raw.from_cp437(),
};
let mut result = ZipFileData {
system: System::from((version_made_by >> 8) as u8),
version_made_by: version_made_by as u8,
encrypted,
using_data_descriptor,
is_utf8,
compression_method: CompressionMethod::parse_from_u16(compression_method),
compression_level: None,
last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
crc32,
compressed_size: compressed_size.into(),
uncompressed_size: uncompressed_size.into(),
file_name,
file_name_raw,
extra_field: Some(Arc::new(extra_field.to_vec())),
central_extra_field: None,
file_comment,
header_start: offset.into(),
extra_data_start: None,
central_header_start,
data_start: OnceLock::new(),
external_attributes: external_file_attributes,
large_file: false,
aes_mode: None,
aes_extra_data_start: 0,
extra_fields: Vec::new(),
};
match parse_extra_field(&mut result) {
Ok(stripped_extra_field) => {
result.extra_field = stripped_extra_field;
}
Err(ZipError::Io(..)) => {}
Err(e) => return Err(e),
}
let aes_enabled = result.compression_method == CompressionMethod::AES;
if aes_enabled && result.aes_mode.is_none() {
return Err(InvalidArchive(
"AES encryption without AES extra data field",
));
}
result.header_start = result
.header_start
.checked_add(archive_offset)
.ok_or(InvalidArchive("Archive header is too large"))?;
Ok(result)
}
pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<Option<Arc<Vec<u8>>>> {
let Some(ref extra_field) = file.extra_field else {
return Ok(None);
};
let extra_field = extra_field.clone();
let mut processed_extra_field = extra_field.clone();
let len = extra_field.len();
let mut reader = io::Cursor::new(&**extra_field);
let mut position = reader.position() as usize;
while (position) < len {
let old_position = position;
let remove = parse_single_extra_field(file, &mut reader, position as u64, false)?;
position = reader.position() as usize;
if remove {
let remaining = len - (position - old_position);
if remaining == 0 {
return Ok(None);
}
let mut new_extra_field = Vec::with_capacity(remaining);
new_extra_field.extend_from_slice(&extra_field[0..old_position]);
new_extra_field.extend_from_slice(&extra_field[position..]);
processed_extra_field = Arc::new(new_extra_field);
}
}
Ok(Some(processed_extra_field))
}
pub(crate) fn parse_single_extra_field<R: Read>(
file: &mut ZipFileData,
reader: &mut R,
bytes_already_read: u64,
disallow_zip64: bool,
) -> ZipResult<bool> {
let kind = reader.read_u16_le()?;
let len = reader.read_u16_le()?;
match kind {
0x0001 => {
if disallow_zip64 {
return Err(InvalidArchive(
"Can't write a custom field using the ZIP64 ID",
));
}
file.large_file = true;
let mut consumed_len = 0;
if len >= 24 || file.uncompressed_size == spec::ZIP64_BYTES_THR {
file.uncompressed_size = reader.read_u64_le()?;
consumed_len += size_of::<u64>();
}
if len >= 24 || file.compressed_size == spec::ZIP64_BYTES_THR {
file.compressed_size = reader.read_u64_le()?;
consumed_len += size_of::<u64>();
}
if len >= 24 || file.header_start == spec::ZIP64_BYTES_THR {
file.header_start = reader.read_u64_le()?;
consumed_len += size_of::<u64>();
}
let Some(leftover_len) = (len as usize).checked_sub(consumed_len) else {
return Err(InvalidArchive("ZIP64 extra-data field is the wrong length"));
};
reader.read_exact(&mut vec![0u8; leftover_len])?;
return Ok(true);
}
0x9901 => {
if len != 7 {
return Err(ZipError::UnsupportedArchive(
"AES extra data field has an unsupported length",
));
}
let vendor_version = reader.read_u16_le()?;
let vendor_id = reader.read_u16_le()?;
let mut out = [0u8];
reader.read_exact(&mut out)?;
let aes_mode = out[0];
let compression_method = CompressionMethod::parse_from_u16(reader.read_u16_le()?);
if vendor_id != 0x4541 {
return Err(InvalidArchive("Invalid AES vendor"));
}
let vendor_version = match vendor_version {
0x0001 => AesVendorVersion::Ae1,
0x0002 => AesVendorVersion::Ae2,
_ => return Err(InvalidArchive("Invalid AES vendor version")),
};
match aes_mode {
0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method)),
0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method)),
0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method)),
_ => return Err(InvalidArchive("Invalid AES encryption strength")),
};
file.compression_method = compression_method;
file.aes_extra_data_start = bytes_already_read;
}
0x5455 => {
file.extra_fields.push(ExtraField::ExtendedTimestamp(
ExtendedTimestamp::try_from_reader(reader, len)?,
));
}
0x6375 => {
file.file_comment = String::from_utf8(
UnicodeExtraField::try_from_reader(reader, len)?
.unwrap_valid(file.file_comment.as_bytes())?
.into_vec(),
)?
.into();
}
0x7075 => {
file.file_name_raw = UnicodeExtraField::try_from_reader(reader, len)?
.unwrap_valid(&file.file_name_raw)?;
file.file_name =
String::from_utf8(file.file_name_raw.clone().into_vec())?.into_boxed_str();
file.is_utf8 = true;
}
_ => {
reader.read_exact(&mut vec![0u8; len as usize])?;
}
}
Ok(false)
}
pub trait HasZipMetadata {
fn get_metadata(&self) -> &ZipFileData;
}
impl<'a> ZipFile<'a> {
pub(crate) fn take_raw_reader(&mut self) -> io::Result<io::Take<&'a mut dyn Read>> {
mem::replace(&mut self.reader, ZipFileReader::NoReader).into_inner()
}
pub fn version_made_by(&self) -> (u8, u8) {
(
self.get_metadata().version_made_by / 10,
self.get_metadata().version_made_by % 10,
)
}
pub fn name(&self) -> &str {
&self.get_metadata().file_name
}
pub fn name_raw(&self) -> &[u8] {
&self.get_metadata().file_name_raw
}
#[deprecated(
since = "0.5.7",
note = "by stripping `..`s from the path, the meaning of paths can change.
`mangled_name` can be used if this behaviour is desirable"
)]
pub fn sanitized_name(&self) -> PathBuf {
self.mangled_name()
}
pub fn mangled_name(&self) -> PathBuf {
self.get_metadata().file_name_sanitized()
}
pub fn enclosed_name(&self) -> Option<PathBuf> {
self.get_metadata().enclosed_name()
}
pub fn comment(&self) -> &str {
&self.get_metadata().file_comment
}
pub fn compression(&self) -> CompressionMethod {
self.get_metadata().compression_method
}
pub fn encrypted(&self) -> bool {
self.data.encrypted
}
pub fn compressed_size(&self) -> u64 {
self.get_metadata().compressed_size
}
pub fn size(&self) -> u64 {
self.get_metadata().uncompressed_size
}
pub fn last_modified(&self) -> Option<DateTime> {
self.data.last_modified_time
}
pub fn is_dir(&self) -> bool {
is_dir(self.name())
}
pub fn is_symlink(&self) -> bool {
self.unix_mode()
.is_some_and(|mode| mode & S_IFLNK == S_IFLNK)
}
pub fn is_file(&self) -> bool {
!self.is_dir() && !self.is_symlink()
}
pub fn unix_mode(&self) -> Option<u32> {
self.get_metadata().unix_mode()
}
pub fn crc32(&self) -> u32 {
self.get_metadata().crc32
}
pub fn extra_data(&self) -> Option<&[u8]> {
self.get_metadata()
.extra_field
.as_ref()
.map(|v| v.deref().deref())
}
pub fn data_start(&self) -> u64 {
*self.data.data_start.get().unwrap()
}
pub fn header_start(&self) -> u64 {
self.get_metadata().header_start
}
pub fn central_header_start(&self) -> u64 {
self.get_metadata().central_header_start
}
}
impl<'a> ZipFile<'a> {
pub fn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> {
self.data.extra_fields.iter()
}
}
impl<'a> HasZipMetadata for ZipFile<'a> {
fn get_metadata(&self) -> &ZipFileData {
self.data.as_ref()
}
}
impl<'a> Read for ZipFile<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
self.reader.read_exact(buf)
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
self.reader.read_to_end(buf)
}
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
self.reader.read_to_string(buf)
}
}
impl<'a, R: Read> Read for ZipFileSeek<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &mut self.reader {
ZipFileSeekReader::Raw(r) => r.read(buf),
}
}
}
impl<'a, R: Seek> Seek for ZipFileSeek<'a, R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match &mut self.reader {
ZipFileSeekReader::Raw(r) => r.seek(pos),
}
}
}
impl<'a, R> HasZipMetadata for ZipFileSeek<'a, R> {
fn get_metadata(&self) -> &ZipFileData {
self.data.as_ref()
}
}
impl<'a> Drop for ZipFile<'a> {
fn drop(&mut self) {
if let Cow::Owned(_) = self.data {
if let Ok(mut inner) = self.take_raw_reader() {
let _ = copy(&mut inner, &mut sink());
}
}
}
}
pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Option<ZipFile<'_>>> {
let mut block = ZipLocalEntryBlock::zeroed();
reader.read_exact(block.as_bytes_mut())?;
match block.magic().from_le() {
spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
_ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR),
}
let block = block.from_le();
let mut result = ZipFileData::from_local_block(block, reader)?;
match parse_extra_field(&mut result) {
Ok(..) | Err(ZipError::Io(..)) => {}
Err(e) => return Err(e),
}
let limit_reader = (reader as &'a mut dyn Read).take(result.compressed_size);
let result_crc32 = result.crc32;
let result_compression_method = result.compression_method;
let crypto_reader = make_crypto_reader(&result, limit_reader, None, None)?;
Ok(Some(ZipFile {
data: Cow::Owned(result),
reader: make_reader(result_compression_method, result_crc32, crypto_reader)?,
}))
}
#[cfg(test)]
mod test {
use crate::result::ZipResult;
use crate::write::SimpleFileOptions;
use crate::CompressionMethod::Stored;
use crate::{ZipArchive, ZipWriter};
use std::io::{Cursor, Read, Write};
use tempdir::TempDir;
#[test]
fn invalid_offset() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/invalid_offset.zip"));
let reader = ZipArchive::new(Cursor::new(v));
assert!(reader.is_err());
}
#[test]
fn invalid_offset2() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/invalid_offset2.zip"));
let reader = ZipArchive::new(Cursor::new(v));
assert!(reader.is_err());
}
#[test]
fn zip64_with_leading_junk() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip"));
let reader = ZipArchive::new(Cursor::new(v)).unwrap();
assert_eq!(reader.len(), 1);
}
#[test]
fn zip_contents() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
assert_eq!(reader.comment(), b"");
assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
}
#[test]
fn zip_read_streaming() {
use super::read_zipfile_from_stream;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
let mut reader = Cursor::new(v);
loop {
if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
break;
}
}
}
#[test]
fn zip_clone() {
use super::ZipArchive;
use std::io::Read;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
let mut reader1 = ZipArchive::new(Cursor::new(v)).unwrap();
let mut reader2 = reader1.clone();
let mut file1 = reader1.by_index(0).unwrap();
let mut file2 = reader2.by_index(0).unwrap();
let t = file1.last_modified().unwrap();
assert_eq!(
(
t.year(),
t.month(),
t.day(),
t.hour(),
t.minute(),
t.second()
),
(1980, 1, 1, 0, 0, 0)
);
let mut buf1 = [0; 5];
let mut buf2 = [0; 5];
let mut buf3 = [0; 5];
let mut buf4 = [0; 5];
file1.read_exact(&mut buf1).unwrap();
file2.read_exact(&mut buf2).unwrap();
file1.read_exact(&mut buf3).unwrap();
file2.read_exact(&mut buf4).unwrap();
assert_eq!(buf1, buf2);
assert_eq!(buf3, buf4);
assert_ne!(buf1, buf3);
}
#[test]
fn file_and_dir_predicates() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip"));
let mut zip = ZipArchive::new(Cursor::new(v)).unwrap();
for i in 0..zip.len() {
let zip_file = zip.by_index(i).unwrap();
let full_name = zip_file.enclosed_name().unwrap();
let file_name = full_name.file_name().unwrap().to_str().unwrap();
assert!(
(file_name.starts_with("dir") && zip_file.is_dir())
|| (file_name.starts_with("file") && zip_file.is_file())
);
}
}
#[test]
fn zip64_magic_in_filenames() {
let files = vec![
include_bytes!("../tests/data/zip64_magic_in_filename_1.zip").to_vec(),
include_bytes!("../tests/data/zip64_magic_in_filename_2.zip").to_vec(),
include_bytes!("../tests/data/zip64_magic_in_filename_3.zip").to_vec(),
include_bytes!("../tests/data/zip64_magic_in_filename_4.zip").to_vec(),
include_bytes!("../tests/data/zip64_magic_in_filename_5.zip").to_vec(),
];
for file in files {
ZipArchive::new(Cursor::new(file)).unwrap();
}
}
#[test]
fn invalid_cde_number_of_files_allocation_smaller_offset() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!(
"../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
));
let reader = ZipArchive::new(Cursor::new(v));
assert!(reader.is_err() || reader.unwrap().is_empty());
}
#[test]
fn invalid_cde_number_of_files_allocation_greater_offset() {
use super::ZipArchive;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!(
"../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
));
let reader = ZipArchive::new(Cursor::new(v));
assert!(reader.is_err());
}
#[cfg(feature = "deflate64")]
#[test]
fn deflate64_index_out_of_bounds() -> std::io::Result<()> {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!(
"../tests/data/raw_deflate64_index_out_of_bounds.zip"
));
let mut reader = ZipArchive::new(Cursor::new(v))?;
std::io::copy(&mut reader.by_index(0)?, &mut std::io::sink()).expect_err("Invalid file");
Ok(())
}
#[cfg(feature = "deflate64")]
#[test]
fn deflate64_not_enough_space() {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/deflate64_issue_25.zip"));
ZipArchive::new(Cursor::new(v)).expect_err("Invalid file");
}
#[cfg(feature = "_deflate-any")]
#[test]
fn test_read_with_data_descriptor() {
use std::io::Read;
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/data_descriptor.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
let mut decompressed = [0u8; 16];
let mut file = reader.by_index(0).unwrap();
assert_eq!(file.read(&mut decompressed).unwrap(), 12);
}
#[test]
fn test_is_symlink() -> std::io::Result<()> {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/symlink.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
assert!(reader.by_index(0).unwrap().is_symlink());
let tempdir = TempDir::new("test_is_symlink")?;
reader.extract(&tempdir).unwrap();
assert!(tempdir.path().join("bar").is_symlink());
Ok(())
}
#[test]
#[cfg(feature = "_deflate-any")]
fn test_utf8_extra_field() {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/chinese.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
reader.by_name("七个房间.txt").unwrap();
}
#[test]
fn test_utf8() {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/linux-7z.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
reader.by_name("你好.txt").unwrap();
}
#[test]
fn test_utf8_2() {
let mut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/windows-7zip.zip"));
let mut reader = ZipArchive::new(Cursor::new(v)).unwrap();
reader.by_name("你好.txt").unwrap();
}
#[test]
fn test_64k_files() -> ZipResult<()> {
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
let options = SimpleFileOptions {
compression_method: Stored,
..Default::default()
};
for i in 0..=u16::MAX {
let file_name = format!("{i}.txt");
writer.start_file(&*file_name, options)?;
writer.write_all(i.to_string().as_bytes())?;
}
let mut reader = ZipArchive::new(writer.finish()?)?;
for i in 0..=u16::MAX {
let expected_name = format!("{i}.txt");
let expected_contents = i.to_string();
let expected_contents = expected_contents.as_bytes();
let mut file = reader.by_name(&expected_name)?;
let mut contents = Vec::with_capacity(expected_contents.len());
file.read_to_end(&mut contents)?;
assert_eq!(contents, expected_contents);
drop(file);
contents.clear();
let mut file = reader.by_index(i as usize)?;
file.read_to_end(&mut contents)?;
assert_eq!(contents, expected_contents);
}
Ok(())
}
}