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