* fix: Rare combination of settings could lead to writing a corrupt archive with overlength extra data * fix: Previous fix was breaking alignment * style: cargo fmt --all * fix: ZIP64 header was being written twice * style: cargo fmt --all * ci(fuzz): Add check that file-creation options are individually valid * fix: Need to update extra_data_start in deep_copy_file * style: cargo fmt --all * test(fuzz): fix bug in Arbitrary impl * fix: Cursor-position bugs when merging archives or opening for append * fix: unintended feature dependency * style: cargo fmt --all * fix: merge_contents was miscalculating new start positions for absorbed archive's files * fix: shallow_copy_file needs to reset CDE location since the CDE is copied * fix: ZIP64 header was being written after AES header location was already calculated * fix: ZIP64 header was being counted twice when writing extra-field length * fix: deep_copy_file was positioning cursor incorrectly * test(fuzz): Reimplement Debug so that it prints the method calls actually made * test(fuzz): Fix issues with `Option<&mut Formatter>` * chore: Partial debug * chore: Revert: `merge_contents` already adjusts header_start and data_start * chore: Revert unused `mut` * style: cargo fmt --all * refactor: eliminate a magic number for CDE block size * chore: WIP: fix bugs * refactor: Minor refactors * refactor: eliminate a magic number for CDE block size * refactor: Minor refactors * refactor: Can use cde_start_pos to locate ZIP64 end locator * chore: Fix import that can no longer be feature-gated * chore: Fix import that can no longer be feature-gated * refactor: Confusing variable name * style: cargo fmt --all and fix Clippy warnings * style: fix another Clippy warning --------- Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
3622 lines
146 KiB
Rust
3622 lines
146 KiB
Rust
//! Types for creating ZIP archives
|
||
|
||
#[cfg(feature = "aes-crypto")]
|
||
use crate::aes::AesWriter;
|
||
use crate::compression::CompressionMethod;
|
||
use crate::read::{
|
||
find_content, parse_single_extra_field, Config, ZipArchive, ZipFile, ZipFileReader,
|
||
};
|
||
use crate::result::{ZipError, ZipResult};
|
||
use crate::spec::{self, FixedSizeBlock, Pod, Zip32CDEBlock};
|
||
#[cfg(feature = "aes-crypto")]
|
||
use crate::types::AesMode;
|
||
use crate::types::{
|
||
ffi, AesVendorVersion, DateTime, Zip64ExtraFieldBlock, ZipFileData, ZipLocalEntryBlock,
|
||
ZipRawValues, MIN_VERSION,
|
||
};
|
||
use crate::write::ffi::S_IFLNK;
|
||
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))]
|
||
use core::num::NonZeroU64;
|
||
use crc32fast::Hasher;
|
||
use indexmap::IndexMap;
|
||
use std::borrow::ToOwned;
|
||
use std::default::Default;
|
||
use std::fmt::{Debug, Formatter};
|
||
use std::io;
|
||
use std::io::prelude::*;
|
||
use std::io::Cursor;
|
||
use std::io::{BufReader, SeekFrom};
|
||
use std::marker::PhantomData;
|
||
use std::mem;
|
||
use std::str::{from_utf8, Utf8Error};
|
||
use std::sync::Arc;
|
||
|
||
#[cfg(feature = "deflate-flate2")]
|
||
use flate2::{write::DeflateEncoder, Compression};
|
||
|
||
#[cfg(feature = "bzip2")]
|
||
use bzip2::write::BzEncoder;
|
||
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
use zopfli::Options;
|
||
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
use std::io::BufWriter;
|
||
use std::mem::size_of;
|
||
use std::path::Path;
|
||
|
||
#[cfg(feature = "zstd")]
|
||
use zstd::stream::write::Encoder as ZstdEncoder;
|
||
|
||
enum MaybeEncrypted<W> {
|
||
Unencrypted(W),
|
||
#[cfg(feature = "aes-crypto")]
|
||
Aes(AesWriter<W>),
|
||
ZipCrypto(crate::zipcrypto::ZipCryptoWriter<W>),
|
||
}
|
||
|
||
impl<W> Debug for MaybeEncrypted<W> {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
// Don't print W, since it may be a huge Vec<u8>
|
||
f.write_str(match self {
|
||
MaybeEncrypted::Unencrypted(_) => "Unencrypted",
|
||
#[cfg(feature = "aes-crypto")]
|
||
MaybeEncrypted::Aes(_) => "AES",
|
||
MaybeEncrypted::ZipCrypto(_) => "ZipCrypto",
|
||
})
|
||
}
|
||
}
|
||
|
||
impl<W: Write> Write for MaybeEncrypted<W> {
|
||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||
match self {
|
||
MaybeEncrypted::Unencrypted(w) => w.write(buf),
|
||
#[cfg(feature = "aes-crypto")]
|
||
MaybeEncrypted::Aes(w) => w.write(buf),
|
||
MaybeEncrypted::ZipCrypto(w) => w.write(buf),
|
||
}
|
||
}
|
||
fn flush(&mut self) -> io::Result<()> {
|
||
match self {
|
||
MaybeEncrypted::Unencrypted(w) => w.flush(),
|
||
#[cfg(feature = "aes-crypto")]
|
||
MaybeEncrypted::Aes(w) => w.flush(),
|
||
MaybeEncrypted::ZipCrypto(w) => w.flush(),
|
||
}
|
||
}
|
||
}
|
||
|
||
enum GenericZipWriter<W: Write + Seek> {
|
||
Closed,
|
||
Storer(MaybeEncrypted<W>),
|
||
#[cfg(feature = "deflate-flate2")]
|
||
Deflater(DeflateEncoder<MaybeEncrypted<W>>),
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
ZopfliDeflater(zopfli::DeflateEncoder<MaybeEncrypted<W>>),
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
BufferedZopfliDeflater(BufWriter<zopfli::DeflateEncoder<MaybeEncrypted<W>>>),
|
||
#[cfg(feature = "bzip2")]
|
||
Bzip2(BzEncoder<MaybeEncrypted<W>>),
|
||
#[cfg(feature = "zstd")]
|
||
Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>),
|
||
}
|
||
|
||
impl<W: Write + Seek> Debug for GenericZipWriter<W> {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
Closed => f.write_str("Closed"),
|
||
Storer(w) => f.write_fmt(format_args!("Storer({:?})", w)),
|
||
#[cfg(feature = "deflate-flate2")]
|
||
GenericZipWriter::Deflater(w) => {
|
||
f.write_fmt(format_args!("Deflater({:?})", w.get_ref()))
|
||
}
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::ZopfliDeflater(_) => f.write_str("ZopfliDeflater"),
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::BufferedZopfliDeflater(_) => f.write_str("BufferedZopfliDeflater"),
|
||
#[cfg(feature = "bzip2")]
|
||
GenericZipWriter::Bzip2(w) => f.write_fmt(format_args!("Bzip2({:?})", w.get_ref())),
|
||
#[cfg(feature = "zstd")]
|
||
GenericZipWriter::Zstd(w) => f.write_fmt(format_args!("Zstd({:?})", w.get_ref())),
|
||
}
|
||
}
|
||
}
|
||
|
||
// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
|
||
pub(crate) mod zip_writer {
|
||
use super::*;
|
||
/// ZIP archive generator
|
||
///
|
||
/// Handles the bookkeeping involved in building an archive, and provides an
|
||
/// API to edit its contents.
|
||
///
|
||
/// ```
|
||
/// # fn doit() -> zip::result::ZipResult<()>
|
||
/// # {
|
||
/// # use zip::ZipWriter;
|
||
/// use std::io::Write;
|
||
/// use zip::write::SimpleFileOptions;
|
||
///
|
||
/// // We use a buffer here, though you'd normally use a `File`
|
||
/// let mut buf = [0; 65536];
|
||
/// let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
|
||
///
|
||
/// let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||
/// zip.start_file("hello_world.txt", options)?;
|
||
/// zip.write(b"Hello, World!")?;
|
||
///
|
||
/// // Apply the changes you've made.
|
||
/// // Dropping the `ZipWriter` will have the same effect, but may silently fail
|
||
/// zip.finish()?;
|
||
///
|
||
/// # Ok(())
|
||
/// # }
|
||
/// # doit().unwrap();
|
||
/// ```
|
||
pub struct ZipWriter<W: Write + Seek> {
|
||
pub(super) inner: GenericZipWriter<W>,
|
||
pub(super) files: IndexMap<Box<str>, ZipFileData>,
|
||
pub(super) stats: ZipWriterStats,
|
||
pub(super) writing_to_file: bool,
|
||
pub(super) writing_raw: bool,
|
||
pub(super) comment: Box<[u8]>,
|
||
pub(super) flush_on_finish_file: bool,
|
||
}
|
||
|
||
impl<W: Write + Seek> Debug for ZipWriter<W> {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
f.write_fmt(format_args!(
|
||
"ZipWriter {{files: {:?}, stats: {:?}, writing_to_file: {}, writing_raw: {}, comment: {:?}, flush_on_finish_file: {}}}",
|
||
self.files, self.stats, self.writing_to_file, self.writing_raw,
|
||
self.comment, self.flush_on_finish_file))
|
||
}
|
||
}
|
||
}
|
||
#[doc(inline)]
|
||
pub use self::sealed::FileOptionExtension;
|
||
use crate::result::ZipError::{InvalidArchive, UnsupportedArchive};
|
||
use crate::unstable::path_to_string;
|
||
use crate::unstable::LittleEndianWriteExt;
|
||
use crate::write::GenericZipWriter::{Closed, Storer};
|
||
use crate::zipcrypto::ZipCryptoKeys;
|
||
use crate::CompressionMethod::Stored;
|
||
pub use zip_writer::ZipWriter;
|
||
|
||
#[derive(Default, Debug)]
|
||
struct ZipWriterStats {
|
||
hasher: Hasher,
|
||
start: u64,
|
||
bytes_written: u64,
|
||
}
|
||
|
||
mod sealed {
|
||
use std::sync::Arc;
|
||
|
||
use super::ExtendedFileOptions;
|
||
|
||
pub trait Sealed {}
|
||
/// File options Extensions
|
||
#[doc(hidden)]
|
||
pub trait FileOptionExtension: Default + Sealed {
|
||
/// Extra Data
|
||
fn extra_data(&self) -> Option<&Arc<Vec<u8>>>;
|
||
/// Central Extra Data
|
||
fn central_extra_data(&self) -> Option<&Arc<Vec<u8>>>;
|
||
}
|
||
impl Sealed for () {}
|
||
impl FileOptionExtension for () {
|
||
fn extra_data(&self) -> Option<&Arc<Vec<u8>>> {
|
||
None
|
||
}
|
||
fn central_extra_data(&self) -> Option<&Arc<Vec<u8>>> {
|
||
None
|
||
}
|
||
}
|
||
impl Sealed for ExtendedFileOptions {}
|
||
|
||
impl FileOptionExtension for ExtendedFileOptions {
|
||
fn extra_data(&self) -> Option<&Arc<Vec<u8>>> {
|
||
Some(&self.extra_data)
|
||
}
|
||
fn central_extra_data(&self) -> Option<&Arc<Vec<u8>>> {
|
||
Some(&self.central_extra_data)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||
pub(crate) enum EncryptWith<'k> {
|
||
#[cfg(feature = "aes-crypto")]
|
||
Aes {
|
||
mode: AesMode,
|
||
password: &'k str,
|
||
},
|
||
ZipCrypto(ZipCryptoKeys, PhantomData<&'k ()>),
|
||
}
|
||
|
||
#[cfg(fuzzing)]
|
||
impl<'a> arbitrary::Arbitrary<'a> for EncryptWith<'a> {
|
||
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||
#[cfg(feature = "aes-crypto")]
|
||
if bool::arbitrary(u)? {
|
||
return Ok(EncryptWith::Aes {
|
||
mode: AesMode::arbitrary(u)?,
|
||
password: u.arbitrary::<&str>()?,
|
||
});
|
||
}
|
||
|
||
Ok(EncryptWith::ZipCrypto(
|
||
ZipCryptoKeys::arbitrary(u)?,
|
||
PhantomData,
|
||
))
|
||
}
|
||
}
|
||
|
||
/// Metadata for a file to be written
|
||
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
|
||
pub struct FileOptions<'k, T: FileOptionExtension> {
|
||
pub(crate) compression_method: CompressionMethod,
|
||
pub(crate) compression_level: Option<i64>,
|
||
pub(crate) last_modified_time: DateTime,
|
||
pub(crate) permissions: Option<u32>,
|
||
pub(crate) large_file: bool,
|
||
pub(crate) encrypt_with: Option<EncryptWith<'k>>,
|
||
pub(crate) extended_options: T,
|
||
pub(crate) alignment: u16,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
pub(super) zopfli_buffer_size: Option<usize>,
|
||
}
|
||
/// Simple File Options. Can be copied and good for simple writing zip files
|
||
pub type SimpleFileOptions = FileOptions<'static, ()>;
|
||
/// Adds Extra Data and Central Extra Data. It does not implement copy.
|
||
pub type FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>;
|
||
/// The Extension for Extra Data and Central Extra Data
|
||
#[derive(Clone, Default, Eq, PartialEq)]
|
||
pub struct ExtendedFileOptions {
|
||
extra_data: Arc<Vec<u8>>,
|
||
central_extra_data: Arc<Vec<u8>>,
|
||
}
|
||
|
||
impl ExtendedFileOptions {
|
||
/// Adds an extra data field, unless we detect that it's invalid.
|
||
pub fn add_extra_data(
|
||
&mut self,
|
||
header_id: u16,
|
||
data: Box<[u8]>,
|
||
central_only: bool,
|
||
) -> ZipResult<()> {
|
||
let len = data.len() + 4;
|
||
if self.extra_data.len() + self.central_extra_data.len() + len > u16::MAX as usize {
|
||
Err(InvalidArchive(
|
||
"Extra data field would be longer than allowed",
|
||
))
|
||
} else {
|
||
let field = if central_only {
|
||
&mut self.central_extra_data
|
||
} else {
|
||
&mut self.extra_data
|
||
};
|
||
let vec = Arc::get_mut(field);
|
||
let vec = match vec {
|
||
Some(exclusive) => exclusive,
|
||
None => {
|
||
*field = Arc::new(field.to_vec());
|
||
Arc::get_mut(field).unwrap()
|
||
}
|
||
};
|
||
Self::add_extra_data_unchecked(vec, header_id, data)?;
|
||
Self::validate_extra_data(vec, true)?;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
pub(crate) fn add_extra_data_unchecked(
|
||
vec: &mut Vec<u8>,
|
||
header_id: u16,
|
||
data: Box<[u8]>,
|
||
) -> Result<(), ZipError> {
|
||
vec.reserve_exact(data.len() + 4);
|
||
vec.write_u16_le(header_id)?;
|
||
vec.write_u16_le(data.len() as u16)?;
|
||
vec.write_all(&data)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn validate_extra_data(data: &[u8], disallow_zip64: bool) -> ZipResult<()> {
|
||
let len = data.len() as u64;
|
||
if len == 0 {
|
||
return Ok(());
|
||
}
|
||
if len > u16::MAX as u64 {
|
||
return Err(ZipError::Io(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"Extra-data field can't exceed u16::MAX bytes",
|
||
)));
|
||
}
|
||
let mut data = Cursor::new(data);
|
||
let mut pos = data.position();
|
||
while pos < len {
|
||
if len - data.position() < 4 {
|
||
return Err(ZipError::Io(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"Extra-data field doesn't have room for ID and length",
|
||
)));
|
||
}
|
||
#[cfg(not(feature = "unreserved"))]
|
||
{
|
||
use crate::unstable::LittleEndianReadExt;
|
||
let header_id = data.read_u16_le()?;
|
||
if EXTRA_FIELD_MAPPING
|
||
.iter()
|
||
.any(|&mapped| mapped == header_id)
|
||
{
|
||
return Err(ZipError::Io(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
format!(
|
||
"Extra data header ID {header_id:#06} requires crate feature \"unreserved\"",
|
||
),
|
||
)));
|
||
}
|
||
data.seek(SeekFrom::Current(-2))?;
|
||
}
|
||
parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64)?;
|
||
pos = data.position();
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Debug for ExtendedFileOptions {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||
f.write_fmt(format_args!("ExtendedFileOptions {{extra_data: vec!{:?}.into(), central_extra_data: vec!{:?}.into()}}",
|
||
self.extra_data, self.central_extra_data))
|
||
}
|
||
}
|
||
|
||
#[cfg(fuzzing)]
|
||
impl<'a> arbitrary::Arbitrary<'a> for FileOptions<'a, ExtendedFileOptions> {
|
||
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||
let mut options = FullFileOptions {
|
||
compression_method: CompressionMethod::arbitrary(u)?,
|
||
compression_level: if bool::arbitrary(u)? {
|
||
Some(u.int_in_range(0..=24)?)
|
||
} else {
|
||
None
|
||
},
|
||
last_modified_time: DateTime::arbitrary(u)?,
|
||
permissions: Option::<u32>::arbitrary(u)?,
|
||
large_file: bool::arbitrary(u)?,
|
||
encrypt_with: Option::<EncryptWith>::arbitrary(u)?,
|
||
alignment: u16::arbitrary(u)?,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: None,
|
||
..Default::default()
|
||
};
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
if options.compression_method == CompressionMethod::Deflated && bool::arbitrary(u)? {
|
||
options.zopfli_buffer_size =
|
||
Some(if bool::arbitrary(u)? { 2 } else { 3 } << u.int_in_range(8..=20)?);
|
||
}
|
||
u.arbitrary_loop(Some(0), Some(10), |u| {
|
||
options
|
||
.add_extra_data(
|
||
u.int_in_range(2..=u16::MAX)?,
|
||
Box::<[u8]>::arbitrary(u)?,
|
||
bool::arbitrary(u)?,
|
||
)
|
||
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
|
||
Ok(core::ops::ControlFlow::Continue(()))
|
||
})?;
|
||
ZipWriter::new(Cursor::new(Vec::new()))
|
||
.start_file("", options.clone())
|
||
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
|
||
Ok(options)
|
||
}
|
||
}
|
||
|
||
impl<'k, T: FileOptionExtension> FileOptions<'k, T> {
|
||
/// Set the compression method for the new file
|
||
///
|
||
/// The default is `CompressionMethod::Deflated` if it is enabled. If not,
|
||
/// `CompressionMethod::Bzip2` is the default if it is enabled. If neither `bzip2` nor `deflate`
|
||
/// is enabled, `CompressionMethod::Zlib` is the default. If all else fails,
|
||
/// `CompressionMethod::Stored` becomes the default and files are written uncompressed.
|
||
#[must_use]
|
||
pub const fn compression_method(mut self, method: CompressionMethod) -> Self {
|
||
self.compression_method = method;
|
||
self
|
||
}
|
||
|
||
/// Set the compression level for the new file
|
||
///
|
||
/// `None` value specifies default compression level.
|
||
///
|
||
/// Range of values depends on compression method:
|
||
/// * `Deflated`: 10 - 264 for Zopfli, 0 - 9 for other encoders. Default is 24 if Zopfli is the
|
||
/// only encoder, or 6 otherwise.
|
||
/// * `Bzip2`: 0 - 9. Default is 6
|
||
/// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
|
||
/// * others: only `None` is allowed
|
||
#[must_use]
|
||
pub const fn compression_level(mut self, level: Option<i64>) -> Self {
|
||
self.compression_level = level;
|
||
self
|
||
}
|
||
|
||
/// Set the last modified time
|
||
///
|
||
/// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
|
||
/// otherwise
|
||
#[must_use]
|
||
pub const fn last_modified_time(mut self, mod_time: DateTime) -> Self {
|
||
self.last_modified_time = mod_time;
|
||
self
|
||
}
|
||
|
||
/// Set the permissions for the new file.
|
||
///
|
||
/// The format is represented with unix-style permissions.
|
||
/// The default is `0o644`, which represents `rw-r--r--` for files,
|
||
/// and `0o755`, which represents `rwxr-xr-x` for directories.
|
||
///
|
||
/// This method only preserves the file permissions bits (via a `& 0o777`) and discards
|
||
/// higher file mode bits. So it cannot be used to denote an entry as a directory,
|
||
/// symlink, or other special file type.
|
||
#[must_use]
|
||
pub const fn unix_permissions(mut self, mode: u32) -> Self {
|
||
self.permissions = Some(mode & 0o777);
|
||
self
|
||
}
|
||
|
||
/// Set whether the new file's compressed and uncompressed size is less than 4 GiB.
|
||
///
|
||
/// If set to `false` and the file exceeds the limit, an I/O error is thrown and the file is
|
||
/// aborted. If set to `true`, readers will require ZIP64 support and if the file does not
|
||
/// exceed the limit, 20 B are wasted. The default is `false`.
|
||
#[must_use]
|
||
pub const fn large_file(mut self, large: bool) -> Self {
|
||
self.large_file = large;
|
||
self
|
||
}
|
||
|
||
pub(crate) fn with_deprecated_encryption(self, password: &[u8]) -> FileOptions<'static, T> {
|
||
FileOptions {
|
||
encrypt_with: Some(EncryptWith::ZipCrypto(
|
||
ZipCryptoKeys::derive(password),
|
||
PhantomData,
|
||
)),
|
||
..self
|
||
}
|
||
}
|
||
|
||
/// Set the AES encryption parameters.
|
||
#[cfg(feature = "aes-crypto")]
|
||
pub fn with_aes_encryption(self, mode: AesMode, password: &str) -> FileOptions<'_, T> {
|
||
FileOptions {
|
||
encrypt_with: Some(EncryptWith::Aes { mode, password }),
|
||
..self
|
||
}
|
||
}
|
||
|
||
/// Sets the size of the buffer used to hold the next block that Zopfli will compress. The
|
||
/// larger the buffer, the more effective the compression, but the more memory is required.
|
||
/// A value of `None` indicates no buffer, which is recommended only when all non-empty writes
|
||
/// are larger than about 32 KiB.
|
||
#[must_use]
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
pub const fn with_zopfli_buffer(mut self, size: Option<usize>) -> Self {
|
||
self.zopfli_buffer_size = size;
|
||
self
|
||
}
|
||
|
||
/// Returns the compression level currently set.
|
||
pub const fn get_compression_level(&self) -> Option<i64> {
|
||
self.compression_level
|
||
}
|
||
/// Sets the alignment to the given number of bytes.
|
||
#[must_use]
|
||
pub const fn with_alignment(mut self, alignment: u16) -> Self {
|
||
self.alignment = alignment;
|
||
self
|
||
}
|
||
}
|
||
impl<'k> FileOptions<'k, ExtendedFileOptions> {
|
||
/// Adds an extra data field.
|
||
pub fn add_extra_data(
|
||
&mut self,
|
||
header_id: u16,
|
||
data: Box<[u8]>,
|
||
central_only: bool,
|
||
) -> ZipResult<()> {
|
||
self.extended_options
|
||
.add_extra_data(header_id, data, central_only)
|
||
}
|
||
|
||
/// Removes the extra data fields.
|
||
#[must_use]
|
||
pub fn clear_extra_data(mut self) -> Self {
|
||
if self.extended_options.extra_data.len() > 0 {
|
||
self.extended_options.extra_data = Arc::new(vec![]);
|
||
}
|
||
if self.extended_options.central_extra_data.len() > 0 {
|
||
self.extended_options.central_extra_data = Arc::new(vec![]);
|
||
}
|
||
self
|
||
}
|
||
}
|
||
impl<'k, T: FileOptionExtension> Default for FileOptions<'k, T> {
|
||
/// Construct a new FileOptions object
|
||
fn default() -> Self {
|
||
Self {
|
||
compression_method: Default::default(),
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default_for_write(),
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: T::default(),
|
||
alignment: 1,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: Some(1 << 15),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<W: Write + Seek> Write for ZipWriter<W> {
|
||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||
if !self.writing_to_file {
|
||
return Err(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"No file has been started",
|
||
));
|
||
}
|
||
if buf.is_empty() {
|
||
return Ok(0);
|
||
}
|
||
match self.inner.ref_mut() {
|
||
Some(ref mut w) => {
|
||
let write_result = w.write(buf);
|
||
if let Ok(count) = write_result {
|
||
self.stats.update(&buf[0..count]);
|
||
if self.stats.bytes_written > spec::ZIP64_BYTES_THR
|
||
&& !self.files.last_mut().unwrap().1.large_file
|
||
{
|
||
let _ = self.abort_file();
|
||
return Err(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"Large file option has not been set",
|
||
));
|
||
}
|
||
}
|
||
write_result
|
||
}
|
||
None => Err(io::Error::new(
|
||
io::ErrorKind::BrokenPipe,
|
||
"write(): ZipWriter was already closed",
|
||
)),
|
||
}
|
||
}
|
||
|
||
fn flush(&mut self) -> io::Result<()> {
|
||
match self.inner.ref_mut() {
|
||
Some(ref mut w) => w.flush(),
|
||
None => Err(io::Error::new(
|
||
io::ErrorKind::BrokenPipe,
|
||
"flush(): ZipWriter was already closed",
|
||
)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl ZipWriterStats {
|
||
fn update(&mut self, buf: &[u8]) {
|
||
self.hasher.update(buf);
|
||
self.bytes_written += buf.len() as u64;
|
||
}
|
||
}
|
||
|
||
impl<A: Read + Write + Seek> ZipWriter<A> {
|
||
/// Initializes the archive from an existing ZIP archive, making it ready for append.
|
||
///
|
||
/// This uses a default configuration to initially read the archive.
|
||
pub fn new_append(readwriter: A) -> ZipResult<ZipWriter<A>> {
|
||
Self::new_append_with_config(Default::default(), readwriter)
|
||
}
|
||
|
||
/// Initializes the archive from an existing ZIP archive, making it ready for append.
|
||
///
|
||
/// This uses the given read configuration to initially read the archive.
|
||
pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult<ZipWriter<A>> {
|
||
readwriter.seek(SeekFrom::Start(0))?;
|
||
if let Ok((footer, shared)) = ZipArchive::get_metadata(config, &mut readwriter) {
|
||
Ok(ZipWriter {
|
||
inner: Storer(MaybeEncrypted::Unencrypted(readwriter)),
|
||
files: shared.files,
|
||
stats: Default::default(),
|
||
writing_to_file: false,
|
||
comment: footer.zip_file_comment,
|
||
writing_raw: true, // avoid recomputing the last file's header
|
||
flush_on_finish_file: false,
|
||
})
|
||
} else {
|
||
Err(InvalidArchive("No central-directory end header found"))
|
||
}
|
||
}
|
||
|
||
/// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed
|
||
/// bytes. It flushes a file's header and body once it starts writing another file. A ZipWriter
|
||
/// will not try to seek back into where a previous file was written unless
|
||
/// either [`ZipWriter::abort_file`] is called while [`ZipWriter::is_writing_file`] returns
|
||
/// false, or [`ZipWriter::deep_copy_file`] is called. In the latter case, it will only need to
|
||
/// read previously-written files and not overwrite them.
|
||
///
|
||
/// Note: when using an `inner` that cannot overwrite flushed bytes, do not wrap it in a
|
||
/// [BufWriter], because that has a [Seek::seek] method that implicitly calls
|
||
/// [BufWriter::flush], and ZipWriter needs to seek backward to update each file's header with
|
||
/// the size and checksum after writing the body.
|
||
///
|
||
/// This setting is false by default.
|
||
pub fn set_flush_on_finish_file(&mut self, flush_on_finish_file: bool) {
|
||
self.flush_on_finish_file = flush_on_finish_file;
|
||
}
|
||
}
|
||
|
||
impl<A: Read + Write + Seek> ZipWriter<A> {
|
||
/// Adds another copy of a file already in this archive. This will produce a larger but more
|
||
/// widely-compatible archive compared to [Self::shallow_copy_file]. Does not copy alignment.
|
||
pub fn deep_copy_file(&mut self, src_name: &str, dest_name: &str) -> ZipResult<()> {
|
||
self.finish_file()?;
|
||
if src_name == dest_name || self.files.contains_key(dest_name) {
|
||
return Err(InvalidArchive("That file already exists"));
|
||
}
|
||
let write_position = self.inner.get_plain().stream_position()?;
|
||
let src_index = self.index_by_name(src_name)?;
|
||
let src_data = &mut self.files[src_index];
|
||
let src_data_start = src_data.data_start();
|
||
debug_assert!(src_data_start <= write_position);
|
||
let mut compressed_size = src_data.compressed_size;
|
||
if compressed_size > (write_position - src_data_start) {
|
||
compressed_size = write_position - src_data_start;
|
||
src_data.compressed_size = compressed_size;
|
||
}
|
||
let mut reader = BufReader::new(ZipFileReader::Raw(find_content(
|
||
src_data,
|
||
self.inner.get_plain(),
|
||
)?));
|
||
let mut copy = Vec::with_capacity(compressed_size as usize);
|
||
reader.read_to_end(&mut copy)?;
|
||
drop(reader);
|
||
self.inner
|
||
.get_plain()
|
||
.seek(SeekFrom::Start(write_position))?;
|
||
let mut new_data = src_data.clone();
|
||
let dest_name_raw = dest_name.as_bytes();
|
||
new_data.file_name = dest_name.into();
|
||
new_data.file_name_raw = dest_name_raw.into();
|
||
new_data.is_utf8 = !dest_name.is_ascii();
|
||
new_data.header_start = write_position;
|
||
let extra_data_start = write_position
|
||
+ size_of::<ZipLocalEntryBlock>() as u64
|
||
+ new_data.file_name_raw.len() as u64;
|
||
new_data.extra_data_start = Some(extra_data_start);
|
||
let mut data_start = extra_data_start;
|
||
if let Some(extra) = &src_data.extra_field {
|
||
data_start += extra.len() as u64;
|
||
}
|
||
new_data.data_start.take();
|
||
new_data.data_start.get_or_init(|| data_start);
|
||
new_data.central_header_start = 0;
|
||
let block = new_data.local_block()?;
|
||
let index = self.insert_file_data(new_data)?;
|
||
let result = (|| {
|
||
let plain_writer = self.inner.get_plain();
|
||
plain_writer.write_all(block.as_bytes())?;
|
||
plain_writer.write_all(dest_name_raw)?;
|
||
let new_data = &self.files[index];
|
||
if let Some(data) = &new_data.extra_field {
|
||
plain_writer.write_all(data)?;
|
||
}
|
||
debug_assert_eq!(data_start, plain_writer.stream_position()?);
|
||
self.writing_to_file = true;
|
||
plain_writer.write_all(©)
|
||
})();
|
||
self.ok_or_abort_file(result)?;
|
||
|
||
// Copying will overwrite the central header
|
||
self.files
|
||
.values_mut()
|
||
.for_each(|file| file.central_header_start = 0);
|
||
|
||
self.writing_to_file = false;
|
||
Ok(())
|
||
}
|
||
|
||
/// Like `deep_copy_file`, but uses Path arguments.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn deep_copy_file_from_path<T: AsRef<Path>, U: AsRef<Path>>(
|
||
&mut self,
|
||
src_path: T,
|
||
dest_path: U,
|
||
) -> ZipResult<()> {
|
||
let src = path_to_string(src_path);
|
||
let dest = path_to_string(dest_path);
|
||
self.deep_copy_file(&src, &dest)
|
||
}
|
||
|
||
/// Write the zip file into the backing stream, then produce a readable archive of that data.
|
||
///
|
||
/// This method avoids parsing the central directory records at the end of the stream for
|
||
/// a slight performance improvement over running [`ZipArchive::new()`] on the output of
|
||
/// [`Self::finish()`].
|
||
///
|
||
///```
|
||
/// # fn main() -> Result<(), zip::result::ZipError> {
|
||
/// use std::io::{Cursor, prelude::*};
|
||
/// use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
|
||
///
|
||
/// let buf = Cursor::new(Vec::new());
|
||
/// let mut zip = ZipWriter::new(buf);
|
||
/// let options = SimpleFileOptions::default();
|
||
/// zip.start_file("a.txt", options)?;
|
||
/// zip.write_all(b"hello\n")?;
|
||
///
|
||
/// let mut zip = zip.finish_into_readable()?;
|
||
/// let mut s: String = String::new();
|
||
/// zip.by_name("a.txt")?.read_to_string(&mut s)?;
|
||
/// assert_eq!(s, "hello\n");
|
||
/// # Ok(())
|
||
/// # }
|
||
///```
|
||
pub fn finish_into_readable(mut self) -> ZipResult<ZipArchive<A>> {
|
||
let central_start = self.finalize()?;
|
||
let inner = mem::replace(&mut self.inner, Closed).unwrap();
|
||
let comment = mem::take(&mut self.comment);
|
||
let files = mem::take(&mut self.files);
|
||
let archive = ZipArchive::from_finalized_writer(files, comment, inner, central_start)?;
|
||
Ok(archive)
|
||
}
|
||
}
|
||
|
||
impl<W: Write + Seek> ZipWriter<W> {
|
||
/// Initializes the archive.
|
||
///
|
||
/// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
|
||
/// After a successful write, the file remains open for writing. After a failed write, call
|
||
/// [`ZipWriter::is_writing_file`] to determine if the file remains open.
|
||
pub fn new(inner: W) -> ZipWriter<W> {
|
||
ZipWriter {
|
||
inner: Storer(MaybeEncrypted::Unencrypted(inner)),
|
||
files: IndexMap::new(),
|
||
stats: Default::default(),
|
||
writing_to_file: false,
|
||
writing_raw: false,
|
||
comment: Box::new([]),
|
||
flush_on_finish_file: false,
|
||
}
|
||
}
|
||
|
||
/// Returns true if a file is currently open for writing.
|
||
pub const fn is_writing_file(&self) -> bool {
|
||
self.writing_to_file && !self.inner.is_closed()
|
||
}
|
||
|
||
/// Set ZIP archive comment.
|
||
pub fn set_comment<S>(&mut self, comment: S)
|
||
where
|
||
S: Into<Box<str>>,
|
||
{
|
||
self.set_raw_comment(comment.into().into_boxed_bytes())
|
||
}
|
||
|
||
/// Set ZIP archive comment.
|
||
///
|
||
/// This sets the raw bytes of the comment. The comment
|
||
/// is typically expected to be encoded in UTF-8.
|
||
pub fn set_raw_comment(&mut self, comment: Box<[u8]>) {
|
||
self.comment = comment;
|
||
}
|
||
|
||
/// Get ZIP archive comment.
|
||
pub fn get_comment(&mut self) -> Result<&str, Utf8Error> {
|
||
from_utf8(self.get_raw_comment())
|
||
}
|
||
|
||
/// Get ZIP archive comment.
|
||
///
|
||
/// This returns the raw bytes of the comment. The comment
|
||
/// is typically expected to be encoded in UTF-8.
|
||
pub const fn get_raw_comment(&self) -> &[u8] {
|
||
&self.comment
|
||
}
|
||
|
||
/// Set the file length and crc32 manually.
|
||
///
|
||
/// # Safety
|
||
///
|
||
/// This overwrites the internal crc32 calculation. It should only be used in case
|
||
/// the underlying [Write] is written independently and you need to adjust the zip metadata.
|
||
pub unsafe fn set_file_metadata(&mut self, length: u64, crc32: u32) -> ZipResult<()> {
|
||
if !self.writing_to_file {
|
||
return Err(ZipError::Io(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"No file has been started",
|
||
)));
|
||
}
|
||
self.stats.hasher = Hasher::new_with_initial_len(crc32, length);
|
||
self.stats.bytes_written = length;
|
||
Ok(())
|
||
}
|
||
|
||
fn ok_or_abort_file<T, E: Into<ZipError>>(&mut self, result: Result<T, E>) -> ZipResult<T> {
|
||
match result {
|
||
Err(e) => {
|
||
let _ = self.abort_file();
|
||
Err(e.into())
|
||
}
|
||
Ok(t) => Ok(t),
|
||
}
|
||
}
|
||
|
||
/// Start a new file for with the requested options.
|
||
fn start_entry<S: ToString, T: FileOptionExtension>(
|
||
&mut self,
|
||
name: S,
|
||
options: FileOptions<T>,
|
||
raw_values: Option<ZipRawValues>,
|
||
) -> ZipResult<()> {
|
||
self.finish_file()?;
|
||
|
||
let header_start = self.inner.get_plain().stream_position()?;
|
||
let raw_values = raw_values.unwrap_or(ZipRawValues {
|
||
crc32: 0,
|
||
compressed_size: 0,
|
||
uncompressed_size: 0,
|
||
});
|
||
|
||
let mut extra_data = match options.extended_options.extra_data() {
|
||
Some(data) => data.to_vec(),
|
||
None => vec![],
|
||
};
|
||
let central_extra_data = options.extended_options.central_extra_data();
|
||
if let Some(zip64_block) =
|
||
Zip64ExtraFieldBlock::maybe_new(options.large_file, 0, 0, header_start)
|
||
{
|
||
let mut new_extra_data = zip64_block.serialize().into_vec();
|
||
new_extra_data.append(&mut extra_data);
|
||
extra_data = new_extra_data;
|
||
}
|
||
// Write AES encryption extra data.
|
||
#[allow(unused_mut)]
|
||
let mut aes_extra_data_start = 0;
|
||
#[cfg(feature = "aes-crypto")]
|
||
if let Some(EncryptWith::Aes { mode, .. }) = options.encrypt_with {
|
||
let aes_dummy_extra_data =
|
||
vec![0x02, 0x00, 0x41, 0x45, mode as u8, 0x00, 0x00].into_boxed_slice();
|
||
aes_extra_data_start = extra_data.len() as u64;
|
||
ExtendedFileOptions::add_extra_data_unchecked(
|
||
&mut extra_data,
|
||
0x9901,
|
||
aes_dummy_extra_data,
|
||
)?;
|
||
}
|
||
|
||
let (compression_method, aes_mode) = match options.encrypt_with {
|
||
#[cfg(feature = "aes-crypto")]
|
||
Some(EncryptWith::Aes { mode, .. }) => (
|
||
CompressionMethod::Aes,
|
||
Some((mode, AesVendorVersion::Ae2, options.compression_method)),
|
||
),
|
||
_ => (options.compression_method, None),
|
||
};
|
||
let header_end = header_start
|
||
+ size_of::<ZipLocalEntryBlock>() as u64
|
||
+ name.to_string().as_bytes().len() as u64;
|
||
if options.alignment > 1 {
|
||
let extra_data_end = header_end + extra_data.len() as u64;
|
||
let align = options.alignment as u64;
|
||
let unaligned_header_bytes = extra_data_end % align;
|
||
if unaligned_header_bytes != 0 {
|
||
let mut pad_length = (align - unaligned_header_bytes) as usize;
|
||
while pad_length < 6 {
|
||
pad_length += align as usize;
|
||
}
|
||
// Add an extra field to the extra_data, per APPNOTE 4.6.11
|
||
let mut pad_body = vec![0; pad_length - 4];
|
||
debug_assert!(pad_body.len() >= 2);
|
||
[pad_body[0], pad_body[1]] = options.alignment.to_le_bytes();
|
||
ExtendedFileOptions::add_extra_data_unchecked(
|
||
&mut extra_data,
|
||
0xa11e,
|
||
pad_body.into_boxed_slice(),
|
||
)?;
|
||
debug_assert_eq!((extra_data.len() as u64 + header_end) % align, 0);
|
||
}
|
||
}
|
||
let extra_data_len = extra_data.len();
|
||
if let Some(data) = central_extra_data {
|
||
if extra_data_len + data.len() > u16::MAX as usize {
|
||
return Err(InvalidArchive(
|
||
"Extra data and central extra data must be less than 64KiB when combined",
|
||
));
|
||
}
|
||
ExtendedFileOptions::validate_extra_data(data, true)?;
|
||
}
|
||
let mut file = ZipFileData::initialize_local_block(
|
||
name,
|
||
&options,
|
||
raw_values,
|
||
header_start,
|
||
None,
|
||
aes_extra_data_start,
|
||
compression_method,
|
||
aes_mode,
|
||
&extra_data,
|
||
);
|
||
file.version_made_by = file.version_made_by.max(file.version_needed() as u8);
|
||
file.extra_data_start = Some(header_end);
|
||
let index = self.insert_file_data(file)?;
|
||
self.writing_to_file = true;
|
||
let result: ZipResult<()> = (|| {
|
||
ExtendedFileOptions::validate_extra_data(&extra_data, false)?;
|
||
let file = &mut self.files[index];
|
||
let block = file.local_block()?;
|
||
let writer = self.inner.get_plain();
|
||
block.write(writer)?;
|
||
// file name
|
||
writer.write_all(&file.file_name_raw)?;
|
||
if extra_data_len > 0 {
|
||
writer.write_all(&extra_data)?;
|
||
file.extra_field = Some(extra_data.into());
|
||
}
|
||
Ok(())
|
||
})();
|
||
self.ok_or_abort_file(result)?;
|
||
let writer = self.inner.get_plain();
|
||
self.stats.start = writer.stream_position()?;
|
||
match options.encrypt_with {
|
||
#[cfg(feature = "aes-crypto")]
|
||
Some(EncryptWith::Aes { mode, password }) => {
|
||
let aeswriter = AesWriter::new(
|
||
mem::replace(&mut self.inner, Closed).unwrap(),
|
||
mode,
|
||
password.as_bytes(),
|
||
)?;
|
||
self.inner = Storer(MaybeEncrypted::Aes(aeswriter));
|
||
}
|
||
Some(EncryptWith::ZipCrypto(keys, ..)) => {
|
||
let mut zipwriter = crate::zipcrypto::ZipCryptoWriter {
|
||
writer: mem::replace(&mut self.inner, Closed).unwrap(),
|
||
buffer: vec![],
|
||
keys,
|
||
};
|
||
self.stats.start = zipwriter.writer.stream_position()?;
|
||
// crypto_header is counted as part of the data
|
||
let crypto_header = [0u8; 12];
|
||
let result = zipwriter.write_all(&crypto_header);
|
||
self.ok_or_abort_file(result)?;
|
||
self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter));
|
||
}
|
||
None => {}
|
||
}
|
||
let file = &mut self.files[index];
|
||
debug_assert!(file.data_start.get().is_none());
|
||
file.data_start.get_or_init(|| self.stats.start);
|
||
self.stats.bytes_written = 0;
|
||
self.stats.hasher = Hasher::new();
|
||
Ok(())
|
||
}
|
||
|
||
fn insert_file_data(&mut self, file: ZipFileData) -> ZipResult<usize> {
|
||
if self.files.contains_key(&file.file_name) {
|
||
return Err(InvalidArchive("Duplicate filename"));
|
||
}
|
||
let name = file.file_name.to_owned();
|
||
self.files.insert(name.clone(), file);
|
||
Ok(self.files.get_index_of(&name).unwrap())
|
||
}
|
||
|
||
fn finish_file(&mut self) -> ZipResult<()> {
|
||
if !self.writing_to_file {
|
||
return Ok(());
|
||
}
|
||
|
||
let make_plain_writer = self.inner.prepare_next_writer(
|
||
Stored,
|
||
None,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
None,
|
||
)?;
|
||
self.inner.switch_to(make_plain_writer)?;
|
||
self.switch_to_non_encrypting_writer()?;
|
||
let writer = self.inner.get_plain();
|
||
|
||
if !self.writing_raw {
|
||
let file = match self.files.last_mut() {
|
||
None => return Ok(()),
|
||
Some((_, f)) => f,
|
||
};
|
||
file.uncompressed_size = self.stats.bytes_written;
|
||
|
||
let file_end = writer.stream_position()?;
|
||
debug_assert!(file_end >= self.stats.start);
|
||
file.compressed_size = file_end - self.stats.start;
|
||
let mut crc = true;
|
||
if let Some(aes_mode) = &mut file.aes_mode {
|
||
// We prefer using AE-1 which provides an extra CRC check, but for small files we
|
||
// switch to AE-2 to prevent being able to use the CRC value to to reconstruct the
|
||
// unencrypted contents.
|
||
//
|
||
// C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq
|
||
aes_mode.1 = if self.stats.bytes_written < 20 {
|
||
crc = false;
|
||
AesVendorVersion::Ae2
|
||
} else {
|
||
AesVendorVersion::Ae1
|
||
};
|
||
}
|
||
file.crc32 = if crc {
|
||
self.stats.hasher.clone().finalize()
|
||
} else {
|
||
0
|
||
};
|
||
update_aes_extra_data(writer, file)?;
|
||
update_local_file_header(writer, file)?;
|
||
writer.seek(SeekFrom::Start(file_end))?;
|
||
}
|
||
if self.flush_on_finish_file {
|
||
let result = writer.flush();
|
||
self.ok_or_abort_file(result)?;
|
||
}
|
||
|
||
self.writing_to_file = false;
|
||
Ok(())
|
||
}
|
||
|
||
fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> {
|
||
match mem::replace(&mut self.inner, Closed) {
|
||
#[cfg(feature = "aes-crypto")]
|
||
Storer(MaybeEncrypted::Aes(writer)) => {
|
||
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish()?));
|
||
}
|
||
Storer(MaybeEncrypted::ZipCrypto(writer)) => {
|
||
let crc32 = self.stats.hasher.clone().finalize();
|
||
self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
|
||
}
|
||
Storer(MaybeEncrypted::Unencrypted(w)) => {
|
||
self.inner = Storer(MaybeEncrypted::Unencrypted(w))
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Removes the file currently being written from the archive if there is one, or else removes
|
||
/// the file most recently written.
|
||
pub fn abort_file(&mut self) -> ZipResult<()> {
|
||
let (_, last_file) = self.files.pop().ok_or(ZipError::FileNotFound)?;
|
||
let make_plain_writer = self.inner.prepare_next_writer(
|
||
Stored,
|
||
None,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
None,
|
||
)?;
|
||
self.inner.switch_to(make_plain_writer)?;
|
||
self.switch_to_non_encrypting_writer()?;
|
||
// Make sure this is the last file, and that no shallow copies of it remain; otherwise we'd
|
||
// overwrite a valid file and corrupt the archive
|
||
let rewind_safe: bool = match last_file.data_start.get() {
|
||
None => self.files.is_empty(),
|
||
Some(last_file_start) => self.files.values().all(|file| {
|
||
file.data_start
|
||
.get()
|
||
.is_some_and(|start| start < last_file_start)
|
||
}),
|
||
};
|
||
if rewind_safe {
|
||
self.inner
|
||
.get_plain()
|
||
.seek(SeekFrom::Start(last_file.header_start))?;
|
||
}
|
||
self.writing_to_file = false;
|
||
Ok(())
|
||
}
|
||
|
||
/// Create a file in the archive and start writing its' contents. The file must not have the
|
||
/// same name as a file already in the archive.
|
||
///
|
||
/// The data should be written using the [`Write`] implementation on this [`ZipWriter`]
|
||
pub fn start_file<S: ToString, T: FileOptionExtension>(
|
||
&mut self,
|
||
name: S,
|
||
mut options: FileOptions<T>,
|
||
) -> ZipResult<()> {
|
||
Self::normalize_options(&mut options);
|
||
let make_new_self = self.inner.prepare_next_writer(
|
||
options.compression_method,
|
||
options.compression_level,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
options.zopfli_buffer_size,
|
||
)?;
|
||
self.start_entry(name, options, None)?;
|
||
let result = self.inner.switch_to(make_new_self);
|
||
self.ok_or_abort_file(result)?;
|
||
self.writing_raw = false;
|
||
Ok(())
|
||
}
|
||
|
||
/* TODO: link to/use Self::finish_into_readable() from https://github.com/zip-rs/zip/pull/400 in
|
||
* this docstring. */
|
||
/// Copy over the entire contents of another archive verbatim.
|
||
///
|
||
/// This method extracts file metadata from the `source` archive, then simply performs a single
|
||
/// big [`io::copy()`](io::copy) to transfer all the actual file contents without any
|
||
/// decompression or decryption. This is more performant than the equivalent operation of
|
||
/// calling [`Self::raw_copy_file()`] for each entry from the `source` archive in sequence.
|
||
///
|
||
///```
|
||
/// # fn main() -> Result<(), zip::result::ZipError> {
|
||
/// use std::io::{Cursor, prelude::*};
|
||
/// use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
|
||
///
|
||
/// let buf = Cursor::new(Vec::new());
|
||
/// let mut zip = ZipWriter::new(buf);
|
||
/// zip.start_file("a.txt", SimpleFileOptions::default())?;
|
||
/// zip.write_all(b"hello\n")?;
|
||
/// let src = ZipArchive::new(zip.finish()?)?;
|
||
///
|
||
/// let buf = Cursor::new(Vec::new());
|
||
/// let mut zip = ZipWriter::new(buf);
|
||
/// zip.start_file("b.txt", SimpleFileOptions::default())?;
|
||
/// zip.write_all(b"hey\n")?;
|
||
/// let src2 = ZipArchive::new(zip.finish()?)?;
|
||
///
|
||
/// let buf = Cursor::new(Vec::new());
|
||
/// let mut zip = ZipWriter::new(buf);
|
||
/// zip.merge_archive(src)?;
|
||
/// zip.merge_archive(src2)?;
|
||
/// let mut result = ZipArchive::new(zip.finish()?)?;
|
||
///
|
||
/// let mut s: String = String::new();
|
||
/// result.by_name("a.txt")?.read_to_string(&mut s)?;
|
||
/// assert_eq!(s, "hello\n");
|
||
/// s.clear();
|
||
/// result.by_name("b.txt")?.read_to_string(&mut s)?;
|
||
/// assert_eq!(s, "hey\n");
|
||
/// # Ok(())
|
||
/// # }
|
||
///```
|
||
pub fn merge_archive<R>(&mut self, mut source: ZipArchive<R>) -> ZipResult<()>
|
||
where
|
||
R: Read + Seek,
|
||
{
|
||
self.finish_file()?;
|
||
|
||
/* Ensure we accept the file contents on faith (and avoid overwriting the data).
|
||
* See raw_copy_file_rename(). */
|
||
self.writing_to_file = true;
|
||
self.writing_raw = true;
|
||
|
||
let writer = self.inner.get_plain();
|
||
/* Get the file entries from the source archive. */
|
||
let new_files = source.merge_contents(writer)?;
|
||
|
||
/* These file entries are now ours! */
|
||
self.files.extend(new_files);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn normalize_options<T: FileOptionExtension>(options: &mut FileOptions<T>) {
|
||
if options.permissions.is_none() {
|
||
options.permissions = Some(0o644);
|
||
}
|
||
if !options.last_modified_time.is_valid() {
|
||
options.last_modified_time = FileOptions::<T>::default().last_modified_time;
|
||
}
|
||
*options.permissions.as_mut().unwrap() |= ffi::S_IFREG;
|
||
}
|
||
|
||
/// Starts a file, taking a Path as argument.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn start_file_from_path<E: FileOptionExtension, P: AsRef<Path>>(
|
||
&mut self,
|
||
path: P,
|
||
options: FileOptions<E>,
|
||
) -> ZipResult<()> {
|
||
self.start_file(path_to_string(path), options)
|
||
}
|
||
|
||
/// Add a new file using the already compressed data from a ZIP file being read and renames it, this
|
||
/// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again.
|
||
/// Any `ZipFile` metadata is copied and not checked, for example the file CRC.
|
||
|
||
/// ```no_run
|
||
/// use std::fs::File;
|
||
/// use std::io::{Read, Seek, Write};
|
||
/// use zip::{ZipArchive, ZipWriter};
|
||
///
|
||
/// fn copy_rename<R, W>(
|
||
/// src: &mut ZipArchive<R>,
|
||
/// dst: &mut ZipWriter<W>,
|
||
/// ) -> zip::result::ZipResult<()>
|
||
/// where
|
||
/// R: Read + Seek,
|
||
/// W: Write + Seek,
|
||
/// {
|
||
/// // Retrieve file entry by name
|
||
/// let file = src.by_name("src_file.txt")?;
|
||
///
|
||
/// // Copy and rename the previously obtained file entry to the destination zip archive
|
||
/// dst.raw_copy_file_rename(file, "new_name.txt")?;
|
||
///
|
||
/// Ok(())
|
||
/// }
|
||
/// ```
|
||
pub fn raw_copy_file_rename<S: ToString>(
|
||
&mut self,
|
||
mut file: ZipFile,
|
||
name: S,
|
||
) -> ZipResult<()> {
|
||
let mut options = SimpleFileOptions::default()
|
||
.large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
|
||
.last_modified_time(
|
||
file.last_modified()
|
||
.unwrap_or_else(DateTime::default_for_write),
|
||
)
|
||
.compression_method(file.compression());
|
||
if let Some(perms) = file.unix_mode() {
|
||
options = options.unix_permissions(perms);
|
||
}
|
||
Self::normalize_options(&mut options);
|
||
|
||
let raw_values = ZipRawValues {
|
||
crc32: file.crc32(),
|
||
compressed_size: file.compressed_size(),
|
||
uncompressed_size: file.size(),
|
||
};
|
||
|
||
self.start_entry(name, options, Some(raw_values))?;
|
||
self.writing_to_file = true;
|
||
self.writing_raw = true;
|
||
|
||
io::copy(&mut file.take_raw_reader()?, self)?;
|
||
self.finish_file()
|
||
}
|
||
|
||
/// Like `raw_copy_file_to_path`, but uses Path arguments.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn raw_copy_file_to_path<P: AsRef<Path>>(
|
||
&mut self,
|
||
file: ZipFile,
|
||
path: P,
|
||
) -> ZipResult<()> {
|
||
self.raw_copy_file_rename(file, path_to_string(path))
|
||
}
|
||
|
||
/// Add a new file using the already compressed data from a ZIP file being read, this allows faster
|
||
/// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile`
|
||
/// metadata is copied and not checked, for example the file CRC.
|
||
///
|
||
/// ```no_run
|
||
/// use std::fs::File;
|
||
/// use std::io::{Read, Seek, Write};
|
||
/// use zip::{ZipArchive, ZipWriter};
|
||
///
|
||
/// fn copy<R, W>(src: &mut ZipArchive<R>, dst: &mut ZipWriter<W>) -> zip::result::ZipResult<()>
|
||
/// where
|
||
/// R: Read + Seek,
|
||
/// W: Write + Seek,
|
||
/// {
|
||
/// // Retrieve file entry by name
|
||
/// let file = src.by_name("src_file.txt")?;
|
||
///
|
||
/// // Copy the previously obtained file entry to the destination zip archive
|
||
/// dst.raw_copy_file(file)?;
|
||
///
|
||
/// Ok(())
|
||
/// }
|
||
/// ```
|
||
pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> {
|
||
let name = file.name().to_owned();
|
||
self.raw_copy_file_rename(file, name)
|
||
}
|
||
|
||
/// Add a directory entry.
|
||
///
|
||
/// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file.
|
||
pub fn add_directory<S, T: FileOptionExtension>(
|
||
&mut self,
|
||
name: S,
|
||
mut options: FileOptions<T>,
|
||
) -> ZipResult<()>
|
||
where
|
||
S: Into<String>,
|
||
{
|
||
if options.permissions.is_none() {
|
||
options.permissions = Some(0o755);
|
||
}
|
||
*options.permissions.as_mut().unwrap() |= 0o40000;
|
||
options.compression_method = Stored;
|
||
options.encrypt_with = None;
|
||
|
||
let name_as_string = name.into();
|
||
// Append a slash to the filename if it does not end with it.
|
||
let name_with_slash = match name_as_string.chars().last() {
|
||
Some('/') | Some('\\') => name_as_string,
|
||
_ => name_as_string + "/",
|
||
};
|
||
|
||
self.start_entry(name_with_slash, options, None)?;
|
||
self.writing_to_file = false;
|
||
self.switch_to_non_encrypting_writer()?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Add a directory entry, taking a Path as argument.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn add_directory_from_path<T: FileOptionExtension, P: AsRef<Path>>(
|
||
&mut self,
|
||
path: P,
|
||
options: FileOptions<T>,
|
||
) -> ZipResult<()> {
|
||
self.add_directory(path_to_string(path), options)
|
||
}
|
||
|
||
/// Finish the last file and write all other zip-structures
|
||
///
|
||
/// This will return the writer, but one should normally not append any data to the end of the file.
|
||
/// Note that the zipfile will also be finished on drop.
|
||
pub fn finish(mut self) -> ZipResult<W> {
|
||
let _central_start = self.finalize()?;
|
||
let inner = mem::replace(&mut self.inner, Closed);
|
||
Ok(inner.unwrap())
|
||
}
|
||
|
||
/// Add a symlink entry.
|
||
///
|
||
/// The zip archive will contain an entry for path `name` which is a symlink to `target`.
|
||
///
|
||
/// No validation or normalization of the paths is performed. For best results,
|
||
/// callers should normalize `\` to `/` and ensure symlinks are relative to other
|
||
/// paths within the zip archive.
|
||
///
|
||
/// WARNING: not all zip implementations preserve symlinks on extract. Some zip
|
||
/// implementations may materialize a symlink as a regular file, possibly with the
|
||
/// content incorrectly set to the symlink target. For maximum portability, consider
|
||
/// storing a regular file instead.
|
||
pub fn add_symlink<N: ToString, T: ToString, E: FileOptionExtension>(
|
||
&mut self,
|
||
name: N,
|
||
target: T,
|
||
mut options: FileOptions<E>,
|
||
) -> ZipResult<()> {
|
||
if options.permissions.is_none() {
|
||
options.permissions = Some(0o777);
|
||
}
|
||
*options.permissions.as_mut().unwrap() |= S_IFLNK;
|
||
// The symlink target is stored as file content. And compressing the target path
|
||
// likely wastes space. So always store.
|
||
options.compression_method = Stored;
|
||
|
||
self.start_entry(name, options, None)?;
|
||
self.writing_to_file = true;
|
||
let result = self.write_all(target.to_string().as_bytes());
|
||
self.ok_or_abort_file(result)?;
|
||
self.writing_raw = false;
|
||
self.finish_file()?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Add a symlink entry, taking Paths to the location and target as arguments.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn add_symlink_from_path<P: AsRef<Path>, T: AsRef<Path>, E: FileOptionExtension>(
|
||
&mut self,
|
||
path: P,
|
||
target: T,
|
||
options: FileOptions<E>,
|
||
) -> ZipResult<()> {
|
||
self.add_symlink(path_to_string(path), path_to_string(target), options)
|
||
}
|
||
|
||
fn finalize(&mut self) -> ZipResult<u64> {
|
||
self.finish_file()?;
|
||
|
||
let mut central_start = self.write_central_and_footer()?;
|
||
let writer = self.inner.get_plain();
|
||
let footer_end = writer.stream_position()?;
|
||
let archive_end = writer.seek(SeekFrom::End(0))?;
|
||
if footer_end < archive_end {
|
||
// Data from an aborted file is past the end of the footer.
|
||
|
||
// Overwrite the magic so the footer is no longer valid.
|
||
writer.seek(SeekFrom::Start(central_start))?;
|
||
writer.write_u32_le(0)?;
|
||
writer.seek(SeekFrom::Start(
|
||
footer_end - size_of::<Zip32CDEBlock>() as u64 - self.comment.len() as u64,
|
||
))?;
|
||
writer.write_u32_le(0)?;
|
||
|
||
// Rewrite the footer at the actual end.
|
||
let central_and_footer_size = footer_end - central_start;
|
||
writer.seek(SeekFrom::End(-(central_and_footer_size as i64)))?;
|
||
central_start = self.write_central_and_footer()?;
|
||
}
|
||
|
||
Ok(central_start)
|
||
}
|
||
|
||
fn write_central_and_footer(&mut self) -> Result<u64, ZipError> {
|
||
let writer = self.inner.get_plain();
|
||
|
||
let mut version_needed = MIN_VERSION as u16;
|
||
let central_start = writer.stream_position()?;
|
||
for file in self.files.values() {
|
||
write_central_directory_header(writer, file)?;
|
||
version_needed = version_needed.max(file.version_needed());
|
||
}
|
||
let central_size = writer.stream_position()? - central_start;
|
||
|
||
if self.files.len() > spec::ZIP64_ENTRY_THR
|
||
|| central_size.max(central_start) > spec::ZIP64_BYTES_THR
|
||
{
|
||
let zip64_footer = spec::Zip64CentralDirectoryEnd {
|
||
version_made_by: version_needed,
|
||
version_needed_to_extract: version_needed,
|
||
disk_number: 0,
|
||
disk_with_central_directory: 0,
|
||
number_of_files_on_this_disk: self.files.len() as u64,
|
||
number_of_files: self.files.len() as u64,
|
||
central_directory_size: central_size,
|
||
central_directory_offset: central_start,
|
||
};
|
||
|
||
zip64_footer.write(writer)?;
|
||
|
||
let zip64_footer = spec::Zip64CentralDirectoryEndLocator {
|
||
disk_with_central_directory: 0,
|
||
end_of_central_directory_offset: central_start + central_size,
|
||
number_of_disks: 1,
|
||
};
|
||
|
||
zip64_footer.write(writer)?;
|
||
}
|
||
|
||
let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16;
|
||
let footer = spec::Zip32CentralDirectoryEnd {
|
||
disk_number: 0,
|
||
disk_with_central_directory: 0,
|
||
zip_file_comment: self.comment.clone(),
|
||
number_of_files_on_this_disk: number_of_files,
|
||
number_of_files,
|
||
central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32,
|
||
central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32,
|
||
};
|
||
|
||
footer.write(writer)?;
|
||
Ok(central_start)
|
||
}
|
||
|
||
fn index_by_name(&self, name: &str) -> ZipResult<usize> {
|
||
self.files.get_index_of(name).ok_or(ZipError::FileNotFound)
|
||
}
|
||
|
||
/// Adds another entry to the central directory referring to the same content as an existing
|
||
/// entry. The file's local-file header will still refer to it by its original name, so
|
||
/// unzipping the file will technically be unspecified behavior. [ZipArchive] ignores the
|
||
/// filename in the local-file header and treat the central directory as authoritative. However,
|
||
/// some other software (e.g. Minecraft) will refuse to extract a file copied this way.
|
||
pub fn shallow_copy_file(&mut self, src_name: &str, dest_name: &str) -> ZipResult<()> {
|
||
self.finish_file()?;
|
||
if src_name == dest_name {
|
||
return Err(InvalidArchive("Trying to copy a file to itself"));
|
||
}
|
||
let src_index = self.index_by_name(src_name)?;
|
||
let mut dest_data = self.files[src_index].to_owned();
|
||
dest_data.file_name = dest_name.to_string().into();
|
||
dest_data.file_name_raw = dest_name.to_string().into_bytes().into();
|
||
dest_data.central_header_start = 0;
|
||
self.insert_file_data(dest_data)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Like `shallow_copy_file`, but uses Path arguments.
|
||
///
|
||
/// This function ensures that the '/' path separator is used and normalizes `.` and `..`. It
|
||
/// ignores any `..` or Windows drive letter that would produce a path outside the ZIP file's
|
||
/// root.
|
||
pub fn shallow_copy_file_from_path<T: AsRef<Path>, U: AsRef<Path>>(
|
||
&mut self,
|
||
src_path: T,
|
||
dest_path: U,
|
||
) -> ZipResult<()> {
|
||
self.shallow_copy_file(&path_to_string(src_path), &path_to_string(dest_path))
|
||
}
|
||
}
|
||
|
||
impl<W: Write + Seek> Drop for ZipWriter<W> {
|
||
fn drop(&mut self) {
|
||
if !self.inner.is_closed() {
|
||
if let Err(e) = self.finalize() {
|
||
let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
type SwitchWriterFunction<W> = Box<dyn FnOnce(MaybeEncrypted<W>) -> GenericZipWriter<W>>;
|
||
|
||
impl<W: Write + Seek> GenericZipWriter<W> {
|
||
fn prepare_next_writer(
|
||
&self,
|
||
compression: CompressionMethod,
|
||
compression_level: Option<i64>,
|
||
#[cfg(feature = "deflate-zopfli")] zopfli_buffer_size: Option<usize>,
|
||
) -> ZipResult<SwitchWriterFunction<W>> {
|
||
if let Closed = self {
|
||
return Err(
|
||
io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed").into(),
|
||
);
|
||
}
|
||
|
||
{
|
||
#[allow(deprecated)]
|
||
#[allow(unreachable_code)]
|
||
match compression {
|
||
Stored => {
|
||
if compression_level.is_some() {
|
||
Err(UnsupportedArchive("Unsupported compression level"))
|
||
} else {
|
||
Ok(Box::new(|bare| Storer(bare)))
|
||
}
|
||
}
|
||
#[cfg(feature = "_deflate-any")]
|
||
CompressionMethod::Deflated => {
|
||
let default = if cfg!(all(
|
||
feature = "deflate-zopfli",
|
||
not(feature = "deflate-flate2")
|
||
)) {
|
||
24
|
||
} else {
|
||
Compression::default().level() as i64
|
||
};
|
||
|
||
let level = clamp_opt(
|
||
compression_level.unwrap_or(default),
|
||
deflate_compression_level_range(),
|
||
)
|
||
.ok_or(UnsupportedArchive("Unsupported compression level"))?
|
||
as u32;
|
||
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
{
|
||
let best_non_zopfli = Compression::best().level();
|
||
if level > best_non_zopfli {
|
||
let options = Options {
|
||
iteration_count: NonZeroU64::try_from(
|
||
(level - best_non_zopfli) as u64,
|
||
)
|
||
.unwrap(),
|
||
..Default::default()
|
||
};
|
||
return Ok(Box::new(move |bare| match zopfli_buffer_size {
|
||
Some(size) => GenericZipWriter::BufferedZopfliDeflater(
|
||
BufWriter::with_capacity(
|
||
size,
|
||
zopfli::DeflateEncoder::new(
|
||
options,
|
||
Default::default(),
|
||
bare,
|
||
),
|
||
),
|
||
),
|
||
None => GenericZipWriter::ZopfliDeflater(
|
||
zopfli::DeflateEncoder::new(options, Default::default(), bare),
|
||
),
|
||
}));
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "deflate-flate2")]
|
||
{
|
||
Ok(Box::new(move |bare| {
|
||
GenericZipWriter::Deflater(DeflateEncoder::new(
|
||
bare,
|
||
Compression::new(level),
|
||
))
|
||
}))
|
||
}
|
||
}
|
||
#[cfg(feature = "deflate64")]
|
||
CompressionMethod::Deflate64 => {
|
||
Err(UnsupportedArchive("Compressing Deflate64 is not supported"))
|
||
}
|
||
#[cfg(feature = "bzip2")]
|
||
CompressionMethod::Bzip2 => {
|
||
let level = clamp_opt(
|
||
compression_level.unwrap_or(bzip2::Compression::default().level() as i64),
|
||
bzip2_compression_level_range(),
|
||
)
|
||
.ok_or(UnsupportedArchive("Unsupported compression level"))?
|
||
as u32;
|
||
Ok(Box::new(move |bare| {
|
||
GenericZipWriter::Bzip2(BzEncoder::new(
|
||
bare,
|
||
bzip2::Compression::new(level),
|
||
))
|
||
}))
|
||
}
|
||
CompressionMethod::AES => Err(UnsupportedArchive(
|
||
"AES encryption is enabled through FileOptions::with_aes_encryption",
|
||
)),
|
||
#[cfg(feature = "zstd")]
|
||
CompressionMethod::Zstd => {
|
||
let level = clamp_opt(
|
||
compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL as i64),
|
||
zstd::compression_level_range(),
|
||
)
|
||
.ok_or(UnsupportedArchive("Unsupported compression level"))?;
|
||
Ok(Box::new(move |bare| {
|
||
GenericZipWriter::Zstd(ZstdEncoder::new(bare, level as i32).unwrap())
|
||
}))
|
||
}
|
||
#[cfg(feature = "lzma")]
|
||
CompressionMethod::Lzma => {
|
||
Err(UnsupportedArchive("LZMA isn't supported for compression"))
|
||
}
|
||
#[cfg(feature = "xz")]
|
||
CompressionMethod::Xz => {
|
||
Err(UnsupportedArchive("XZ isn't supported for compression"))
|
||
}
|
||
CompressionMethod::Unsupported(..) => {
|
||
Err(UnsupportedArchive("Unsupported compression"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn switch_to(&mut self, make_new_self: SwitchWriterFunction<W>) -> ZipResult<()> {
|
||
let bare = match mem::replace(self, Closed) {
|
||
Storer(w) => w,
|
||
#[cfg(feature = "deflate-flate2")]
|
||
GenericZipWriter::Deflater(w) => w.finish()?,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::ZopfliDeflater(w) => w.finish()?,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::BufferedZopfliDeflater(w) => w
|
||
.into_inner()
|
||
.map_err(|e| ZipError::Io(e.into_error()))?
|
||
.finish()?,
|
||
#[cfg(feature = "bzip2")]
|
||
GenericZipWriter::Bzip2(w) => w.finish()?,
|
||
#[cfg(feature = "zstd")]
|
||
GenericZipWriter::Zstd(w) => w.finish()?,
|
||
Closed => {
|
||
return Err(io::Error::new(
|
||
io::ErrorKind::BrokenPipe,
|
||
"ZipWriter was already closed",
|
||
)
|
||
.into());
|
||
}
|
||
};
|
||
*self = make_new_self(bare);
|
||
Ok(())
|
||
}
|
||
|
||
fn ref_mut(&mut self) -> Option<&mut dyn Write> {
|
||
match self {
|
||
Storer(ref mut w) => Some(w as &mut dyn Write),
|
||
#[cfg(feature = "deflate-flate2")]
|
||
GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write),
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::ZopfliDeflater(w) => Some(w as &mut dyn Write),
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
GenericZipWriter::BufferedZopfliDeflater(w) => Some(w as &mut dyn Write),
|
||
#[cfg(feature = "bzip2")]
|
||
GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write),
|
||
#[cfg(feature = "zstd")]
|
||
GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write),
|
||
Closed => None,
|
||
}
|
||
}
|
||
|
||
const fn is_closed(&self) -> bool {
|
||
matches!(*self, Closed)
|
||
}
|
||
|
||
fn get_plain(&mut self) -> &mut W {
|
||
match *self {
|
||
Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w,
|
||
_ => panic!("Should have switched to stored and unencrypted beforehand"),
|
||
}
|
||
}
|
||
|
||
fn unwrap(self) -> W {
|
||
match self {
|
||
Storer(MaybeEncrypted::Unencrypted(w)) => w,
|
||
_ => panic!("Should have switched to stored and unencrypted beforehand"),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(feature = "_deflate-any")]
|
||
fn deflate_compression_level_range() -> std::ops::RangeInclusive<i64> {
|
||
let min = if cfg!(feature = "deflate-flate2") {
|
||
Compression::fast().level() as i64
|
||
} else {
|
||
Compression::best().level() as i64 + 1
|
||
};
|
||
|
||
let max = Compression::best().level() as i64
|
||
+ if cfg!(feature = "deflate-zopfli") {
|
||
u8::MAX as i64
|
||
} else {
|
||
0
|
||
};
|
||
|
||
min..=max
|
||
}
|
||
|
||
#[cfg(feature = "bzip2")]
|
||
fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i64> {
|
||
let min = bzip2::Compression::fast().level() as i64;
|
||
let max = bzip2::Compression::best().level() as i64;
|
||
min..=max
|
||
}
|
||
|
||
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd"))]
|
||
fn clamp_opt<T: Ord + Copy, U: Ord + Copy + TryFrom<T>>(
|
||
value: T,
|
||
range: std::ops::RangeInclusive<U>,
|
||
) -> Option<T> {
|
||
if range.contains(&value.try_into().ok()?) {
|
||
Some(value)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
fn update_aes_extra_data<W: Write + Seek>(writer: &mut W, file: &mut ZipFileData) -> ZipResult<()> {
|
||
let Some((aes_mode, version, compression_method)) = file.aes_mode else {
|
||
return Ok(());
|
||
};
|
||
|
||
let extra_data_start = file.extra_data_start.unwrap();
|
||
|
||
writer.seek(SeekFrom::Start(
|
||
extra_data_start + file.aes_extra_data_start,
|
||
))?;
|
||
|
||
let mut buf = Vec::new();
|
||
|
||
/* TODO: implement this using the Block trait! */
|
||
// Extra field header ID.
|
||
buf.write_u16_le(0x9901)?;
|
||
// Data size.
|
||
buf.write_u16_le(7)?;
|
||
// Integer version number.
|
||
buf.write_u16_le(version as u16)?;
|
||
// Vendor ID.
|
||
buf.write_all(b"AE")?;
|
||
// AES encryption strength.
|
||
buf.write_all(&[aes_mode as u8])?;
|
||
// Real compression method.
|
||
buf.write_u16_le(compression_method.serialize_to_u16())?;
|
||
|
||
writer.write_all(&buf)?;
|
||
|
||
let aes_extra_data_start = file.aes_extra_data_start as usize;
|
||
let extra_field = Arc::get_mut(file.extra_field.as_mut().unwrap()).unwrap();
|
||
extra_field[aes_extra_data_start..aes_extra_data_start + buf.len()].copy_from_slice(&buf);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
||
const CRC32_OFFSET: u64 = 14;
|
||
writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
|
||
writer.write_u32_le(file.crc32)?;
|
||
if file.large_file {
|
||
update_local_zip64_extra_field(writer, file)?;
|
||
} else {
|
||
// check compressed size as well as it can also be slightly larger than uncompressed size
|
||
if file.compressed_size > spec::ZIP64_BYTES_THR {
|
||
return Err(ZipError::Io(io::Error::new(
|
||
io::ErrorKind::Other,
|
||
"Large file option has not been set",
|
||
)));
|
||
}
|
||
writer.write_u32_le(file.compressed_size as u32)?;
|
||
// uncompressed size is already checked on write to catch it as soon as possible
|
||
writer.write_u32_le(file.uncompressed_size as u32)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
||
let block = file.block()?;
|
||
block.write(writer)?;
|
||
// file name
|
||
writer.write_all(&file.file_name_raw)?;
|
||
// extra field
|
||
if let Some(extra_field) = &file.extra_field {
|
||
writer.write_all(extra_field)?;
|
||
}
|
||
if let Some(central_extra_field) = &file.central_extra_field {
|
||
writer.write_all(central_extra_field)?;
|
||
}
|
||
// file comment
|
||
writer.write_all(file.file_comment.as_bytes())?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn update_local_zip64_extra_field<T: Write + Seek>(
|
||
writer: &mut T,
|
||
file: &ZipFileData,
|
||
) -> ZipResult<()> {
|
||
let block = file.zip64_extra_field_block().ok_or(InvalidArchive(
|
||
"Attempted to update a nonexistent ZIP64 extra field",
|
||
))?;
|
||
|
||
let zip64_extra_field_start = file.header_start
|
||
+ size_of::<ZipLocalEntryBlock>() as u64
|
||
+ file.file_name_raw.len() as u64;
|
||
|
||
writer.seek(SeekFrom::Start(zip64_extra_field_start))?;
|
||
let block = block.serialize();
|
||
writer.write_all(&block)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(not(feature = "unreserved"))]
|
||
const EXTRA_FIELD_MAPPING: [u16; 43] = [
|
||
0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, 0x0017,
|
||
0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605, 0x2705,
|
||
0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356, 0x554e,
|
||
0x5855, 0x6542, 0x756e, 0x7855, 0xa220, 0xfd4a, 0x9902,
|
||
];
|
||
|
||
#[cfg(test)]
|
||
#[allow(unknown_lints)] // needless_update is new in clippy pre 1.29.0
|
||
#[allow(clippy::needless_update)] // So we can use the same FileOptions decls with and without zopfli_buffer_size
|
||
#[allow(clippy::octal_escapes)] // many false positives in converted fuzz cases
|
||
mod test {
|
||
use super::{ExtendedFileOptions, FileOptions, FullFileOptions, ZipWriter};
|
||
use crate::compression::CompressionMethod;
|
||
use crate::result::ZipResult;
|
||
use crate::types::DateTime;
|
||
use crate::write::EncryptWith::ZipCrypto;
|
||
use crate::write::SimpleFileOptions;
|
||
use crate::zipcrypto::ZipCryptoKeys;
|
||
use crate::CompressionMethod::Stored;
|
||
use crate::ZipArchive;
|
||
use std::io::{Cursor, Read, Write};
|
||
use std::marker::PhantomData;
|
||
use std::path::PathBuf;
|
||
|
||
#[test]
|
||
fn write_empty_zip() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_comment("ZIP");
|
||
let result = writer.finish().unwrap();
|
||
assert_eq!(result.get_ref().len(), 25);
|
||
assert_eq!(
|
||
*result.get_ref(),
|
||
[80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 90, 73, 80]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn unix_permissions_bitmask() {
|
||
// unix_permissions() throws away upper bits.
|
||
let options = SimpleFileOptions::default().unix_permissions(0o120777);
|
||
assert_eq!(options.permissions, Some(0o777));
|
||
}
|
||
|
||
#[test]
|
||
fn write_zip_dir() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.add_directory(
|
||
"test",
|
||
SimpleFileOptions::default().last_modified_time(
|
||
DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
|
||
),
|
||
)
|
||
.unwrap();
|
||
assert!(writer
|
||
.write(b"writing to a directory is not allowed, and will not write any data")
|
||
.is_err());
|
||
let result = writer.finish().unwrap();
|
||
assert_eq!(result.get_ref().len(), 108);
|
||
assert_eq!(
|
||
*result.get_ref(),
|
||
&[
|
||
80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 20, 3, 20, 0, 0, 0, 0, 0,
|
||
163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0,
|
||
1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0,
|
||
] as &[u8]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_symlink_simple() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.add_symlink(
|
||
"name",
|
||
"target",
|
||
SimpleFileOptions::default().last_modified_time(
|
||
DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
|
||
),
|
||
)
|
||
.unwrap();
|
||
assert!(writer
|
||
.write(b"writing to a symlink is not allowed and will not write any data")
|
||
.is_err());
|
||
let result = writer.finish().unwrap();
|
||
assert_eq!(result.get_ref().len(), 112);
|
||
assert_eq!(
|
||
*result.get_ref(),
|
||
&[
|
||
80u8, 75, 3, 4, 10, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0,
|
||
6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1,
|
||
2, 10, 3, 10, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0,
|
||
0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101,
|
||
80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0
|
||
] as &[u8],
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_path_normalization() {
|
||
let mut path = PathBuf::new();
|
||
path.push("foo");
|
||
path.push("bar");
|
||
path.push("..");
|
||
path.push(".");
|
||
path.push("example.txt");
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file_from_path(path, SimpleFileOptions::default())
|
||
.unwrap();
|
||
let archive = writer.finish_into_readable().unwrap();
|
||
assert_eq!(Some("foo/example.txt"), archive.name_for_index(0));
|
||
}
|
||
|
||
#[test]
|
||
fn write_symlink_wonky_paths() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.add_symlink(
|
||
"directory\\link",
|
||
"/absolute/symlink\\with\\mixed/slashes",
|
||
SimpleFileOptions::default().last_modified_time(
|
||
DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(),
|
||
),
|
||
)
|
||
.unwrap();
|
||
assert!(writer
|
||
.write(b"writing to a symlink is not allowed and will not write any data")
|
||
.is_err());
|
||
let result = writer.finish().unwrap();
|
||
assert_eq!(result.get_ref().len(), 162);
|
||
assert_eq!(
|
||
*result.get_ref(),
|
||
&[
|
||
80u8, 75, 3, 4, 10, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0,
|
||
36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105,
|
||
110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105,
|
||
110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97,
|
||
115, 104, 101, 115, 80, 75, 1, 2, 10, 3, 10, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95,
|
||
41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
|
||
161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110,
|
||
107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0
|
||
] as &[u8],
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn write_mimetype_zip() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: Some(33188),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: (),
|
||
alignment: 1,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: None,
|
||
};
|
||
writer.start_file("mimetype", options).unwrap();
|
||
writer
|
||
.write_all(b"application/vnd.oasis.opendocument.text")
|
||
.unwrap();
|
||
let result = writer.finish().unwrap();
|
||
|
||
assert_eq!(result.get_ref().len(), 153);
|
||
let mut v = Vec::new();
|
||
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
|
||
assert_eq!(result.get_ref(), &v);
|
||
}
|
||
|
||
const RT_TEST_TEXT: &str = "And I can't stop thinking about the moments that I lost to you\
|
||
And I can't stop thinking of things I used to do\
|
||
And I can't stop making bad decisions\
|
||
And I can't stop eating stuff you make me chew\
|
||
I put on a smile like you wanna see\
|
||
Another day goes by that I long to be like you";
|
||
const RT_TEST_FILENAME: &str = "subfolder/sub-subfolder/can't_stop.txt";
|
||
const SECOND_FILENAME: &str = "different_name.xyz";
|
||
const THIRD_FILENAME: &str = "third_name.xyz";
|
||
|
||
#[test]
|
||
fn write_non_utf8() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: Some(33188),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: (),
|
||
alignment: 1,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: None,
|
||
};
|
||
|
||
// GB18030
|
||
// "中文" = [214, 208, 206, 196]
|
||
let filename = unsafe { String::from_utf8_unchecked(vec![214, 208, 206, 196]) };
|
||
writer.start_file(filename, options).unwrap();
|
||
writer.write_all(b"encoding GB18030").unwrap();
|
||
|
||
// SHIFT_JIS
|
||
// "日文" = [147, 250, 149, 182]
|
||
let filename = unsafe { String::from_utf8_unchecked(vec![147, 250, 149, 182]) };
|
||
writer.start_file(filename, options).unwrap();
|
||
writer.write_all(b"encoding SHIFT_JIS").unwrap();
|
||
let result = writer.finish().unwrap();
|
||
|
||
assert_eq!(result.get_ref().len(), 224);
|
||
|
||
let mut v = Vec::new();
|
||
v.extend_from_slice(include_bytes!("../tests/data/non_utf8.zip"));
|
||
|
||
assert_eq!(result.get_ref(), &v);
|
||
}
|
||
|
||
#[test]
|
||
fn path_to_string() {
|
||
let mut path = PathBuf::new();
|
||
#[cfg(windows)]
|
||
path.push(r"C:\");
|
||
#[cfg(unix)]
|
||
path.push("/");
|
||
path.push("windows");
|
||
path.push("..");
|
||
path.push(".");
|
||
path.push("system32");
|
||
let path_str = super::path_to_string(&path);
|
||
assert_eq!(&*path_str, "system32");
|
||
}
|
||
|
||
#[test]
|
||
fn test_shallow_copy() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::default(),
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: Some(33188),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: (),
|
||
alignment: 0,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: None,
|
||
};
|
||
writer.start_file(RT_TEST_FILENAME, options).unwrap();
|
||
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();
|
||
writer
|
||
.shallow_copy_file(RT_TEST_FILENAME, SECOND_FILENAME)
|
||
.unwrap();
|
||
writer
|
||
.shallow_copy_file(RT_TEST_FILENAME, SECOND_FILENAME)
|
||
.expect_err("Duplicate filename");
|
||
let zip = writer.finish().unwrap();
|
||
let mut writer = ZipWriter::new_append(zip).unwrap();
|
||
writer
|
||
.shallow_copy_file(SECOND_FILENAME, SECOND_FILENAME)
|
||
.expect_err("Duplicate filename");
|
||
let mut reader = writer.finish_into_readable().unwrap();
|
||
let mut file_names: Vec<&str> = reader.file_names().collect();
|
||
file_names.sort();
|
||
let mut expected_file_names = vec![RT_TEST_FILENAME, SECOND_FILENAME];
|
||
expected_file_names.sort();
|
||
assert_eq!(file_names, expected_file_names);
|
||
let mut first_file_content = String::new();
|
||
reader
|
||
.by_name(RT_TEST_FILENAME)
|
||
.unwrap()
|
||
.read_to_string(&mut first_file_content)
|
||
.unwrap();
|
||
assert_eq!(first_file_content, RT_TEST_TEXT);
|
||
let mut second_file_content = String::new();
|
||
reader
|
||
.by_name(SECOND_FILENAME)
|
||
.unwrap()
|
||
.read_to_string(&mut second_file_content)
|
||
.unwrap();
|
||
assert_eq!(second_file_content, RT_TEST_TEXT);
|
||
}
|
||
|
||
#[test]
|
||
fn test_deep_copy() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::default(),
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: Some(33188),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: (),
|
||
alignment: 0,
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
zopfli_buffer_size: None,
|
||
};
|
||
writer.start_file(RT_TEST_FILENAME, options).unwrap();
|
||
writer.write_all(RT_TEST_TEXT.as_ref()).unwrap();
|
||
writer
|
||
.deep_copy_file(RT_TEST_FILENAME, SECOND_FILENAME)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let mut writer = ZipWriter::new_append(zip).unwrap();
|
||
writer
|
||
.deep_copy_file(RT_TEST_FILENAME, THIRD_FILENAME)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let mut reader = ZipArchive::new(zip).unwrap();
|
||
let mut file_names: Vec<&str> = reader.file_names().collect();
|
||
file_names.sort();
|
||
let mut expected_file_names = vec![RT_TEST_FILENAME, SECOND_FILENAME, THIRD_FILENAME];
|
||
expected_file_names.sort();
|
||
assert_eq!(file_names, expected_file_names);
|
||
let mut first_file_content = String::new();
|
||
reader
|
||
.by_name(RT_TEST_FILENAME)
|
||
.unwrap()
|
||
.read_to_string(&mut first_file_content)
|
||
.unwrap();
|
||
assert_eq!(first_file_content, RT_TEST_TEXT);
|
||
let mut second_file_content = String::new();
|
||
reader
|
||
.by_name(SECOND_FILENAME)
|
||
.unwrap()
|
||
.read_to_string(&mut second_file_content)
|
||
.unwrap();
|
||
assert_eq!(second_file_content, RT_TEST_TEXT);
|
||
}
|
||
|
||
#[test]
|
||
fn duplicate_filenames() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file("foo/bar/test", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer
|
||
.write_all("The quick brown 🦊 jumps over the lazy 🐕".as_bytes())
|
||
.unwrap();
|
||
writer
|
||
.start_file("foo/bar/test", SimpleFileOptions::default())
|
||
.expect_err("Expected duplicate filename not to be allowed");
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file(
|
||
"PK\u{6}\u{7}\0\0\0\u{11}\0\0\0\0\0\0\0\0\0\0\0\0",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator_2() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file(
|
||
"PK\u{6}\u{6}\0\0\0\0\0\0\0\0\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator_2a() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file(
|
||
"PK\u{6}\u{6}PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator_3() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file("\0PK\u{6}\u{6}", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer
|
||
.start_file(
|
||
"\0\u{4}\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{3}",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator_4() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file("PK\u{6}\u{6}", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer
|
||
.start_file("\0\0\0\0\0\0", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer
|
||
.start_file("\0", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer.start_file("", SimpleFileOptions::default()).unwrap();
|
||
writer
|
||
.start_file("\0\0", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer
|
||
.start_file(
|
||
"\0\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn test_filename_looks_like_zip64_locator_5() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.add_directory("", SimpleFileOptions::default().with_alignment(21))
|
||
.unwrap();
|
||
let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap();
|
||
writer.shallow_copy_file("/", "").unwrap();
|
||
writer.shallow_copy_file("", "\0").unwrap();
|
||
writer.shallow_copy_file("\0", "PK\u{6}\u{6}").unwrap();
|
||
let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap();
|
||
writer
|
||
.start_file("\0\0\0\0\0\0", SimpleFileOptions::default())
|
||
.unwrap();
|
||
let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap();
|
||
writer
|
||
.start_file(
|
||
"#PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
|
||
SimpleFileOptions::default(),
|
||
)
|
||
.unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let _ = ZipArchive::new(zip).unwrap();
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn remove_shallow_copy_keeps_original() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer
|
||
.start_file("original", SimpleFileOptions::default())
|
||
.unwrap();
|
||
writer.write_all(RT_TEST_TEXT.as_bytes()).unwrap();
|
||
writer
|
||
.shallow_copy_file("original", "shallow_copy")
|
||
.unwrap();
|
||
writer.abort_file().unwrap();
|
||
let mut zip = ZipArchive::new(writer.finish().unwrap()).unwrap();
|
||
let mut file = zip.by_name("original").unwrap();
|
||
let mut contents = Vec::new();
|
||
file.read_to_end(&mut contents).unwrap();
|
||
assert_eq!(RT_TEST_TEXT.as_bytes(), contents);
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn remove_encrypted_file() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let first_file_options = SimpleFileOptions::default()
|
||
.with_alignment(65535)
|
||
.with_deprecated_encryption(b"Password");
|
||
writer.start_file("", first_file_options).unwrap();
|
||
writer.abort_file().unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let mut writer = ZipWriter::new(zip);
|
||
writer.start_file("", SimpleFileOptions::default()).unwrap();
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn remove_encrypted_aligned_symlink() -> ZipResult<()> {
|
||
let mut options = SimpleFileOptions::default();
|
||
options = options.with_deprecated_encryption(b"Password");
|
||
options.alignment = 65535;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.add_symlink("", "s\t\0\0ggggg\0\0", options).unwrap();
|
||
writer.abort_file().unwrap();
|
||
let zip = writer.finish().unwrap();
|
||
let mut writer = ZipWriter::new_append(zip).unwrap();
|
||
writer.start_file("", SimpleFileOptions::default()).unwrap();
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(feature = "deflate-zopfli")]
|
||
#[test]
|
||
fn zopfli_empty_write() -> ZipResult<()> {
|
||
let mut options = SimpleFileOptions::default();
|
||
options = options
|
||
.compression_method(CompressionMethod::default())
|
||
.compression_level(Some(264));
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.start_file("", options).unwrap();
|
||
writer.write_all(&[]).unwrap();
|
||
writer.write_all(&[]).unwrap();
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn crash_with_no_features() -> ZipResult<()> {
|
||
const ORIGINAL_FILE_NAME: &str = "PK\u{6}\u{6}\0\0\0\0\0\0\0\0\0\u{2}g\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\u{7}\0\t'";
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let mut options = SimpleFileOptions::default();
|
||
options = options.with_alignment(3584).compression_method(Stored);
|
||
writer.start_file(ORIGINAL_FILE_NAME, options)?;
|
||
let archive = writer.finish()?;
|
||
let mut writer = ZipWriter::new_append(archive)?;
|
||
writer.shallow_copy_file(ORIGINAL_FILE_NAME, "\u{6}\\")?;
|
||
writer.finish()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_alignment() {
|
||
let page_size = 4096;
|
||
let options = SimpleFileOptions::default()
|
||
.compression_method(Stored)
|
||
.with_alignment(page_size);
|
||
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let contents = b"sleeping";
|
||
let () = zip.start_file("sleep", options).unwrap();
|
||
let _count = zip.write(&contents[..]).unwrap();
|
||
let mut zip = zip.finish_into_readable().unwrap();
|
||
let file = zip.by_index(0).unwrap();
|
||
assert_eq!(file.name(), "sleep");
|
||
assert_eq!(file.data_start(), page_size.into());
|
||
}
|
||
|
||
#[test]
|
||
fn test_crash_short_read() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let comment = vec![
|
||
1, 80, 75, 5, 6, 237, 237, 237, 237, 237, 237, 237, 237, 44, 255, 191, 255, 255, 255,
|
||
255, 255, 255, 255, 255, 16,
|
||
]
|
||
.into_boxed_slice();
|
||
writer.set_raw_comment(comment);
|
||
let options = SimpleFileOptions::default()
|
||
.compression_method(Stored)
|
||
.with_alignment(11823);
|
||
writer.start_file("", options).unwrap();
|
||
writer.write_all(&[255, 255, 44, 255, 0]).unwrap();
|
||
let written = writer.finish().unwrap();
|
||
let _ = ZipWriter::new_append(written).unwrap();
|
||
}
|
||
|
||
#[cfg(all(feature = "_deflate-any", feature = "aes-crypto"))]
|
||
#[test]
|
||
fn test_fuzz_failure_2024_05_08() -> ZipResult<()> {
|
||
let mut first_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let mut second_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = SimpleFileOptions::default()
|
||
.compression_method(Stored)
|
||
.with_alignment(46036);
|
||
second_writer.add_symlink("\0", "", options)?;
|
||
let second_archive = second_writer.finish_into_readable()?.into_inner();
|
||
let mut second_writer = ZipWriter::new_append(second_archive)?;
|
||
let options = SimpleFileOptions::default()
|
||
.compression_method(CompressionMethod::Deflated)
|
||
.large_file(true)
|
||
.with_alignment(46036)
|
||
.with_aes_encryption(crate::AesMode::Aes128, "\0\0");
|
||
second_writer.add_symlink("", "", options)?;
|
||
let second_archive = second_writer.finish_into_readable()?.into_inner();
|
||
let mut second_writer = ZipWriter::new_append(second_archive)?;
|
||
let options = SimpleFileOptions::default().compression_method(Stored);
|
||
second_writer.start_file(" ", options)?;
|
||
let second_archive = second_writer.finish_into_readable()?;
|
||
first_writer.merge_archive(second_archive)?;
|
||
let _ = ZipArchive::new(first_writer.finish()?)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(feature = "bzip2")]
|
||
#[test]
|
||
fn test_fuzz_failure_2024_06_08() -> ZipResult<()> {
|
||
use crate::write::ExtendedFileOptions;
|
||
use CompressionMethod::Bzip2;
|
||
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
const SYMLINK_PATH: &str = "PK\u{6}\u{6}K\u{6}\u{6}\u{6}\0\0\0\0\u{18}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{2},\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{2},\0\0\0\0\0\0\0\0\0\0l\0\0\0\0\0\0PK\u{6}\u{7}P\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0";
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Bzip2,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 5, 20, 21, 0, 57)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 2048,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path(SYMLINK_PATH, "||\0\0\0\0", options)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.deep_copy_file_from_path(SYMLINK_PATH, "")?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.abort_file()?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.deep_copy_file_from_path(SYMLINK_PATH, "foo")?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_short_extra_data() {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![99, 0, 15, 0, 207].into(),
|
||
},
|
||
..Default::default()
|
||
};
|
||
assert!(writer.start_file_from_path("", options).is_err());
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(not(feature = "unreserved"))]
|
||
fn test_invalid_extra_data() -> ZipResult<()> {
|
||
use crate::write::ExtendedFileOptions;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 1, 4, 6, 54, 0)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![
|
||
7, 0, 15, 0, 207, 117, 177, 117, 112, 2, 0, 255, 255, 131, 255, 255, 255, 80,
|
||
185,
|
||
]
|
||
.into(),
|
||
},
|
||
alignment: 32787,
|
||
..Default::default()
|
||
};
|
||
assert!(writer.start_file_from_path("", options).is_err());
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(not(feature = "unreserved"))]
|
||
fn test_invalid_extra_data_unreserved() {
|
||
use crate::write::ExtendedFileOptions;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2021, 8, 8, 1, 0, 29).unwrap(),
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![
|
||
1, 41, 4, 0, 1, 255, 245, 117, 117, 112, 5, 0, 80, 255, 149, 255, 247,
|
||
]
|
||
.into(),
|
||
},
|
||
alignment: 4103,
|
||
..Default::default()
|
||
};
|
||
assert!(writer.start_file_from_path("", options).is_err());
|
||
}
|
||
|
||
#[cfg(feature = "deflate64")]
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_13a() -> ZipResult<()> {
|
||
use crate::write::ExtendedFileOptions;
|
||
use CompressionMethod::Deflate64;
|
||
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Deflate64,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2039, 4, 17, 6, 18, 19)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 4,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("", options)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_13b() -> ZipResult<()> {
|
||
use crate::write::ExtendedFileOptions;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 4, 14, 6, 11, 54)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 185,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path("", "", options)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer.deep_copy_file_from_path("", "_copy")?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FullFileOptions {
|
||
compression_method: Stored,
|
||
large_file: true,
|
||
alignment: 93,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0", options)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer.deep_copy_file_from_path("\0", "")?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer.deep_copy_file_from_path("", "copy")?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14a() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2083, 5, 30, 21, 45, 35)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 2565,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path("", "", options)?;
|
||
writer.abort_file()?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("", options)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[allow(deprecated)]
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14b() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2078, 3, 6, 12, 48, 58)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 65521,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\u{4}\0@\n//\u{c}", options)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer.abort_file()?;
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Unsupported(65535),
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2055, 10, 2, 11, 48, 49)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![255, 255, 1, 0, 255, 0, 0, 0, 0].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("", options)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14c() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2060, 4, 6, 13, 13, 3)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0", options)?;
|
||
writer.write_all(&([]))?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer.deep_copy_file_from_path("\0", "")?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer.deep_copy_file_from_path("", "_copy")?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(all(feature = "_deflate-any", feature = "aes-crypto"))]
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14d() -> ZipResult<()> {
|
||
use crate::write::EncryptWith::Aes;
|
||
use crate::AesMode::Aes256;
|
||
use CompressionMethod::Deflated;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Deflated,
|
||
compression_level: Some(5),
|
||
last_modified_time: DateTime::from_date_and_time(2107, 4, 8, 15, 54, 19)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: Some(Aes {
|
||
mode: Aes256,
|
||
password: "",
|
||
}),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![2, 0, 1, 0, 0].into(),
|
||
central_extra_data: vec![
|
||
35, 229, 2, 0, 41, 41, 231, 44, 2, 0, 52, 233, 82, 201, 0, 0, 3, 0, 2, 0, 233,
|
||
255, 3, 0, 2, 0, 26, 154, 38, 251, 0, 0,
|
||
]
|
||
.into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
assert!(writer.add_directory_from_path("", options).is_err());
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_14e() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1988, 1, 1, 1, 6, 26)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![76, 0, 1, 0, 0, 2, 0, 0, 0].into(),
|
||
central_extra_data: vec![
|
||
1, 149, 1, 0, 255, 3, 0, 0, 0, 2, 255, 0, 0, 12, 65, 1, 0, 0, 67, 149, 0, 0,
|
||
76, 149, 2, 0, 149, 149, 67, 149, 0, 0,
|
||
]
|
||
.into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
assert!(writer.add_directory_from_path("", options).is_err());
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[allow(deprecated)]
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_17() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer =
|
||
ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Unsupported(
|
||
65535,
|
||
),
|
||
compression_level: Some(5),
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
2107, 2, 8, 15, 0, 0,
|
||
)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: Some(ZipCrypto(
|
||
ZipCryptoKeys::of(
|
||
0x63ff, 0xc62d3103, 0xfffe00ea,
|
||
),
|
||
PhantomData,
|
||
)),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 255,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path("1\0PK\u{6}\u{6}\u{b}\u{6}\u{6}\u{6}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{b}\0\0PK\u{1}\u{2},\0\0\0\0\0\0\0\0\0\0\0\u{10}\0\0\0K\u{6}\u{6}\0\0\0\0\0\0\0\0PK\u{2}\u{6}", "", options)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
1992, 7, 3, 0, 0, 0,
|
||
)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 43,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path(
|
||
"\0\0\0\u{3}\0\u{1a}\u{1a}\u{1a}\u{1a}\u{1a}\u{1a}",
|
||
options,
|
||
)?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
2006, 3, 27, 2, 24, 26,
|
||
)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 26,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0K\u{6}\u{6}\0PK\u{6}\u{7}PK\u{6}\u{6}\0\0\0\0\0\0\0\0PK\u{2}\u{6}", options)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: Some(17),
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
2103, 4, 10, 23, 15, 18,
|
||
)?,
|
||
permissions: Some(3284386755),
|
||
large_file: true,
|
||
encrypt_with: Some(ZipCrypto(
|
||
ZipCryptoKeys::of(
|
||
0x8888c5bf, 0x88888888, 0xff888888,
|
||
),
|
||
PhantomData,
|
||
)),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![3, 0, 1, 0, 255, 144, 136, 0, 0]
|
||
.into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path("", "\nu", options)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer =
|
||
ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.abort_file()?;
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Unsupported(49603),
|
||
compression_level: Some(20),
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
2047, 4, 14, 3, 15, 14,
|
||
)?,
|
||
permissions: Some(3284386755),
|
||
large_file: true,
|
||
encrypt_with: Some(ZipCrypto(
|
||
ZipCryptoKeys::of(0xc3, 0x0, 0x0),
|
||
PhantomData,
|
||
)),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("", options)?;
|
||
writer.deep_copy_file_from_path("/", "")?;
|
||
writer.shallow_copy_file_from_path("", "copy")?;
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
assert!(writer.shallow_copy_file_from_path("", "copy").is_err());
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_17a() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
const PATH_1: &str = "\0I\01\0P\0\0\u{2}\0\0\u{1a}\u{1a}\u{1a}\u{1a}\u{1b}\u{1a}UT\u{5}\0\0\u{1a}\u{1a}\u{1a}\u{1a}UT\u{5}\0\u{1}\0\u{1a}\u{1a}\u{1a}UT\t\0uc\u{5}\0\0\0\0\u{7f}\u{7f}\u{7f}\u{7f}PK\u{6}";
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1981, 1, 1, 0, 24, 21)?,
|
||
permissions: Some(16908288),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 20555,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path(
|
||
"\0\u{7}\u{1}\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{2};",
|
||
options,
|
||
)?;
|
||
writer.write_all(
|
||
&([
|
||
255, 255, 255, 255, 253, 253, 253, 203, 203, 203, 253, 253, 253, 253, 255,
|
||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 249, 191, 225, 225,
|
||
241, 197,
|
||
]),
|
||
)?;
|
||
writer.write_all(
|
||
&([
|
||
197, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||
255, 75, 0,
|
||
]),
|
||
)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 11, 14, 10, 46, 47)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path(PATH_1, options)?;
|
||
writer.deep_copy_file_from_path(PATH_1, "eee\u{6}\0\0\0\0\0\0\0\0\0\0\0$\0\0\0\0\0\0\u{7f}\u{7f}PK\u{6}\u{6}K\u{6}\u{6}\u{6}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{1e},\0\0\0\0\0\0\0\0\0\0\0\u{8}\0*\0\0\u{1}PK\u{6}\u{7}PK\u{6}\u{6}\0\0\0\0\0\0\0\0}K\u{2}\u{6}")?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.deep_copy_file_from_path(PATH_1, "")?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.shallow_copy_file_from_path("", "copy")?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[allow(clippy::octal_escapes)]
|
||
#[cfg(feature = "bzip2")]
|
||
fn test_fuzz_crash_2024_06_17b() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
1981, 1, 1, 0, 0, 21,
|
||
)?,
|
||
permissions: Some(16908288),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 20555,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0\u{7}\u{1}\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{2};\u{1a}\u{18}\u{1a}UT\t.........................\0u", options)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Bzip2,
|
||
compression_level: Some(5),
|
||
last_modified_time: DateTime::from_date_and_time(
|
||
2055, 7, 7, 3, 6, 6,
|
||
)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0\0\0\0..\0\0\0\0\0\u{7f}\u{7f}PK\u{6}\u{6}K\u{6}\u{6}\u{6}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{1}\0\0PK\u{1}\u{1e},\0\0\0\0\0\0\0\0\0\0\0\u{8}\0*\0\0\u{1}PK\u{6}\u{7}PK\u{6}\u{6}\0\0\0\0\0\0\0\0}K\u{2}\u{6}", options)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(
|
||
writer.finish_into_readable()?.into_inner(),
|
||
)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer =
|
||
ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer =
|
||
ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_18() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_raw_comment(Box::<[u8]>::from([
|
||
80, 75, 5, 6, 255, 255, 255, 255, 255, 255, 80, 75, 5, 6, 255, 255, 255, 255, 255, 255,
|
||
13, 0, 13, 13, 13, 13, 13, 255, 255, 255, 255, 255, 255, 255, 255,
|
||
]));
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
writer.set_raw_comment(Box::new([]));
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_18a() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
writer.set_raw_comment(Box::<[u8]>::from([]));
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FullFileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2107, 4, 8, 14, 0, 19)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![
|
||
182, 180, 1, 0, 180, 182, 74, 0, 0, 200, 0, 0, 0, 2, 0, 0, 0,
|
||
]
|
||
.into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 1542,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("\0\0PK\u{6}\u{6}K\u{6}PK\u{3}\u{4}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{1}\u{1}\0PK\u{1}\u{2},\0\0\0\0\0\0\0\0\0\0\0P\u{7}\u{4}/.\0KP\0\0;\0\0\0\u{1e}\0\0\0\0\0\0\0\0\0\0\0\0\0", options)?;
|
||
let finished = writer.finish_into_readable()?;
|
||
assert_eq!(1, finished.file_names().count());
|
||
writer = ZipWriter::new_append(finished.into_inner())?;
|
||
let options = FullFileOptions {
|
||
compression_method: Stored,
|
||
compression_level: Some(5),
|
||
last_modified_time: DateTime::from_date_and_time(2107, 4, 1, 0, 0, 0)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: Some(ZipCrypto(
|
||
ZipCryptoKeys::of(0x0, 0x62e4b50, 0x100),
|
||
PhantomData,
|
||
)),
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path(
|
||
"\0K\u{6}\0PK\u{6}\u{7}PK\u{6}\u{6}\0\0\0\0\0\0\0\0PK\u{2}\u{6}",
|
||
"\u{8}\0\0\0\0/\0",
|
||
options,
|
||
)?;
|
||
let finished = writer.finish_into_readable()?;
|
||
assert_eq!(2, finished.file_names().count());
|
||
writer = ZipWriter::new_append(finished.into_inner())?;
|
||
assert_eq!(2, writer.files.len());
|
||
writer
|
||
};
|
||
let finished = sub_writer.finish_into_readable()?;
|
||
assert_eq!(2, finished.file_names().count());
|
||
writer.merge_archive(finished)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(all(feature = "bzip2", feature = "aes-crypto"))]
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_18b() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(true);
|
||
writer.set_raw_comment([0].into());
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
assert_eq!(writer.get_raw_comment()[0], 0);
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Bzip2,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2009, 6, 3, 13, 37, 39)?,
|
||
permissions: Some(2644352413),
|
||
large_file: true,
|
||
encrypt_with: Some(crate::write::EncryptWith::Aes {
|
||
mode: crate::AesMode::Aes256,
|
||
password: "",
|
||
}),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 255,
|
||
..Default::default()
|
||
};
|
||
writer.add_symlink_from_path("", "", options)?;
|
||
writer.deep_copy_file_from_path("", "PK\u{5}\u{6}\0\0\0\0\0\0\0\0\0\0\0\0\0\u{4}\0\0\0")?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
assert_eq!(writer.get_raw_comment()[0], 0);
|
||
writer.deep_copy_file_from_path(
|
||
"PK\u{5}\u{6}\0\0\0\0\0\0\0\0\0\0\0\0\0\u{4}\0\0\0",
|
||
"\u{2}yy\u{5}qu\0",
|
||
)?;
|
||
let finished = writer.finish()?;
|
||
let archive = ZipArchive::new(finished.clone())?;
|
||
assert_eq!(archive.comment(), [0]);
|
||
writer = ZipWriter::new_append(finished)?;
|
||
assert_eq!(writer.get_raw_comment()[0], 0);
|
||
let _ = writer.finish_into_readable()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_fuzz_crash_2024_06_19() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 3, 1, 19, 55, 58)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 256,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path(
|
||
"\0\0\0PK\u{5}\u{6}\0\0\0\0\u{1}\0\u{12}\u{6}\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\0",
|
||
options,
|
||
)?;
|
||
writer.set_flush_on_finish_file(false);
|
||
writer.shallow_copy_file_from_path(
|
||
"\0\0\0PK\u{5}\u{6}\0\0\0\0\u{1}\0\u{12}\u{6}\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\0",
|
||
"",
|
||
)?;
|
||
writer.set_flush_on_finish_file(false);
|
||
writer.deep_copy_file_from_path("", "copy")?;
|
||
writer.abort_file()?;
|
||
writer.set_flush_on_finish_file(false);
|
||
writer.set_raw_comment([255, 0].into());
|
||
writer.abort_file()?;
|
||
assert_eq!(writer.get_raw_comment(), [255, 0]);
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
assert_eq!(writer.get_raw_comment(), [255, 0]);
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("", options)?;
|
||
assert_eq!(writer.get_raw_comment(), [255, 0]);
|
||
let archive = writer.finish_into_readable()?;
|
||
assert_eq!(archive.comment(), [255, 0]);
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn fuzz_crash_2024_06_21() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FullFileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 2, 1, 0, 0, 0)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
..Default::default()
|
||
};
|
||
const LONG_PATH: &str = "\0@PK\u{6}\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0@/\0\0\00ΝPK\u{5}\u{6}O\0\u{10}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0@PK\u{6}\u{7}\u{6}\0/@\0\0\0\0\0\0\0\0 \0\0";
|
||
writer.start_file_from_path(LONG_PATH, options)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer.deep_copy_file_from_path(LONG_PATH, "oo\0\0\0")?;
|
||
writer.abort_file()?;
|
||
writer.set_raw_comment([33].into());
|
||
let archive = writer.finish_into_readable()?;
|
||
writer = ZipWriter::new_append(archive.into_inner())?;
|
||
assert!(writer.get_raw_comment().starts_with(&[33]));
|
||
let archive = writer.finish_into_readable()?;
|
||
assert!(archive.comment().starts_with(&[33]));
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "bzip2")]
|
||
fn fuzz_crash_2024_07_17() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: CompressionMethod::Bzip2,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2095, 2, 16, 21, 0, 1)?,
|
||
permissions: Some(84238341),
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![117, 99, 6, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 2, 0, 0, 0].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("", options)?;
|
||
//writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.deep_copy_file_from_path("", "copy")?;
|
||
let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn fuzz_crash_2024_07_19() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 6, 1, 0, 34, 47)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 45232,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("", options)?;
|
||
writer.deep_copy_file_from_path("/", "")?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.deep_copy_file_from_path("", "copy")?;
|
||
let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "aes-crypto")]
|
||
fn fuzz_crash_2024_07_19a() -> ZipResult<()> {
|
||
use crate::write::EncryptWith::Aes;
|
||
use crate::AesMode::Aes128;
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2107, 6, 5, 13, 0, 21)?,
|
||
permissions: None,
|
||
large_file: true,
|
||
encrypt_with: Some(Aes {
|
||
mode: Aes128,
|
||
password: "",
|
||
}),
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![3, 0, 4, 0, 209, 53, 53, 8, 2, 61, 0, 0].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 65535,
|
||
..Default::default()
|
||
};
|
||
writer.start_file_from_path("", options)?;
|
||
let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn fuzz_crash_2024_07_20() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(true);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2041, 8, 2, 19, 38, 0)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("\0\0\0\0\0\0\07黻", options)?;
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.set_flush_on_finish_file(false);
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 4,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("\0\0\0黻", options)?;
|
||
writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
writer.abort_file()?;
|
||
let options = FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(1980, 1, 1, 0, 7, 0)?,
|
||
permissions: Some(2663103419),
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 32256,
|
||
..Default::default()
|
||
};
|
||
writer.add_directory_from_path("\0", options)?;
|
||
writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn fuzz_crash_2024_07_21() -> ZipResult<()> {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
let sub_writer = {
|
||
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||
writer.add_directory_from_path(
|
||
"",
|
||
FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::from_date_and_time(2105, 8, 1, 15, 0, 0)?,
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 0,
|
||
..Default::default()
|
||
},
|
||
)?;
|
||
writer.abort_file()?;
|
||
let mut writer = ZipWriter::new_append(writer.finish()?)?;
|
||
writer.add_directory_from_path(
|
||
"",
|
||
FileOptions {
|
||
compression_method: Stored,
|
||
compression_level: None,
|
||
last_modified_time: DateTime::default(),
|
||
permissions: None,
|
||
large_file: false,
|
||
encrypt_with: None,
|
||
extended_options: ExtendedFileOptions {
|
||
extra_data: vec![].into(),
|
||
central_extra_data: vec![].into(),
|
||
},
|
||
alignment: 16,
|
||
..Default::default()
|
||
},
|
||
)?;
|
||
ZipWriter::new_append(writer.finish()?)?
|
||
};
|
||
writer.merge_archive(sub_writer.finish_into_readable()?)?;
|
||
let writer = ZipWriter::new_append(writer.finish()?)?;
|
||
let _ = writer.finish_into_readable()?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|