1use async_trait::async_trait;
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24use std::time::UNIX_EPOCH;
25
26use super::{
27 BackendError, BackendResult, EntryInfo, KernelBackend, LocalBackend, PatchOp, ReadRange,
28 ToolInfo, ToolResult, WriteMode,
29};
30use crate::tools::{ExecContext, ToolArgs};
31use crate::vfs::{EntryType, Filesystem, MountInfo, VfsRouter};
32
33pub struct VirtualOverlayBackend {
38 inner: Arc<dyn KernelBackend>,
40 vfs: Arc<VfsRouter>,
42}
43
44impl VirtualOverlayBackend {
45 pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
58 Self { inner, vfs }
59 }
60
61 fn is_virtual_path(path: &Path) -> bool {
63 let path_str = path.to_string_lossy();
64 path_str == "/v" || path_str.starts_with("/v/")
65 }
66
67 pub fn inner(&self) -> &Arc<dyn KernelBackend> {
69 &self.inner
70 }
71
72 pub fn vfs(&self) -> &Arc<VfsRouter> {
74 &self.vfs
75 }
76}
77
78impl std::fmt::Debug for VirtualOverlayBackend {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("VirtualOverlayBackend")
81 .field("inner_type", &self.inner.backend_type())
82 .field("vfs", &self.vfs)
83 .finish()
84 }
85}
86
87#[async_trait]
88impl KernelBackend for VirtualOverlayBackend {
89 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
94 if Self::is_virtual_path(path) {
95 let content = self.vfs.read(path).await?;
96 match range {
97 Some(r) => Ok(LocalBackend::apply_read_range(&content, &r)),
98 None => Ok(content),
99 }
100 } else {
101 self.inner.read(path, range).await
102 }
103 }
104
105 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
106 if Self::is_virtual_path(path) {
107 match mode {
108 WriteMode::CreateNew => {
109 if self.vfs.exists(path).await {
110 return Err(BackendError::AlreadyExists(path.display().to_string()));
111 }
112 self.vfs.write(path, content).await?;
113 }
114 WriteMode::Overwrite | WriteMode::Truncate => {
115 self.vfs.write(path, content).await?;
116 }
117 WriteMode::UpdateOnly => {
118 if !self.vfs.exists(path).await {
119 return Err(BackendError::NotFound(path.display().to_string()));
120 }
121 self.vfs.write(path, content).await?;
122 }
123 }
124 Ok(())
125 } else {
126 self.inner.write(path, content, mode).await
127 }
128 }
129
130 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
131 if Self::is_virtual_path(path) {
132 let mut existing = match self.vfs.read(path).await {
133 Ok(data) => data,
134 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
135 Err(e) => return Err(e.into()),
136 };
137 existing.extend_from_slice(content);
138 self.vfs.write(path, &existing).await?;
139 Ok(())
140 } else {
141 self.inner.append(path, content).await
142 }
143 }
144
145 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
146 if Self::is_virtual_path(path) {
147 let data = self.vfs.read(path).await?;
149 let mut content = String::from_utf8(data)
150 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
151
152 for op in ops {
154 LocalBackend::apply_patch_op(&mut content, op)?;
155 }
156
157 self.vfs.write(path, content.as_bytes()).await?;
159 Ok(())
160 } else {
161 self.inner.patch(path, ops).await
162 }
163 }
164
165 async fn list(&self, path: &Path) -> BackendResult<Vec<EntryInfo>> {
170 if Self::is_virtual_path(path) {
171 let entries = self.vfs.list(path).await?;
172 Ok(entries
173 .into_iter()
174 .map(|e| {
175 let (is_dir, is_file, is_symlink) = match e.entry_type {
176 EntryType::Directory => (true, false, false),
177 EntryType::File => (false, true, false),
178 EntryType::Symlink => (false, false, true),
179 };
180 EntryInfo {
181 name: e.name,
182 is_dir,
183 is_file,
184 is_symlink,
185 size: e.size,
186 modified: None,
187 permissions: None,
188 symlink_target: e.symlink_target,
189 }
190 })
191 .collect())
192 } else if path.to_string_lossy() == "/" || path.to_string_lossy().is_empty() {
193 let mut entries = self.inner.list(path).await?;
195 if !entries.iter().any(|e| e.name == "v") {
197 entries.push(EntryInfo::directory("v"));
198 }
199 Ok(entries)
200 } else {
201 self.inner.list(path).await
202 }
203 }
204
205 async fn stat(&self, path: &Path) -> BackendResult<EntryInfo> {
206 if Self::is_virtual_path(path) {
207 let meta = self.vfs.stat(path).await?;
208 let modified = meta.modified.and_then(|t| {
209 t.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs())
210 });
211 Ok(EntryInfo {
212 name: path
213 .file_name()
214 .map(|s| s.to_string_lossy().to_string())
215 .unwrap_or_else(|| "v".to_string()),
216 is_dir: meta.is_dir,
217 is_file: meta.is_file,
218 is_symlink: meta.is_symlink,
219 size: meta.size,
220 modified,
221 permissions: None,
222 symlink_target: None,
223 })
224 } else {
225 self.inner.stat(path).await
226 }
227 }
228
229 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
230 if Self::is_virtual_path(path) {
231 self.vfs.mkdir(path).await?;
232 Ok(())
233 } else {
234 self.inner.mkdir(path).await
235 }
236 }
237
238 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
239 if Self::is_virtual_path(path) {
240 if recursive
241 && let Ok(meta) = self.vfs.stat(path).await
242 && meta.is_dir
243 && let Ok(entries) = self.vfs.list(path).await
244 {
245 for entry in entries {
246 let child_path = path.join(&entry.name);
247 Box::pin(self.remove(&child_path, true)).await?;
248 }
249 }
250 self.vfs.remove(path).await?;
251 Ok(())
252 } else {
253 self.inner.remove(path, recursive).await
254 }
255 }
256
257 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
258 let from_virtual = Self::is_virtual_path(from);
259 let to_virtual = Self::is_virtual_path(to);
260
261 if from_virtual != to_virtual {
262 return Err(BackendError::InvalidOperation(
263 "cannot rename between virtual and non-virtual paths".into(),
264 ));
265 }
266
267 if from_virtual {
268 self.vfs.rename(from, to).await?;
269 Ok(())
270 } else {
271 self.inner.rename(from, to).await
272 }
273 }
274
275 async fn exists(&self, path: &Path) -> bool {
276 if Self::is_virtual_path(path) {
277 self.vfs.exists(path).await
278 } else {
279 self.inner.exists(path).await
280 }
281 }
282
283 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
288 if Self::is_virtual_path(path) {
289 Ok(self.vfs.read_link(path).await?)
290 } else {
291 self.inner.read_link(path).await
292 }
293 }
294
295 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
296 if Self::is_virtual_path(link) {
297 self.vfs.symlink(target, link).await?;
298 Ok(())
299 } else {
300 self.inner.symlink(target, link).await
301 }
302 }
303
304 async fn call_tool(
309 &self,
310 name: &str,
311 args: ToolArgs,
312 ctx: &mut ExecContext,
313 ) -> BackendResult<ToolResult> {
314 self.inner.call_tool(name, args, ctx).await
316 }
317
318 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
319 self.inner.list_tools().await
320 }
321
322 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
323 self.inner.get_tool(name).await
324 }
325
326 fn read_only(&self) -> bool {
331 self.inner.read_only() && self.vfs.read_only()
333 }
334
335 fn backend_type(&self) -> &str {
336 "virtual-overlay"
337 }
338
339 fn mounts(&self) -> Vec<MountInfo> {
340 let mut mounts = self.inner.mounts();
341 mounts.extend(self.vfs.list_mounts());
342 mounts
343 }
344
345 fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
346 if Self::is_virtual_path(path) {
347 None
349 } else {
350 self.inner.resolve_real_path(path)
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::backend::testing::MockBackend;
359 use crate::vfs::MemoryFs;
360
361 async fn make_overlay() -> VirtualOverlayBackend {
362 let (mock, _) = MockBackend::new();
364 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
365
366 let mut vfs = VfsRouter::new();
368 let mem = MemoryFs::new();
369 mem.write(Path::new("blobs/test.bin"), b"blob data").await.unwrap();
370 mem.mkdir(Path::new("jobs")).await.unwrap();
371 vfs.mount("/v", mem);
372
373 VirtualOverlayBackend::new(inner, Arc::new(vfs))
374 }
375
376 #[tokio::test]
377 async fn test_virtual_path_detection() {
378 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v")));
379 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/")));
380 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/jobs")));
381 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/blobs/test.bin")));
382
383 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/docs")));
384 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/g/repo")));
385 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/")));
386 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/var")));
387 }
388
389 #[tokio::test]
390 async fn test_read_virtual_path() {
391 let overlay = make_overlay().await;
392 let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
393 assert_eq!(content, b"blob data");
394 }
395
396 #[tokio::test]
397 async fn test_write_virtual_path() {
398 let overlay = make_overlay().await;
399 overlay
400 .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
401 .await
402 .unwrap();
403 let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
404 assert_eq!(content, b"new data");
405 }
406
407 #[tokio::test]
408 async fn test_list_virtual_path() {
409 let overlay = make_overlay().await;
410 let entries = overlay.list(Path::new("/v")).await.unwrap();
411 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
412 assert!(names.contains(&"blobs"));
413 assert!(names.contains(&"jobs"));
414 }
415
416 #[tokio::test]
417 async fn test_root_listing_includes_v() {
418 let overlay = make_overlay().await;
419 let entries = overlay.list(Path::new("/")).await.unwrap();
420 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
421 assert!(names.contains(&"v"), "Root listing should include 'v' directory");
422 }
423
424 #[tokio::test]
425 async fn test_stat_virtual_path() {
426 let overlay = make_overlay().await;
427 let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
428 assert!(info.is_file);
429 assert_eq!(info.size, 9); }
431
432 #[tokio::test]
433 async fn test_exists_virtual_path() {
434 let overlay = make_overlay().await;
435 assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
436 assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
437 }
438
439 #[tokio::test]
440 async fn test_mkdir_virtual_path() {
441 let overlay = make_overlay().await;
442 overlay.mkdir(Path::new("/v/newdir")).await.unwrap();
443 assert!(overlay.exists(Path::new("/v/newdir")).await);
444 }
445
446 #[tokio::test]
447 async fn test_remove_virtual_path() {
448 let overlay = make_overlay().await;
449 overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
450 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
451 }
452
453 #[tokio::test]
454 async fn test_rename_within_virtual() {
455 let overlay = make_overlay().await;
456 overlay
457 .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
458 .await
459 .unwrap();
460 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
461 assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
462 }
463
464 #[tokio::test]
465 async fn test_rename_across_boundary_fails() {
466 let overlay = make_overlay().await;
467 let result = overlay
468 .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
469 .await;
470 assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
471 }
472
473 #[tokio::test]
474 async fn test_backend_type() {
475 let overlay = make_overlay().await;
476 assert_eq!(overlay.backend_type(), "virtual-overlay");
477 }
478
479 #[tokio::test]
480 async fn test_resolve_real_path_virtual() {
481 let overlay = make_overlay().await;
482 assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
484 }
485}