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 list_mount_children(&self, dir: &Path) -> Vec<DirEntry> {
146 let dir = Self::normalize_mount_path(dir.to_path_buf());
147 let prefix = format!("{}/", dir.to_string_lossy());
148 let mut seen = std::collections::HashSet::new();
149 let mut entries = Vec::new();
150 for mount_path in self.mounts.keys() {
151 let mount_str = mount_path.to_string_lossy();
152 if let Some(rest) = mount_str.strip_prefix(&prefix) {
153 let first = rest.split('/').next().unwrap_or("");
154 if !first.is_empty() && seen.insert(first.to_string()) {
155 entries.push(DirEntry::directory(first));
156 }
157 }
158 }
159 entries.sort_by(|a, b| a.name.cmp(&b.name));
160 entries
161 }
162
163 fn path_basename(path: &Path) -> String {
166 path.file_name()
167 .map(|n| n.to_string_lossy().into_owned())
168 .unwrap_or_else(|| "/".to_string())
169 }
170
171 fn find_mount(&self, path: &Path) -> io::Result<(Arc<dyn Filesystem>, PathBuf)> {
175 let (_, fs, relative) = self.mount_of(path)?;
176 Ok((fs, relative))
177 }
178
179 fn mount_of(&self, path: &Path) -> io::Result<(&Path, Arc<dyn Filesystem>, PathBuf)> {
182 let path_str = path.to_string_lossy();
183 let normalized = if path_str.starts_with('/') {
184 path.to_path_buf()
185 } else {
186 PathBuf::from(format!("/{}", path_str))
187 };
188
189 let mut best_match: Option<(&PathBuf, &Arc<dyn Filesystem>)> = None;
191
192 for (mount_path, fs) in &self.mounts {
193 let mount_str = mount_path.to_string_lossy();
194
195 let is_match = if mount_str == "/" {
197 true } else {
199 let normalized_str = normalized.to_string_lossy();
200 normalized_str == mount_str.as_ref()
201 || normalized_str.starts_with(&format!("{}/", mount_str))
202 };
203
204 if is_match {
205 let dominated = best_match
207 .as_ref()
208 .is_none_or(|(bp, _)| mount_path.as_os_str().len() > bp.as_os_str().len());
209 if dominated {
210 best_match = Some((mount_path, fs));
211 }
212 }
213 }
214
215 match best_match {
216 Some((mount_path, fs)) => {
217 let mount_str = mount_path.to_string_lossy();
219 let normalized_str = normalized.to_string_lossy();
220
221 let relative = if mount_str == "/" {
222 normalized_str.trim_start_matches('/').to_string()
223 } else {
224 normalized_str
225 .strip_prefix(mount_str.as_ref())
226 .unwrap_or("")
227 .trim_start_matches('/')
228 .to_string()
229 };
230
231 Ok((mount_path.as_path(), Arc::clone(fs), PathBuf::from(relative)))
232 }
233 None => Err(io::Error::new(
234 io::ErrorKind::NotFound,
235 format!("no mount point for path: {}", path.display()),
236 )),
237 }
238 }
239}
240
241fn lexical_absolute(path: &Path) -> PathBuf {
244 let mut out = PathBuf::from("/");
245 for component in path.components() {
246 match component {
247 std::path::Component::Normal(name) => out.push(name),
248 std::path::Component::ParentDir => {
249 out.pop();
250 }
251 _ => {}
252 }
253 }
254 out
255}
256
257fn relative_path_from(from: &Path, to: &Path) -> PathBuf {
261 let mut from_parts = from.components().skip(1).peekable();
262 let mut to_parts = to.components().skip(1).peekable();
263 while let (Some(a), Some(b)) = (from_parts.peek(), to_parts.peek()) {
264 if a != b {
265 break;
266 }
267 from_parts.next();
268 to_parts.next();
269 }
270 let mut relative = PathBuf::new();
271 for _ in from_parts {
272 relative.push("..");
273 }
274 for part in to_parts {
275 relative.push(part);
276 }
277 if relative.as_os_str().is_empty() {
278 relative.push(".");
279 }
280 relative
281}
282
283#[async_trait]
284impl Filesystem for VfsRouter {
285 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
286 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
287 let (fs, relative) = self.find_mount(path)?;
288 fs.read(&relative).await
289 }
290
291 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
292 async fn read_range(
293 &self,
294 path: &Path,
295 range: Option<kaish_vfs::ReadRange>,
296 ) -> io::Result<Vec<u8>> {
297 let (fs, relative) = self.find_mount(path)?;
302 fs.read_range(&relative, range).await
303 }
304
305 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
306 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
307 let (fs, relative) = self.find_mount(path)?;
308 fs.write(&relative, data).await
309 }
310
311 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
312 async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
313 let (fs, relative) = self.find_mount(path)?;
319 fs.append(&relative, data).await
320 }
321
322 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
323 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
324 let path_str = path.to_string_lossy();
326 if path_str.is_empty() || path_str == "/" {
327 return self.list_root().await;
328 }
329
330 match self.find_mount(path) {
331 Ok((fs, relative)) => fs.list(&relative).await,
332 Err(e) => {
335 if self.has_mount_under(path) {
336 Ok(self.list_mount_children(path))
337 } else {
338 Err(e)
339 }
340 }
341 }
342 }
343
344 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
345 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
346 let path_str = path.to_string_lossy();
348 if path_str.is_empty() || path_str == "/" {
349 return Ok(DirEntry::directory("/"));
350 }
351
352 let normalized = Self::normalize_mount_path(path.to_path_buf());
354 if self.mounts.contains_key(&normalized) {
355 let name = path
356 .file_name()
357 .map(|n| n.to_string_lossy().into_owned())
358 .unwrap_or_else(|| "/".to_string());
359 return Ok(DirEntry::directory(name));
360 }
361
362 match self.find_mount(path) {
363 Ok((fs, relative)) => fs.stat(&relative).await,
364 Err(e) => {
367 if self.has_mount_under(path) {
368 Ok(DirEntry::directory(Self::path_basename(path)))
369 } else {
370 Err(e)
371 }
372 }
373 }
374 }
375
376 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
377 let (fs, relative) = self.find_mount(path)?;
378 fs.read_link(&relative).await
379 }
380
381 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
382 let (link_mount, fs, relative_link) = self.mount_of(link)?;
383 let target = if target.is_absolute() {
388 let target = lexical_absolute(target);
389 let (target_mount, _, _) = self.mount_of(&target)?;
390 if target_mount != link_mount {
391 return Err(io::Error::new(
392 io::ErrorKind::InvalidInput,
393 format!(
394 "symlink target {} is on mount {} and the link {} is on mount {}; a link cannot cross mounts",
395 target.display(),
396 target_mount.display(),
397 link.display(),
398 link_mount.display()
399 ),
400 ));
401 }
402 let link_dir = lexical_absolute(link);
403 let link_dir = link_dir.parent().unwrap_or(Path::new("/"));
404 relative_path_from(link_dir, &target)
405 } else {
406 target.to_path_buf()
407 };
408 fs.symlink(&target, &relative_link).await
409 }
410
411 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
412 let path_str = path.to_string_lossy();
414 if path_str.is_empty() || path_str == "/" {
415 return Ok(DirEntry::directory("/"));
416 }
417
418 let normalized = Self::normalize_mount_path(path.to_path_buf());
420 if self.mounts.contains_key(&normalized) {
421 let name = path
422 .file_name()
423 .map(|n| n.to_string_lossy().into_owned())
424 .unwrap_or_else(|| "/".to_string());
425 return Ok(DirEntry::directory(name));
426 }
427
428 match self.find_mount(path) {
429 Ok((fs, relative)) => fs.lstat(&relative).await,
430 Err(e) => {
433 if self.has_mount_under(path) {
434 Ok(DirEntry::directory(Self::path_basename(path)))
435 } else {
436 Err(e)
437 }
438 }
439 }
440 }
441
442 async fn mkdir(&self, path: &Path) -> io::Result<()> {
443 let (fs, relative) = self.find_mount(path)?;
444 fs.mkdir(&relative).await
445 }
446
447 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> io::Result<()> {
448 let (fs, relative) = self.find_mount(path)?;
449 fs.set_mtime(&relative, mtime).await
450 }
451
452 async fn remove(&self, path: &Path) -> io::Result<()> {
453 let (fs, relative) = self.find_mount(path)?;
454 fs.remove(&relative).await
455 }
456
457 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
458 let (from_fs, from_relative) = self.find_mount(from)?;
459 let (to_fs, to_relative) = self.find_mount(to)?;
460
461 if !Arc::ptr_eq(&from_fs, &to_fs) {
463 return Err(io::Error::new(
464 io::ErrorKind::Unsupported,
465 "cannot rename across different mount points",
466 ));
467 }
468
469 from_fs.rename(&from_relative, &to_relative).await
470 }
471
472 async fn path_access(&self, path: &Path) -> io::Result<PathAccess> {
484 match self.find_mount(path) {
485 Ok((fs, relative)) => fs.path_access(&relative).await,
486 Err(e) => {
487 let path_str = path.to_string_lossy();
488 let is_synthesized =
489 path_str.is_empty() || path_str == "/" || self.has_mount_under(path);
490 if is_synthesized {
491 Ok(PathAccess::resolve(Some(SYNTHESIZED_DIRECTORY_MODE), true))
492 } else {
493 Err(e)
494 }
495 }
496 }
497 }
498
499 fn read_only(&self) -> bool {
500 if self.mounts.is_empty() {
504 return false;
505 }
506 self.mounts.values().all(|fs| fs.read_only())
507 }
508}
509
510impl VfsRouter {
511 async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
513 let mut entries = Vec::new();
514 let mut seen_names = std::collections::HashSet::new();
515
516 for mount_path in self.mounts.keys() {
517 let mount_str = mount_path.to_string_lossy();
518 if mount_str == "/" {
519 if let Some(fs) = self.mounts.get(mount_path)
521 && let Ok(root_entries) = fs.list(Path::new("")).await {
522 for entry in root_entries {
523 if seen_names.insert(entry.name.clone()) {
524 entries.push(entry);
525 }
526 }
527 }
528 } else {
529 let first_component = mount_str
531 .trim_start_matches('/')
532 .split('/')
533 .next()
534 .unwrap_or("");
535
536 if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
537 entries.push(DirEntry::directory(first_component));
538 }
539 }
540 }
541
542 entries.sort_by(|a, b| a.name.cmp(&b.name));
543 Ok(entries)
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::vfs::MemoryFs;
551
552 #[tokio::test]
553 async fn symlink_absolute_target_on_the_same_mount_is_stored_relative() {
554 let mut router = VfsRouter::new();
555 let data = MemoryFs::new();
556 data.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
557 data.mkdir(Path::new("home")).await.unwrap();
558 router.mount("/data", data);
559
560 router
561 .symlink(Path::new("/data/etc/hosts"), Path::new("/data/home/link"))
562 .await
563 .unwrap();
564
565 assert_eq!(
566 router.read_link(Path::new("/data/home/link")).await.unwrap(),
567 PathBuf::from("../etc/hosts")
568 );
569 assert_eq!(router.read(Path::new("/data/home/link")).await.unwrap(), b"hosts");
570 }
571
572 #[tokio::test]
573 async fn symlink_absolute_target_beside_the_link_is_a_bare_name() {
574 let mut router = VfsRouter::new();
575 let root = MemoryFs::new();
576 root.write(Path::new("a/target"), b"t").await.unwrap();
577 router.mount("/", root);
578
579 router
580 .symlink(Path::new("/a/target"), Path::new("/a/link"))
581 .await
582 .unwrap();
583 assert_eq!(
584 router.read_link(Path::new("/a/link")).await.unwrap(),
585 PathBuf::from("target")
586 );
587 }
588
589 #[tokio::test]
590 async fn symlink_absolute_target_with_dotdot_is_normalized_first() {
591 let mut router = VfsRouter::new();
592 let root = MemoryFs::new();
593 root.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
594 root.mkdir(Path::new("home")).await.unwrap();
595 router.mount("/", root);
596
597 router
598 .symlink(Path::new("/home/../etc/./hosts"), Path::new("/home/link"))
599 .await
600 .unwrap();
601 assert_eq!(
602 router.read_link(Path::new("/home/link")).await.unwrap(),
603 PathBuf::from("../etc/hosts")
604 );
605 assert_eq!(router.read(Path::new("/home/link")).await.unwrap(), b"hosts");
606 }
607
608 #[tokio::test]
609 async fn symlink_relative_target_is_stored_verbatim() {
610 let mut router = VfsRouter::new();
611 router.mount("/data", MemoryFs::new());
612
613 router
614 .symlink(Path::new("../x/../y"), Path::new("/data/d/link"))
615 .await
616 .unwrap();
617 assert_eq!(
618 router.read_link(Path::new("/data/d/link")).await.unwrap(),
619 PathBuf::from("../x/../y")
620 );
621 }
622
623 #[tokio::test]
624 async fn symlink_across_mounts_is_refused_and_creates_nothing() {
625 let mut router = VfsRouter::new();
626 router.mount("/data", MemoryFs::new());
627 let scratch = MemoryFs::new();
628 scratch.write(Path::new("x"), b"x").await.unwrap();
629 router.mount("/scratch", scratch);
630
631 let error = router
632 .symlink(Path::new("/scratch/x"), Path::new("/data/link"))
633 .await
634 .unwrap_err();
635 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
636 let message = error.to_string();
637 assert!(message.contains("/scratch") && message.contains("/data"), "{message}");
638 assert!(router.lstat(Path::new("/data/link")).await.is_err(), "nothing created");
639 }
640
641 #[test]
642 fn relative_path_from_walks_up_then_down() {
643 let rel = |from: &str, to: &str| relative_path_from(Path::new(from), Path::new(to));
644 assert_eq!(rel("/a/b", "/a/c/d"), PathBuf::from("../c/d"));
645 assert_eq!(rel("/a", "/a/x"), PathBuf::from("x"));
646 assert_eq!(rel("/", "/x/y"), PathBuf::from("x/y"));
647 assert_eq!(rel("/a/b/c", "/"), PathBuf::from("../../.."));
648 assert_eq!(rel("/a/b", "/a/b"), PathBuf::from("."));
649 }
650
651 #[tokio::test]
652 async fn test_basic_mount() {
653 let mut router = VfsRouter::new();
654 let scratch = MemoryFs::new();
655 scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
656 router.mount("/scratch", scratch);
657
658 let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
659 assert_eq!(data, b"hello");
660 }
661
662 #[tokio::test]
663 async fn test_multiple_mounts() {
664 let mut router = VfsRouter::new();
665
666 let scratch = MemoryFs::new();
667 scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
668 router.mount("/scratch", scratch);
669
670 let data = MemoryFs::new();
671 data.write(Path::new("b.txt"), b"data").await.unwrap();
672 router.mount("/data", data);
673
674 assert_eq!(
675 router.read(Path::new("/scratch/a.txt")).await.unwrap(),
676 b"scratch"
677 );
678 assert_eq!(
679 router.read(Path::new("/data/b.txt")).await.unwrap(),
680 b"data"
681 );
682 }
683
684 #[tokio::test]
685 async fn test_nested_mount() {
686 let mut router = VfsRouter::new();
687
688 let outer = MemoryFs::new();
689 outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
690 router.mount("/mnt", outer);
691
692 let inner = MemoryFs::new();
693 inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
694 router.mount("/mnt/project", inner);
695
696 assert_eq!(
698 router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
699 b"outer"
700 );
701
702 assert_eq!(
704 router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
705 b"inner"
706 );
707 }
708
709 #[tokio::test]
710 async fn test_list_root() {
711 let mut router = VfsRouter::new();
712 router.mount("/scratch", MemoryFs::new());
713 router.mount("/mnt/a", MemoryFs::new());
714 router.mount("/mnt/b", MemoryFs::new());
715
716 let entries = router.list(Path::new("/")).await.unwrap();
717 let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
718
719 assert!(names.contains(&&"scratch".to_string()));
720 assert!(names.contains(&&"mnt".to_string()));
721 }
722
723 #[tokio::test]
724 async fn test_unmount() {
725 let mut router = VfsRouter::new();
726
727 let fs = MemoryFs::new();
728 fs.write(Path::new("test.txt"), b"data").await.unwrap();
729 router.mount("/scratch", fs);
730
731 assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
732
733 router.unmount("/scratch");
734
735 assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
736 }
737
738 #[tokio::test]
739 async fn test_list_mounts() {
740 let mut router = VfsRouter::new();
741 router.mount("/scratch", MemoryFs::new());
742 router.mount("/data", MemoryFs::new());
743
744 let mounts = router.list_mounts();
745 assert_eq!(mounts.len(), 2);
746
747 let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
748 assert!(paths.contains(&&PathBuf::from("/scratch")));
749 assert!(paths.contains(&&PathBuf::from("/data")));
750 }
751
752 #[tokio::test]
753 async fn test_no_mount_error() {
754 let router = VfsRouter::new();
755 let result = router.read(Path::new("/nothing/here.txt")).await;
756 assert!(result.is_err());
757 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
758 }
759
760 #[tokio::test]
761 async fn test_root_mount() {
762 let mut router = VfsRouter::new();
763
764 let root = MemoryFs::new();
765 root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
766 router.mount("/", root);
767
768 let data = router.read(Path::new("/at-root.txt")).await.unwrap();
769 assert_eq!(data, b"root file");
770 }
771
772 #[tokio::test]
773 async fn test_write_through_router() {
774 let mut router = VfsRouter::new();
775 router.mount("/scratch", MemoryFs::new());
776
777 router
778 .write(Path::new("/scratch/new.txt"), b"created")
779 .await
780 .unwrap();
781
782 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
783 assert_eq!(data, b"created");
784 }
785
786 #[tokio::test]
787 async fn test_stat_mount_point() {
788 let mut router = VfsRouter::new();
789 router.mount("/scratch", MemoryFs::new());
790
791 let entry = router.stat(Path::new("/scratch")).await.unwrap();
792 assert!(entry.is_dir());
793 }
794
795 #[tokio::test]
796 async fn test_stat_root() {
797 let router = VfsRouter::new();
798 let entry = router.stat(Path::new("/")).await.unwrap();
799 assert!(entry.is_dir());
800 }
801
802 #[tokio::test]
803 async fn test_rename_same_mount() {
804 let mut router = VfsRouter::new();
805 let mem = MemoryFs::new();
806 mem.write(Path::new("old.txt"), b"data").await.unwrap();
807 router.mount("/scratch", mem);
808
809 router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
810
811 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
813 assert_eq!(data, b"data");
814
815 assert!(!router.exists(Path::new("/scratch/old.txt")).await);
817 }
818
819 #[tokio::test]
820 async fn test_rename_cross_mount_fails() {
821 let mut router = VfsRouter::new();
822 let mem1 = MemoryFs::new();
823 mem1.write(Path::new("file.txt"), b"data").await.unwrap();
824 router.mount("/mount1", mem1);
825 router.mount("/mount2", MemoryFs::new());
826
827 let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
828 assert!(result.is_err());
829 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
830 }
831
832 #[tokio::test]
838 async fn path_access_agrees_with_stat_on_synthesized_directories() {
839 let mut router = VfsRouter::new();
840 router.mount("/v/docs", MemoryFs::new());
841
842 for path in ["/", "/v"] {
843 let path = Path::new(path);
844 assert!(
845 router.stat(path).await.is_ok(),
846 "{} is synthesized by stat",
847 path.display()
848 );
849 let access = router
850 .path_access(path)
851 .await
852 .unwrap_or_else(|e| panic!("path_access must not error where stat succeeds: {e}"));
853 assert!(access.readable, "{} must be readable", path.display());
854 assert!(access.executable, "{} must be searchable", path.display());
855 assert!(
856 !access.writable,
857 "the router creates nothing in {}",
858 path.display()
859 );
860 }
861 }
862
863 #[tokio::test]
865 async fn path_access_errors_on_a_path_with_no_mount() {
866 let mut router = VfsRouter::new();
867 router.mount("/v/docs", MemoryFs::new());
868 assert!(router.path_access(Path::new("/nope")).await.is_err());
869 assert!(router.path_access(Path::new("/v/docs/absent")).await.is_err());
870 }
871
872 #[tokio::test]
874 async fn path_access_at_a_mount_point_asks_the_mount() {
875 let mut router = VfsRouter::new();
876 router.mount("/rw", MemoryFs::new());
877 router.mount("/ro", BuiltinFsStub);
878
879 assert!(router.path_access(Path::new("/rw")).await.unwrap().writable);
880 assert!(!router.path_access(Path::new("/ro")).await.unwrap().writable);
881 assert!(router.path_access(Path::new("/ro")).await.unwrap().readable);
882 }
883
884 struct BuiltinFsStub;
887
888 #[async_trait]
889 impl Filesystem for BuiltinFsStub {
890 async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
891 Ok(Vec::new())
892 }
893 async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
894 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
895 }
896 async fn list(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
897 Ok(Vec::new())
898 }
899 async fn stat(&self, _path: &Path) -> io::Result<DirEntry> {
900 Ok(DirEntry::directory("."))
901 }
902 async fn mkdir(&self, _path: &Path) -> io::Result<()> {
903 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
904 }
905 async fn remove(&self, _path: &Path) -> io::Result<()> {
906 Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
907 }
908 fn read_only(&self) -> bool {
909 true
910 }
911 }
912
913 #[tokio::test]
914 async fn read_only_empty_router_returns_false() {
915 let router = VfsRouter::new();
916 assert!(!router.read_only());
917 }
918
919 #[cfg(feature = "localfs")]
920 #[tokio::test]
921 async fn read_only_all_read_only_mounts_returns_true() {
922 use crate::vfs::LocalFs;
923
924 let t1 = tempfile::tempdir().unwrap();
925 let t2 = tempfile::tempdir().unwrap();
926
927 let mut router = VfsRouter::new();
928 router.mount("/a", LocalFs::read_only(t1.path().to_path_buf()));
929 router.mount("/b", LocalFs::read_only(t2.path().to_path_buf()));
930
931 assert!(router.read_only());
932 }
933
934 #[cfg(feature = "localfs")]
935 #[tokio::test]
936 async fn read_only_mixed_mounts_returns_false() {
937 use crate::vfs::LocalFs;
938
939 let t1 = tempfile::tempdir().unwrap();
940
941 let mut router = VfsRouter::new();
942 router.mount("/ro", LocalFs::read_only(t1.path().to_path_buf()));
943 router.mount("/rw", MemoryFs::new());
944
945 assert!(!router.read_only());
946 }
947
948 #[tokio::test]
955 async fn test_list_synthesizes_intermediate_dir() {
956 let mut router = VfsRouter::new();
957 router.mount("/v/jobs", MemoryFs::new());
958 router.mount("/v/blobs", MemoryFs::new());
959
960 let entries = router.list(Path::new("/v")).await.unwrap();
961 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
962 assert_eq!(names, vec!["blobs", "jobs"]); }
964
965 #[tokio::test]
966 async fn test_stat_intermediate_dir_is_directory() {
967 let mut router = VfsRouter::new();
968 router.mount("/v/jobs", MemoryFs::new());
969
970 assert!(router.stat(Path::new("/v")).await.unwrap().is_dir());
971 assert!(router.lstat(Path::new("/v")).await.unwrap().is_dir());
972 }
973
974 #[tokio::test]
975 async fn test_deep_intermediate_dir() {
976 let mut router = VfsRouter::new();
977 router.mount("/v/etc/rc", MemoryFs::new());
978
979 let v: Vec<_> = router.list(Path::new("/v")).await.unwrap();
980 assert_eq!(v.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["etc"]);
981 let etc: Vec<_> = router.list(Path::new("/v/etc")).await.unwrap();
982 assert_eq!(etc.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["rc"]);
983 assert!(router.stat(Path::new("/v/etc")).await.unwrap().is_dir());
984 }
985
986 #[tokio::test]
987 async fn test_has_mount_under() {
988 let mut router = VfsRouter::new();
989 router.mount("/v/jobs", MemoryFs::new());
990
991 assert!(router.has_mount_under(Path::new("/v")));
992 assert!(router.has_mount_under(Path::new("/")));
993 assert!(!router.has_mount_under(Path::new("/v/jobs")));
995 assert!(!router.has_mount_under(Path::new("/other")));
996 }
997
998 #[tokio::test]
999 async fn test_nonexistent_ancestor_still_notfound() {
1000 let mut router = VfsRouter::new();
1001 router.mount("/v/jobs", MemoryFs::new());
1002
1003 assert_eq!(
1006 router.list(Path::new("/nope")).await.unwrap_err().kind(),
1007 io::ErrorKind::NotFound
1008 );
1009 assert_eq!(
1010 router.stat(Path::new("/nope")).await.unwrap_err().kind(),
1011 io::ErrorKind::NotFound
1012 );
1013 }
1014}