fix: (#33) Rare combination of settings could lead to writing a corrupt archive with overlength extra data, and data_start locations when reading the archive back were also wrong (#221)
* 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>
This commit is contained in:
parent
fd5f804072
commit
6d8ab6224b
3 changed files with 436 additions and 309 deletions
25
src/read.rs
25
src/read.rs
|
@ -359,7 +359,7 @@ fn find_data_start(
|
|||
// easily overflow a u16.
|
||||
block.file_name_length as u64 + block.extra_field_length as u64;
|
||||
let data_start =
|
||||
data.header_start + mem::size_of::<ZipLocalEntryBlock>() as u64 + variable_fields_len;
|
||||
data.header_start + size_of::<ZipLocalEntryBlock>() as u64 + variable_fields_len;
|
||||
// Set the value so we don't have to read it again.
|
||||
match data.data_start.set(data_start) {
|
||||
Ok(()) => (),
|
||||
|
@ -497,22 +497,28 @@ impl<R: Read + Seek> ZipArchive<R> {
|
|||
* assert_eq!(0, new_files[0].header_start); // Avoid this.
|
||||
*/
|
||||
|
||||
let new_initial_header_start = w.stream_position()?;
|
||||
let first_new_file_header_start = w.stream_position()?;
|
||||
|
||||
/* Push back file header starts for all entries in the covered files. */
|
||||
new_files.values_mut().try_for_each(|f| {
|
||||
/* This is probably the only really important thing to change. */
|
||||
f.header_start = f.header_start.checked_add(new_initial_header_start).ok_or(
|
||||
ZipError::InvalidArchive("new header start from merge would have been too large"),
|
||||
)?;
|
||||
f.header_start = f
|
||||
.header_start
|
||||
.checked_add(first_new_file_header_start)
|
||||
.ok_or(InvalidArchive(
|
||||
"new header start from merge would have been too large",
|
||||
))?;
|
||||
/* This is only ever used internally to cache metadata lookups (it's not part of the
|
||||
* zip spec), and 0 is the sentinel value. */
|
||||
// f.central_header_start = 0;
|
||||
f.central_header_start = 0;
|
||||
/* This is an atomic variable so it can be updated from another thread in the
|
||||
* implementation (which is good!). */
|
||||
if let Some(old_data_start) = f.data_start.take() {
|
||||
let new_data_start = old_data_start.checked_add(new_initial_header_start).ok_or(
|
||||
ZipError::InvalidArchive("new data start from merge would have been too large"),
|
||||
)?;
|
||||
let new_data_start = old_data_start
|
||||
.checked_add(first_new_file_header_start)
|
||||
.ok_or(InvalidArchive(
|
||||
"new data start from merge would have been too large",
|
||||
))?;
|
||||
f.data_start.get_or_init(|| new_data_start);
|
||||
}
|
||||
Ok::<_, ZipError>(())
|
||||
|
@ -1239,7 +1245,6 @@ pub(crate) fn central_header_to_zip_file<R: Read + Seek>(
|
|||
"A file can't start after its central-directory header",
|
||||
));
|
||||
}
|
||||
file.data_start.get_or_init(|| data_start);
|
||||
reader.seek(SeekFrom::Start(central_header_end))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
|
109
src/types.rs
109
src/types.rs
|
@ -24,7 +24,7 @@ use crate::extra_fields::ExtraField;
|
|||
use crate::result::DateTimeRangeError;
|
||||
use crate::spec::is_dir;
|
||||
use crate::types::ffi::S_IFDIR;
|
||||
use crate::CompressionMethod;
|
||||
use crate::{CompressionMethod, ZIP64_BYTES_THR};
|
||||
#[cfg(feature = "time")]
|
||||
use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
|
||||
|
||||
|
@ -625,10 +625,10 @@ impl ZipFileData {
|
|||
extra_field: &[u8],
|
||||
) -> Self
|
||||
where
|
||||
S: Into<Box<str>>,
|
||||
S: ToString,
|
||||
{
|
||||
let permissions = options.permissions.unwrap_or(0o100644);
|
||||
let file_name: Box<str> = name.into();
|
||||
let file_name: Box<str> = name.to_string().into_boxed_str();
|
||||
let file_name_raw: Box<[u8]> = file_name.bytes().collect();
|
||||
let mut local_block = ZipFileData {
|
||||
system: System::Unix,
|
||||
|
@ -778,12 +778,8 @@ impl ZipFileData {
|
|||
pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
|
||||
let compressed_size: u32 = self.clamp_size_field(self.compressed_size);
|
||||
let uncompressed_size: u32 = self.clamp_size_field(self.uncompressed_size);
|
||||
|
||||
let extra_block_len: usize = self
|
||||
.zip64_extra_field_block()
|
||||
.map(|block| block.full_size())
|
||||
.unwrap_or(0);
|
||||
let extra_field_length: u16 = (self.extra_field_len() + extra_block_len)
|
||||
let extra_field_length: u16 = self
|
||||
.extra_field_len()
|
||||
.try_into()
|
||||
.map_err(|_| ZipError::InvalidArchive("Extra data field is too large"))?;
|
||||
|
||||
|
@ -805,7 +801,7 @@ impl ZipFileData {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn block(&self, zip64_extra_field_length: u16) -> ZipResult<ZipCentralEntryBlock> {
|
||||
pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
|
||||
let extra_field_len: u16 = self.extra_field_len().try_into().unwrap();
|
||||
let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap();
|
||||
let last_modified_time = self
|
||||
|
@ -832,11 +828,9 @@ impl ZipFileData {
|
|||
.try_into()
|
||||
.unwrap(),
|
||||
file_name_length: self.file_name_raw.len().try_into().unwrap(),
|
||||
extra_field_length: zip64_extra_field_length
|
||||
.checked_add(extra_field_len + central_extra_field_len)
|
||||
.ok_or(ZipError::InvalidArchive(
|
||||
"Extra field length in central directory exceeds 64KiB",
|
||||
))?,
|
||||
extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
|
||||
ZipError::InvalidArchive("Extra field length in central directory exceeds 64KiB"),
|
||||
)?,
|
||||
file_comment_length: self.file_comment.as_bytes().len().try_into().unwrap(),
|
||||
disk_number: 0,
|
||||
internal_file_attributes: 0,
|
||||
|
@ -850,45 +844,12 @@ impl ZipFileData {
|
|||
}
|
||||
|
||||
pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
|
||||
let uncompressed_size: Option<u64> =
|
||||
if self.uncompressed_size >= spec::ZIP64_BYTES_THR || self.large_file {
|
||||
Some(spec::ZIP64_BYTES_THR)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compressed_size: Option<u64> =
|
||||
if self.compressed_size >= spec::ZIP64_BYTES_THR || self.large_file {
|
||||
Some(spec::ZIP64_BYTES_THR)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let header_start: Option<u64> = if self.header_start >= spec::ZIP64_BYTES_THR {
|
||||
Some(spec::ZIP64_BYTES_THR)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut size: u16 = 0;
|
||||
if uncompressed_size.is_some() {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
}
|
||||
if compressed_size.is_some() {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
}
|
||||
if header_start.is_some() {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
}
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Zip64ExtraFieldBlock {
|
||||
magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
|
||||
size,
|
||||
uncompressed_size,
|
||||
compressed_size,
|
||||
header_start,
|
||||
})
|
||||
Zip64ExtraFieldBlock::maybe_new(
|
||||
self.large_file,
|
||||
self.uncompressed_size,
|
||||
self.compressed_size,
|
||||
self.header_start,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1002,6 +963,46 @@ pub(crate) struct Zip64ExtraFieldBlock {
|
|||
// u32: disk start number
|
||||
}
|
||||
|
||||
impl Zip64ExtraFieldBlock {
|
||||
pub(crate) fn maybe_new(
|
||||
large_file: bool,
|
||||
uncompressed_size: u64,
|
||||
compressed_size: u64,
|
||||
header_start: u64,
|
||||
) -> Option<Zip64ExtraFieldBlock> {
|
||||
let mut size: u16 = 0;
|
||||
let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
Some(uncompressed_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
Some(compressed_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let header_start = if header_start >= ZIP64_BYTES_THR {
|
||||
size += mem::size_of::<u64>() as u16;
|
||||
Some(header_start)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Zip64ExtraFieldBlock {
|
||||
magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
|
||||
size,
|
||||
uncompressed_size,
|
||||
compressed_size,
|
||||
header_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Zip64ExtraFieldBlock {
|
||||
pub fn full_size(&self) -> usize {
|
||||
assert!(self.size > 0);
|
||||
|
|
611
src/write.rs
611
src/write.rs
|
@ -7,11 +7,12 @@ use crate::read::{
|
|||
find_content, parse_single_extra_field, Config, ZipArchive, ZipFile, ZipFileReader,
|
||||
};
|
||||
use crate::result::{ZipError, ZipResult};
|
||||
use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock};
|
||||
use crate::spec::{self, FixedSizeBlock, Pod, Zip32CDEBlock};
|
||||
#[cfg(feature = "aes-crypto")]
|
||||
use crate::types::AesMode;
|
||||
use crate::types::{
|
||||
ffi, AesVendorVersion, DateTime, ZipFileData, ZipLocalEntryBlock, ZipRawValues, MIN_VERSION,
|
||||
ffi, AesVendorVersion, DateTime, Zip64ExtraFieldBlock, ZipFileData, ZipLocalEntryBlock,
|
||||
ZipRawValues, MIN_VERSION,
|
||||
};
|
||||
use crate::write::ffi::S_IFLNK;
|
||||
#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))]
|
||||
|
@ -304,7 +305,7 @@ impl ExtendedFileOptions {
|
|||
}
|
||||
};
|
||||
Self::add_extra_data_unchecked(vec, header_id, data)?;
|
||||
Self::validate_extra_data(vec, 0)?;
|
||||
Self::validate_extra_data(vec, true)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -321,12 +322,12 @@ impl ExtendedFileOptions {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_extra_data(data: &[u8], reserved: u64) -> ZipResult<()> {
|
||||
fn validate_extra_data(data: &[u8], disallow_zip64: bool) -> ZipResult<()> {
|
||||
let len = data.len() as u64;
|
||||
if len == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if len + reserved > u16::MAX as u64 {
|
||||
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",
|
||||
|
@ -358,7 +359,7 @@ impl ExtendedFileOptions {
|
|||
}
|
||||
data.seek(SeekFrom::Current(-2))?;
|
||||
}
|
||||
parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, true)?;
|
||||
parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64)?;
|
||||
pos = data.position();
|
||||
}
|
||||
Ok(())
|
||||
|
@ -406,6 +407,9 @@ impl<'a> arbitrary::Arbitrary<'a> for FileOptions<'a, ExtendedFileOptions> {
|
|||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -668,19 +672,13 @@ impl<A: Read + Write + Seek> ZipWriter<A> {
|
|||
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 data_start = src_data.data_start();
|
||||
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 - data_start) {
|
||||
compressed_size = write_position - data_start;
|
||||
if compressed_size > (write_position - src_data_start) {
|
||||
compressed_size = write_position - src_data_start;
|
||||
src_data.compressed_size = compressed_size;
|
||||
}
|
||||
let uncompressed_size = src_data.uncompressed_size;
|
||||
|
||||
let raw_values = ZipRawValues {
|
||||
crc32: src_data.crc32,
|
||||
compressed_size,
|
||||
uncompressed_size,
|
||||
};
|
||||
let mut reader = BufReader::new(ZipFileReader::Raw(find_content(
|
||||
src_data,
|
||||
self.inner.get_plain(),
|
||||
|
@ -691,56 +689,46 @@ impl<A: Read + Write + Seek> ZipWriter<A> {
|
|||
self.inner
|
||||
.get_plain()
|
||||
.seek(SeekFrom::Start(write_position))?;
|
||||
if src_data.extra_field.is_some() || src_data.central_extra_field.is_some() {
|
||||
let mut options = FileOptions::<ExtendedFileOptions> {
|
||||
compression_method: src_data.compression_method,
|
||||
compression_level: src_data.compression_level,
|
||||
last_modified_time: src_data
|
||||
.last_modified_time
|
||||
.unwrap_or_else(DateTime::default_for_write),
|
||||
permissions: src_data.unix_mode(),
|
||||
large_file: src_data.large_file,
|
||||
encrypt_with: None,
|
||||
extended_options: ExtendedFileOptions {
|
||||
extra_data: src_data.extra_field.clone().unwrap_or_default(),
|
||||
central_extra_data: src_data.central_extra_field.clone().unwrap_or_default(),
|
||||
},
|
||||
alignment: 1,
|
||||
#[cfg(feature = "deflate-zopfli")]
|
||||
zopfli_buffer_size: None,
|
||||
};
|
||||
if let Some(perms) = src_data.unix_mode() {
|
||||
options = options.unix_permissions(perms);
|
||||
}
|
||||
Self::normalize_options(&mut options);
|
||||
self.start_entry(dest_name, options, Some(raw_values))?;
|
||||
} else {
|
||||
let mut options = FileOptions::<()> {
|
||||
compression_method: src_data.compression_method,
|
||||
compression_level: src_data.compression_level,
|
||||
last_modified_time: src_data
|
||||
.last_modified_time
|
||||
.unwrap_or_else(DateTime::default_for_write),
|
||||
permissions: src_data.unix_mode(),
|
||||
large_file: src_data.large_file,
|
||||
encrypt_with: None,
|
||||
extended_options: (),
|
||||
alignment: 1,
|
||||
#[cfg(feature = "deflate-zopfli")]
|
||||
zopfli_buffer_size: None,
|
||||
};
|
||||
if let Some(perms) = src_data.unix_mode() {
|
||||
options = options.unix_permissions(perms);
|
||||
}
|
||||
Self::normalize_options(&mut options);
|
||||
self.start_entry(dest_name, options, Some(raw_values))?;
|
||||
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;
|
||||
}
|
||||
|
||||
self.writing_to_file = true;
|
||||
self.writing_raw = true;
|
||||
let result = self.write_all(©);
|
||||
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)?;
|
||||
self.finish_file()
|
||||
|
||||
// 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.
|
||||
|
@ -873,18 +861,15 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
}
|
||||
|
||||
/// Start a new file for with the requested options.
|
||||
fn start_entry<S, SToOwned, T: FileOptionExtension>(
|
||||
fn start_entry<S: ToString, T: FileOptionExtension>(
|
||||
&mut self,
|
||||
name: S,
|
||||
options: FileOptions<T>,
|
||||
raw_values: Option<ZipRawValues>,
|
||||
) -> ZipResult<()>
|
||||
where
|
||||
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
|
||||
SToOwned: Into<Box<str>>,
|
||||
{
|
||||
) -> 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,
|
||||
|
@ -896,7 +881,13 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
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;
|
||||
|
@ -911,125 +902,110 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
aes_dummy_extra_data,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let header_start = self.inner.get_plain().stream_position()?;
|
||||
|
||||
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 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);
|
||||
let block = file.local_block();
|
||||
let index = self.insert_file_data(file)?;
|
||||
let writer = self.inner.get_plain();
|
||||
let result = block?.write(writer);
|
||||
self.ok_or_abort_file(result)?;
|
||||
let writer = self.inner.get_plain();
|
||||
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)?;
|
||||
let zip64_start = writer.stream_position()?;
|
||||
if file.large_file {
|
||||
write_local_zip64_extra_field(writer, file)?;
|
||||
}
|
||||
let header_end = writer.stream_position()?;
|
||||
file.extra_data_start = Some(header_end);
|
||||
let mut extra_data_end = header_end + extra_data.len() as u64;
|
||||
if options.alignment > 1 {
|
||||
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(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let extra_data_len = extra_data.len();
|
||||
if extra_data_len > 0 {
|
||||
let result = (|| {
|
||||
ExtendedFileOptions::validate_extra_data(
|
||||
&extra_data,
|
||||
header_end - zip64_start,
|
||||
)?;
|
||||
writer.write_all(&extra_data)?;
|
||||
extra_data_end = writer.stream_position()?;
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(e) = result {
|
||||
let _ = self.abort_file();
|
||||
return Err(e);
|
||||
}
|
||||
debug_assert_eq!(extra_data_end % (options.alignment.max(1) as u64), 0);
|
||||
self.stats.start = extra_data_end;
|
||||
writer.write_all(&extra_data)?;
|
||||
file.extra_field = Some(extra_data.into());
|
||||
} else {
|
||||
self.stats.start = extra_data_end;
|
||||
}
|
||||
if let Some(data) = central_extra_data {
|
||||
let validation_result =
|
||||
ExtendedFileOptions::validate_extra_data(data, extra_data_end - zip64_start);
|
||||
if let Err(e) = validation_result {
|
||||
let _ = self.abort_file();
|
||||
return Err(e);
|
||||
}
|
||||
file.central_extra_field = Some(data.clone());
|
||||
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));
|
||||
}
|
||||
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,
|
||||
};
|
||||
let crypto_header = [0u8; 12];
|
||||
|
||||
zipwriter.write_all(&crypto_header)?;
|
||||
self.stats.start = zipwriter.writer.stream_position()?;
|
||||
self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter));
|
||||
}
|
||||
None => {}
|
||||
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));
|
||||
}
|
||||
|
||||
debug_assert!(file.data_start.get().is_none());
|
||||
file.data_start.get_or_init(|| self.stats.start);
|
||||
self.writing_to_file = true;
|
||||
self.stats.bytes_written = 0;
|
||||
self.stats.hasher = Hasher::new();
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -1152,15 +1128,11 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
/// 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, T: FileOptionExtension, SToOwned>(
|
||||
pub fn start_file<S: ToString, T: FileOptionExtension>(
|
||||
&mut self,
|
||||
name: S,
|
||||
mut options: FileOptions<T>,
|
||||
) -> ZipResult<()>
|
||||
where
|
||||
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
|
||||
SToOwned: Into<Box<str>>,
|
||||
{
|
||||
) -> ZipResult<()> {
|
||||
Self::normalize_options(&mut options);
|
||||
let make_new_self = self.inner.prepare_next_writer(
|
||||
options.compression_method,
|
||||
|
@ -1286,11 +1258,11 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn raw_copy_file_rename<S, SToOwned>(&mut self, mut file: ZipFile, name: S) -> ZipResult<()>
|
||||
where
|
||||
S: Into<Box<str>> + ToOwned<Owned = SToOwned>,
|
||||
SToOwned: Into<Box<str>>,
|
||||
{
|
||||
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(
|
||||
|
@ -1314,8 +1286,7 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
self.writing_raw = true;
|
||||
|
||||
io::copy(&mut file.take_raw_reader()?, self)?;
|
||||
|
||||
Ok(())
|
||||
self.finish_file()
|
||||
}
|
||||
|
||||
/// Like `raw_copy_file_to_path`, but uses Path arguments.
|
||||
|
@ -1425,17 +1396,12 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
/// 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, NToOwned, T, E: FileOptionExtension>(
|
||||
pub fn add_symlink<N: ToString, T: ToString, E: FileOptionExtension>(
|
||||
&mut self,
|
||||
name: N,
|
||||
target: T,
|
||||
mut options: FileOptions<E>,
|
||||
) -> ZipResult<()>
|
||||
where
|
||||
N: Into<Box<str>> + ToOwned<Owned = NToOwned>,
|
||||
NToOwned: Into<Box<str>>,
|
||||
T: Into<Box<str>>,
|
||||
{
|
||||
) -> ZipResult<()> {
|
||||
if options.permissions.is_none() {
|
||||
options.permissions = Some(0o777);
|
||||
}
|
||||
|
@ -1446,7 +1412,7 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
|
||||
self.start_entry(name, options, None)?;
|
||||
self.writing_to_file = true;
|
||||
let result = self.write_all(target.into().as_bytes());
|
||||
let result = self.write_all(target.to_string().as_bytes());
|
||||
self.ok_or_abort_file(result)?;
|
||||
self.writing_raw = false;
|
||||
self.finish_file()?;
|
||||
|
@ -1474,8 +1440,8 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
let mut central_start = self.write_central_and_footer()?;
|
||||
let writer = self.inner.get_plain();
|
||||
let footer_end = writer.stream_position()?;
|
||||
let file_end = writer.seek(SeekFrom::End(0))?;
|
||||
if footer_end < file_end {
|
||||
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.
|
||||
|
@ -1564,7 +1530,9 @@ impl<W: Write + Seek> ZipWriter<W> {
|
|||
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(())
|
||||
}
|
||||
|
||||
|
@ -1855,12 +1823,7 @@ fn update_aes_extra_data<W: Write + Seek>(writer: &mut W, file: &mut ZipFileData
|
|||
|
||||
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
|
||||
.splice(
|
||||
aes_extra_data_start..(aes_extra_data_start + buf.len()),
|
||||
buf,
|
||||
)
|
||||
.count();
|
||||
extra_field[aes_extra_data_start..aes_extra_data_start + buf.len()].copy_from_slice(&buf);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1887,16 +1850,10 @@ fn update_local_file_header<T: Write + Seek>(writer: &mut T, file: &ZipFileData)
|
|||
}
|
||||
|
||||
fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
||||
// buffer zip64 extra field to determine its variable length
|
||||
let mut zip64_extra_field = [0; 28];
|
||||
let zip64_extra_field_length =
|
||||
write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?;
|
||||
let block = file.block(zip64_extra_field_length)?;
|
||||
let block = file.block()?;
|
||||
block.write(writer)?;
|
||||
// file name
|
||||
writer.write_all(&file.file_name_raw)?;
|
||||
// zip64 extra field
|
||||
writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?;
|
||||
// extra field
|
||||
if let Some(extra_field) = &file.extra_field {
|
||||
writer.write_all(extra_field)?;
|
||||
|
@ -1910,19 +1867,6 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_local_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
|
||||
// This entry in the Local header MUST include BOTH original
|
||||
// and compressed file size fields.
|
||||
let Some(block) = file.zip64_extra_field_block() else {
|
||||
return Err(InvalidArchive(
|
||||
"Attempted to write a ZIP64 extra field for a file that's within zip32 limits",
|
||||
));
|
||||
};
|
||||
let block = block.serialize();
|
||||
writer.write_all(&block)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_local_zip64_extra_field<T: Write + Seek>(
|
||||
writer: &mut T,
|
||||
file: &ZipFileData,
|
||||
|
@ -1941,22 +1885,6 @@ fn update_local_zip64_extra_field<T: Write + Seek>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<u16> {
|
||||
// The order of the fields in the zip64 extended
|
||||
// information record is fixed, but the fields MUST
|
||||
// only appear if the corresponding Local or Central
|
||||
// directory record field is set to 0xFFFF or 0xFFFFFFFF.
|
||||
match file.zip64_extra_field_block() {
|
||||
None => Ok(0),
|
||||
Some(block) => {
|
||||
let block = block.serialize();
|
||||
writer.write_all(&block)?;
|
||||
let len: u16 = block.len().try_into().unwrap();
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unreserved"))]
|
||||
const EXTRA_FIELD_MAPPING: [u16; 43] = [
|
||||
0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, 0x0017,
|
||||
|
@ -3498,4 +3426,197 @@ mod test {
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue