test(fuzz): Make cargo fuzz fmt fuzz_write output more reliably equivalent to the code path it follows (#224)

This commit is contained in:
Chris Hennick 2024-07-26 14:42:03 -07:00 committed by GitHub
parent 546e49d7f2
commit a29b860395
Signed by: DevComp
GPG key ID: B5690EEEBB952194
3 changed files with 119 additions and 154 deletions

View file

@ -1,14 +1,18 @@
#![no_main] #![no_main]
use arbitrary::Arbitrary; use arbitrary::Arbitrary;
use core::fmt::{Debug, Formatter}; use core::fmt::{Debug};
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use replace_with::replace_with_or_abort; use replace_with::replace_with_or_abort;
use std::borrow::Cow; 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 std::path::PathBuf;
use tikv_jemallocator::Jemalloc; use tikv_jemallocator::Jemalloc;
use zip::result::{ZipError, ZipResult};
use zip::unstable::path_to_string; use zip::unstable::path_to_string;
use zip::write::FullFileOptions;
#[global_allocator] #[global_allocator]
static GLOBAL: Jemalloc = Jemalloc; static GLOBAL: Jemalloc = Jemalloc;
@ -17,12 +21,12 @@ static GLOBAL: Jemalloc = Jemalloc;
pub enum BasicFileOperation<'k> { pub enum BasicFileOperation<'k> {
WriteNormalFile { WriteNormalFile {
contents: Box<[Box<[u8]>]>, contents: Box<[Box<[u8]>]>,
options: zip::write::FullFileOptions<'k>, options: FullFileOptions<'k>,
}, },
WriteDirectory(zip::write::FullFileOptions<'k>), WriteDirectory(FullFileOptions<'k>),
WriteSymlinkWithTarget { WriteSymlinkWithTarget {
target: PathBuf, target: PathBuf,
options: zip::write::FullFileOptions<'k>, options: FullFileOptions<'k>,
}, },
ShallowCopy(Box<FileOperation<'k>>), ShallowCopy(Box<FileOperation<'k>>),
DeepCopy(Box<FileOperation<'k>>), DeepCopy(Box<FileOperation<'k>>),
@ -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)] #[derive(Arbitrary, Clone)]
pub struct FuzzTestCase<'k> { pub struct FuzzTestCase<'k> {
operations: Box<[(FileOperation<'k>, bool)]>, operations: Box<[(FileOperation<'k>, bool)]>,
flush_on_finish_file: 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) { fn deduplicate_paths(copy: &mut Cow<PathBuf>, original: &PathBuf) {
if path_to_string(&**copy) == path_to_string(original) { if path_to_string(&**copy) == path_to_string(original) {
let new_path = match original.file_name() { let new_path = match original.file_name() {
@ -192,16 +85,15 @@ fn deduplicate_paths(copy: &mut Cow<PathBuf>, original: &PathBuf) {
} }
} }
fn do_operation<'k, T>( fn do_operation<'k>(
writer: &mut zip::ZipWriter<T>, writer: &mut zip::ZipWriter<Cursor<Vec<u8>>>,
operation: &FileOperation<'k>, operation: &FileOperation<'k>,
abort: bool, abort: bool,
flush_on_finish_file: bool, flush_on_finish_file: bool,
files_added: &mut usize, files_added: &mut usize,
) -> Result<(), Box<dyn std::error::Error>> stringifier: &mut impl Write,
where panic_on_error: bool
T: Read + Write + Seek, ) -> Result<(), Box<dyn std::error::Error>> {
{
writer.set_flush_on_finish_file(flush_on_finish_file); writer.set_flush_on_finish_file(flush_on_finish_file);
let mut path = Cow::Borrowed(&operation.path); let mut path = Cow::Borrowed(&operation.path);
match &operation.basic { match &operation.basic {
@ -213,17 +105,25 @@ where
if uncompressed_size >= u32::MAX as usize { if uncompressed_size >= u32::MAX as usize {
options = options.large_file(true); 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)?; writer.start_file_from_path(&*path, options)?;
for chunk in contents.iter() { for chunk in contents.iter() {
writeln!(stringifier, "writer.write_all(&{:?})?;", chunk)?;
writer.write_all(&chunk)?; writer.write_all(&chunk)?;
} }
*files_added += 1; *files_added += 1;
} }
BasicFileOperation::WriteDirectory(options) => { BasicFileOperation::WriteDirectory(options) => {
writeln!(stringifier, "writer.add_directory_from_path(&{:?}, {:?})?;", path, options)?;
writer.add_directory_from_path(&*path, options.to_owned())?; writer.add_directory_from_path(&*path, options.to_owned())?;
*files_added += 1; *files_added += 1;
} }
BasicFileOperation::WriteSymlinkWithTarget { target, options } => { BasicFileOperation::WriteSymlinkWithTarget { target, options } => {
writeln!(stringifier, "writer.add_symlink_from_path(&{:?}, {:?}, {:?});", path, target, options)?;
writer.add_symlink_from_path(&*path, target, options.to_owned())?; writer.add_symlink_from_path(&*path, target, options.to_owned())?;
*files_added += 1; *files_added += 1;
} }
@ -232,7 +132,8 @@ where
return Ok(()); return Ok(());
}; };
deduplicate_paths(&mut path, &base_path); 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)?; writer.shallow_copy_file_from_path(&*base_path, &*path)?;
*files_added += 1; *files_added += 1;
} }
@ -241,13 +142,16 @@ where
return Ok(()); return Ok(());
}; };
deduplicate_paths(&mut path, &base_path); 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)?; writer.deep_copy_file_from_path(&*base_path, &*path)?;
*files_added += 1; *files_added += 1;
} }
BasicFileOperation::MergeWithOtherFile { operations } => { BasicFileOperation::MergeWithOtherFile { operations } => {
let mut other_writer = zip::ZipWriter::new(Cursor::new(Vec::new())); let mut other_writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut inner_files_added = 0; 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)| { operations.iter().for_each(|(operation, abort)| {
let _ = do_operation( let _ = do_operation(
&mut other_writer, &mut other_writer,
@ -255,16 +159,21 @@ where
*abort, *abort,
false, false,
&mut inner_files_added, &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()?)?; writer.merge_archive(other_writer.finish_into_readable()?)?;
*files_added += inner_files_added; *files_added += inner_files_added;
} }
BasicFileOperation::SetArchiveComment(comment) => { BasicFileOperation::SetArchiveComment(comment) => {
writeln!(stringifier, "writer.set_raw_comment({:?})?;", comment)?;
writer.set_raw_comment(comment.clone()); writer.set_raw_comment(comment.clone());
} }
} }
if abort && *files_added != 0 { if abort && *files_added != 0 {
writeln!(stringifier, "writer.abort_file()?;")?;
writer.abort_file()?; writer.abort_file()?;
*files_added -= 1; *files_added -= 1;
} }
@ -272,19 +181,39 @@ where
// comment, then there will be junk after the new comment that we can't get rid of. Thus, we // 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 // can only check that the expected is a prefix of the actual
match operation.reopen { match operation.reopen {
ReopenOption::DoNotReopen => return Ok(()), ReopenOption::DoNotReopen => {
writeln!(stringifier, "writer")?;
return Ok(())
},
ReopenOption::ViaFinish => { ReopenOption::ViaFinish => {
let old_comment = writer.get_raw_comment().to_owned(); let old_comment = writer.get_raw_comment().to_owned();
replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| { writeln!(stringifier, "let mut writer = ZipWriter::new_append(writer.finish()?)?;")?;
zip::ZipWriter::new_append(old_writer.finish().unwrap()).unwrap() 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()))
})
}); });
if panic_on_error {
assert!(writer.get_raw_comment().starts_with(&old_comment)); assert!(writer.get_raw_comment().starts_with(&old_comment));
} }
}
ReopenOption::ViaFinishIntoReadable => { ReopenOption::ViaFinishIntoReadable => {
let old_comment = writer.get_raw_comment().to_owned(); let old_comment = writer.get_raw_comment().to_owned();
replace_with_or_abort(writer, |old_writer: zip::ZipWriter<T>| { writeln!(stringifier, "let mut writer = ZipWriter::new_append(writer.finish()?)?;")?;
zip::ZipWriter::new_append(old_writer.finish_into_readable().unwrap().into_inner()) replace_with_or_abort(writer, |old_writer| {
.unwrap() (|| -> 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)); assert!(writer.get_raw_comment().starts_with(&old_comment));
} }
@ -292,27 +221,63 @@ where
Ok(()) Ok(())
} }
fuzz_target!(|test_case: FuzzTestCase| { impl <'k> FuzzTestCase<'k> {
fn execute(&self, stringifier: &mut impl Write, panic_on_error: bool) -> ZipResult<()> {
let mut files_added = 0; let mut files_added = 0;
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new())); let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut final_reopen = false; let mut final_reopen = false;
if let Some((last_op, _)) = test_case.operations.last() { if let Some((last_op, _)) = self.operations.last() {
if last_op.reopen != ReopenOption::ViaFinishIntoReadable { if last_op.reopen != ReopenOption::ViaFinishIntoReadable {
final_reopen = true; final_reopen = true;
} }
} }
#[allow(unknown_lints)] #[allow(unknown_lints)]
#[allow(boxed_slice_into_iter)] #[allow(boxed_slice_into_iter)]
for (operation, abort) in test_case.operations.into_iter() { for (operation, abort) in self.operations.iter() {
let _ = do_operation( let _ = do_operation(
&mut writer, &mut writer,
&operation, &operation,
*abort, *abort,
test_case.flush_on_finish_file, self.flush_on_finish_file,
&mut files_added, &mut files_added,
stringifier,
panic_on_error
); );
} }
if final_reopen { if final_reopen {
let _ = writer.finish_into_readable().unwrap(); writeln!(stringifier, "let _ = writer.finish_into_readable()?;")
.map_err(|_| ZipError::InvalidArchive(""))?;
let _ = writer.finish_into_readable()?;
} }
Ok(())
}
}
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(())
}
}
#[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()
}); });

