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