#![allow(clippy::unwrap_used)]
use async_trait::async_trait;
use std::collections::HashMap;
use std::io::{Error as IoError, ErrorKind};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use super::limits::{FsLimits, FsUsage};
use super::traits::{DirEntry, FileSystem, FileSystemExt, FileType, Metadata};
use crate::error::Result;
#[cfg(feature = "failpoints")]
use fail::fail_point;
pub struct InMemoryFs {
entries: RwLock<HashMap<PathBuf, FsEntry>>,
limits: FsLimits,
}
pub type LazyLoader = Arc<dyn Fn() -> Vec<u8> + Send + Sync>;
enum FsEntry {
File {
content: Vec<u8>,
metadata: Metadata,
},
LazyFile {
loader: LazyLoader,
metadata: Metadata,
},
Directory {
metadata: Metadata,
},
Symlink {
target: PathBuf,
metadata: Metadata,
},
Fifo {
content: Vec<u8>,
metadata: Metadata,
},
}
impl Clone for FsEntry {
fn clone(&self) -> Self {
match self {
Self::File { content, metadata } => Self::File {
content: content.clone(),
metadata: metadata.clone(),
},
Self::LazyFile { loader, metadata } => Self::LazyFile {
loader: Arc::clone(loader),
metadata: metadata.clone(),
},
Self::Directory { metadata } => Self::Directory {
metadata: metadata.clone(),
},
Self::Symlink { target, metadata } => Self::Symlink {
target: target.clone(),
metadata: metadata.clone(),
},
Self::Fifo { content, metadata } => Self::Fifo {
content: content.clone(),
metadata: metadata.clone(),
},
}
}
}
impl std::fmt::Debug for FsEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::File { content, metadata } => f
.debug_struct("File")
.field("content_len", &content.len())
.field("metadata", metadata)
.finish(),
Self::LazyFile { metadata, .. } => f
.debug_struct("LazyFile")
.field("metadata", metadata)
.finish(),
Self::Directory { metadata } => f
.debug_struct("Directory")
.field("metadata", metadata)
.finish(),
Self::Symlink { target, metadata } => f
.debug_struct("Symlink")
.field("target", target)
.field("metadata", metadata)
.finish(),
Self::Fifo { content, metadata } => f
.debug_struct("Fifo")
.field("content_len", &content.len())
.field("metadata", metadata)
.finish(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VfsSnapshot {
entries: Vec<VfsEntry>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct VfsEntry {
path: PathBuf,
kind: VfsEntryKind,
mode: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
enum VfsEntryKind {
File { content: Vec<u8> },
Directory,
Symlink { target: PathBuf },
Fifo,
}
impl Default for InMemoryFs {
fn default() -> Self {
Self::new()
}
}
impl InMemoryFs {
pub fn new() -> Self {
Self::with_limits(FsLimits::default())
}
pub fn with_limits(limits: FsLimits) -> Self {
let mut entries = HashMap::new();
entries.insert(
PathBuf::from("/"),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
for dir in &["/tmp", "/home", "/home/user", "/dev"] {
entries.insert(
PathBuf::from(dir),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
entries.insert(
PathBuf::from("/dev/null"),
FsEntry::File {
content: Vec::new(),
metadata: Metadata {
file_type: FileType::File,
size: 0,
mode: 0o666,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
for dev in &["/dev/urandom", "/dev/random"] {
entries.insert(
PathBuf::from(dev),
FsEntry::File {
content: Vec::new(),
metadata: Metadata {
file_type: FileType::File,
size: 0,
mode: 0o666,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
entries.insert(
PathBuf::from("/dev/fd"),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
Self {
entries: RwLock::new(entries),
limits,
}
}
fn generate_random_bytes() -> Vec<u8> {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
const SIZE: usize = 8192;
let mut buf = Vec::with_capacity(SIZE);
while buf.len() < SIZE {
let h = RandomState::new().build_hasher().finish();
buf.extend_from_slice(&h.to_ne_bytes());
}
buf.truncate(SIZE);
buf
}
fn compute_usage(&self) -> FsUsage {
let entries = self.entries.read().unwrap();
let mut total_bytes = 0u64;
let mut file_count = 0u64;
let mut dir_count = 0u64;
for entry in entries.values() {
match entry {
FsEntry::File { content, .. } | FsEntry::Fifo { content, .. } => {
total_bytes += content.len() as u64;
file_count += 1;
}
FsEntry::Directory { .. } => {
dir_count += 1;
}
FsEntry::LazyFile { metadata, .. } => {
total_bytes += metadata.size;
file_count += 1;
}
FsEntry::Symlink { .. } => {
file_count += 1;
}
}
}
FsUsage::new(total_bytes, file_count, dir_count)
}
fn check_write_limits(
&self,
entries: &HashMap<PathBuf, FsEntry>,
path: &Path,
new_size: usize,
) -> Result<()> {
self.limits
.check_file_size(new_size as u64)
.map_err(|e| IoError::other(e.to_string()))?;
let mut current_total = 0u64;
let mut current_file_count = 0u64;
let mut old_file_size = 0u64;
let mut is_new_file = true;
for (entry_path, entry) in entries.iter() {
match entry {
FsEntry::File { content, .. } | FsEntry::Fifo { content, .. } => {
let size = content.len() as u64;
current_total += size;
current_file_count += 1;
if entry_path == path {
old_file_size = size;
is_new_file = false;
}
}
FsEntry::LazyFile { metadata, .. } => {
current_total += metadata.size;
current_file_count += 1;
if entry_path == path {
old_file_size = metadata.size;
is_new_file = false;
}
}
_ => {}
}
}
if is_new_file {
self.limits
.check_file_count(current_file_count)
.map_err(|e| IoError::other(e.to_string()))?;
}
let new_total = current_total - old_file_size + new_size as u64;
if new_total > self.limits.max_total_bytes {
return Err(IoError::other(format!(
"filesystem full: {} bytes would exceed {} byte limit",
new_total, self.limits.max_total_bytes
))
.into());
}
Ok(())
}
pub fn snapshot(&self) -> VfsSnapshot {
let mut entries = self.entries.write().unwrap();
let lazy_paths: Vec<PathBuf> = entries
.iter()
.filter(|(_, e)| matches!(e, FsEntry::LazyFile { .. }))
.map(|(p, _)| p.clone())
.collect();
for path in lazy_paths {
if let Some(FsEntry::LazyFile { loader, metadata }) = entries.remove(&path) {
let content = loader();
let mut metadata = metadata;
metadata.size = content.len() as u64;
entries.insert(path, FsEntry::File { content, metadata });
}
}
let mut files = Vec::new();
for (path, entry) in entries.iter() {
match entry {
FsEntry::File { content, metadata } => {
files.push(VfsEntry {
path: path.clone(),
kind: VfsEntryKind::File {
content: content.clone(),
},
mode: metadata.mode,
});
}
FsEntry::LazyFile { .. } => {
unreachable!()
}
FsEntry::Directory { metadata } => {
files.push(VfsEntry {
path: path.clone(),
kind: VfsEntryKind::Directory,
mode: metadata.mode,
});
}
FsEntry::Symlink {
target, metadata, ..
} => {
files.push(VfsEntry {
path: path.clone(),
kind: VfsEntryKind::Symlink {
target: target.clone(),
},
mode: metadata.mode,
});
}
FsEntry::Fifo { metadata, .. } => {
files.push(VfsEntry {
path: path.clone(),
kind: VfsEntryKind::Fifo,
mode: metadata.mode,
});
}
}
}
VfsSnapshot { entries: files }
}
pub fn restore(&self, snapshot: &VfsSnapshot) {
let mut total_bytes = 0u64;
let mut file_count = 0u64;
for entry in &snapshot.entries {
if self.limits.validate_path(&entry.path).is_err() {
return;
}
if let VfsEntryKind::File { content } = &entry.kind {
if self.limits.check_file_size(content.len() as u64).is_err() {
return;
}
total_bytes += content.len() as u64;
file_count += 1;
}
}
if total_bytes > self.limits.max_total_bytes {
return;
}
if self.limits.check_file_count(file_count).is_err() {
return;
}
let mut entries = self.entries.write().unwrap();
entries.clear();
let now = SystemTime::now();
for entry in &snapshot.entries {
match &entry.kind {
VfsEntryKind::File { content } => {
entries.insert(
entry.path.clone(),
FsEntry::File {
content: content.clone(),
metadata: Metadata {
file_type: FileType::File,
size: content.len() as u64,
mode: entry.mode,
modified: now,
created: now,
},
},
);
}
VfsEntryKind::Directory => {
entries.insert(
entry.path.clone(),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: entry.mode,
modified: now,
created: now,
},
},
);
}
VfsEntryKind::Symlink { target } => {
entries.insert(
entry.path.clone(),
FsEntry::Symlink {
target: target.clone(),
metadata: Metadata {
file_type: FileType::Symlink,
size: 0,
mode: entry.mode,
modified: now,
created: now,
},
},
);
}
VfsEntryKind::Fifo => {
entries.insert(
entry.path.clone(),
FsEntry::Fifo {
content: Vec::new(),
metadata: Metadata {
file_type: FileType::Fifo,
size: 0,
mode: entry.mode,
modified: now,
created: now,
},
},
);
}
}
}
}
fn normalize_path(path: &Path) -> PathBuf {
super::normalize_path(path)
}
pub fn add_file(&self, path: impl AsRef<Path>, content: impl AsRef<[u8]>, mode: u32) {
let path = Self::normalize_path(path.as_ref());
let content = content.as_ref();
if self.limits.validate_path(&path).is_err() {
return;
}
let mut entries = self.entries.write().unwrap();
if self
.check_write_limits(&entries, &path, content.len())
.is_err()
{
return;
}
if let Some(parent) = path.parent() {
let mut current = PathBuf::from("/");
for component in parent.components().skip(1) {
current.push(component);
if !entries.contains_key(¤t) {
entries.insert(
current.clone(),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
}
}
entries.insert(
path,
FsEntry::File {
content: content.to_vec(),
metadata: Metadata {
file_type: FileType::File,
size: content.len() as u64,
mode,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
pub fn add_lazy_file(
&self,
path: impl AsRef<Path>,
size_hint: u64,
mode: u32,
loader: LazyLoader,
) {
let path = Self::normalize_path(path.as_ref());
if self.limits.validate_path(&path).is_err() {
return;
}
let mut entries = self.entries.write().unwrap();
let Ok(size_hint_usize) = usize::try_from(size_hint) else {
return;
};
if self
.check_write_limits(&entries, &path, size_hint_usize)
.is_err()
{
return;
}
if let Some(parent) = path.parent() {
let mut current = PathBuf::from("/");
for component in parent.components().skip(1) {
current.push(component);
if !entries.contains_key(¤t) {
entries.insert(
current.clone(),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
}
}
entries.insert(
path,
FsEntry::LazyFile {
loader,
metadata: Metadata {
file_type: FileType::File,
size: size_hint,
mode,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
}
#[async_trait]
impl FileSystem for InMemoryFs {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
#[cfg(feature = "failpoints")]
fail_point!("fs::read_file", |action| {
match action.as_deref() {
Some("io_error") => {
return Err(IoError::other("injected I/O error").into());
}
Some("permission_denied") => {
return Err(
IoError::new(ErrorKind::PermissionDenied, "permission denied").into(),
);
}
Some("corrupt_data") => {
return Ok(vec![0xFF, 0xFE, 0x00, 0x01]);
}
_ => {}
}
Err(IoError::other("fail point triggered").into())
});
let path = Self::normalize_path(path);
if path == Path::new("/dev/urandom") || path == Path::new("/dev/random") {
return Ok(Self::generate_random_bytes());
}
{
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(FsEntry::File { content, .. }) | Some(FsEntry::Fifo { content, .. }) => {
return Ok(content.clone());
}
Some(FsEntry::Directory { .. }) => {
return Err(IoError::other("is a directory").into());
}
Some(FsEntry::Symlink { .. }) => {
return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
}
Some(FsEntry::LazyFile { .. }) => {
}
None => {
return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
}
}
}
let mut entries = self.entries.write().unwrap();
match entries.get(&path) {
Some(FsEntry::LazyFile { .. }) => {
if let Some(FsEntry::LazyFile { loader, metadata }) = entries.remove(&path) {
let content = loader();
if let Err(err) = self.check_write_limits(&entries, &path, content.len()) {
entries.insert(path, FsEntry::LazyFile { loader, metadata });
return Err(err);
}
let mut metadata = metadata;
metadata.size = content.len() as u64;
let result = content.clone();
entries.insert(path, FsEntry::File { content, metadata });
return Ok(result);
}
unreachable!()
}
Some(FsEntry::File { content, .. }) | Some(FsEntry::Fifo { content, .. }) => {
return Ok(content.clone());
}
Some(FsEntry::Directory { .. }) => {
return Err(IoError::other("is a directory").into());
}
Some(FsEntry::Symlink { .. }) => {
Err(IoError::new(ErrorKind::NotFound, "file not found").into())
}
None => Err(IoError::new(ErrorKind::NotFound, "file not found").into()),
}
}
async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
#[cfg(feature = "failpoints")]
fail_point!("fs::write_file", |action| {
match action.as_deref() {
Some("io_error") => {
return Err(IoError::other("injected I/O error").into());
}
Some("disk_full") => {
return Err(IoError::other("no space left on device").into());
}
Some("permission_denied") => {
return Err(
IoError::new(ErrorKind::PermissionDenied, "permission denied").into(),
);
}
Some("partial_write") => {
return Err(IoError::new(ErrorKind::Interrupted, "partial write").into());
}
_ => {}
}
Err(IoError::other("fail point triggered").into())
});
let path = Self::normalize_path(path);
if path == Path::new("/dev/null") {
return Ok(());
}
let mut entries = self.entries.write().unwrap();
if let Some(parent) = path.parent()
&& !entries.contains_key(parent)
&& parent != Path::new("/")
{
return Err(IoError::new(ErrorKind::NotFound, "parent directory not found").into());
}
if let Some(FsEntry::Directory { .. }) = entries.get(&path) {
return Err(IoError::other("is a directory").into());
}
self.check_write_limits(&entries, &path, content.len())?;
let is_fifo = matches!(entries.get(&path), Some(FsEntry::Fifo { .. }));
if is_fifo {
entries.insert(
path,
FsEntry::Fifo {
content: content.to_vec(),
metadata: Metadata {
file_type: FileType::Fifo,
size: content.len() as u64,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
} else {
entries.insert(
path,
FsEntry::File {
content: content.to_vec(),
metadata: Metadata {
file_type: FileType::File,
size: content.len() as u64,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
Ok(())
}
async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
if path == Path::new("/dev/null") {
return Ok(());
}
let mut entries = self.entries.write().unwrap();
match entries.get(&path) {
Some(FsEntry::Directory { .. }) => {
return Err(IoError::other("is a directory").into());
}
Some(FsEntry::Symlink { .. }) => {
return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
}
None => {
self.check_write_limits(&entries, &path, content.len())?;
if let Some(parent) = path.parent()
&& !entries.contains_key(parent)
&& parent != Path::new("/")
{
return Err(
IoError::new(ErrorKind::NotFound, "parent directory not found").into(),
);
}
entries.insert(
path,
FsEntry::File {
content: content.to_vec(),
metadata: Metadata {
file_type: FileType::File,
size: content.len() as u64,
mode: 0o644,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
return Ok(());
}
Some(FsEntry::LazyFile { .. }) => {
if let Some(FsEntry::LazyFile { loader, metadata }) = entries.remove(&path) {
let loaded = loader();
let mut metadata = metadata;
metadata.size = loaded.len() as u64;
entries.insert(
path.clone(),
FsEntry::File {
content: loaded,
metadata,
},
);
}
}
Some(FsEntry::File { .. } | FsEntry::Fifo { .. }) => {
}
}
let current_file_size = match entries.get(&path) {
Some(FsEntry::File {
content: existing, ..
})
| Some(FsEntry::Fifo {
content: existing, ..
}) => existing.len(),
_ => 0,
};
let new_file_size = current_file_size + content.len();
self.limits
.check_file_size(new_file_size as u64)
.map_err(|e| IoError::other(e.to_string()))?;
let mut current_total = 0u64;
for entry in entries.values() {
match entry {
FsEntry::File {
content: file_content,
..
}
| FsEntry::Fifo {
content: file_content,
..
} => {
current_total += file_content.len() as u64;
}
_ => {}
}
}
let new_total = current_total + content.len() as u64;
if new_total > self.limits.max_total_bytes {
return Err(IoError::other(format!(
"filesystem full: {} bytes would exceed {} byte limit",
new_total, self.limits.max_total_bytes
))
.into());
}
if let Some(
FsEntry::File {
content: existing,
metadata,
}
| FsEntry::Fifo {
content: existing,
metadata,
},
) = entries.get_mut(&path)
{
existing.extend_from_slice(content);
metadata.size = existing.len() as u64;
metadata.modified = SystemTime::now();
}
Ok(())
}
async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let mut entries = self.entries.write().unwrap();
if recursive {
let mut current = PathBuf::from("/");
for component in path.components().skip(1) {
current.push(component);
match entries.get(¤t) {
Some(FsEntry::Directory { .. }) => {
}
Some(
FsEntry::File { .. }
| FsEntry::LazyFile { .. }
| FsEntry::Symlink { .. }
| FsEntry::Fifo { .. },
) => {
return Err(IoError::new(ErrorKind::AlreadyExists, "file exists").into());
}
None => {
let dir_count = entries
.values()
.filter(|e| matches!(e, FsEntry::Directory { .. }))
.count() as u64;
self.limits
.check_dir_count(dir_count)
.map_err(|e| IoError::other(e.to_string()))?;
entries.insert(
current.clone(),
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
}
}
} else {
if let Some(parent) = path.parent()
&& !entries.contains_key(parent)
&& parent != Path::new("/")
{
return Err(IoError::new(ErrorKind::NotFound, "parent directory not found").into());
}
if entries.contains_key(&path) {
return Err(IoError::new(ErrorKind::AlreadyExists, "directory exists").into());
}
let dir_count = entries
.values()
.filter(|e| matches!(e, FsEntry::Directory { .. }))
.count() as u64;
self.limits
.check_dir_count(dir_count)
.map_err(|e| IoError::other(e.to_string()))?;
entries.insert(
path,
FsEntry::Directory {
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
}
Ok(())
}
async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let mut entries = self.entries.write().unwrap();
match entries.get(&path) {
Some(FsEntry::Directory { .. }) => {
if recursive {
let to_remove: Vec<PathBuf> = entries
.keys()
.filter(|p| p.starts_with(&path))
.cloned()
.collect();
for p in to_remove {
entries.remove(&p);
}
} else {
let has_children = entries
.keys()
.any(|p| p != &path && p.parent() == Some(&path));
if has_children {
return Err(IoError::other("directory not empty").into());
}
entries.remove(&path);
}
}
Some(
FsEntry::File { .. }
| FsEntry::LazyFile { .. }
| FsEntry::Symlink { .. }
| FsEntry::Fifo { .. },
) => {
entries.remove(&path);
}
None => {
return Err(IoError::new(ErrorKind::NotFound, "not found").into());
}
}
Ok(())
}
async fn stat(&self, path: &Path) -> Result<Metadata> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(FsEntry::File { metadata, .. })
| Some(FsEntry::LazyFile { metadata, .. })
| Some(FsEntry::Directory { metadata })
| Some(FsEntry::Symlink { metadata, .. })
| Some(FsEntry::Fifo { metadata, .. }) => Ok(metadata.clone()),
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(FsEntry::Directory { .. }) => {
let mut result = Vec::new();
for (entry_path, entry) in entries.iter() {
if entry_path.parent() == Some(&path) && entry_path != &path {
let name = entry_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let metadata = match entry {
FsEntry::File { metadata, .. }
| FsEntry::LazyFile { metadata, .. }
| FsEntry::Directory { metadata }
| FsEntry::Symlink { metadata, .. }
| FsEntry::Fifo { metadata, .. } => metadata.clone(),
};
result.push(DirEntry { name, metadata });
}
}
Ok(result)
}
Some(_) => Err(IoError::other("not a directory").into()),
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
async fn exists(&self, path: &Path) -> Result<bool> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let entries = self.entries.read().unwrap();
Ok(entries.contains_key(&path))
}
async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
self.limits
.validate_path(from)
.map_err(|e| IoError::other(e.to_string()))?;
self.limits
.validate_path(to)
.map_err(|e| IoError::other(e.to_string()))?;
let from = Self::normalize_path(from);
let to = Self::normalize_path(to);
let mut entries = self.entries.write().unwrap();
let entry = entries
.remove(&from)
.ok_or_else(|| IoError::new(ErrorKind::NotFound, "not found"))?;
if matches!(
&entry,
FsEntry::File { .. } | FsEntry::Symlink { .. } | FsEntry::Fifo { .. }
) && matches!(entries.get(&to), Some(FsEntry::Directory { .. }))
{
entries.insert(from, entry);
return Err(IoError::other("cannot rename file over directory").into());
}
entries.insert(to, entry);
Ok(())
}
async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
self.limits
.validate_path(from)
.map_err(|e| IoError::other(e.to_string()))?;
self.limits
.validate_path(to)
.map_err(|e| IoError::other(e.to_string()))?;
let from = Self::normalize_path(from);
let to = Self::normalize_path(to);
let mut entries = self.entries.write().unwrap();
let entry = entries
.get(&from)
.cloned()
.ok_or_else(|| IoError::new(ErrorKind::NotFound, "not found"))?;
let entry_size = match &entry {
FsEntry::File { content, .. } | FsEntry::Fifo { content, .. } => content.len() as u64,
_ => 0,
};
self.check_write_limits(&entries, &to, entry_size as usize)?;
entries.insert(to, entry);
Ok(())
}
async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
self.limits
.validate_path(link)
.map_err(|e| IoError::other(e.to_string()))?;
let link = Self::normalize_path(link);
let mut entries = self.entries.write().unwrap();
let is_new = !entries.contains_key(&link);
if is_new {
let file_count = entries
.values()
.filter(|e| {
matches!(
e,
FsEntry::File { .. }
| FsEntry::LazyFile { .. }
| FsEntry::Fifo { .. }
| FsEntry::Symlink { .. }
)
})
.count() as u64;
self.limits
.check_file_count(file_count)
.map_err(|e| IoError::other(e.to_string()))?;
}
entries.insert(
link,
FsEntry::Symlink {
target: target.to_path_buf(),
metadata: Metadata {
file_type: FileType::Symlink,
size: 0,
mode: 0o777,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
Ok(())
}
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let entries = self.entries.read().unwrap();
match entries.get(&path) {
Some(FsEntry::Symlink { target, .. }) => Ok(target.clone()),
Some(_) => Err(IoError::other("not a symlink").into()),
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let mut entries = self.entries.write().unwrap();
match entries.get_mut(&path) {
Some(FsEntry::File { metadata, .. })
| Some(FsEntry::LazyFile { metadata, .. })
| Some(FsEntry::Directory { metadata })
| Some(FsEntry::Symlink { metadata, .. })
| Some(FsEntry::Fifo { metadata, .. }) => {
metadata.mode = mode;
Ok(())
}
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
async fn set_modified_time(&self, path: &Path, time: SystemTime) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let mut entries = self.entries.write().unwrap();
match entries.get_mut(&path) {
Some(FsEntry::File { metadata, .. })
| Some(FsEntry::LazyFile { metadata, .. })
| Some(FsEntry::Directory { metadata })
| Some(FsEntry::Symlink { metadata, .. })
| Some(FsEntry::Fifo { metadata, .. }) => {
metadata.modified = time;
Ok(())
}
None => Err(IoError::new(ErrorKind::NotFound, "not found").into()),
}
}
}
#[async_trait]
impl FileSystemExt for InMemoryFs {
async fn mkfifo(&self, path: &Path, mode: u32) -> Result<()> {
self.limits
.validate_path(path)
.map_err(|e| IoError::other(e.to_string()))?;
let path = Self::normalize_path(path);
let mut entries = self.entries.write().unwrap();
if let Some(parent) = path.parent()
&& !entries.contains_key(parent)
&& parent != Path::new("/")
{
return Err(IoError::new(ErrorKind::NotFound, "parent directory not found").into());
}
if entries.contains_key(&path) {
return Err(IoError::new(ErrorKind::AlreadyExists, "file exists").into());
}
let file_count = entries
.values()
.filter(|e| {
matches!(
e,
FsEntry::File { .. }
| FsEntry::LazyFile { .. }
| FsEntry::Fifo { .. }
| FsEntry::Symlink { .. }
)
})
.count() as u64;
self.limits
.check_file_count(file_count)
.map_err(|e| IoError::other(e.to_string()))?;
entries.insert(
path,
FsEntry::Fifo {
content: Vec::new(),
metadata: Metadata {
file_type: FileType::Fifo,
size: 0,
mode,
modified: SystemTime::now(),
created: SystemTime::now(),
},
},
);
Ok(())
}
fn usage(&self) -> FsUsage {
self.compute_usage()
}
fn limits(&self) -> FsLimits {
self.limits.clone()
}
fn vfs_snapshot(&self) -> Option<VfsSnapshot> {
Some(self.snapshot())
}
fn vfs_restore(&self, snapshot: &VfsSnapshot) -> bool {
self.restore(snapshot);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_write_and_read_file() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/test.txt"), b"hello world")
.await
.unwrap();
let content = fs.read_file(Path::new("/tmp/test.txt")).await.unwrap();
assert_eq!(content, b"hello world");
}
#[tokio::test]
async fn test_mkdir_and_read_dir() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/mydir"), false).await.unwrap();
fs.write_file(Path::new("/tmp/mydir/file.txt"), b"test")
.await
.unwrap();
let entries = fs.read_dir(Path::new("/tmp/mydir")).await.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "file.txt");
}
#[tokio::test]
async fn test_exists() {
let fs = InMemoryFs::new();
assert!(fs.exists(Path::new("/tmp")).await.unwrap());
assert!(!fs.exists(Path::new("/tmp/nonexistent")).await.unwrap());
}
#[tokio::test]
async fn test_add_file_basic() {
let fs = InMemoryFs::new();
fs.add_file("/tmp/added.txt", "hello from add_file", 0o644);
let content = fs.read_file(Path::new("/tmp/added.txt")).await.unwrap();
assert_eq!(content, b"hello from add_file");
}
#[tokio::test]
async fn test_add_file_with_mode() {
let fs = InMemoryFs::new();
fs.add_file("/etc/readonly.conf", "secret", 0o444);
let stat = fs.stat(Path::new("/etc/readonly.conf")).await.unwrap();
assert_eq!(stat.mode, 0o444);
}
#[tokio::test]
async fn test_add_file_creates_parent_directories() {
let fs = InMemoryFs::new();
fs.add_file("/a/b/c/d/nested.txt", "deep content", 0o644);
assert!(fs.exists(Path::new("/a/b/c/d/nested.txt")).await.unwrap());
assert!(fs.exists(Path::new("/a")).await.unwrap());
assert!(fs.exists(Path::new("/a/b")).await.unwrap());
assert!(fs.exists(Path::new("/a/b/c")).await.unwrap());
assert!(fs.exists(Path::new("/a/b/c/d")).await.unwrap());
let content = fs
.read_file(Path::new("/a/b/c/d/nested.txt"))
.await
.unwrap();
assert_eq!(content, b"deep content");
}
#[tokio::test]
async fn test_add_file_binary() {
let fs = InMemoryFs::new();
let binary_data = vec![0x00, 0xFF, 0x89, 0x50, 0x4E, 0x47];
fs.add_file("/data/binary.bin", &binary_data, 0o644);
let content = fs.read_file(Path::new("/data/binary.bin")).await.unwrap();
assert_eq!(content, binary_data);
}
#[tokio::test]
async fn test_file_size_limit() {
let limits = FsLimits::new().max_file_size(100);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/small.txt"), &[0u8; 50])
.await
.unwrap();
fs.write_file(Path::new("/tmp/exact.txt"), &[0u8; 100])
.await
.unwrap();
let result = fs
.write_file(Path::new("/tmp/large.txt"), &[0u8; 101])
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("file too large") || err.contains("exceeds"));
}
#[tokio::test]
async fn test_total_bytes_limit() {
let limits = FsLimits::new().max_total_bytes(200);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/file1.txt"), &[0u8; 100])
.await
.unwrap();
fs.write_file(Path::new("/tmp/file2.txt"), &[0u8; 50])
.await
.unwrap();
let result = fs
.write_file(Path::new("/tmp/file3.txt"), &[0u8; 100])
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("filesystem full") || err.contains("exceeds"));
}
#[tokio::test]
async fn test_file_count_limit() {
let limits = FsLimits::new().max_file_count(6); let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/file1.txt"), b"1")
.await
.unwrap();
fs.write_file(Path::new("/tmp/file2.txt"), b"2")
.await
.unwrap();
fs.write_file(Path::new("/tmp/file3.txt"), b"3")
.await
.unwrap();
let result = fs.write_file(Path::new("/tmp/file4.txt"), b"4").await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too many files") || err.contains("limit"));
}
#[tokio::test]
async fn test_overwrite_does_not_increase_count() {
let limits = FsLimits::new().max_file_count(5); let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/file1.txt"), b"original")
.await
.unwrap();
fs.write_file(Path::new("/tmp/file2.txt"), b"original")
.await
.unwrap();
fs.write_file(Path::new("/tmp/file1.txt"), b"updated")
.await
.unwrap();
let result = fs.write_file(Path::new("/tmp/file3.txt"), b"new").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_append_respects_limits() {
let limits = FsLimits::new().max_file_size(100);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/append.txt"), &[0u8; 50])
.await
.unwrap();
fs.append_file(Path::new("/tmp/append.txt"), &[0u8; 30])
.await
.unwrap();
let result = fs
.append_file(Path::new("/tmp/append.txt"), &[0u8; 50])
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_usage_tracking() {
let fs = InMemoryFs::new();
let usage = fs.usage();
assert_eq!(usage.total_bytes, 0); assert_eq!(usage.file_count, 3);
fs.write_file(Path::new("/tmp/test.txt"), b"hello")
.await
.unwrap();
let usage = fs.usage();
assert_eq!(usage.total_bytes, 5);
assert_eq!(usage.file_count, 4); }
#[tokio::test]
async fn test_limits_method() {
let limits = FsLimits::new()
.max_total_bytes(1000)
.max_file_size(500)
.max_file_count(10);
let fs = InMemoryFs::with_limits(limits.clone());
let returned = fs.limits();
assert_eq!(returned.max_total_bytes, 1000);
assert_eq!(returned.max_file_size, 500);
assert_eq!(returned.max_file_count, 10);
}
#[tokio::test]
async fn test_unlimited_fs() {
let fs = InMemoryFs::with_limits(FsLimits::unlimited());
fs.write_file(Path::new("/tmp/large.txt"), &[0u8; 10_000_000])
.await
.unwrap();
let limits = fs.limits();
assert_eq!(limits.max_total_bytes, u64::MAX);
}
#[tokio::test]
async fn test_delete_frees_space() {
let limits = FsLimits::new().max_total_bytes(100);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/file.txt"), &[0u8; 80])
.await
.unwrap();
let result = fs.write_file(Path::new("/tmp/more.txt"), &[0u8; 80]).await;
assert!(result.is_err());
fs.remove(Path::new("/tmp/file.txt"), false).await.unwrap();
fs.write_file(Path::new("/tmp/more.txt"), &[0u8; 80])
.await
.unwrap();
}
#[tokio::test]
async fn test_write_file_to_directory_fails() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/mydir"), false).await.unwrap();
let result = fs.write_file(Path::new("/tmp/mydir"), b"content").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("directory"),
"Error should mention directory: {}",
err
);
}
#[tokio::test]
async fn test_append_file_to_directory_fails() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/appenddir"), false).await.unwrap();
let result = fs
.append_file(Path::new("/tmp/appenddir"), b"content")
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("directory"),
"Error should mention directory: {}",
err
);
}
#[tokio::test]
async fn test_mkdir_on_existing_file_fails() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/myfile"), b"content")
.await
.unwrap();
let result = fs.mkdir(Path::new("/tmp/myfile"), false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mkdir_recursive_on_existing_file_fails() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/myfile"), b"content")
.await
.unwrap();
let result = fs.mkdir(Path::new("/tmp/myfile"), true).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mkdir_on_existing_directory_fails() {
let fs = InMemoryFs::new();
let result = fs.mkdir(Path::new("/tmp"), false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mkdir_recursive_on_existing_directory_succeeds() {
let fs = InMemoryFs::new();
let result = fs.mkdir(Path::new("/tmp"), true).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_write_file_overwrites_existing_file() {
let fs = InMemoryFs::new();
fs.write_file(Path::new("/tmp/file.txt"), b"original")
.await
.unwrap();
fs.write_file(Path::new("/tmp/file.txt"), b"updated")
.await
.unwrap();
let content = fs.read_file(Path::new("/tmp/file.txt")).await.unwrap();
assert_eq!(content, b"updated");
}
#[tokio::test]
async fn test_add_file_respects_file_size_limit() {
let limits = FsLimits {
max_file_size: 100,
..FsLimits::default()
};
let fs = InMemoryFs::with_limits(limits);
fs.add_file("/tmp/huge.bin", vec![0u8; 200], 0o644);
assert!(!fs.exists(Path::new("/tmp/huge.bin")).await.unwrap());
}
#[tokio::test]
async fn test_add_file_respects_total_bytes_limit() {
let limits = FsLimits {
max_total_bytes: 50,
..FsLimits::default()
};
let fs = InMemoryFs::with_limits(limits);
fs.add_file("/tmp/big.bin", vec![0u8; 60], 0o644);
assert!(!fs.exists(Path::new("/tmp/big.bin")).await.unwrap());
}
#[tokio::test]
async fn test_restore_respects_file_size_limit() {
let unlimited = InMemoryFs::with_limits(FsLimits::unlimited());
unlimited.add_file("/tmp/huge.bin", vec![0u8; 200], 0o644);
let snapshot = unlimited.snapshot();
let limited = InMemoryFs::with_limits(FsLimits {
max_file_size: 100,
..FsLimits::default()
});
limited.restore(&snapshot);
assert!(!limited.exists(Path::new("/tmp/huge.bin")).await.unwrap());
}
#[tokio::test]
async fn test_append_file_no_toctou_race() {
use std::sync::Arc;
let limits = FsLimits::new().max_file_size(100);
let fs = Arc::new(InMemoryFs::with_limits(limits));
fs.write_file(Path::new("/tmp/race.txt"), &[b'A'; 80])
.await
.unwrap();
let mut handles = vec![];
for _ in 0..10 {
let fs_clone = fs.clone();
handles.push(tokio::spawn(async move {
fs_clone
.append_file(Path::new("/tmp/race.txt"), &[b'B'; 25])
.await
}));
}
let mut success_count = 0;
for handle in handles {
if handle.await.unwrap().is_ok() {
success_count += 1;
}
}
assert_eq!(
success_count, 0,
"no appends should succeed: 80+25=105 exceeds 100 byte file limit"
);
let content = fs.read_file(Path::new("/tmp/race.txt")).await.unwrap();
assert_eq!(content.len(), 80);
}
#[tokio::test]
async fn test_append_creates_new_file_atomic() {
let fs = InMemoryFs::new();
fs.append_file(Path::new("/tmp/new.txt"), b"hello")
.await
.unwrap();
let content = fs.read_file(Path::new("/tmp/new.txt")).await.unwrap();
assert_eq!(content, b"hello");
}
#[tokio::test]
async fn test_append_to_directory_fails() {
let fs = InMemoryFs::new();
fs.mkdir(Path::new("/tmp/dir"), false).await.unwrap();
let result = fs.append_file(Path::new("/tmp/dir"), b"data").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_path_on_copy() {
let limits = FsLimits::new().max_path_depth(3);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/src.txt"), b"data")
.await
.unwrap();
let deep = Path::new("/a/b/c/d/e/f.txt");
let result = fs.copy(Path::new("/tmp/src.txt"), deep).await;
assert!(result.is_err(), "copy to deep path should be rejected");
}
#[tokio::test]
async fn test_validate_path_on_rename() {
let limits = FsLimits::new().max_path_depth(3);
let fs = InMemoryFs::with_limits(limits);
fs.write_file(Path::new("/tmp/src.txt"), b"data")
.await
.unwrap();
let deep = Path::new("/a/b/c/d/e/f.txt");
let result = fs.rename(Path::new("/tmp/src.txt"), deep).await;
assert!(result.is_err(), "rename to deep path should be rejected");
}
#[tokio::test]
async fn test_copy_respects_write_limits() {
let limits = FsLimits::new().max_file_count(10);
let fs = InMemoryFs::with_limits(limits);
for i in 0..10 {
let _ = fs
.write_file(Path::new(&format!("/tmp/f{i}.txt")), b"x")
.await;
}
let result = fs
.copy(Path::new("/tmp/f0.txt"), Path::new("/tmp/copy.txt"))
.await;
assert!(
result.is_err(),
"copy should respect file count write limits"
);
}
#[tokio::test]
async fn test_validate_path_on_chmod() {
let limits = FsLimits::new().max_path_depth(3);
let fs = InMemoryFs::with_limits(limits);
let deep = Path::new("/a/b/c/d/e/f.txt");
let result = fs.chmod(deep, 0o755).await;
assert!(result.is_err(), "chmod on deep path should be rejected");
}
#[tokio::test]
async fn test_dev_urandom_returns_bytes() {
let fs = InMemoryFs::new();
let content = fs.read_file(Path::new("/dev/urandom")).await.unwrap();
assert_eq!(content.len(), 8192);
}
#[tokio::test]
async fn test_dev_random_returns_bytes() {
let fs = InMemoryFs::new();
let content = fs.read_file(Path::new("/dev/random")).await.unwrap();
assert_eq!(content.len(), 8192);
}
#[tokio::test]
async fn test_dev_urandom_returns_different_data() {
let fs = InMemoryFs::new();
let a = fs.read_file(Path::new("/dev/urandom")).await.unwrap();
let b = fs.read_file(Path::new("/dev/urandom")).await.unwrap();
assert_ne!(a, b);
}
#[tokio::test]
async fn test_dev_urandom_exists_in_fs() {
let fs = InMemoryFs::new();
let exists = fs.exists(Path::new("/dev/urandom")).await.unwrap();
assert!(exists, "/dev/urandom should exist in VFS");
}
#[tokio::test]
async fn test_dev_urandom_write_succeeds() {
let fs = InMemoryFs::new();
let result = fs.write_file(Path::new("/dev/urandom"), b"ignored").await;
assert!(result.is_ok());
let content = fs.read_file(Path::new("/dev/urandom")).await.unwrap();
assert_eq!(content.len(), 8192);
}
#[tokio::test]
async fn test_dev_urandom_path_normalization() {
let fs = InMemoryFs::new();
let content = fs
.read_file(Path::new("/dev/../dev/urandom"))
.await
.unwrap();
assert_eq!(content.len(), 8192);
}
#[tokio::test]
async fn test_lazy_file_read() {
let fs = InMemoryFs::new();
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let called_clone = Arc::clone(&called);
fs.add_lazy_file(
"/tmp/lazy.txt",
5,
0o644,
Arc::new(move || {
called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
b"hello".to_vec()
}),
);
let meta = fs.stat(Path::new("/tmp/lazy.txt")).await.unwrap();
assert_eq!(meta.file_type, FileType::File);
assert!(!called.load(std::sync::atomic::Ordering::SeqCst));
let content = fs.read_file(Path::new("/tmp/lazy.txt")).await.unwrap();
assert_eq!(content, b"hello");
assert!(called.load(std::sync::atomic::Ordering::SeqCst));
}
#[tokio::test]
async fn test_lazy_file_write_before_read_skips_loader() {
let fs = InMemoryFs::new();
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let called_clone = Arc::clone(&called);
fs.add_lazy_file(
"/tmp/lazy.txt",
5,
0o644,
Arc::new(move || {
called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
b"lazy".to_vec()
}),
);
fs.write_file(Path::new("/tmp/lazy.txt"), b"eager")
.await
.unwrap();
let content = fs.read_file(Path::new("/tmp/lazy.txt")).await.unwrap();
assert_eq!(content, b"eager");
assert!(!called.load(std::sync::atomic::Ordering::SeqCst));
}
#[tokio::test]
async fn test_lazy_file_exists_and_readdir() {
let fs = InMemoryFs::new();
fs.add_lazy_file("/tmp/lazy.txt", 10, 0o644, Arc::new(|| b"content".to_vec()));
assert!(fs.exists(Path::new("/tmp/lazy.txt")).await.unwrap());
let entries = fs.read_dir(Path::new("/tmp")).await.unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"lazy.txt"));
}
#[tokio::test]
async fn test_lazy_file_snapshot_materializes() {
let fs = InMemoryFs::new();
fs.add_lazy_file("/tmp/lazy.txt", 6, 0o644, Arc::new(|| b"snappy".to_vec()));
let snapshot = fs.snapshot();
let content = fs.read_file(Path::new("/tmp/lazy.txt")).await.unwrap();
assert_eq!(content, b"snappy");
let has_file = snapshot
.entries
.iter()
.any(|e| e.path == Path::new("/tmp/lazy.txt"));
assert!(has_file);
}
#[tokio::test]
async fn test_add_lazy_file_respects_limits() {
let limits = FsLimits::new().max_total_bytes(100);
let fs = InMemoryFs::with_limits(limits);
fs.add_lazy_file("/tmp/too-big.txt", 101, 0o644, Arc::new(|| vec![0; 101]));
assert!(!fs.exists(Path::new("/tmp/too-big.txt")).await.unwrap());
}
#[tokio::test]
async fn test_lazy_file_materialization_respects_limits() {
let limits = FsLimits::new().max_file_size(5);
let fs = InMemoryFs::with_limits(limits);
fs.add_lazy_file("/tmp/lazy.txt", 1, 0o644, Arc::new(|| b"toolarge".to_vec()));
let result = fs.read_file(Path::new("/tmp/lazy.txt")).await;
assert!(result.is_err());
assert!(fs.exists(Path::new("/tmp/lazy.txt")).await.unwrap());
}
#[tokio::test]
async fn test_mkdir_respects_dir_count_limit() {
let limits = FsLimits::new().max_dir_count(8); let fs = InMemoryFs::with_limits(limits);
fs.mkdir(Path::new("/tmp/dir1"), false).await.unwrap();
fs.mkdir(Path::new("/tmp/dir2"), false).await.unwrap();
let result = fs.mkdir(Path::new("/tmp/dir3"), false).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too many directories") || err.contains("limit"));
}
#[tokio::test]
async fn test_mkdir_recursive_respects_dir_count_limit() {
let limits = FsLimits::new().max_dir_count(7);
let fs = InMemoryFs::with_limits(limits);
fs.mkdir(Path::new("/tmp/a"), true).await.unwrap();
let result = fs.mkdir(Path::new("/tmp/b"), true).await;
assert!(result.is_err());
let limits2 = FsLimits::new().max_dir_count(7);
let fs2 = InMemoryFs::with_limits(limits2);
let result = fs2.mkdir(Path::new("/tmp/a/b"), true).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_symlink_respects_file_count_limit() {
let limits = FsLimits::new().max_file_count(5); let fs = InMemoryFs::with_limits(limits);
fs.symlink(Path::new("/tmp/target1"), Path::new("/tmp/link1"))
.await
.unwrap();
fs.symlink(Path::new("/tmp/target2"), Path::new("/tmp/link2"))
.await
.unwrap();
let result = fs
.symlink(Path::new("/tmp/target3"), Path::new("/tmp/link3"))
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too many files") || err.contains("limit"));
}
#[tokio::test]
async fn test_mkfifo_respects_file_count_limit() {
let limits = FsLimits::new().max_file_count(5); let fs = InMemoryFs::with_limits(limits);
fs.mkfifo(Path::new("/tmp/fifo1"), 0o644).await.unwrap();
fs.mkfifo(Path::new("/tmp/fifo2"), 0o644).await.unwrap();
let result = fs.mkfifo(Path::new("/tmp/fifo3"), 0o644).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("too many files") || err.contains("limit"));
}
}