use alloc::vec::Vec;
use core::{cell::RefCell, time::Duration};
#[cfg(feature = "std")]
use std::{fs, fs::File, io::Write};
use std::{
fs::OpenOptions,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use crate::{
bolts::serdeany::SerdeAnyMap,
corpus::{Corpus, Testcase},
inputs::Input,
state::HasMetadata,
Error,
};
#[cfg(feature = "std")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OnDiskMetadataFormat {
Postcard,
Json,
JsonPretty,
}
#[cfg(feature = "std")]
#[derive(Debug, Serialize)]
pub struct OnDiskMetadata<'a> {
metadata: &'a SerdeAnyMap,
exec_time: &'a Option<Duration>,
executions: &'a usize,
}
#[cfg(feature = "std")]
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
#[serde(bound = "I: serde::de::DeserializeOwned")]
pub struct OnDiskCorpus<I>
where
I: Input,
{
entries: Vec<RefCell<Testcase<I>>>,
current: Option<usize>,
dir_path: PathBuf,
meta_format: Option<OnDiskMetadataFormat>,
}
impl<I> Corpus<I> for OnDiskCorpus<I>
where
I: Input,
{
#[inline]
fn count(&self) -> usize {
self.entries.len()
}
#[inline]
fn add(&mut self, mut testcase: Testcase<I>) -> Result<usize, Error> {
self.save_testcase(&mut testcase)?;
self.entries.push(RefCell::new(testcase));
Ok(self.entries.len() - 1)
}
#[inline]
fn replace(&mut self, idx: usize, mut testcase: Testcase<I>) -> Result<Testcase<I>, Error> {
if idx >= self.entries.len() {
return Err(Error::key_not_found(format!("Index {idx} out of bounds")));
}
self.save_testcase(&mut testcase)?;
let previous = self.entries[idx].replace(testcase);
self.remove_testcase(&previous)?;
Ok(previous)
}
#[inline]
fn remove(&mut self, idx: usize) -> Result<Option<Testcase<I>>, Error> {
if idx >= self.entries.len() {
Ok(None)
} else {
let prev = self.entries.remove(idx).into_inner();
self.remove_testcase(&prev)?;
Ok(Some(prev))
}
}
#[inline]
fn get(&self, idx: usize) -> Result<&RefCell<Testcase<I>>, Error> {
Ok(&self.entries[idx])
}
#[inline]
fn current(&self) -> &Option<usize> {
&self.current
}
#[inline]
fn current_mut(&mut self) -> &mut Option<usize> {
&mut self.current
}
}
impl<I> OnDiskCorpus<I>
where
I: Input,
{
pub fn new<P>(dir_path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
fn new<I: Input>(dir_path: PathBuf) -> Result<OnDiskCorpus<I>, Error> {
fs::create_dir_all(&dir_path)?;
Ok(OnDiskCorpus {
entries: vec![],
current: None,
dir_path,
meta_format: None,
})
}
new(dir_path.as_ref().to_path_buf())
}
pub fn new_save_meta(
dir_path: PathBuf,
meta_format: Option<OnDiskMetadataFormat>,
) -> Result<Self, Error> {
fs::create_dir_all(&dir_path)?;
Ok(Self {
entries: vec![],
current: None,
dir_path,
meta_format,
})
}
fn save_testcase(&mut self, testcase: &mut Testcase<I>) -> Result<(), Error> {
if testcase.filename().is_none() {
let file_orig = testcase
.input()
.as_ref()
.unwrap()
.generate_name(self.entries.len());
let mut file = file_orig.clone();
let mut ctr = 2;
let filename = loop {
let lockfile = format!(".{file}.lafl_lock");
if OpenOptions::new()
.write(true)
.create_new(true)
.open(self.dir_path.join(lockfile))
.is_ok()
{
break self.dir_path.join(file);
}
file = format!("{}-{ctr}", &file_orig);
ctr += 1;
};
let filename_str = filename.to_str().expect("Invalid Path");
testcase.set_filename(filename_str.into());
};
if self.meta_format.is_some() {
let mut filename = PathBuf::from(testcase.filename().as_ref().unwrap());
filename.set_file_name(format!(
".{}.metadata",
filename.file_name().unwrap().to_string_lossy()
));
let mut tmpfile_name = PathBuf::from(&filename);
tmpfile_name.set_file_name(format!(
".{}.tmp",
tmpfile_name.file_name().unwrap().to_string_lossy()
));
let ondisk_meta = OnDiskMetadata {
metadata: testcase.metadata(),
exec_time: testcase.exec_time(),
executions: testcase.executions(),
};
let mut tmpfile = File::create(&tmpfile_name)?;
let serialized = match self.meta_format.as_ref().unwrap() {
OnDiskMetadataFormat::Postcard => postcard::to_allocvec(&ondisk_meta)?,
OnDiskMetadataFormat::Json => serde_json::to_vec(&ondisk_meta)?,
OnDiskMetadataFormat::JsonPretty => serde_json::to_vec_pretty(&ondisk_meta)?,
};
tmpfile.write_all(&serialized)?;
fs::rename(&tmpfile_name, &filename)?;
}
testcase
.store_input()
.expect("Could not save testcase to disk");
Ok(())
}
fn remove_testcase(&mut self, testcase: &Testcase<I>) -> Result<(), Error> {
if let Some(filename) = testcase.filename() {
fs::remove_file(filename)?;
}
if self.meta_format.is_some() {
let mut filename = PathBuf::from(testcase.filename().as_ref().unwrap());
filename.set_file_name(format!(
".{}.metadata",
filename.file_name().unwrap().to_string_lossy()
));
fs::remove_file(filename)?;
}
Ok(())
}
}
#[cfg(feature = "python")]
pub mod pybind {
use alloc::string::String;
use std::path::PathBuf;
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{
corpus::{pybind::PythonCorpus, OnDiskCorpus},
inputs::BytesInput,
};
#[pyclass(unsendable, name = "OnDiskCorpus")]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PythonOnDiskCorpus {
pub inner: OnDiskCorpus<BytesInput>,
}
#[pymethods]
impl PythonOnDiskCorpus {
#[new]
fn new(path: String) -> Self {
Self {
inner: OnDiskCorpus::new(PathBuf::from(path)).unwrap(),
}
}
fn as_corpus(slf: Py<Self>) -> PythonCorpus {
PythonCorpus::new_on_disk(slf)
}
}
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PythonOnDiskCorpus>()?;
Ok(())
}
}