1use 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; type 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 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 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#[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 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 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 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(¤t).is_err() {
1870 fs.create_dir(¤t).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}