use super::{
FileHint, Fs, FsCapabilities, FsDirEntry, FsFile, FsMetadata, FsOpenOptions, SyncMode,
};
use crate::io;
use crate::path::{Path, PathBuf};
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use hashbrown::{HashMap, HashSet};
use std::path::Component;
#[derive(Default)]
struct CrashState {
durable: HashMap<PathBuf, Vec<u8>>,
touched: HashSet<PathBuf>,
track_entries: bool,
pending_entries: HashSet<PathBuf>,
link_group: HashMap<PathBuf, u64>,
next_link_group: u64,
}
impl CrashState {
fn mark_pending(&mut self, path: &Path) {
if self.track_entries {
self.pending_entries.insert(path.to_path_buf());
}
}
fn names_of(&self, path: &Path) -> Vec<PathBuf> {
match self.link_group.get(path) {
Some(&group) => self
.link_group
.iter()
.filter(|&(_, &other)| other == group)
.map(|(name, _)| name.clone())
.collect(),
None => alloc::vec![path.to_path_buf()],
}
}
fn set_durable(&mut self, path: &Path, bytes: Vec<u8>) {
let mut names = self.names_of(path);
let last = names.pop().unwrap_or_else(|| path.to_path_buf());
for name in names {
self.durable.insert(name, bytes.clone());
}
self.durable.insert(last, bytes);
}
fn forget_name(&mut self, path: &Path) {
self.durable.remove(path);
self.pending_entries.remove(path);
let touched = self.touched.remove(path);
if let Some(group) = self.link_group.remove(path)
&& touched
{
let survivors: Vec<PathBuf> = self
.link_group
.iter()
.filter(|&(_, &other)| other == group)
.map(|(name, _)| name.clone())
.collect();
self.touched.extend(survivors);
}
}
fn link(&mut self, src: &Path, dst: &Path) {
let group = if let Some(&group) = self.link_group.get(src) {
group
} else {
let group = self.next_link_group;
self.next_link_group += 1;
self.link_group.insert(src.to_path_buf(), group);
group
};
self.link_group.insert(dst.to_path_buf(), group);
}
}
const MAX_SYMLINKS: usize = 40;
fn components_reversed(path: &Path) -> Vec<PathBuf> {
path.components()
.rev()
.map(|component| PathBuf::from(component.as_os_str()))
.collect()
}
fn folded(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => match out.components().next_back() {
Some(Component::Normal(_)) => {
out.pop();
}
Some(Component::RootDir | Component::Prefix(_)) => {}
_ => out.push(component.as_os_str()),
},
other => out.push(other.as_os_str()),
}
}
if out.as_os_str().is_empty() {
out.push(".");
}
out
}
#[derive(Clone)]
pub struct CrashFs {
inner: Arc<dyn Fs>,
state: Arc<spin::Mutex<CrashState>>,
namespace: Arc<parking_lot::Mutex<()>>,
}
impl CrashFs {
#[must_use]
pub fn new<F: Fs>(inner: F) -> Self {
Self::from_shared(Arc::new(inner))
}
#[must_use]
pub fn from_shared(inner: Arc<dyn Fs>) -> Self {
Self {
inner,
state: Arc::new(spin::Mutex::new(CrashState::default())),
namespace: Arc::new(parking_lot::Mutex::new(())),
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn tracking_directory_entries(self) -> Self {
self.state.lock().track_entries = true;
self
}
#[must_use]
pub fn inner(&self) -> Arc<dyn Fs> {
Arc::clone(&self.inner)
}
pub fn crash(&self) {
let mut state = self.state.lock();
let lost: Vec<PathBuf> = state.pending_entries.drain().collect();
for path in &lost {
state.durable.remove(path);
state.link_group.remove(path);
state.touched.insert(path.clone());
}
let paths: Vec<PathBuf> = state
.touched
.iter()
.chain(state.durable.keys())
.cloned()
.collect();
for path in paths {
match state.durable.get(&path) {
Some(bytes) => self.restore_durable(&path, bytes),
None => {
match self.inner.remove_file(&path) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => {
panic!("crash(): removing un-synced {} failed: {e}", path.display())
}
}
}
}
}
state.touched = state.durable.keys().cloned().collect();
let CrashState {
durable,
link_group,
..
} = &mut *state;
link_group.retain(|name, _| durable.contains_key(name));
}
fn restore_durable(&self, path: &Path, bytes: &[u8]) {
let mut file = self
.inner
.open(
path,
&FsOpenOptions::new().write(true).create(true).truncate(true),
)
.unwrap_or_else(|e| {
panic!(
"crash(): reopening {} for rollback failed: {e}",
path.display()
)
});
std::io::Write::write_all(&mut file, bytes).unwrap_or_else(|e| {
panic!(
"crash(): rewriting durable image of {} failed: {e}",
path.display()
)
});
}
fn read_baseline(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
let mut f = match self.inner.open(path, &FsOpenOptions::new().read(true)) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut f, &mut buf)?;
Ok(Some(buf))
}
fn capture_first_touch(&self, path: &Path) -> io::Result<()> {
let pb = path.to_path_buf();
let first_touch = {
let state = self.state.lock();
!state
.names_of(path)
.iter()
.any(|name| state.touched.contains(name))
};
if first_touch {
let baseline = self.read_baseline(path)?;
if let Some(bytes) = baseline {
self.state.lock().set_durable(path, bytes);
}
}
self.state.lock().touched.insert(pb);
Ok(())
}
fn entry_of(&self, path: &Path) -> io::Result<PathBuf> {
self.resolve(path, true)
}
fn name_of(&self, path: &Path) -> io::Result<PathBuf> {
self.resolve(path, false)
}
fn name_after(&self, path: &Path) -> PathBuf {
self.name_of(path).unwrap_or_else(|_| path.to_path_buf())
}
fn resolve(&self, path: &Path, follow_final: bool) -> io::Result<PathBuf> {
let mut resolved = PathBuf::new();
let mut rest: Vec<PathBuf> = components_reversed(path);
let mut followed = 0;
while let Some(part) = rest.pop() {
if !matches!(part.components().next(), Some(Component::Normal(_))) {
if !matches!(part.components().next(), Some(Component::CurDir)) {
resolved.push(part);
}
continue;
}
let candidate = resolved.join(&part);
if rest.is_empty() && !follow_final {
resolved = candidate;
break;
}
match self.inner.read_link(&candidate)? {
Some(target) => {
if followed == MAX_SYMLINKS {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"too many levels of symbolic links",
));
}
followed += 1;
rest.extend(components_reversed(&target));
}
None => resolved = candidate,
}
}
if resolved.as_os_str().is_empty() {
resolved.push(".");
}
let folded = folded(&resolved);
if folded == resolved {
return Ok(resolved);
}
let (spelled, plain) = match (resolved.components().next_back(), folded.file_name()) {
(Some(Component::Normal(_)), Some(_)) => (
crate::file::entry_directory(&resolved).to_path_buf(),
crate::file::entry_directory(&folded).to_path_buf(),
),
_ => (resolved.clone(), folded.clone()),
};
Ok(
if matches!(self.inner.same_file(&spelled, &plain), Ok(true)) {
folded
} else {
resolved
},
)
}
fn listed_as_spelled(&self, path: &Path) -> bool {
let Some(name) = path.file_name() else {
return false;
};
self.inner
.read_dir(crate::file::entry_directory(path))
.is_ok_and(|entries| {
entries
.iter()
.any(|entry| std::ffi::OsStr::new(&entry.file_name) == name)
})
}
fn new_entry(&self, path: &Path) {
self.state.lock().mark_pending(path);
}
fn sync_entries_of(
&self,
directory: &Path,
sync: impl FnOnce() -> io::Result<()>,
) -> io::Result<()> {
let _namespace = self.namespace.lock();
sync()?;
if !self.state.lock().track_entries {
return Ok(());
}
let directory = self
.entry_of(directory)
.unwrap_or_else(|_| directory.to_path_buf());
let pending = self.state.lock().pending_entries.clone();
let covered: Vec<PathBuf> = pending
.into_iter()
.filter(|entry| {
let parent = crate::file::entry_directory(entry);
parent == directory || matches!(self.inner.same_file(parent, &directory), Ok(true))
})
.collect();
let mut state = self.state.lock();
for entry in &covered {
state.pending_entries.remove(entry);
}
Ok(())
}
fn track_copy(&self, src: &Path, dst: &Path) -> io::Result<()> {
let (src_durable, src_touched) = {
let mut state = self.state.lock();
if state.track_entries {
state.touched.insert(dst.to_path_buf());
state.mark_pending(dst);
}
(state.durable.get(src).cloned(), state.touched.contains(src))
};
let dst_image = match src_durable {
Some(bytes) => Some(bytes),
None if !src_touched => self.read_baseline(src)?,
None => None,
};
{
let mut state = self.state.lock();
if let Some(bytes) = dst_image {
state.durable.insert(dst.to_path_buf(), bytes);
}
state.touched.insert(dst.to_path_buf());
}
Ok(())
}
}
impl Fs for CrashFs {
fn open(&self, path: &Path, opts: &FsOpenOptions) -> io::Result<Box<dyn FsFile>> {
let writable = opts.write || opts.create || opts.create_new || opts.append || opts.truncate;
let may_create = opts.create || opts.create_new;
let namespace = self.namespace.lock();
if opts.create_new {
let inner = self.inner.open(path, opts)?;
let entry = self.name_after(path);
self.state.lock().touched.insert(entry.clone());
self.new_entry(&entry);
drop(namespace);
return Ok(Box::new(CrashFile {
inner,
path: entry,
fs: Arc::clone(&self.inner),
state: Arc::clone(&self.state),
}));
}
if !writable {
let inner = self.inner.open(path, opts)?;
let entry = self.entry_of(path).unwrap_or_else(|_| path.to_path_buf());
drop(namespace);
return Ok(Box::new(CrashFile {
inner,
path: entry,
fs: Arc::clone(&self.inner),
state: Arc::clone(&self.state),
}));
}
let entry = self.entry_of(path)?;
let tracking = self.state.lock().track_entries;
let creates = tracking && may_create && !self.inner.exists(&entry)?;
self.capture_first_touch(&entry)?;
let inner = self.inner.open(path, opts)?;
if creates {
self.new_entry(&entry);
}
drop(namespace);
Ok(Box::new(CrashFile {
inner,
path: entry,
fs: Arc::clone(&self.inner),
state: Arc::clone(&self.state),
}))
}
fn create_dir_all(&self, path: &Path) -> io::Result<()> {
self.inner.create_dir_all(path)
}
fn create_dir(&self, path: &Path) -> io::Result<()> {
self.inner.create_dir(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<FsDirEntry>> {
self.inner.read_dir(path)
}
fn remove_file(&self, path: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.inner.remove_file(path)?;
let name = self.name_after(path);
self.state.lock().forget_name(&name);
Ok(())
}
fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.inner.remove_dir_all(path)?;
let name = self.name_after(path);
let path = name.as_path();
let mut state = self.state.lock();
let linked: Vec<PathBuf> = state
.link_group
.keys()
.filter(|k| k.starts_with(path))
.cloned()
.collect();
for name in &linked {
state.forget_name(name);
}
state.durable.retain(|k, _| !k.starts_with(path));
state.touched.retain(|k| !k.starts_with(path));
state.pending_entries.retain(|k| !k.starts_with(path));
Ok(())
}
fn same_file(&self, a: &Path, b: &Path) -> io::Result<bool> {
self.inner.same_file(a, b)
}
fn read_link(&self, path: &Path) -> io::Result<Option<PathBuf>> {
self.inner.read_link(path)
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
let spelled = || (self.listed_as_spelled(from), self.listed_as_spelled(to));
let before = matches!(self.inner.same_file(from, to), Ok(true)).then(spelled);
self.inner.rename(from, to)?;
let unchanged = match before {
Some(before) => before == spelled(),
None => {
(matches!(self.inner.read_link(from), Ok(Some(_)))
|| matches!(self.inner.exists(from), Ok(true)))
&& self.listed_as_spelled(from)
}
};
if from == to || unchanged {
return Ok(());
}
let (from, to) = (self.name_after(from), self.name_after(to));
let (from, to) = (from.as_path(), to.as_path());
let mut state = self.state.lock();
let from_durable = state.durable.remove(from);
let from_touched = state.touched.remove(from);
let from_group = state.link_group.remove(from);
state.pending_entries.remove(from);
state.forget_name(to);
if let Some(bytes) = from_durable {
state.durable.insert(to.to_path_buf(), bytes);
}
if from_touched {
state.touched.insert(to.to_path_buf());
}
state.mark_pending(to);
if let Some(group) = from_group {
state.link_group.insert(to.to_path_buf(), group);
}
Ok(())
}
fn metadata(&self, path: &Path) -> io::Result<FsMetadata> {
self.inner.metadata(path)
}
fn sync_directory(&self, path: &Path) -> io::Result<()> {
self.sync_entries_of(path, || self.inner.sync_directory(path))
}
fn sync_directory_with(&self, path: &Path, mode: SyncMode) -> io::Result<()> {
self.sync_entries_of(path, || self.inner.sync_directory_with(path, mode))
}
fn exists(&self, path: &Path) -> io::Result<bool> {
self.inner.exists(path)
}
fn hard_link(&self, src: &Path, dst: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.inner.hard_link(src, dst)?;
let link = self.name_after(dst);
if matches!(self.inner.read_link(dst), Ok(Some(_))) {
self.new_entry(&link);
return Ok(());
}
let file = self.entry_of(src).unwrap_or_else(|_| self.name_after(src));
self.track_copy(&file, &link)?;
if matches!(self.inner.same_file(&file, &link), Ok(true)) {
let mut state = self.state.lock();
state.link(&file, &link);
if let Some(bytes) = state.durable.get(&link).cloned() {
state.set_durable(&link, bytes);
}
}
Ok(())
}
fn backend_id(&self) -> Option<u64> {
self.inner.backend_id()
}
fn volume_id(&self, path: &Path) -> Option<u64> {
self.inner.volume_id(path)
}
fn capabilities(&self, path: &Path) -> FsCapabilities {
self.inner.capabilities(path)
}
fn try_disable_cow(&self, path: &Path) -> io::Result<()> {
self.inner.try_disable_cow(path)
}
fn punch_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.capture_first_touch(&self.entry_of(path)?)?;
self.inner.punch_hole(path, offset, len)
}
fn reflink_file(&self, src: &Path, dst: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.inner.reflink_file(src, dst)?;
let file = self.entry_of(src).unwrap_or_else(|_| self.name_after(src));
self.track_copy(&file, &self.name_after(dst))?;
Ok(())
}
fn truncate_file(&self, path: &Path) -> io::Result<()> {
let _namespace = self.namespace.lock();
self.capture_first_touch(&self.entry_of(path)?)?;
self.inner.truncate_file(path)
}
fn hard_link_count(&self, path: &Path) -> io::Result<u64> {
self.inner.hard_link_count(path)
}
fn available_space(&self, path: &Path) -> io::Result<u64> {
self.inner.available_space(path)
}
fn allocated_size(&self, path: &Path) -> io::Result<Option<u64>> {
self.inner.allocated_size(path)
}
fn extent_is_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<Option<bool>> {
self.inner.extent_is_hole(path, offset, len)
}
fn extent_contains_hole(&self, path: &Path, offset: u64, len: u64) -> io::Result<Option<bool>> {
self.inner.extent_contains_hole(path, offset, len)
}
}
struct CrashFile {
inner: Box<dyn FsFile>,
path: PathBuf,
fs: Arc<dyn Fs>,
state: Arc<spin::Mutex<CrashState>>,
}
impl CrashFile {
fn snapshot(&self) -> io::Result<()> {
let mut rf = self.fs.open(&self.path, &FsOpenOptions::new().read(true))?;
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut rf, &mut buf)?;
self.state.lock().set_durable(&self.path, buf);
Ok(())
}
}
impl std::io::Read for CrashFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl std::io::Write for CrashFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl std::io::Seek for CrashFile {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
impl FsFile for CrashFile {
fn sync_all(&self) -> io::Result<()> {
self.inner.sync_all()?;
self.snapshot()
}
fn sync_data(&self) -> io::Result<()> {
self.inner.sync_data()?;
self.snapshot()
}
fn sync_all_with(&self, mode: SyncMode) -> io::Result<()> {
self.inner.sync_all_with(mode)?;
self.snapshot()
}
fn sync_data_with(&self, mode: SyncMode) -> io::Result<()> {
self.inner.sync_data_with(mode)?;
self.snapshot()
}
fn metadata(&self) -> io::Result<FsMetadata> {
self.inner.metadata()
}
fn hard_link_count(&self) -> io::Result<u64> {
self.inner.hard_link_count()
}
fn set_len(&self, size: u64) -> io::Result<()> {
self.inner.set_len(size)
}
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
self.inner.read_at(buf, offset)
}
fn lock_exclusive(&self) -> io::Result<()> {
self.inner.lock_exclusive()
}
fn try_lock_exclusive(&self) -> io::Result<bool> {
self.inner.try_lock_exclusive()
}
fn hint(&self, hint: FileHint) -> io::Result<()> {
self.inner.hint(hint)
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
mod tests;