use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct DirEntry {
pub path: PathBuf,
pub name: String,
pub is_dir: bool,
}
pub trait FileSystem: Send + Sync {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
fn exists(&self, path: &Path) -> bool;
}
pub struct LocalFs;
impl FileSystem for LocalFs {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let ft = entry.file_type()?;
entries.push(DirEntry {
path: entry.path(),
name: entry.file_name().to_string_lossy().into_owned(),
is_dir: ft.is_dir(),
});
}
Ok(entries)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
}
pub struct RootedFs {
root: PathBuf,
}
impl RootedFs {
pub fn new(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
}
}
fn resolve(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
}
}
}
impl FileSystem for RootedFs {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(self.resolve(path))
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let resolved = self.resolve(path);
let mut entries = Vec::new();
for entry in std::fs::read_dir(&resolved)? {
let entry = entry?;
let ft = entry.file_type()?;
entries.push(DirEntry {
path: entry.path(),
name: entry.file_name().to_string_lossy().into_owned(),
is_dir: ft.is_dir(),
});
}
Ok(entries)
}
fn exists(&self, path: &Path) -> bool {
self.resolve(path).exists()
}
}
pub struct MemoryFs {
files: HashMap<PathBuf, Vec<u8>>,
}
impl MemoryFs {
pub fn new(entries: &[(&str, &str)]) -> Self {
let mut files = HashMap::new();
for (path, content) in entries {
files.insert(normalize(path), content.as_bytes().to_vec());
}
Self { files }
}
pub fn from_bytes(entries: &[(&str, &[u8])]) -> Self {
let mut files = HashMap::new();
for (path, content) in entries {
files.insert(normalize(path), content.to_vec());
}
Self { files }
}
}
impl FileSystem for MemoryFs {
fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
let key = normalize(&path.to_string_lossy());
self.files
.get(&key)
.cloned()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{}", path.display())))
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let dir = normalize(&path.to_string_lossy());
let prefix = if dir.as_os_str().is_empty() {
PathBuf::new()
} else {
dir.clone()
};
let mut seen = HashSet::new();
let mut entries = Vec::new();
for file_path in self.files.keys() {
let child_name = if prefix.as_os_str().is_empty() {
file_path.components().next()
} else if file_path.starts_with(&prefix) {
file_path
.strip_prefix(&prefix)
.ok()
.and_then(|rest| rest.components().next())
} else {
None
};
if let Some(component) = child_name {
let name = component.as_os_str().to_string_lossy().into_owned();
if seen.insert(name.clone()) {
let child_path = if prefix.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
prefix.join(&name)
};
let is_dir = !self.files.contains_key(&child_path);
entries.push(DirEntry {
path: child_path,
name,
is_dir,
});
}
}
}
if entries.is_empty() && !self.exists(path) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{}", path.display()),
));
}
Ok(entries)
}
fn exists(&self, path: &Path) -> bool {
let key = normalize(&path.to_string_lossy());
if self.files.contains_key(&key) {
return true;
}
if key.as_os_str().is_empty() {
return true;
}
self.files.keys().any(|p| p.starts_with(&key))
}
}
pub(crate) fn normalize_dir<'a>(dir: &'a str) -> Cow<'a, str> {
let stripped_prefix = dir.strip_prefix("./");
let d = stripped_prefix.unwrap_or(dir);
let stripped_suffix = d.strip_suffix('/');
let d = stripped_suffix.unwrap_or(d);
if d.is_empty() {
Cow::Owned(".".to_string())
} else if stripped_prefix.is_some() || stripped_suffix.is_some() {
Cow::Owned(d.to_string())
} else {
Cow::Borrowed(dir)
}
}
fn normalize(s: &str) -> PathBuf {
let s = s.strip_prefix("./").unwrap_or(s);
let s = s.strip_suffix('/').unwrap_or(s);
if s == "." || s.is_empty() {
PathBuf::new()
} else {
PathBuf::from(s)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn memory_fs_read_file() {
let fs = MemoryFs::new(&[("hello.txt", "world")]);
let content = fs.read_file(Path::new("hello.txt")).unwrap();
assert_eq!(content, b"world");
}
#[test]
fn memory_fs_read_file_not_found() {
let fs = MemoryFs::new(&[]);
assert!(fs.read_file(Path::new("missing.txt")).is_err());
}
#[test]
fn memory_fs_read_dir_root() {
let fs = MemoryFs::new(&[
("package.json", "{}"),
("src/main.rs", "fn main() {}"),
("src/lib.rs", ""),
]);
let entries = fs.read_dir(Path::new(".")).unwrap();
let names: HashSet<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, HashSet::from(["package.json", "src"]));
assert!(entries.iter().find(|e| e.name == "src").unwrap().is_dir);
assert!(
!entries
.iter()
.find(|e| e.name == "package.json")
.unwrap()
.is_dir
);
}
#[test]
fn memory_fs_read_dir_subdirectory() {
let fs = MemoryFs::new(&[("apps/api/package.json", "{}"), ("apps/web/index.ts", "")]);
let entries = fs.read_dir(Path::new("apps")).unwrap();
let names: HashSet<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, HashSet::from(["api", "web"]));
assert!(entries.iter().all(|e| e.is_dir));
}
#[test]
fn memory_fs_exists_file() {
let fs = MemoryFs::new(&[("a/b/c.txt", "content")]);
assert!(fs.exists(Path::new("a/b/c.txt")));
assert!(fs.exists(Path::new("a/b")));
assert!(fs.exists(Path::new("a")));
assert!(!fs.exists(Path::new("x")));
}
#[test]
fn memory_fs_exists_root() {
let fs = MemoryFs::new(&[("file.txt", "")]);
assert!(fs.exists(Path::new(".")));
assert!(fs.exists(Path::new("")));
}
#[test]
fn memory_fs_empty() {
let fs = MemoryFs::new(&[]);
assert!(!fs.exists(Path::new("anything")));
assert!(fs.exists(Path::new(".")));
assert!(fs.read_dir(Path::new(".")).unwrap().is_empty());
}
#[test]
fn memory_fs_normalized_paths() {
let fs = MemoryFs::new(&[("./src/main.rs", "fn main() {}")]);
assert!(fs.read_file(Path::new("src/main.rs")).is_ok());
assert!(fs.exists(Path::new("src")));
}
}