use async_trait::async_trait;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::{
BackendError, BackendResult, KernelBackend, LocalBackend, PatchOp, ReadRange,
ToolInfo, ToolResult, WriteMode,
};
use crate::tools::{ToolArgs, ToolCtx};
use crate::vfs::{DirEntry, Filesystem, MountInfo, VfsRouter};
fn dir_basename(path: &Path) -> String {
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string())
}
fn synth_dir_note(path: &Path) -> String {
format!("{} is a synthesized directory that only holds kaish mounts", path.display())
}
pub struct VirtualOverlayBackend {
inner: Arc<dyn KernelBackend>,
vfs: Arc<VfsRouter>,
}
impl VirtualOverlayBackend {
pub fn new(inner: Arc<dyn KernelBackend>, vfs: Arc<VfsRouter>) -> Self {
Self { inner, vfs }
}
fn is_virtual_path(&self, path: &Path) -> bool {
self.vfs.has_mount(path)
}
fn is_shared_ancestor(&self, path: &Path) -> bool {
!self.is_virtual_path(path) && self.vfs.has_mount_under(path)
}
pub fn inner(&self) -> &Arc<dyn KernelBackend> {
&self.inner
}
pub fn vfs(&self) -> &Arc<VfsRouter> {
&self.vfs
}
}
impl std::fmt::Debug for VirtualOverlayBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VirtualOverlayBackend")
.field("inner_type", &self.inner.backend_type())
.field("vfs", &self.vfs)
.finish()
}
}
#[async_trait]
impl KernelBackend for VirtualOverlayBackend {
async fn read(&self, path: &Path, range: Option<ReadRange>) -> BackendResult<Vec<u8>> {
if self.is_virtual_path(path) {
Ok(self.vfs.read_range(path, range).await?)
} else if self.is_shared_ancestor(path) {
Err(BackendError::IsDirectory(synth_dir_note(path)))
} else {
self.inner.read(path, range).await
}
}
async fn write(&self, path: &Path, content: &[u8], mode: WriteMode) -> BackendResult<()> {
if self.is_virtual_path(path) {
match mode {
WriteMode::CreateNew => {
if self.vfs.exists(path).await {
return Err(BackendError::AlreadyExists(path.display().to_string()));
}
self.vfs.write(path, content).await?;
}
WriteMode::Overwrite | WriteMode::Truncate => {
self.vfs.write(path, content).await?;
}
WriteMode::UpdateOnly => {
if !self.vfs.exists(path).await {
return Err(BackendError::NotFound(path.display().to_string()));
}
self.vfs.write(path, content).await?;
}
_ => {
self.vfs.write(path, content).await?;
}
}
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::IsDirectory(synth_dir_note(path)))
} else {
self.inner.write(path, content, mode).await
}
}
async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> BackendResult<()> {
if self.is_virtual_path(path) {
self.vfs.set_mtime(path, mtime).await?;
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::InvalidOperation(format!("cannot set mtime: {}", synth_dir_note(path))))
} else {
self.inner.set_mtime(path, mtime).await
}
}
async fn append(&self, path: &Path, content: &[u8]) -> BackendResult<()> {
if self.is_virtual_path(path) {
self.vfs.append(path, content).await?;
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::IsDirectory(synth_dir_note(path)))
} else {
self.inner.append(path, content).await
}
}
async fn patch(&self, path: &Path, ops: &[PatchOp]) -> BackendResult<()> {
if self.is_virtual_path(path) {
let data = self.vfs.read(path).await?;
let mut content = String::from_utf8(data)
.map_err(|e| BackendError::InvalidOperation(format!("file is not valid UTF-8: {}", e)))?;
for op in ops {
LocalBackend::apply_patch_op(&mut content, op)?;
}
self.vfs.write(path, content.as_bytes()).await?;
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::IsDirectory(synth_dir_note(path)))
} else {
self.inner.patch(path, ops).await
}
}
async fn list(&self, path: &Path) -> BackendResult<Vec<DirEntry>> {
if self.is_virtual_path(path) {
Ok(self.vfs.list(path).await?)
} else if self.is_shared_ancestor(path) {
let mut by_name: std::collections::HashMap<String, DirEntry> =
std::collections::HashMap::new();
if let Ok(inner_entries) = self.inner.list(path).await {
for entry in inner_entries {
by_name.insert(entry.name.clone(), entry);
}
}
for entry in self.vfs.list(path).await? {
if self.vfs.has_mount(&path.join(&entry.name)) {
by_name.insert(entry.name.clone(), entry);
} else {
by_name.entry(entry.name.clone()).or_insert(entry);
}
}
let mut entries: Vec<DirEntry> = by_name.into_values().collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
} else {
self.inner.list(path).await
}
}
async fn stat(&self, path: &Path) -> BackendResult<DirEntry> {
if self.is_virtual_path(path) {
Ok(self.vfs.stat(path).await?)
} else if self.is_shared_ancestor(path) {
match self.inner.stat(path).await {
Ok(entry) if entry.is_dir() => Ok(entry),
_ => Ok(DirEntry::directory(dir_basename(path))),
}
} else {
self.inner.stat(path).await
}
}
async fn mkdir(&self, path: &Path) -> BackendResult<()> {
if self.is_virtual_path(path) {
self.vfs.mkdir(path).await?;
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::AlreadyExists(synth_dir_note(path)))
} else {
self.inner.mkdir(path).await
}
}
async fn remove(&self, path: &Path, recursive: bool) -> BackendResult<()> {
if self.is_virtual_path(path) {
if recursive
&& let Ok(entry) = self.vfs.lstat(path).await
&& entry.is_dir()
&& let Ok(entries) = self.vfs.list(path).await
{
for entry in entries {
let child_path = path.join(&entry.name);
Box::pin(self.remove(&child_path, true)).await?;
}
}
self.vfs.remove(path).await?;
Ok(())
} else if self.is_shared_ancestor(path) {
Err(BackendError::InvalidOperation(format!("cannot remove: {}", synth_dir_note(path))))
} else {
self.inner.remove(path, recursive).await
}
}
async fn rename(&self, from: &Path, to: &Path) -> BackendResult<()> {
let from_virtual = self.is_virtual_path(from);
let to_virtual = self.is_virtual_path(to);
if from_virtual != to_virtual {
return Err(BackendError::InvalidOperation(
"cannot rename between virtual and non-virtual paths".into(),
));
}
if self.is_shared_ancestor(from) || self.is_shared_ancestor(to) {
return Err(BackendError::InvalidOperation(format!(
"cannot rename: {} is a synthesized directory",
if self.is_shared_ancestor(from) { from.display() } else { to.display() }
)));
}
if from_virtual {
self.vfs.rename(from, to).await?;
Ok(())
} else {
self.inner.rename(from, to).await
}
}
async fn exists(&self, path: &Path) -> bool {
if self.is_virtual_path(path) {
self.vfs.exists(path).await
} else {
self.is_shared_ancestor(path) || self.inner.exists(path).await
}
}
async fn lstat(&self, path: &Path) -> BackendResult<DirEntry> {
if self.is_virtual_path(path) {
Ok(self.vfs.lstat(path).await?)
} else if self.is_shared_ancestor(path) {
match self.inner.lstat(path).await {
Ok(entry) if entry.is_dir() => Ok(entry),
_ => Ok(DirEntry::directory(dir_basename(path))),
}
} else {
self.inner.lstat(path).await
}
}
async fn read_link(&self, path: &Path) -> BackendResult<PathBuf> {
if self.is_virtual_path(path) {
Ok(self.vfs.read_link(path).await?)
} else if self.is_shared_ancestor(path) {
Err(BackendError::InvalidOperation(format!(
"{} is a directory, not a symlink",
path.display()
)))
} else {
self.inner.read_link(path).await
}
}
async fn symlink(&self, target: &Path, link: &Path) -> BackendResult<()> {
if self.is_virtual_path(link) {
self.vfs.symlink(target, link).await?;
Ok(())
} else if self.is_shared_ancestor(link) {
Err(BackendError::AlreadyExists(synth_dir_note(link)))
} else {
self.inner.symlink(target, link).await
}
}
async fn call_tool(
&self,
name: &str,
args: ToolArgs,
ctx: &mut dyn ToolCtx,
) -> BackendResult<ToolResult> {
self.inner.call_tool(name, args, ctx).await
}
async fn list_tools(&self) -> BackendResult<Vec<ToolInfo>> {
self.inner.list_tools().await
}
async fn get_tool(&self, name: &str) -> BackendResult<Option<ToolInfo>> {
self.inner.get_tool(name).await
}
fn read_only(&self) -> bool {
self.inner.read_only() && self.vfs.read_only()
}
fn backend_type(&self) -> &str {
"virtual-overlay"
}
fn mounts(&self) -> Vec<MountInfo> {
let mut mounts = self.inner.mounts();
mounts.extend(self.vfs.list_mounts());
mounts
}
fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
if self.is_virtual_path(path) {
None
} else {
self.inner.resolve_real_path(path)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::testing::MockBackend;
use crate::vfs::MemoryFs;
async fn make_overlay() -> VirtualOverlayBackend {
let (mock, _) = MockBackend::new();
let inner: Arc<dyn KernelBackend> = Arc::new(mock);
let mut vfs = VfsRouter::new();
let blobs = MemoryFs::new();
blobs.write(Path::new("test.bin"), b"blob data").await.unwrap();
vfs.mount("/v/blobs", blobs);
vfs.mount("/v/jobs", MemoryFs::new());
VirtualOverlayBackend::new(inner, Arc::new(vfs))
}
#[tokio::test]
async fn test_virtual_path_detection() {
let overlay = make_overlay().await;
assert!(overlay.is_virtual_path(Path::new("/v/jobs")));
assert!(overlay.is_virtual_path(Path::new("/v/blobs")));
assert!(overlay.is_virtual_path(Path::new("/v/blobs/test.bin")));
assert!(!overlay.is_virtual_path(Path::new("/v")));
assert!(!overlay.is_virtual_path(Path::new("/v/")));
assert!(!overlay.is_virtual_path(Path::new("/v/unclaimed")));
assert!(!overlay.is_virtual_path(Path::new("/docs")));
assert!(!overlay.is_virtual_path(Path::new("/g/repo")));
assert!(!overlay.is_virtual_path(Path::new("/")));
assert!(!overlay.is_virtual_path(Path::new("/var")));
}
#[tokio::test]
async fn test_non_v_mount_is_virtual_path() {
let (mock, _) = MockBackend::new();
let inner: Arc<dyn KernelBackend> = Arc::new(mock);
let mut vfs = VfsRouter::new();
vfs.mount("/dev", MemoryFs::new());
let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
assert!(overlay.is_virtual_path(Path::new("/dev/null")));
assert!(!overlay.is_virtual_path(Path::new("/docs")));
}
#[tokio::test]
async fn test_read_virtual_path() {
let overlay = make_overlay().await;
let content = overlay.read(Path::new("/v/blobs/test.bin"), None).await.unwrap();
assert_eq!(content, b"blob data");
}
#[tokio::test]
async fn test_write_virtual_path() {
let overlay = make_overlay().await;
overlay
.write(Path::new("/v/blobs/new.bin"), b"new data", WriteMode::Overwrite)
.await
.unwrap();
let content = overlay.read(Path::new("/v/blobs/new.bin"), None).await.unwrap();
assert_eq!(content, b"new data");
}
#[tokio::test]
async fn test_list_virtual_path() {
let overlay = make_overlay().await;
let entries = overlay.list(Path::new("/v")).await.unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"blobs"));
assert!(names.contains(&"jobs"));
}
#[tokio::test]
async fn test_root_listing_includes_v() {
let overlay = make_overlay().await;
let entries = overlay.list(Path::new("/")).await.unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"v"), "Root listing should include 'v' directory");
}
#[tokio::test]
async fn test_stat_virtual_path() {
let overlay = make_overlay().await;
let info = overlay.stat(Path::new("/v/blobs/test.bin")).await.unwrap();
assert!(info.is_file());
assert_eq!(info.size, 9); }
#[tokio::test]
async fn test_exists_virtual_path() {
let overlay = make_overlay().await;
assert!(overlay.exists(Path::new("/v/blobs/test.bin")).await);
assert!(!overlay.exists(Path::new("/v/blobs/nonexistent")).await);
}
#[tokio::test]
async fn test_mkdir_virtual_path() {
let overlay = make_overlay().await;
overlay.mkdir(Path::new("/v/blobs/newdir")).await.unwrap();
assert!(overlay.exists(Path::new("/v/blobs/newdir")).await);
}
#[tokio::test]
async fn test_remove_virtual_path() {
let overlay = make_overlay().await;
overlay.remove(Path::new("/v/blobs/test.bin"), false).await.unwrap();
assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
}
#[tokio::test]
async fn test_rename_within_virtual() {
let overlay = make_overlay().await;
overlay
.rename(Path::new("/v/blobs/test.bin"), Path::new("/v/blobs/renamed.bin"))
.await
.unwrap();
assert!(!overlay.exists(Path::new("/v/blobs/test.bin")).await);
assert!(overlay.exists(Path::new("/v/blobs/renamed.bin")).await);
}
#[tokio::test]
async fn test_rename_across_boundary_fails() {
let overlay = make_overlay().await;
let result = overlay
.rename(Path::new("/v/blobs/test.bin"), Path::new("/docs/test.bin"))
.await;
assert!(matches!(result, Err(BackendError::InvalidOperation(_))));
}
#[tokio::test]
async fn test_backend_type() {
let overlay = make_overlay().await;
assert_eq!(overlay.backend_type(), "virtual-overlay");
}
#[tokio::test]
async fn test_resolve_real_path_virtual() {
let overlay = make_overlay().await;
assert!(overlay.resolve_real_path(Path::new("/v/blobs/test.bin")).is_none());
}
async fn overlay_over_inner(cas: bool) -> VirtualOverlayBackend {
let mut inner_router = VfsRouter::new();
let inner_mem = MemoryFs::new();
if cas {
inner_mem
.write(Path::new("v/cas/blob.bin"), b"cas data")
.await
.unwrap();
}
inner_router.mount("/", inner_mem);
let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
let mut vfs = VfsRouter::new();
vfs.mount("/v/jobs", MemoryFs::new());
vfs.mount("/dev", MemoryFs::new());
VirtualOverlayBackend::new(inner, Arc::new(vfs))
}
#[tokio::test]
async fn test_unclaimed_v_reaches_inner_backend() {
let overlay = overlay_over_inner(true).await;
let data = overlay.read(Path::new("/v/cas/blob.bin"), None).await.unwrap();
assert_eq!(data, b"cas data");
assert!(overlay.exists(Path::new("/v/cas/blob.bin")).await);
}
#[tokio::test]
async fn test_v_listing_unions_kaish_and_inner() {
let overlay = overlay_over_inner(true).await;
let names: Vec<String> = overlay
.list(Path::new("/v"))
.await
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(names.iter().any(|n| n == "jobs"), "kaish mount missing: {names:?}");
assert!(names.iter().any(|n| n == "cas"), "embedder mount missing: {names:?}");
}
#[tokio::test]
async fn test_v_synthesized_when_inner_lacks_it() {
let overlay = overlay_over_inner(false).await;
assert!(overlay.stat(Path::new("/v")).await.unwrap().is_dir());
assert!(overlay.lstat(Path::new("/v")).await.unwrap().is_dir());
assert!(overlay.exists(Path::new("/v")).await);
let names: Vec<String> = overlay
.list(Path::new("/v"))
.await
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(names, vec!["jobs".to_string()]);
}
#[cfg(feature = "localfs")]
#[tokio::test]
async fn test_unclaimed_v_resolves_to_inner_real_path() {
use crate::vfs::LocalFs;
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("v/cas")).unwrap();
std::fs::write(dir.path().join("v/cas/blob.bin"), b"x").unwrap();
let mut inner_router = VfsRouter::new();
inner_router.mount("/", LocalFs::read_only(dir.path().to_path_buf()));
let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
let mut vfs = VfsRouter::new();
vfs.mount("/v/jobs", MemoryFs::new());
let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
let real = overlay.resolve_real_path(Path::new("/v/cas/blob.bin"));
assert!(real.is_some(), "unclaimed /v/* must resolve to the embedder real path");
assert!(real.unwrap().ends_with("v/cas/blob.bin"));
assert!(overlay.resolve_real_path(Path::new("/v/jobs/1")).is_none());
}
#[tokio::test]
async fn test_root_lists_both_v_and_dev() {
let overlay = overlay_over_inner(false).await;
let names: Vec<String> = overlay
.list(Path::new("/"))
.await
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(names.iter().any(|n| n == "v"), "{names:?}");
assert!(names.iter().any(|n| n == "dev"), "{names:?}");
}
#[tokio::test]
async fn test_shared_ancestor_is_a_directory_even_over_an_inner_file() {
let inner_mem = MemoryFs::new();
inner_mem.write(Path::new("v"), b"i am a file").await.unwrap();
let mut inner_router = VfsRouter::new();
inner_router.mount("/", inner_mem);
let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
let mut vfs = VfsRouter::new();
vfs.mount("/v/jobs", MemoryFs::new());
let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
assert!(overlay.stat(Path::new("/v")).await.unwrap().is_dir(), "kaish dir wins over inner file");
assert!(overlay.lstat(Path::new("/v")).await.unwrap().is_dir());
assert!(overlay.exists(Path::new("/v")).await);
let names: Vec<String> = overlay
.list(Path::new("/v"))
.await
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(names, vec!["jobs".to_string()], "lists kaish mount; no NotADirectory error");
}
#[cfg(feature = "localfs")]
#[tokio::test]
async fn test_listing_keeps_inner_real_metadata_for_intermediate_child() {
use crate::vfs::LocalFs;
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("v")).unwrap();
let mut inner_router = VfsRouter::new();
inner_router.mount("/", LocalFs::read_only(dir.path().to_path_buf()));
let inner: Arc<dyn KernelBackend> = Arc::new(LocalBackend::new(Arc::new(inner_router)));
let mut vfs = VfsRouter::new();
vfs.mount("/v/jobs", MemoryFs::new());
vfs.mount("/dev", MemoryFs::new());
let overlay = VirtualOverlayBackend::new(inner, Arc::new(vfs));
let entries = overlay.list(Path::new("/")).await.unwrap();
let v = entries.iter().find(|e| e.name == "v").expect("v listed");
let dev = entries.iter().find(|e| e.name == "dev").expect("dev listed");
assert!(v.is_dir());
assert!(v.modified.is_some(), "intermediate child keeps inner real metadata");
assert!(dev.is_dir());
assert!(dev.modified.is_none(), "real kaish mount shadows inner");
}
#[tokio::test]
async fn test_mutations_on_shared_ancestor_are_rejected_clearly() {
let overlay = overlay_over_inner(false).await;
assert!(
matches!(overlay.mkdir(Path::new("/v")).await, Err(BackendError::AlreadyExists(_))),
"mkdir on an existing synthesized dir → AlreadyExists"
);
assert!(
matches!(overlay.remove(Path::new("/v"), true).await, Err(BackendError::InvalidOperation(_))),
"remove of a synthesized dir that holds kaish mounts → InvalidOperation"
);
assert!(
matches!(
overlay.set_mtime(Path::new("/v"), std::time::SystemTime::now()).await,
Err(BackendError::InvalidOperation(_))
),
"set_mtime (touch) on a synthesized dir → InvalidOperation"
);
assert!(
matches!(
overlay.write(Path::new("/v"), b"x", WriteMode::Overwrite).await,
Err(BackendError::IsDirectory(_))
),
"write to a synthesized dir → IsDirectory"
);
assert!(
matches!(overlay.read(Path::new("/v"), None).await, Err(BackendError::IsDirectory(_))),
"read of a synthesized dir → IsDirectory"
);
}
}