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