1use async_trait::async_trait;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29use super::{
30 BackendError, BackendResult, KernelBackend, LocalBackend, PatchOp, ReadRange,
31 ToolInfo, ToolResult, WriteMode,
32};
33use crate::tools::{ToolArgs, ToolCtx};
34use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
35
36pub struct VirtualOverlayBackend {
41 inner: Arc<dyn KernelBackend>,
43 vfs: Arc<VfsRouter>,
45}
46
47impl VirtualOverlayBackend {
48 pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
61 Self { inner, vfs }
62 }
63
64 fn is_virtual_path(&self, path: &Path) -> bool {
73 let path_str = path.to_string_lossy();
74 path_str == "/v" || path_str.starts_with("/v/") || self.vfs.has_mount(path)
75 }
76
77 pub fn inner(&self) -> &Arc<dyn KernelBackend> {
79 &self.inner
80 }
81
82 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 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
104 if self.is_virtual_path(path) {
105 Ok(self.vfs.read_range(path, range).await?)
106 } else {
107 self.inner.read(path, range).await
108 }
109 }
110
111 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
112 if self.is_virtual_path(path) {
113 match mode {
114 WriteMode::CreateNew => {
115 if self.vfs.exists(path).await {
116 return Err(BackendError::AlreadyExists(path.display().to_string()));
117 }
118 self.vfs.write(path, content).await?;
119 }
120 WriteMode::Overwrite | WriteMode::Truncate => {
121 self.vfs.write(path, content).await?;
122 }
123 WriteMode::UpdateOnly => {
124 if !self.vfs.exists(path).await {
125 return Err(BackendError::NotFound(path.display().to_string()));
126 }
127 self.vfs.write(path, content).await?;
128 }
129 _ => {
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 set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
141 if self.is_virtual_path(path) {
142 self.vfs.set_mtime(path, mtime).await?;
143 Ok(())
144 } else {
145 self.inner.set_mtime(path, mtime).await
146 }
147 }
148
149 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
150 if self.is_virtual_path(path) {
151 let mut existing = match self.vfs.read(path).await {
152 Ok(data) => data,
153 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
154 Err(e) => return Err(e.into()),
155 };
156 existing.extend_from_slice(content);
157 self.vfs.write(path, &existing).await?;
158 Ok(())
159 } else {
160 self.inner.append(path, content).await
161 }
162 }
163
164 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
165 if self.is_virtual_path(path) {
166 let data = self.vfs.read(path).await?;
168 let mut content = String::from_utf8(data)
169 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
170
171 for op in ops {
173 LocalBackend::apply_patch_op(&mut content, op)?;
174 }
175
176 self.vfs.write(path, content.as_bytes()).await?;
178 Ok(())
179 } else {
180 self.inner.patch(path, ops).await
181 }
182 }
183
184 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
189 if self.is_virtual_path(path) {
190 Ok(self.vfs.list(path).await?)
191 } else if path.to_string_lossy() == "/" || path.to_string_lossy().is_empty() {
192 let mut entries = self.inner.list(path).await?;
194 if !entries.iter().any(|e| e.name == "v") {
196 entries.push(DirEntry::directory("v"));
197 }
198 Ok(entries)
199 } else {
200 self.inner.list(path).await
201 }
202 }
203
204 async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
205 if self.is_virtual_path(path) {
206 Ok(self.vfs.stat(path).await?)
207 } else {
208 self.inner.stat(path).await
209 }
210 }
211
212 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
213 if self.is_virtual_path(path) {
214 self.vfs.mkdir(path).await?;
215 Ok(())
216 } else {
217 self.inner.mkdir(path).await
218 }
219 }
220
221 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
222 if self.is_virtual_path(path) {
223 if recursive
224 && let Ok(entry) = self.vfs.lstat(path).await
225 && entry.is_dir()
226 && let Ok(entries) = self.vfs.list(path).await
227 {
228 for entry in entries {
229 let child_path = path.join(&entry.name);
230 Box::pin(self.remove(&child_path, true)).await?;
231 }
232 }
233 self.vfs.remove(path).await?;
234 Ok(())
235 } else {
236 self.inner.remove(path, recursive).await
237 }
238 }
239
240 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
241 let from_virtual = self.is_virtual_path(from);
242 let to_virtual = self.is_virtual_path(to);
243
244 if from_virtual != to_virtual {
245 return Err(BackendError::InvalidOperation(
246 "cannot rename between virtual and non-virtual paths".into(),
247 ));
248 }
249
250 if from_virtual {
251 self.vfs.rename(from, to).await?;
252 Ok(())
253 } else {
254 self.inner.rename(from, to).await
255 }
256 }
257
258 async fn exists(&self, path: &Path) -> bool {
259 if self.is_virtual_path(path) {
260 self.vfs.exists(path).await
261 } else {
262 self.inner.exists(path).await
263 }
264 }
265
266 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
271 if self.is_virtual_path(path) {
272 Ok(self.vfs.lstat(path).await?)
273 } else {
274 self.inner.lstat(path).await
275 }
276 }
277
278 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
279 if self.is_virtual_path(path) {
280 Ok(self.vfs.read_link(path).await?)
281 } else {
282 self.inner.read_link(path).await
283 }
284 }
285
286 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
287 if self.is_virtual_path(link) {
288 self.vfs.symlink(target, link).await?;
289 Ok(())
290 } else {
291 self.inner.symlink(target, link).await
292 }
293 }
294
295 async fn call_tool(
300 &self,
301 name: &str,
302 args: ToolArgs,
303 ctx: &mut dyn ToolCtx,
304 ) -> BackendResult<ToolResult> {
305 self.inner.call_tool(name, args, ctx).await
307 }
308
309 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
310 self.inner.list_tools().await
311 }
312
313 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
314 self.inner.get_tool(name).await
315 }
316
317 fn read_only(&self) -> bool {
322 self.inner.read_only() && self.vfs.read_only()
324 }
325
326 fn backend_type(&self) -> &str {
327 "virtual-overlay"
328 }
329
330 fn mounts(&self) -> Vec<MountInfo> {
331 let mut mounts = self.inner.mounts();
332 mounts.extend(self.vfs.list_mounts());
333 mounts
334 }
335
336 fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
337 if self.is_virtual_path(path) {
338 None
340 } else {
341 self.inner.resolve_real_path(path)
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::backend::testing::MockBackend;
350 use crate::vfs::MemoryFs;
351
352 async fn make_overlay() -> VirtualOverlayBackend {
353 let (mock, _) = MockBackend::new();
355 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
356
357 let mut vfs = VfsRouter::new();
359 let mem = MemoryFs::new();
360 mem.write(Path::new("blobs/test.bin"), b"blob data").await.unwrap();
361 mem.mkdir(Path::new("jobs")).await.unwrap();
362 vfs.mount("/v", mem);
363
364 VirtualOverlayBackend::new(inner, Arc::new(vfs))
365 }
366
367 #[tokio::test]
368 async fn test_virtual_path_detection() {
369 let overlay = make_overlay().await;
370 assert!(overlay.is_virtual_path(Path::new("/v")));
371 assert!(overlay.is_virtual_path(Path::new("/v/")));
372 assert!(overlay.is_virtual_path(Path::new("/v/jobs")));
373 assert!(overlay.is_virtual_path(Path::new("/v/blobs/test.bin")));
374
375 assert!(!overlay.is_virtual_path(Path::new("/docs")));
376 assert!(!overlay.is_virtual_path(Path::new("/g/repo")));
377 assert!(!overlay.is_virtual_path(Path::new("/")));
378 assert!(!overlay.is_virtual_path(Path::new("/var")));
379 }
380
381 #[tokio::test]
382 async fn test_non_v_mount_is_virtual_path() {
383 let (mock, _) = MockBackend::new();
388 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
389 let mut vfs = VfsRouter::new();
390 vfs.mount("/dev", MemoryFs::new());
391 let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
392
393 assert!(overlay.is_virtual_path(Path::new("/dev/null")));
394 assert!(!overlay.is_virtual_path(Path::new("/docs")));
395 }
396
397 #[tokio::test]
398 async fn test_read_virtual_path() {
399 let overlay = make_overlay().await;
400 let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
401 assert_eq!(content, b"blob data");
402 }
403
404 #[tokio::test]
405 async fn test_write_virtual_path() {
406 let overlay = make_overlay().await;
407 overlay
408 .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
409 .await
410 .unwrap();
411 let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
412 assert_eq!(content, b"new data");
413 }
414
415 #[tokio::test]
416 async fn test_list_virtual_path() {
417 let overlay = make_overlay().await;
418 let entries = overlay.list(Path::new("/v")).await.unwrap();
419 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
420 assert!(names.contains(&"blobs"));
421 assert!(names.contains(&"jobs"));
422 }
423
424 #[tokio::test]
425 async fn test_root_listing_includes_v() {
426 let overlay = make_overlay().await;
427 let entries = overlay.list(Path::new("/")).await.unwrap();
428 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
429 assert!(names.contains(&"v"), "Root listing should include 'v' directory");
430 }
431
432 #[tokio::test]
433 async fn test_stat_virtual_path() {
434 let overlay = make_overlay().await;
435 let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
436 assert!(info.is_file());
437 assert_eq!(info.size, 9); }
439
440 #[tokio::test]
441 async fn test_exists_virtual_path() {
442 let overlay = make_overlay().await;
443 assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
444 assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
445 }
446
447 #[tokio::test]
448 async fn test_mkdir_virtual_path() {
449 let overlay = make_overlay().await;
450 overlay.mkdir(Path::new("/v/newdir")).await.unwrap();
451 assert!(overlay.exists(Path::new("/v/newdir")).await);
452 }
453
454 #[tokio::test]
455 async fn test_remove_virtual_path() {
456 let overlay = make_overlay().await;
457 overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
458 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
459 }
460
461 #[tokio::test]
462 async fn test_rename_within_virtual() {
463 let overlay = make_overlay().await;
464 overlay
465 .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
466 .await
467 .unwrap();
468 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
469 assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
470 }
471
472 #[tokio::test]
473 async fn test_rename_across_boundary_fails() {
474 let overlay = make_overlay().await;
475 let result = overlay
476 .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
477 .await;
478 assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
479 }
480
481 #[tokio::test]
482 async fn test_backend_type() {
483 let overlay = make_overlay().await;
484 assert_eq!(overlay.backend_type(), "virtual-overlay");
485 }
486
487 #[tokio::test]
488 async fn test_resolve_real_path_virtual() {
489 let overlay = make_overlay().await;
490 assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
492 }
493}