use std::collections::HashMap;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::{Result, StorageError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VfsEntry {
pub cid: String,
pub name: String,
pub size: u64,
pub encrypted: bool,
pub is_dir: bool,
pub created: u64,
}
pub struct VirtualFs {
entries: HashMap<String, VfsEntry>,
}
impl VirtualFs {
pub fn new() -> Self {
let mut entries = HashMap::new();
entries.insert(
"/".to_string(),
VfsEntry {
cid: String::new(),
name: "/".to_string(),
size: 0,
encrypted: false,
is_dir: true,
created: now_secs(),
},
);
Self { entries }
}
pub fn add_file(
&mut self,
path: &str,
cid: &str,
size: u64,
encrypted: bool,
) -> Result<()> {
let path = normalize_path(path);
let parent = parent_path(&path);
if !self.entries.contains_key(&parent) {
return Err(StorageError::Filesystem(format!(
"parent directory not found: {parent}"
)));
}
let name = path.rsplit('/').next().unwrap_or(&path).to_string();
self.entries.insert(
path,
VfsEntry {
cid: cid.to_string(),
name,
size,
encrypted,
is_dir: false,
created: now_secs(),
},
);
Ok(())
}
pub fn mkdir(&mut self, path: &str) -> Result<()> {
let path = normalize_path(path);
let parent = parent_path(&path);
if !self.entries.contains_key(&parent) {
return Err(StorageError::Filesystem(format!(
"parent directory not found: {parent}"
)));
}
let name = path.rsplit('/').next().unwrap_or(&path).to_string();
self.entries.insert(
path,
VfsEntry {
cid: String::new(),
name,
size: 0,
encrypted: false,
is_dir: true,
created: now_secs(),
},
);
Ok(())
}
pub fn get(&self, path: &str) -> Result<&VfsEntry> {
let path = normalize_path(path);
self.entries
.get(&path)
.ok_or(StorageError::NotFound(path))
}
pub fn ls(&self, dir_path: &str) -> Result<Vec<&VfsEntry>> {
let dir_path = normalize_path(dir_path);
let entry = self
.entries
.get(&dir_path)
.ok_or_else(|| StorageError::NotFound(dir_path.clone()))?;
if !entry.is_dir {
return Err(StorageError::Filesystem(format!(
"not a directory: {dir_path}"
)));
}
let prefix = if dir_path == "/" {
"/".to_string()
} else {
format!("{dir_path}/")
};
let children: Vec<&VfsEntry> = self
.entries
.iter()
.filter(|(k, _)| {
if let Some(rest) = k.strip_prefix(&prefix) {
!rest.is_empty() && !rest.contains('/')
} else {
false
}
})
.map(|(_, v)| v)
.collect();
Ok(children)
}
pub fn remove(&mut self, path: &str) -> Result<()> {
let path = normalize_path(path);
if path == "/" {
return Err(StorageError::Filesystem(
"cannot remove root directory".into(),
));
}
let entry = self
.entries
.get(&path)
.ok_or_else(|| StorageError::NotFound(path.clone()))?;
if entry.is_dir && !self.ls(&path)?.is_empty() {
return Err(StorageError::Filesystem(format!(
"directory not empty: {path}"
)));
}
self.entries.remove(&path);
Ok(())
}
pub fn exists(&self, path: &str) -> bool {
self.entries.contains_key(&normalize_path(path))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.len() <= 1
}
pub fn to_json(&self) -> Result<String> {
let entries: Vec<&VfsEntry> = self.entries.values().collect();
serde_json::to_string_pretty(&entries)
.map_err(|e| StorageError::Filesystem(format!("serialization failed: {e}")))
}
}
impl Default for VirtualFs {
fn default() -> Self {
Self::new()
}
}
fn normalize_path(path: &str) -> String {
let path = path.trim();
if path.is_empty() || path == "/" {
return "/".to_string();
}
let mut p = if path.starts_with('/') {
path.to_string()
} else {
format!("/{path}")
};
if p.ends_with('/') && p.len() > 1 {
p.pop();
}
p
}
fn parent_path(path: &str) -> String {
if path == "/" {
return "/".to_string();
}
match path.rfind('/') {
Some(0) => "/".to_string(),
Some(i) => path[..i].to_string(),
None => "/".to_string(),
}
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_exists_by_default() {
let fs = VirtualFs::new();
assert!(fs.exists("/"));
assert!(fs.get("/").unwrap().is_dir);
}
#[test]
fn add_file_to_root() {
let mut fs = VirtualFs::new();
fs.add_file("/readme.txt", "QmTest123", 42, false).unwrap();
let entry = fs.get("/readme.txt").unwrap();
assert_eq!(entry.cid, "QmTest123");
assert_eq!(entry.size, 42);
assert!(!entry.is_dir);
assert!(!entry.encrypted);
}
#[test]
fn add_encrypted_file() {
let mut fs = VirtualFs::new();
fs.add_file("/secret.dat", "QmEnc456", 100, true).unwrap();
assert!(fs.get("/secret.dat").unwrap().encrypted);
}
#[test]
fn mkdir_and_nested_file() {
let mut fs = VirtualFs::new();
fs.mkdir("/docs").unwrap();
fs.add_file("/docs/paper.pdf", "QmPaper", 1024, false)
.unwrap();
let entry = fs.get("/docs/paper.pdf").unwrap();
assert_eq!(entry.name, "paper.pdf");
}
#[test]
fn mkdir_missing_parent_fails() {
let mut fs = VirtualFs::new();
let result = fs.mkdir("/a/b/c");
assert!(result.is_err());
}
#[test]
fn ls_root() {
let mut fs = VirtualFs::new();
fs.add_file("/a.txt", "Qm1", 1, false).unwrap();
fs.add_file("/b.txt", "Qm2", 2, false).unwrap();
fs.mkdir("/subdir").unwrap();
let children = fs.ls("/").unwrap();
assert_eq!(children.len(), 3);
}
#[test]
fn ls_excludes_nested() {
let mut fs = VirtualFs::new();
fs.mkdir("/dir").unwrap();
fs.add_file("/dir/file.txt", "Qm1", 10, false).unwrap();
let root_children = fs.ls("/").unwrap();
assert_eq!(root_children.len(), 1);
assert!(root_children[0].is_dir);
}
#[test]
fn remove_file() {
let mut fs = VirtualFs::new();
fs.add_file("/tmp.txt", "Qm1", 1, false).unwrap();
assert!(fs.exists("/tmp.txt"));
fs.remove("/tmp.txt").unwrap();
assert!(!fs.exists("/tmp.txt"));
}
#[test]
fn remove_root_fails() {
let mut fs = VirtualFs::new();
assert!(fs.remove("/").is_err());
}
#[test]
fn remove_nonempty_dir_fails() {
let mut fs = VirtualFs::new();
fs.mkdir("/dir").unwrap();
fs.add_file("/dir/file", "Qm1", 1, false).unwrap();
assert!(fs.remove("/dir").is_err());
}
#[test]
fn normalize_paths() {
assert_eq!(normalize_path(""), "/");
assert_eq!(normalize_path("/"), "/");
assert_eq!(normalize_path("foo"), "/foo");
assert_eq!(normalize_path("/foo/"), "/foo");
assert_eq!(normalize_path("/a/b"), "/a/b");
}
#[test]
fn parent_paths() {
assert_eq!(parent_path("/"), "/");
assert_eq!(parent_path("/foo"), "/");
assert_eq!(parent_path("/a/b"), "/a");
assert_eq!(parent_path("/a/b/c"), "/a/b");
}
#[test]
fn to_json_works() {
let mut fs = VirtualFs::new();
fs.add_file("/test.txt", "Qm123", 5, false).unwrap();
let json = fs.to_json().unwrap();
assert!(json.contains("Qm123"));
}
#[test]
fn len_and_is_empty() {
let mut fs = VirtualFs::new();
assert!(fs.is_empty()); fs.add_file("/a", "Qm1", 1, false).unwrap();
assert!(!fs.is_empty());
assert_eq!(fs.len(), 2); }
}