virtual_filesystem/
mountable_fs.rs1use crate::file::{DirEntry, File, Metadata, OpenOptions};
2use crate::tree::{normalize_and_relativize, Entry, FilesystemTree};
3use crate::util::{already_exists, invalid_path, not_found, not_supported};
4use crate::FileSystem;
5use itertools::Itertools;
6use std::collections::hash_map;
7use std::ffi::OsStr;
8use std::path::Path;
9
10type FS = Box<dyn FileSystem + Send + Sync>;
11
12#[derive(Default)]
14pub struct MountableFS {
15 inner: FilesystemTree<FS>,
16}
17
18impl MountableFS {
19 pub fn mount<P: AsRef<Path>>(
25 &self,
26 path: P,
27 fs: Box<dyn FileSystem + Send + Sync>,
28 ) -> crate::Result<()> {
29 let normalized_path = normalize_and_relativize(path);
31 let parent_path = normalized_path.parent().ok_or_else(invalid_path)?;
32 let child_path = normalized_path
33 .file_name()
34 .and_then(OsStr::to_str)
35 .ok_or_else(invalid_path)?;
36
37 self.inner.create_dir_all(parent_path, |dir| {
39 if let hash_map::Entry::Vacant(vac) = dir.entry(child_path.to_owned()) {
40 vac.insert(Entry::UserData(fs));
41 Ok(())
42 } else {
43 Err(already_exists())
44 }
45 })??;
46
47 Ok(())
48 }
49}
50
51impl<'a> FromIterator<(&'a str, Box<dyn FileSystem + Send + Sync>)> for MountableFS {
52 fn from_iter<T: IntoIterator<Item = (&'a str, Box<dyn FileSystem + Send + Sync>)>>(
53 iter: T,
54 ) -> Self {
55 let mountable_fs = Self::default();
56 for (path, fs) in iter {
57 mountable_fs.mount(path, fs).unwrap();
58 }
59 mountable_fs
60 }
61}
62
63impl FileSystem for MountableFS {
64 fn create_dir(&self, _path: &str) -> crate::Result<()> {
65 Err(not_supported())
66 }
67
68 fn metadata(&self, path: &str) -> crate::Result<Metadata> {
69 self.inner.with_entry(path, |maybe_directory| {
70 match maybe_directory {
71 Ok(_dir) => Ok(Metadata::directory()),
72 Err((fs, remaining_path)) => {
73 if remaining_path.as_os_str().is_empty() {
74 Ok(Metadata::directory())
76 } else {
77 fs.metadata(remaining_path.to_str().unwrap())
79 }
80 }
81 }
82 })
83 }
84
85 fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
86 self.inner.with_entry(path, |maybe_directory| {
87 maybe_directory
88 .err()
89 .map(|(fs, remaining_path)| {
90 fs.open_file_options(remaining_path.to_str().unwrap(), options)
92 })
93 .ok_or_else(not_found)
94 })?
95 }
96
97 fn read_dir(
98 &self,
99 path: &str,
100 ) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
101 self.inner
102 .with_entry(path, |maybe_entry| match maybe_entry {
103 Ok(dir) => {
104 let entries = dir
106 .iter()
107 .map(|(path, _)| {
108 Ok(DirEntry {
110 path: path.into(),
111 metadata: Metadata::directory(),
112 })
113 })
114 .collect_vec();
115
116 Ok::<Box<dyn Iterator<Item = crate::Result<DirEntry>>>, _>(Box::new(
117 entries.into_iter(),
118 ))
119 }
120 Err((fs, remaining_path)) => {
121 fs.read_dir(remaining_path.to_str().unwrap())
123 }
124 })
125 }
126
127 fn remove_dir(&self, _path: &str) -> crate::Result<()> {
128 Err(not_supported())
129 }
130
131 fn remove_file(&self, _path: &str) -> crate::Result<()> {
132 Err(not_supported())
133 }
134}
135
136#[cfg(test)]
137mod test {
138 use crate::file::Metadata;
139 use crate::memory_fs::MemoryFS;
140 use crate::mountable_fs::MountableFS;
141 use crate::util::test::read_directory;
142 use crate::{FileSystem, MockFileSystem};
143 use std::io::Write;
144
145 const TEST_PATHS: [&str; 4] = [
146 "test/abc",
147 "/test/abc",
148 "./test//abc",
149 "//test\\def//../abc",
150 ];
151
152 #[test]
153 fn mount() {
154 for mount_point in TEST_PATHS {
155 let fs = MountableFS::default();
156 assert!(!fs.exists("test/abc").unwrap());
157
158 fs.mount(mount_point, Box::new(MockFileSystem::new()))
159 .unwrap();
160 assert!(fs.exists("test/abc").unwrap());
161 }
162 }
163
164 #[test]
165 fn double_mount() {
166 for mount_point in TEST_PATHS {
167 let fs = MountableFS::default();
168 fs.mount(mount_point, Box::new(MockFileSystem::new()))
169 .unwrap();
170 assert!(fs
171 .mount(mount_point, Box::new(MockFileSystem::new()))
172 .is_err())
173 }
174 }
175
176 fn mounted_fs() -> MountableFS {
177 let fs = MountableFS::default();
178
179 let memory_fs = MemoryFS::default();
180 write!(memory_fs.create_file("abc").unwrap(), "file").unwrap();
181 memory_fs.create_dir_all("folder/and/it").unwrap();
182 fs.mount("test", Box::new(memory_fs)).unwrap();
183
184 fs
185 }
186
187 #[test]
188 fn metadata() {
189 let fs = mounted_fs();
190
191 for path in TEST_PATHS {
192 assert_eq!(fs.metadata(path).unwrap(), Metadata::file(4));
193 }
194
195 assert_eq!(fs.metadata("test/folder").unwrap(), Metadata::directory());
196 }
197
198 #[test]
199 fn open_file() {
200 let fs = mounted_fs();
201
202 for path in TEST_PATHS {
203 assert_eq!(
204 fs.open_file(path).unwrap().read_into_string().unwrap(),
205 "file"
206 );
207 }
208
209 assert!(fs.open_file("folder").is_err());
210 }
211
212 #[test]
213 fn read_dir() {
214 let fs = mounted_fs();
215
216 for path in ["/", "//", "", ".", "./", "test/something/else/../../../"] {
217 let dir = read_directory(&fs, path);
218 itertools::assert_equal(dir.keys(), vec!["test"]);
219 itertools::assert_equal(dir.values(), vec![&Metadata::directory()])
220 }
221
222 for path in ["/test", "./test/", "\\test/\\", "test/../test//"] {
223 let dir = read_directory(&fs, path);
224 itertools::assert_equal(dir.keys(), vec!["abc", "folder"]);
225 itertools::assert_equal(
226 dir.values(),
227 vec![&Metadata::file(4), &Metadata::directory()],
228 )
229 }
230 }
231
232 #[test]
233 fn exists() {
234 let fs = mounted_fs();
235
236 for path in ["/", "//", "", ".", "./", "test/something/else/../../../"] {
237 assert!(fs.exists(path).unwrap());
238 }
239
240 for path in TEST_PATHS {
241 assert!(fs.exists(path).unwrap());
242 }
243
244 assert!(!fs.exists("nonsense").unwrap());
245 assert!(!fs.exists("test/nonsense").unwrap());
246 assert!(fs.exists("test/folder").unwrap());
247 assert!(fs.exists("test/folder/and/").unwrap());
248 }
249}