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