1use 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
43pub struct VirtualOverlayBackend {
48 inner: Arc<dyn KernelBackend>,
50 vfs: Arc<VfsRouter>,
52}
53
54impl VirtualOverlayBackend {
55 pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
68 Self { inner, vfs }
69 }
70
71 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 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 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 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 for op in ops {
164 LocalBackend::apply_patch_op(&mut content, op)?;
165 }
166
167 self.vfs.write(path, content.as_bytes()).await?;
169 Ok(())
170 } else {
171 self.inner.patch(path, ops).await
172 }
173 }
174
175 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 let mut entries = self.inner.list(path).await?;
205 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 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 async fn call_tool(
319 &self,
320 name: &str,
321 args: ToolArgs,
322 ctx: &mut ExecContext,
323 ) -> BackendResult<ToolResult> {
324 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 fn read_only(&self) -> bool {
341 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 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 let (mock, _) = MockBackend::new();
374 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
375
376 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); }
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 assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
494 }
495}