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