Skip to main content

virtual_fs/
mount_fs.rs

1//! A mount-topology filesystem that routes operations by path,
2//! its not as simple as TmpFs. not currently used but was used by
3//! the previously implementation of Deploy - now using TmpFs
4
5use crate::*;
6
7use std::{
8    borrow::Cow,
9    collections::{BTreeMap, BTreeSet},
10    ffi::OsString,
11    path::{Path, PathBuf},
12    sync::{Arc, RwLock},
13    time::{SystemTime, UNIX_EPOCH},
14};
15
16const MIN_METADATA_TIMESTAMP: u64 = 1_000_000_000; // 1 second in nano seconds
17
18type DynFileSystem = Arc<dyn FileSystem + Send + Sync>;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ExactMountConflictMode {
22    Fail,
23    KeepExisting,
24    ReplaceExisting,
25}
26
27#[derive(Debug, Clone)]
28struct MountedFileSystem {
29    fs: DynFileSystem,
30    source_path: PathBuf,
31}
32
33#[derive(Debug, Default)]
34struct MountNode {
35    /// Creation timestamp in nanoseconds since the Unix epoch.
36    /// Set once when the node is first inserted into the tree.
37    created_at: u64,
38    mount: Option<MountedFileSystem>,
39    children: BTreeMap<OsString, MountNode>,
40}
41
42#[derive(Debug, Clone)]
43struct ExactNode {
44    path: PathBuf,
45    fs: Option<DynFileSystem>,
46    source_path: PathBuf,
47    child_names: BTreeSet<OsString>,
48    /// Timestamp reported for this node in nanoseconds since the Unix epoch.
49    /// For synthetic non-mounted nodes, this may be generated at lookup time
50    /// rather than representing an original creation event.
51    created_at: u64,
52}
53
54impl ExactNode {
55    fn has_children(&self) -> bool {
56        !self.child_names.is_empty()
57    }
58}
59
60#[derive(Debug, Clone)]
61struct ResolvedMount {
62    mount_path: PathBuf,
63    delegated_path: PathBuf,
64    fs: DynFileSystem,
65}
66
67#[derive(Debug, Clone)]
68pub struct MountPoint {
69    pub path: PathBuf,
70    pub name: String,
71    pub fs: Option<DynFileSystem>,
72    pub children: Option<Arc<MountFileSystem>>,
73}
74
75#[derive(Debug, Clone)]
76pub struct MountEntry {
77    pub path: PathBuf,
78    pub fs: DynFileSystem,
79    pub source_path: PathBuf,
80}
81
82impl MountPoint {
83    pub fn fs(&self) -> Option<&(dyn FileSystem + Send + Sync)> {
84        self.fs.as_deref()
85    }
86
87    pub fn mount_point_ref(&self) -> MountPointRef<'_> {
88        MountPointRef {
89            path: self.path.clone(),
90            name: self.name.clone(),
91            fs: self.fs.as_deref(),
92        }
93    }
94}
95
96/// Allows different filesystems of different types
97/// to be mounted at various mount points
98#[derive(Debug)]
99pub struct MountFileSystem {
100    root: RwLock<MountNode>,
101}
102
103impl Default for MountFileSystem {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl MountFileSystem {
110    pub fn new() -> Self {
111        let ts = Self::now_nanos();
112        Self {
113            root: RwLock::new(MountNode {
114                created_at: ts,
115                ..MountNode::default()
116            }),
117        }
118    }
119
120    pub fn mount(
121        &self,
122        path: impl AsRef<Path>,
123        fs: Arc<dyn FileSystem + Send + Sync>,
124    ) -> Result<()> {
125        self.mount_with_source(path, Path::new("/"), fs)
126    }
127
128    pub fn mount_with_source(
129        &self,
130        path: impl AsRef<Path>,
131        source_path: impl AsRef<Path>,
132        fs: Arc<dyn FileSystem + Send + Sync>,
133    ) -> Result<()> {
134        let path = self.prepare_path(path.as_ref())?;
135        let source_path = Self::normalize_source_path(source_path.as_ref());
136        let ts = Self::now_nanos();
137        let mut root = self.root.write().unwrap();
138        let node = Self::mount_node_mut(&mut root, &Self::path_components(&path), ts);
139
140        if node.mount.is_some() {
141            Err(FsError::AlreadyExists)
142        } else {
143            node.mount = Some(MountedFileSystem { fs, source_path });
144            Ok(())
145        }
146    }
147
148    pub fn filesystem_at(
149        &self,
150        path: impl AsRef<Path>,
151    ) -> Option<Arc<dyn FileSystem + Send + Sync>> {
152        self.exact_node(path.as_ref()).and_then(|node| node.fs)
153    }
154
155    pub fn clear(&mut self) {
156        *self.root.write().unwrap() = MountNode {
157            created_at: Self::now_nanos(),
158            ..MountNode::default()
159        };
160    }
161
162    fn prepare_path(&self, path: &Path) -> Result<PathBuf> {
163        let mut normalized = PathBuf::new();
164
165        for component in path.components() {
166            match component {
167                std::path::Component::RootDir | std::path::Component::CurDir => {}
168                std::path::Component::ParentDir => {
169                    if !normalized.pop() {
170                        return Err(FsError::InvalidInput);
171                    }
172                }
173                std::path::Component::Normal(part) => normalized.push(part),
174                std::path::Component::Prefix(_) => return Err(FsError::InvalidInput),
175            }
176        }
177
178        Ok(normalized)
179    }
180
181    fn path_components(path: &Path) -> Vec<OsString> {
182        path.components()
183            .map(|component| component.as_os_str().to_os_string())
184            .collect()
185    }
186
187    fn absolute_path(components: &[OsString]) -> PathBuf {
188        let mut path = PathBuf::from("/");
189        for component in components {
190            path.push(component);
191        }
192        path
193    }
194
195    fn normalize_source_path(path: &Path) -> PathBuf {
196        let mut normalized = PathBuf::from("/");
197        normalized.push(path.strip_prefix("/").unwrap_or(path));
198        normalized
199    }
200
201    fn now_nanos() -> u64 {
202        SystemTime::now()
203            .duration_since(UNIX_EPOCH)
204            .map_or(0, |d| d.as_nanos() as u64)
205            .max(MIN_METADATA_TIMESTAMP)
206    }
207
208    fn directory_metadata_at(ts: u64) -> Metadata {
209        Metadata {
210            ft: FileType::new_dir(),
211            accessed: ts,
212            created: ts,
213            modified: ts,
214            len: 0,
215        }
216    }
217
218    fn should_fallback_to_synthetic_dir(error: &FsError) -> bool {
219        matches!(
220            error,
221            FsError::Unsupported | FsError::NotAFile | FsError::BaseNotDirectory
222        )
223    }
224
225    fn synthetic_entry(name: OsString, base: &Path, ts: u64) -> DirEntry {
226        DirEntry {
227            path: base.join(PathBuf::from(name)),
228            metadata: Ok(Self::directory_metadata_at(ts)),
229        }
230    }
231
232    fn mounted(node: &MountNode) -> Option<MountedFileSystem> {
233        node.mount.clone()
234    }
235
236    fn collect_mount_entries(node: &MountNode, path: &Path, entries: &mut Vec<MountEntry>) {
237        if let Some(mount) = Self::mounted(node) {
238            entries.push(MountEntry {
239                path: path.to_path_buf(),
240                fs: mount.fs,
241                source_path: mount.source_path,
242            });
243        }
244
245        for (child_name, child) in &node.children {
246            let child_path = path.join(child_name);
247            Self::collect_mount_entries(child, &child_path, entries);
248        }
249    }
250
251    fn find_node<'a>(node: &'a MountNode, components: &[OsString]) -> Option<&'a MountNode> {
252        let mut node = node;
253        for component in components {
254            node = node.children.get(component)?;
255        }
256        Some(node)
257    }
258
259    fn exact_node(&self, path: &Path) -> Option<ExactNode> {
260        let path = self.prepare_path(path).ok()?;
261        let components = Self::path_components(&path);
262        let visible_path = Path::new("/").join(&path);
263        let root = self.root.read().unwrap();
264        let node = Self::find_node(&root, &components)?;
265        let mounted = Self::mounted(node);
266
267        Some(ExactNode {
268            path: visible_path.clone(),
269            fs: mounted.as_ref().map(|mount| mount.fs.clone()),
270            created_at: node.created_at,
271            source_path: mounted
272                .map(|mount| mount.source_path)
273                .unwrap_or_else(|| PathBuf::from("/")),
274            child_names: node.children.keys().cloned().collect(),
275        })
276    }
277
278    fn resolve_mount(&self, path: impl AsRef<Path>) -> Option<ResolvedMount> {
279        let path = self.prepare_path(path.as_ref()).ok()?;
280        let components = Self::path_components(&path);
281        let root = self.root.read().unwrap();
282        let mut node = &*root;
283        let mut best = Self::mounted(node).map(|mount| ResolvedMount {
284            mount_path: PathBuf::from("/"),
285            delegated_path: mount.source_path.join(
286                Self::absolute_path(&components)
287                    .strip_prefix("/")
288                    .unwrap_or(Path::new("")),
289            ),
290            fs: mount.fs,
291        });
292
293        for (index, component) in components.iter().enumerate() {
294            let Some(child) = node.children.get(component) else {
295                break;
296            };
297            node = child;
298
299            if let Some(mount) = Self::mounted(node) {
300                best = Some(ResolvedMount {
301                    mount_path: Self::absolute_path(&components[..=index]),
302                    delegated_path: mount.source_path.join(
303                        Self::absolute_path(&components[index + 1..])
304                            .strip_prefix("/")
305                            .unwrap_or(Path::new("")),
306                    ),
307                    fs: mount.fs,
308                });
309            }
310        }
311
312        best
313    }
314
315    /// Creates, in the nearest enclosing mount, the directories that are only
316    /// visible because a deeper mount sits below them (e.g. `/usr` for a mount
317    /// at `/usr/local/ssl`). Without this, such a directory stats as a
318    /// directory but nothing can be created inside it. Best-effort: errors are
319    /// left for the caller's own operation to report.
320    fn back_synthetic_dirs(&self, components: &[OsString]) {
321        let mut pending = Vec::new();
322        {
323            let root = self.root.read().unwrap();
324            let mut node = &*root;
325            let mut mount = Self::mounted(node);
326            let mut mount_depth = 0;
327
328            for (index, component) in components.iter().enumerate() {
329                let Some(child) = node.children.get(component) else {
330                    break;
331                };
332                node = child;
333
334                if let Some(child_mount) = Self::mounted(node) {
335                    mount = Some(child_mount);
336                    mount_depth = index + 1;
337                    pending.clear();
338                } else if let Some(mount) = &mount {
339                    let relative = Self::absolute_path(&components[mount_depth..=index]);
340                    let relative = relative.strip_prefix("/").unwrap_or(Path::new(""));
341                    pending.push((mount.fs.clone(), mount.source_path.join(relative)));
342                }
343            }
344        }
345
346        for (fs, path) in pending {
347            let _ = fs.create_dir(&path);
348        }
349    }
350
351    fn back_synthetic_parents(&self, path: &Path) {
352        let components = Self::path_components(path);
353        if let Some((_, parents)) = components.split_last() {
354            self.back_synthetic_dirs(parents);
355        }
356    }
357
358    fn rebase_entries(entries: &mut ReadDir, source_prefix: &Path, target_prefix: &Path) {
359        for entry in &mut entries.data {
360            let suffix = entry.path.strip_prefix(source_prefix).unwrap_or_else(|_| {
361                entry
362                    .path
363                    .strip_prefix(Path::new("/"))
364                    .unwrap_or(&entry.path)
365            });
366            entry.path = target_prefix.join(suffix);
367        }
368    }
369
370    fn read_dir_from_exact_node(&self, node: &ExactNode) -> Result<ReadDir> {
371        let mut entries = Vec::new();
372
373        let backing = if let Some(fs) = &node.fs {
374            Some((
375                fs.read_dir(&node.source_path),
376                Cow::Borrowed(node.source_path.as_path()),
377            ))
378        } else {
379            self.resolve_mount(&node.path).map(|resolved| {
380                (
381                    resolved.fs.read_dir(&resolved.delegated_path),
382                    Cow::Owned(resolved.delegated_path),
383                )
384            })
385        };
386
387        if let Some((base_entries, source_path)) = backing {
388            match base_entries {
389                Ok(mut base_entries) => {
390                    Self::rebase_entries(&mut base_entries, &source_path, &node.path);
391                    entries.extend(base_entries.data.into_iter().filter(|entry| {
392                        entry
393                            .path
394                            .file_name()
395                            .map(|name| !node.child_names.contains(name))
396                            .unwrap_or(true)
397                    }));
398                }
399                Err(FsError::EntryNotFound) if node.has_children() => {}
400                Err(error)
401                    if node.has_children() && Self::should_fallback_to_synthetic_dir(&error) => {}
402                Err(error) => return Err(error),
403            }
404        }
405
406        entries.extend(
407            node.child_names
408                .iter()
409                .cloned()
410                .map(|name| Self::synthetic_entry(name, &node.path, node.created_at)),
411        );
412
413        Ok(ReadDir::new(entries))
414    }
415
416    fn mount_node_mut<'a>(
417        node: &'a mut MountNode,
418        components: &[OsString],
419        ts: u64,
420    ) -> &'a mut MountNode {
421        let mut node = node;
422        for component in components {
423            node = node
424                .children
425                .entry(component.clone())
426                .or_insert_with(|| MountNode {
427                    created_at: ts,
428                    ..MountNode::default()
429                });
430        }
431
432        node
433    }
434
435    fn clear_descendants(node: &mut MountNode) {
436        node.children.clear();
437    }
438
439    /// Overwrite the mount at `path`.
440    pub fn set_mount(
441        &self,
442        path: impl AsRef<Path>,
443        fs: Arc<dyn FileSystem + Send + Sync>,
444    ) -> Result<()> {
445        let path = self.prepare_path(path.as_ref())?;
446        let ts = Self::now_nanos();
447        let mut root = self.root.write().unwrap();
448        let node = Self::mount_node_mut(&mut root, &Self::path_components(&path), ts);
449        node.mount = Some(MountedFileSystem {
450            fs,
451            source_path: PathBuf::from("/"),
452        });
453        Ok(())
454    }
455
456    pub fn add_mount_entries_with_mode(
457        &self,
458        entries: impl IntoIterator<Item = MountEntry>,
459        conflict_mode: ExactMountConflictMode,
460    ) -> Result<()> {
461        let mut skipped_subtrees = Vec::<PathBuf>::new();
462
463        for entry in entries {
464            if skipped_subtrees
465                .iter()
466                .any(|prefix| entry.path.starts_with(prefix))
467            {
468                continue;
469            }
470
471            let exact_conflict = self.filesystem_at(&entry.path).is_some();
472            if exact_conflict {
473                match conflict_mode {
474                    ExactMountConflictMode::Fail => return Err(FsError::AlreadyExists),
475                    ExactMountConflictMode::KeepExisting => {
476                        skipped_subtrees.push(entry.path);
477                        continue;
478                    }
479                    ExactMountConflictMode::ReplaceExisting => {
480                        let ts = Self::now_nanos();
481                        let mut root = self.root.write().unwrap();
482                        let node = Self::mount_node_mut(
483                            &mut root,
484                            &Self::path_components(&self.prepare_path(&entry.path)?),
485                            ts,
486                        );
487                        Self::clear_descendants(node);
488                        node.mount = Some(MountedFileSystem {
489                            fs: entry.fs,
490                            source_path: entry.source_path,
491                        });
492                        continue;
493                    }
494                }
495            }
496
497            self.mount_with_source(&entry.path, &entry.source_path, entry.fs)?;
498        }
499
500        Ok(())
501    }
502    pub fn mount_entries(&self) -> Vec<MountEntry> {
503        let mut entries = Vec::new();
504        let root = self.root.read().unwrap();
505        Self::collect_mount_entries(&root, Path::new("/"), &mut entries);
506        entries
507    }
508}
509
510impl FileSystem for MountFileSystem {
511    fn readlink(&self, path: &Path) -> Result<PathBuf> {
512        let path = self.prepare_path(path)?;
513
514        if path.as_os_str().is_empty() {
515            Err(FsError::NotAFile)
516        } else {
517            if let Some(node) = self.exact_node(&path)
518                && node.fs.is_none()
519            {
520                return Err(FsError::EntryNotFound);
521            }
522
523            match self.resolve_mount(path) {
524                Some(resolved) => resolved.fs.readlink(&resolved.delegated_path),
525                None => Err(FsError::EntryNotFound),
526            }
527        }
528    }
529
530    fn read_dir(&self, path: &Path) -> Result<ReadDir> {
531        let path = self.prepare_path(path)?;
532
533        if let Some(node) = self.exact_node(&path) {
534            return self.read_dir_from_exact_node(&node);
535        }
536
537        match self.resolve_mount(path.clone()) {
538            Some(resolved) => {
539                let mut entries = resolved.fs.read_dir(&resolved.delegated_path)?;
540                Self::rebase_entries(
541                    &mut entries,
542                    &resolved.delegated_path,
543                    &Path::new("/").join(&path),
544                );
545                Ok(entries)
546            }
547            None => Err(FsError::EntryNotFound),
548        }
549    }
550
551    fn create_dir(&self, path: &Path) -> Result<()> {
552        let path = self.prepare_path(path)?;
553
554        if path.as_os_str().is_empty() {
555            return Ok(());
556        }
557
558        if let Some(node) = self.exact_node(&path) {
559            return if let Some(fs) = node.fs {
560                let result = fs.create_dir(Path::new("/"));
561
562                match result {
563                    Ok(()) | Err(FsError::AlreadyExists) => Ok(()),
564                    Err(error) if Self::should_fallback_to_synthetic_dir(&error) => Ok(()),
565                    Err(error) => Err(error),
566                }
567            } else {
568                self.back_synthetic_dirs(&Self::path_components(&path));
569                Ok(())
570            };
571        }
572
573        self.back_synthetic_parents(&path);
574        match self.resolve_mount(path) {
575            Some(resolved) => {
576                let result = resolved.fs.create_dir(&resolved.delegated_path);
577
578                if let Err(error) = result
579                    && error == FsError::AlreadyExists
580                {
581                    return Ok(());
582                }
583
584                result
585            }
586            None => Err(FsError::EntryNotFound),
587        }
588    }
589
590    fn create_symlink(&self, source: &Path, target: &Path) -> Result<()> {
591        let target = self.prepare_path(target)?;
592
593        if target.as_os_str().is_empty() {
594            return Err(FsError::AlreadyExists);
595        }
596
597        if self.exact_node(&target).is_some() {
598            return Err(FsError::AlreadyExists);
599        }
600
601        match self.resolve_mount(target) {
602            Some(resolved) => resolved.fs.create_symlink(source, &resolved.delegated_path),
603            None => Err(FsError::EntryNotFound),
604        }
605    }
606
607    fn hard_link(&self, source: &Path, target: &Path) -> Result<()> {
608        let source = self.prepare_path(source)?;
609        let target = self.prepare_path(target)?;
610
611        if source.as_os_str().is_empty() {
612            return Err(FsError::PermissionDenied);
613        }
614
615        if target.as_os_str().is_empty() {
616            return Err(FsError::AlreadyExists);
617        }
618
619        if let Some(node) = self.exact_node(&target)
620            && (node.fs.is_some() || node.has_children())
621        {
622            return Err(FsError::AlreadyExists);
623        }
624
625        match (self.resolve_mount(source), self.resolve_mount(target)) {
626            (Some(source_mount), Some(target_mount))
627                if source_mount.mount_path == target_mount.mount_path =>
628            {
629                source_mount
630                    .fs
631                    .hard_link(&source_mount.delegated_path, &target_mount.delegated_path)
632            }
633            (Some(_), Some(_)) => Err(FsError::Unsupported),
634            _ => Err(FsError::EntryNotFound),
635        }
636    }
637
638    fn remove_dir(&self, path: &Path) -> Result<()> {
639        let path = self.prepare_path(path)?;
640
641        if path.as_os_str().is_empty() {
642            return Err(FsError::PermissionDenied);
643        }
644
645        if let Some(node) = self.exact_node(&path) {
646            return if node.fs.is_some() || node.has_children() {
647                Err(FsError::PermissionDenied)
648            } else {
649                Err(FsError::EntryNotFound)
650            };
651        }
652
653        match self.resolve_mount(path) {
654            Some(resolved) => resolved.fs.remove_dir(&resolved.delegated_path),
655            None => Err(FsError::EntryNotFound),
656        }
657    }
658
659    fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> {
660        Box::pin(async move {
661            let from = self.prepare_path(from)?;
662            let to = self.prepare_path(to)?;
663
664            if from.as_os_str().is_empty() {
665                return Err(FsError::PermissionDenied);
666            }
667
668            if let Some(node) = self.exact_node(&from)
669                && (node.fs.is_some() || node.has_children())
670            {
671                return Err(FsError::PermissionDenied);
672            }
673
674            if let Some(node) = self.exact_node(&to)
675                && (node.fs.is_some() || node.has_children())
676            {
677                return Err(FsError::PermissionDenied);
678            }
679
680            match (self.resolve_mount(from), self.resolve_mount(to)) {
681                (Some(from_mount), Some(to_mount))
682                    if from_mount.mount_path == to_mount.mount_path =>
683                {
684                    from_mount
685                        .fs
686                        .rename(&from_mount.delegated_path, &to_mount.delegated_path)
687                        .await
688                }
689                (Some(from_mount), Some(to_mount)) => {
690                    ops::move_across_filesystems(
691                        from_mount.fs.as_ref(),
692                        to_mount.fs.as_ref(),
693                        &from_mount.delegated_path,
694                        &to_mount.delegated_path,
695                    )
696                    .await
697                }
698                _ => Err(FsError::EntryNotFound),
699            }
700        })
701    }
702
703    fn metadata(&self, path: &Path) -> Result<Metadata> {
704        let path = self.prepare_path(path)?;
705
706        if let Some(node) = self.exact_node(&path) {
707            return if let Some(fs) = node.fs {
708                fs.metadata(&node.source_path).or_else(|error| {
709                    if Self::should_fallback_to_synthetic_dir(&error) {
710                        Ok(Self::directory_metadata_at(node.created_at))
711                    } else {
712                        Err(error)
713                    }
714                })
715            } else if node.has_children() {
716                Ok(Self::directory_metadata_at(node.created_at))
717            } else {
718                Err(FsError::EntryNotFound)
719            };
720        }
721
722        match self.resolve_mount(path) {
723            Some(resolved) => resolved.fs.metadata(&resolved.delegated_path),
724            None => Err(FsError::EntryNotFound),
725        }
726    }
727
728    fn symlink_metadata(&self, path: &Path) -> Result<Metadata> {
729        let path = self.prepare_path(path)?;
730
731        if let Some(node) = self.exact_node(&path) {
732            return if let Some(fs) = node.fs {
733                fs.symlink_metadata(&node.source_path).or_else(|error| {
734                    if Self::should_fallback_to_synthetic_dir(&error) {
735                        Ok(Self::directory_metadata_at(node.created_at))
736                    } else {
737                        Err(error)
738                    }
739                })
740            } else if node.has_children() {
741                Ok(Self::directory_metadata_at(node.created_at))
742            } else {
743                Err(FsError::EntryNotFound)
744            };
745        }
746
747        match self.resolve_mount(path) {
748            Some(resolved) => resolved.fs.symlink_metadata(&resolved.delegated_path),
749            None => Err(FsError::EntryNotFound),
750        }
751    }
752
753    fn remove_file(&self, path: &Path) -> Result<()> {
754        let path = self.prepare_path(path)?;
755
756        if path.as_os_str().is_empty() {
757            return Err(FsError::NotAFile);
758        }
759
760        if let Some(node) = self.exact_node(&path) {
761            return if node.fs.is_some() || node.has_children() {
762                Err(FsError::PermissionDenied)
763            } else {
764                Err(FsError::EntryNotFound)
765            };
766        }
767
768        match self.resolve_mount(path) {
769            Some(resolved) => resolved.fs.remove_file(&resolved.delegated_path),
770            None => Err(FsError::EntryNotFound),
771        }
772    }
773
774    fn new_open_options(&self) -> OpenOptions<'_> {
775        OpenOptions::new(self)
776    }
777}
778
779#[derive(Debug)]
780pub struct MountPointRef<'a> {
781    pub path: PathBuf,
782    pub name: String,
783    pub fs: Option<&'a (dyn FileSystem + Send + Sync)>,
784}
785
786impl FileOpener for MountFileSystem {
787    fn open(
788        &self,
789        path: &Path,
790        conf: &OpenOptionsConfig,
791    ) -> Result<Box<dyn VirtualFile + Send + Sync>> {
792        let path = self.prepare_path(path)?;
793
794        if path.as_os_str().is_empty() {
795            return Err(FsError::NotAFile);
796        }
797
798        if let Some(node) = self.exact_node(&path)
799            && node.fs.is_none()
800        {
801            return Err(FsError::NotAFile);
802        }
803
804        if conf.create || conf.create_new {
805            self.back_synthetic_parents(&path);
806        }
807        match self.resolve_mount(path) {
808            Some(resolved) => resolved
809                .fs
810                .new_open_options()
811                .options(conf.clone())
812                .open(resolved.delegated_path),
813            None => Err(FsError::EntryNotFound),
814        }
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use std::{
821        collections::HashSet,
822        path::{Path, PathBuf},
823        sync::Arc,
824    };
825
826    use tokio::io::AsyncWriteExt;
827
828    use crate::{FileSystem as FileSystemTrait, FsError, MountFileSystem, TmpFileSystem, mem_fs};
829
830    use super::{FileOpener, OpenOptionsConfig};
831
832    #[derive(Debug, Clone, Default)]
833    struct MountlessFileSystem {
834        inner: mem_fs::FileSystem,
835    }
836
837    #[derive(Debug, Clone, Default)]
838    struct RootOpaqueFileSystem {
839        inner: mem_fs::FileSystem,
840    }
841
842    #[derive(Debug, Clone, Default)]
843    struct RootPermissionDeniedFileSystem;
844
845    impl FileSystemTrait for MountlessFileSystem {
846        fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
847            self.inner.readlink(path)
848        }
849
850        fn read_dir(&self, path: &Path) -> crate::Result<crate::ReadDir> {
851            self.inner.read_dir(path)
852        }
853
854        fn create_dir(&self, path: &Path) -> crate::Result<()> {
855            self.inner.create_dir(path)
856        }
857
858        fn remove_dir(&self, path: &Path) -> crate::Result<()> {
859            self.inner.remove_dir(path)
860        }
861
862        fn rename<'a>(
863            &'a self,
864            from: &'a Path,
865            to: &'a Path,
866        ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
867            Box::pin(async move { self.inner.rename(from, to).await })
868        }
869
870        fn metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
871            self.inner.metadata(path)
872        }
873
874        fn symlink_metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
875            self.inner.symlink_metadata(path)
876        }
877
878        fn remove_file(&self, path: &Path) -> crate::Result<()> {
879            self.inner.remove_file(path)
880        }
881
882        fn new_open_options(&self) -> crate::OpenOptions<'_> {
883            self.inner.new_open_options()
884        }
885    }
886
887    impl FileOpener for MountlessFileSystem {
888        fn open(
889            &self,
890            path: &Path,
891            conf: &OpenOptionsConfig,
892        ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
893            self.inner
894                .new_open_options()
895                .options(conf.clone())
896                .open(path)
897        }
898    }
899
900    impl FileSystemTrait for RootOpaqueFileSystem {
901        fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
902            self.inner.readlink(path)
903        }
904
905        fn read_dir(&self, path: &Path) -> crate::Result<crate::ReadDir> {
906            if path == Path::new("/") {
907                Err(FsError::Unsupported)
908            } else {
909                self.inner.read_dir(path)
910            }
911        }
912
913        fn create_dir(&self, path: &Path) -> crate::Result<()> {
914            self.inner.create_dir(path)
915        }
916
917        fn remove_dir(&self, path: &Path) -> crate::Result<()> {
918            self.inner.remove_dir(path)
919        }
920
921        fn rename<'a>(
922            &'a self,
923            from: &'a Path,
924            to: &'a Path,
925        ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
926            Box::pin(async move { self.inner.rename(from, to).await })
927        }
928
929        fn metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
930            if path == Path::new("/") {
931                Err(FsError::Unsupported)
932            } else {
933                self.inner.metadata(path)
934            }
935        }
936
937        fn symlink_metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
938            if path == Path::new("/") {
939                Err(FsError::Unsupported)
940            } else {
941                self.inner.symlink_metadata(path)
942            }
943        }
944
945        fn remove_file(&self, path: &Path) -> crate::Result<()> {
946            self.inner.remove_file(path)
947        }
948
949        fn new_open_options(&self) -> crate::OpenOptions<'_> {
950            self.inner.new_open_options()
951        }
952    }
953
954    impl FileOpener for RootOpaqueFileSystem {
955        fn open(
956            &self,
957            path: &Path,
958            conf: &OpenOptionsConfig,
959        ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
960            self.inner
961                .new_open_options()
962                .options(conf.clone())
963                .open(path)
964        }
965    }
966
967    impl FileSystemTrait for RootPermissionDeniedFileSystem {
968        fn readlink(&self, _path: &Path) -> crate::Result<PathBuf> {
969            Err(FsError::PermissionDenied)
970        }
971
972        fn read_dir(&self, _path: &Path) -> crate::Result<crate::ReadDir> {
973            Err(FsError::PermissionDenied)
974        }
975
976        fn create_dir(&self, _path: &Path) -> crate::Result<()> {
977            Err(FsError::PermissionDenied)
978        }
979
980        fn remove_dir(&self, _path: &Path) -> crate::Result<()> {
981            Err(FsError::PermissionDenied)
982        }
983
984        fn rename<'a>(
985            &'a self,
986            _from: &'a Path,
987            _to: &'a Path,
988        ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
989            Box::pin(async { Err(FsError::PermissionDenied) })
990        }
991
992        fn metadata(&self, _path: &Path) -> crate::Result<crate::Metadata> {
993            Err(FsError::PermissionDenied)
994        }
995
996        fn symlink_metadata(&self, _path: &Path) -> crate::Result<crate::Metadata> {
997            Err(FsError::PermissionDenied)
998        }
999
1000        fn remove_file(&self, _path: &Path) -> crate::Result<()> {
1001            Err(FsError::PermissionDenied)
1002        }
1003
1004        fn new_open_options(&self) -> crate::OpenOptions<'_> {
1005            crate::OpenOptions::new(self)
1006        }
1007    }
1008
1009    impl FileOpener for RootPermissionDeniedFileSystem {
1010        fn open(
1011            &self,
1012            _path: &Path,
1013            _conf: &OpenOptionsConfig,
1014        ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
1015            Err(FsError::PermissionDenied)
1016        }
1017    }
1018
1019    fn gen_filesystem() -> MountFileSystem {
1020        let union = MountFileSystem::new();
1021        let a = mem_fs::FileSystem::default();
1022        let b = mem_fs::FileSystem::default();
1023        let c = mem_fs::FileSystem::default();
1024        let d = mem_fs::FileSystem::default();
1025        let e = mem_fs::FileSystem::default();
1026        let f = mem_fs::FileSystem::default();
1027        let g = mem_fs::FileSystem::default();
1028        let h = mem_fs::FileSystem::default();
1029
1030        union
1031            .mount(PathBuf::from("/test_new_filesystem").as_path(), Arc::new(a))
1032            .unwrap();
1033        union
1034            .mount(PathBuf::from("/test_create_dir").as_path(), Arc::new(b))
1035            .unwrap();
1036        union
1037            .mount(PathBuf::from("/test_remove_dir").as_path(), Arc::new(c))
1038            .unwrap();
1039        union
1040            .mount(PathBuf::from("/test_rename").as_path(), Arc::new(d))
1041            .unwrap();
1042        union
1043            .mount(PathBuf::from("/test_metadata").as_path(), Arc::new(e))
1044            .unwrap();
1045        union
1046            .mount(PathBuf::from("/test_remove_file").as_path(), Arc::new(f))
1047            .unwrap();
1048        union
1049            .mount(PathBuf::from("/test_readdir").as_path(), Arc::new(g))
1050            .unwrap();
1051        union
1052            .mount(PathBuf::from("/test_canonicalize").as_path(), Arc::new(h))
1053            .unwrap();
1054
1055        union
1056    }
1057
1058    fn gen_nested_filesystem() -> MountFileSystem {
1059        let union = MountFileSystem::new();
1060        let a = mem_fs::FileSystem::default();
1061        a.open(
1062            &PathBuf::from("/data-a.txt"),
1063            &OpenOptionsConfig {
1064                read: true,
1065                write: true,
1066                create_new: false,
1067                create: true,
1068                append: false,
1069                truncate: false,
1070            },
1071        )
1072        .unwrap();
1073        let b = mem_fs::FileSystem::default();
1074        b.open(
1075            &PathBuf::from("/data-b.txt"),
1076            &OpenOptionsConfig {
1077                read: true,
1078                write: true,
1079                create_new: false,
1080                create: true,
1081                append: false,
1082                truncate: false,
1083            },
1084        )
1085        .unwrap();
1086
1087        union
1088            .mount(PathBuf::from("/app/a").as_path(), Arc::new(a))
1089            .unwrap();
1090        union
1091            .mount(PathBuf::from("/app/b").as_path(), Arc::new(b))
1092            .unwrap();
1093
1094        union
1095    }
1096
1097    #[tokio::test]
1098    async fn test_nested_read_dir() {
1099        let fs = gen_nested_filesystem();
1100
1101        let root_contents: Vec<PathBuf> = fs
1102            .read_dir(&PathBuf::from("/"))
1103            .unwrap()
1104            .map(|e| e.unwrap().path.clone())
1105            .collect();
1106        assert_eq!(root_contents, vec![PathBuf::from("/app")]);
1107
1108        let app_contents: HashSet<PathBuf> = fs
1109            .read_dir(&PathBuf::from("/app"))
1110            .unwrap()
1111            .map(|e| e.unwrap().path)
1112            .collect();
1113        assert_eq!(
1114            app_contents,
1115            HashSet::from_iter([PathBuf::from("/app/a"), PathBuf::from("/app/b")].into_iter())
1116        );
1117
1118        let a_contents: Vec<PathBuf> = fs
1119            .read_dir(&PathBuf::from("/app/a"))
1120            .unwrap()
1121            .map(|e| e.unwrap().path.clone())
1122            .collect();
1123        assert_eq!(a_contents, vec![PathBuf::from("/app/a/data-a.txt")]);
1124
1125        let b_contents: Vec<PathBuf> = fs
1126            .read_dir(&PathBuf::from("/app/b"))
1127            .unwrap()
1128            .map(|e| e.unwrap().path)
1129            .collect();
1130        assert_eq!(b_contents, vec![PathBuf::from("/app/b/data-b.txt")]);
1131    }
1132
1133    #[tokio::test]
1134    async fn test_nested_metadata() {
1135        let fs = gen_nested_filesystem();
1136
1137        assert!(fs.metadata(&PathBuf::from("/")).is_ok());
1138        assert!(fs.metadata(&PathBuf::from("/app")).is_ok());
1139        assert!(fs.metadata(&PathBuf::from("/app/a")).is_ok());
1140        assert!(fs.metadata(&PathBuf::from("/app/b")).is_ok());
1141        assert!(fs.metadata(&PathBuf::from("/app/a/data-a.txt")).is_ok());
1142        assert!(fs.metadata(&PathBuf::from("/app/b/data-b.txt")).is_ok());
1143    }
1144
1145    #[tokio::test]
1146    async fn test_nested_symlink_metadata() {
1147        let fs = gen_nested_filesystem();
1148
1149        assert!(fs.symlink_metadata(&PathBuf::from("/")).is_ok());
1150        assert!(fs.symlink_metadata(&PathBuf::from("/app")).is_ok());
1151        assert!(fs.symlink_metadata(&PathBuf::from("/app/a")).is_ok());
1152        assert!(fs.symlink_metadata(&PathBuf::from("/app/b")).is_ok());
1153        assert!(
1154            fs.symlink_metadata(&PathBuf::from("/app/a/data-a.txt"))
1155                .is_ok()
1156        );
1157        assert!(
1158            fs.symlink_metadata(&PathBuf::from("/app/b/data-b.txt"))
1159                .is_ok()
1160        );
1161    }
1162
1163    #[tokio::test]
1164    async fn test_import_mounts_preserves_nested_root_mounts() {
1165        let primary = MountFileSystem::new();
1166        let openssl = mem_fs::FileSystem::default();
1167        openssl.create_dir(Path::new("/certs")).unwrap();
1168        openssl
1169            .new_open_options()
1170            .write(true)
1171            .create_new(true)
1172            .open(Path::new("/certs/ca.pem"))
1173            .unwrap();
1174        primary
1175            .mount(Path::new("/openssl"), Arc::new(openssl))
1176            .unwrap();
1177
1178        let injected = MountFileSystem::new();
1179        let app = mem_fs::FileSystem::default();
1180        app.new_open_options()
1181            .write(true)
1182            .create_new(true)
1183            .open(Path::new("/index.php"))
1184            .unwrap();
1185        injected.mount(Path::new("/app"), Arc::new(app)).unwrap();
1186
1187        let assets = mem_fs::FileSystem::default();
1188        assets.create_dir(Path::new("/css")).unwrap();
1189        assets
1190            .new_open_options()
1191            .write(true)
1192            .create_new(true)
1193            .open(Path::new("/css/site.css"))
1194            .unwrap();
1195        injected
1196            .mount(Path::new("/opt/assets"), Arc::new(assets))
1197            .unwrap();
1198
1199        primary
1200            .add_mount_entries_with_mode(
1201                injected.mount_entries(),
1202                super::ExactMountConflictMode::Fail,
1203            )
1204            .unwrap();
1205
1206        let root_contents = read_dir_names(&primary, "/");
1207        assert!(root_contents.contains(&"app".to_string()));
1208        assert!(root_contents.contains(&"opt".to_string()));
1209        assert!(root_contents.contains(&"openssl".to_string()));
1210        assert!(primary.metadata(Path::new("/app/index.php")).is_ok());
1211        assert!(
1212            primary
1213                .metadata(Path::new("/opt/assets/css/site.css"))
1214                .is_ok()
1215        );
1216        assert!(primary.metadata(Path::new("/openssl/certs/ca.pem")).is_ok());
1217    }
1218
1219    #[tokio::test]
1220    async fn test_nested_mount_under_non_mountable_leaf_is_supported() {
1221        let fs = MountFileSystem::new();
1222
1223        let top = MountlessFileSystem::default();
1224        top.create_dir(Path::new("/bin")).unwrap();
1225        top.new_open_options()
1226            .write(true)
1227            .create_new(true)
1228            .open(Path::new("/bin/tool"))
1229            .unwrap();
1230
1231        let nested = mem_fs::FileSystem::default();
1232        nested.create_dir(Path::new("/css")).unwrap();
1233        nested
1234            .new_open_options()
1235            .write(true)
1236            .create_new(true)
1237            .open(Path::new("/css/site.css"))
1238            .unwrap();
1239
1240        fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1241        fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1242            .unwrap();
1243
1244        assert!(fs.metadata(Path::new("/opt/bin/tool")).is_ok());
1245        assert!(fs.metadata(Path::new("/opt/assets/css/site.css")).is_ok());
1246    }
1247
1248    #[tokio::test]
1249    async fn test_normalized_paths_still_route_to_deepest_mount() {
1250        let fs = MountFileSystem::new();
1251
1252        let top = MountlessFileSystem::default();
1253        top.create_dir(Path::new("/bin")).unwrap();
1254        top.new_open_options()
1255            .write(true)
1256            .create_new(true)
1257            .open(Path::new("/bin/tool"))
1258            .unwrap();
1259
1260        let nested = mem_fs::FileSystem::default();
1261        nested.create_dir(Path::new("/css")).unwrap();
1262        nested
1263            .new_open_options()
1264            .write(true)
1265            .create_new(true)
1266            .open(Path::new("/css/site.css"))
1267            .unwrap();
1268
1269        fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1270        fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1271            .unwrap();
1272
1273        assert!(
1274            fs.metadata(Path::new("/opt/./assets/../assets/css/site.css"))
1275                .unwrap()
1276                .is_file()
1277        );
1278    }
1279
1280    #[tokio::test]
1281    async fn test_invalid_above_root_path_is_rejected() {
1282        let fs = MountFileSystem::new();
1283        fs.mount(Path::new("/"), Arc::new(mem_fs::FileSystem::default()))
1284            .unwrap();
1285
1286        assert_eq!(fs.metadata(Path::new("../foo")), Err(FsError::InvalidInput));
1287    }
1288
1289    #[tokio::test]
1290    async fn test_exact_mount_metadata_falls_back_to_synthetic_directory() {
1291        let fs = MountFileSystem::new();
1292        fs.mount(
1293            Path::new("/opaque"),
1294            Arc::new(RootOpaqueFileSystem::default()),
1295        )
1296        .unwrap();
1297
1298        let meta1 = fs.metadata(Path::new("/opaque")).unwrap();
1299        let sym1 = fs.symlink_metadata(Path::new("/opaque")).unwrap();
1300        assert!(meta1.is_dir());
1301        assert!(sym1.is_dir());
1302
1303        // Timestamps must be non-zero (regression guard against the old `0` placeholders).
1304        assert!(meta1.created > 0, "created timestamp must be non-zero");
1305        assert!(meta1.modified > 0, "modified timestamp must be non-zero");
1306        assert!(meta1.accessed > 0, "accessed timestamp must be non-zero");
1307
1308        // Repeated calls must return the same stable timestamps (not re-sampled each time).
1309        let meta2 = fs.metadata(Path::new("/opaque")).unwrap();
1310        assert_eq!(meta1.created, meta2.created, "created must be stable");
1311        assert_eq!(meta1.modified, meta2.modified, "modified must be stable");
1312        assert_eq!(meta1.accessed, meta2.accessed, "accessed must be stable");
1313
1314        assert_eq!(fs.create_dir(Path::new("/opaque")), Ok(()));
1315    }
1316
1317    #[tokio::test]
1318    async fn test_exact_mount_read_dir_falls_back_to_child_mounts_when_root_is_unlistable() {
1319        let fs = MountFileSystem::new();
1320        fs.mount(
1321            Path::new("/opaque"),
1322            Arc::new(RootOpaqueFileSystem::default()),
1323        )
1324        .unwrap();
1325        fs.mount(
1326            Path::new("/opaque/assets"),
1327            Arc::new(mem_fs::FileSystem::default()),
1328        )
1329        .unwrap();
1330
1331        assert_eq!(read_dir_names(&fs, "/opaque"), vec!["assets".to_string()]);
1332    }
1333
1334    #[tokio::test]
1335    async fn test_exact_mount_fallback_does_not_mask_permission_denied() {
1336        let fs = MountFileSystem::new();
1337        fs.mount(
1338            Path::new("/denied"),
1339            Arc::new(RootPermissionDeniedFileSystem),
1340        )
1341        .unwrap();
1342        fs.mount(
1343            Path::new("/denied/assets"),
1344            Arc::new(mem_fs::FileSystem::default()),
1345        )
1346        .unwrap();
1347
1348        assert_eq!(
1349            fs.metadata(Path::new("/denied")),
1350            Err(FsError::PermissionDenied)
1351        );
1352        assert_eq!(
1353            fs.symlink_metadata(Path::new("/denied")),
1354            Err(FsError::PermissionDenied)
1355        );
1356        assert_eq!(
1357            fs.read_dir(Path::new("/denied")).map(|_| ()),
1358            Err(FsError::PermissionDenied)
1359        );
1360        assert_eq!(
1361            fs.create_dir(Path::new("/denied")),
1362            Err(FsError::PermissionDenied)
1363        );
1364    }
1365
1366    #[tokio::test]
1367    async fn test_keep_existing_conflict_skips_the_other_subtree() {
1368        let primary = MountFileSystem::new();
1369        let user_mount = mem_fs::FileSystem::default();
1370        user_mount
1371            .new_open_options()
1372            .write(true)
1373            .create_new(true)
1374            .open(Path::new("/user.txt"))
1375            .unwrap();
1376        primary
1377            .mount(Path::new("/python"), Arc::new(user_mount))
1378            .unwrap();
1379
1380        let injected = MountFileSystem::new();
1381        let package_mount = mem_fs::FileSystem::default();
1382        package_mount
1383            .new_open_options()
1384            .write(true)
1385            .create_new(true)
1386            .open(Path::new("/pkg.txt"))
1387            .unwrap();
1388        injected
1389            .mount(Path::new("/python"), Arc::new(package_mount))
1390            .unwrap();
1391
1392        let package_child = mem_fs::FileSystem::default();
1393        package_child
1394            .new_open_options()
1395            .write(true)
1396            .create_new(true)
1397            .open(Path::new("/child.txt"))
1398            .unwrap();
1399        injected
1400            .mount(Path::new("/python/lib"), Arc::new(package_child))
1401            .unwrap();
1402
1403        primary
1404            .add_mount_entries_with_mode(
1405                injected.mount_entries(),
1406                super::ExactMountConflictMode::KeepExisting,
1407            )
1408            .unwrap();
1409
1410        assert!(
1411            primary
1412                .metadata(Path::new("/python/user.txt"))
1413                .unwrap()
1414                .is_file()
1415        );
1416        assert_eq!(
1417            primary.metadata(Path::new("/python/pkg.txt")),
1418            Err(FsError::EntryNotFound)
1419        );
1420        assert_eq!(
1421            primary.metadata(Path::new("/python/lib/child.txt")),
1422            Err(FsError::EntryNotFound)
1423        );
1424    }
1425
1426    #[tokio::test]
1427    async fn test_replace_existing_conflict_replaces_the_whole_subtree() {
1428        let primary = MountFileSystem::new();
1429        let user_mount = mem_fs::FileSystem::default();
1430        user_mount
1431            .new_open_options()
1432            .write(true)
1433            .create_new(true)
1434            .open(Path::new("/user.txt"))
1435            .unwrap();
1436        let user_child = mem_fs::FileSystem::default();
1437        user_child
1438            .new_open_options()
1439            .write(true)
1440            .create_new(true)
1441            .open(Path::new("/user-child.txt"))
1442            .unwrap();
1443        primary
1444            .mount(Path::new("/python"), Arc::new(user_mount))
1445            .unwrap();
1446        primary
1447            .mount(Path::new("/python/lib"), Arc::new(user_child))
1448            .unwrap();
1449
1450        let injected = MountFileSystem::new();
1451        let package_mount = mem_fs::FileSystem::default();
1452        package_mount
1453            .new_open_options()
1454            .write(true)
1455            .create_new(true)
1456            .open(Path::new("/pkg.txt"))
1457            .unwrap();
1458        let package_child = mem_fs::FileSystem::default();
1459        package_child
1460            .new_open_options()
1461            .write(true)
1462            .create_new(true)
1463            .open(Path::new("/pkg-child.txt"))
1464            .unwrap();
1465        injected
1466            .mount(Path::new("/python"), Arc::new(package_mount))
1467            .unwrap();
1468        injected
1469            .mount(Path::new("/python/lib"), Arc::new(package_child))
1470            .unwrap();
1471
1472        primary
1473            .add_mount_entries_with_mode(
1474                injected.mount_entries(),
1475                super::ExactMountConflictMode::ReplaceExisting,
1476            )
1477            .unwrap();
1478
1479        assert_eq!(
1480            primary.metadata(Path::new("/python/user.txt")),
1481            Err(FsError::EntryNotFound)
1482        );
1483        assert_eq!(
1484            primary.metadata(Path::new("/python/lib/user-child.txt")),
1485            Err(FsError::EntryNotFound)
1486        );
1487        assert!(
1488            primary
1489                .metadata(Path::new("/python/pkg.txt"))
1490                .unwrap()
1491                .is_file()
1492        );
1493        assert!(
1494            primary
1495                .metadata(Path::new("/python/lib/pkg-child.txt"))
1496                .unwrap()
1497                .is_file()
1498        );
1499    }
1500
1501    #[tokio::test]
1502    async fn test_exact_mountpoints_reject_destructive_mutation() {
1503        let fs = MountFileSystem::new();
1504        let mounted = mem_fs::FileSystem::default();
1505        mounted.create_dir(Path::new("/dir")).unwrap();
1506        mounted
1507            .new_open_options()
1508            .write(true)
1509            .create_new(true)
1510            .open(Path::new("/file.txt"))
1511            .unwrap();
1512
1513        fs.mount(Path::new("/mounted"), Arc::new(mounted)).unwrap();
1514
1515        assert_eq!(
1516            fs.remove_dir(Path::new("/mounted")),
1517            Err(FsError::PermissionDenied)
1518        );
1519        assert_eq!(
1520            fs.remove_file(Path::new("/mounted")),
1521            Err(FsError::PermissionDenied)
1522        );
1523        assert_eq!(
1524            fs.rename(Path::new("/mounted"), Path::new("/other")).await,
1525            Err(FsError::PermissionDenied)
1526        );
1527        assert_eq!(
1528            fs.rename(Path::new("/mounted/file.txt"), Path::new("/mounted"))
1529                .await,
1530            Err(FsError::PermissionDenied)
1531        );
1532    }
1533
1534    #[tokio::test]
1535    async fn test_parent_read_dir_merges_leaf_entries_with_child_mounts() {
1536        let fs = MountFileSystem::new();
1537
1538        let top = MountlessFileSystem::default();
1539        top.create_dir(Path::new("/bin")).unwrap();
1540        top.new_open_options()
1541            .write(true)
1542            .create_new(true)
1543            .open(Path::new("/bin/tool"))
1544            .unwrap();
1545
1546        let nested = mem_fs::FileSystem::default();
1547        nested.create_dir(Path::new("/css")).unwrap();
1548        nested
1549            .new_open_options()
1550            .write(true)
1551            .create_new(true)
1552            .open(Path::new("/css/site.css"))
1553            .unwrap();
1554
1555        fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1556        fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1557            .unwrap();
1558
1559        let opt_contents = read_dir_names(&fs, "/opt");
1560        assert!(opt_contents.contains(&"bin".to_string()));
1561        assert!(opt_contents.contains(&"assets".to_string()));
1562    }
1563
1564    #[tokio::test]
1565    async fn test_child_mount_shadows_same_named_parent_entry() {
1566        let fs = MountFileSystem::new();
1567
1568        let top = MountlessFileSystem::default();
1569        top.new_open_options()
1570            .write(true)
1571            .create_new(true)
1572            .open(Path::new("/assets"))
1573            .unwrap();
1574
1575        let nested = mem_fs::FileSystem::default();
1576        nested.create_dir(Path::new("/css")).unwrap();
1577        nested
1578            .new_open_options()
1579            .write(true)
1580            .create_new(true)
1581            .open(Path::new("/css/site.css"))
1582            .unwrap();
1583
1584        fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1585        fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1586            .unwrap();
1587
1588        assert!(fs.metadata(Path::new("/opt/assets")).unwrap().is_dir());
1589        assert_eq!(
1590            read_dir_names(&fs, "/opt")
1591                .into_iter()
1592                .filter(|entry| entry == "assets")
1593                .count(),
1594            1,
1595        );
1596        assert!(fs.metadata(Path::new("/opt/assets/css/site.css")).is_ok());
1597    }
1598
1599    #[tokio::test]
1600    async fn test_read_dir_rebases_entries_under_nested_mount_subdirectory() {
1601        let fs = MountFileSystem::new();
1602
1603        let nested = mem_fs::FileSystem::default();
1604        nested.create_dir(Path::new("/css")).unwrap();
1605        nested
1606            .new_open_options()
1607            .write(true)
1608            .create_new(true)
1609            .open(Path::new("/css/site.css"))
1610            .unwrap();
1611
1612        fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1613            .unwrap();
1614
1615        let css_contents: Vec<PathBuf> = fs
1616            .read_dir(Path::new("/opt/assets/css"))
1617            .unwrap()
1618            .map(|entry| entry.unwrap().path)
1619            .collect();
1620
1621        assert_eq!(
1622            css_contents,
1623            vec![PathBuf::from("/opt/assets/css/site.css")]
1624        );
1625    }
1626
1627    #[tokio::test]
1628    async fn test_mount_with_source_path_exposes_subtree() {
1629        let fs = MountFileSystem::new();
1630
1631        let source = mem_fs::FileSystem::default();
1632        source.create_dir(Path::new("/python")).unwrap();
1633        source
1634            .new_open_options()
1635            .write(true)
1636            .create_new(true)
1637            .open(Path::new("/python/lib.py"))
1638            .unwrap();
1639
1640        fs.mount_with_source(
1641            Path::new("/runtime"),
1642            Path::new("/python"),
1643            Arc::new(source),
1644        )
1645        .unwrap();
1646
1647        assert!(fs.metadata(Path::new("/runtime/lib.py")).unwrap().is_file());
1648        assert_eq!(read_dir_names(&fs, "/runtime"), vec!["lib.py".to_string()]);
1649    }
1650
1651    #[tokio::test]
1652    async fn test_nested_mount_inside_tree_preserves_sibling_files() {
1653        let fs = MountFileSystem::new();
1654
1655        let python = mem_fs::FileSystem::default();
1656        create_dir_all(&python, Path::new("/usr/local/lib/python3.13/encodings"));
1657        python
1658            .new_open_options()
1659            .write(true)
1660            .create_new(true)
1661            .open(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1662            .unwrap();
1663
1664        let host = mem_fs::FileSystem::default();
1665        host.new_open_options()
1666            .write(true)
1667            .create_new(true)
1668            .open(Path::new("/marker.txt"))
1669            .unwrap();
1670
1671        fs.mount(Path::new("/"), Arc::new(python)).unwrap();
1672        fs.mount(Path::new("/usr/local/lib/python3.13/test"), Arc::new(host))
1673            .unwrap();
1674
1675        assert!(
1676            fs.metadata(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1677                .unwrap()
1678                .is_file()
1679        );
1680        assert!(
1681            fs.metadata(Path::new("/usr/local/lib/python3.13/test/marker.txt"))
1682                .unwrap()
1683                .is_file()
1684        );
1685
1686        fs.new_open_options()
1687            .read(true)
1688            .open(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1689            .unwrap();
1690        fs.new_open_options()
1691            .read(true)
1692            .open(Path::new("/usr/local/lib/python3.13/test/marker.txt"))
1693            .unwrap();
1694
1695        let mut entries = read_dir_names(&fs, "/usr/local/lib/python3.13");
1696        entries.sort();
1697        assert_eq!(entries, vec!["encodings".to_string(), "test".to_string()]);
1698    }
1699
1700    #[tokio::test]
1701    async fn test_synthetic_parent_without_backing_dir_lists_child_mount() {
1702        let fs = MountFileSystem::new();
1703        fs.mount(Path::new("/"), Arc::new(mem_fs::FileSystem::default()))
1704            .unwrap();
1705
1706        let child = mem_fs::FileSystem::default();
1707        child
1708            .new_open_options()
1709            .write(true)
1710            .create_new(true)
1711            .open(Path::new("/marker.txt"))
1712            .unwrap();
1713        fs.mount(Path::new("/foo/bar"), Arc::new(child)).unwrap();
1714
1715        let entries = read_dir_names(&fs, "/foo");
1716        assert_eq!(entries, vec!["bar".to_string()]);
1717    }
1718
1719    #[tokio::test]
1720    async fn test_synthetic_parent_accepts_new_entries() {
1721        let fs = MountFileSystem::new();
1722        let root = Arc::new(mem_fs::FileSystem::default());
1723        fs.mount(Path::new("/"), root.clone()).unwrap();
1724        fs.mount(
1725            Path::new("/usr/local/ssl"),
1726            Arc::new(mem_fs::FileSystem::default()),
1727        )
1728        .unwrap();
1729
1730        // `/usr` and `/usr/local` exist only because of the nested mount.
1731        fs.new_open_options()
1732            .write(true)
1733            .create(true)
1734            .open(Path::new("/usr/x"))
1735            .unwrap();
1736        assert_eq!(fs.create_dir(Path::new("/usr/local")), Ok(()));
1737        assert_eq!(fs.create_dir(Path::new("/usr/local/bin")), Ok(()));
1738        assert_eq!(fs.create_dir(Path::new("/usr/bin")), Ok(()));
1739
1740        assert!(root.metadata(Path::new("/usr/x")).unwrap().is_file());
1741        assert!(root.metadata(Path::new("/usr/local/bin")).unwrap().is_dir());
1742        let mut entries = read_dir_names(&fs, "/usr");
1743        entries.sort();
1744        assert_eq!(entries, ["bin", "local", "x"]);
1745        let mut entries = read_dir_names(&fs, "/usr/local");
1746        entries.sort();
1747        assert_eq!(entries, ["bin", "ssl"]);
1748    }
1749
1750    #[tokio::test]
1751    async fn test_import_mounts_allows_shared_prefix_without_exact_mount_conflict() {
1752        let primary = MountFileSystem::new();
1753        let bin = mem_fs::FileSystem::default();
1754        bin.new_open_options()
1755            .write(true)
1756            .create_new(true)
1757            .open(Path::new("/tool"))
1758            .unwrap();
1759        primary.mount(Path::new("/opt/bin"), Arc::new(bin)).unwrap();
1760
1761        let injected = MountFileSystem::new();
1762        let assets = mem_fs::FileSystem::default();
1763        assets
1764            .new_open_options()
1765            .write(true)
1766            .create_new(true)
1767            .open(Path::new("/logo.svg"))
1768            .unwrap();
1769        injected
1770            .mount(Path::new("/opt/assets"), Arc::new(assets))
1771            .unwrap();
1772
1773        primary
1774            .add_mount_entries_with_mode(
1775                injected.mount_entries(),
1776                super::ExactMountConflictMode::Fail,
1777            )
1778            .unwrap();
1779
1780        assert!(primary.metadata(Path::new("/opt/bin/tool")).is_ok());
1781        assert!(primary.metadata(Path::new("/opt/assets/logo.svg")).is_ok());
1782    }
1783
1784    #[tokio::test]
1785    async fn test_import_mounts_rejects_exact_mount_conflict() {
1786        let primary = MountFileSystem::new();
1787        primary
1788            .mount(
1789                Path::new("/opt/bin"),
1790                Arc::new(mem_fs::FileSystem::default()),
1791            )
1792            .unwrap();
1793
1794        let injected = MountFileSystem::new();
1795        injected
1796            .mount(
1797                Path::new("/opt/bin"),
1798                Arc::new(mem_fs::FileSystem::default()),
1799            )
1800            .unwrap();
1801
1802        assert_eq!(
1803            primary.add_mount_entries_with_mode(
1804                injected.mount_entries(),
1805                super::ExactMountConflictMode::Fail,
1806            ),
1807            Err(FsError::AlreadyExists)
1808        );
1809    }
1810
1811    #[tokio::test]
1812    async fn test_new_filesystem() {
1813        let fs = gen_filesystem();
1814        assert!(
1815            fs.read_dir(Path::new("/test_new_filesystem")).is_ok(),
1816            "hostfs can read root"
1817        );
1818        let mut file_write = fs
1819            .new_open_options()
1820            .read(true)
1821            .write(true)
1822            .create_new(true)
1823            .open(Path::new("/test_new_filesystem/foo2.txt"))
1824            .unwrap();
1825        file_write.write_all(b"hello").await.unwrap();
1826        let _ = std::fs::remove_file("/test_new_filesystem/foo2.txt");
1827    }
1828
1829    #[tokio::test]
1830    async fn test_create_dir() {
1831        let fs = gen_filesystem();
1832
1833        assert_eq!(fs.create_dir(Path::new("/")), Ok(()));
1834
1835        assert_eq!(fs.create_dir(Path::new("/test_create_dir")), Ok(()));
1836
1837        assert_eq!(
1838            fs.create_dir(Path::new("/test_create_dir/foo")),
1839            Ok(()),
1840            "creating a directory",
1841        );
1842
1843        let cur_dir = read_dir_names(&fs, "/test_create_dir");
1844
1845        if !cur_dir.contains(&"foo".to_string()) {
1846            panic!("cur_dir does not contain foo: {cur_dir:#?}");
1847        }
1848
1849        assert!(
1850            cur_dir.contains(&"foo".to_string()),
1851            "the root is updated and well-defined"
1852        );
1853
1854        assert_eq!(
1855            fs.create_dir(Path::new("/test_create_dir/foo/bar")),
1856            Ok(()),
1857            "creating a sub-directory",
1858        );
1859
1860        let foo_dir = read_dir_names(&fs, "/test_create_dir/foo");
1861
1862        assert!(
1863            foo_dir.contains(&"bar".to_string()),
1864            "the foo directory is updated and well-defined"
1865        );
1866
1867        let bar_dir = read_dir_names(&fs, "/test_create_dir/foo/bar");
1868
1869        assert!(
1870            bar_dir.is_empty(),
1871            "the foo directory is updated and well-defined"
1872        );
1873        let _ = fs_extra::remove_items(&["/test_create_dir"]);
1874    }
1875
1876    #[tokio::test]
1877    async fn test_remove_dir() {
1878        let fs = gen_filesystem();
1879
1880        assert_eq!(
1881            fs.remove_dir(Path::new("/")),
1882            Err(FsError::PermissionDenied),
1883            "cannot remove the root directory",
1884        );
1885
1886        assert_eq!(
1887            fs.remove_dir(Path::new("/foo")),
1888            Err(FsError::EntryNotFound),
1889            "cannot remove a directory that doesn't exist",
1890        );
1891
1892        assert_eq!(fs.create_dir(Path::new("/test_remove_dir")), Ok(()));
1893
1894        assert_eq!(
1895            fs.create_dir(Path::new("/test_remove_dir/foo")),
1896            Ok(()),
1897            "creating a directory",
1898        );
1899
1900        assert_eq!(
1901            fs.create_dir(Path::new("/test_remove_dir/foo/bar")),
1902            Ok(()),
1903            "creating a sub-directory",
1904        );
1905
1906        assert!(
1907            read_dir_names(&fs, "/test_remove_dir/foo").contains(&"bar".to_string()),
1908            "./foo/bar exists"
1909        );
1910
1911        assert_eq!(
1912            fs.remove_dir(Path::new("/test_remove_dir/foo")),
1913            Err(FsError::DirectoryNotEmpty),
1914            "removing a directory that has children",
1915        );
1916
1917        assert_eq!(
1918            fs.remove_dir(Path::new("/test_remove_dir/foo/bar")),
1919            Ok(()),
1920            "removing a sub-directory",
1921        );
1922
1923        assert_eq!(
1924            fs.remove_dir(Path::new("/test_remove_dir/foo")),
1925            Ok(()),
1926            "removing a directory",
1927        );
1928
1929        assert!(
1930            !read_dir_names(&fs, "/test_remove_dir").contains(&"foo".to_string()),
1931            "the foo directory still exists"
1932        );
1933    }
1934
1935    fn read_dir_names(fs: &dyn crate::FileSystem, path: &str) -> Vec<String> {
1936        fs.read_dir(Path::new(path))
1937            .unwrap()
1938            .filter_map(|entry| Some(entry.ok()?.file_name().to_str()?.to_string()))
1939            .collect::<Vec<_>>()
1940    }
1941
1942    fn create_dir_all(fs: &mem_fs::FileSystem, path: &Path) {
1943        let mut current = PathBuf::from("/");
1944
1945        for component in path.iter().skip(1) {
1946            current.push(component);
1947
1948            if fs.metadata(&current).is_err() {
1949                fs.create_dir(&current).unwrap();
1950            }
1951        }
1952    }
1953
1954    #[tokio::test]
1955    async fn test_rename() {
1956        let fs = gen_filesystem();
1957
1958        assert_eq!(
1959            fs.rename(Path::new("/"), Path::new("/bar")).await,
1960            Err(FsError::PermissionDenied),
1961            "renaming a directory that has no parent",
1962        );
1963        assert_eq!(
1964            fs.rename(Path::new("/foo"), Path::new("/")).await,
1965            Err(FsError::PermissionDenied),
1966            "renaming to the synthetic root directory is rejected",
1967        );
1968
1969        assert_eq!(fs.create_dir(Path::new("/test_rename")), Ok(()));
1970        assert_eq!(fs.create_dir(Path::new("/test_rename/foo")), Ok(()));
1971        assert_eq!(fs.create_dir(Path::new("/test_rename/foo/qux")), Ok(()));
1972
1973        assert_eq!(
1974            fs.rename(
1975                Path::new("/test_rename/foo"),
1976                Path::new("/test_rename/bar/baz")
1977            )
1978            .await,
1979            Err(FsError::EntryNotFound),
1980            "renaming to a directory that has parent that doesn't exist",
1981        );
1982
1983        assert_eq!(fs.create_dir(Path::new("/test_rename/bar")), Ok(()));
1984
1985        assert_eq!(
1986            fs.rename(Path::new("/test_rename/foo"), Path::new("/test_rename/bar"))
1987                .await,
1988            Ok(()),
1989            "renaming to a directory that has parent that exists",
1990        );
1991
1992        assert!(
1993            fs.new_open_options()
1994                .write(true)
1995                .create_new(true)
1996                .open(Path::new("/test_rename/bar/hello1.txt"))
1997                .is_ok(),
1998            "creating a new file (`hello1.txt`)",
1999        );
2000        assert!(
2001            fs.new_open_options()
2002                .write(true)
2003                .create_new(true)
2004                .open(Path::new("/test_rename/bar/hello2.txt"))
2005                .is_ok(),
2006            "creating a new file (`hello2.txt`)",
2007        );
2008
2009        let cur_dir = read_dir_names(&fs, "/test_rename");
2010
2011        assert!(
2012            !cur_dir.contains(&"foo".to_string()),
2013            "the foo directory still exists"
2014        );
2015
2016        assert!(
2017            cur_dir.contains(&"bar".to_string()),
2018            "the bar directory still exists"
2019        );
2020
2021        let bar_dir = read_dir_names(&fs, "/test_rename/bar");
2022
2023        if !bar_dir.contains(&"qux".to_string()) {
2024            println!("qux does not exist: {bar_dir:?}")
2025        }
2026
2027        let qux_dir = read_dir_names(&fs, "/test_rename/bar/qux");
2028
2029        assert!(qux_dir.is_empty(), "the qux directory is empty");
2030
2031        assert!(
2032            read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
2033            "the /bar/hello1.txt file exists"
2034        );
2035
2036        assert!(
2037            read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
2038            "the /bar/hello2.txt file exists"
2039        );
2040
2041        assert_eq!(
2042            fs.create_dir(Path::new("/test_rename/foo")),
2043            Ok(()),
2044            "create ./foo again",
2045        );
2046
2047        assert_eq!(
2048            fs.rename(
2049                Path::new("/test_rename/bar/hello2.txt"),
2050                Path::new("/test_rename/foo/world2.txt")
2051            )
2052            .await,
2053            Ok(()),
2054            "renaming (and moving) a file",
2055        );
2056
2057        assert_eq!(
2058            fs.rename(
2059                Path::new("/test_rename/foo"),
2060                Path::new("/test_rename/bar/baz")
2061            )
2062            .await,
2063            Ok(()),
2064            "renaming a directory",
2065        );
2066
2067        assert_eq!(
2068            fs.rename(
2069                Path::new("/test_rename/bar/hello1.txt"),
2070                Path::new("/test_rename/bar/world1.txt")
2071            )
2072            .await,
2073            Ok(()),
2074            "renaming a file (in the same directory)",
2075        );
2076
2077        assert!(
2078            read_dir_names(&fs, "/test_rename").contains(&"bar".to_string()),
2079            "./bar exists"
2080        );
2081
2082        assert!(
2083            read_dir_names(&fs, "/test_rename/bar").contains(&"baz".to_string()),
2084            "/bar/baz exists"
2085        );
2086        assert!(
2087            !read_dir_names(&fs, "/test_rename").contains(&"foo".to_string()),
2088            "foo does not exist anymore"
2089        );
2090        assert!(
2091            read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
2092            "/bar/baz/world2.txt exists"
2093        );
2094        assert!(
2095            read_dir_names(&fs, "/test_rename/bar").contains(&"world1.txt".to_string()),
2096            "/bar/world1.txt (ex hello1.txt) exists"
2097        );
2098        assert!(
2099            !read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
2100            "hello1.txt was moved"
2101        );
2102        assert!(
2103            !read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
2104            "hello2.txt was moved"
2105        );
2106        assert!(
2107            read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
2108            "world2.txt was moved to the correct place"
2109        );
2110
2111        let _ = fs_extra::remove_items(&["/test_rename"]);
2112    }
2113
2114    #[tokio::test]
2115    async fn cross_mount_file_rename_copies_and_removes_source() {
2116        let fs = MountFileSystem::new();
2117        let left = TmpFileSystem::new();
2118        let right = TmpFileSystem::new();
2119
2120        left.new_open_options()
2121            .create(true)
2122            .write(true)
2123            .open(Path::new("/from.txt"))
2124            .unwrap();
2125
2126        fs.mount(Path::new("/left"), Arc::new(left.clone()))
2127            .unwrap();
2128        fs.mount(Path::new("/right"), Arc::new(right.clone()))
2129            .unwrap();
2130
2131        fs.rename(Path::new("/left/from.txt"), Path::new("/right/to.txt"))
2132            .await
2133            .unwrap();
2134
2135        assert_eq!(
2136            left.metadata(Path::new("/from.txt")),
2137            Err(FsError::EntryNotFound)
2138        );
2139        assert!(right.metadata(Path::new("/to.txt")).unwrap().is_file());
2140    }
2141
2142    #[tokio::test]
2143    async fn test_metadata() {
2144        use std::thread::sleep;
2145        use std::time::Duration;
2146
2147        let fs = gen_filesystem();
2148
2149        let root_metadata = fs.metadata(Path::new("/test_metadata")).unwrap();
2150
2151        assert!(root_metadata.ft.dir);
2152        assert_eq!(root_metadata.accessed, root_metadata.created);
2153        assert_eq!(root_metadata.modified, root_metadata.created);
2154        assert!(root_metadata.modified > 0);
2155
2156        assert_eq!(fs.create_dir(Path::new("/test_metadata/foo")), Ok(()));
2157
2158        let foo_metadata = fs.metadata(Path::new("/test_metadata/foo"));
2159        assert!(foo_metadata.is_ok());
2160        let foo_metadata = foo_metadata.unwrap();
2161
2162        assert!(foo_metadata.ft.dir);
2163        assert!(foo_metadata.accessed == foo_metadata.created);
2164        assert!(foo_metadata.modified == foo_metadata.created);
2165        assert!(foo_metadata.modified > 0);
2166
2167        sleep(Duration::from_secs(3));
2168
2169        assert_eq!(
2170            fs.rename(
2171                Path::new("/test_metadata/foo"),
2172                Path::new("/test_metadata/bar")
2173            )
2174            .await,
2175            Ok(())
2176        );
2177
2178        let bar_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
2179        assert!(bar_metadata.ft.dir);
2180        assert!(bar_metadata.accessed == foo_metadata.accessed);
2181        assert!(bar_metadata.created == foo_metadata.created);
2182        assert!(bar_metadata.modified > foo_metadata.modified);
2183
2184        let root_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
2185        assert!(
2186            root_metadata.modified > foo_metadata.modified,
2187            "the parent modified time was updated"
2188        );
2189
2190        let _ = fs_extra::remove_items(&["/test_metadata"]);
2191    }
2192
2193    #[tokio::test]
2194    async fn test_remove_file() {
2195        let fs = gen_filesystem();
2196
2197        assert!(
2198            fs.new_open_options()
2199                .write(true)
2200                .create_new(true)
2201                .open(Path::new("/test_remove_file/foo.txt"))
2202                .is_ok(),
2203            "creating a new file",
2204        );
2205
2206        assert!(read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
2207
2208        assert_eq!(
2209            fs.remove_file(Path::new("/test_remove_file/foo.txt")),
2210            Ok(()),
2211            "removing a file that exists",
2212        );
2213
2214        assert!(!read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
2215
2216        assert_eq!(
2217            fs.remove_file(Path::new("/test_remove_file/foo.txt")),
2218            Err(FsError::EntryNotFound),
2219            "removing a file that doesn't exists",
2220        );
2221
2222        let _ = fs_extra::remove_items(&["./test_remove_file"]);
2223    }
2224
2225    #[tokio::test]
2226    async fn test_readdir() {
2227        let fs = gen_filesystem();
2228
2229        assert_eq!(
2230            fs.create_dir(Path::new("/test_readdir/foo")),
2231            Ok(()),
2232            "creating `foo`"
2233        );
2234        assert_eq!(
2235            fs.create_dir(Path::new("/test_readdir/foo/sub")),
2236            Ok(()),
2237            "creating `sub`"
2238        );
2239        assert_eq!(
2240            fs.create_dir(Path::new("/test_readdir/bar")),
2241            Ok(()),
2242            "creating `bar`"
2243        );
2244        assert_eq!(
2245            fs.create_dir(Path::new("/test_readdir/baz")),
2246            Ok(()),
2247            "creating `bar`"
2248        );
2249        assert!(
2250            fs.new_open_options()
2251                .write(true)
2252                .create_new(true)
2253                .open(Path::new("/test_readdir/a.txt"))
2254                .is_ok(),
2255            "creating `a.txt`",
2256        );
2257        assert!(
2258            fs.new_open_options()
2259                .write(true)
2260                .create_new(true)
2261                .open(Path::new("/test_readdir/b.txt"))
2262                .is_ok(),
2263            "creating `b.txt`",
2264        );
2265
2266        println!("fs: {fs:?}");
2267
2268        let readdir = fs.read_dir(Path::new("/test_readdir"));
2269
2270        assert!(readdir.is_ok(), "reading the directory `/test_readdir/`");
2271
2272        let mut readdir = readdir.unwrap();
2273
2274        let next = readdir.next().unwrap().unwrap();
2275        assert!(next.path.ends_with("foo"), "checking entry #1");
2276        println!("entry 1: {next:#?}");
2277        assert!(next.file_type().unwrap().is_dir(), "checking entry #1");
2278
2279        let next = readdir.next().unwrap().unwrap();
2280        assert!(next.path.ends_with("bar"), "checking entry #2");
2281        assert!(next.file_type().unwrap().is_dir(), "checking entry #2");
2282
2283        let next = readdir.next().unwrap().unwrap();
2284        assert!(next.path.ends_with("baz"), "checking entry #3");
2285        assert!(next.file_type().unwrap().is_dir(), "checking entry #3");
2286
2287        let next = readdir.next().unwrap().unwrap();
2288        assert!(next.path.ends_with("a.txt"), "checking entry #2");
2289        assert!(next.file_type().unwrap().is_file(), "checking entry #4");
2290
2291        let next = readdir.next().unwrap().unwrap();
2292        assert!(next.path.ends_with("b.txt"), "checking entry #2");
2293        assert!(next.file_type().unwrap().is_file(), "checking entry #5");
2294
2295        if let Some(s) = readdir.next() {
2296            panic!("next: {s:?}");
2297        }
2298
2299        let _ = fs_extra::remove_items(&["./test_readdir"]);
2300    }
2301}