Skip to main content

kaish_kernel/backend/
overlay.rs

1//! VirtualOverlayBackend: Routes /v/* paths to internal VFS while delegating everything else.
2//!
3//! This backend is designed for embedders who provide their own `KernelBackend` but want
4//! kaish's virtual filesystems (like `/v/jobs` for job observability) to work automatically.
5//!
6//! # Usage
7//!
8//! Prefer using `Kernel::with_backend()` which handles overlay setup automatically:
9//!
10//! ```ignore
11//! let kernel = Kernel::with_backend(my_backend, config, |vfs| {
12//!     vfs.mount_arc("/v/docs", docs_fs);
13//! }, |_| {})?;
14//! ```
15//!
16//! # Path Routing
17//!
18//! - `/v/*` → Internal VFS (JobFs, MemoryFs for blobs, etc.)
19//! - Everything else → Custom backend
20
21use async_trait::async_trait;
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24
25use super::{
26    BackendError, BackendResult, KernelBackend, LocalBackend, PatchOp, ReadRange,
27    ToolInfo, ToolResult, WriteMode,
28};
29use crate::tools::{ToolArgs, ToolCtx};
30use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
31
32/// Backend that overlays virtual paths (`/v/*`) on top of a custom backend.
33///
34/// This enables embedders to provide their own storage backend while still
35/// getting kaish's virtual filesystem features like `/v/jobs` for job observability.
36pub struct VirtualOverlayBackend {
37    /// Custom backend for most paths (embedder-provided).
38    inner: Arc<dyn KernelBackend>,
39    /// VFS for /v/* paths (internal virtual filesystems).
40    vfs: Arc<VfsRouter>,
41}
42
43impl VirtualOverlayBackend {
44    /// Create a new virtual overlay backend.
45    ///
46    /// # Arguments
47    ///
48    /// * `inner` - The custom backend to delegate non-virtual paths to
49    /// * `vfs` - VFS router containing virtual filesystem mounts (typically at /v/*)
50    ///
51    /// # Example
52    ///
53    /// ```ignore
54    /// let overlay = VirtualOverlayBackend::new(my_backend, vfs);
55    /// ```
56    pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
57        Self { inner, vfs }
58    }
59
60    /// Check if a path should be handled by the VFS (virtual paths).
61    fn is_virtual_path(path: &Path) -> bool {
62        let path_str = path.to_string_lossy();
63        path_str == "/v" || path_str.starts_with("/v/")
64    }
65
66    /// Get the inner backend.
67    pub fn inner(&self) -> &Arc<dyn KernelBackend> {
68        &self.inner
69    }
70
71    /// Get the VFS router.
72    pub fn vfs(&self) -> &Arc<VfsRouter> {
73        &self.vfs
74    }
75}
76
77impl std::fmt::Debug for VirtualOverlayBackend {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("VirtualOverlayBackend")
80            .field("inner_type", &self.inner.backend_type())
81            .field("vfs", &self.vfs)
82            .finish()
83    }
84}
85
86#[async_trait]
87impl KernelBackend for VirtualOverlayBackend {
88    // ═══════════════════════════════════════════════════════════════════════════
89    // File Operations
90    // ═══════════════════════════════════════════════════════════════════════════
91
92    async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
93        if Self::is_virtual_path(path) {
94            Ok(self.vfs.read_range(path, range).await?)
95        } else {
96            self.inner.read(path, range).await
97        }
98    }
99
100    async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
101        if Self::is_virtual_path(path) {
102            match mode {
103                WriteMode::CreateNew => {
104                    if self.vfs.exists(path).await {
105                        return Err(BackendError::AlreadyExists(path.display().to_string()));
106                    }
107                    self.vfs.write(path, content).await?;
108                }
109                WriteMode::Overwrite | WriteMode::Truncate => {
110                    self.vfs.write(path, content).await?;
111                }
112                WriteMode::UpdateOnly => {
113                    if !self.vfs.exists(path).await {
114                        return Err(BackendError::NotFound(path.display().to_string()));
115                    }
116                    self.vfs.write(path, content).await?;
117                }
118                // WriteMode is #[non_exhaustive] — treat unknown modes as Overwrite
119                _ => {
120                    self.vfs.write(path, content).await?;
121                }
122            }
123            Ok(())
124        } else {
125            self.inner.write(path, content, mode).await
126        }
127    }
128
129    async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
130        if Self::is_virtual_path(path) {
131            self.vfs.set_mtime(path, mtime).await?;
132            Ok(())
133        } else {
134            self.inner.set_mtime(path, mtime).await
135        }
136    }
137
138    async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
139        if Self::is_virtual_path(path) {
140            let mut existing = match self.vfs.read(path).await {
141                Ok(data) => data,
142                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
143                Err(e) => return Err(e.into()),
144            };
145            existing.extend_from_slice(content);
146            self.vfs.write(path, &existing).await?;
147            Ok(())
148        } else {
149            self.inner.append(path, content).await
150        }
151    }
152
153    async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
154        if Self::is_virtual_path(path) {
155            // Read existing content
156            let data = self.vfs.read(path).await?;
157            let mut content = String::from_utf8(data)
158                .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
159
160            // Apply each patch operation
161            for op in ops {
162                LocalBackend::apply_patch_op(&mut content, op)?;
163            }
164
165            // Write back
166            self.vfs.write(path, content.as_bytes()).await?;
167            Ok(())
168        } else {
169            self.inner.patch(path, ops).await
170        }
171    }
172
173    // ═══════════════════════════════════════════════════════════════════════════
174    // Directory Operations
175    // ═══════════════════════════════════════════════════════════════════════════
176
177    async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
178        if Self::is_virtual_path(path) {
179            Ok(self.vfs.list(path).await?)
180        } else if path.to_string_lossy() == "/" || path.to_string_lossy().is_empty() {
181            // Root listing: combine inner backend's root with /v
182            let mut entries = self.inner.list(path).await?;
183            // Add /v if not already present
184            if !entries.iter().any(|e| e.name == "v") {
185                entries.push(DirEntry::directory("v"));
186            }
187            Ok(entries)
188        } else {
189            self.inner.list(path).await
190        }
191    }
192
193    async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
194        if Self::is_virtual_path(path) {
195            Ok(self.vfs.stat(path).await?)
196        } else {
197            self.inner.stat(path).await
198        }
199    }
200
201    async fn mkdir(&self, path: &Path) -> BackendResult<()> {
202        if Self::is_virtual_path(path) {
203            self.vfs.mkdir(path).await?;
204            Ok(())
205        } else {
206            self.inner.mkdir(path).await
207        }
208    }
209
210    async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
211        if Self::is_virtual_path(path) {
212            if recursive
213                && let Ok(entry) = self.vfs.stat(path).await
214                && entry.is_dir()
215                && let Ok(entries) = self.vfs.list(path).await
216            {
217                for entry in entries {
218                    let child_path = path.join(&entry.name);
219                    Box::pin(self.remove(&child_path, true)).await?;
220                }
221            }
222            self.vfs.remove(path).await?;
223            Ok(())
224        } else {
225            self.inner.remove(path, recursive).await
226        }
227    }
228
229    async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
230        let from_virtual = Self::is_virtual_path(from);
231        let to_virtual = Self::is_virtual_path(to);
232
233        if from_virtual != to_virtual {
234            return Err(BackendError::InvalidOperation(
235                "cannot rename between virtual and non-virtual paths".into(),
236            ));
237        }
238
239        if from_virtual {
240            self.vfs.rename(from, to).await?;
241            Ok(())
242        } else {
243            self.inner.rename(from, to).await
244        }
245    }
246
247    async fn exists(&self, path: &Path) -> bool {
248        if Self::is_virtual_path(path) {
249            self.vfs.exists(path).await
250        } else {
251            self.inner.exists(path).await
252        }
253    }
254
255    // ═══════════════════════════════════════════════════════════════════════════
256    // Symlink Operations
257    // ═══════════════════════════════════════════════════════════════════════════
258
259    async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
260        if Self::is_virtual_path(path) {
261            Ok(self.vfs.lstat(path).await?)
262        } else {
263            self.inner.lstat(path).await
264        }
265    }
266
267    async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
268        if Self::is_virtual_path(path) {
269            Ok(self.vfs.read_link(path).await?)
270        } else {
271            self.inner.read_link(path).await
272        }
273    }
274
275    async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
276        if Self::is_virtual_path(link) {
277            self.vfs.symlink(target, link).await?;
278            Ok(())
279        } else {
280            self.inner.symlink(target, link).await
281        }
282    }
283
284    // ═══════════════════════════════════════════════════════════════════════════
285    // Tool Dispatch
286    // ═══════════════════════════════════════════════════════════════════════════
287
288    async fn call_tool(
289        &self,
290        name: &str,
291        args: ToolArgs,
292        ctx: &mut dyn ToolCtx,
293    ) -> BackendResult<ToolResult> {
294        // Tools are dispatched through the inner backend
295        self.inner.call_tool(name, args, ctx).await
296    }
297
298    async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
299        self.inner.list_tools().await
300    }
301
302    async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
303        self.inner.get_tool(name).await
304    }
305
306    // ═══════════════════════════════════════════════════════════════════════════
307    // Backend Information
308    // ═══════════════════════════════════════════════════════════════════════════
309
310    fn read_only(&self) -> bool {
311        // We're not read-only if either layer is writable
312        self.inner.read_only() && self.vfs.read_only()
313    }
314
315    fn backend_type(&self) -> &str {
316        "virtual-overlay"
317    }
318
319    fn mounts(&self) -> Vec<MountInfo> {
320        let mut mounts = self.inner.mounts();
321        mounts.extend(self.vfs.list_mounts());
322        mounts
323    }
324
325    fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
326        if Self::is_virtual_path(path) {
327            // Virtual paths don't map to real filesystem
328            None
329        } else {
330            self.inner.resolve_real_path(path)
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::backend::testing::MockBackend;
339    use crate::vfs::MemoryFs;
340
341    async fn make_overlay() -> VirtualOverlayBackend {
342        // Create mock inner backend
343        let (mock, _) = MockBackend::new();
344        let inner: Arc<dyn KernelBackend> = Arc::new(mock);
345
346        // Create VFS with /v mounted
347        let mut vfs = VfsRouter::new();
348        let mem = MemoryFs::new();
349        mem.write(Path::new("blobs/test.bin"), b"blob data").await.unwrap();
350        mem.mkdir(Path::new("jobs")).await.unwrap();
351        vfs.mount("/v", mem);
352
353        VirtualOverlayBackend::new(inner, Arc::new(vfs))
354    }
355
356    #[tokio::test]
357    async fn test_virtual_path_detection() {
358        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v")));
359        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/")));
360        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/jobs")));
361        assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/blobs/test.bin")));
362
363        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/docs")));
364        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/g/repo")));
365        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/")));
366        assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/var")));
367    }
368
369    #[tokio::test]
370    async fn test_read_virtual_path() {
371        let overlay = make_overlay().await;
372        let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
373        assert_eq!(content, b"blob data");
374    }
375
376    #[tokio::test]
377    async fn test_write_virtual_path() {
378        let overlay = make_overlay().await;
379        overlay
380            .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
381            .await
382            .unwrap();
383        let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
384        assert_eq!(content, b"new data");
385    }
386
387    #[tokio::test]
388    async fn test_list_virtual_path() {
389        let overlay = make_overlay().await;
390        let entries = overlay.list(Path::new("/v")).await.unwrap();
391        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
392        assert!(names.contains(&"blobs"));
393        assert!(names.contains(&"jobs"));
394    }
395
396    #[tokio::test]
397    async fn test_root_listing_includes_v() {
398        let overlay = make_overlay().await;
399        let entries = overlay.list(Path::new("/")).await.unwrap();
400        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
401        assert!(names.contains(&"v"), "Root listing should include 'v' directory");
402    }
403
404    #[tokio::test]
405    async fn test_stat_virtual_path() {
406        let overlay = make_overlay().await;
407        let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
408        assert!(info.is_file());
409        assert_eq!(info.size, 9); // "blob data".len()
410    }
411
412    #[tokio::test]
413    async fn test_exists_virtual_path() {
414        let overlay = make_overlay().await;
415        assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
416        assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
417    }
418
419    #[tokio::test]
420    async fn test_mkdir_virtual_path() {
421        let overlay = make_overlay().await;
422        overlay.mkdir(Path::new("/v/newdir")).await.unwrap();
423        assert!(overlay.exists(Path::new("/v/newdir")).await);
424    }
425
426    #[tokio::test]
427    async fn test_remove_virtual_path() {
428        let overlay = make_overlay().await;
429        overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
430        assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
431    }
432
433    #[tokio::test]
434    async fn test_rename_within_virtual() {
435        let overlay = make_overlay().await;
436        overlay
437            .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
438            .await
439            .unwrap();
440        assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
441        assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
442    }
443
444    #[tokio::test]
445    async fn test_rename_across_boundary_fails() {
446        let overlay = make_overlay().await;
447        let result = overlay
448            .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
449            .await;
450        assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
451    }
452
453    #[tokio::test]
454    async fn test_backend_type() {
455        let overlay = make_overlay().await;
456        assert_eq!(overlay.backend_type(), "virtual-overlay");
457    }
458
459    #[tokio::test]
460    async fn test_resolve_real_path_virtual() {
461        let overlay = make_overlay().await;
462        // Virtual paths don't resolve to real paths
463        assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
464    }
465}