virtual_filesystem/
mountable_fs.rs

1use 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/// A filesystem that supports the mounting of other filesystems at designated paths (excluding the root).
13#[derive(Default)]
14pub struct MountableFS {
15    inner: FilesystemTree<FS>,
16}
17
18impl MountableFS {
19    /// Mounts a filesystem at the given path.
20    ///
21    /// # Arguments
22    /// `path`: The path to mount the filesystem at.  
23    /// `fs`: The filesystem to mount.  
24    pub fn mount<P: AsRef<Path>>(
25        &self,
26        path: P,
27        fs: Box<dyn FileSystem + Send + Sync>,
28    ) -> crate::Result<()> {
29        // find the parent path
30        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        // create the parent path
38        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                        // the root directory of a filesystem is a directory
75                        Ok(Metadata::directory())
76                    } else {
77                        // `remaining_path` is derived from `path`, so this is safe
78                        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                    // `remaining_path` is derived from `path`, so this is safe
91                    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                    // we should have a directory
105                    let entries = dir
106                        .iter()
107                        .map(|(path, _)| {
108                            // filesystems and directories are both functionally directories
109                            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                    // `remaining_path` is derived from `path`, so this is safe
122                    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}