Skip to main content

kaish_kernel/vfs/
router.rs

1//! VFS router for mount point management.
2//!
3//! Routes filesystem operations to the appropriate backend based on path.
4
5use super::traits::{DirEntry, EntryType, Filesystem, Metadata};
6use async_trait::async_trait;
7use std::collections::BTreeMap;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12/// Information about a mount point.
13#[derive(Debug, Clone)]
14pub struct MountInfo {
15    /// The mount path (e.g., "/mnt/project").
16    pub path: PathBuf,
17    /// Whether this mount is read-only.
18    pub read_only: bool,
19}
20
21/// Routes filesystem operations to mounted backends.
22///
23/// Mount points are matched by longest prefix. For example, if `/mnt` and
24/// `/mnt/project` are both mounted, a path like `/mnt/project/src/main.rs`
25/// will be routed to the `/mnt/project` mount.
26#[derive(Default)]
27pub struct VfsRouter {
28    /// Mount points, keyed by path. Uses BTreeMap for ordered iteration.
29    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    /// Create a new empty VFS router.
42    pub fn new() -> Self {
43        Self {
44            mounts: BTreeMap::new(),
45        }
46    }
47
48    /// Mount a filesystem at the given path.
49    ///
50    /// The path should be absolute (start with `/`). If a filesystem is
51    /// already mounted at this path, it will be replaced.
52    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    /// Mount a filesystem (already wrapped in Arc) at the given path.
58    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    /// Unmount the filesystem at the given path.
64    ///
65    /// Returns `true` if a mount was removed, `false` if nothing was mounted there.
66    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    /// List all current mounts.
72    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    /// Normalize a mount path: ensure it starts with `/` and has no trailing slash.
83    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    /// Resolve a VFS path to a real filesystem path.
96    ///
97    /// Returns `Some(path)` if the VFS path maps to a real filesystem (like LocalFs),
98    /// or `None` if the path is in a virtual filesystem (like MemoryFs).
99    ///
100    /// This is needed for tools like `git` that must use real paths with external libraries.
101    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    /// Find the mount point for a given path.
107    ///
108    /// Returns the mount and the path relative to that mount.
109    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        // Find longest matching mount point
118        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            // Check if the path starts with this mount point
124            let is_match = if mount_str == "/" {
125                true // Root matches everything
126            } 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                // Keep the longest match
134                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                // Calculate relative path
146                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        // Special case: listing root might need to show mount points
186        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<Metadata> {
197        // Special case: root always exists
198        let path_str = path.to_string_lossy();
199        if path_str.is_empty() || path_str == "/" {
200            return Ok(Metadata {
201                is_dir: true,
202                is_file: false,
203                is_symlink: false,
204                size: 0,
205                modified: None,
206            });
207        }
208
209        // Check if path is a mount point itself
210        let normalized = Self::normalize_mount_path(path.to_path_buf());
211        if self.mounts.contains_key(&normalized) {
212            return Ok(Metadata {
213                is_dir: true,
214                is_file: false,
215                is_symlink: false,
216                size: 0,
217                modified: None,
218            });
219        }
220
221        let (fs, relative) = self.find_mount(path)?;
222        fs.stat(&relative).await
223    }
224
225    async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
226        let (fs, relative) = self.find_mount(path)?;
227        fs.read_link(&relative).await
228    }
229
230    async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
231        let (fs, relative) = self.find_mount(link)?;
232        fs.symlink(target, &relative).await
233    }
234
235    async fn lstat(&self, path: &Path) -> io::Result<Metadata> {
236        // Special case: root always exists
237        let path_str = path.to_string_lossy();
238        if path_str.is_empty() || path_str == "/" {
239            return Ok(Metadata {
240                is_dir: true,
241                is_file: false,
242                is_symlink: false,
243                size: 0,
244                modified: None,
245            });
246        }
247
248        // Check if path is a mount point itself
249        let normalized = Self::normalize_mount_path(path.to_path_buf());
250        if self.mounts.contains_key(&normalized) {
251            return Ok(Metadata {
252                is_dir: true,
253                is_file: false,
254                is_symlink: false,
255                size: 0,
256                modified: None,
257            });
258        }
259
260        let (fs, relative) = self.find_mount(path)?;
261        fs.lstat(&relative).await
262    }
263
264    async fn mkdir(&self, path: &Path) -> io::Result<()> {
265        let (fs, relative) = self.find_mount(path)?;
266        fs.mkdir(&relative).await
267    }
268
269    async fn remove(&self, path: &Path) -> io::Result<()> {
270        let (fs, relative) = self.find_mount(path)?;
271        fs.remove(&relative).await
272    }
273
274    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
275        let (from_fs, from_relative) = self.find_mount(from)?;
276        let (to_fs, to_relative) = self.find_mount(to)?;
277
278        // Check if both paths are on the same mount by comparing Arc pointers
279        if !Arc::ptr_eq(&from_fs, &to_fs) {
280            return Err(io::Error::new(
281                io::ErrorKind::Unsupported,
282                "cannot rename across different mount points",
283            ));
284        }
285
286        from_fs.rename(&from_relative, &to_relative).await
287    }
288
289    fn read_only(&self) -> bool {
290        // Router itself isn't read-only; individual mounts might be
291        false
292    }
293}
294
295impl VfsRouter {
296    /// List the root directory, synthesizing entries from mount points.
297    async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
298        let mut entries = Vec::new();
299        let mut seen_names = std::collections::HashSet::new();
300
301        for mount_path in self.mounts.keys() {
302            let mount_str = mount_path.to_string_lossy();
303            if mount_str == "/" {
304                // Root mount: list its contents directly
305                if let Some(fs) = self.mounts.get(mount_path)
306                    && let Ok(root_entries) = fs.list(Path::new("")).await {
307                        for entry in root_entries {
308                            if seen_names.insert(entry.name.clone()) {
309                                entries.push(entry);
310                            }
311                        }
312                    }
313            } else {
314                // Non-root mount: extract first path component
315                let first_component = mount_str
316                    .trim_start_matches('/')
317                    .split('/')
318                    .next()
319                    .unwrap_or("");
320
321                if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
322                    entries.push(DirEntry {
323                        name: first_component.to_string(),
324                        entry_type: EntryType::Directory,
325                        size: 0,
326                        symlink_target: None,
327                    });
328                }
329            }
330        }
331
332        entries.sort_by(|a, b| a.name.cmp(&b.name));
333        Ok(entries)
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::vfs::MemoryFs;
341
342    #[tokio::test]
343    async fn test_basic_mount() {
344        let mut router = VfsRouter::new();
345        let scratch = MemoryFs::new();
346        scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
347        router.mount("/scratch", scratch);
348
349        let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
350        assert_eq!(data, b"hello");
351    }
352
353    #[tokio::test]
354    async fn test_multiple_mounts() {
355        let mut router = VfsRouter::new();
356
357        let scratch = MemoryFs::new();
358        scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
359        router.mount("/scratch", scratch);
360
361        let data = MemoryFs::new();
362        data.write(Path::new("b.txt"), b"data").await.unwrap();
363        router.mount("/data", data);
364
365        assert_eq!(
366            router.read(Path::new("/scratch/a.txt")).await.unwrap(),
367            b"scratch"
368        );
369        assert_eq!(
370            router.read(Path::new("/data/b.txt")).await.unwrap(),
371            b"data"
372        );
373    }
374
375    #[tokio::test]
376    async fn test_nested_mount() {
377        let mut router = VfsRouter::new();
378
379        let outer = MemoryFs::new();
380        outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
381        router.mount("/mnt", outer);
382
383        let inner = MemoryFs::new();
384        inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
385        router.mount("/mnt/project", inner);
386
387        // /mnt/outer.txt should come from outer mount
388        assert_eq!(
389            router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
390            b"outer"
391        );
392
393        // /mnt/project/inner.txt should come from inner mount
394        assert_eq!(
395            router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
396            b"inner"
397        );
398    }
399
400    #[tokio::test]
401    async fn test_list_root() {
402        let mut router = VfsRouter::new();
403        router.mount("/scratch", MemoryFs::new());
404        router.mount("/mnt/a", MemoryFs::new());
405        router.mount("/mnt/b", MemoryFs::new());
406
407        let entries = router.list(Path::new("/")).await.unwrap();
408        let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
409
410        assert!(names.contains(&&"scratch".to_string()));
411        assert!(names.contains(&&"mnt".to_string()));
412    }
413
414    #[tokio::test]
415    async fn test_unmount() {
416        let mut router = VfsRouter::new();
417
418        let fs = MemoryFs::new();
419        fs.write(Path::new("test.txt"), b"data").await.unwrap();
420        router.mount("/scratch", fs);
421
422        assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
423
424        router.unmount("/scratch");
425
426        assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
427    }
428
429    #[tokio::test]
430    async fn test_list_mounts() {
431        let mut router = VfsRouter::new();
432        router.mount("/scratch", MemoryFs::new());
433        router.mount("/data", MemoryFs::new());
434
435        let mounts = router.list_mounts();
436        assert_eq!(mounts.len(), 2);
437
438        let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
439        assert!(paths.contains(&&PathBuf::from("/scratch")));
440        assert!(paths.contains(&&PathBuf::from("/data")));
441    }
442
443    #[tokio::test]
444    async fn test_no_mount_error() {
445        let router = VfsRouter::new();
446        let result = router.read(Path::new("/nothing/here.txt")).await;
447        assert!(result.is_err());
448        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
449    }
450
451    #[tokio::test]
452    async fn test_root_mount() {
453        let mut router = VfsRouter::new();
454
455        let root = MemoryFs::new();
456        root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
457        router.mount("/", root);
458
459        let data = router.read(Path::new("/at-root.txt")).await.unwrap();
460        assert_eq!(data, b"root file");
461    }
462
463    #[tokio::test]
464    async fn test_write_through_router() {
465        let mut router = VfsRouter::new();
466        router.mount("/scratch", MemoryFs::new());
467
468        router
469            .write(Path::new("/scratch/new.txt"), b"created")
470            .await
471            .unwrap();
472
473        let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
474        assert_eq!(data, b"created");
475    }
476
477    #[tokio::test]
478    async fn test_stat_mount_point() {
479        let mut router = VfsRouter::new();
480        router.mount("/scratch", MemoryFs::new());
481
482        let meta = router.stat(Path::new("/scratch")).await.unwrap();
483        assert!(meta.is_dir);
484    }
485
486    #[tokio::test]
487    async fn test_stat_root() {
488        let router = VfsRouter::new();
489        let meta = router.stat(Path::new("/")).await.unwrap();
490        assert!(meta.is_dir);
491    }
492
493    #[tokio::test]
494    async fn test_rename_same_mount() {
495        let mut router = VfsRouter::new();
496        let mem = MemoryFs::new();
497        mem.write(Path::new("old.txt"), b"data").await.unwrap();
498        router.mount("/scratch", mem);
499
500        router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
501
502        // New path exists
503        let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
504        assert_eq!(data, b"data");
505
506        // Old path doesn't exist
507        assert!(!router.exists(Path::new("/scratch/old.txt")).await);
508    }
509
510    #[tokio::test]
511    async fn test_rename_cross_mount_fails() {
512        let mut router = VfsRouter::new();
513        let mem1 = MemoryFs::new();
514        mem1.write(Path::new("file.txt"), b"data").await.unwrap();
515        router.mount("/mount1", mem1);
516        router.mount("/mount2", MemoryFs::new());
517
518        let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
519        assert!(result.is_err());
520        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
521    }
522}