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:
Chris Hennick 2024-07-28 19:24:07 -07:00 committed by GitHub
parent fd5f804072
commit 6d8ab6224b
Signed by: DevComp
GPG key ID: B5690EEEBB952194
3 changed files with 436 additions and 309 deletions

View file

@ -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)
}

View file

@ -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);

View file

@ -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(&copy);
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(&copy)
})();
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(())
}
}