vfs/impls/
embedded.rs

1use std::borrow::Cow;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Debug;
4use std::io::Cursor;
5use std::marker::PhantomData;
6use std::time::{Duration, SystemTime};
7
8use rust_embed::RustEmbed;
9
10use crate::error::VfsErrorKind;
11use crate::{FileSystem, SeekAndRead, SeekAndWrite, VfsFileType, VfsMetadata, VfsResult};
12
13type EmbeddedPath = Cow<'static, str>;
14
15#[derive(Debug)]
16/// a read-only file system embedded in the executable
17/// see [rust-embed](https://docs.rs/rust-embed/) for how to create a `RustEmbed`
18pub struct EmbeddedFS<T>
19where
20    T: RustEmbed + Send + Sync + Debug + 'static,
21{
22    p: PhantomData<T>,
23    directory_map: HashMap<EmbeddedPath, HashSet<EmbeddedPath>>,
24    files: HashMap<EmbeddedPath, u64>,
25}
26
27impl<T> EmbeddedFS<T>
28where
29    T: RustEmbed + Send + Sync + Debug + 'static,
30{
31    pub fn new() -> Self {
32        let mut directory_map: HashMap<EmbeddedPath, HashSet<EmbeddedPath>> = Default::default();
33        let mut files: HashMap<EmbeddedPath, u64> = Default::default();
34        for file in T::iter() {
35            let mut path = file.clone();
36            files.insert(
37                file.clone(),
38                T::get(&path).expect("Path should exist").data.len() as u64,
39            );
40            while let Some((prefix, suffix)) = rsplit_once_cow(&path, "/") {
41                let children = directory_map.entry(prefix.clone()).or_default();
42                children.insert(suffix);
43                path = prefix;
44            }
45            let children = directory_map.entry("".into()).or_default();
46            children.insert(path);
47        }
48        EmbeddedFS {
49            p: PhantomData,
50            directory_map,
51            files,
52        }
53    }
54}
55
56impl<T> Default for EmbeddedFS<T>
57where
58    T: RustEmbed + Send + Sync + Debug + 'static,
59{
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65fn rsplit_once_cow(input: &EmbeddedPath, delimiter: &str) -> Option<(EmbeddedPath, EmbeddedPath)> {
66    let mut result: Vec<_> = match input {
67        EmbeddedPath::Borrowed(s) => s.rsplitn(2, delimiter).map(Cow::Borrowed).collect(),
68        EmbeddedPath::Owned(s) => s
69            .rsplitn(2, delimiter)
70            .map(|a| Cow::Owned(a.to_string()))
71            .collect(),
72    };
73    if result.len() == 2 {
74        Some((result.remove(1), result.remove(0)))
75    } else {
76        None
77    }
78}
79
80impl<T> FileSystem for EmbeddedFS<T>
81where
82    T: RustEmbed + Send + Sync + Debug + 'static,
83{
84    fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
85        let normalized_path = normalize_path(path)?;
86        if let Some(children) = self.directory_map.get(normalized_path) {
87            Ok(Box::new(
88                children.clone().into_iter().map(|path| path.into_owned()),
89            ))
90        } else {
91            if self.files.contains_key(normalized_path) {
92                // Actually a file
93                return Err(VfsErrorKind::Other("Not a directory".into()).into());
94            }
95            Err(VfsErrorKind::FileNotFound.into())
96        }
97    }
98
99    fn create_dir(&self, _path: &str) -> VfsResult<()> {
100        Err(VfsErrorKind::NotSupported.into())
101    }
102
103    fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
104        match T::get(path.split_at(1).1) {
105            None => Err(VfsErrorKind::FileNotFound.into()),
106            Some(file) => Ok(Box::new(Cursor::new(file.data))),
107        }
108    }
109
110    fn create_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
111        Err(VfsErrorKind::NotSupported.into())
112    }
113
114    fn append_file(&self, _path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
115        Err(VfsErrorKind::NotSupported.into())
116    }
117
118    fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
119        let normalized_path = normalize_path(path)?;
120        if let Some(len) = self.files.get(normalized_path) {
121            return match T::get(path.split_at(1).1) {
122                None => Err(VfsErrorKind::FileNotFound.into()),
123                Some(file) => Ok(VfsMetadata {
124                    file_type: VfsFileType::File,
125                    len: *len,
126                    modified: file
127                        .metadata
128                        .last_modified()
129                        .map(|secs| SystemTime::UNIX_EPOCH + Duration::from_secs(secs)),
130                    created: file
131                        .metadata
132                        .created()
133                        .map(|secs| SystemTime::UNIX_EPOCH + Duration::from_secs(secs)),
134                    accessed: None,
135                }),
136            };
137        }
138        if self.directory_map.contains_key(normalized_path) {
139            return Ok(VfsMetadata {
140                file_type: VfsFileType::Directory,
141                len: 0,
142                modified: None,
143                created: None,
144                accessed: None,
145            });
146        }
147        Err(VfsErrorKind::FileNotFound.into())
148    }
149
150    fn exists(&self, path: &str) -> VfsResult<bool> {
151        let path = normalize_path(path)?;
152        if self.files.contains_key(path) {
153            return Ok(true);
154        }
155        if self.directory_map.contains_key(path) {
156            return Ok(true);
157        }
158        if path.is_empty() {
159            // Root always exists
160            return Ok(true);
161        }
162        Ok(false)
163    }
164
165    fn remove_file(&self, _path: &str) -> VfsResult<()> {
166        Err(VfsErrorKind::NotSupported.into())
167    }
168
169    fn remove_dir(&self, _path: &str) -> VfsResult<()> {
170        Err(VfsErrorKind::NotSupported.into())
171    }
172}
173
174fn normalize_path(path: &str) -> VfsResult<&str> {
175    if path.is_empty() {
176        return Ok("");
177    }
178    let path = &path[1..];
179    Ok(path)
180}
181
182#[cfg(test)]
183mod tests {
184    use std::collections::HashSet;
185    use std::io::Read;
186
187    use crate::{FileSystem, VfsFileType, VfsPath};
188
189    use super::*;
190
191    #[derive(RustEmbed, Debug)]
192    #[folder = "test/test_directory"]
193    struct TestEmbed;
194
195    fn get_test_fs() -> EmbeddedFS<TestEmbed> {
196        EmbeddedFS::new()
197    }
198
199    test_vfs_readonly!({ get_test_fs() });
200    #[test]
201    fn read_dir_lists_directory() {
202        let fs = get_test_fs();
203        assert_eq!(
204            fs.read_dir("/").unwrap().collect::<HashSet<_>>(),
205            vec!["a", "a.txt.dir", "c", "a.txt", "b.txt"]
206                .into_iter()
207                .map(String::from)
208                .collect::<HashSet<_>>()
209        );
210        assert_eq!(
211            fs.read_dir("/a").unwrap().collect::<HashSet<_>>(),
212            vec!["d.txt", "x"]
213                .into_iter()
214                .map(String::from)
215                .collect::<HashSet<_>>()
216        );
217        assert_eq!(
218            fs.read_dir("/a.txt.dir").unwrap().collect::<HashSet<_>>(),
219            vec!["g.txt"]
220                .into_iter()
221                .map(String::from)
222                .collect::<HashSet<_>>()
223        );
224    }
225
226    #[test]
227    fn read_dir_no_directory_err() {
228        let fs = get_test_fs();
229        assert!(match fs.read_dir("/c/f").map(|_| ()).unwrap_err().kind() {
230            VfsErrorKind::FileNotFound => true,
231            _ => false,
232        });
233        assert!(
234            match fs.read_dir("/a.txt.").map(|_| ()).unwrap_err().kind() {
235                VfsErrorKind::FileNotFound => true,
236                _ => false,
237            }
238        );
239        assert!(
240            match fs.read_dir("/abc/def/ghi").map(|_| ()).unwrap_err().kind() {
241                VfsErrorKind::FileNotFound => true,
242                _ => false,
243            }
244        );
245    }
246
247    #[test]
248    fn read_dir_on_file_err() {
249        let fs = get_test_fs();
250        assert!(
251            match fs.read_dir("/a.txt").map(|_| ()).unwrap_err().kind() {
252                VfsErrorKind::Other(message) => message == "Not a directory",
253                _ => false,
254            }
255        );
256        assert!(
257            match fs.read_dir("/a/d.txt").map(|_| ()).unwrap_err().kind() {
258                VfsErrorKind::Other(message) => message == "Not a directory",
259                _ => false,
260            }
261        );
262    }
263
264    #[test]
265    fn create_dir_not_supported() {
266        let fs = get_test_fs();
267        assert!(
268            match fs.create_dir("/abc").map(|_| ()).unwrap_err().kind() {
269                VfsErrorKind::NotSupported => true,
270                _ => false,
271            }
272        )
273    }
274
275    #[test]
276    fn open_file() {
277        let fs = get_test_fs();
278        let mut text = String::new();
279        fs.open_file("/a.txt")
280            .unwrap()
281            .read_to_string(&mut text)
282            .unwrap();
283        assert_eq!(text, "a");
284    }
285
286    #[test]
287    fn open_empty_file() {
288        let fs = get_test_fs();
289        let mut text = String::new();
290        fs.open_file("/a.txt.dir/g.txt")
291            .unwrap()
292            .read_to_string(&mut text)
293            .unwrap();
294        assert_eq!(text, "");
295    }
296
297    #[test]
298    fn open_file_not_found() {
299        let fs = get_test_fs();
300        // FIXME: These tests have been weakened since the FS implementations aren't intended to
301        //      provide paths for errors. Maybe this could be handled better
302        assert!(match fs.open_file("/") {
303            Err(err) => match err.kind() {
304                VfsErrorKind::FileNotFound => true,
305                _ => false,
306            },
307            _ => false,
308        });
309        assert!(match fs.open_file("/abc.txt") {
310            Err(err) => match err.kind() {
311                VfsErrorKind::FileNotFound => true,
312                _ => false,
313            },
314            _ => false,
315        });
316        assert!(match fs.open_file("/c/f.txt") {
317            Err(err) => match err.kind() {
318                VfsErrorKind::FileNotFound => true,
319                _ => false,
320            },
321            _ => false,
322        });
323    }
324
325    #[test]
326    fn create_file_not_supported() {
327        let fs = get_test_fs();
328        assert!(
329            match fs.create_file("/abc.txt").map(|_| ()).unwrap_err().kind() {
330                VfsErrorKind::NotSupported => true,
331                _ => false,
332            }
333        );
334    }
335
336    #[test]
337    fn append_file_not_supported() {
338        let fs = get_test_fs();
339        assert!(
340            match fs.append_file("/abc.txt").map(|_| ()).unwrap_err().kind() {
341                VfsErrorKind::NotSupported => true,
342                _ => false,
343            }
344        );
345    }
346
347    #[test]
348    fn metadata_file() {
349        let fs = get_test_fs();
350        let d = fs.metadata("/a/d.txt").unwrap();
351        assert_eq!(d.len, 1);
352        assert_eq!(d.file_type, VfsFileType::File);
353
354        let g = fs.metadata("/a.txt.dir/g.txt").unwrap();
355        assert_eq!(g.len, 0);
356        assert_eq!(g.file_type, VfsFileType::File);
357    }
358
359    #[test]
360    fn metadata_directory() {
361        let fs = get_test_fs();
362        let root = fs.metadata("/").unwrap();
363        assert_eq!(root.len, 0);
364        assert_eq!(root.file_type, VfsFileType::Directory);
365
366        // The empty path is treated as root
367        let root = fs.metadata("").unwrap();
368        assert_eq!(root.len, 0);
369        assert_eq!(root.file_type, VfsFileType::Directory);
370
371        let a = fs.metadata("/a").unwrap();
372        assert_eq!(a.len, 0);
373        assert_eq!(a.file_type, VfsFileType::Directory);
374    }
375
376    #[test]
377    fn metadata_not_found() {
378        let fs = get_test_fs();
379        assert!(match fs.metadata("/abc.txt") {
380            Err(err) => match err.kind() {
381                VfsErrorKind::FileNotFound => true,
382                _ => false,
383            },
384            _ => false,
385        });
386    }
387
388    #[test]
389    fn exists() {
390        let fs = get_test_fs();
391        assert!(fs.exists("").unwrap());
392        assert!(fs.exists("/a").unwrap());
393        assert!(fs.exists("/a/d.txt").unwrap());
394        assert!(fs.exists("/a.txt.dir").unwrap());
395        assert!(fs.exists("/a.txt.dir/g.txt").unwrap());
396        assert!(fs.exists("/c").unwrap());
397        assert!(fs.exists("/c/e.txt").unwrap());
398        assert!(fs.exists("/a.txt").unwrap());
399        assert!(fs.exists("/b.txt").unwrap());
400
401        assert!(!fs.exists("/abc").unwrap());
402        assert!(!fs.exists("/a.txt.").unwrap());
403    }
404
405    #[test]
406    fn remove_file_not_supported() {
407        let fs = get_test_fs();
408        assert!(
409            match fs.remove_file("/abc.txt").map(|_| ()).unwrap_err().kind() {
410                VfsErrorKind::NotSupported => true,
411                _ => false,
412            }
413        );
414    }
415
416    #[test]
417    fn remove_dir_not_supported() {
418        let fs = get_test_fs();
419        assert!(
420            match fs.remove_dir("/abc.txt").map(|_| ()).unwrap_err().kind() {
421                VfsErrorKind::NotSupported => true,
422                _ => false,
423            }
424        );
425    }
426
427    #[test]
428    fn integration() {
429        let root: VfsPath = get_test_fs().into();
430        let a_file = root.join("a.txt").unwrap();
431        assert!(a_file.exists().unwrap());
432        let mut text = String::new();
433        a_file
434            .open_file()
435            .unwrap()
436            .read_to_string(&mut text)
437            .unwrap();
438        assert_eq!(text.as_str(), "a");
439        assert_eq!(a_file.filename(), String::from("a.txt"));
440
441        text.clear();
442        root.join("a")
443            .unwrap()
444            .join("d.txt")
445            .unwrap()
446            .open_file()
447            .unwrap()
448            .read_to_string(&mut text)
449            .unwrap();
450        assert_eq!(text, String::from("d"));
451
452        assert!(root.join("a.txt.dir").unwrap().exists().unwrap());
453        assert!(!root.join("g").unwrap().exists().unwrap());
454    }
455}