mod entry;
mod file;
use crate::file::{DirEntry, Metadata, OpenOptions};
use crate::memory_fs::file::{FileHandle, FileMode};
use crate::tree::{Directory, Entry, FilesystemTree};
use crate::util::{already_exists, invalid_path, not_found};
use crate::FileSystem;
use itertools::Itertools;
use parking_lot::Mutex;
use std::collections::{hash_map, HashMap};
use std::ffi::OsStr;
use std::path::Path;
use std::sync::Arc;
type File = Arc<Mutex<Vec<u8>>>;
#[derive(Default)]
pub struct MemoryFS {
inner: FilesystemTree<File>,
}
impl MemoryFS {
fn with_parent_and_child_name<R, P: AsRef<Path>, F: FnOnce(&mut Directory<File>, &str) -> R>(
&self,
path: P,
f: F,
) -> crate::Result<R> {
let parent_directory = path.as_ref().parent().ok_or_else(invalid_path)?;
let child_name = path
.as_ref()
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(invalid_path)?;
self.inner
.with_directory(parent_directory, |dir| f(dir, child_name))
}
}
impl FileSystem for MemoryFS {
fn create_dir(&self, path: &str) -> crate::Result<()> {
self.with_parent_and_child_name(path, |dir, directory_name| {
match dir.entry(directory_name.to_owned()) {
hash_map::Entry::Vacant(vac) => {
vac.insert(Entry::Directory(HashMap::default()));
Ok(())
}
_ => Err(already_exists()),
}
})?
}
fn metadata(&self, path: &str) -> crate::Result<Metadata> {
self.with_parent_and_child_name(path, |dir, file_name| match dir.get(file_name) {
Some(Entry::Directory(_)) => Ok(Metadata::directory()),
Some(Entry::UserData(file)) => Ok(Metadata::file(file.lock().len() as u64)),
None => Err(not_found()),
})?
}
fn open_file_options(
&self,
path: &str,
options: &OpenOptions,
) -> crate::Result<Box<dyn crate::File>> {
let mut file = self.with_parent_and_child_name(path, |dir, file_name| {
let file = match dir.entry(file_name.to_owned()) {
hash_map::Entry::Occupied(entry) => {
if let Entry::UserData(file) = entry.get() {
file.clone()
} else {
return Err(not_found());
}
}
hash_map::Entry::Vacant(vacant) => {
if options.create {
let file = File::new(Mutex::default());
vacant.insert(Entry::UserData(file.clone()));
file
} else {
return Err(not_found());
}
}
};
let mode = FileMode::from_options(options);
Ok(FileHandle::new(file, mode))
})??;
if options.truncate {
file.clear();
}
Ok(Box::new(file))
}
fn read_dir(
&self,
path: &str,
) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
self.inner.with_directory(path, |dir| {
let iter: Box<dyn Iterator<Item = crate::Result<DirEntry>>> = Box::new(
dir.iter()
.map(|(name, entry)| {
Ok(DirEntry {
path: name.into(),
metadata: entry.into(),
})
})
.collect_vec()
.into_iter(),
);
iter
})
}
fn remove_dir(&self, path: &str) -> crate::Result<()> {
self.with_parent_and_child_name(path, |parent, dir| match parent.entry(dir.to_owned()) {
hash_map::Entry::Occupied(occ) if matches!(occ.get(), Entry::Directory(_)) => {
occ.remove();
Ok(())
}
_ => Err(not_found()),
})?
}
fn remove_file(&self, path: &str) -> crate::Result<()> {
self.with_parent_and_child_name(path, |parent, dir| match parent.entry(dir.to_owned()) {
hash_map::Entry::Occupied(occ) if matches!(occ.get(), Entry::UserData(_)) => {
occ.remove();
Ok(())
}
_ => Err(not_found()),
})?
}
fn create_dir_all(&self, path: &str) -> crate::Result<()> {
self.inner.create_dir_all(path, |_| ())
}
}
#[cfg(test)]
mod test {
use crate::file::{FileType, Metadata};
use crate::memory_fs::MemoryFS;
use crate::FileSystem;
use std::collections::BTreeMap;
use std::io::Write;
fn memory_fs() -> MemoryFS {
let fs = MemoryFS::default();
write!(fs.create_file("file").unwrap(), "something interesting").unwrap();
fs.create_dir_all("folder/and/it/goes/deeper").unwrap();
write!(fs.create_file("folder/and/it/goes/desc").unwrap(), "goes").unwrap();
fs
}
fn read_directory(fs: &MemoryFS, dir: &str) -> BTreeMap<String, Metadata> {
fs.read_dir(dir)
.unwrap()
.map(|entry| {
let entry = entry.unwrap();
(entry.path.to_str().unwrap().to_owned(), entry.metadata)
})
.collect()
}
#[test]
fn create_file() {
memory_fs();
}
#[test]
fn metadata() {
let fs = memory_fs();
for name in ["file", "/file", "./file", "test/../file"] {
let md = fs.metadata(name).unwrap();
assert_eq!(md.file_type, FileType::File);
assert_eq!(md.len, 21);
}
for name in ["folder", "/folder", "./folder", "test/../folder"] {
let md = fs.metadata(name).unwrap();
assert_eq!(md.file_type, FileType::Directory);
assert_eq!(md.len, 0);
}
for name in [
"folder/and/it/goes/desc",
"/folder/and/it/goes/desc",
"./folder/and/it/goes/desc",
"test/../folder/and/it/goes/desc",
] {
let md = fs.metadata(name).unwrap();
assert_eq!(md.file_type, FileType::File);
assert_eq!(md.len, 4);
}
}
#[test]
fn read_dir() {
let fs = memory_fs();
for name in ["", "/", "./", "//", "\\"] {
let files = read_directory(&fs, name);
itertools::assert_equal(files.keys(), vec!["file", "folder"]);
itertools::assert_equal(
files.values(),
vec![&Metadata::file(21), &Metadata::directory()],
)
}
for name in [
"folder/and/it/goes",
"/folder/and/it/goes",
"./folder/and/it/goes/",
"///folder/and/it/goes///",
"\\folder\\and\\it\\goes\\",
] {
let files = read_directory(&fs, name);
itertools::assert_equal(files.keys(), vec!["deeper", "desc"]);
itertools::assert_equal(
files.values(),
vec![&Metadata::directory(), &Metadata::file(4)],
)
}
for name in [
"folder/and/../..",
"./folder/and/../..",
".//folder/and//../..",
"\\folder//../folder/and/../..",
] {
let files = read_directory(&fs, name);
itertools::assert_equal(files.keys(), vec!["file", "folder"]);
itertools::assert_equal(
files.values(),
vec![&Metadata::file(21), &Metadata::directory()],
)
}
}
#[test]
fn remove_dir() {
let fs = memory_fs();
assert!(fs.exists("folder/and/it/goes").unwrap());
fs.remove_dir("folder/and/it").unwrap();
assert!(!fs.exists("folder/and/it/goes").unwrap());
assert!(!fs.exists("/folder/and/it").unwrap());
assert!(!fs.exists("/folder/and/it/goes/desc").unwrap());
}
#[test]
fn remove_file() {
let fs = memory_fs();
assert!(fs.exists("folder/and/it/goes/desc").unwrap());
fs.remove_file("folder/and/it/goes/desc").unwrap();
assert!(fs.exists("folder/and/it/goes/deeper").unwrap());
assert!(!fs.exists("folder/and/it/goes/desc").unwrap());
}
}