use alloc::string::{String, ToString};
use core::cell::{Ref, RefCell, RefMut};
use std::{
fs,
fs::{File, OpenOptions},
io,
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
use fs2::FileExt;
#[cfg(feature = "gzip")]
use libafl_bolts::compress::GzipCompressor;
use serde::{Deserialize, Serialize};
use super::{
EnableDisableCorpus, HasTestcase,
ondisk::{OnDiskMetadata, OnDiskMetadataFormat},
};
use crate::{
Error, HasMetadata,
corpus::{Corpus, CorpusId, InMemoryCorpus, Testcase},
inputs::Input,
};
fn create_new<P: AsRef<Path>>(path: P) -> Result<File, io::Error> {
OpenOptions::new()
.write(true)
.read(true)
.create_new(true)
.open(path)
}
fn try_create_new<P: AsRef<Path>>(path: P) -> Result<Option<File>, io::Error> {
match create_new(path) {
Ok(ret) => Ok(Some(ret)),
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(None),
Err(err) => Err(err),
}
}
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct InMemoryOnDiskCorpus<I> {
inner: InMemoryCorpus<I>,
dir_path: PathBuf,
meta_format: Option<OnDiskMetadataFormat>,
prefix: Option<String>,
locking: bool,
}
impl<I> Corpus<I> for InMemoryOnDiskCorpus<I>
where
I: Input,
{
#[inline]
fn count(&self) -> usize {
self.inner.count()
}
fn count_disabled(&self) -> usize {
self.inner.count_disabled()
}
#[inline]
fn count_all(&self) -> usize {
self.inner.count_all()
}
#[inline]
fn add(&mut self, testcase: Testcase<I>) -> Result<CorpusId, Error> {
let id = self.inner.add(testcase)?;
let testcase = &mut self.get(id).unwrap().borrow_mut();
self.save_testcase(testcase, Some(id))?;
*testcase.input_mut() = None;
Ok(id)
}
#[inline]
fn add_disabled(&mut self, testcase: Testcase<I>) -> Result<CorpusId, Error> {
let id = self.inner.add_disabled(testcase)?;
let testcase = &mut self.get_from_all(id).unwrap().borrow_mut();
self.save_testcase(testcase, Some(id))?;
*testcase.input_mut() = None;
Ok(id)
}
#[inline]
fn replace(&mut self, id: CorpusId, testcase: Testcase<I>) -> Result<Testcase<I>, Error> {
let entry = self.inner.replace(id, testcase)?;
self.remove_testcase(&entry)?;
let testcase = &mut self.get(id).unwrap().borrow_mut();
self.save_testcase(testcase, Some(id))?;
*testcase.input_mut() = None;
Ok(entry)
}
#[inline]
fn remove(&mut self, id: CorpusId) -> Result<Testcase<I>, Error> {
let entry = self.inner.remove(id)?;
self.remove_testcase(&entry)?;
Ok(entry)
}
#[inline]
fn get(&self, id: CorpusId) -> Result<&RefCell<Testcase<I>>, Error> {
self.inner.get(id)
}
#[inline]
fn get_from_all(&self, id: CorpusId) -> Result<&RefCell<Testcase<I>>, Error> {
self.inner.get_from_all(id)
}
#[inline]
fn current(&self) -> &Option<CorpusId> {
self.inner.current()
}
#[inline]
fn current_mut(&mut self) -> &mut Option<CorpusId> {
self.inner.current_mut()
}
#[inline]
fn next(&self, id: CorpusId) -> Option<CorpusId> {
self.inner.next(id)
}
#[inline]
fn peek_free_id(&self) -> CorpusId {
self.inner.peek_free_id()
}
#[inline]
fn prev(&self, id: CorpusId) -> Option<CorpusId> {
self.inner.prev(id)
}
#[inline]
fn first(&self) -> Option<CorpusId> {
self.inner.first()
}
#[inline]
fn last(&self) -> Option<CorpusId> {
self.inner.last()
}
#[inline]
fn nth(&self, nth: usize) -> CorpusId {
self.inner.nth(nth)
}
#[inline]
fn nth_from_all(&self, nth: usize) -> CorpusId {
self.inner.nth_from_all(nth)
}
fn load_input_into(&self, testcase: &mut Testcase<I>) -> Result<(), Error> {
if testcase.input_mut().is_none() {
let Some(file_path) = testcase.file_path().as_ref() else {
return Err(Error::illegal_argument(
"No file path set for testcase. Could not load inputs.",
));
};
let input = I::from_file(file_path)?;
testcase.set_input(input);
}
Ok(())
}
fn store_input_from(&self, testcase: &Testcase<I>) -> Result<(), Error> {
let Some(file_path) = testcase.file_path() else {
return Err(Error::illegal_argument(
"No file path set for testcase. Could not store input to disk.",
));
};
let Some(input) = testcase.input() else {
return Err(Error::illegal_argument(
"No input available for testcase. Could not store anything.",
));
};
input.to_file(file_path)
}
}
impl<I> EnableDisableCorpus for InMemoryOnDiskCorpus<I>
where
I: Input,
{
#[inline]
fn disable(&mut self, id: CorpusId) -> Result<(), Error> {
self.inner.disable(id)?;
let testcase_cell = &mut self.get_from_all(id).unwrap().borrow_mut();
self.save_testcase(testcase_cell, Some(id))?;
Ok(())
}
#[inline]
fn enable(&mut self, id: CorpusId) -> Result<(), Error> {
self.inner.enable(id)?;
let testcase_cell = &mut self.get_from_all(id).unwrap().borrow_mut();
self.save_testcase(testcase_cell, Some(id))?;
Ok(())
}
}
impl<I> HasTestcase<I> for InMemoryOnDiskCorpus<I>
where
I: Input,
{
fn testcase(&self, id: CorpusId) -> Result<Ref<'_, Testcase<I>>, Error> {
Ok(self.get(id)?.borrow())
}
fn testcase_mut(&self, id: CorpusId) -> Result<RefMut<'_, Testcase<I>>, Error> {
Ok(self.get(id)?.borrow_mut())
}
}
impl<I> InMemoryOnDiskCorpus<I> {
pub fn new<P>(dir_path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
Self::_new(
dir_path.as_ref(),
Some(OnDiskMetadataFormat::JsonPretty),
None,
true,
)
}
pub fn with_meta_format<P>(
dir_path: P,
meta_format: Option<OnDiskMetadataFormat>,
) -> Result<Self, Error>
where
P: AsRef<Path>,
{
Self::_new(dir_path.as_ref(), meta_format, None, true)
}
pub fn with_meta_format_and_prefix<P>(
dir_path: P,
meta_format: Option<OnDiskMetadataFormat>,
prefix: Option<String>,
locking: bool,
) -> Result<Self, Error>
where
P: AsRef<Path>,
{
Self::_new(dir_path.as_ref(), meta_format, prefix, locking)
}
pub fn no_meta<P>(dir_path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
Self::_new(dir_path.as_ref(), None, None, true)
}
fn _new(
dir_path: &Path,
meta_format: Option<OnDiskMetadataFormat>,
prefix: Option<String>,
locking: bool,
) -> Result<Self, Error> {
match fs::create_dir_all(dir_path) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e.into()),
}
Ok(InMemoryOnDiskCorpus {
inner: InMemoryCorpus::new(),
dir_path: dir_path.into(),
meta_format,
prefix,
locking,
})
}
#[inline]
pub fn rename_testcase(
&self,
testcase: &mut Testcase<I>,
filename: String,
id: Option<CorpusId>,
) -> Result<(), Error>
where
I: Input,
{
if testcase.filename().is_some() {
let old_filename = testcase.filename_mut().take().unwrap();
let new_filename = filename;
if old_filename == new_filename {
*testcase.filename_mut() = Some(old_filename);
return Ok(());
}
let new_file_path = self.dir_path.join(&new_filename);
self.remove_testcase(testcase)?;
*testcase.filename_mut() = Some(new_filename);
self.save_testcase(testcase, id)?;
*testcase.file_path_mut() = Some(new_file_path);
Ok(())
} else {
Err(Error::illegal_argument(
"Cannot rename testcase without name!",
))
}
}
fn save_testcase(&self, testcase: &mut Testcase<I>, id: Option<CorpusId>) -> Result<(), Error>
where
I: Input,
{
let file_name = testcase.filename_mut().take().unwrap_or_else(|| {
testcase.input().as_ref().unwrap().generate_name(id)
});
let mut ctr = 1;
if self.locking {
let lockfile_name = format!(".{file_name}");
let lockfile_path = self.dir_path.join(lockfile_name);
let mut lockfile = try_create_new(&lockfile_path)?.unwrap_or(
OpenOptions::new()
.write(true)
.read(true)
.open(&lockfile_path)?,
);
lockfile.lock_exclusive()?;
let mut old_ctr = String::new();
lockfile.read_to_string(&mut old_ctr)?;
if !old_ctr.is_empty() {
ctr = old_ctr.trim().parse::<u32>()? + 1;
}
lockfile.seek(SeekFrom::Start(0))?;
lockfile.write_all(ctr.to_string().as_bytes())?;
}
if testcase.file_path().is_none() {
*testcase.file_path_mut() = Some(self.dir_path.join(&file_name));
}
*testcase.filename_mut() = Some(file_name);
if self.meta_format.is_some() {
let metafile_name = if self.locking {
format!(
".{}_{}.metadata",
testcase.filename().as_ref().unwrap(),
ctr
)
} else {
format!(".{}.metadata", testcase.filename().as_ref().unwrap())
};
let metafile_path = self.dir_path.join(&metafile_name);
let mut tmpfile_path = metafile_path.clone();
tmpfile_path.set_file_name(format!(".{metafile_name}.tmp",));
let ondisk_meta = OnDiskMetadata {
metadata: testcase.metadata_map(),
exec_time: testcase.exec_time(),
executions: testcase.executions(),
};
let mut tmpfile = File::create(&tmpfile_path)?;
let json_error =
|err| Error::serialize(format!("Failed to json-ify metadata: {err:?}"));
let serialized = match self.meta_format.as_ref().unwrap() {
OnDiskMetadataFormat::Postcard => postcard::to_allocvec(&ondisk_meta)?,
OnDiskMetadataFormat::Json => {
serde_json::to_vec(&ondisk_meta).map_err(json_error)?
}
OnDiskMetadataFormat::JsonPretty => {
serde_json::to_vec_pretty(&ondisk_meta).map_err(json_error)?
}
#[cfg(feature = "gzip")]
OnDiskMetadataFormat::JsonGzip => GzipCompressor::new()
.compress(&serde_json::to_vec_pretty(&ondisk_meta).map_err(json_error)?),
};
tmpfile.write_all(&serialized)?;
fs::rename(&tmpfile_path, &metafile_path)?;
*testcase.metadata_path_mut() = Some(metafile_path);
}
if ctr == 1 {
if let Err(err) = self.store_input_from(testcase) {
if self.locking {
return Err(err);
}
log::error!(
"An error occurred when trying to write a testcase without locking: {err}"
);
}
}
Ok(())
}
fn remove_testcase(&self, testcase: &Testcase<I>) -> Result<(), Error> {
if let Some(filename) = testcase.filename() {
let mut ctr = String::new();
if self.locking {
let lockfile_path = self.dir_path.join(format!(".{filename}"));
let mut lockfile = OpenOptions::new()
.write(true)
.read(true)
.open(&lockfile_path)?;
lockfile.lock_exclusive()?;
lockfile.read_to_string(&mut ctr)?;
ctr = ctr.trim().to_string();
if ctr == "1" {
FileExt::unlock(&lockfile)?;
drop(fs::remove_file(lockfile_path));
} else {
lockfile.seek(SeekFrom::Start(0))?;
lockfile.write_all((ctr.parse::<u32>()? - 1).to_string().as_bytes())?;
return Ok(());
}
}
fs::remove_file(self.dir_path.join(filename))?;
if self.meta_format.is_some() {
if self.locking {
fs::remove_file(self.dir_path.join(format!(".{filename}_{ctr}.metadata")))?;
} else {
fs::remove_file(self.dir_path.join(format!(".{filename}.metadata")))?;
}
}
}
Ok(())
}
#[must_use]
pub fn dir_path(&self) -> &PathBuf {
&self.dir_path
}
}
#[cfg(test)]
mod tests {
#[cfg(not(miri))]
use std::{env, fs, io::Write};
#[cfg(not(miri))]
use super::{create_new, try_create_new};
#[test]
#[cfg(not(miri))]
fn test() {
let tmp = env::temp_dir();
let path = tmp.join("testfile.tmp");
_ = fs::remove_file(&path);
let mut f = create_new(&path).unwrap();
f.write_all(&[0; 1]).unwrap();
match try_create_new(&path) {
Ok(None) => (),
Ok(_) => panic!(
"File {} did not exist even though it should have?",
&path.display()
),
Err(e) => panic!("An unexpected error occurred: {e}"),
}
drop(f);
fs::remove_file(path).unwrap();
}
}