use super::fs::{
CowStrategyUsed, DirEntryInfo, FileKind, FileStat, StagedFile, StoreDirLockGuard, StoreFile,
StoreFs,
};
use crate::store::StoreError;
use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
#[derive(Default)]
struct MemTree {
files: BTreeMap<PathBuf, Vec<u8>>,
dirs: BTreeSet<PathBuf>,
locks: BTreeSet<PathBuf>,
}
#[derive(Clone, Default)]
pub struct MemFs {
tree: Arc<Mutex<MemTree>>,
}
impl MemFs {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn lock_tree(&self) -> std::sync::MutexGuard<'_, MemTree> {
self.tree.lock().unwrap_or_else(PoisonError::into_inner)
}
fn not_found(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::NotFound,
format!("MemFs: no such file or directory: {}", path.display()),
)
}
fn is_a_directory(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::IsADirectory,
format!("MemFs: is a directory: {}", path.display()),
)
}
fn not_a_directory(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::NotADirectory,
format!("MemFs: not a directory: {}", path.display()),
)
}
fn parent_must_exist(tree: &MemTree, path: &Path) -> io::Result<()> {
match path.parent() {
None => Ok(()),
Some(parent) if parent.as_os_str().is_empty() => Ok(()),
Some(parent) if tree.dirs.contains(parent) => Ok(()),
Some(parent) => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("MemFs: parent directory missing: {}", parent.display()),
)),
}
}
}
struct MemStoreFile {
path: PathBuf,
tree: Arc<Mutex<MemTree>>,
}
impl MemStoreFile {
fn lock_tree(&self) -> std::sync::MutexGuard<'_, MemTree> {
self.tree.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl StoreFile for MemStoreFile {
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
let mut tree = self.lock_tree();
match tree.files.get_mut(&self.path) {
Some(bytes) => {
bytes.extend_from_slice(buf);
Ok(())
}
None => Err(MemFs::not_found(&self.path)),
}
}
fn sync_data(&mut self) -> io::Result<()> {
Ok(())
}
fn sync_all(&mut self) -> io::Result<()> {
Ok(())
}
fn len(&self) -> io::Result<u64> {
let tree = self.lock_tree();
let bytes = tree
.files
.get(&self.path)
.ok_or_else(|| MemFs::not_found(&self.path))?;
Ok(u64::try_from(bytes.len()).unwrap_or(u64::MAX))
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
let tree = self.lock_tree();
let bytes = tree
.files
.get(&self.path)
.ok_or_else(|| MemFs::not_found(&self.path))?;
let Ok(start) = usize::try_from(offset) else {
return Ok(0);
};
if start >= bytes.len() {
return Ok(0);
}
let n = buf.len().min(bytes.len() - start);
buf[..n].copy_from_slice(&bytes[start..start + n]);
Ok(n)
}
fn as_std_file(&self) -> Option<&std::fs::File> {
None
}
}
struct MemStagedFile {
buf: Vec<u8>,
tree: Arc<Mutex<MemTree>>,
}
impl StagedFile for MemStagedFile {
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.buf.extend_from_slice(buf);
Ok(())
}
fn sync_all(&mut self) -> io::Result<()> {
Ok(())
}
fn persist(
self: Box<Self>,
final_path: &Path,
_admission: crate::store::platform::sync::ParentDirSyncAdmission,
) -> io::Result<()> {
let mut tree = self.tree.lock().unwrap_or_else(PoisonError::into_inner);
if tree.dirs.contains(final_path) {
return Err(MemFs::is_a_directory(final_path));
}
MemFs::parent_must_exist(&tree, final_path)?;
tree.files.insert(final_path.to_path_buf(), self.buf);
Ok(())
}
}
struct MemDirLockGuard {
path: PathBuf,
tree: Arc<Mutex<MemTree>>,
}
impl StoreDirLockGuard for MemDirLockGuard {}
impl Drop for MemDirLockGuard {
fn drop(&mut self) {
let mut tree = self.tree.lock().unwrap_or_else(PoisonError::into_inner);
tree.locks.remove(&self.path);
}
}
impl StoreFs for MemFs {
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntryInfo>> {
let tree = self.lock_tree();
if !tree.dirs.contains(path) {
if tree.files.contains_key(path) {
return Err(MemFs::not_a_directory(path));
}
return Err(MemFs::not_found(path));
}
let mut entries = Vec::new();
for file_path in tree.files.keys() {
if file_path.parent() == Some(path) {
if let Some(name) = file_path.file_name() {
entries.push(DirEntryInfo {
name: name.to_os_string(),
kind: FileKind::File,
});
}
}
}
for dir_path in &tree.dirs {
if dir_path.parent() == Some(path) {
if let Some(name) = dir_path.file_name() {
entries.push(DirEntryInfo {
name: name.to_os_string(),
kind: FileKind::Dir,
});
}
}
}
Ok(entries)
}
fn create_dir_all(&self, path: &Path) -> io::Result<()> {
let mut tree = self.lock_tree();
let mut current = PathBuf::new();
for component in path.components() {
current.push(component);
if tree.files.contains_key(¤t) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("MemFs: not a directory: {}", current.display()),
));
}
tree.dirs.insert(current.clone());
}
Ok(())
}
fn create_new_file(&self, path: &Path) -> Result<Box<dyn StoreFile>, StoreError> {
let mut tree = self.lock_tree();
MemFs::parent_must_exist(&tree, path).map_err(StoreError::Io)?;
if tree.files.contains_key(path) || tree.dirs.contains(path) {
return Err(StoreError::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("MemFs: file exists: {}", path.display()),
)));
}
tree.files.insert(path.to_path_buf(), Vec::new());
Ok(Box::new(MemStoreFile {
path: path.to_path_buf(),
tree: Arc::clone(&self.tree),
}))
}
fn open_file(&self, path: &Path) -> io::Result<Box<dyn StoreFile>> {
let tree = self.lock_tree();
if tree.dirs.contains(path) {
return Err(MemFs::is_a_directory(path));
}
if !tree.files.contains_key(path) {
return Err(MemFs::not_found(path));
}
Ok(Box::new(MemStoreFile {
path: path.to_path_buf(),
tree: Arc::clone(&self.tree),
}))
}
fn sync_parent_dir(&self, _path: &Path) -> Result<(), StoreError> {
Ok(())
}
fn reject_symlink_leaf(&self, _path: &Path, _purpose: &str) -> Result<(), StoreError> {
Ok(())
}
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
let tree = self.lock_tree();
if let Some(bytes) = tree.files.get(path) {
return Ok(bytes.clone());
}
if tree.dirs.contains(path) {
return Err(MemFs::is_a_directory(path));
}
Err(MemFs::not_found(path))
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
let tree = self.lock_tree();
if tree.dirs.contains(path) || tree.files.contains_key(path) {
Ok(path.to_path_buf())
} else {
Err(MemFs::not_found(path))
}
}
fn symlink_metadata(&self, path: &Path) -> io::Result<FileStat> {
self.metadata(path)
}
fn cow_copy_file(
&self,
from: &Path,
to: &Path,
_preference: crate::store::CopyPreference,
) -> io::Result<CowStrategyUsed> {
self.copy(from, to)?;
Ok(CowStrategyUsed::DeepCopy)
}
fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
let mut tree = self.lock_tree();
if tree.dirs.contains(to) {
return Err(MemFs::is_a_directory(to));
}
let bytes = match tree.files.get(from) {
Some(bytes) => bytes.clone(),
None if tree.dirs.contains(from) => return Err(MemFs::is_a_directory(from)),
None => return Err(MemFs::not_found(from)),
};
MemFs::parent_must_exist(&tree, to)?;
let len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
tree.files.insert(to.to_path_buf(), bytes);
Ok(len)
}
fn metadata(&self, path: &Path) -> io::Result<FileStat> {
let tree = self.lock_tree();
if let Some(bytes) = tree.files.get(path) {
return Ok(FileStat {
len: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
kind: FileKind::File,
});
}
if tree.dirs.contains(path) {
return Ok(FileStat {
len: 0,
kind: FileKind::Dir,
});
}
Err(MemFs::not_found(path))
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
let mut tree = self.lock_tree();
if tree.dirs.contains(to) {
return Err(MemFs::is_a_directory(to));
}
MemFs::parent_must_exist(&tree, to)?;
let bytes = tree
.files
.remove(from)
.ok_or_else(|| MemFs::not_found(from))?;
tree.files.insert(to.to_path_buf(), bytes);
Ok(())
}
fn remove_file(&self, path: &Path) -> io::Result<()> {
let mut tree = self.lock_tree();
if tree.dirs.contains(path) {
return Err(MemFs::is_a_directory(path));
}
match tree.files.remove(path) {
Some(_) => Ok(()),
None => Err(MemFs::not_found(path)),
}
}
fn remove_dir_all(&self, path: &Path) -> io::Result<()> {
let mut tree = self.lock_tree();
if !tree.dirs.contains(path) {
return Err(MemFs::not_found(path));
}
tree.files
.retain(|file_path, _| !file_path.starts_with(path));
tree.dirs.retain(|dir_path| !dir_path.starts_with(path));
Ok(())
}
fn named_temp_in(&self, dir: &Path) -> io::Result<Box<dyn StagedFile>> {
let tree = self.lock_tree();
if !tree.dirs.contains(dir) {
return Err(MemFs::not_found(dir));
}
Ok(Box::new(MemStagedFile {
buf: Vec::new(),
tree: Arc::clone(&self.tree),
}))
}
fn try_lock_store_dir(
&self,
lock_path: &Path,
) -> Result<Option<Box<dyn StoreDirLockGuard>>, StoreError> {
let mut tree = self.lock_tree();
if tree.locks.contains(lock_path) {
return Ok(None);
}
tree.locks.insert(lock_path.to_path_buf());
Ok(Some(Box::new(MemDirLockGuard {
path: lock_path.to_path_buf(),
tree: Arc::clone(&self.tree),
})))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::platform::sync::admit_current_parent_dir_sync;
#[test]
fn persist_over_a_directory_fails_closed_preserving_the_collision_invariant() {
let fs = MemFs::new();
let root = Path::new("/virtual/publish");
fs.create_dir_all(root).expect("seed root");
let occupied = root.join("visibility_ranges.fbv");
fs.create_dir_all(&occupied)
.expect("seed occupying directory");
let mut staged = fs.named_temp_in(root).expect("stage temp");
staged.write_all(b"fresh-metadata").expect("write staged");
staged.sync_all().expect("sync staged");
let admission = admit_current_parent_dir_sync().expect("mint parent-dir-sync admission");
let published = staged.persist(&occupied, admission);
assert!(
matches!(&published, Err(error) if error.kind() == io::ErrorKind::IsADirectory),
"publishing onto a directory must fail closed with IsADirectory, got {published:?}"
);
assert!(
matches!(fs.read(&occupied), Err(error) if error.kind() == io::ErrorKind::IsADirectory),
"the path must stay a directory; no colliding file may be inserted"
);
}
#[test]
fn persist_to_a_fresh_path_publishes_atomically() {
let fs = MemFs::new();
let root = Path::new("/virtual/publish-ok");
fs.create_dir_all(root).expect("seed root");
let mut staged = fs.named_temp_in(root).expect("stage temp");
staged.write_all(b"published-bytes").expect("write staged");
staged.sync_all().expect("sync staged");
let admission = admit_current_parent_dir_sync().expect("mint parent-dir-sync admission");
let final_path = root.join("segment.fbat");
staged
.persist(&final_path, admission)
.expect("publish to a fresh path");
assert_eq!(
fs.read(&final_path).expect("read published bytes"),
b"published-bytes",
"a fresh publish lands the staged bytes atomically"
);
}
#[test]
fn persist_into_a_missing_parent_directory_fails_not_found() {
let fs = MemFs::new();
let root = Path::new("/virtual/publish-parent");
fs.create_dir_all(root).expect("seed root");
let mut staged = fs.named_temp_in(root).expect("stage temp");
staged.write_all(b"orphan").expect("write staged");
staged.sync_all().expect("sync staged");
let admission = admit_current_parent_dir_sync().expect("mint parent-dir-sync admission");
let orphan = Path::new("/virtual/missing/file");
let published = staged.persist(orphan, admission);
assert!(
matches!(&published, Err(error) if error.kind() == io::ErrorKind::NotFound),
"publishing into a missing parent must fail NotFound, got {published:?}"
);
assert!(
matches!(fs.read(orphan), Err(error) if error.kind() == io::ErrorKind::NotFound),
"no unreachable file may be inserted for a missing-parent publish"
);
}
}