fix: May have to consider multiple CDEs before filtering for validity
This commit is contained in:
parent
45472486f1
commit
9bf914d7d4
3 changed files with 129 additions and 57 deletions
81
src/read.rs
81
src/read.rs
|
@ -8,7 +8,7 @@ use crate::crc32::Crc32Reader;
|
||||||
use crate::extra_fields::{ExtendedTimestamp, ExtraField};
|
use crate::extra_fields::{ExtendedTimestamp, ExtraField};
|
||||||
use crate::read::zip_archive::{Shared, SharedBuilder};
|
use crate::read::zip_archive::{Shared, SharedBuilder};
|
||||||
use crate::result::{ZipError, ZipResult};
|
use crate::result::{ZipError, ZipResult};
|
||||||
use crate::spec::{self, FixedSizeBlock};
|
use crate::spec::{self, FixedSizeBlock, Zip32CentralDirectoryEnd};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
|
AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData,
|
||||||
ZipLocalEntryBlock,
|
ZipLocalEntryBlock,
|
||||||
|
@ -23,6 +23,7 @@ use std::mem;
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
#[cfg(feature = "deflate-flate2")]
|
#[cfg(feature = "deflate-flate2")]
|
||||||
|
@ -671,51 +672,55 @@ impl<R: Read + Seek> ZipArchive<R> {
|
||||||
pub(crate) fn get_metadata(
|
pub(crate) fn get_metadata(
|
||||||
config: Config,
|
config: Config,
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
footer: &spec::Zip32CentralDirectoryEnd,
|
) -> ZipResult<(Zip32CentralDirectoryEnd, Shared)> {
|
||||||
cde_start_pos: u64,
|
|
||||||
) -> ZipResult<Shared> {
|
|
||||||
// Check if file has a zip64 footer
|
|
||||||
let results = Self::get_directory_info_zip64(&config, reader, footer, cde_start_pos)
|
|
||||||
.unwrap_or_else(|e| vec![Err(e)]);
|
|
||||||
let mut invalid_errors = Vec::new();
|
let mut invalid_errors = Vec::new();
|
||||||
let mut unsupported_errors = Vec::new();
|
let mut unsupported_errors = Vec::new();
|
||||||
let mut ok_results = Vec::new();
|
let mut ok_results = Vec::new();
|
||||||
results
|
let cde_locations = spec::Zip32CentralDirectoryEnd::find_and_parse(reader)?;
|
||||||
.into_iter()
|
IntoIterator::into_iter(cde_locations).for_each(|(footer, cde_start_pos)| {
|
||||||
.map(|result| {
|
|
||||||
result.and_then(|dir_info| Self::read_central_header(dir_info, config, reader))
|
|
||||||
})
|
|
||||||
.for_each(|result| {
|
|
||||||
Self::sort_result(
|
|
||||||
result,
|
|
||||||
&mut invalid_errors,
|
|
||||||
&mut unsupported_errors,
|
|
||||||
&mut ok_results,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if ok_results.is_empty() {
|
|
||||||
let zip32_result =
|
let zip32_result =
|
||||||
Self::get_directory_info_zip32(&config, reader, footer, cde_start_pos);
|
Self::get_directory_info_zip32(&config, reader, &footer, cde_start_pos);
|
||||||
Self::sort_result(
|
Self::sort_result(
|
||||||
zip32_result.and_then(|result| Self::read_central_header(result, config, reader)),
|
zip32_result.and_then(|result| Self::read_central_header(result, config, reader)),
|
||||||
&mut invalid_errors,
|
&mut invalid_errors,
|
||||||
&mut unsupported_errors,
|
&mut unsupported_errors,
|
||||||
&mut ok_results,
|
&mut ok_results,
|
||||||
|
&footer,
|
||||||
);
|
);
|
||||||
}
|
// Check if file has a zip64 footer
|
||||||
|
if let Ok(zip64_footers) =
|
||||||
|
Self::get_directory_info_zip64(&config, reader, &footer, cde_start_pos)
|
||||||
|
{
|
||||||
|
zip64_footers
|
||||||
|
.into_iter()
|
||||||
|
.map(|result| {
|
||||||
|
result.and_then(|dir_info| {
|
||||||
|
Self::read_central_header(dir_info, config, reader)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.for_each(|result| {
|
||||||
|
Self::sort_result(
|
||||||
|
result,
|
||||||
|
&mut invalid_errors,
|
||||||
|
&mut unsupported_errors,
|
||||||
|
&mut ok_results,
|
||||||
|
&footer,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
if ok_results.is_empty() {
|
if ok_results.is_empty() {
|
||||||
return Err(unsupported_errors
|
return Err(unsupported_errors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or_else(|| invalid_errors.into_iter().next().unwrap()));
|
.unwrap_or_else(|| invalid_errors.into_iter().next().unwrap()));
|
||||||
}
|
}
|
||||||
let shared = ok_results
|
let (footer, shared) = ok_results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.max_by_key(|shared| (shared.dir_start - shared.offset, shared.dir_start))
|
.max_by_key(|(_, shared)| (shared.dir_start - shared.offset, shared.dir_start))
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.build();
|
|
||||||
reader.seek(io::SeekFrom::Start(shared.dir_start))?;
|
reader.seek(io::SeekFrom::Start(shared.dir_start))?;
|
||||||
Ok(shared)
|
Ok((Rc::try_unwrap(footer).unwrap(), shared.build()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_central_header(
|
fn read_central_header(
|
||||||
|
@ -751,14 +756,15 @@ impl<R: Read + Seek> ZipArchive<R> {
|
||||||
result: Result<SharedBuilder, ZipError>,
|
result: Result<SharedBuilder, ZipError>,
|
||||||
invalid_errors: &mut Vec<ZipError>,
|
invalid_errors: &mut Vec<ZipError>,
|
||||||
unsupported_errors: &mut Vec<ZipError>,
|
unsupported_errors: &mut Vec<ZipError>,
|
||||||
ok_results: &mut Vec<SharedBuilder>,
|
ok_results: &mut Vec<(Rc<Zip32CentralDirectoryEnd>, SharedBuilder)>,
|
||||||
|
footer: &Rc<Zip32CentralDirectoryEnd>,
|
||||||
) {
|
) {
|
||||||
match result {
|
match result {
|
||||||
Err(ZipError::UnsupportedArchive(e)) => {
|
Err(ZipError::UnsupportedArchive(e)) => {
|
||||||
unsupported_errors.push(ZipError::UnsupportedArchive(e))
|
unsupported_errors.push(ZipError::UnsupportedArchive(e))
|
||||||
}
|
}
|
||||||
Err(e) => invalid_errors.push(e),
|
Err(e) => invalid_errors.push(e),
|
||||||
Ok(o) => ok_results.push(o),
|
Ok(o) => ok_results.push((footer.clone(), o)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -810,15 +816,12 @@ impl<R: Read + Seek> ZipArchive<R> {
|
||||||
///
|
///
|
||||||
/// This uses the central directory record of the ZIP file, and ignores local file headers.
|
/// This uses the central directory record of the ZIP file, and ignores local file headers.
|
||||||
pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
|
pub fn with_config(config: Config, mut reader: R) -> ZipResult<ZipArchive<R>> {
|
||||||
let mut results = spec::Zip32CentralDirectoryEnd::find_and_parse(&mut reader)?;
|
if let Ok((footer, shared)) = Self::get_metadata(config, &mut reader) {
|
||||||
for (footer, cde_start_pos) in results.iter_mut() {
|
return Ok(ZipArchive {
|
||||||
if let Ok(shared) = Self::get_metadata(config, &mut reader, footer, *cde_start_pos) {
|
reader,
|
||||||
return Ok(ZipArchive {
|
shared: shared.into(),
|
||||||
reader,
|
comment: footer.zip_file_comment.into(),
|
||||||
shared: shared.into(),
|
});
|
||||||
comment: Arc::from(mem::replace(&mut footer.zip_file_comment, Box::new([]))),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(InvalidArchive("No valid central directory found"))
|
Err(InvalidArchive("No valid central directory found"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ use memchr::memmem::FinderRev;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// "Magic" header values used in the zip spec to locate metadata records.
|
/// "Magic" header values used in the zip spec to locate metadata records.
|
||||||
///
|
///
|
||||||
|
@ -298,7 +299,7 @@ impl Zip32CentralDirectoryEnd {
|
||||||
|
|
||||||
pub fn find_and_parse<T: Read + Seek>(
|
pub fn find_and_parse<T: Read + Seek>(
|
||||||
reader: &mut T,
|
reader: &mut T,
|
||||||
) -> ZipResult<Box<[(Zip32CentralDirectoryEnd, u64)]>> {
|
) -> ZipResult<Box<[(Rc<Zip32CentralDirectoryEnd>, u64)]>> {
|
||||||
let mut results = vec![];
|
let mut results = vec![];
|
||||||
let file_length = reader.seek(io::SeekFrom::End(0))?;
|
let file_length = reader.seek(io::SeekFrom::End(0))?;
|
||||||
|
|
||||||
|
@ -338,7 +339,7 @@ impl Zip32CentralDirectoryEnd {
|
||||||
reader.seek(io::SeekFrom::Start(cde_start_pos))?;
|
reader.seek(io::SeekFrom::Start(cde_start_pos))?;
|
||||||
/* Drop any headers that don't parse. */
|
/* Drop any headers that don't parse. */
|
||||||
if let Ok(cde) = Self::parse(reader) {
|
if let Ok(cde) = Self::parse(reader) {
|
||||||
results.push((cde, cde_start_pos));
|
results.push((Rc::new(cde), cde_start_pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
100
src/write.rs
100
src/write.rs
|
@ -625,23 +625,19 @@ impl<A: Read + Write + Seek> ZipWriter<A> {
|
||||||
///
|
///
|
||||||
/// This uses the given read configuration to initially read the archive.
|
/// 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>> {
|
pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult<ZipWriter<A>> {
|
||||||
let mut results = spec::Zip32CentralDirectoryEnd::find_and_parse(&mut readwriter)?;
|
if let Ok((footer, shared)) = ZipArchive::get_metadata(config, &mut readwriter) {
|
||||||
for (footer, cde_start_pos) in results.iter_mut() {
|
Ok(ZipWriter {
|
||||||
if let Ok(metadata) =
|
inner: Storer(MaybeEncrypted::Unencrypted(readwriter)),
|
||||||
ZipArchive::get_metadata(config, &mut readwriter, footer, *cde_start_pos)
|
files: shared.files,
|
||||||
{
|
stats: Default::default(),
|
||||||
return Ok(ZipWriter {
|
writing_to_file: false,
|
||||||
inner: Storer(MaybeEncrypted::Unencrypted(readwriter)),
|
comment: footer.zip_file_comment,
|
||||||
files: metadata.files,
|
writing_raw: true, // avoid recomputing the last file's header
|
||||||
stats: Default::default(),
|
flush_on_finish_file: false,
|
||||||
writing_to_file: false,
|
})
|
||||||
comment: mem::replace(&mut footer.zip_file_comment, Box::new([])),
|
} else {
|
||||||
writing_raw: true, // avoid recomputing the last file's header
|
Err(InvalidArchive("No central-directory end header found"))
|
||||||
flush_on_finish_file: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(InvalidArchive("No central-directory end header found"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed
|
/// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed
|
||||||
|
@ -3300,4 +3296,76 @@ mod test {
|
||||||
let _ = writer.finish_into_readable()?;
|
let _ = writer.finish_into_readable()?;
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue