1use super::{DirEntry, Filesystem};
6use kaish_vfs::PathAccess;
7use async_trait::async_trait;
8use std::collections::BTreeMap;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13pub use kaish_types::backend::MountInfo;
17
18const SYNTHESIZED_DIRECTORY_MODE: u32 = 0o555;
23
24#[derive(Default)]
30pub struct VfsRouter {
31 mounts: BTreeMap<PathBuf, Arc<dyn Filesystem>>,
33}
34
35impl std::fmt::Debug for VfsRouter {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("VfsRouter")
38 .field("mounts", &self.mounts.keys().collect::<Vec<_>>())
39 .finish()
40 }
41}
42
43impl VfsRouter {
44 pub fn new() -> Self {
46 Self {
47 mounts: BTreeMap::new(),
48 }
49 }
50
51 pub fn mount(&mut self, path: impl Into<PathBuf>, fs: impl Filesystem + 'static) {
56 let path = Self::normalize_mount_path(path.into());
57 self.mounts.insert(path, Arc::new(fs));
58 }
59
60 pub fn mount_arc(&mut self, path: impl Into<PathBuf>, fs: Arc<dyn Filesystem>) {
62 let path = Self::normalize_mount_path(path.into());
63 self.mounts.insert(path, fs);
64 }
65
66 pub fn unmount(&mut self, path: impl AsRef<Path>) -> bool {
70 let path = Self::normalize_mount_path(path.as_ref().to_path_buf());
71 self.mounts.remove(&path).is_some()
72 }
73
74 pub fn list_mounts(&self) -> Vec<MountInfo> {
76 self.mounts
77 .iter()
78 .map(|(path, fs)| MountInfo {
79 path: path.clone(),
80 read_only: fs.read_only(),
81 resident_bytes: fs.resident_bytes(),
82 })
83 .collect()
84 }
85
86 fn normalize_mount_path(path: PathBuf) -> PathBuf {
88 let s = path.to_string_lossy();
89 let s = s.trim_end_matches('/');
90 if s.is_empty() {
91 PathBuf::from("/")
92 } else if !s.starts_with('/') {
93 PathBuf::from(format!("/{}", s))
94 } else {
95 PathBuf::from(s)
96 }
97 }
98
99 pub fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
106 let (fs, relative) = self.find_mount(path).ok()?;
107 fs.real_path(&relative)
108 }
109
110 pub(crate) fn has_mount(&self, path: &Path) -> bool {
116 self.find_mount(path).is_ok()
117 }
118
119 pub(crate) fn has_mount_under(&self, dir: &Path) -> bool {
128 let dir = Self::normalize_mount_path(dir.to_path_buf());
129 let dir_str = dir.to_string_lossy();
130 self.mounts.keys().any(|mount_path| {
131 let mount_str = mount_path.to_string_lossy();
132 if dir_str == "/" {
133 mount_str != "/"
134 } else {
135 mount_str.starts_with(&format!("{}/", dir_str))
136 }
137 })
138 }
139
140 fn or_synthesized_ancestor<T>(
161 &self,
162 path: &Path,
163 error: io::Error,
164 synthesize: impl FnOnce() -> T,
165 ) -> io::Result<T> {
166 if error.kind() == io::ErrorKind::NotFound && self.has_mount_under(path) {
167 Ok(synthesize())
168 } else {
169 Err(error)
170 }
171 }
172
173 fn list_mount_children(&self, dir: &Path) -> Vec<DirEntry> {
174 let dir = Self::normalize_mount_path(dir.to_path_buf());
175 let prefix = format!("{}/", dir.to_string_lossy());
176 let mut seen = std::collections::HashSet::new();
177 let mut entries = Vec::new();
178 for mount_path in self.mounts.keys() {
179 let mount_str = mount_path.to_string_lossy();
180 if let Some(rest) = mount_str.strip_prefix(&prefix) {
181 let first = rest.split('/').next().unwrap_or("");
182 if !first.is_empty() && seen.insert(first.to_string()) {
183 entries.push(DirEntry::directory(first));
184 }
185 }
186 }
187 entries.sort_by(|a, b| a.name.cmp(&b.name));
188 entries
189 }
190
191 fn path_basename(path: &Path) -> String {
194 path.file_name()
195 .map(|n| n.to_string_lossy().into_owned())
196 .unwrap_or_else(|| "/".to_string())
197 }
198
199 fn find_mount(&self, path: &Path) -> io::Result<(Arc<dyn Filesystem>, PathBuf)> {
203 let (_, fs, relative) = self.mount_of(path)?;
204 Ok((fs, relative))
205 }
206
207 fn mount_of(&self, path: &Path) -> io::Result<(&Path, Arc<dyn Filesystem>, PathBuf)> {
210 let path_str = path.to_string_lossy();
211 let normalized = if path_str.starts_with('/') {
212 path.to_path_buf()
213 } else {
214 PathBuf::from(format!("/{}", path_str))
215 };
216
217 let mut best_match: Option<(&PathBuf, &Arc<dyn Filesystem>)> = None;
219
220 for (mount_path, fs) in &self.mounts {
221 let mount_str = mount_path.to_string_lossy();
222
223 let is_match = if mount_str == "/" {
225 true } else {
227 let normalized_str = normalized.to_string_lossy();
228 normalized_str == mount_str.as_ref()
229 || normalized_str.starts_with(&format!("{}/", mount_str))
230 };
231
232 if is_match {
233 let dominated = best_match
235 .as_ref()
236 .is_none_or(|(bp, _)| mount_path.as_os_str().len() > bp.as_os_str().len());
237 if dominated {
238 best_match = Some((mount_path, fs));
239 }
240 }
241 }
242
243 match best_match {
244 Some((mount_path, fs)) => {
245 let mount_str = mount_path.to_string_lossy();
247 let normalized_str = normalized.to_string_lossy();
248
249 let relative = if mount_str == "/" {
250 normalized_str.trim_start_matches('/').to_string()
251 } else {
252 normalized_str
253 .strip_prefix(mount_str.as_ref())
254 .unwrap_or("")
255 .trim_start_matches('/')
256 .to_string()
257 };
258
259 Ok((mount_path.as_path(), Arc::clone(fs), PathBuf::from(relative)))
260 }
261 None => Err(io::Error::new(
262 io::ErrorKind::NotFound,
263 format!("no mount point for path: {}", path.display()),
264 )),
265 }
266 }
267}
268
269fn lexical_absolute(path: &Path) -> PathBuf {
272 let mut out = PathBuf::from("/");
273 for component in path.components() {
274 match component {
275 std::path::Component::Normal(name) => out.push(name),
276 std::path::Component::ParentDir => {
277 out.pop();
278 }
279 _ => {}
280 }
281 }
282 out
283}
284
285fn relative_path_from(from: &Path, to: &Path) -> PathBuf {
289 let mut from_parts = from.components().skip(1).peekable();
290 let mut to_parts = to.components().skip(1).peekable();
291 while let (Some(a), Some(b)) = (from_parts.peek(), to_parts.peek()) {
292 if a != b {
293 break;
294 }
295 from_parts.next();
296 to_parts.next();
297 }
298 let mut relative = PathBuf::new();
299 for _ in from_parts {
300 relative.push("..");
301 }
302 for part in to_parts {
303 relative.push(part);
304 }
305 if relative.as_os_str().is_empty() {
306 relative.push(".");
307 }
308 relative
309}
310
311#[async_trait]
312impl Filesystem for VfsRouter {
313 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
314 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
315 let (fs, relative) = self.find_mount(path)?;
316 fs.read(&relative).await
317 }
318
319 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
320 async fn read_range(
321 &self,
322 path: &Path,
323 range: Option<kaish_vfs::ReadRange>,
324 ) -> io::Result<Vec<u8>> {
325 let (fs, relative) = self.find_mount(path)?;
330 fs.read_range(&relative, range).await
331 }
332
333 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
334 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
335 let (fs, relative) = self.find_mount(path)?;
336 fs.write(&relative, data).await
337 }
338
339 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
340 async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
341 let (fs, relative) = self.find_mount(path)?;
347 fs.append(&relative, data).await
348 }
349
350 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
351 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
352 let path_str = path.to_string_lossy();
354 if path_str.is_empty() || path_str == "/" {
355 return self.list_root().await;
356 }
357
358 let answer = match self.find_mount(path) {
359 Ok((fs, relative)) => fs.list(&relative).await,
360 Err(e) => Err(e),
361 };
362 match answer {
363 Ok(entries) => Ok(entries),
364 Err(e) => self.or_synthesized_ancestor(path, e, || self.list_mount_children(path)),
367 }
368 }
369
370 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
371 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
372 let path_str = path.to_string_lossy();
374 if path_str.is_empty() || path_str == "/" {
375 return Ok(DirEntry::directory("/"));
376 }
377
378 let normalized = Self::normalize_mount_path(path.to_path_buf());
380 if self.mounts.contains_key(&normalized) {
381 let name = path
382 .file_name()
383 .map(|n| n.to_string_lossy().into_owned())
384 .unwrap_or_else(|| "/".to_string());
385 return Ok(DirEntry::directory(name));
386 }
387
388 let answer = match self.find_mount(path) {
389 Ok((fs, relative)) => fs.stat(&relative).await,
390 Err(e) => Err(e),
391 };
392 match answer {
393 Ok(entry) => Ok(entry),
394 Err(e) => {
397 self.or_synthesized_ancestor(path, e, || {
398 DirEntry::directory(Self::path_basename(path))
399 })
400 }
401 }
402 }
403
404 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
405 let (fs, relative) = self.find_mount(path)?;
406 fs.read_link(&relative).await
407 }
408
409 async fn canonicalize(&self, path: &Path, allow_missing_final: bool) -> io::Result<PathBuf> {
422 let normalized = lexical_absolute(path);
423 if normalized == Path::new("/") {
424 return Ok(PathBuf::from("/"));
425 }
426
427 let answer = match self.mount_of(&normalized) {
428 Ok((mount_path, fs, relative)) => {
429 let mount_path = mount_path.to_path_buf();
430 fs.canonicalize(&relative, allow_missing_final)
431 .await
432 .map(|resolved| mount_path.join(resolved))
433 }
434 Err(e) => Err(e),
435 };
436 match answer {
437 Ok(resolved) => Ok(resolved),
438 Err(e) => self.or_synthesized_ancestor(&normalized, e, || normalized.clone()),
439 }
440 }
441
442 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
443 let (link_mount, fs, relative_link) = self.mount_of(link)?;
444 let target = if target.is_absolute() {
449 let target = lexical_absolute(target);
450 let (target_mount, _, _) = self.mount_of(&target)?;
451 if target_mount != link_mount {
452 return Err(io::Error::new(
453 io::ErrorKind::InvalidInput,
454 format!(
455 "symlink target {} is on mount {} and the link {} is on mount {}; a link cannot cross mounts",
456 target.display(),
457 target_mount.display(),
458 link.display(),
459 link_mount.display()
460 ),
461 ));
462 }
463 let link_dir = lexical_absolute(link);
464 let link_dir = link_dir.parent().unwrap_or(Path::new("/"));
465 relative_path_from(link_dir, &target)
466 } else {
467 target.to_path_buf()
468 };
469 fs.symlink(&target, &relative_link).await
470 }
471
472 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
473 let path_str = path.to_string_lossy();
475 if path_str.is_empty() || path_str == "/" {
476 return Ok(DirEntry::directory("/"));
477 }
478
479 let normalized = Self::normalize_mount_path(path.to_path_buf());
481 if self.mounts.contains_key(&normalized) {
482 let name = path
483 .file_name()
484 .map(|n| n.to_string_lossy().into_owned())
485 .unwrap_or_else(|| "/".to_string());
486 return Ok(DirEntry::directory(name));
487 }
488
489 let answer = match self.find_mount(path) {
490 Ok((fs, relative)) => fs.lstat(&relative).await,
491 Err(e) => Err(e),
492 };
493 match answer {
494 Ok(entry) => Ok(entry),
495 Err(e) => {
498 self.or_synthesized_ancestor(path, e, || {
499 DirEntry::directory(Self::path_basename(path))
500 })
501 }
502 }
503 }
504
505 async fn mkdir(&self, path: &Path) -> io::Result<()> {
506 let (fs, relative) = self.find_mount(path)?;
507 fs.mkdir(&relative).await
508 }
509
510 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> io::Result<()> {
511 let (fs, relative) = self.find_mount(path)?;
512 fs.set_mtime(&relative, mtime).await
513 }
514
515 async fn remove(&self, path: &Path) -> io::Result<()> {
516 let (fs, relative) = self.find_mount(path)?;
517 fs.remove(&relative).await
518 }
519
520 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
521 let (from_fs, from_relative) = self.find_mount(from)?;
522 let (to_fs, to_relative) = self.find_mount(to)?;
523
524 if !Arc::ptr_eq(&from_fs, &to_fs) {
526 return Err(io::Error::new(
527 io::ErrorKind::Unsupported,
528 "cannot rename across different mount points",
529 ));
530 }
531
532 from_fs.rename(&from_relative, &to_relative).await
533 }
534
535 async fn path_access(&self, path: &Path) -> io::Result<PathAccess> {
547 let path_str = path.to_string_lossy();
548 if path_str.is_empty() || path_str == "/" {
549 return Ok(PathAccess::resolve(Some(SYNTHESIZED_DIRECTORY_MODE), true));
550 }
551 let answer = match self.find_mount(path) {
552 Ok((fs, relative)) => fs.path_access(&relative).await,
553 Err(e) => Err(e),
554 };
555 match answer {
556 Ok(access) => Ok(access),
557 Err(e) => self.or_synthesized_ancestor(path, e, || {
558 PathAccess::resolve(Some(SYNTHESIZED_DIRECTORY_MODE), true)
559 }),
560 }
561 }
562
563 fn read_only(&self) -> bool {
564 if self.mounts.is_empty() {
568 return false;
569 }
570 self.mounts.values().all(|fs| fs.read_only())
571 }
572}
573
574impl VfsRouter {
575 async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
577 let mut entries = Vec::new();
578 let mut seen_names = std::collections::HashSet::new();
579
580 for mount_path in self.mounts.keys() {
581 let mount_str = mount_path.to_string_lossy();
582 if mount_str == "/" {
583 if let Some(fs) = self.mounts.get(mount_path)
585 && let Ok(root_entries) = fs.list(Path::new("")).await {
586 for entry in root_entries {
587 if seen_names.insert(entry.name.clone()) {
588 entries.push(entry);
589 }
590 }
591 }
592 } else {
593 let first_component = mount_str
595 .trim_start_matches('/')
596 .split('/')
597 .next()
598 .unwrap_or("");
599
600 if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
601 entries.push(DirEntry::directory(first_component));
602 }
603 }
604 }
605
606 entries.sort_by(|a, b| a.name.cmp(&b.name));
607 Ok(entries)
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::vfs::MemoryFs;
615
616 #[tokio::test]
617 async fn symlink_absolute_target_on_the_same_mount_is_stored_relative() {
618 let mut router = VfsRouter::new();
619 let data = MemoryFs::new();
620 data.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
621 data.mkdir(Path::new("home")).await.unwrap();
622 router.mount("/data", data);
623
624 router
625 .symlink(Path::new("/data/etc/hosts"), Path::new("/data/home/link"))
626 .await
627 .unwrap();
628
629 assert_eq!(
630 router.read_link(Path::new("/data/home/link")).await.unwrap(),
631 PathBuf::from("../etc/hosts")
632 );
633 assert_eq!(router.read(Path::new("/data/home/link")).await.unwrap(), b"hosts");
634 }
635
636 #[tokio::test]
637 async fn symlink_absolute_target_beside_the_link_is_a_bare_name() {
638 let mut router = VfsRouter::new();
639 let root = MemoryFs::new();
640 root.write(Path::new("a/target"), b"t").await.unwrap();
641 router.mount("/", root);
642
643 router
644 .symlink(Path::new("/a/target"), Path::new("/a/link"))
645 .await
646 .unwrap();
647 assert_eq!(
648 router.read_link(Path::new("/a/link")).await.unwrap(),
649 PathBuf::from("target")
650 );
651 }
652
653 #[tokio::test]
654 async fn symlink_absolute_target_with_dotdot_is_normalized_first() {
655 let mut router = VfsRouter::new();
656 let root = MemoryFs::new();
657 root.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
658 root.mkdir(Path::new("home")).await.unwrap();
659 router.mount("/", root);
660
661 router
662 .symlink(Path::new("/home/../etc/./hosts"), Path::new("/home/link"))
663 .await
664 .unwrap();
665 assert_eq!(
666 router.read_link(Path::new("/home/link")).await.unwrap(),
667 PathBuf::from("../etc/hosts")
668 );
669 assert_eq!(router.read(Path::new("/home/link")).await.unwrap(), b"hosts");
670 }
671
672 #[tokio::test]
673 async fn symlink_relative_target_is_stored_verbatim() {
674 let mut router = VfsRouter::new();
675 router.mount("/data", MemoryFs::new());
676
677 router
678 .symlink(Path::new("../x/../y"), Path::new("/data/d/link"))
679 .await
680 .unwrap();
681 assert_eq!(
682 router.read_link(Path::new("/data/d/link")).await.unwrap(),
683 PathBuf::from("../x/../y")
684 );
685 }
686
687 #[tokio::test]
688 async fn symlink_across_mounts_is_refused_and_creates_nothing() {
689 let mut router = VfsRouter::new();
690 router.mount("/data", MemoryFs::new());
691 let scratch = MemoryFs::new();
692 scratch.write(Path::new("x"), b"x").await.unwrap();
693 router.mount("/scratch", scratch);
694
695 let error = router
696 .symlink(Path::new("/scratch/x"), Path::new("/data/link"))
697 .await
698 .unwrap_err();
699 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
700 let message = error.to_string();
701 assert!(message.contains("/scratch") && message.contains("/data"), "{message}");
702 assert!(router.lstat(Path::new("/data/link")).await.is_err(), "nothing created");
703 }
704
705 #[test]
706 fn relative_path_from_walks_up_then_down() {
707 let rel = |from: &str, to: &str| relative_path_from(Path::new(from), Path::new(to));
708 assert_eq!(rel("/a/b", "/a/c/d"), PathBuf::from("../c/d"));
709 assert_eq!(rel("/a", "/a/x"), PathBuf::from("x"));
710 assert_eq!(rel("/", "/x/y"), PathBuf::from("x/y"));
711 assert_eq!(rel("/a/b/c", "/"), PathBuf::from("../../.."));
712 assert_eq!(rel("/a/b", "/a/b"), PathBuf::from("."));
713 }
714
715 #[tokio::test]
716 async fn test_basic_mount() {
717 let mut router = VfsRouter::new();
718 let scratch = MemoryFs::new();
719 scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
720 router.mount("/scratch", scratch);
721
722 let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
723 assert_eq!(data, b"hello");
724 }
725
726 #[tokio::test]
727 async fn test_multiple_mounts() {
728 let mut router = VfsRouter::new();
729
730 let scratch = MemoryFs::new();
731 scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
732 router.mount("/scratch", scratch);
733
734 let data = MemoryFs::new();
735 data.write(Path::new("b.txt"), b"data").await.unwrap();
736 router.mount("/data", data);
737
738 assert_eq!(
739 router.read(Path::new("/scratch/a.txt")).await.unwrap(),
740 b"scratch"
741 );
742 assert_eq!(
743 router.read(Path::new("/data/b.txt")).await.unwrap(),
744 b"data"
745 );
746 }
747
748 #[tokio::test]
749 async fn test_nested_mount() {
750 let mut router = VfsRouter::new();
751
752 let outer = MemoryFs::new();
753 outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
754 router.mount("/mnt", outer);
755
756 let inner = MemoryFs::new();
757 inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
758 router.mount("/mnt/project", inner);
759
760 assert_eq!(
762 router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
763 b"outer"
764 );
765
766 assert_eq!(
768 router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
769 b"inner"
770 );
771 }
772
773 #[tokio::test]
774 async fn test_list_root() {
775 let mut router = VfsRouter::new();
776 router.mount("/scratch", MemoryFs::new());
777 router.mount("/mnt/a", MemoryFs::new());
778 router.mount("/mnt/b", MemoryFs::new());
779
780 let entries = router.list(Path::new("/")).await.unwrap();
781 let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
782
783 assert!(names.contains(&&"scratch".to_string()));
784 assert!(names.contains(&&"mnt".to_string()));
785 }
786
787 #[tokio::test]
788 async fn test_unmount() {
789 let mut router = VfsRouter::new();
790
791 let fs = MemoryFs::new();
792 fs.write(Path::new("test.txt"), b"data").await.unwrap();
793 router.mount("/scratch", fs);
794
795 assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
796
797 router.unmount("/scratch");
798
799 assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
800 }
801
802 #[tokio::test]
803 async fn test_list_mounts() {
804 let mut router = VfsRouter::new();
805 router.mount("/scratch", MemoryFs::new());
806 router.mount("/data", MemoryFs::new());
807
808 let mounts = router.list_mounts();
809 assert_eq!(mounts.len(), 2);
810
811 let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
812 assert!(paths.contains(&&PathBuf::from("/scratch")));
813 assert!(paths.contains(&&PathBuf::from("/data")));
814 }
815
816 #[tokio::test]
817 async fn test_no_mount_error() {
818 let router = VfsRouter::new();
819 let result = router.read(Path::new("/nothing/here.txt")).await;
820 assert!(result.is_err());
821 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
822 }
823
824 #[tokio::test]
825 async fn test_root_mount() {
826 let mut router = VfsRouter::new();
827
828 let root = MemoryFs::new();
829 root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
830 router.mount("/", root);
831
832 let data = router.read(Path::new("/at-root.txt")).await.unwrap();
833 assert_eq!(data, b"root file");
834 }
835
836 #[tokio::test]
837 async fn test_write_through_router() {
838 let mut router = VfsRouter::new();
839 router.mount("/scratch", MemoryFs::new());
840
841 router
842 .write(Path::new("/scratch/new.txt"), b"created")
843 .await
844 .unwrap();
845
846 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
847 assert_eq!(data, b"created");
848 }
849
850 #[tokio::test]
851 async fn test_stat_mount_point() {
852 let mut router = VfsRouter::new();
853 router.mount("/scratch", MemoryFs::new());
854
855 let entry = router.stat(Path::new("/scratch")).await.unwrap();
856 assert!(entry.is_dir());
857 }
858
859 #[tokio::test]
860 async fn test_stat_root() {
861 let router = VfsRouter::new();
862 let entry = router.stat(Path::new("/")).await.unwrap();
863 assert!(entry.is_dir());
864 }
865
866 #[tokio::test]
867 async fn test_rename_same_mount() {
868 let mut router = VfsRouter::new();
869 let mem = MemoryFs::new();
870 mem.write(Path::new("old.txt"), b"data").await.unwrap();
871 router.mount("/scratch", mem);
872
873 router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
874
875 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
877 assert_eq!(data, b"data");
878
879 assert!(!router.exists(Path::new("/scratch/old.txt")).await);
881 }
882
883 #[tokio::test]
884 async fn test_rename_cross_mount_fails() {
885 let mut router = VfsRouter::new();
886 let mem1 = MemoryFs::new();
887 mem1.write(Path::new("file.txt"), b"data").await.unwrap();
888 router.mount("/mount1", mem1);
889 router.mount("/mount2", MemoryFs::new());
890
891 let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
892 assert!(result.is_err());
893 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
894 }
895
896 #[tokio::test]
902 async fn path_access_agrees_with_stat_on_synthesized_directories() {
903 let mut router = VfsRouter::new();
904 router.mount("/v/docs", MemoryFs::new());
905
906 for path in ["/", "/v"] {
907 let path = Path::new(path);
908 assert!(
909 router.stat(path).await.is_ok(),
910 "{} is synthesized by stat",
911 path.display()
912 );
913 let access = router
914 .path_access(path)
915 .await
916 .unwrap_or_else(|e| panic!("path_access must not error where stat succeeds: {e}"));
917 assert!(access.readable, "{} must be readable", path.display());
918 assert!(access.executable, "{} must be searchable", path.display());
919 assert!(
920 !access.writable,
921 "the router creates nothing in {}",
922 path.display()
923 );
924 }
925 }
926
927 #[tokio::test]
929 async fn path_access_errors_on_a_path_with_no_mount() {
930 let mut router = VfsRouter::new();
931 router.mount("/v/docs", MemoryFs::new());
932 assert!(router.path_access(Path::new("/nope")).await.is_err());
933 assert!(router.path_access(Path::new("/v/docs/absent")).await.is_err());
934 }
935
936 #[tokio::test]
938 async fn path_access_at_a_mount_point_asks_the_mount() {
939 let mut router = VfsRouter::new();
940 router.mount("/rw", MemoryFs::new());
941 router.mount("/ro", BuiltinFsStub);
942
943 assert!(router.path_access(Path::new("/rw")).await.unwrap().writable);
944 assert!(!router.path_access(Path::new("/ro")).await.unwrap().writable);
945 assert!(router.path_access(Path::new("/ro")).await.unwrap().readable);
946 }
947
948 struct BuiltinFsStub;
951
952 #[async_trait]
953 impl Filesystem for BuiltinFsStub {
954 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
955 Ok(Vec::new())
956 }
957 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
958 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
959 }
960 async fn list(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
961 Ok(Vec::new())
962 }
963 async fn stat(&self, _path: &Path) -> io::Result<DirEntry> {
964 Ok(DirEntry::directory("."))
965 }
966 async fn mkdir(&self, _path: &Path) -> io::Result<()> {
967 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
968 }
969 async fn remove(&self, _path: &Path) -> io::Result<()> {
970 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
971 }
972 fn read_only(&self) -> bool {
973 true
974 }
975 }
976
977 #[tokio::test]
978 async fn read_only_empty_router_returns_false() {
979 let router = VfsRouter::new();
980 assert!(!router.read_only());
981 }
982
983 #[cfg(feature = "localfs")]
984 #[tokio::test]
985 async fn read_only_all_read_only_mounts_returns_true() {
986 use crate::vfs::LocalFs;
987
988 let t1 = tempfile::tempdir().unwrap();
989 let t2 = tempfile::tempdir().unwrap();
990
991 let mut router = VfsRouter::new();
992 router.mount("/a", LocalFs::read_only(t1.path().to_path_buf()));
993 router.mount("/b", LocalFs::read_only(t2.path().to_path_buf()));
994
995 assert!(router.read_only());
996 }
997
998 #[cfg(feature = "localfs")]
999 #[tokio::test]
1000 async fn read_only_mixed_mounts_returns_false() {
1001 use crate::vfs::LocalFs;
1002
1003 let t1 = tempfile::tempdir().unwrap();
1004
1005 let mut router = VfsRouter::new();
1006 router.mount("/ro", LocalFs::read_only(t1.path().to_path_buf()));
1007 router.mount("/rw", MemoryFs::new());
1008
1009 assert!(!router.read_only());
1010 }
1011
1012 #[tokio::test]
1019 async fn test_list_synthesizes_intermediate_dir() {
1020 let mut router = VfsRouter::new();
1021 router.mount("/v/jobs", MemoryFs::new());
1022 router.mount("/v/blobs", MemoryFs::new());
1023
1024 let entries = router.list(Path::new("/v")).await.unwrap();
1025 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1026 assert_eq!(names, vec!["blobs", "jobs"]); }
1028
1029 #[tokio::test]
1030 async fn test_stat_intermediate_dir_is_directory() {
1031 let mut router = VfsRouter::new();
1032 router.mount("/v/jobs", MemoryFs::new());
1033
1034 assert!(router.stat(Path::new("/v")).await.unwrap().is_dir());
1035 assert!(router.lstat(Path::new("/v")).await.unwrap().is_dir());
1036 }
1037
1038 #[tokio::test]
1039 async fn test_deep_intermediate_dir() {
1040 let mut router = VfsRouter::new();
1041 router.mount("/v/etc/rc", MemoryFs::new());
1042
1043 let v: Vec<_> = router.list(Path::new("/v")).await.unwrap();
1044 assert_eq!(v.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["etc"]);
1045 let etc: Vec<_> = router.list(Path::new("/v/etc")).await.unwrap();
1046 assert_eq!(etc.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["rc"]);
1047 assert!(router.stat(Path::new("/v/etc")).await.unwrap().is_dir());
1048 }
1049
1050 #[tokio::test]
1051 async fn test_has_mount_under() {
1052 let mut router = VfsRouter::new();
1053 router.mount("/v/jobs", MemoryFs::new());
1054
1055 assert!(router.has_mount_under(Path::new("/v")));
1056 assert!(router.has_mount_under(Path::new("/")));
1057 assert!(!router.has_mount_under(Path::new("/v/jobs")));
1059 assert!(!router.has_mount_under(Path::new("/other")));
1060 }
1061
1062 #[tokio::test]
1063 async fn test_nonexistent_ancestor_still_notfound() {
1064 let mut router = VfsRouter::new();
1065 router.mount("/v/jobs", MemoryFs::new());
1066
1067 assert_eq!(
1070 router.list(Path::new("/nope")).await.unwrap_err().kind(),
1071 io::ErrorKind::NotFound
1072 );
1073 assert_eq!(
1074 router.stat(Path::new("/nope")).await.unwrap_err().kind(),
1075 io::ErrorKind::NotFound
1076 );
1077 }
1078}