#[cfg(feature = "aes-crypto")] usecrate::aes::{AesReader, AesReaderValid}; usecrate::compression::{CompressionMethod, Decompressor}; usecrate::cp437::FromCp437; usecrate::crc32::Crc32Reader; usecrate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs}; usecrate::read::zip_archive::{Shared, SharedBuilder}; usecrate::result::{ZipError, ZipResult}; usecrate::spec::{self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod}; usecrate::types::{
AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
ZipLocalEntryBlock,
}; usecrate::write::SimpleFileOptions; usecrate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; usecrate::ZIP64_BYTES_THR; use indexmap::IndexMap; use std::borrow::Cow; use std::ffi::OsStr; 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::{Component, Path, PathBuf}; use std::sync::{Arc, OnceLock};
mod config;
pubuse config::*;
/// Provides high level API for reading from a stream. pub(crate) mod stream;
#[cfg(feature = "lzma")] pub(crate) mod lzma;
pub(crate) mod magic_finder;
// Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely pub(crate) mod zip_archive { use indexmap::IndexMap; use std::sync::Arc;
/// Extract immutable data from `ZipArchive` to make it cheap to clone #[derive(Debug)] pub(crate) struct Shared { pub(crate) files: 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, pub(crate) comment: Box<[u8]>, pub(crate) zip64_comment: Option<Box<[u8]>>,
}
#[derive(Debug)] pub(crate) struct SharedBuilder { pub(crate) files: Vec<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<()> { /// use zip::HasZipMetadata; /// 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>,
}
}
// Explicit Ok and ? are needed to convert io::Error to ZipError
Ok(SeekableTake::new(reader, data.compressed_size)?)
}
fn find_data_start(
data: &ZipFileData,
reader: &mut (impl Read + Seek + Sized),
) -> Result<u64, ZipError> { // Go to start of data.
reader.seek(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 + 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(_) => {
debug_assert_eq!(*data.data_start.get().unwrap(), data_start);
}
}
impl<'a> TryFrom<&'a CentralDirectoryEndInfo> for CentralDirectoryInfo { type Error = ZipError;
fn try_from(value: &'a CentralDirectoryEndInfo) -> Result<Self, Self::Error> { let (relative_cd_offset, number_of_files, disk_number, disk_with_central_directory) = match &value.eocd64 {
Some(DataAndPosition { data: eocd64, .. }) => { if eocd64.number_of_files_on_this_disk > eocd64.number_of_files { return Err(InvalidArchive( "ZIP64 footer indicates more files on this disk than in the whole archive",
));
} elseif eocd64.version_needed_to_extract > eocd64.version_made_by { return Err(InvalidArchive( "ZIP64 footer indicates a new version is needed to extract this archive than the \
version that wrote it",
));
}
(
eocd64.central_directory_offset,
eocd64.number_of_files as usize,
eocd64.disk_number,
eocd64.disk_with_central_directory,
)
}
_ => (
value.eocd.data.central_directory_offset as u64,
value.eocd.data.number_of_files_on_this_disk as usize,
value.eocd.data.disk_number as u32,
value.eocd.data.disk_with_central_directory as u32,
),
};
let directory_start = relative_cd_offset
.checked_add(value.archive_offset)
.ok_or(InvalidArchive("Invalid central directory size or offset"))?;
/// 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 + 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 first_new_file_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(first_new_file_header_start)
.ok_or(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(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>(())
})?;
/* 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)
}
/// 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) -> ZipResult<Shared> { // End of the probed region, initially set to the end of the file let file_len = reader.seek(io::SeekFrom::End(0))?; letmut end_exclusive = file_len;
loop { // Find the EOCD and possibly EOCD64 entries and determine the archive offset. let cde = spec::find_central_directory(
reader,
config.archive_offset,
end_exclusive,
file_len,
)?;
// Turn EOCD into internal representation. let Ok(shared) = CentralDirectoryInfo::try_from(&cde)
.and_then(|info| Self::read_central_header(info, config, reader)) else { // The next EOCD candidate should start before the current one.
end_exclusive = cde.eocd.position; continue;
};
fn read_central_header(
dir_info: CentralDirectoryInfo,
config: Config,
reader: &mut R,
) -> Result<SharedBuilder, ZipError> { // 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
};
if dir_info.disk_number != dir_info.disk_with_central_directory { return unsupported_zip_error("Support for multi-disk files is not implemented");
}
if file_capacity.saturating_mul(size_of::<ZipFileData>()) > isize::MAX as usize { return unsupported_zip_error("Oversized central directory");
}
letmut files = Vec::with_capacity(file_capacity);
reader.seek(SeekFrom::Start(dir_info.directory_start))?; for _ in0..dir_info.number_of_files { let file = central_header_to_zip_file(reader, &dir_info)?;
files.push(file);
}
/// 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 shared = Self::get_metadata(config, &mut reader)?;
/// Extract a Zip archive into a directory, overwriting files if they /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. Symbolic links are only /// created and followed if the target is within the destination directory (this is checked /// conservatively using [`std::fs::canonicalize`]). /// /// 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<()> { self.extract_internal(directory, None::<fn(&Path) -> bool>)
}
/// Extracts a Zip archive into a directory in the same fashion as /// [`ZipArchive::extract`], but detects a "root" directory in the archive /// (a single top-level directory that contains the rest of the archive's /// entries) and extracts its contents directly. /// /// For a sensible default `filter`, you can use [`root_dir_common_filter`]. /// For a custom `filter`, see [`RootDirFilter`]. /// /// See [`ZipArchive::root_dir`] for more information on how the root /// directory is detected and the meaning of the `filter` parameter. /// /// ## Example /// /// Imagine a Zip archive with the following structure: /// /// ```text /// root/file1.txt /// root/file2.txt /// root/sub/file3.txt /// root/sub/subsub/file4.txt /// ``` /// /// If the archive is extracted to `foo` using [`ZipArchive::extract`], /// the resulting directory structure will be: /// /// ```text /// foo/root/file1.txt /// foo/root/file2.txt /// foo/root/sub/file3.txt /// foo/root/sub/subsub/file4.txt /// ``` /// /// If the archive is extracted to `foo` using /// [`ZipArchive::extract_unwrapped_root_dir`], the resulting directory /// structure will be: /// /// ```text /// foo/file1.txt /// foo/file2.txt /// foo/sub/file3.txt /// foo/sub/subsub/file4.txt /// ``` /// /// ## Example - No Root Directory /// /// Imagine a Zip archive with the following structure: /// /// ```text /// root/file1.txt /// root/file2.txt /// root/sub/file3.txt /// root/sub/subsub/file4.txt /// other/file5.txt /// ``` /// /// Due to the presence of the `other` directory, /// [`ZipArchive::extract_unwrapped_root_dir`] will extract this in the same /// fashion as [`ZipArchive::extract`] as there is now no "root directory." pubfn extract_unwrapped_root_dir<P: AsRef<Path>>(
&mutself,
directory: P,
root_dir_filter: impl RootDirFilter,
) -> ZipResult<()> { self.extract_internal(directory, Some(root_dir_filter))
}
// If we have a root dir, simplify the path components to be more // appropriate for passing to `safe_prepare_path` let root_dir = root_dir
.as_ref()
.map(|(root_dir, filter)| { crate::path::simplified_components(root_dir)
.ok_or_else(|| { // Should be unreachable
debug_assert!(false, "Invalid root dir path");
InvalidArchive("Invalid root dir path")
})
.map(|root_dir| (root_dir, filter))
})
.transpose()?;
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { letmut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
Some(target)
} else { if file.is_dir() { crate::read::make_writable_dir_all(&outpath)?; continue;
}
None
};
drop(file);
iflet Some(target) = symlink_target {
make_symlink(&outpath, &target, &self.shared.files)?; 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(())
}
/// Number of files contained in this zip. pubfn len(&self) -> usize { self.shared.files.len()
}
/// Get the starting offset of the zip central directory. pubfn central_directory_start(&self) -> u64 { self.shared.dir_start
}
/// 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.shared.comment
}
/// Get the ZIP64 comment of the zip archive, if it is ZIP64. pubfn zip64_comment(&self) -> Option<&[u8]> { self.shared.zip64_comment.as_deref()
}
/// 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())
}
/// Search for a file entry by name and return a seekable object. pubfn by_name_seek(&mutself, name: &str) -> ZipResult<ZipFileSeek<R>> { self.by_index_seek(self.index_for_name(name).ok_or(ZipError::FileNotFound)?)
}
/// Search for a file entry by index and return a seekable object. pubfn by_index_seek(&mutself, index: usize) -> ZipResult<ZipFileSeek<R>> { let reader = &mutself.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),
})
})
}
/// 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 {
reader: ZipFileReader::Raw(find_content(data, reader)?),
data: Cow::Borrowed(data),
})
}
/// Find the "root directory" of an archive if it exists, filtering out /// irrelevant entries when searching. /// /// Our definition of a "root directory" is a single top-level directory /// that contains the rest of the archive's entries. This is useful for /// extracting archives that contain a single top-level directory that /// you want to "unwrap" and extract directly. /// /// For a sensible default filter, you can use [`root_dir_common_filter`]. /// For a custom filter, see [`RootDirFilter`]. pubfn root_dir(&self, filter: impl RootDirFilter) -> ZipResult<Option<PathBuf>> { letmut root_dir: Option<PathBuf> = None;
for i in0..self.len() { let (_, file) = self
.shared
.files
.get_index(i)
.ok_or(ZipError::FileNotFound)?;
let path = match file.enclosed_name() {
Some(path) => path,
None => return Ok(None),
};
// If this entry is located at the root of the archive... if path.components().count() == 1 { if file.is_dir() { // If it's a directory, it could be the root directory.
replace_root_dir!(path);
} else { // If it's anything else, this archive does not have a // root directory. return Ok(None);
}
}
// Find the root directory for this entry. letmut path = path.as_path(); whilelet Some(parent) = path.parent().filter(|path| *path != Path::new("")) {
path = parent;
}
replace_root_dir!(path);
}
Ok(root_dir)
}
/// 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,
central_directory: &CentralDirectoryInfo,
) -> ZipResult<ZipFileData> { let central_header_start = reader.stream_position()?;
// Parse central header let block = ZipCentralEntryBlock::parse(reader)?;
let file = central_header_to_zip_file_inner(
reader,
central_directory.archive_offset,
central_header_start,
block,
)?;
let central_header_end = reader.stream_position()?;
/* FIXMEpatcheduntilhttps://github.com/zip-rs/zip2/issues/384 is addressed. iffile.header_start>=central_directory.directory_start{ returnErr(InvalidArchive( "Alocalfileentrycan'tstartafterthecentraldirectory", )); }
/// 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(),
}; 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",
));
}
// Account for shifted zip offsets.
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(); letmut processed_extra_field = extra_field.clone(); let len = extra_field.len(); letmut reader = io::Cursor::new(&**extra_field);
/* TODO: codify this structure into Zip64ExtraFieldBlock fields! */ letmut 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);
} letmut 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 { // Zip64 extended information extra field 0x0001 => { if disallow_zip64 { return Err(InvalidArchive( "Can't write a custom field using the ZIP64 ID",
));
}
file.large_file = true; letmut 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);
} 0x000a => { // NTFS extra field
file.extra_fields
.push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?));
} 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()?);
file.extra_fields.push(ExtraField::ExtendedTimestamp(
ExtendedTimestamp::try_from_reader(reader, len)?,
));
} 0x6375 => { // Info-ZIP Unicode Comment Extra Field // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt
file.file_comment = String::from_utf8(
UnicodeExtraField::try_from_reader(reader, len)?
.unwrap_valid(file.file_comment.as_bytes())?
.into_vec(),
)?
.into();
} 0x7075 => { // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt
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])?; // Other fields are ignored
}
}
Ok(false)
}
/// A trait for exposing file metadata inside the zip. pubtrait HasZipMetadata { /// Get the file metadata fn get_metadata(&self) -> &ZipFileData;
}
/// Methods for retrieving information on zip files impl<'a> ZipFile<'a> { pub(crate) fn take_raw_reader(&mutself) -> io::Result<io::Take<&an style='color:blue'>'a mut dyn Read>> {
mem::replace(&mutself.reader, ZipFileReader::NoReader).into_inner()
}
/// Get the version of the file pubfn version_made_by(&self) -> (u8, u8) {
( self.get_metadata().version_made_by / 10, self.get_metadata().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.get_metadata().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.get_metadata().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`]: `PathBuf::Component::ParentDir` pubfn mangled_name(&self) -> PathBuf { self.get_metadata().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.get_metadata().enclosed_name()
}
/// Prepare the path for extraction by creating necessary missing directories and checking for symlinks to be contained within the base path. /// /// `base_path` parameter is assumed to be canonicalized. pub(crate) fn safe_prepare_path(
&self,
base_path: &Path,
outpath: &mut PathBuf,
root_dir: Option<&(Vec<&OsStr>, impl RootDirFilter)>,
) -> ZipResult<()> { let components = self
.simplified_components()
.ok_or(InvalidArchive("Invalid file path"))?;
let components = match root_dir {
Some((root_dir, filter)) => match components.strip_prefix(&**root_dir) {
Some(components) => components,
// In this case, we expect that the file was not in the root // directory, but was filtered out when searching for the // root directory.
None => { // We could technically find ourselves at this code // path if the user provides an unstable or // non-deterministic `filter` function. // // If debug assertions are on, we should panic here. // Otherwise, the safest thing to do here is to just // extract as-is.
debug_assert!(
!filter(&PathBuf::from_iter(components.iter())), "Root directory filter should not match at this point"
);
// Extract as-is.
&components[..]
}
},
None => &components[..],
};
let components_len = components.len();
for (is_last, component) in components
.iter()
.copied()
.enumerate()
.map(|(i, c)| (i == components_len - 1, c))
{ // we can skip the target directory itself because the base path is assumed to be "trusted" (if the user say extract to a symlink we can follow it)
outpath.push(component);
// check if the path is a symlink, the target must be _inherently_ within the directory for limit in (0..5u8).rev() { let meta = match std::fs::symlink_metadata(&outpath) {
Ok(meta) => meta,
Err(e) if e.kind() == io::ErrorKind::NotFound => { if !is_last { crate::read::make_writable_dir_all(&outpath)?;
} break;
}
Err(e) => return Err(e.into()),
};
if !meta.is_symlink() { break;
}
if limit == 0 { return Err(InvalidArchive("Extraction followed a symlink too deep"));
}
// note that we cannot accept links that do not inherently resolve to a path inside the directory to prevent: // - disclosure of unrelated path exists (no check for a path exist and then ../ out) // - issues with file-system specific path resolution (case sensitivity, etc) let target = std::fs::read_link(&outpath)?;
if !crate::path::simplified_components(&target)
.ok_or(InvalidArchive("Invalid symlink target path"))?
.starts_with(
&crate::path::simplified_components(base_path)
.ok_or(InvalidArchive("Invalid base path"))?,
)
{ let is_absolute_enclosed = base_path
.components()
.map(Some)
.chain(std::iter::once(None))
.zip(target.components().map(Some).chain(std::iter::repeat(None)))
.all(|(a, b)| match (a, b) { // both components are normal
(Some(Component::Normal(a)), Some(Component::Normal(b))) => a == b, // both components consumed fully
(None, None) => true, // target consumed fully but base path is not
(Some(_), None) => false, // base path consumed fully but target is not (and normal)
(None, Some(Component::CurDir | Component::Normal(_))) => true,
_ => false,
});
if !is_absolute_enclosed { return Err(InvalidArchive("Symlink is not inherently safe"));
}
}
outpath.push(target);
}
}
Ok(())
}
/// Get the comment of the file pubfn comment(&self) -> &str {
&self.get_metadata().file_comment
}
/// Get the compression method used to store the file pubfn compression(&self) -> CompressionMethod { self.get_metadata().compression_method
}
/// Get if the files is encrypted or not pubfn encrypted(&self) -> bool { self.data.encrypted
}
/// Get the size of the file, in bytes, in the archive pubfn compressed_size(&self) -> u64 { self.get_metadata().compressed_size
}
/// Get the size of the file, in bytes, when uncompressed pubfn size(&self) -> u64 { self.get_metadata().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.get_metadata().unix_mode()
}
/// Get the CRC32 hash of the original file pubfn crc32(&self) -> u32 { self.get_metadata().crc32
}
/// Get the extra data of the zip header for this file pubfn extra_data(&self) -> Option<&[u8]> { self.get_metadata()
.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()
}
/// Get the starting offset of the zip header for this file pubfn header_start(&self) -> u64 { self.get_metadata().header_start
} /// Get the starting offset of the zip header in the central directory for this file pubfn central_header_start(&self) -> u64 { self.get_metadata().central_header_start
}
/// Get the [`SimpleFileOptions`] that would be used to write this file to /// a new zip archive. pubfn options(&self) -> SimpleFileOptions { letmut options = SimpleFileOptions::default()
.large_file(self.compressed_size().max(self.size()) > ZIP64_BYTES_THR)
.compression_method(self.compression())
.unix_permissions(self.unix_mode().unwrap_or(0o644) | S_IFREG)
.last_modified_time( self.last_modified()
.filter(|m| m.is_valid())
.unwrap_or_else(DateTime::default_for_write),
);
options.normalize();
options
}
}
/// Methods for retrieving information on zip files impl ZipFile<'_> { /// iterate through all extra fields pubfn extra_data_fields(&self) -> impl Iterator<Item = &ExtraField> { self.data.extra_fields.iter()
}
}
impl Drop for ZipFile<'_> { 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. iflet Ok(mut inner) = self.take_raw_reader() { let _ = copy(&mut inner, &mut sink());
}
}
}
}
/// 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<R: Read>(reader: &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? */
let limit_reader = (reader as &mutdyn 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)?;
/// A filter that determines whether an entry should be ignored when searching /// for the root directory of a Zip archive. /// /// Returns `true` if the entry should be considered, and `false` if it should /// be ignored. /// /// See [`root_dir_common_filter`] for a sensible default filter. pubtrait RootDirFilter: Fn(&Path) -> bool {} impl<F: Fn(&Path) -> bool> RootDirFilter for F {}
/// Common filters when finding the root directory of a Zip archive. /// /// This filter is a sensible default for most use cases and filters out common /// system files that are usually irrelevant to the contents of the archive. /// /// Currently, the filter ignores: /// - `/__MACOSX/` /// - `/.DS_Store` /// - `/Thumbs.db` /// /// **This function is not guaranteed to be stable and may change in future versions.** /// /// # Example /// /// ```rust /// # use std::path::Path; /// assert!(zip::read::root_dir_common_filter(Path::new("foo.txt"))); /// assert!(!zip::read::root_dir_common_filter(Path::new(".DS_Store"))); /// assert!(!zip::read::root_dir_common_filter(Path::new("Thumbs.db"))); /// assert!(!zip::read::root_dir_common_filter(Path::new("__MACOSX"))); /// assert!(!zip::read::root_dir_common_filter(Path::new("__MACOSX/foo.txt"))); /// ``` pubfn root_dir_common_filter(path: &Path) -> bool { const COMMON_FILTER_ROOT_FILES: &[&str] = &[".DS_Store", "Thumbs.db"];
#[cfg(test)] mod test { usecrate::result::ZipResult; usecrate::write::SimpleFileOptions; usecrate::CompressionMethod::Stored; usecrate::{ZipArchive, ZipWriter}; use std::io::{Cursor, Read, Write}; use tempfile::TempDir;
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());
}
#[test] fn test_64k_files() -> ZipResult<()> { letmut writer = ZipWriter::new(Cursor::new(Vec::new())); let options = SimpleFileOptions {
compression_method: Stored,
..Default::default()
}; for i in0..=u16::MAX { let file_name = format!("{i}.txt");
writer.start_file(&*file_name, options)?;
writer.write_all(i.to_string().as_bytes())?;
}
letmut reader = ZipArchive::new(writer.finish()?)?; for i in0..=u16::MAX { let expected_name = format!("{i}.txt"); let expected_contents = i.to_string(); let expected_contents = expected_contents.as_bytes(); letmut file = reader.by_name(&expected_name)?; letmut contents = Vec::with_capacity(expected_contents.len());
file.read_to_end(&mut contents)?;
assert_eq!(contents, expected_contents);
drop(file);
contents.clear(); letmut file = reader.by_index(i as usize)?;
file.read_to_end(&mut contents)?;
assert_eq!(contents, expected_contents);
}
Ok(())
}
/// Symlinks being extracted shouldn't be followed out of the destination directory. #[test] fn test_cannot_symlink_outside_destination() -> ZipResult<()> { use std::fs::create_dir;
letmut writer = ZipWriter::new(Cursor::new(Vec::new()));
writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
writer.start_file("symlink/dest-file", SimpleFileOptions::default())?; letmut reader = writer.finish_into_readable()?; let dest_parent =
TempDir::with_prefix("read__test_cannot_symlink_outside_destination").unwrap(); let dest_sibling = dest_parent.path().join("dest-sibling");
create_dir(&dest_sibling)?; let dest = dest_parent.path().join("dest");
create_dir(&dest)?;
assert!(reader.extract(dest).is_err());
assert!(!dest_sibling.join("dest-file").exists());
Ok(())
}
#[test] fn test_can_create_destination() -> ZipResult<()> { letmut v = Vec::new();
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); letmut reader = ZipArchive::new(Cursor::new(v))?; let dest = TempDir::with_prefix("read__test_can_create_destination").unwrap();
reader.extract(&dest)?;
assert!(dest.path().join("mimetype").exists());
Ok(())
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.24 Sekunden
(vorverarbeitet am 2026-08-27)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.