1use 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
32pub struct VirtualOverlayBackend {
37 inner: Arc<dyn KernelBackend>,
39 vfs: Arc<VfsRouter>,
41}
42
43impl VirtualOverlayBackend {
44 pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
57 Self { inner, vfs }
58 }
59
60 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 pub fn inner(&self) -> &Arc<dyn KernelBackend> {
68 &self.inner
69 }
70
71 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 async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
93 if Self::is_virtual_path(path) {
94 let content = self.vfs.read(path).await?;
95 match range {
96 Some(r) => Ok(LocalBackend::apply_read_range(&content, &r)),
97 None => Ok(content),
98 }
99 } else {
100 self.inner.read(path, range).await
101 }
102 }
103
104 async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
105 if Self::is_virtual_path(path) {
106 match mode {
107 WriteMode::CreateNew => {
108 if self.vfs.exists(path).await {
109 return Err(BackendError::AlreadyExists(path.display().to_string()));
110 }
111 self.vfs.write(path, content).await?;
112 }
113 WriteMode::Overwrite | WriteMode::Truncate => {
114 self.vfs.write(path, content).await?;
115 }
116 WriteMode::UpdateOnly => {
117 if !self.vfs.exists(path).await {
118 return Err(BackendError::NotFound(path.display().to_string()));
119 }
120 self.vfs.write(path, content).await?;
121 }
122 _ => {
124 self.vfs.write(path, content).await?;
125 }
126 }
127 Ok(())
128 } else {
129 self.inner.write(path, content, mode).await
130 }
131 }
132
133 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
134 if Self::is_virtual_path(path) {
135 self.vfs.set_mtime(path, mtime).await?;
136 Ok(())
137 } else {
138 self.inner.set_mtime(path, mtime).await
139 }
140 }
141
142 async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
143 if Self::is_virtual_path(path) {
144 let mut existing = match self.vfs.read(path).await {
145 Ok(data) => data,
146 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
147 Err(e) => return Err(e.into()),
148 };
149 existing.extend_from_slice(content);
150 self.vfs.write(path, &existing).await?;
151 Ok(())
152 } else {
153 self.inner.append(path, content).await
154 }
155 }
156
157 async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
158 if Self::is_virtual_path(path) {
159 let data = self.vfs.read(path).await?;
161 let mut content = String::from_utf8(data)
162 .map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
163
164 for op in ops {
166 LocalBackend::apply_patch_op(&mut content, op)?;
167 }
168
169 self.vfs.write(path, content.as_bytes()).await?;
171 Ok(())
172 } else {
173 self.inner.patch(path, ops).await
174 }
175 }
176
177 async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
182 if Self::is_virtual_path(path) {
183 Ok(self.vfs.list(path).await?)
184 } else if path.to_string_lossy() == "/" || path.to_string_lossy().is_empty() {
185 let mut entries = self.inner.list(path).await?;
187 if !entries.iter().any(|e| e.name == "v") {
189 entries.push(DirEntry::directory("v"));
190 }
191 Ok(entries)
192 } else {
193 self.inner.list(path).await
194 }
195 }
196
197 async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
198 if Self::is_virtual_path(path) {
199 Ok(self.vfs.stat(path).await?)
200 } else {
201 self.inner.stat(path).await
202 }
203 }
204
205 async fn mkdir(&self, path: &Path) -> BackendResult<()> {
206 if Self::is_virtual_path(path) {
207 self.vfs.mkdir(path).await?;
208 Ok(())
209 } else {
210 self.inner.mkdir(path).await
211 }
212 }
213
214 async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
215 if Self::is_virtual_path(path) {
216 if recursive
217 && let Ok(entry) = self.vfs.stat(path).await
218 && entry.is_dir()
219 && let Ok(entries) = self.vfs.list(path).await
220 {
221 for entry in entries {
222 let child_path = path.join(&entry.name);
223 Box::pin(self.remove(&child_path, true)).await?;
224 }
225 }
226 self.vfs.remove(path).await?;
227 Ok(())
228 } else {
229 self.inner.remove(path, recursive).await
230 }
231 }
232
233 async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
234 let from_virtual = Self::is_virtual_path(from);
235 let to_virtual = Self::is_virtual_path(to);
236
237 if from_virtual != to_virtual {
238 return Err(BackendError::InvalidOperation(
239 "cannot rename between virtual and non-virtual paths".into(),
240 ));
241 }
242
243 if from_virtual {
244 self.vfs.rename(from, to).await?;
245 Ok(())
246 } else {
247 self.inner.rename(from, to).await
248 }
249 }
250
251 async fn exists(&self, path: &Path) -> bool {
252 if Self::is_virtual_path(path) {
253 self.vfs.exists(path).await
254 } else {
255 self.inner.exists(path).await
256 }
257 }
258
259 async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
264 if Self::is_virtual_path(path) {
265 Ok(self.vfs.lstat(path).await?)
266 } else {
267 self.inner.lstat(path).await
268 }
269 }
270
271 async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
272 if Self::is_virtual_path(path) {
273 Ok(self.vfs.read_link(path).await?)
274 } else {
275 self.inner.read_link(path).await
276 }
277 }
278
279 async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
280 if Self::is_virtual_path(link) {
281 self.vfs.symlink(target, link).await?;
282 Ok(())
283 } else {
284 self.inner.symlink(target, link).await
285 }
286 }
287
288 async fn call_tool(
293 &self,
294 name: &str,
295 args: ToolArgs,
296 ctx: &mut dyn ToolCtx,
297 ) -> BackendResult<ToolResult> {
298 self.inner.call_tool(name, args, ctx).await
300 }
301
302 async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
303 self.inner.list_tools().await
304 }
305
306 async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
307 self.inner.get_tool(name).await
308 }
309
310 fn read_only(&self) -> bool {
315 self.inner.read_only() && self.vfs.read_only()
317 }
318
319 fn backend_type(&self) -> &str {
320 "virtual-overlay"
321 }
322
323 fn mounts(&self) -> Vec<MountInfo> {
324 let mut mounts = self.inner.mounts();
325 mounts.extend(self.vfs.list_mounts());
326 mounts
327 }
328
329 fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
330 if Self::is_virtual_path(path) {
331 None
333 } else {
334 self.inner.resolve_real_path(path)
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::backend::testing::MockBackend;
343 use crate::vfs::MemoryFs;
344
345 async fn make_overlay() -> VirtualOverlayBackend {
346 let (mock, _) = MockBackend::new();
348 let inner: Arc<dyn KernelBackend> = Arc::new(mock);
349
350 let mut vfs = VfsRouter::new();
352 let mem = MemoryFs::new();
353 mem.write(Path::new("blobs/test.bin"), b"blob data").await.unwrap();
354 mem.mkdir(Path::new("jobs")).await.unwrap();
355 vfs.mount("/v", mem);
356
357 VirtualOverlayBackend::new(inner, Arc::new(vfs))
358 }
359
360 #[tokio::test]
361 async fn test_virtual_path_detection() {
362 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v")));
363 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/")));
364 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/jobs")));
365 assert!(VirtualOverlayBackend::is_virtual_path(Path::new("/v/blobs/test.bin")));
366
367 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/docs")));
368 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/g/repo")));
369 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/")));
370 assert!(!VirtualOverlayBackend::is_virtual_path(Path::new("/var")));
371 }
372
373 #[tokio::test]
374 async fn test_read_virtual_path() {
375 let overlay = make_overlay().await;
376 let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
377 assert_eq!(content, b"blob data");
378 }
379
380 #[tokio::test]
381 async fn test_write_virtual_path() {
382 let overlay = make_overlay().await;
383 overlay
384 .write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
385 .await
386 .unwrap();
387 let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
388 assert_eq!(content, b"new data");
389 }
390
391 #[tokio::test]
392 async fn test_list_virtual_path() {
393 let overlay = make_overlay().await;
394 let entries = overlay.list(Path::new("/v")).await.unwrap();
395 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
396 assert!(names.contains(&"blobs"));
397 assert!(names.contains(&"jobs"));
398 }
399
400 #[tokio::test]
401 async fn test_root_listing_includes_v() {
402 let overlay = make_overlay().await;
403 let entries = overlay.list(Path::new("/")).await.unwrap();
404 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
405 assert!(names.contains(&"v"), "Root listing should include 'v' directory");
406 }
407
408 #[tokio::test]
409 async fn test_stat_virtual_path() {
410 let overlay = make_overlay().await;
411 let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
412 assert!(info.is_file());
413 assert_eq!(info.size, 9); }
415
416 #[tokio::test]
417 async fn test_exists_virtual_path() {
418 let overlay = make_overlay().await;
419 assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
420 assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
421 }
422
423 #[tokio::test]
424 async fn test_mkdir_virtual_path() {
425 let overlay = make_overlay().await;
426 overlay.mkdir(Path::new("/v/newdir")).await.unwrap();
427 assert!(overlay.exists(Path::new("/v/newdir")).await);
428 }
429
430 #[tokio::test]
431 async fn test_remove_virtual_path() {
432 let overlay = make_overlay().await;
433 overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
434 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
435 }
436
437 #[tokio::test]
438 async fn test_rename_within_virtual() {
439 let overlay = make_overlay().await;
440 overlay
441 .rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
442 .await
443 .unwrap();
444 assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
445 assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
446 }
447
448 #[tokio::test]
449 async fn test_rename_across_boundary_fails() {
450 let overlay = make_overlay().await;
451 let result = overlay
452 .rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
453 .await;
454 assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
455 }
456
457 #[tokio::test]
458 async fn test_backend_type() {
459 let overlay = make_overlay().await;
460 assert_eq!(overlay.backend_type(), "virtual-overlay");
461 }
462
463 #[tokio::test]
464 async fn test_resolve_real_path_virtual() {
465 let overlay = make_overlay().await;
466 assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
468 }
469}