use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use crate::vfs::Vfs;
use super::plan::{FaultPlan, FileState, State, TornTail};
#[derive(Clone)]
pub struct FaultFs(Arc<Mutex<State>>);
impl FaultFs {
pub fn new(plan: FaultPlan) -> Self {
FaultFs(Arc::new(Mutex::new(State {
files: BTreeMap::new(),
dirs: BTreeSet::new(),
op: 0,
bytes_read: 0,
plan,
})))
}
fn state(&self) -> MutexGuard<'_, State> {
self.0.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn power_cut(&self) {
let mut state = self.state();
cut(&mut state);
}
pub fn op_count(&self) -> u64 {
self.state().op
}
pub fn bytes_read(&self) -> u64 {
self.state().bytes_read
}
fn charge(&self, op_name: &'static str) -> io::Result<()> {
let mut state = self.state();
state.op = state.op.saturating_add(1);
if state.plan.cut_at_op == Some(state.op) {
state.plan.cut_at_op = None;
cut(&mut state);
return Err(io::Error::other(format!(
"power lost during {op_name} (op {})",
state.op
)));
}
Ok(())
}
}
fn cut(state: &mut State) {
let torn = state.plan.torn_tail;
state.files.retain(|_, file| file.durable_entry);
for file in state.files.values_mut() {
let delta = file.live.get(file.durable.len()..).unwrap_or(&[]);
let keep = match torn {
TornTail::None => 0,
TornTail::KeepBytes(k) => usize::try_from(k).unwrap_or(usize::MAX).min(delta.len()),
};
let mut settled = file.durable.clone();
settled.extend_from_slice(delta.get(..keep).unwrap_or(&[]));
file.durable = settled.clone();
file.live = settled;
}
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
fn not_found(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::NotFound,
format!("no such file: {}", path.display()),
)
}
impl Vfs for FaultFs {
fn create_dir_all(&mut self, dir: &Path) -> io::Result<()> {
self.state().dirs.insert(dir.to_path_buf());
self.charge("create_dir_all")
}
fn list(&self, dir: &Path) -> io::Result<Vec<PathBuf>> {
FaultFs::charge(self, "list")?;
let state = self.state();
let mut files: Vec<PathBuf> = state
.files
.keys()
.filter(|p| p.parent() == Some(dir))
.cloned()
.collect();
files.sort();
Ok(files)
}
fn list_dirs(&self, dir: &Path) -> io::Result<Vec<PathBuf>> {
FaultFs::charge(self, "list_dirs")?;
let state = self.state();
let mut dirs: Vec<PathBuf> = state
.files
.keys()
.filter_map(|path| path.parent())
.filter(|parent| parent.parent() == Some(dir))
.map(Path::to_path_buf)
.collect();
dirs.sort();
dirs.dedup();
Ok(dirs)
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
FaultFs::charge(self, "read")?;
let mut state = self.state();
let bytes = state
.files
.get(path)
.map(|f| f.live.clone())
.ok_or_else(|| not_found(path))?;
let moved = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
state.bytes_read = state.bytes_read.saturating_add(moved);
Ok(bytes)
}
fn size(&self, path: &Path) -> io::Result<u64> {
FaultFs::charge(self, "size")?;
let state = self.state();
let file = state.files.get(path).ok_or_else(|| not_found(path))?;
Ok(u64::try_from(file.live.len()).unwrap_or(u64::MAX))
}
fn read_at(&self, path: &Path, offset: u64, len: u64) -> io::Result<Vec<u8>> {
FaultFs::charge(self, "read_at")?;
let mut state = self.state();
let file = state.files.get(path).ok_or_else(|| not_found(path))?;
let at = usize::try_from(offset).unwrap_or(usize::MAX);
let take = usize::try_from(len).unwrap_or(usize::MAX);
let tail = file.live.get(at..).unwrap_or(&[]);
let bytes = tail.get(..take.min(tail.len())).unwrap_or(&[]).to_vec();
let moved = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
state.bytes_read = state.bytes_read.saturating_add(moved);
Ok(bytes)
}
fn append(&mut self, path: &Path, bytes: &[u8]) -> io::Result<()> {
{
let mut state = self.state();
let file = state.files.entry(path.to_path_buf()).or_insert(FileState {
durable: Vec::new(),
live: Vec::new(),
durable_entry: false,
});
file.live.extend_from_slice(bytes);
}
{
let mut state = self.state();
if let Some(needle) = state.plan.cut_on_write
&& contains(bytes, needle.as_bytes())
{
state.plan.cut_on_write = None;
state.op = state.op.saturating_add(1);
cut(&mut state);
return Err(io::Error::other(format!(
"power lost during append carrying `{needle}` (op {})",
state.op
)));
}
}
self.charge("append")
}
fn truncate(&mut self, path: &Path, len: u64) -> io::Result<()> {
{
let mut state = self.state();
let file = state.files.get_mut(path).ok_or_else(|| not_found(path))?;
let len = usize::try_from(len).unwrap_or(usize::MAX);
file.live.truncate(len);
}
self.charge("truncate")
}
fn rename(&mut self, from: &Path, to: &Path) -> io::Result<()> {
{
let mut state = self.state();
let Some(mut file) = state.files.remove(from) else {
return Err(not_found(from));
};
file.durable_entry = false;
state.files.insert(to.to_path_buf(), file);
}
self.charge("rename")
}
fn sync_data(&mut self, path: &Path) -> io::Result<()> {
self.charge("sync_data")?;
let mut state = self.state();
let file = state.files.get_mut(path).ok_or_else(|| not_found(path))?;
file.durable = file.live.clone();
Ok(())
}
fn sync_dir(&mut self, dir: &Path) -> io::Result<()> {
self.charge("sync_dir")?;
let mut state = self.state();
for (path, file) in state.files.iter_mut() {
if path.parent() == Some(dir) {
file.durable_entry = true;
}
}
Ok(())
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
{
let mut state = self.state();
if state.files.remove(path).is_none() {
return Err(not_found(path));
}
}
self.charge("remove_file")
}
fn copy_permissions(&mut self, from: &Path, to: &Path) -> io::Result<()> {
for path in [from, to] {
if !self.exists(path) {
return Err(not_found(path));
}
}
self.charge("copy_permissions")
}
fn exists(&self, path: &Path) -> bool {
self.state().files.contains_key(path)
}
}
#[cfg(test)]
mod tests;