use std::{
fs,
io::{self, Read},
path::{Path, PathBuf},
};
use lexe_crypto::rng::{RngExt, ThreadFastRng};
pub trait Ffs {
#[cfg_attr(not(feature = "unstable"), allow(dead_code))]
fn read(&self, filename: &str) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.read_into(filename, &mut buf)?;
Ok(buf)
}
fn read_into(&self, filename: &str, buf: &mut Vec<u8>) -> io::Result<()>;
#[cfg_attr(not(feature = "unstable"), allow(dead_code))]
fn read_dir(&self) -> io::Result<Vec<String>> {
let mut filenames = Vec::new();
self.read_dir_visitor(|filename| {
filenames.push(filename.to_owned());
Ok(())
})?;
Ok(filenames)
}
fn read_dir_visitor(
&self,
dir_visitor: impl FnMut(&str) -> io::Result<()>,
) -> io::Result<()>;
fn write(&self, filename: &str, data: &[u8]) -> io::Result<()>;
fn delete_all(&self) -> io::Result<()>;
#[cfg_attr(not(feature = "unstable"), allow(dead_code))]
fn delete(&self, filename: &str) -> io::Result<()>;
}
#[derive(Clone)]
pub struct DiskFs {
base_dir: PathBuf,
write_dir: PathBuf,
}
impl DiskFs {
pub fn create_dir_all(base_dir: PathBuf) -> anyhow::Result<Self> {
fs::create_dir_all(base_dir.as_path())?;
let write_dir = Self::write_dir_path(&base_dir);
fsext::remove_dir_all_idempotent(&write_dir)?;
fs::create_dir(write_dir.as_path())?;
Ok(Self {
base_dir,
write_dir,
})
}
pub fn create_clean_dir_all(base_dir: PathBuf) -> anyhow::Result<Self> {
fsext::remove_dir_all_idempotent(&base_dir)?;
fs::create_dir_all(base_dir.as_path())?;
let write_dir = Self::write_dir_path(&base_dir);
fs::create_dir(write_dir.as_path())?;
Ok(Self {
base_dir,
write_dir,
})
}
fn write_dir_path(base_dir: &Path) -> PathBuf {
base_dir.join(".write")
}
}
impl Ffs for DiskFs {
fn read_into(&self, filename: &str, buf: &mut Vec<u8>) -> io::Result<()> {
let mut file = fs::File::open(self.base_dir.join(filename).as_path())?;
file.read_to_end(buf)?;
Ok(())
}
fn read_dir_visitor(
&self,
mut dir_visitor: impl FnMut(&str) -> io::Result<()>,
) -> io::Result<()> {
for maybe_file_entry in self.base_dir.read_dir()? {
let file_entry = maybe_file_entry?;
if file_entry.file_type()?.is_file() {
if let Some(filename) = file_entry.file_name().to_str() {
dir_visitor(filename)?;
}
}
}
Ok(())
}
fn write(&self, filename: &str, data: &[u8]) -> io::Result<()> {
let final_dest_path = self.base_dir.join(filename);
let tmp_write_path = {
let name: [u8; 16] = ThreadFastRng::new().gen_alphanum_bytes();
let name_str = std::str::from_utf8(name.as_slice())
.expect("ASCII is all valid UTF-8");
self.write_dir.join(name_str)
};
fs::write(tmp_write_path.as_path(), data)?;
fs::rename(tmp_write_path.as_path(), final_dest_path)?;
Ok(())
}
fn delete_all(&self) -> io::Result<()> {
fs::remove_dir_all(self.base_dir.as_path())?;
fs::create_dir(self.base_dir.as_path())?;
fs::create_dir(self.write_dir.as_path())?;
Ok(())
}
fn delete(&self, filename: &str) -> io::Result<()> {
fs::remove_file(self.base_dir.join(filename).as_path())?;
Ok(())
}
}
pub mod fsext {
use std::{fs, io, path::Path};
pub fn remove_dir_all_idempotent(dir: &Path) -> io::Result<bool> {
match fs::remove_dir_all(dir) {
Ok(()) => Ok(true),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
}
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils {
use std::{cell::RefCell, collections::BTreeMap, io};
use lexe_crypto::rng::{FastRng, RngSliceExt};
use super::Ffs;
fn io_err_not_found(filename: &str) -> io::Error {
io::Error::new(io::ErrorKind::NotFound, filename)
}
#[derive(Debug)]
pub struct InMemoryFfs {
inner: RefCell<InMemoryFfsInner>,
}
#[derive(Debug)]
struct InMemoryFfsInner {
rng: FastRng,
files: BTreeMap<String, Vec<u8>>,
}
impl InMemoryFfs {
pub fn new() -> Self {
Self {
inner: RefCell::new(InMemoryFfsInner {
rng: FastRng::new(),
files: BTreeMap::new(),
}),
}
}
pub fn from_rng(rng: FastRng) -> Self {
Self {
inner: RefCell::new(InMemoryFfsInner {
rng,
files: BTreeMap::new(),
}),
}
}
}
impl Default for InMemoryFfs {
fn default() -> Self {
Self::new()
}
}
impl Ffs for InMemoryFfs {
fn read_into(
&self,
filename: &str,
buf: &mut Vec<u8>,
) -> io::Result<()> {
match self.inner.borrow().files.get(filename) {
Some(data) => buf.extend_from_slice(data),
None => return Err(io_err_not_found(filename)),
}
Ok(())
}
fn read_dir_visitor(
&self,
mut dir_visitor: impl FnMut(&str) -> io::Result<()>,
) -> io::Result<()> {
let mut filenames = self
.inner
.borrow()
.files
.keys()
.cloned()
.collect::<Vec<_>>();
filenames.shuffle(&mut self.inner.borrow_mut().rng);
for filename in &filenames {
dir_visitor(filename)?;
}
Ok(())
}
fn write(&self, filename: &str, data: &[u8]) -> io::Result<()> {
self.inner
.borrow_mut()
.files
.insert(filename.to_owned(), data.to_owned());
Ok(())
}
fn delete_all(&self) -> io::Result<()> {
self.inner.borrow_mut().files = BTreeMap::new();
Ok(())
}
fn delete(&self, filename: &str) -> io::Result<()> {
match self.inner.borrow_mut().files.remove(filename) {
Some(_) => Ok(()),
None => Err(io_err_not_found(filename)),
}
}
}
}