Skip to main content

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.keys().map(|path| {
106                            // filesystems and directories are both functionally directories
107                            Ok(DirEntry {
108                                path: path.into(),
109                                metadata: Metadata::directory(),
110                            })
111                        })
112                        .collect_vec();
113
114                    Ok::<Box<dyn Iterator<Item = crate::Result<DirEntry>>>, _>(Box::new(
115                        entries.into_iter(),
116                    ))
117                }
118                Err((fs, remaining_path)) => {
119                    // `remaining_path` is derived from `path`, so this is safe
120                    fs.read_dir(remaining_path.to_str().unwrap())
121                }
122            })
123    }
124
125    fn remove_dir(&self, _path: &str) -> crate::Result<()> {
126        Err(not_supported())
127    }
128
129    fn remove_file(&self, _path: &str) -> crate::Result<()> {
130        Err(not_supported())
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use crate::file::Metadata;
137    use crate::memory_fs::MemoryFS;
138    use crate::mountable_fs::MountableFS;
139    use crate::util::test::read_directory;
140    use crate::{FileSystem, MockFileSystem};
141    use std::io::Write;
142
143    const TEST_PATHS: [&str; 4] = [
144        "test/abc",
145        "/test/abc",
146        "./test//abc",
147        "//test\\def//../abc",
148    ];
149
150    #[test]
151    fn mount() {
152        for mount_point in TEST_PATHS {
153            let fs = MountableFS::default();
154            assert!(!fs.exists("test/abc").unwrap());
155
156            fs.mount(mount_point, Box::new(MockFileSystem::new()))
157                .unwrap();
158            assert!(fs.exists("test/abc").unwrap());
159        }
160    }
161
162    #[test]
163    fn double_mount() {
164        for mount_point in TEST_PATHS {
165            let fs = MountableFS::default();
166            fs.mount(mount_point, Box::new(MockFileSystem::new()))
167                .unwrap();
168            assert!(fs
169                .mount(mount_point, Box::new(MockFileSystem::new()))
170                .is_err())
171        }
172    }
173
174    fn mounted_fs() -> MountableFS {
175        let fs = MountableFS::default();
176
177        let memory_fs = MemoryFS::default();
178        write!(memory_fs.create_file("abc").unwrap(), "file").unwrap();
179        memory_fs.create_dir_all("folder/and/it").unwrap();
180        fs.mount("test", Box::new(memory_fs)).unwrap();
181
182        fs
183    }
184
185    #[test]
186    fn metadata() {
187        let fs = mounted_fs();
188
189        for path in TEST_PATHS {
190            assert_eq!(fs.metadata(path).unwrap(), Metadata::file(4));
191        }
192
193        assert_eq!(fs.metadata("test/folder").unwrap(), Metadata::directory());
194    }
195
196    #[test]
197    fn open_file() {
198        let fs = mounted_fs();
199
200        for path in TEST_PATHS {
201            assert_eq!(
202                fs.open_file(path).unwrap().read_into_string().unwrap(),
203                "file"
204            );
205        }
206
207        assert!(fs.open_file("folder").is_err());
208    }
209
210    #[test]
211    fn read_dir() {
212        let fs = mounted_fs();
213
214        for path in ["/", "//", "", ".", "./", "test/something/else/../../../"] {
215            let dir = read_directory(&fs, path);
216            itertools::assert_equal(dir.keys(), vec!["test"]);
217            itertools::assert_equal(dir.values(), vec![&Metadata::directory()])
218        }
219
220        for path in ["/test", "./test/", "\\test/\\", "test/../test//"] {
221            let dir = read_directory(&fs, path);
222            itertools::assert_equal(dir.keys(), vec!["abc", "folder"]);
223            itertools::assert_equal(
224                dir.values(),
225                vec![&Metadata::file(4), &Metadata::directory()],
226            )
227        }
228    }
229
230    #[test]
231    fn exists() {
232        let fs = mounted_fs();
233
234        for path in ["/", "//", "", ".", "./", "test/something/else/../../../"] {
235            assert!(fs.exists(path).unwrap());
236        }
237
238        for path in TEST_PATHS {
239            assert!(fs.exists(path).unwrap());
240        }
241
242        assert!(!fs.exists("nonsense").unwrap());
243        assert!(!fs.exists("test/nonsense").unwrap());
244        assert!(fs.exists("test/folder").unwrap());
245        assert!(fs.exists("test/folder/and/").unwrap());
246    }
247}