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