virtual_filesystem/memory_fs/
mod.rs

1mod entry;
2mod file;
3
4use crate::file::{DirEntry, Metadata, OpenOptions};
5use crate::memory_fs::file::{FileHandle, FileMode};
6use crate::tree::{Directory, Entry, FilesystemTree};
7use crate::util::{already_exists, invalid_path, not_found};
8use crate::FileSystem;
9use itertools::Itertools;
10use parking_lot::Mutex;
11use std::collections::{hash_map, HashMap};
12use std::ffi::OsStr;
13use std::path::Path;
14use std::sync::Arc;
15
16/// A file within the memory filesystem.
17type File = Arc<Mutex<Vec<u8>>>;
18
19/// A memory-backed filesystem. All files are stored within.
20#[derive(Default)]
21pub struct MemoryFS {
22    inner: FilesystemTree<File>,
23}
24
25impl MemoryFS {
26    fn with_parent_and_child_name<R, P: AsRef<Path>, F: FnOnce(&mut Directory<File>, &str) -> R>(
27        &self,
28        path: P,
29        f: F,
30    ) -> crate::Result<R> {
31        let parent_directory = path.as_ref().parent().ok_or_else(invalid_path)?;
32        let child_name = path
33            .as_ref()
34            .file_name()
35            .and_then(OsStr::to_str)
36            .ok_or_else(invalid_path)?;
37
38        // fetch the parent directory and insert the new directory, if not already existent
39        self.inner
40            .with_directory(parent_directory, |dir| f(dir, child_name))
41    }
42}
43
44impl FileSystem for MemoryFS {
45    fn create_dir(&self, path: &str) -> crate::Result<()> {
46        // fetch the parent directory and insert the new directory, if not already existent
47        self.with_parent_and_child_name(path, |dir, directory_name| {
48            match dir.entry(directory_name.to_owned()) {
49                hash_map::Entry::Vacant(vac) => {
50                    vac.insert(Entry::Directory(HashMap::default()));
51                    Ok(())
52                }
53                _ => Err(already_exists()),
54            }
55        })?
56    }
57
58    fn metadata(&self, path: &str) -> crate::Result<Metadata> {
59        // fetch the parent directory, because the entry can either be a folder or file
60        self.with_parent_and_child_name(path, |dir, file_name| match dir.get(file_name) {
61            Some(Entry::Directory(_)) => Ok(Metadata::directory()),
62            Some(Entry::UserData(file)) => Ok(Metadata::file(file.lock().len() as u64)),
63            None => Err(not_found()),
64        })?
65    }
66
67    fn open_file_options(
68        &self,
69        path: &str,
70        options: &OpenOptions,
71    ) -> crate::Result<Box<dyn crate::File>> {
72        // grab the file
73        let mut file = self.with_parent_and_child_name(path, |dir, file_name| {
74            let file = match dir.entry(file_name.to_owned()) {
75                hash_map::Entry::Occupied(entry) => {
76                    // of course we can only grab the file if it's a file
77                    if let Entry::UserData(file) = entry.get() {
78                        file.clone()
79                    } else {
80                        return Err(not_found());
81                    }
82                }
83                hash_map::Entry::Vacant(vacant) => {
84                    if options.create {
85                        // create a new empty file and return it
86                        let file = File::new(Mutex::default());
87                        vacant.insert(Entry::UserData(file.clone()));
88                        file
89                    } else {
90                        return Err(not_found());
91                    }
92                }
93            };
94
95            let mode = FileMode::from_options(options);
96            Ok(FileHandle::new(file, mode))
97        })??;
98
99        // if we want to truncate the file, clear the contents
100        if options.truncate {
101            file.clear();
102        }
103
104        Ok(Box::new(file))
105    }
106
107    fn read_dir(
108        &self,
109        path: &str,
110    ) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
111        self.inner.with_directory(path, |dir| {
112            let iter: Box<dyn Iterator<Item = crate::Result<DirEntry>>> = Box::new(
113                dir.iter()
114                    .map(|(name, entry)| {
115                        Ok(DirEntry {
116                            path: name.into(),
117                            metadata: entry.into(),
118                        })
119                    })
120                    .collect_vec()
121                    .into_iter(),
122            );
123            iter
124        })
125    }
126
127    fn remove_dir(&self, path: &str) -> crate::Result<()> {
128        self.with_parent_and_child_name(path, |parent, dir| match parent.entry(dir.to_owned()) {
129            hash_map::Entry::Occupied(occ) if matches!(occ.get(), Entry::Directory(_)) => {
130                occ.remove();
131                Ok(())
132            }
133            _ => Err(not_found()),
134        })?
135    }
136
137    fn remove_file(&self, path: &str) -> crate::Result<()> {
138        self.with_parent_and_child_name(path, |parent, dir| match parent.entry(dir.to_owned()) {
139            hash_map::Entry::Occupied(occ) if matches!(occ.get(), Entry::UserData(_)) => {
140                occ.remove();
141                Ok(())
142            }
143            _ => Err(not_found()),
144        })?
145    }
146
147    fn create_dir_all(&self, path: &str) -> crate::Result<()> {
148        self.inner.create_dir_all(path, |_| ())
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use crate::file::{FileType, Metadata};
155    use crate::memory_fs::MemoryFS;
156    use crate::FileSystem;
157    use std::collections::BTreeMap;
158    use std::io::Write;
159
160    fn memory_fs() -> MemoryFS {
161        let fs = MemoryFS::default();
162
163        write!(fs.create_file("file").unwrap(), "something interesting").unwrap();
164        fs.create_dir_all("folder/and/it/goes/deeper").unwrap();
165        write!(fs.create_file("folder/and/it/goes/desc").unwrap(), "goes").unwrap();
166
167        fs
168    }
169
170    fn read_directory(fs: &MemoryFS, dir: &str) -> BTreeMap<String, Metadata> {
171        fs.read_dir(dir)
172            .unwrap()
173            .map(|entry| {
174                let entry = entry.unwrap();
175                (entry.path.to_str().unwrap().to_owned(), entry.metadata)
176            })
177            .collect()
178    }
179
180    #[test]
181    fn create_file() {
182        memory_fs();
183    }
184
185    #[test]
186    fn metadata() {
187        let fs = memory_fs();
188
189        // basic file
190        for name in ["file", "/file", "./file", "test/../file"] {
191            let md = fs.metadata(name).unwrap();
192            assert_eq!(md.file_type, FileType::File);
193            assert_eq!(md.len, 21);
194        }
195
196        // basic folder
197        for name in ["folder", "/folder", "./folder", "test/../folder"] {
198            let md = fs.metadata(name).unwrap();
199            assert_eq!(md.file_type, FileType::Directory);
200            assert_eq!(md.len, 0);
201        }
202
203        // nested file
204        for name in [
205            "folder/and/it/goes/desc",
206            "/folder/and/it/goes/desc",
207            "./folder/and/it/goes/desc",
208            "test/../folder/and/it/goes/desc",
209        ] {
210            let md = fs.metadata(name).unwrap();
211            assert_eq!(md.file_type, FileType::File);
212            assert_eq!(md.len, 4);
213        }
214    }
215
216    #[test]
217    fn read_dir() {
218        let fs = memory_fs();
219
220        // simple
221        for name in ["", "/", "./", "//", "\\"] {
222            let files = read_directory(&fs, name);
223            itertools::assert_equal(files.keys(), vec!["file", "folder"]);
224            itertools::assert_equal(
225                files.values(),
226                vec![&Metadata::file(21), &Metadata::directory()],
227            )
228        }
229
230        // nested
231        for name in [
232            "folder/and/it/goes",
233            "/folder/and/it/goes",
234            "./folder/and/it/goes/",
235            "///folder/and/it/goes///",
236            "\\folder\\and\\it\\goes\\",
237        ] {
238            let files = read_directory(&fs, name);
239            itertools::assert_equal(files.keys(), vec!["deeper", "desc"]);
240            itertools::assert_equal(
241                files.values(),
242                vec![&Metadata::directory(), &Metadata::file(4)],
243            )
244        }
245
246        // traversal
247        for name in [
248            "folder/and/../..",
249            "./folder/and/../..",
250            ".//folder/and//../..",
251            "\\folder//../folder/and/../..",
252        ] {
253            let files = read_directory(&fs, name);
254            itertools::assert_equal(files.keys(), vec!["file", "folder"]);
255            itertools::assert_equal(
256                files.values(),
257                vec![&Metadata::file(21), &Metadata::directory()],
258            )
259        }
260    }
261
262    #[test]
263    fn remove_dir() {
264        let fs = memory_fs();
265
266        assert!(fs.exists("folder/and/it/goes").unwrap());
267        fs.remove_dir("folder/and/it").unwrap();
268        assert!(!fs.exists("folder/and/it/goes").unwrap());
269        assert!(!fs.exists("/folder/and/it").unwrap());
270        assert!(!fs.exists("/folder/and/it/goes/desc").unwrap());
271    }
272
273    #[test]
274    fn remove_file() {
275        let fs = memory_fs();
276
277        assert!(fs.exists("folder/and/it/goes/desc").unwrap());
278        fs.remove_file("folder/and/it/goes/desc").unwrap();
279        assert!(fs.exists("folder/and/it/goes/deeper").unwrap());
280        assert!(!fs.exists("folder/and/it/goes/desc").unwrap());
281    }
282}