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