pub mod shim;
use crate::world::World;
use indexmap::{IndexMap, IndexSet};
use rand::{Rng, RngCore};
use rand_distr::{Distribution, Exp};
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
thread_local! {
static WORKER_FS_CONTEXT: RefCell<Option<WorkerContext>> = const { RefCell::new(None) };
}
struct WorkerContext {
fs: Arc<Mutex<Fs>>,
time: Duration,
rng: Mutex<rand::rngs::ThreadRng>,
}
pub struct FsHandle {
fs: Arc<Mutex<Fs>>,
time: Duration,
}
impl FsHandle {
pub fn current() -> Self {
World::current(|world| {
let addr = world.current.expect("current host missing");
let host = world.hosts.get(&addr).unwrap();
let time = host.timer.since_epoch();
FsHandle {
fs: Arc::clone(&host.fs),
time,
}
})
}
pub fn enter(&self) -> FsHandleGuard {
WORKER_FS_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = Some(WorkerContext {
fs: Arc::clone(&self.fs),
time: self.time,
rng: Mutex::new(rand::rng()),
});
});
FsHandleGuard { _private: () }
}
}
#[must_use = "the filesystem context is only active while this guard is held"]
pub struct FsHandleGuard {
_private: (),
}
impl Drop for FsHandleGuard {
fn drop(&mut self) {
WORKER_FS_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = None;
});
}
}
pub(crate) struct FsContext<'a> {
pub fs: &'a mut Fs,
rng: &'a mut dyn RngCore,
pub now: Duration,
}
impl FsContext<'_> {
fn in_worker_context() -> bool {
WORKER_FS_CONTEXT.with(|ctx| ctx.borrow().is_some())
}
pub fn current<R>(f: impl FnOnce(FsContext<'_>) -> R) -> R {
if Self::in_worker_context() {
WORKER_FS_CONTEXT.with(|ctx| {
let borrowed = ctx.borrow();
let worker_ctx = borrowed.as_ref().unwrap();
let mut fs_guard = worker_ctx.fs.lock().unwrap();
let now = worker_ctx.time;
let mut rng_guard = worker_ctx.rng.lock().unwrap();
f(FsContext {
fs: &mut fs_guard,
rng: &mut rng_guard,
now,
})
})
} else {
World::current(|world| {
let addr = world.current.expect("current host missing");
let now = world.hosts[&addr].timer.since_epoch();
let World { hosts, rng, .. } = world;
let host = hosts.get_mut(&addr).unwrap();
let mut fs_guard = host.fs.lock().unwrap();
f(FsContext {
fs: &mut fs_guard,
rng: &mut **rng,
now,
})
})
}
}
pub fn current_if_set(f: impl FnOnce(FsContext<'_>)) {
if Self::in_worker_context() {
WORKER_FS_CONTEXT.with(|ctx| {
let borrowed = ctx.borrow();
let worker_ctx = borrowed.as_ref().unwrap();
let mut fs_guard = worker_ctx.fs.lock().unwrap();
let now = worker_ctx.time;
let mut rng_guard = worker_ctx.rng.lock().unwrap();
f(FsContext {
fs: &mut fs_guard,
rng: &mut rng_guard,
now,
});
});
} else {
World::current_if_set(|world| {
let addr = world.current.expect("current host missing");
let now = world.hosts[&addr].timer.since_epoch();
let World { hosts, rng, .. } = world;
let host = hosts.get_mut(&addr).unwrap();
let mut fs_guard = host.fs.lock().unwrap();
f(FsContext {
fs: &mut fs_guard,
rng: &mut **rng,
now,
})
})
}
}
pub fn random_bool(&mut self, probability: f64) -> bool {
self.rng.random_bool(probability)
}
pub fn random_range(&mut self, range: Range<usize>) -> usize {
self.rng.random_range(range)
}
pub fn random_u8(&mut self) -> u8 {
self.rng.random()
}
}
#[derive(Debug, Clone)]
pub struct FsCorruption {
pub path: PathBuf,
pub offset: u64,
pub len: usize,
}
#[derive(Debug, Clone)]
pub struct IoLatency {
pub(crate) min_latency: Duration,
pub(crate) max_latency: Duration,
pub(crate) distribution: Exp<f64>,
}
impl Default for IoLatency {
fn default() -> Self {
Self {
min_latency: Duration::from_micros(50),
max_latency: Duration::from_millis(5),
distribution: Exp::new(5.0).unwrap(),
}
}
}
impl IoLatency {
pub fn min_latency(&mut self, value: Duration) -> &mut Self {
self.min_latency = value;
self
}
pub fn max_latency(&mut self, value: Duration) -> &mut Self {
self.max_latency = value;
self
}
pub fn distribution(&mut self, lambda: f64) -> &mut Self {
self.distribution = Exp::new(lambda).expect("lambda must be positive");
self
}
}
#[derive(Debug, Clone)]
pub struct PageCacheConfig {
pub(crate) page_size: u64,
pub(crate) max_pages: usize,
pub(crate) random_eviction_probability: f64,
}
impl Default for PageCacheConfig {
fn default() -> Self {
Self {
page_size: 4096,
max_pages: 256,
random_eviction_probability: 0.0,
}
}
}
impl PageCacheConfig {
pub fn page_size(&mut self, size: u64) -> &mut Self {
assert!(size > 0, "page_size must be positive");
self.page_size = size;
self
}
pub fn max_pages(&mut self, count: usize) -> &mut Self {
self.max_pages = count;
self
}
pub fn random_eviction_probability(&mut self, prob: f64) -> &mut Self {
assert!(
(0.0..=1.0).contains(&prob),
"random_eviction_probability must be between 0.0 and 1.0"
);
self.random_eviction_probability = prob;
self
}
}
#[derive(Debug, Clone)]
pub struct FsConfig {
pub(crate) sync_probability: f64,
pub(crate) capacity: Option<u64>,
pub(crate) io_error_probability: f64,
pub(crate) corruption_probability: f64,
pub(crate) noatime: bool,
pub(crate) direct_io_alignment: u64,
pub(crate) block_size: Option<u64>,
pub(crate) io_latency: Option<IoLatency>,
pub(crate) page_cache: Option<PageCacheConfig>,
}
impl Default for FsConfig {
fn default() -> Self {
Self {
sync_probability: 0.0,
capacity: None,
io_error_probability: 0.0,
corruption_probability: 0.0,
noatime: true,
direct_io_alignment: 512,
block_size: None,
io_latency: None,
page_cache: None,
}
}
}
impl FsConfig {
pub fn sync_probability(&mut self, value: f64) -> &mut Self {
assert!(
(0.0..=1.0).contains(&value),
"sync_probability must be between 0.0 and 1.0"
);
self.sync_probability = value;
self
}
pub fn capacity(&mut self, bytes: u64) -> &mut Self {
self.capacity = Some(bytes);
self
}
pub fn io_error_probability(&mut self, value: f64) -> &mut Self {
assert!(
(0.0..=1.0).contains(&value),
"io_error_probability must be between 0.0 and 1.0"
);
self.io_error_probability = value;
self
}
pub fn corruption_probability(&mut self, value: f64) -> &mut Self {
assert!(
(0.0..=1.0).contains(&value),
"corruption_probability must be between 0.0 and 1.0"
);
self.corruption_probability = value;
self
}
pub fn noatime(&mut self, value: bool) -> &mut Self {
if !value {
unimplemented!("atime tracking is not implemented; noatime must be true");
}
self.noatime = value;
self
}
pub fn direct_io_alignment(&mut self, bytes: u64) -> &mut Self {
assert!(bytes > 0, "direct_io_alignment must be positive");
assert!(
bytes.is_power_of_two(),
"direct_io_alignment must be a power of two"
);
self.direct_io_alignment = bytes;
self
}
pub fn block_size(&mut self, bytes: u64) -> &mut Self {
assert!(bytes > 0, "block_size must be positive");
self.block_size = Some(bytes);
self
}
pub fn io_latency(&mut self) -> &mut IoLatency {
self.io_latency.get_or_insert_with(IoLatency::default)
}
pub fn page_cache(&mut self) -> &mut PageCacheConfig {
self.page_cache.get_or_insert_with(PageCacheConfig::default)
}
}
#[derive(Debug, Clone)]
pub(crate) enum PendingOp {
CreateFile {
path: PathBuf,
time: Duration,
mode: u32,
},
CreateDir {
path: PathBuf,
time: Duration,
mode: u32,
},
CreateSymlink {
path: PathBuf,
target: PathBuf,
time: Duration,
},
CreateHardLink {
path: PathBuf,
target: PathBuf,
time: Duration,
},
Write {
path: PathBuf,
offset: u64,
data: Vec<u8>,
time: Duration,
},
SetLen {
path: PathBuf,
len: u64,
time: Duration,
},
SetPermissions {
path: PathBuf,
mode: u32,
time: Duration,
},
Rename { from: PathBuf, to: PathBuf },
RemoveFile { path: PathBuf },
RemoveDir { path: PathBuf },
}
#[derive(Debug, Clone)]
pub(crate) struct FileData {
pub(crate) content: Vec<u8>,
pub(crate) crtime: Duration,
pub(crate) mtime: Duration,
pub(crate) ctime: Duration,
pub(crate) mode: u32,
pub(crate) nlink: u64,
}
impl FileData {
#[allow(dead_code)]
fn new(now: Duration) -> Self {
Self::with_mode(now, 0o644)
}
fn with_mode(now: Duration, mode: u32) -> Self {
Self {
content: Vec::new(),
crtime: now,
mtime: now,
ctime: now,
mode,
nlink: 1,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DirData {
pub(crate) crtime: Duration,
pub(crate) mtime: Duration,
pub(crate) ctime: Duration,
pub(crate) mode: u32,
}
impl DirData {
fn new(now: Duration) -> Self {
Self::with_mode(now, 0o755)
}
fn with_mode(now: Duration, mode: u32) -> Self {
Self {
crtime: now,
mtime: now,
ctime: now,
mode,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct SymlinkData {
pub(crate) target: PathBuf,
pub(crate) crtime: Duration,
pub(crate) mtime: Duration,
pub(crate) ctime: Duration,
}
impl SymlinkData {
fn new(target: PathBuf, now: Duration) -> Self {
Self {
target,
crtime: now,
mtime: now,
ctime: now,
}
}
}
type PageKey = (PathBuf, u64);
pub(crate) struct PageCache {
pages: IndexSet<PageKey>,
config: PageCacheConfig,
}
impl PageCache {
pub fn new(config: PageCacheConfig) -> Self {
Self {
pages: IndexSet::new(),
config,
}
}
pub fn access(&mut self, path: &Path, offset: u64, rng: &mut dyn RngCore) -> bool {
let page_idx = offset / self.config.page_size;
let key = (path.to_path_buf(), page_idx);
if self.config.random_eviction_probability > 0.0
&& rng.random_bool(self.config.random_eviction_probability)
{
self.pages.swap_remove(&key);
return false;
}
if self.pages.swap_remove(&key) {
self.pages.insert(key);
return true;
}
false
}
pub fn insert(&mut self, path: &Path, offset: u64) {
let page_idx = offset / self.config.page_size;
let key = (path.to_path_buf(), page_idx);
while self.pages.len() >= self.config.max_pages {
self.pages.shift_remove_index(0);
}
self.pages.swap_remove(&key);
self.pages.insert(key);
}
pub fn invalidate_file(&mut self, path: &Path) {
self.pages.retain(|(p, _)| p != path);
}
}
pub(crate) struct Fs {
pub(crate) persisted_files: IndexMap<PathBuf, FileData>,
pub(crate) persisted_dirs: IndexMap<PathBuf, DirData>,
pub(crate) persisted_symlinks: IndexMap<PathBuf, SymlinkData>,
synced_entries: IndexSet<PathBuf>,
pub(crate) pending: Vec<PendingOp>,
pub(crate) open_handles: IndexMap<u64, PathBuf>,
next_fd: u64,
pub(crate) sync_probability: f64,
pub(crate) capacity: Option<u64>,
pub(crate) io_error_probability: f64,
pub(crate) corruption_probability: f64,
pub(crate) direct_io_alignment: u64,
pub(crate) block_size: Option<u64>,
pub(crate) io_latency: Option<IoLatency>,
pub(crate) page_cache: Option<PageCache>,
}
impl Fs {
pub(crate) fn new(config: FsConfig) -> Self {
let mut persisted_dirs = IndexMap::new();
let mut synced_entries = IndexSet::new();
persisted_dirs.insert(PathBuf::from("/"), DirData::new(Duration::ZERO));
synced_entries.insert(PathBuf::from("/"));
Self {
persisted_files: IndexMap::new(),
persisted_dirs,
persisted_symlinks: IndexMap::new(),
synced_entries,
pending: Vec::new(),
open_handles: IndexMap::new(),
next_fd: 0,
sync_probability: config.sync_probability,
capacity: config.capacity,
io_error_probability: config.io_error_probability,
corruption_probability: config.corruption_probability,
direct_io_alignment: config.direct_io_alignment,
block_size: config.block_size,
io_latency: config.io_latency,
page_cache: config.page_cache.map(PageCache::new),
}
}
pub(crate) fn calculate_latency(&self, rng: &mut dyn RngCore, cache_hit: bool) -> Duration {
if cache_hit && self.page_cache.is_some() {
return Duration::from_nanos(100);
}
match &self.io_latency {
Some(cfg) => {
let range = cfg.max_latency.saturating_sub(cfg.min_latency);
let sample: f64 = cfg.distribution.sample(rng);
let scaled = range.mul_f64(sample.min(1.0));
cfg.min_latency + scaled
}
None => Duration::ZERO,
}
}
pub(crate) fn used_bytes(&self) -> u64 {
let mut total: u64 = self
.persisted_files
.values()
.map(|v| v.content.len() as u64)
.sum();
for op in &self.pending {
match op {
PendingOp::Write {
path, offset, data, ..
} => {
let persisted_len = self
.persisted_files
.get(path)
.map(|v| v.content.len() as u64)
.unwrap_or(0);
let write_end = offset + data.len() as u64;
if write_end > persisted_len {
total += write_end - persisted_len;
}
}
PendingOp::SetLen { path, len, .. } => {
let persisted_len = self
.persisted_files
.get(path)
.map(|v| v.content.len() as u64)
.unwrap_or(0);
if *len > persisted_len {
total += len - persisted_len;
}
}
_ => {}
}
}
total
}
pub(crate) fn check_space(&self, additional_bytes: u64) -> Result<(), &'static str> {
if let Some(cap) = self.capacity {
let used = self.used_bytes();
if used.saturating_add(additional_bytes) > cap {
return Err("No space left on device");
}
}
Ok(())
}
pub(crate) fn alloc_fd(&mut self) -> u64 {
let fd = self.next_fd;
self.next_fd += 1;
fd
}
pub(crate) fn discard_pending(&mut self, rng: &mut dyn RngCore) {
if let Some(block_size) = self.block_size {
self.apply_torn_writes(block_size, rng);
}
self.pending.clear();
self.persisted_files
.retain(|path, _| self.synced_entries.contains(path));
self.persisted_dirs
.retain(|path, _| self.synced_entries.contains(path));
self.persisted_symlinks
.retain(|path, _| self.synced_entries.contains(path));
}
fn apply_torn_writes(&mut self, block_size: u64, rng: &mut dyn RngCore) {
let torn_writes: Vec<_> = self
.pending
.iter()
.filter_map(|op| {
if let PendingOp::Write {
path,
offset,
data,
time,
} = op
{
if !self.synced_entries.contains(path) {
return None;
}
let total_blocks = (data.len() as u64).div_ceil(block_size);
if total_blocks == 0 {
return None;
}
let surviving_blocks = rng.random_range(0..=total_blocks as usize) as u64;
if surviving_blocks == 0 {
return None;
}
let surviving_bytes =
(surviving_blocks * block_size).min(data.len() as u64) as usize;
Some((
path.clone(),
*offset,
data[..surviving_bytes].to_vec(),
*time,
))
} else {
None
}
})
.collect();
for (path, offset, data, time) in torn_writes {
if let Some(file_data) = self.persisted_files.get_mut(&path) {
let end = offset as usize + data.len();
if end > file_data.content.len() {
file_data.content.resize(end, 0);
}
file_data.content[offset as usize..end].copy_from_slice(&data);
file_data.mtime = time;
file_data.ctime = time;
}
}
}
fn apply_op_to_persisted(&mut self, op: &PendingOp) {
match op {
PendingOp::CreateFile { path, time, mode } => {
self.persisted_files
.entry(path.clone())
.or_insert_with(|| FileData::with_mode(*time, *mode));
}
PendingOp::CreateDir { path, time, mode } => {
self.persisted_dirs
.entry(path.clone())
.or_insert_with(|| DirData::with_mode(*time, *mode));
}
PendingOp::CreateSymlink { path, target, time } => {
self.persisted_symlinks
.entry(path.clone())
.or_insert_with(|| SymlinkData::new(target.clone(), *time));
}
PendingOp::CreateHardLink { path, target, time } => {
if let Some(target_data) = self.persisted_files.get(target).cloned() {
let new_nlink = target_data.nlink + 1;
if let Some(t) = self.persisted_files.get_mut(target) {
t.nlink = new_nlink;
t.ctime = *time; }
let mut link_data = target_data;
link_data.nlink = new_nlink;
self.persisted_files.insert(path.clone(), link_data);
}
}
PendingOp::Write {
path,
offset,
data,
time,
} => {
if let Some(file_data) = self.persisted_files.get_mut(path) {
let end = *offset as usize + data.len();
if end > file_data.content.len() {
file_data.content.resize(end, 0);
}
file_data.content[*offset as usize..end].copy_from_slice(data);
file_data.mtime = *time;
file_data.ctime = *time;
}
}
PendingOp::SetLen { path, len, time } => {
if let Some(file_data) = self.persisted_files.get_mut(path) {
file_data.content.resize(*len as usize, 0);
file_data.mtime = *time;
file_data.ctime = *time;
}
}
PendingOp::SetPermissions { path, mode, time } => {
if let Some(file_data) = self.persisted_files.get_mut(path) {
file_data.mode = *mode;
file_data.ctime = *time;
} else if let Some(dir_data) = self.persisted_dirs.get_mut(path) {
dir_data.mode = *mode;
dir_data.ctime = *time;
}
}
PendingOp::Rename { from, to } => {
if let Some(file_data) = self.persisted_files.swap_remove(from) {
self.persisted_files.insert(to.clone(), file_data);
} else if let Some(dir_data) = self.persisted_dirs.swap_remove(from) {
self.persisted_dirs.insert(to.clone(), dir_data);
} else if let Some(symlink_data) = self.persisted_symlinks.swap_remove(from) {
self.persisted_symlinks.insert(to.clone(), symlink_data);
}
}
PendingOp::RemoveFile { path } => {
self.persisted_files.swap_remove(path);
self.persisted_symlinks.swap_remove(path);
}
PendingOp::RemoveDir { path } => {
self.persisted_dirs.swap_remove(path);
}
}
}
fn resolve_persisted_path(&self, path: &Path) -> PathBuf {
let mut current = path.to_path_buf();
for op in self.pending.iter().rev() {
if let PendingOp::Rename { from, to } = op {
if to == ¤t {
current = from.clone();
}
}
}
current
}
fn resolve_hardlink_target(&self, path: &Path) -> Option<PathBuf> {
for op in &self.pending {
if let PendingOp::CreateHardLink {
path: p, target, ..
} = op
{
if p == path {
return Some(target.clone());
}
}
}
None
}
fn resolve_content_path(&self, path: &Path) -> PathBuf {
if let Some(target) = self.resolve_hardlink_target(path) {
return self.resolve_content_path(&target);
}
self.resolve_persisted_path(path)
}
pub(crate) fn file_exists(&self, path: &Path) -> bool {
let mut exists = self.persisted_files.contains_key(path);
for op in &self.pending {
match op {
PendingOp::CreateFile { path: p, .. } if p == path => exists = true,
PendingOp::CreateHardLink { path: p, .. } if p == path => exists = true,
PendingOp::RemoveFile { path: p } if p == path => exists = false,
PendingOp::Rename { from, to: _ } if from == path => exists = false,
PendingOp::Rename { from: _, to } if to == path => exists = true,
_ => {}
}
}
exists
}
pub(crate) fn dir_exists(&self, path: &Path) -> bool {
let mut exists = self.persisted_dirs.contains_key(path);
for op in &self.pending {
match op {
PendingOp::CreateDir { path: p, .. } if p == path => exists = true,
PendingOp::RemoveDir { path: p } if p == path => exists = false,
PendingOp::Rename { from, to: _ } if from == path => {
if self.persisted_dirs.contains_key(from) {
exists = false;
}
}
PendingOp::Rename { from, to } if to == path => {
if self.persisted_dirs.contains_key(from) {
exists = true;
}
}
_ => {}
}
}
exists
}
pub(crate) fn symlink_exists(&self, path: &Path) -> bool {
let mut exists = self.persisted_symlinks.contains_key(path);
for op in &self.pending {
match op {
PendingOp::CreateSymlink { path: p, .. } if p == path => exists = true,
PendingOp::RemoveFile { path: p } if p == path => exists = false,
PendingOp::Rename { from, to: _ } if from == path => {
if self.persisted_symlinks.contains_key(from) || exists {
exists = false;
}
}
PendingOp::Rename { from, to } if to == path => {
if self.persisted_symlinks.contains_key(from)
|| self.was_symlink_created_at(from)
{
exists = true;
}
}
_ => {}
}
}
exists
}
fn was_symlink_created_at(&self, path: &Path) -> bool {
for op in &self.pending {
if let PendingOp::CreateSymlink { path: p, .. } = op {
if p == path {
return true;
}
}
}
false
}
pub(crate) fn file_len(&self, path: &Path) -> u64 {
let content_path = self.resolve_content_path(path);
let mut len = self
.persisted_files
.get(&content_path)
.map(|v| v.content.len() as u64)
.unwrap_or(0);
for op in &self.pending {
match op {
PendingOp::Write {
path: p,
offset,
data,
..
} => {
if p == &content_path || self.path_renamed_to(p, &content_path) {
let end = offset + data.len() as u64;
if end > len {
len = end;
}
}
}
PendingOp::SetLen {
path: p,
len: new_len,
..
} => {
if p == &content_path || self.path_renamed_to(p, &content_path) {
len = *new_len;
}
}
_ => {}
}
}
len
}
pub(crate) fn mkdir(&mut self, path: &Path, time: Duration) -> Result<(), &'static str> {
self.mkdir_with_mode(path, time, 0o755)
}
pub(crate) fn mkdir_with_mode(
&mut self,
path: &Path,
time: Duration,
mode: u32,
) -> Result<(), &'static str> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !self.dir_exists(parent) {
return Err("No such file or directory");
}
}
if self.dir_exists(path) || self.file_exists(path) || self.symlink_exists(path) {
return Err("File exists");
}
self.pending.push(PendingOp::CreateDir {
path: path.to_path_buf(),
time,
mode,
});
Ok(())
}
pub(crate) fn rmdir(&mut self, path: &Path) -> Result<(), &'static str> {
if !self.dir_exists(path) {
return Err("No such file or directory");
}
if self.dir_has_children(path) {
return Err("Directory not empty");
}
self.pending.push(PendingOp::RemoveDir {
path: path.to_path_buf(),
});
Ok(())
}
fn dir_has_children(&self, path: &Path) -> bool {
for file_path in self.persisted_files.keys() {
if file_path.parent() == Some(path) && self.file_exists(file_path) {
return true;
}
}
for dir_path in self.persisted_dirs.keys() {
if dir_path.parent() == Some(path) && self.dir_exists(dir_path) {
return true;
}
}
for symlink_path in self.persisted_symlinks.keys() {
if symlink_path.parent() == Some(path) && self.symlink_exists(symlink_path) {
return true;
}
}
for op in &self.pending {
match op {
PendingOp::CreateFile { path: p, .. } if p.parent() == Some(path) => {
if self.file_exists(p) {
return true;
}
}
PendingOp::CreateDir { path: p, .. } if p.parent() == Some(path) => {
if self.dir_exists(p) {
return true;
}
}
PendingOp::CreateSymlink { path: p, .. } if p.parent() == Some(path) => {
if self.symlink_exists(p) {
return true;
}
}
_ => {}
}
}
false
}
pub(crate) fn parent_exists(&self, path: &Path) -> bool {
match path.parent() {
None => true,
Some(parent) if parent.as_os_str().is_empty() => true,
Some(parent) => self.dir_exists(parent),
}
}
pub(crate) fn unlink(&mut self, path: &Path) -> Result<(), &'static str> {
if !self.file_exists(path) && !self.symlink_exists(path) {
return Err("No such file or directory");
}
if let Some(cache) = &mut self.page_cache {
cache.invalidate_file(path);
}
self.pending.push(PendingOp::RemoveFile {
path: path.to_path_buf(),
});
Ok(())
}
pub(crate) fn rename(&mut self, from: &Path, to: &Path) -> Result<(), &'static str> {
if !self.parent_exists(to) {
return Err("No such file or directory");
}
if self.file_exists(from) {
if self.dir_exists(to) {
return Err("Is a directory");
}
self.pending.push(PendingOp::Rename {
from: from.to_path_buf(),
to: to.to_path_buf(),
});
return Ok(());
}
if self.dir_exists(from) {
if self.file_exists(to) || self.symlink_exists(to) {
return Err("Not a directory");
}
if self.dir_exists(to) && self.dir_has_children(to) {
return Err("Directory not empty");
}
self.pending.push(PendingOp::Rename {
from: from.to_path_buf(),
to: to.to_path_buf(),
});
return Ok(());
}
if self.symlink_exists(from) {
if self.dir_exists(to) {
return Err("Is a directory");
}
self.pending.push(PendingOp::Rename {
from: from.to_path_buf(),
to: to.to_path_buf(),
});
return Ok(());
}
Err("No such file or directory")
}
pub(crate) fn sync_file(&mut self, path: &Path) -> Result<(), &'static str> {
if !self.file_exists(path) {
return Err("No such file or directory");
}
let (to_flush, to_keep): (Vec<_>, Vec<_>) =
self.pending.drain(..).partition(|op| match op {
PendingOp::Write { path: p, .. } => p == path,
PendingOp::SetLen { path: p, .. } => p == path,
_ => false,
});
if !self.persisted_files.contains_key(path) {
let (crtime, mode) = to_keep
.iter()
.find_map(|op| match op {
PendingOp::CreateFile {
path: p,
time,
mode,
} if p == path => Some((*time, *mode)),
_ => None,
})
.unwrap_or((Duration::ZERO, 0o644));
self.persisted_files
.insert(path.to_path_buf(), FileData::with_mode(crtime, mode));
}
self.pending = to_keep;
for op in &to_flush {
self.apply_op_to_persisted(op);
}
Ok(())
}
pub(crate) fn sync_file_data(&mut self, path: &Path) -> Result<(), &'static str> {
if !self.file_exists(path) {
return Err("No such file or directory");
}
let (to_flush, to_keep): (Vec<_>, Vec<_>) =
self.pending.drain(..).partition(|op| match op {
PendingOp::Write { path: p, .. } => p == path,
PendingOp::SetLen { path: p, .. } => p == path,
_ => false,
});
if !self.persisted_files.contains_key(path) {
let (crtime, mode) = to_keep
.iter()
.find_map(|op| match op {
PendingOp::CreateFile {
path: p,
time,
mode,
} if p == path => Some((*time, *mode)),
_ => None,
})
.unwrap_or((Duration::ZERO, 0o644));
self.persisted_files
.insert(path.to_path_buf(), FileData::with_mode(crtime, mode));
}
self.pending = to_keep;
for op in &to_flush {
self.apply_op_to_persisted(op);
}
Ok(())
}
pub(crate) fn sync_dir(&mut self, path: &Path, time: Duration) -> Result<(), &'static str> {
if !self.dir_exists(path) {
return Err("No such file or directory");
}
let (to_flush, to_keep): (Vec<_>, Vec<_>) = self.pending.drain(..).partition(|op| {
match op {
PendingOp::CreateDir { path: p, .. } if p == path => true,
PendingOp::CreateFile { path: p, .. } => p.parent() == Some(path),
PendingOp::CreateDir { path: p, .. } => p.parent() == Some(path),
PendingOp::CreateSymlink { path: p, .. } => p.parent() == Some(path),
PendingOp::CreateHardLink { path: p, .. } => p.parent() == Some(path),
PendingOp::RemoveFile { path: p } => p.parent() == Some(path),
PendingOp::RemoveDir { path: p } => p.parent() == Some(path),
PendingOp::Rename { from, to } => {
from.parent() == Some(path) || to.parent() == Some(path)
}
_ => false,
}
});
self.pending = to_keep;
let mut dir_modified = false;
for op in &to_flush {
match op {
PendingOp::CreateFile { path: p, .. } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.insert(p.clone());
}
PendingOp::CreateDir { path: p, .. } if p == path => {
self.synced_entries.insert(p.clone());
}
PendingOp::CreateDir { path: p, .. } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.insert(p.clone());
}
PendingOp::CreateSymlink { path: p, .. } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.insert(p.clone());
}
PendingOp::CreateHardLink { path: p, .. } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.insert(p.clone());
}
PendingOp::RemoveFile { path: p } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.swap_remove(p);
}
PendingOp::RemoveDir { path: p } if p.parent() == Some(path) => {
dir_modified = true;
self.synced_entries.swap_remove(p);
}
PendingOp::Rename { from, to } => {
if from.parent() == Some(path) {
dir_modified = true;
self.synced_entries.swap_remove(from);
}
if to.parent() == Some(path) {
dir_modified = true;
self.synced_entries.insert(to.clone());
}
}
_ => {}
}
self.apply_op_to_persisted(op);
}
if dir_modified {
if let Some(dir_data) = self.persisted_dirs.get_mut(path) {
dir_data.mtime = time;
dir_data.ctime = time;
}
}
Ok(())
}
pub(crate) fn read_file(&self, path: &Path, buf: &mut [u8], offset: u64) -> usize {
if buf.is_empty() {
return 0;
}
let file_len = self.file_len(path);
if offset >= file_len {
return 0;
}
let available = (file_len - offset) as usize;
let to_read = buf.len().min(available);
buf[..to_read].fill(0);
let content_path = self.resolve_content_path(path);
if let Some(file_data) = self.persisted_files.get(&content_path) {
let persisted_end = (file_data.content.len() as u64).min(offset + to_read as u64);
if offset < persisted_end {
let src_start = offset as usize;
let src_end = persisted_end as usize;
let dst_len = src_end - src_start;
buf[..dst_len].copy_from_slice(&file_data.content[src_start..src_end]);
}
}
for op in &self.pending {
if let PendingOp::Write {
path: p,
offset: write_off,
data,
..
} = op
{
let write_applies = p == &content_path || self.path_renamed_to(p, &content_path);
if write_applies {
let write_end = write_off + data.len() as u64;
let read_end = offset + to_read as u64;
if *write_off < read_end && write_end > offset {
let overlap_start = write_off.max(&offset);
let overlap_end = write_end.min(read_end);
let src_offset = (overlap_start - write_off) as usize;
let dst_offset = (overlap_start - offset) as usize;
let len = (overlap_end - overlap_start) as usize;
buf[dst_offset..dst_offset + len]
.copy_from_slice(&data[src_offset..src_offset + len]);
}
}
}
}
to_read
}
fn path_renamed_to(&self, from: &Path, to: &Path) -> bool {
let mut current = from.to_path_buf();
for op in &self.pending {
if let PendingOp::Rename { from: f, to: t } = op {
if f == ¤t {
current = t.clone();
}
}
}
current == to
}
pub(crate) fn write_file(&mut self, path: &Path, offset: u64, data: &[u8], time: Duration) {
if !data.is_empty() {
self.pending.push(PendingOp::Write {
path: path.to_path_buf(),
offset,
data: data.to_vec(),
time,
});
}
}
pub(crate) fn set_file_len(&mut self, path: &Path, len: u64, time: Duration) {
if let Some(cache) = &mut self.page_cache {
cache.invalidate_file(path);
}
self.pending.push(PendingOp::SetLen {
path: path.to_path_buf(),
len,
time,
});
}
#[allow(dead_code)]
pub(crate) fn create_file(&mut self, path: &Path, time: Duration) {
self.create_file_with_mode(path, time, 0o644);
}
pub(crate) fn create_file_with_mode(&mut self, path: &Path, time: Duration, mode: u32) {
self.pending.push(PendingOp::CreateFile {
path: path.to_path_buf(),
time,
mode,
});
}
pub(crate) fn create_symlink(
&mut self,
path: &Path,
target: &Path,
time: Duration,
) -> Result<(), &'static str> {
if !self.parent_exists(path) {
return Err("No such file or directory");
}
if self.file_exists(path) || self.dir_exists(path) || self.symlink_exists(path) {
return Err("File exists");
}
self.pending.push(PendingOp::CreateSymlink {
path: path.to_path_buf(),
target: target.to_path_buf(),
time,
});
Ok(())
}
pub(crate) fn create_hard_link(
&mut self,
path: &Path,
target: &Path,
time: Duration,
) -> Result<(), &'static str> {
if !self.parent_exists(path) {
return Err("No such file or directory");
}
if !self.file_exists(target) {
return Err("No such file or directory");
}
if self.file_exists(path) || self.dir_exists(path) || self.symlink_exists(path) {
return Err("File exists");
}
self.pending.push(PendingOp::CreateHardLink {
path: path.to_path_buf(),
target: target.to_path_buf(),
time,
});
Ok(())
}
pub(crate) fn read_link(&self, path: &Path) -> Result<PathBuf, &'static str> {
let original_path = self.resolve_symlink_path(path);
for op in self.pending.iter().rev() {
match op {
PendingOp::CreateSymlink {
path: p, target, ..
} if p == &original_path => {
return Ok(target.clone());
}
PendingOp::RemoveFile { path: p } if p == &original_path => {
return Err("No such file or directory");
}
_ => {}
}
}
self.persisted_symlinks
.get(&original_path)
.map(|s| s.target.clone())
.ok_or("No such file or directory")
}
fn resolve_symlink_path(&self, path: &Path) -> PathBuf {
let mut current = path.to_path_buf();
for op in self.pending.iter().rev() {
if let PendingOp::Rename { from, to } = op {
if to == ¤t {
current = from.clone();
}
}
}
current
}
pub(crate) fn set_permissions(
&mut self,
path: &Path,
mode: u32,
time: Duration,
) -> Result<(), &'static str> {
if !self.file_exists(path) && !self.dir_exists(path) {
return Err("No such file or directory");
}
self.pending.push(PendingOp::SetPermissions {
path: path.to_path_buf(),
mode,
time,
});
Ok(())
}
#[allow(dead_code)]
pub(crate) fn file_mode(&self, path: &Path) -> Option<u32> {
for op in self.pending.iter().rev() {
match op {
PendingOp::SetPermissions { path: p, mode, .. } if p == path => {
return Some(*mode);
}
PendingOp::CreateFile { path: p, mode, .. } if p == path => {
return Some(*mode);
}
_ => {}
}
}
self.persisted_files.get(path).map(|f| f.mode)
}
#[allow(dead_code)]
pub(crate) fn dir_mode(&self, path: &Path) -> Option<u32> {
for op in self.pending.iter().rev() {
match op {
PendingOp::SetPermissions { path: p, mode, .. } if p == path => {
return Some(*mode);
}
PendingOp::CreateDir { path: p, mode, .. } if p == path => {
return Some(*mode);
}
_ => {}
}
}
self.persisted_dirs.get(path).map(|d| d.mode)
}
pub(crate) fn file_nlink(&self, path: &Path) -> u64 {
if let Some(target) = self.resolve_hardlink_target(path) {
return self.file_nlink(&target);
}
let mut nlink = self.persisted_files.get(path).map(|f| f.nlink).unwrap_or(1);
for op in &self.pending {
if let PendingOp::CreateHardLink { target, .. } = op {
if target == path {
nlink += 1;
}
}
}
nlink
}
pub(crate) fn file_timestamps(&self, path: &Path) -> Option<(Duration, Duration, Duration)> {
let persisted_path = self.resolve_persisted_path(path);
let mut timestamps = self
.persisted_files
.get(&persisted_path)
.map(|f| (f.crtime, f.mtime, f.ctime));
for op in &self.pending {
match op {
PendingOp::CreateFile { path: p, time, .. } if p == path => {
timestamps = Some((*time, *time, *time));
}
PendingOp::Write { path: p, time, .. }
| PendingOp::SetLen { path: p, time, .. } => {
if (p == path || self.path_renamed_to(p, path)) && timestamps.is_some() {
let (crtime, _, _) = timestamps.unwrap();
timestamps = Some((crtime, *time, *time));
}
}
_ => {}
}
}
timestamps
}
pub(crate) fn dir_timestamps(&self, path: &Path) -> Option<(Duration, Duration, Duration)> {
let mut timestamps = self
.persisted_dirs
.get(path)
.map(|d| (d.crtime, d.mtime, d.ctime));
for op in &self.pending {
match op {
PendingOp::CreateDir { path: p, time, .. } if p == path => {
timestamps = Some((*time, *time, *time));
}
PendingOp::CreateFile { path: p, time, .. } if p.parent() == Some(path) => {
if let Some((crtime, _, _)) = timestamps {
timestamps = Some((crtime, *time, *time));
}
}
PendingOp::CreateDir { path: p, time, .. } if p.parent() == Some(path) => {
if let Some((crtime, _, _)) = timestamps {
timestamps = Some((crtime, *time, *time));
}
}
PendingOp::RemoveFile { path: p } if p.parent() == Some(path) => {
if let Some((crtime, mtime, _)) = timestamps {
timestamps = Some((crtime, mtime, mtime));
}
}
PendingOp::RemoveDir { path: p } if p.parent() == Some(path) => {
if let Some((crtime, mtime, _)) = timestamps {
timestamps = Some((crtime, mtime, mtime));
}
}
PendingOp::Rename { from, to } => {
if from.parent() == Some(path) || to.parent() == Some(path) {
if let Some((crtime, mtime, _)) = timestamps {
timestamps = Some((crtime, mtime, mtime));
}
}
}
_ => {}
}
}
timestamps
}
pub(crate) fn symlink_timestamps(&self, path: &Path) -> Option<(Duration, Duration, Duration)> {
let mut timestamps = self
.persisted_symlinks
.get(path)
.map(|s| (s.crtime, s.mtime, s.ctime));
for op in &self.pending {
if let PendingOp::CreateSymlink { path: p, time, .. } = op {
if p == path {
timestamps = Some((*time, *time, *time));
}
}
}
timestamps
}
pub(crate) fn dir_entries(&self, path: &Path) -> Vec<PathBuf> {
use std::collections::HashSet;
let mut entries: HashSet<PathBuf> = HashSet::new();
for file_path in self.persisted_files.keys() {
if file_path.parent() == Some(path) && self.file_exists(file_path) {
entries.insert(file_path.clone());
}
}
for dir_path in self.persisted_dirs.keys() {
if dir_path.parent() == Some(path) && self.dir_exists(dir_path) {
entries.insert(dir_path.clone());
}
}
for symlink_path in self.persisted_symlinks.keys() {
if symlink_path.parent() == Some(path) && self.symlink_exists(symlink_path) {
entries.insert(symlink_path.clone());
}
}
for op in &self.pending {
match op {
PendingOp::CreateFile { path: p, .. } if p.parent() == Some(path) => {
if self.file_exists(p) {
entries.insert(p.clone());
}
}
PendingOp::CreateDir { path: p, .. } if p.parent() == Some(path) => {
if self.dir_exists(p) {
entries.insert(p.clone());
}
}
PendingOp::CreateSymlink { path: p, .. } if p.parent() == Some(path) => {
if self.symlink_exists(p) {
entries.insert(p.clone());
}
}
PendingOp::Rename { to, .. } if to.parent() == Some(path) => {
if self.file_exists(to) || self.dir_exists(to) || self.symlink_exists(to) {
entries.insert(to.clone());
}
}
_ => {}
}
}
entries.into_iter().collect()
}
}
impl Default for Fs {
fn default() -> Self {
Self::new(FsConfig::default())
}
}