View file

@ -1050,7 +1050,7 @@ pub enum AesVendorVersion {
} }
/// AES variant used. /// AES variant used.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] #[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
#[repr(u8)] #[repr(u8)]
pub enum AesMode { pub enum AesMode {

View file

@ -225,7 +225,7 @@ mod sealed {
} }
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum EncryptWith<'k> { pub(crate) enum EncryptWith<'k> {
#[cfg(feature = "aes-crypto")] #[cfg(feature = "aes-crypto")]
Aes { Aes {
@ -254,7 +254,7 @@ impl<'a> arbitrary::Arbitrary<'a> for EncryptWith<'a> {
} }
/// Metadata for a file to be written /// Metadata for a file to be written
#[derive(Clone, Debug, Copy)] #[derive(Clone, Debug, Copy, Eq, PartialEq)]
pub struct FileOptions<'k, T: FileOptionExtension> { pub struct FileOptions<'k, T: FileOptionExtension> {
pub(crate) compression_method: CompressionMethod, pub(crate) compression_method: CompressionMethod,
pub(crate) compression_level: Option<i64>, pub(crate) compression_level: Option<i64>,
@ -272,7 +272,7 @@ pub type SimpleFileOptions = FileOptions<'static, ()>;
/// Adds Extra Data and Central Extra Data. It does not implement copy. /// Adds Extra Data and Central Extra Data. It does not implement copy.
pub type FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>; pub type FullFileOptions<'k> = FileOptions<'k, ExtendedFileOptions>;
/// The Extension for Extra Data and Central Extra Data /// The Extension for Extra Data and Central Extra Data
#[derive(Clone, Default)] #[derive(Clone, Default, Eq, PartialEq)]
pub struct ExtendedFileOptions { pub struct ExtendedFileOptions {
extra_data: Arc<Vec<u8>>, extra_data: Arc<Vec<u8>>,
central_extra_data: Arc<Vec<u8>>, central_extra_data: Arc<Vec<u8>>,