#[cfg(feature = "aes-crypto")] usecrate::aes::{AesReader, AesReaderValid}; usecrate::compression::CompressionMethod; usecrate::cp437::FromCp437; usecrate::crc32::Crc32Reader; usecrate::extra_fields::{ExtendedTimestamp, ExtraField}; usecrate::read::zip_archive::Shared; usecrate::result::{ZipError, ZipResult}; usecrate::spec::{self, FixedSizeBlock}; usecrate::types::{
AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
ZipLocalEntryBlock,
}; usecrate::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}; use std::mem; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock};
#[cfg(feature = "deflate-flate2")] use flate2::read::DeflateDecoder;
#[cfg(feature = "deflate64")] use deflate64::Deflate64Decoder;
#[cfg(feature = "bzip2")] use bzip2::read::BzDecoder;
#[cfg(feature = "zstd")] use zstd::stream::read::Decoder as ZstdDecoder;
mod config;
pubuse config::*;
/// Provides high level API for reading from a stream. pub(crate) mod stream;
#[cfg(feature = "lzma")] pub(crate) mod lzma;
// Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely pub(crate) mod zip_archive { use std::sync::Arc;
/// Extract immutable data from `ZipArchive` to make it cheap to clone #[derive(Debug)] pub(crate) struct Shared { pub(crate) files: super::IndexMap<Box<str>, super::ZipFileData>, pub(super) offset: u64, pub(super) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. #[allow(dead_code)] pub(super) config: super::Config,
}
/// ZIP archive reader /// /// At the moment, this type is cheap to clone if this is the case for the /// reader it uses. However, this is not guaranteed by this crate and it may /// change in the future. /// /// ```no_run /// use std::io::prelude::*; /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> { /// let mut zip = zip::ZipArchive::new(reader)?; /// /// for i in 0..zip.len() { /// let mut file = zip.by_index(i)?; /// println!("Filename: {}", file.name()); /// std::io::copy(&mut file, &mut std::io::stdout())?; /// } /// /// Ok(()) /// } /// ``` #[derive(Clone, Debug)] pubstruct ZipArchive<R> { pub(super) reader: R, pub(super) shared: Arc<Shared>, pub(super) comment: Arc<[u8]>,
}
}
impl<'a> ZipFileReader<'a> { /// Consumes this decoder, returning the underlying reader. pubfn drain(self) { letmut inner = matchself {
ZipFileReader::NoReader => panic!("ZipFileReader was in an invalid state"),
ZipFileReader::Raw(r) => r,
ZipFileReader::Stored(r) => r.into_inner().into_inner(), #[cfg(feature = "_deflate-any")]
ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(), #[cfg(feature = "deflate64")]
ZipFileReader::Deflate64(r) => r.into_inner().into_inner().into_inner().into_inner(), #[cfg(feature = "bzip2")]
ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(), #[cfg(feature = "zstd")]
ZipFileReader::Zstd(r) => r.into_inner().finish().into_inner().into_inner(), #[cfg(feature = "lzma")]
ZipFileReader::Lzma(r) => { // Lzma reader owns its buffer rather than mutably borrowing it, so we have to drop // it separately iflet Ok(mut remaining) = r.into_inner().finish() { let _ = copy(&mut remaining, &mut sink());
} return;
}
}; let _ = copy(&mut inner, &mut sink());
}
}
/// A struct for reading a zip file pubstruct ZipFile<'a> { pub(crate) data: Cow<'a, ZipFileData>, pub(crate) crypto_reader: Option<CryptoReader<'a>>, pub(crate) reader: ZipFileReader<'a>,
}
pub(crate) fn find_content<'a>(
data: &ZipFileData,
reader: &'a mut (impl Read + Seek),
) -> ZipResult<io::Take<&'a mut dyn Read>> { // TODO: use .get_or_try_init() once stabilized to provide a closure returning a Result! let data_start = match data.data_start.get() {
Some(data_start) => *data_start,
None => { // Go to start of data.
reader.seek(io::SeekFrom::Start(data.header_start))?;
// Parse static-sized fields and check the magic value. let block = ZipLocalEntryBlock::parse(reader)?;
// Calculate the end of the local header from the fields we just parsed. let variable_fields_len = // Each of these fields must be converted to u64 before adding, as the result may // easily overflow a u16.
block.file_name_length as u64 + block.extra_field_length as u64; let data_start = data.header_start
+ mem::size_of::<ZipLocalEntryBlock>() as u64
+ variable_fields_len; // Set the value so we don't have to read it again. match data.data_start.set(data_start) {
Ok(()) => (), // If the value was already set in the meantime, ensure it matches (this is probably // unnecessary).
Err(_) => {
assert_eq!(*data.data_start.get().unwrap(), data_start);
}
}
data_start
}
};
reader.seek(io::SeekFrom::Start(data_start))?;
Ok((reader as &mutdyn Read).take(data.compressed_size))
}
/// Total size of the files in the archive, if it can be known. Doesn't include directories or /// metadata. pubfn decompressed_size(&self) -> Option<u128> { letmut total = 0u128; for file inself.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 + io::Seek>(
&mutself, mut w: W,
) -> ZipResult<IndexMap<Box<str>, ZipFileData>> { ifself.shared.files.is_empty() { return Ok(IndexMap::new());
} letmut new_files = self.shared.files.clone(); /* The first file header will probably start at the beginning of the file, but zip doesn't *enforcethat,andexecutablezipslikePEXfileswillhaveashebanglinesowill *definitelybegreaterthan0. * *assert_eq!(0,new_files[0].header_start);// Avoid this.
*/
let new_initial_header_start = w.stream_position()?; /* Push back file header starts for all entries in the covered files. */
new_files.values_mut().try_for_each(|f| { /* This is probably the only really important thing to change. */
f.header_start = f.header_start.checked_add(new_initial_header_start).ok_or(
ZipError::InvalidArchive("new header start from merge would have been too large"),
)?; /* This is only ever used internally to cache metadata lookups (it's not part of the
* zip spec), and 0 is the sentinel value. */
f.central_header_start = 0; /* This is an atomic variable so it can be updated from another thread in the
* implementation (which is good!). */ iflet Some(old_data_start) = f.data_start.take() { let new_data_start = old_data_start.checked_add(new_initial_header_start).ok_or(
ZipError::InvalidArchive("new data start from merge would have been too large"),
)?;
f.data_start.get_or_init(|| new_data_start);
}
Ok::<_, ZipError>(())
})?;
/* Rewind to the beginning of the file. * *NB:we*could*decidetostartcopyingfromnew_files[0].header_startinstead,which *wouldavoidcopyingovere.g.anypexshebangsorotherfilecontentsthatstartbefore *thefirstzipfileentry.However,zipfilesactuallyshouldn'tcareaboutgarbagedata *in*between*realentries,sincethecentraldirectoryheaderrecordsthecorrectstart *locationofeach,andkeepingtrackofthatmathismorecomplicatedlogicthatwillonly *rarelybeused,sincemostzipsthatgetmergedtogetherarelikelytobeproduced *specificallyforthatpurpose(andthereforeareunlikelytohaveashebangorother *preface).Finally,thispreservesanydatathatmightactuallybeuseful.
*/ self.reader.rewind()?; /* Find the end of the file data. */ let length_to_read = self.shared.dir_start; /* Produce a Read that reads bytes up until the start of the central directory header. *This"as&mutdynRead"trickisusedelsewheretoavoidhavingtoclonetheunderlying
* handle, which it really shouldn't need to anyway. */ letmut limited_raw = (&mutself.reader as &>mutdyn Read).take(length_to_read); /* Copy over file data from source archive directly. */
io::copy(&mut limited_raw, &mut w)?;
/* Return the files we've just written to the data stream. */
Ok(new_files)
}
fn get_directory_info_zip32(
config: &Config,
reader: &mut R,
footer: &spec::Zip32CentralDirectoryEnd,
cde_start_pos: u64,
) -> ZipResult<CentralDirectoryInfo> { let archive_offset = match config.archive_offset {
ArchiveOffset::Known(n) => n,
ArchiveOffset::FromCentralDirectory | ArchiveOffset::Detect => { // Some zip files have data prepended to them, resulting in the // offsets all being too small. Get the amount of error by comparing // the actual file position we found the CDE at with the offset // recorded in the CDE. letmut 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(ZipError::InvalidArchive( "Invalid central directory size or offset",
))?;
if config.archive_offset == ArchiveOffset::Detect { // Check whether the archive offset makes sense by peeking at the directory start. If it // doesn't, fall back to using no archive offset. This supports zips with the central // directory entries somewhere other than directly preceding the end of central directory.
reader.seek(io::SeekFrom::Start(
offset + footer.central_directory_offset as u64,
))?; letmut 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,
})
}
constfn 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,
footer: &spec::Zip32CentralDirectoryEnd,
cde_start_pos: u64,
) -> ZipResult<Vec<ZipResult<CentralDirectoryInfo>>> { // See if there's a ZIP64 footer. The ZIP64 locator if present will // have its signature 20 bytes in front of the standard footer. The // standard footer, in turn, is 22+N bytes large, where N is the // comment length. Therefore: /* TODO: compute this from constant sizes and offsets! */
reader.seek(io::SeekFrom::End(
-(20 + 22 + footer.zip_file_comment.len() as i64),
))?; let locator64 = spec::Zip64CentralDirectoryEndLocator::parse(reader)?;
// We need to reassess `archive_offset`. We know where the ZIP64 // central-directory-end structure *should* be, but unfortunately we // don't know how to precisely relate that location to our current // actual offset in the file, since there may be junk at its // beginning. Therefore we need to perform another search, as in // read::Zip32CentralDirectoryEnd::find_and_parse, except now we search // forward. There may be multiple results because of Zip64 central-directory signatures in // ZIP comment data.
let search_upper_bound = cde_start_pos
.checked_sub(Self::zip64_cde_len() as u64)
.ok_or(ZipError::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 = spec::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| { // Check whether the archive offset makes sense by peeking at the directory start. // // If any errors occur or no header signature is found, fall back to no offset to see if that works.
reader.seek(io::SeekFrom::Start(start)).ok()?; letmut 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(ZipError::InvalidArchive( "Invalid central directory size or offset",
))?; if directory_start > search_upper_bound {
Err(ZipError::InvalidArchive( "Invalid central directory size or offset",
))
} elseif footer64.number_of_files_on_this_disk > footer64.number_of_files {
Err(ZipError::InvalidArchive( "ZIP64 footer indicates more files on this disk than in the whole archive",
))
} elseif footer64.version_needed_to_extract > footer64.version_made_by {
Err(ZipError::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,
})
}
}).collect();
Ok(results)
}
/// Get the directory start offset and number of files. This is done in a /// separate function to ease the control flow design. pub(crate) fn get_metadata(
config: Config,
reader: &mut R,
footer: &spec::Zip32CentralDirectoryEnd,
cde_start_pos: u64,
) -> ZipResult<Shared> { // Check if file has a zip64 footer letmut results = Self::get_directory_info_zip64(&config, reader, footer, cde_start_pos)
.unwrap_or_else(|e| vec![Err(e)]); let zip32_result = Self::get_directory_info_zip32(&config, reader, footer, cde_start_pos); letmut invalid_errors = Vec::new(); letmut unsupported_errors = Vec::new(); letmut ok_results = Vec::new();
results.iter_mut().for_each(|result| { iflet Ok(central_dir) = result { iflet Ok(zip32_central_dir) = &zip32_result { // Both zip32 and zip64 footers exist, so check if the zip64 footer is valid; if not, try zip32 if central_dir.number_of_files != zip32_central_dir.number_of_files
&& zip32_central_dir.number_of_files != u16::MAX as usize
{
*result = Err(ZipError::InvalidArchive( "ZIP32 and ZIP64 file counts don't match",
)); return;
} if central_dir.disk_number != zip32_central_dir.disk_number
&& zip32_central_dir.disk_number != u16::MAX as u32
{
*result = Err(ZipError::InvalidArchive( "ZIP32 and ZIP64 disk numbers don't match",
)); return;
} if central_dir.disk_with_central_directory
!= zip32_central_dir.disk_with_central_directory
&& zip32_central_dir.disk_with_central_directory != u16::MAX as u32
{
*result = Err(ZipError::InvalidArchive( "ZIP32 and ZIP64 last-disk numbers don't match",
));
}
}
}
});
results.push(zip32_result);
results
.into_iter()
.map(|result| {
result.and_then(|dir_info| { // If the parsed number of files is greater than the offset then // something fishy is going on and we shouldn't trust number_of_files. let file_capacity = if dir_info.number_of_files > dir_info.directory_start as usize { 0
} else {
dir_info.number_of_files
}; letmut files = IndexMap::with_capacity(file_capacity);
reader.seek(io::SeekFrom::Start(dir_info.directory_start))?; for _ in0..dir_info.number_of_files { let file = central_header_to_zip_file(reader, dir_info.archive_offset)?;
files.insert(file.file_name.clone(), file);
} if dir_info.disk_number != dir_info.disk_with_central_directory {
unsupported_zip_error("Support for multi-disk files is not implemented")
} else {
Ok(Shared {
files,
offset: dir_info.archive_offset,
dir_start: dir_info.directory_start,
config,
})
}
})
})
.for_each(|result| match result {
Err(ZipError::UnsupportedArchive(e)) => {
unsupported_errors.push(ZipError::UnsupportedArchive(e))
}
Err(e) => invalid_errors.push(e),
Ok(o) => ok_results.push(o),
}); if ok_results.is_empty() { return Err(unsupported_errors
.into_iter()
.next()
.unwrap_or_else(|| invalid_errors.into_iter().next().unwrap()));
} let shared = ok_results
.into_iter()
.max_by_key(|shared| shared.dir_start)
.unwrap();
reader.seek(io::SeekFrom::Start(shared.dir_start))?;
Ok(shared)
}
/// Returns the verification value and salt for the AES encryption of the file /// /// It fails if the file number is invalid. /// /// # Returns /// /// - None if the file is not encrypted with AES #[cfg(feature = "aes-crypto")] pubfn get_aes_verification_key_and_salt(
&mutself,
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, &mutself.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))
}
}
}
/// Read a ZIP archive, collecting the files it contains. /// /// This uses the central directory record of the ZIP file, and ignores local file headers. /// /// A default [`Config`] is used. pubfn new(reader: R) -> ZipResult<ZipArchive<R>> { Self::with_config(Default::default(), reader)
}
/// Read a ZIP archive providing a read configuration, collecting the files it contains. /// /// This uses the central directory record of the ZIP file, and ignores local file headers. pubfn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> { let (footer, cde_start_pos) = spec::Zip32CentralDirectoryEnd::find_and_parse(&yle='color:red'>mut reader)?; let shared = Self::get_metadata(config, &mut reader, &footer, cde_start_pos)?;
Ok(ZipArchive {
reader,
shared: shared.into(),
comment: footer.zip_file_comment.into(),
})
}
/// Extract a Zip archive into a directory, overwriting files if they /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. /// /// Extraction is not atomic. If an error is encountered, some of the files /// may be left on disk. However, on Unix targets, no newly-created directories with part but /// not all of their contents extracted will be readable, writable or usable as process working /// directories by any non-root user except you. /// /// On Unix and Windows, symbolic links are extracted correctly. On other platforms such as /// WebAssembly, symbolic links aren't supported, so they're extracted as normal files /// containing the target path in UTF-8. pubfn extract<P: AsRef<Path>>(&mutself, directory: P) -> ZipResult<()> { use std::fs; #[cfg(unix)] letmut files_by_unix_mode = Vec::new(); for i in0..self.len() { letmut file = self.by_index(i)?; let filepath = file
.enclosed_name()
.ok_or(ZipError::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)) { letmut target = Vec::with_capacity(file.size() as usize);
file.read_exact(&mut target)?;
Some(target)
} else {
None
};
drop(file); iflet Some(p) = outpath.parent() { Self::make_writable_dir_all(p)?;
} iflet Some(target) = symlink_target { #[cfg(unix)]
{ use std::os::unix::ffi::OsStringExt; let target = OsString::from_vec(target); let target_path = directory.as_ref().join(target);
std::os::unix::fs::symlink(target_path, 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
} elseiflet 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;
} letmut file = self.by_index(i)?; letmut outfile = fs::File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?; #[cfg(unix)]
{ // Check for real permissions, which we'll set in a second pass iflet 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 { // Ensure we update children's permissions before making a parent unwritable
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)]
{ // Dirs must be writable until all normal files are extracted 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(())
}
/// Number of files contained in this zip. pubfn len(&self) -> usize { self.shared.files.len()
}
/// Whether this zip archive contains no files pubfn is_empty(&self) -> bool { self.len() == 0
}
/// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes. /// /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size /// of that prepended data. pubfn offset(&self) -> u64 { self.shared.offset
}
/// Get the comment of the zip archive. pubfn comment(&self) -> &[u8] {
&self.comment
}
/// Returns an iterator over all the file and directory names in this archive. pubfn file_names(&self) -> impl Iterator<Item = &str> { self.shared.files.keys().map(|s| s.as_ref())
}
/// Search for a file entry by name, decrypt with given password /// /// # Warning /// /// The implementation of the cryptographic algorithms has not /// gone through a correctness review, and you should assume it is insecure: /// passwords used with this API may be compromised. /// /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us /// to check for a 1/256 chance that the password is correct. /// There are many passwords out there that will also pass the validity checks /// we are able to perform. This is a weakness of the ZipCrypto algorithm, /// due to its fairly primitive approach to cryptography. pubfn by_name_decrypt(&mutself, name: &str, password: &[u8]) -> ZipResult<ZipFile> { self.by_name_with_optional_password(name, Some(password))
}
/// Search for a file entry by name pubfn by_name(&mutself, name: &str) -> ZipResult<ZipFile> { self.by_name_with_optional_password(name, None)
}
/// Get the index of a file entry by name, if it's present. #[inline(always)] pubfn index_for_name(&self, name: &str) -> Option<usize> { self.shared.files.get_index_of(name)
}
/// Get the index of a file entry by path, if it's present. #[inline(always)] pubfn index_for_path<T: AsRef<Path>>(&self, path: T) -> Option<usize> { self.index_for_name(&path_to_string(path))
}
/// Get the name of a file entry, if it's present. #[inline(always)] pubfn name_for_index(&self, index: usize) -> Option<&str> { self.shared
.files
.get_index(index)
.map(|(name, _)| name.as_ref())
}
/// Get a contained file by index, decrypt with given password /// /// # Warning /// /// The implementation of the cryptographic algorithms has not /// gone through a correctness review, and you should assume it is insecure: /// passwords used with this API may be compromised. /// /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us /// to check for a 1/256 chance that the password is correct. /// There are many passwords out there that will also pass the validity checks /// we are able to perform. This is a weakness of the ZipCrypto algorithm, /// due to its fairly primitive approach to cryptography. pubfn by_index_decrypt(
&mutself,
file_number: usize,
password: &[u8],
) -> ZipResult<ZipFile<'_>> { self.by_index_with_optional_password(file_number, Some(password))
}
/// Get a contained file by index pubfn by_index(&mutself, file_number: usize) -> ZipResult<ZipFile<'_>> { self.by_index_with_optional_password(file_number, None)
}
/// Get a contained file by index without decompressing it pubfn by_index_raw(&mutself, file_number: usize) -> ZipResult<ZipFile<'_>> { let reader = &mutself.reader; let (_, data) = self
.shared
.files
.get_index(file_number)
.ok_or(ZipError::FileNotFound)?;
Ok(ZipFile {
crypto_reader: None,
reader: ZipFileReader::Raw(find_content(data, reader)?),
data: Cow::Borrowed(data),
})
}
/// Unwrap and return the inner reader object /// /// The position of the reader is undefined. pubfn into_inner(self) -> R { self.reader
}
}
/// Holds the AES information of a file in the zip archive #[derive(Debug)] #[cfg(feature = "aes-crypto")] pubstruct AesInfo { /// The AES encryption mode pub aes_mode: AesMode, /// The verification key pub verification_value: [u8; PWD_VERIFY_LENGTH], /// The salt pub salt: Vec<u8>,
}
/// Parse a central directory entry to collect the information for the file. 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()?;
// Parse central header let block = ZipCentralEntryBlock::parse(reader)?;
central_header_to_zip_file_inner(reader, archive_offset, central_header_start, block)
}
/// Parse a central directory entry to collect the information for the file. 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 { // magic,
version_made_by, // version_to_extract,
flags,
compression_method,
last_mod_time,
last_mod_date,
crc32,
compressed_size,
uncompressed_size,
file_name_length,
extra_field_length,
file_comment_length, // disk_number, // internal_file_attributes,
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 asusize)?;
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(),
};
// Construct the result letmut result = ZipFileData {
system: System::from((version_made_by >> 8) as u8), /* NB: this strips the top 8 bits! */
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(),
};
let aes_enabled = result.compression_method == CompressionMethod::AES; if aes_enabled && result.aes_mode.is_none() { return Err(ZipError::InvalidArchive( "AES encryption without AES extra data field",
));
}
// Account for shifted zip offsets.
result.header_start = result
.header_start
.checked_add(archive_offset)
.ok_or(ZipError::InvalidArchive("Archive header is too large"))?;
/* TODO: codify this structure into Zip64ExtraFieldBlock fields! */ while (reader.position() as usize) < extra_field.len() { let kind = reader.read_u16_le()?; let len = reader.read_u16_le()?; letmut len_left = len as i64; match kind { // Zip64 extended information extra field 0x0001 => { if file.uncompressed_size == spec::ZIP64_BYTES_THR {
file.large_file = true;
file.uncompressed_size = reader.read_u64_le()?;
len_left -= 8;
} if file.compressed_size == spec::ZIP64_BYTES_THR {
file.large_file = true;
file.compressed_size = reader.read_u64_le()?;
len_left -= 8;
} if file.header_start == spec::ZIP64_BYTES_THR {
file.header_start = reader.read_u64_le()?;
len_left -= 8;
}
} 0x9901 => { // AES 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()?; letmut out = [0u8];
reader.read_exact(&mut out)?; let aes_mode = out[0]; let compression_method = CompressionMethod::parse_from_u16(reader.read_u16_le()?);
/// Get the version of the file pubfn version_made_by(&self) -> (u8, u8) {
( self.data.version_made_by / 10, self.data.version_made_by % 10,
)
}
/// Get the name of the file /// /// # Warnings /// /// It is dangerous to use this name directly when extracting an archive. /// It may contain an absolute path (`/etc/shadow`), or break out of the /// current directory (`../runtime`). Carelessly writing to these paths /// allows an attacker to craft a ZIP archive that will overwrite critical /// files. /// /// You can use the [`ZipFile::enclosed_name`] method to validate the name /// as a safe path. pubfn name(&self) -> &str {
&self.data.file_name
}
/// Get the name of the file, in the raw (internal) byte representation. /// /// The encoding of this data is currently undefined. pubfn name_raw(&self) -> &[u8] {
&self.data.file_name_raw
}
/// Get the name of the file in a sanitized form. It truncates the name to the first NULL byte, /// removes a leading '/' and removes '..' parts. #[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"
)] pubfn sanitized_name(&self) -> PathBuf { self.mangled_name()
}
/// Rewrite the path, ignoring any path components with special meaning. /// /// - Absolute paths are made relative /// - [`ParentDir`]s are ignored /// - Truncates the filename at a NULL byte /// /// This is appropriate if you need to be able to extract *something* from /// any archive, but will easily misrepresent trivial paths like /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this, /// [`ZipFile::enclosed_name`] is the better option in most scenarios. /// /// [`ParentDir`]: `Component::ParentDir` pubfn mangled_name(&self) -> PathBuf { self.data.file_name_sanitized()
}
/// Ensure the file path is safe to use as a [`Path`]. /// /// - It can't contain NULL bytes /// - It can't resolve to a path outside the current directory /// > `foo/../bar` is fine, `foo/../../bar` is not. /// - It can't be an absolute path /// /// This will read well-formed ZIP files correctly, and is resistant /// to path-based exploits. It is recommended over /// [`ZipFile::mangled_name`]. pubfn enclosed_name(&self) -> Option<PathBuf> { self.data.enclosed_name()
}
/// Get the comment of the file pubfn comment(&self) -> &str {
&self.data.file_comment
}
/// Get the compression method used to store the file pubfn compression(&self) -> CompressionMethod { self.data.compression_method
}
/// Get the size of the file, in bytes, in the archive pubfn compressed_size(&self) -> u64 { self.data.compressed_size
}
/// Get the size of the file, in bytes, when uncompressed pubfn size(&self) -> u64 { self.data.uncompressed_size
}
/// Get the time the file was last modified pubfn last_modified(&self) -> Option<DateTime> { self.data.last_modified_time
} /// Returns whether the file is actually a directory pubfn is_dir(&self) -> bool {
is_dir(self.name())
}
/// Returns whether the file is actually a symbolic link pubfn is_symlink(&self) -> bool { self.unix_mode()
.is_some_and(|mode| mode & S_IFLNK == S_IFLNK)
}
/// Returns whether the file is a normal file (i.e. not a directory or symlink) pubfn is_file(&self) -> bool {
!self.is_dir() && !self.is_symlink()
}
/// Get unix mode for the file pubfn unix_mode(&self) -> Option<u32> { self.data.unix_mode()
}
/// Get the CRC32 hash of the original file pubfn crc32(&self) -> u32 { self.data.crc32
}
/// Get the extra data of the zip header for this file pubfn extra_data(&self) -> Option<&[u8]> { self.data.extra_field.as_ref().map(|v| v.deref().deref())
}
/// Get the starting offset of the data of the compressed file pubfn data_start(&self) -> u64 {
*self.data.data_start.get().unwrap_or(&0)
}
/// Get the starting offset of the zip header for this file pubfn header_start(&self) -> u64 { self.data.header_start
} /// Get the starting offset of the zip header in the central directory for this file pubfn central_header_start(&self) -> u64 { self.data.central_header_start
}
/// iterate through all extra fields pubfn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> { self.data.extra_fields.iter()
}
}
impl<'a> Drop for ZipFile<'a> { fn drop(&mutself) { // self.data is Owned, this reader is constructed by a streaming reader. // In this case, we want to exhaust the reader so that the next file is accessible. iflet Cow::Owned(_) = self.data { // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped. match &mutself.reader {
ZipFileReader::NoReader => { let innerreader = self.crypto_reader.take(); let _ = copy(
&mut innerreader.expect("Invalid reader state").into_inner(),
&mut sink(),
);
}
reader => { let innerreader = std::mem::replace(reader, ZipFileReader::NoReader);
innerreader.drain();
}
};
}
}
}
/// Read ZipFile structures from a non-seekable reader. /// /// This is an alternative method to read a zip file. If possible, use the ZipArchive functions /// as some information will be missing when reading this manner. /// /// Reads a file header from the start of the stream. Will return `Ok(Some(..))` if a file is /// present at the start of the stream. Returns `Ok(None)` if the start of the central directory /// is encountered. No more files should be read after this. /// /// The Drop implementation of ZipFile ensures that the reader will be correctly positioned after /// the structure is done. /// /// Missing fields are: /// * `comment`: set to an empty string /// * `data_start`: set to 0 /// * `external_attributes`: `unix_mode()`: will return None pubfn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult<Option<ZipFile<'_>>> { // We can't use the typical ::parse() method, as we follow separate code paths depending on the // "magic" value (since the magic value will be from the central directory header if we've // finished iterating over all the actual files). /* TODO: smallvec? */ letmut block = [0u8; mem::size_of::<ZipLocalEntryBlock>()];
reader.read_exact(&mut block)?; let block: Box<[u8]> = block.into();
let signature = spec::Magic::from_first_le_bytes(&block);
match signature {
spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (),
spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None),
_ => return Err(ZipError::InvalidArchive("Invalid local file header")),
}
let block = ZipLocalEntryBlock::interpret(&block)?;
letmut result = ZipFileData::from_local_block(block, reader)?;
letmut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip")); letmut zip = ZipArchive::new(Cursor::new(v)).unwrap();
for i in0..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(),
]; // Although we don't allow adding files whose names contain the ZIP64 CDB-end or // CDB-end-locator signatures, we still read them when they aren't genuinely ambiguous. for file in files {
ZipArchive::new(Cursor::new(file)).unwrap();
}
}
/// test case to ensure we don't preemptively over allocate based on the /// declared number of files in the CDE of an invalid zip when the number of /// files declared is more than the alleged offset in the CDE #[test] fn invalid_cde_number_of_files_allocation_smaller_offset() { usesuper::ZipArchive;
letmut 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 case to ensure we don't preemptively over allocate based on the /// declared number of files in the CDE of an invalid zip when the number of /// files declared is less than the alleged offset in the CDE #[test] fn invalid_cde_number_of_files_allocation_greater_offset() { usesuper::ZipArchive;
letmut 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());
}