1use super::traits::{DirEntry, Filesystem};
6use async_trait::async_trait;
7use std::collections::BTreeMap;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
14pub struct MountInfo {
15 pub path: PathBuf,
17 pub read_only: bool,
19}
20
21#[derive(Default)]
27pub struct VfsRouter {
28 mounts: BTreeMap<PathBuf, Arc<dyn Filesystem>>,
30}
31
32impl std::fmt::Debug for VfsRouter {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("VfsRouter")
35 .field("mounts", &self.mounts.keys().collect::<Vec<_>>())
36 .finish()
37 }
38}
39
40impl VfsRouter {
41 pub fn new() -> Self {
43 Self {
44 mounts: BTreeMap::new(),
45 }
46 }
47
48 pub fn mount(&mut self, path: impl Into<PathBuf>, fs: impl Filesystem + 'static) {
53 let path = Self::normalize_mount_path(path.into());
54 self.mounts.insert(path, Arc::new(fs));
55 }
56
57 pub fn mount_arc(&mut self, path: impl Into<PathBuf>, fs: Arc<dyn Filesystem>) {
59 let path = Self::normalize_mount_path(path.into());
60 self.mounts.insert(path, fs);
61 }
62
63 pub fn unmount(&mut self, path: impl AsRef<Path>) -> bool {
67 let path = Self::normalize_mount_path(path.as_ref().to_path_buf());
68 self.mounts.remove(&path).is_some()
69 }
70
71 pub fn list_mounts(&self) -> Vec<MountInfo> {
73 self.mounts
74 .iter()
75 .map(|(path, fs)| MountInfo {
76 path: path.clone(),
77 read_only: fs.read_only(),
78 })
79 .collect()
80 }
81
82 fn normalize_mount_path(path: PathBuf) -> PathBuf {
84 let s = path.to_string_lossy();
85 let s = s.trim_end_matches('/');
86 if s.is_empty() {
87 PathBuf::from("/")
88 } else if !s.starts_with('/') {
89 PathBuf::from(format!("/{}", s))
90 } else {
91 PathBuf::from(s)
92 }
93 }
94
95 pub fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
102 let (fs, relative) = self.find_mount(path).ok()?;
103 fs.real_path(&relative)
104 }
105
106 fn find_mount(&self, path: &Path) -> io::Result<(Arc<dyn Filesystem>, PathBuf)> {
110 let path_str = path.to_string_lossy();
111 let normalized = if path_str.starts_with('/') {
112 path.to_path_buf()
113 } else {
114 PathBuf::from(format!("/{}", path_str))
115 };
116
117 let mut best_match: Option<(&PathBuf, &Arc<dyn Filesystem>)> = None;
119
120 for (mount_path, fs) in &self.mounts {
121 let mount_str = mount_path.to_string_lossy();
122
123 let is_match = if mount_str == "/" {
125 true } else {
127 let normalized_str = normalized.to_string_lossy();
128 normalized_str == mount_str.as_ref()
129 || normalized_str.starts_with(&format!("{}/", mount_str))
130 };
131
132 if is_match {
133 let dominated = best_match
135 .as_ref()
136 .is_none_or(|(bp, _)| mount_path.as_os_str().len() > bp.as_os_str().len());
137 if dominated {
138 best_match = Some((mount_path, fs));
139 }
140 }
141 }
142
143 match best_match {
144 Some((mount_path, fs)) => {
145 let mount_str = mount_path.to_string_lossy();
147 let normalized_str = normalized.to_string_lossy();
148
149 let relative = if mount_str == "/" {
150 normalized_str.trim_start_matches('/').to_string()
151 } else {
152 normalized_str
153 .strip_prefix(mount_str.as_ref())
154 .unwrap_or("")
155 .trim_start_matches('/')
156 .to_string()
157 };
158
159 Ok((Arc::clone(fs), PathBuf::from(relative)))
160 }
161 None => Err(io::Error::new(
162 io::ErrorKind::NotFound,
163 format!("no mount point for path: {}", path.display()),
164 )),
165 }
166 }
167}
168
169#[async_trait]
170impl Filesystem for VfsRouter {
171 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
172 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
173 let (fs, relative) = self.find_mount(path)?;
174 fs.read(&relative).await
175 }
176
177 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
178 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
179 let (fs, relative) = self.find_mount(path)?;
180 fs.write(&relative, data).await
181 }
182
183 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
184 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
185 let path_str = path.to_string_lossy();
187 if path_str.is_empty() || path_str == "/" {
188 return self.list_root().await;
189 }
190
191 let (fs, relative) = self.find_mount(path)?;
192 fs.list(&relative).await
193 }
194
195 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
196 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
197 let path_str = path.to_string_lossy();
199 if path_str.is_empty() || path_str == "/" {
200 return Ok(DirEntry::directory("/"));
201 }
202
203 let normalized = Self::normalize_mount_path(path.to_path_buf());
205 if self.mounts.contains_key(&normalized) {
206 let name = path
207 .file_name()
208 .map(|n| n.to_string_lossy().into_owned())
209 .unwrap_or_else(|| "/".to_string());
210 return Ok(DirEntry::directory(name));
211 }
212
213 let (fs, relative) = self.find_mount(path)?;
214 fs.stat(&relative).await
215 }
216
217 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
218 let (fs, relative) = self.find_mount(path)?;
219 fs.read_link(&relative).await
220 }
221
222 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
223 let (fs, relative) = self.find_mount(link)?;
224 fs.symlink(target, &relative).await
225 }
226
227 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
228 let path_str = path.to_string_lossy();
230 if path_str.is_empty() || path_str == "/" {
231 return Ok(DirEntry::directory("/"));
232 }
233
234 let normalized = Self::normalize_mount_path(path.to_path_buf());
236 if self.mounts.contains_key(&normalized) {
237 let name = path
238 .file_name()
239 .map(|n| n.to_string_lossy().into_owned())
240 .unwrap_or_else(|| "/".to_string());
241 return Ok(DirEntry::directory(name));
242 }
243
244 let (fs, relative) = self.find_mount(path)?;
245 fs.lstat(&relative).await
246 }
247
248 async fn mkdir(&self, path: &Path) -> io::Result<()> {
249 let (fs, relative) = self.find_mount(path)?;
250 fs.mkdir(&relative).await
251 }
252
253 async fn remove(&self, path: &Path) -> io::Result<()> {
254 let (fs, relative) = self.find_mount(path)?;
255 fs.remove(&relative).await
256 }
257
258 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
259 let (from_fs, from_relative) = self.find_mount(from)?;
260 let (to_fs, to_relative) = self.find_mount(to)?;
261
262 if !Arc::ptr_eq(&from_fs, &to_fs) {
264 return Err(io::Error::new(
265 io::ErrorKind::Unsupported,
266 "cannot rename across different mount points",
267 ));
268 }
269
270 from_fs.rename(&from_relative, &to_relative).await
271 }
272
273 fn read_only(&self) -> bool {
274 if self.mounts.is_empty() {
278 return false;
279 }
280 self.mounts.values().all(|fs| fs.read_only())
281 }
282}
283
284impl VfsRouter {
285 async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
287 let mut entries = Vec::new();
288 let mut seen_names = std::collections::HashSet::new();
289
290 for mount_path in self.mounts.keys() {
291 let mount_str = mount_path.to_string_lossy();
292 if mount_str == "/" {
293 if let Some(fs) = self.mounts.get(mount_path)
295 && let Ok(root_entries) = fs.list(Path::new("")).await {
296 for entry in root_entries {
297 if seen_names.insert(entry.name.clone()) {
298 entries.push(entry);
299 }
300 }
301 }
302 } else {
303 let first_component = mount_str
305 .trim_start_matches('/')
306 .split('/')
307 .next()
308 .unwrap_or("");
309
310 if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
311 entries.push(DirEntry::directory(first_component));
312 }
313 }
314 }
315
316 entries.sort_by(|a, b| a.name.cmp(&b.name));
317 Ok(entries)
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::vfs::MemoryFs;
325
326 #[tokio::test]
327 async fn test_basic_mount() {
328 let mut router = VfsRouter::new();
329 let scratch = MemoryFs::new();
330 scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
331 router.mount("/scratch", scratch);
332
333 let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
334 assert_eq!(data, b"hello");
335 }
336
337 #[tokio::test]
338 async fn test_multiple_mounts() {
339 let mut router = VfsRouter::new();
340
341 let scratch = MemoryFs::new();
342 scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
343 router.mount("/scratch", scratch);
344
345 let data = MemoryFs::new();
346 data.write(Path::new("b.txt"), b"data").await.unwrap();
347 router.mount("/data", data);
348
349 assert_eq!(
350 router.read(Path::new("/scratch/a.txt")).await.unwrap(),
351 b"scratch"
352 );
353 assert_eq!(
354 router.read(Path::new("/data/b.txt")).await.unwrap(),
355 b"data"
356 );
357 }
358
359 #[tokio::test]
360 async fn test_nested_mount() {
361 let mut router = VfsRouter::new();
362
363 let outer = MemoryFs::new();
364 outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
365 router.mount("/mnt", outer);
366
367 let inner = MemoryFs::new();
368 inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
369 router.mount("/mnt/project", inner);
370
371 assert_eq!(
373 router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
374 b"outer"
375 );
376
377 assert_eq!(
379 router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
380 b"inner"
381 );
382 }
383
384 #[tokio::test]
385 async fn test_list_root() {
386 let mut router = VfsRouter::new();
387 router.mount("/scratch", MemoryFs::new());
388 router.mount("/mnt/a", MemoryFs::new());
389 router.mount("/mnt/b", MemoryFs::new());
390
391 let entries = router.list(Path::new("/")).await.unwrap();
392 let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
393
394 assert!(names.contains(&&"scratch".to_string()));
395 assert!(names.contains(&&"mnt".to_string()));
396 }
397
398 #[tokio::test]
399 async fn test_unmount() {
400 let mut router = VfsRouter::new();
401
402 let fs = MemoryFs::new();
403 fs.write(Path::new("test.txt"), b"data").await.unwrap();
404 router.mount("/scratch", fs);
405
406 assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
407
408 router.unmount("/scratch");
409
410 assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
411 }
412
413 #[tokio::test]
414 async fn test_list_mounts() {
415 let mut router = VfsRouter::new();
416 router.mount("/scratch", MemoryFs::new());
417 router.mount("/data", MemoryFs::new());
418
419 let mounts = router.list_mounts();
420 assert_eq!(mounts.len(), 2);
421
422 let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
423 assert!(paths.contains(&&PathBuf::from("/scratch")));
424 assert!(paths.contains(&&PathBuf::from("/data")));
425 }
426
427 #[tokio::test]
428 async fn test_no_mount_error() {
429 let router = VfsRouter::new();
430 let result = router.read(Path::new("/nothing/here.txt")).await;
431 assert!(result.is_err());
432 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
433 }
434
435 #[tokio::test]
436 async fn test_root_mount() {
437 let mut router = VfsRouter::new();
438
439 let root = MemoryFs::new();
440 root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
441 router.mount("/", root);
442
443 let data = router.read(Path::new("/at-root.txt")).await.unwrap();
444 assert_eq!(data, b"root file");
445 }
446
447 #[tokio::test]
448 async fn test_write_through_router() {
449 let mut router = VfsRouter::new();
450 router.mount("/scratch", MemoryFs::new());
451
452 router
453 .write(Path::new("/scratch/new.txt"), b"created")
454 .await
455 .unwrap();
456
457 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
458 assert_eq!(data, b"created");
459 }
460
461 #[tokio::test]
462 async fn test_stat_mount_point() {
463 let mut router = VfsRouter::new();
464 router.mount("/scratch", MemoryFs::new());
465
466 let entry = router.stat(Path::new("/scratch")).await.unwrap();
467 assert!(entry.is_dir());
468 }
469
470 #[tokio::test]
471 async fn test_stat_root() {
472 let router = VfsRouter::new();
473 let entry = router.stat(Path::new("/")).await.unwrap();
474 assert!(entry.is_dir());
475 }
476
477 #[tokio::test]
478 async fn test_rename_same_mount() {
479 let mut router = VfsRouter::new();
480 let mem = MemoryFs::new();
481 mem.write(Path::new("old.txt"), b"data").await.unwrap();
482 router.mount("/scratch", mem);
483
484 router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
485
486 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
488 assert_eq!(data, b"data");
489
490 assert!(!router.exists(Path::new("/scratch/old.txt")).await);
492 }
493
494 #[tokio::test]
495 async fn test_rename_cross_mount_fails() {
496 let mut router = VfsRouter::new();
497 let mem1 = MemoryFs::new();
498 mem1.write(Path::new("file.txt"), b"data").await.unwrap();
499 router.mount("/mount1", mem1);
500 router.mount("/mount2", MemoryFs::new());
501
502 let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
503 assert!(result.is_err());
504 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
505 }
506
507 #[tokio::test]
508 async fn read_only_empty_router_returns_false() {
509 let router = VfsRouter::new();
510 assert!(!router.read_only());
511 }
512
513 #[tokio::test]
514 async fn read_only_all_read_only_mounts_returns_true() {
515 use crate::vfs::local::LocalFs;
516
517 let t1 = tempfile::tempdir().unwrap();
518 let t2 = tempfile::tempdir().unwrap();
519
520 let mut router = VfsRouter::new();
521 router.mount("/a", LocalFs::read_only(t1.path().to_path_buf()));
522 router.mount("/b", LocalFs::read_only(t2.path().to_path_buf()));
523
524 assert!(router.read_only());
525 }
526
527 #[tokio::test]
528 async fn read_only_mixed_mounts_returns_false() {
529 use crate::vfs::local::LocalFs;
530
531 let t1 = tempfile::tempdir().unwrap();
532
533 let mut router = VfsRouter::new();
534 router.mount("/ro", LocalFs::read_only(t1.path().to_path_buf()));
535 router.mount("/rw", MemoryFs::new());
536
537 assert!(!router.read_only());
538 }
539}