use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::SystemTime;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::error::{VfsError, VfsResult};
#[derive(Debug, Clone)]
pub struct Metadata {
pub is_dir: bool,
pub size: u64,
pub created: SystemTime,
pub modified: SystemTime,
pub accessed: SystemTime,
}
impl Default for Metadata {
fn default() -> Self {
let now = SystemTime::now();
Self {
is_dir: false,
size: 0,
created: now,
modified: now,
accessed: now,
}
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub metadata: Metadata,
}
#[async_trait]
pub trait VfsStorage: Send + Sync {
async fn read(&self, path: &str) -> VfsResult<Vec<u8>>;
async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>>;
async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()>;
async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()>;
async fn set_size(&self, path: &str, size: u64) -> VfsResult<()>;
async fn delete(&self, path: &str) -> VfsResult<()>;
async fn exists(&self, path: &str) -> VfsResult<bool>;
async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>>;
async fn stat(&self, path: &str) -> VfsResult<Metadata>;
async fn mkdir(&self, path: &str) -> VfsResult<()>;
async fn rmdir(&self, path: &str) -> VfsResult<()>;
async fn rename(&self, from: &str, to: &str) -> VfsResult<()>;
fn mkdir_sync(&self, _path: &str) -> VfsResult<()> {
Err(VfsError::Storage(
"mkdir_sync not implemented for this storage backend".to_string(),
))
}
}
#[derive(Clone)]
pub struct ArcStorage(Arc<dyn VfsStorage>);
impl ArcStorage {
pub fn new(storage: Arc<dyn VfsStorage>) -> Self {
Self(storage)
}
}
impl std::fmt::Debug for ArcStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ArcStorage")
.field(&"<dyn VfsStorage>")
.finish()
}
}
#[async_trait]
impl VfsStorage for ArcStorage {
async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
self.0.read(path).await
}
async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>> {
self.0.read_at(path, offset, len).await
}
async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()> {
self.0.write(path, data).await
}
async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()> {
self.0.write_at(path, offset, data).await
}
async fn set_size(&self, path: &str, size: u64) -> VfsResult<()> {
self.0.set_size(path, size).await
}
async fn delete(&self, path: &str) -> VfsResult<()> {
self.0.delete(path).await
}
async fn exists(&self, path: &str) -> VfsResult<bool> {
self.0.exists(path).await
}
async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
self.0.list(path).await
}
async fn stat(&self, path: &str) -> VfsResult<Metadata> {
self.0.stat(path).await
}
async fn mkdir(&self, path: &str) -> VfsResult<()> {
self.0.mkdir(path).await
}
async fn rmdir(&self, path: &str) -> VfsResult<()> {
self.0.rmdir(path).await
}
async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
self.0.rename(from, to).await
}
fn mkdir_sync(&self, path: &str) -> VfsResult<()> {
self.0.mkdir_sync(path)
}
}
#[derive(Debug, Default)]
struct StorageState {
files: HashMap<String, FileData>,
directories: HashSet<String>,
}
#[derive(Debug)]
pub struct InMemoryStorage {
state: RwLock<StorageState>,
}
#[derive(Debug, Clone)]
struct FileData {
content: Vec<u8>,
created: SystemTime,
modified: SystemTime,
accessed: SystemTime,
}
impl Default for InMemoryStorage {
fn default() -> Self {
Self::new()
}
}
impl InMemoryStorage {
#[must_use]
pub fn new() -> Self {
let mut directories = HashSet::new();
directories.insert("/".to_string());
Self {
state: RwLock::new(StorageState {
files: HashMap::new(),
directories,
}),
}
}
fn normalize_path(path: &str) -> VfsResult<String> {
if !path.starts_with('/') {
return Err(VfsError::InvalidPath(format!(
"path must be absolute: {path}"
)));
}
let mut components: Vec<&str> = Vec::new();
for component in path.split('/') {
match component {
"" | "." => continue,
".." => {
if components.is_empty() {
return Err(VfsError::InvalidPath("path escapes root".to_string()));
}
components.pop();
}
c => components.push(c),
}
}
if components.is_empty() {
Ok("/".to_string())
} else {
Ok(format!("/{}", components.join("/")))
}
}
fn parent_path(path: &str) -> Option<String> {
if path == "/" {
return None;
}
let normalized = Self::normalize_path(path).ok()?;
if normalized == "/" {
return None;
}
match normalized.rfind('/') {
Some(0) => Some("/".to_string()),
Some(idx) => Some(normalized[..idx].to_string()),
None => None,
}
}
fn check_parent_exists_with_state(state: &StorageState, path: &str) -> VfsResult<()> {
if let Some(parent) = Self::parent_path(path)
&& !state.directories.contains(&parent)
{
return Err(VfsError::NotFound(format!("parent directory: {parent}")));
}
Ok(())
}
}
#[async_trait]
impl VfsStorage for InMemoryStorage {
async fn read(&self, path: &str) -> VfsResult<Vec<u8>> {
let path = Self::normalize_path(path)?;
let state = self.state.read().await;
match state.files.get(&path) {
Some(data) => Ok(data.content.clone()),
None => {
if state.directories.contains(&path) {
Err(VfsError::NotFile(path))
} else {
Err(VfsError::NotFound(path))
}
}
}
}
async fn read_at(&self, path: &str, offset: u64, len: u64) -> VfsResult<Vec<u8>> {
let path = Self::normalize_path(path)?;
let state = self.state.read().await;
match state.files.get(&path) {
Some(data) => {
let offset = offset as usize;
let len = len as usize;
if offset >= data.content.len() {
Ok(Vec::new())
} else {
let end = (offset + len).min(data.content.len());
Ok(data.content[offset..end].to_vec())
}
}
None => {
if state.directories.contains(&path) {
Err(VfsError::NotFile(path))
} else {
Err(VfsError::NotFound(path))
}
}
}
}
async fn write(&self, path: &str, data: &[u8]) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let mut state = self.state.write().await;
Self::check_parent_exists_with_state(&state, &path)?;
if state.directories.contains(&path) {
return Err(VfsError::NotFile(path));
}
let now = SystemTime::now();
let file_data = state.files.entry(path).or_insert_with(|| FileData {
content: Vec::new(),
created: now,
modified: now,
accessed: now,
});
file_data.content = data.to_vec();
file_data.modified = now;
Ok(())
}
async fn write_at(&self, path: &str, offset: u64, data: &[u8]) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let mut state = self.state.write().await;
Self::check_parent_exists_with_state(&state, &path)?;
if state.directories.contains(&path) {
return Err(VfsError::NotFile(path));
}
let now = SystemTime::now();
let offset = offset as usize;
let file_data = state.files.entry(path).or_insert_with(|| FileData {
content: Vec::new(),
created: now,
modified: now,
accessed: now,
});
let needed_len = offset + data.len();
if file_data.content.len() < needed_len {
file_data.content.resize(needed_len, 0);
}
file_data.content[offset..offset + data.len()].copy_from_slice(data);
file_data.modified = now;
Ok(())
}
async fn set_size(&self, path: &str, size: u64) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let now = SystemTime::now();
let mut state = self.state.write().await;
match state.files.get_mut(&path) {
Some(data) => {
data.content.resize(size as usize, 0);
data.modified = now;
Ok(())
}
None => Err(VfsError::NotFound(path)),
}
}
async fn delete(&self, path: &str) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let mut state = self.state.write().await;
if state.files.remove(&path).is_some() {
Ok(())
} else if state.directories.contains(&path) {
Err(VfsError::NotFile(path))
} else {
Err(VfsError::NotFound(path))
}
}
async fn exists(&self, path: &str) -> VfsResult<bool> {
let path = Self::normalize_path(path)?;
let state = self.state.read().await;
Ok(state.files.contains_key(&path) || state.directories.contains(&path))
}
async fn list(&self, path: &str) -> VfsResult<Vec<DirEntry>> {
let path = Self::normalize_path(path)?;
let state = self.state.read().await;
if !state.directories.contains(&path) {
if state.files.contains_key(&path) {
return Err(VfsError::NotDirectory(path));
} else {
return Err(VfsError::NotFound(path));
}
}
let prefix = if path == "/" {
"/".to_string()
} else {
format!("{path}/")
};
let mut entries = Vec::new();
let mut seen_names = HashSet::new();
for (file_path, data) in &state.files {
if let Some(rest) = file_path.strip_prefix(&prefix) {
if !rest.contains('/') && !rest.is_empty() {
seen_names.insert(rest.to_string());
entries.push(DirEntry {
name: rest.to_string(),
metadata: Metadata {
is_dir: false,
size: data.content.len() as u64,
created: data.created,
modified: data.modified,
accessed: data.accessed,
},
});
}
}
}
for dir_path in &state.directories {
if let Some(rest) = dir_path.strip_prefix(&prefix) {
if !rest.contains('/') && !rest.is_empty() && !seen_names.contains(rest) {
let now = SystemTime::now();
entries.push(DirEntry {
name: rest.to_string(),
metadata: Metadata {
is_dir: true,
size: 0,
created: now,
modified: now,
accessed: now,
},
});
}
}
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
async fn stat(&self, path: &str) -> VfsResult<Metadata> {
let path = Self::normalize_path(path)?;
let state = self.state.read().await;
if let Some(data) = state.files.get(&path) {
return Ok(Metadata {
is_dir: false,
size: data.content.len() as u64,
created: data.created,
modified: data.modified,
accessed: data.accessed,
});
}
if state.directories.contains(&path) {
let now = SystemTime::now();
return Ok(Metadata {
is_dir: true,
size: 0,
created: now,
modified: now,
accessed: now,
});
}
Err(VfsError::NotFound(path))
}
async fn mkdir(&self, path: &str) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let mut state = self.state.write().await;
Self::check_parent_exists_with_state(&state, &path)?;
if state.files.contains_key(&path) {
return Err(VfsError::AlreadyExists(path));
}
if state.directories.contains(&path) {
return Err(VfsError::AlreadyExists(path));
}
state.directories.insert(path);
Ok(())
}
async fn rmdir(&self, path: &str) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
if path == "/" {
return Err(VfsError::PermissionDenied(
"cannot remove root directory".to_string(),
));
}
let mut state = self.state.write().await;
if !state.directories.contains(&path) {
if state.files.contains_key(&path) {
return Err(VfsError::NotDirectory(path));
} else {
return Err(VfsError::NotFound(path));
}
}
let prefix = format!("{path}/");
for file_path in state.files.keys() {
if file_path.starts_with(&prefix) {
return Err(VfsError::DirectoryNotEmpty(path));
}
}
for dir_path in &state.directories {
if dir_path.starts_with(&prefix) {
return Err(VfsError::DirectoryNotEmpty(path));
}
}
state.directories.remove(&path);
Ok(())
}
async fn rename(&self, from: &str, to: &str) -> VfsResult<()> {
let from = Self::normalize_path(from)?;
let to = Self::normalize_path(to)?;
if from == to {
return Ok(());
}
let mut state = self.state.write().await;
Self::check_parent_exists_with_state(&state, &to)?;
if state.files.contains_key(&from) {
if state.directories.contains(&to) {
return Err(VfsError::AlreadyExists(to));
}
if let Some(data) = state.files.remove(&from) {
state.files.insert(to, data);
return Ok(());
}
}
if state.directories.contains(&from) {
if state.files.contains_key(&to) {
return Err(VfsError::AlreadyExists(to));
}
let from_prefix = format!("{from}/");
let to_prefix = format!("{to}/");
let files_to_rename: Vec<_> = state
.files
.keys()
.filter(|p| p.starts_with(&from_prefix))
.cloned()
.collect();
for old_path in files_to_rename {
if let Some(data) = state.files.remove(&old_path) {
let new_path = old_path.replacen(&from_prefix, &to_prefix, 1);
state.files.insert(new_path, data);
}
}
let dirs_to_rename: Vec<_> = state
.directories
.iter()
.filter(|p| *p == &from || p.starts_with(&from_prefix))
.cloned()
.collect();
for old_path in dirs_to_rename {
state.directories.remove(&old_path);
let new_path = if old_path == from {
to.clone()
} else {
old_path.replacen(&from_prefix, &to_prefix, 1)
};
state.directories.insert(new_path);
}
return Ok(());
}
Err(VfsError::NotFound(from))
}
fn mkdir_sync(&self, path: &str) -> VfsResult<()> {
let path = Self::normalize_path(path)?;
let mut dirs_to_create = Vec::new();
let mut current = String::new();
for component in path.split('/').filter(|s| !s.is_empty()) {
current = format!("{}/{}", current, component);
dirs_to_create.push(current.clone());
}
if let Ok(mut state) = self.state.try_write() {
for dir in dirs_to_create {
state.directories.insert(dir);
}
return Ok(());
}
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.block_on(async {
let mut state = self.state.write().await;
for dir in dirs_to_create {
state.directories.insert(dir);
}
});
} else {
let mut state = self.state.blocking_write();
for dir in dirs_to_create {
state.directories.insert(dir);
}
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn test_file_operations() {
let storage = InMemoryStorage::new();
storage.write("/test.txt", b"hello").await.unwrap();
let content = storage.read("/test.txt").await.unwrap();
assert_eq!(content, b"hello");
let partial = storage.read_at("/test.txt", 2, 3).await.unwrap();
assert_eq!(partial, b"llo");
storage.write("/test.txt", b"world").await.unwrap();
let content = storage.read("/test.txt").await.unwrap();
assert_eq!(content, b"world");
storage.delete("/test.txt").await.unwrap();
assert!(storage.read("/test.txt").await.is_err());
}
#[tokio::test]
async fn test_directory_operations() {
let storage = InMemoryStorage::new();
storage.mkdir("/subdir").await.unwrap();
assert!(storage.exists("/subdir").await.unwrap());
storage.write("/subdir/file.txt", b"content").await.unwrap();
let entries = storage.list("/subdir").await.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "file.txt");
assert!(storage.rmdir("/subdir").await.is_err());
storage.delete("/subdir/file.txt").await.unwrap();
storage.rmdir("/subdir").await.unwrap();
assert!(!storage.exists("/subdir").await.unwrap());
}
#[tokio::test]
async fn test_path_normalization() {
let storage = InMemoryStorage::new();
storage.write("/test.txt", b"data").await.unwrap();
assert!(storage.exists("/test.txt").await.unwrap());
assert!(storage.exists("/./test.txt").await.unwrap());
storage.mkdir("/dir").await.unwrap();
storage.write("/dir/file.txt", b"data").await.unwrap();
let content = storage.read("/dir/../dir/file.txt").await.unwrap();
assert_eq!(content, b"data");
}
#[tokio::test]
async fn test_rename() {
let storage = InMemoryStorage::new();
storage.write("/old.txt", b"content").await.unwrap();
storage.rename("/old.txt", "/new.txt").await.unwrap();
assert!(!storage.exists("/old.txt").await.unwrap());
assert!(storage.exists("/new.txt").await.unwrap());
storage.mkdir("/olddir").await.unwrap();
storage.write("/olddir/file.txt", b"data").await.unwrap();
storage.rename("/olddir", "/newdir").await.unwrap();
assert!(!storage.exists("/olddir").await.unwrap());
assert!(storage.exists("/newdir").await.unwrap());
assert!(storage.exists("/newdir/file.txt").await.unwrap());
}
#[tokio::test]
async fn test_stat() {
let storage = InMemoryStorage::new();
storage.write("/file.txt", b"hello").await.unwrap();
let meta = storage.stat("/file.txt").await.unwrap();
assert!(!meta.is_dir);
assert_eq!(meta.size, 5);
storage.mkdir("/dir").await.unwrap();
let meta = storage.stat("/dir").await.unwrap();
assert!(meta.is_dir);
}
#[tokio::test]
async fn test_write_at() {
let storage = InMemoryStorage::new();
storage.write_at("/file.txt", 5, b"world").await.unwrap();
let content = storage.read("/file.txt").await.unwrap();
assert_eq!(content.len(), 10);
assert_eq!(&content[5..], b"world");
assert_eq!(&content[0..5], &[0, 0, 0, 0, 0]);
storage.write_at("/file.txt", 0, b"hello").await.unwrap();
let content = storage.read("/file.txt").await.unwrap();
assert_eq!(&content, b"helloworld");
}
#[test]
fn test_mkdir_sync() {
let storage = InMemoryStorage::new();
storage.mkdir_sync("/data").unwrap();
let state = storage.state.blocking_read();
assert!(state.directories.contains("/data"));
}
#[test]
fn test_mkdir_sync_nested() {
let storage = InMemoryStorage::new();
storage.mkdir_sync("/data/subdir/nested").unwrap();
let state = storage.state.blocking_read();
assert!(state.directories.contains("/data"));
assert!(state.directories.contains("/data/subdir"));
assert!(state.directories.contains("/data/subdir/nested"));
}
}