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)]
16pub 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 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 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 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 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}