zip-rs-wasm/fuzz/fuzz_targets/fuzz_write.rs

241 lines
8.5 KiB
Rust
Executable file

#![no_main]
use arbitrary::Arbitrary;
use core::fmt::{Debug, Formatter};
use libfuzzer_sys::fuzz_target;
use replace_with::replace_with_or_abort;
use std::io::{Cursor, Read, Seek, Write};
use std::path::PathBuf;
use zip::result::ZipError;
#[derive(Arbitrary, Clone)]
pub enum BasicFileOperation<'k> {
WriteNormalFile {
contents: Box<[Box<[u8]>]>,
options: zip::write::FullFileOptions<'k>,
},
WriteDirectory(zip::write::FullFileOptions<'k>),
WriteSymlinkWithTarget {
target: PathBuf,
options: zip::write::FullFileOptions<'k>,
},
ShallowCopy(Box<FileOperation<'k>>),
DeepCopy(Box<FileOperation<'k>>),
MergeWithOtherFile {
operations: Box<[(FileOperation<'k>, bool)]>
}
}
#[derive(Arbitrary, Clone, Debug, Eq, PartialEq)]
pub enum ReopenOption {
DoNotReopen,
ViaFinish,
ViaFinishIntoReadable
}
#[derive(Arbitrary, Clone)]
pub struct FileOperation<'k> {
basic: BasicFileOperation<'k>,
path: PathBuf,
reopen: ReopenOption,
// 'abort' flag is separate, to prevent trying to copy an aborted file
}
impl <'k> Debug for FileOperation<'k> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.basic {
BasicFileOperation::WriteNormalFile {contents, options} => {
f.write_fmt(format_args!("let options = {:?};\n\
writer.start_file_from_path({:?}, options)?;\n\
writer.write_all(&({:?}[..] as [u8]))?;\n\
drop(options);\n",
options, self.path, contents))
},
BasicFileOperation::WriteDirectory(options) => {
f.write_fmt(format_args!("let options = {:?};\n\
writer.add_directory_from_path({:?}, options)?;\n\
drop(options);\n",
options, self.path))
},
BasicFileOperation::WriteSymlinkWithTarget {target, options} => {
f.write_fmt(format_args!("let options = {:?};\n\
writer.add_symlink_from_path({:?}, {:?}, options)?;\n",
options, self.path, target.to_owned()))
},
BasicFileOperation::ShallowCopy(base) => {
f.write_fmt(format_args!("let path = {:?};\n\
{{\n\
{:?}
}}\n\
writer.shallow_copy_file_from_path(path, {:?})?;\n", base.path, base, self.path))
},
BasicFileOperation::DeepCopy(base) => {
f.write_fmt(format_args!("let path = {:?};\n\
{{\n\
{:?}
}}\n\
writer.shallow_copy_file_from_path(path, {:?})?;\n\
drop(path);\n", base.path, base, self.path))
},
BasicFileOperation::MergeWithOtherFile {operations} => {
f.write_str("let sub_writer = {\n\
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));\n")?;
operations.iter().map(|op| {
f.write_fmt(format_args!("{:?}", op.0))?;
if op.1 {
f.write_str("writer.abort_file()?;")
} else {
Ok(())
}
}).collect::<Result<(), _>>()?;
f.write_str("writer\n\
};\n\
writer = sub_writer;\n")
},
}?;
match &self.reopen {
ReopenOption::DoNotReopen => Ok(()),
ReopenOption::ViaFinish => {
f.write_str("writer = ZipWriter::new_append(writer.finish()?)?;\n")
},
ReopenOption::ViaFinishIntoReadable => {
f.write_str("writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?;\n")
}
}
}
}
#[derive(Arbitrary, Clone)]
pub struct FuzzTestCase<'k> {
comment: Box<[u8]>,
operations: Box<[(FileOperation<'k>, bool)]>,
flush_on_finish_file: bool,
}
impl <'k> Debug for FuzzTestCase<'k> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"let mut writer = ZipWriter::new(Cursor::new(Vec::new()));\n\
writer.set_flush_on_finish_file({:?});\n", self.flush_on_finish_file))?;
self.operations.iter().map(|op| {
f.write_fmt(format_args!("{:?}", op.0))?;
if op.1 {
f.write_str("writer.abort_file()?;")
} else {
Ok(())
}
})
.collect::<Result<(), _>>()?;
if self.comment.len() > 0 {
f.write_fmt(format_args!("writer.set_raw_comment(Box::<[u8]>::from({:?}));\n", self.comment))?;
}
f.write_str("writer\n")
}
}
fn do_operation<'k, T>(
writer: &mut zip::ZipWriter<T>,
operation: &FileOperation<'k>,
abort: bool,
flush_on_finish_file: bool,
files_added: &mut usize
) -> Result<(), Box<dyn std::error::Error>>
where
T: Read + Write + Seek,
{
writer.set_flush_on_finish_file(flush_on_finish_file);
let path = &operation.path;
match &operation.basic {
BasicFileOperation::WriteNormalFile {
contents,
options,
..
} => {
let uncompressed_size = contents.iter().map(|chunk| chunk.len()).sum::<usize>();
let mut options = (*options).to_owned();
if uncompressed_size >= u32::MAX as usize {
options = options.large_file(true);
}
writer.start_file_from_path(path, options)?;
for chunk in contents.iter() {
writer.write_all(&chunk)?;
}
files_added += 1;
}
BasicFileOperation::WriteDirectory(options) => {
writer.add_directory_from_path(path, options.to_owned())?;
files_added += 1;
}
BasicFileOperation::WriteSymlinkWithTarget { target, options } => {
writer.add_symlink_from_path(&path, target, options.to_owned())?;
files_added += 1;
}
BasicFileOperation::ShallowCopy(base) => {
do_operation(writer, &base, false, flush_on_finish_file)?;
writer.shallow_copy_file_from_path(&base.path, &path)?;
files_added += 1;
}
BasicFileOperation::DeepCopy(base) => {
do_operation(writer, &base, false, flush_on_finish_file)?;
writer.deep_copy_file_from_path(&base.path, &path)?;
files_added += 1;
}
BasicFileOperation::MergeWithOtherFile { operations } => {
let mut other_writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut inner_files_added = 0;
operations.iter().for_each(|(operation, abort)| {
let _ = do_operation(
&mut other_writer,
&operation,
*abort,
false,
&mut inner_files_added
);
});
writer.merge_archive(other_writer.finish_into_readable()?)?;
files_added += inner_files_added;
}
}
if abort && files_added != 0 {
writer.abort_file()?;
files_added -= 1;
}
let old_comment = writer.get_raw_comment().to_owned();
match operation.reopen {
ReopenOption::DoNotReopen => {},
ReopenOption::ViaFinish => replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| {
zip::ZipWriter::new_append(old_writer.finish().unwrap()).unwrap()
}),
ReopenOption::ViaFinishIntoReadable => replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| {
zip::ZipWriter::new_append(old_writer.finish_into_readable().unwrap().into_inner()).unwrap()
}),
}
assert_eq!(&old_comment, writer.get_raw_comment());
Ok(())
}
fuzz_target!(|test_case: FuzzTestCase| {
let mut file_added = false;
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
writer.set_raw_comment(test_case.comment);
let mut final_reopen = false;
if let Some((last_op, _)) = test_case.operations.last() {
if last_op.reopen != ReopenOption::ViaFinishIntoReadable {
final_reopen = true;
}
}
#[allow(unknown_lints)]
#[allow(boxed_slice_into_iter)]
for (operation, abort) in test_case.operations.into_iter() {
let _ = do_operation(
&mut writer,
&operation,
*abort,
test_case.flush_on_finish_file,
&mut file_added
);
}
if final_reopen {
let _ = writer.finish_into_readable().unwrap();
}
});