Skip to content

Commit

Permalink
test(fuzz): Make cargo fuzz fmt fuzz_write output more reliably equ…
Browse files Browse the repository at this point in the history
…ivalent to the code path it follows (#224)
  • Loading branch information
Pr0methean authored Jul 26, 2024
1 parent 546e49d commit a29b860
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 154 deletions.
265 changes: 115 additions & 150 deletions fuzz/fuzz_targets/fuzz_write.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#![no_main]

use arbitrary::Arbitrary;
use core::fmt::{Debug, Formatter};
use core::fmt::{Debug};
use libfuzzer_sys::fuzz_target;
use replace_with::replace_with_or_abort;
use std::borrow::Cow;
use std::io::{Cursor, Read, Seek, Write};
use std::fmt::{Arguments, Formatter, Write};
use std::io::Cursor;
use std::io::Write as IoWrite;
use std::path::PathBuf;
use tikv_jemallocator::Jemalloc;
use zip::result::{ZipError, ZipResult};
use zip::unstable::path_to_string;
use zip::write::FullFileOptions;

#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
Expand All @@ -17,12 +21,12 @@ static GLOBAL: Jemalloc = Jemalloc;
pub enum BasicFileOperation<'k> {
WriteNormalFile {
contents: Box<[Box<[u8]>]>,
options: zip::write::FullFileOptions<'k>,
options: FullFileOptions<'k>,
},
WriteDirectory(zip::write::FullFileOptions<'k>),
WriteDirectory(FullFileOptions<'k>),
WriteSymlinkWithTarget {
target: PathBuf,
options: zip::write::FullFileOptions<'k>,
options: FullFileOptions<'k>,
},
ShallowCopy(Box<FileOperation<'k>>),
DeepCopy(Box<FileOperation<'k>>),
Expand Down Expand Up @@ -61,123 +65,12 @@ impl<'k> FileOperation<'k> {
}
}

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",
options, self.path
))?;
for content_slice in contents {
f.write_fmt(format_args!("writer.write_all(&({:?}))?;\n", content_slice))?;
}
}
BasicFileOperation::WriteDirectory(options) => {
f.write_fmt(format_args!(
"let options = {:?};\n\
writer.add_directory_from_path({:?}, 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) => {
let Some(base_path) = base.get_path() else {
return Ok(());
};
f.write_fmt(format_args!(
"{:?}writer.shallow_copy_file_from_path({:?}, {:?})?;\n",
base, base_path, self.path
))?;
}
BasicFileOperation::DeepCopy(base) => {
let Some(base_path) = base.get_path() else {
return Ok(());
};
f.write_fmt(format_args!(
"{:?}writer.deep_copy_file_from_path({:?}, {:?})?;\n",
base, base_path, self.path
))?;
}
BasicFileOperation::MergeWithOtherFile { operations } => {
f.write_str(
"let sub_writer = {\n\
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));\n\
writer.set_flush_on_finish_file(false);\n",
)?;
operations
.iter()
.map(|op| {
f.write_fmt(format_args!("{:?}", op.0))?;
if op.1 {
f.write_str("writer.abort_file()?;\n")
} else {
Ok(())
}
})
.collect::<Result<(), _>>()?;
f.write_str(
"writer\n\
};\n\
writer.merge_archive(sub_writer.finish_into_readable()?)?;\n",
)?;
}
BasicFileOperation::SetArchiveComment(comment) => {
f.write_fmt(format_args!(
"writer.set_raw_comment({:?}.into());\n",
comment
))?;
}
}
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> {
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()?;\n")
} else {
Ok(())
}
})
.collect::<Result<(), _>>()?;
f.write_str("writer\n")
}
}

fn deduplicate_paths(copy: &mut Cow<PathBuf>, original: &PathBuf) {
if path_to_string(&**copy) == path_to_string(original) {
let new_path = match original.file_name() {
Expand All @@ -192,16 +85,15 @@ fn deduplicate_paths(copy: &mut Cow<PathBuf>, original: &PathBuf) {
}
}

fn do_operation<'k, T>(
writer: &mut zip::ZipWriter<T>,
fn do_operation<'k>(
writer: &mut zip::ZipWriter<Cursor<Vec<u8>>>,
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,
{
stringifier: &mut impl Write,
panic_on_error: bool
) -> Result<(), Box<dyn std::error::Error>> {
writer.set_flush_on_finish_file(flush_on_finish_file);
let mut path = Cow::Borrowed(&operation.path);
match &operation.basic {
Expand All @@ -213,17 +105,25 @@ where
if uncompressed_size >= u32::MAX as usize {
options = options.large_file(true);
}
if options == FullFileOptions::default() {
writeln!(stringifier, "writer.start_file_from_path({:?}, Default::default())?;", path)?;
} else {
writeln!(stringifier, "writer.start_file_from_path({:?}, {:?})?;", path, options)?;
}
writer.start_file_from_path(&*path, options)?;
for chunk in contents.iter() {
writeln!(stringifier, "writer.write_all(&{:?})?;", chunk)?;
writer.write_all(&chunk)?;
}
*files_added += 1;
}
BasicFileOperation::WriteDirectory(options) => {
writeln!(stringifier, "writer.add_directory_from_path(&{:?}, {:?})?;", path, options)?;
writer.add_directory_from_path(&*path, options.to_owned())?;
*files_added += 1;
}
BasicFileOperation::WriteSymlinkWithTarget { target, options } => {
writeln!(stringifier, "writer.add_symlink_from_path(&{:?}, {:?}, {:?});", path, target, options)?;
writer.add_symlink_from_path(&*path, target, options.to_owned())?;
*files_added += 1;
}
Expand All @@ -232,7 +132,8 @@ where
return Ok(());
};
deduplicate_paths(&mut path, &base_path);
do_operation(writer, &base, false, flush_on_finish_file, files_added)?;
do_operation(writer, &base, false, flush_on_finish_file, files_added, stringifier, panic_on_error)?;
writeln!(stringifier, "writer.shallow_copy_file_from_path({:?}, {:?});", base_path, path)?;
writer.shallow_copy_file_from_path(&*base_path, &*path)?;
*files_added += 1;
}
Expand All @@ -241,78 +142,142 @@ where
return Ok(());
};
deduplicate_paths(&mut path, &base_path);
do_operation(writer, &base, false, flush_on_finish_file, files_added)?;
do_operation(writer, &base, false, flush_on_finish_file, files_added, stringifier, panic_on_error)?;
writeln!(stringifier, "writer.deep_copy_file_from_path({:?}, {:?});", base_path, path)?;
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;
writeln!(stringifier,
"let sub_writer = {{\nlet mut writer = ZipWriter::new(Cursor::new(Vec::new()));")?;
operations.iter().for_each(|(operation, abort)| {
let _ = do_operation(
&mut other_writer,
&operation,
*abort,
false,
&mut inner_files_added,
stringifier,
panic_on_error
);
});
writeln!(stringifier, "writer\n}};\nwriter.merge_archive(sub_writer.finish_into_readable()?)?;")?;
writer.merge_archive(other_writer.finish_into_readable()?)?;
*files_added += inner_files_added;
}
BasicFileOperation::SetArchiveComment(comment) => {
writeln!(stringifier, "writer.set_raw_comment({:?})?;", comment)?;
writer.set_raw_comment(comment.clone());
}
}
if abort && *files_added != 0 {
writeln!(stringifier, "writer.abort_file()?;")?;
writer.abort_file()?;
*files_added -= 1;
}
// If a comment is set, we finish the archive, reopen it for append and then set a shorter
// comment, then there will be junk after the new comment that we can't get rid of. Thus, we
// can only check that the expected is a prefix of the actual
match operation.reopen {
ReopenOption::DoNotReopen => return Ok(()),
ReopenOption::DoNotReopen => {
writeln!(stringifier, "writer")?;
return Ok(())
},
ReopenOption::ViaFinish => {
let old_comment = writer.get_raw_comment().to_owned();
replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| {
zip::ZipWriter::new_append(old_writer.finish().unwrap()).unwrap()
writeln!(stringifier, "let mut writer = ZipWriter::new_append(writer.finish()?)?;")?;
replace_with_or_abort(writer, |old_writer: zip::ZipWriter<Cursor<Vec<u8>>>| {
(|| -> ZipResult<zip::ZipWriter<Cursor<Vec<u8>>>> {
zip::ZipWriter::new_append(old_writer.finish()?)
})().unwrap_or_else(|_| {
if panic_on_error {
panic!("Failed to create new ZipWriter")
}
zip::ZipWriter::new(Cursor::new(Vec::new()))
})
});
assert!(writer.get_raw_comment().starts_with(&old_comment));
if panic_on_error {
assert!(writer.get_raw_comment().starts_with(&old_comment));
}
}
ReopenOption::ViaFinishIntoReadable => {
let old_comment = writer.get_raw_comment().to_owned();
replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| {
zip::ZipWriter::new_append(old_writer.finish_into_readable().unwrap().into_inner())
.unwrap()
writeln!(stringifier, "let mut writer = ZipWriter::new_append(writer.finish()?)?;")?;
replace_with_or_abort(writer, |old_writer| {
(|| -> ZipResult<zip::ZipWriter<Cursor<Vec<u8>>>> {
zip::ZipWriter::new_append(old_writer.finish()?)
})().unwrap_or_else(|_| {
if panic_on_error {
panic!("Failed to create new ZipWriter")
}
zip::ZipWriter::new(Cursor::new(Vec::new()))
})
});
assert!(writer.get_raw_comment().starts_with(&old_comment));
}
}
Ok(())
}

fuzz_target!(|test_case: FuzzTestCase| {
let mut files_added = 0;
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut final_reopen = false;
if let Some((last_op, _)) = test_case.operations.last() {
if last_op.reopen != ReopenOption::ViaFinishIntoReadable {
final_reopen = true;
impl <'k> FuzzTestCase<'k> {
fn execute(&self, stringifier: &mut impl Write, panic_on_error: bool) -> ZipResult<()> {
let mut files_added = 0;
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut final_reopen = false;
if let Some((last_op, _)) = self.operations.last() {
if last_op.reopen != ReopenOption::ViaFinishIntoReadable {
final_reopen = true;
}
}
#[allow(unknown_lints)]
#[allow(boxed_slice_into_iter)]
for (operation, abort) in self.operations.iter() {
let _ = do_operation(
&mut writer,
&operation,
*abort,
self.flush_on_finish_file,
&mut files_added,
stringifier,
panic_on_error
);
}
if final_reopen {
writeln!(stringifier, "let _ = writer.finish_into_readable()?;")
.map_err(|_| ZipError::InvalidArchive(""))?;
let _ = writer.finish_into_readable()?;
}
Ok(())
}
#[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 files_added,
);
}

impl <'k> Debug for FuzzTestCase<'k> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "let mut writer = ZipWriter::new(Cursor::new(Vec::new()));")?;
let _ = self.execute(f, false);
Ok(())
}
if final_reopen {
let _ = writer.finish_into_readable().unwrap();
}

#[derive(Default, Eq, PartialEq)]
struct NoopWrite {}

impl Write for NoopWrite {
fn write_str(&mut self, _: &str) -> std::fmt::Result {
Ok(())
}

fn write_char(&mut self, _: char) -> std::fmt::Result {
Ok(())
}

fn write_fmt(&mut self, _: Arguments<'_>) -> std::fmt::Result {
Ok(())
}
}

fuzz_target!(|test_case: FuzzTestCase| {
test_case.execute(&mut NoopWrite::default(), true).unwrap()
});
2 changes: 1 addition & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,7 @@ pub enum AesVendorVersion {
}

/// AES variant used.
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum AesMode {
Expand Down
Loading

0 comments on commit a29b860

Please sign in to comment.