use super::{DirEntry, Filesystem};
use kaish_vfs::PathAccess;
use async_trait::async_trait;
use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub use kaish_types::backend::MountInfo;
const SYNTHESIZED_DIRECTORY_MODE: u32 = 0o555;
#[derive(Default)]
pub struct VfsRouter {
mounts: BTreeMap<PathBuf, Arc<dyn Filesystem>>,
}
impl std::fmt::Debug for VfsRouter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VfsRouter")
.field("mounts", &self.mounts.keys().collect::<Vec<_>>())
.finish()
}
}
impl VfsRouter {
pub fn new() -> Self {
Self {
mounts: BTreeMap::new(),
}
}
pub fn mount(&mut self, path: impl Into<PathBuf>, fs: impl Filesystem + 'static) {
let path = Self::normalize_mount_path(path.into());
self.mounts.insert(path, Arc::new(fs));
}
pub fn mount_arc(&mut self, path: impl Into<PathBuf>, fs: Arc<dyn Filesystem>) {
let path = Self::normalize_mount_path(path.into());
self.mounts.insert(path, fs);
}
pub fn unmount(&mut self, path: impl AsRef<Path>) -> bool {
let path = Self::normalize_mount_path(path.as_ref().to_path_buf());
self.mounts.remove(&path).is_some()
}
pub fn list_mounts(&self) -> Vec<MountInfo> {
self.mounts
.iter()
.map(|(path, fs)| MountInfo {
path: path.clone(),
read_only: fs.read_only(),
resident_bytes: fs.resident_bytes(),
})
.collect()
}
fn normalize_mount_path(path: PathBuf) -> PathBuf {
let s = path.to_string_lossy();
let s = s.trim_end_matches('/');
if s.is_empty() {
PathBuf::from("/")
} else if !s.starts_with('/') {
PathBuf::from(format!("/{}", s))
} else {
PathBuf::from(s)
}
}
pub fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
let (fs, relative) = self.find_mount(path).ok()?;
fs.real_path(&relative)
}
pub(crate) fn has_mount(&self, path: &Path) -> bool {
self.find_mount(path).is_ok()
}
pub(crate) fn has_mount_under(&self, dir: &Path) -> bool {
let dir = Self::normalize_mount_path(dir.to_path_buf());
let dir_str = dir.to_string_lossy();
self.mounts.keys().any(|mount_path| {
let mount_str = mount_path.to_string_lossy();
if dir_str == "/" {
mount_str != "/"
} else {
mount_str.starts_with(&format!("{}/", dir_str))
}
})
}
fn list_mount_children(&self, dir: &Path) -> Vec<DirEntry> {
let dir = Self::normalize_mount_path(dir.to_path_buf());
let prefix = format!("{}/", dir.to_string_lossy());
let mut seen = std::collections::HashSet::new();
let mut entries = Vec::new();
for mount_path in self.mounts.keys() {
let mount_str = mount_path.to_string_lossy();
if let Some(rest) = mount_str.strip_prefix(&prefix) {
let first = rest.split('/').next().unwrap_or("");
if !first.is_empty() && seen.insert(first.to_string()) {
entries.push(DirEntry::directory(first));
}
}
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
entries
}
fn path_basename(path: &Path) -> String {
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string())
}
fn find_mount(&self, path: &Path) -> io::Result<(Arc<dyn Filesystem>, PathBuf)> {
let (_, fs, relative) = self.mount_of(path)?;
Ok((fs, relative))
}
fn mount_of(&self, path: &Path) -> io::Result<(&Path, Arc<dyn Filesystem>, PathBuf)> {
let path_str = path.to_string_lossy();
let normalized = if path_str.starts_with('/') {
path.to_path_buf()
} else {
PathBuf::from(format!("/{}", path_str))
};
let mut best_match: Option<(&PathBuf, &Arc<dyn Filesystem>)> = None;
for (mount_path, fs) in &self.mounts {
let mount_str = mount_path.to_string_lossy();
let is_match = if mount_str == "/" {
true } else {
let normalized_str = normalized.to_string_lossy();
normalized_str == mount_str.as_ref()
|| normalized_str.starts_with(&format!("{}/", mount_str))
};
if is_match {
let dominated = best_match
.as_ref()
.is_none_or(|(bp, _)| mount_path.as_os_str().len() > bp.as_os_str().len());
if dominated {
best_match = Some((mount_path, fs));
}
}
}
match best_match {
Some((mount_path, fs)) => {
let mount_str = mount_path.to_string_lossy();
let normalized_str = normalized.to_string_lossy();
let relative = if mount_str == "/" {
normalized_str.trim_start_matches('/').to_string()
} else {
normalized_str
.strip_prefix(mount_str.as_ref())
.unwrap_or("")
.trim_start_matches('/')
.to_string()
};
Ok((mount_path.as_path(), Arc::clone(fs), PathBuf::from(relative)))
}
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no mount point for path: {}", path.display()),
)),
}
}
}
fn lexical_absolute(path: &Path) -> PathBuf {
let mut out = PathBuf::from("/");
for component in path.components() {
match component {
std::path::Component::Normal(name) => out.push(name),
std::path::Component::ParentDir => {
out.pop();
}
_ => {}
}
}
out
}
fn relative_path_from(from: &Path, to: &Path) -> PathBuf {
let mut from_parts = from.components().skip(1).peekable();
let mut to_parts = to.components().skip(1).peekable();
while let (Some(a), Some(b)) = (from_parts.peek(), to_parts.peek()) {
if a != b {
break;
}
from_parts.next();
to_parts.next();
}
let mut relative = PathBuf::new();
for _ in from_parts {
relative.push("..");
}
for part in to_parts {
relative.push(part);
}
if relative.as_os_str().is_empty() {
relative.push(".");
}
relative
}
#[async_trait]
impl Filesystem for VfsRouter {
#[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
let (fs, relative) = self.find_mount(path)?;
fs.read(&relative).await
}
#[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
async fn read_range(
&self,
path: &Path,
range: Option<kaish_vfs::ReadRange>,
) -> io::Result<Vec<u8>> {
let (fs, relative) = self.find_mount(path)?;
fs.read_range(&relative, range).await
}
#[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
let (fs, relative) = self.find_mount(path)?;
fs.write(&relative, data).await
}
#[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
let (fs, relative) = self.find_mount(path)?;
fs.append(&relative, data).await
}
#[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let path_str = path.to_string_lossy();
if path_str.is_empty() || path_str == "/" {
return self.list_root().await;
}
match self.find_mount(path) {
Ok((fs, relative)) => fs.list(&relative).await,
Err(e) => {
if self.has_mount_under(path) {
Ok(self.list_mount_children(path))
} else {
Err(e)
}
}
}
}
#[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
let path_str = path.to_string_lossy();
if path_str.is_empty() || path_str == "/" {
return Ok(DirEntry::directory("/"));
}
let normalized = Self::normalize_mount_path(path.to_path_buf());
if self.mounts.contains_key(&normalized) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string());
return Ok(DirEntry::directory(name));
}
match self.find_mount(path) {
Ok((fs, relative)) => fs.stat(&relative).await,
Err(e) => {
if self.has_mount_under(path) {
Ok(DirEntry::directory(Self::path_basename(path)))
} else {
Err(e)
}
}
}
}
async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
let (fs, relative) = self.find_mount(path)?;
fs.read_link(&relative).await
}
async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
let (link_mount, fs, relative_link) = self.mount_of(link)?;
let target = if target.is_absolute() {
let target = lexical_absolute(target);
let (target_mount, _, _) = self.mount_of(&target)?;
if target_mount != link_mount {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"symlink target {} is on mount {} and the link {} is on mount {}; a link cannot cross mounts",
target.display(),
target_mount.display(),
link.display(),
link_mount.display()
),
));
}
let link_dir = lexical_absolute(link);
let link_dir = link_dir.parent().unwrap_or(Path::new("/"));
relative_path_from(link_dir, &target)
} else {
target.to_path_buf()
};
fs.symlink(&target, &relative_link).await
}
async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
let path_str = path.to_string_lossy();
if path_str.is_empty() || path_str == "/" {
return Ok(DirEntry::directory("/"));
}
let normalized = Self::normalize_mount_path(path.to_path_buf());
if self.mounts.contains_key(&normalized) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "/".to_string());
return Ok(DirEntry::directory(name));
}
match self.find_mount(path) {
Ok((fs, relative)) => fs.lstat(&relative).await,
Err(e) => {
if self.has_mount_under(path) {
Ok(DirEntry::directory(Self::path_basename(path)))
} else {
Err(e)
}
}
}
}
async fn mkdir(&self, path: &Path) -> io::Result<()> {
let (fs, relative) = self.find_mount(path)?;
fs.mkdir(&relative).await
}
async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> io::Result<()> {
let (fs, relative) = self.find_mount(path)?;
fs.set_mtime(&relative, mtime).await
}
async fn remove(&self, path: &Path) -> io::Result<()> {
let (fs, relative) = self.find_mount(path)?;
fs.remove(&relative).await
}
async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
let (from_fs, from_relative) = self.find_mount(from)?;
let (to_fs, to_relative) = self.find_mount(to)?;
if !Arc::ptr_eq(&from_fs, &to_fs) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"cannot rename across different mount points",
));
}
from_fs.rename(&from_relative, &to_relative).await
}
async fn path_access(&self, path: &Path) -> io::Result<PathAccess> {
match self.find_mount(path) {
Ok((fs, relative)) => fs.path_access(&relative).await,
Err(e) => {
let path_str = path.to_string_lossy();
let is_synthesized =
path_str.is_empty() || path_str == "/" || self.has_mount_under(path);
if is_synthesized {
Ok(PathAccess::resolve(Some(SYNTHESIZED_DIRECTORY_MODE), true))
} else {
Err(e)
}
}
}
}
fn read_only(&self) -> bool {
if self.mounts.is_empty() {
return false;
}
self.mounts.values().all(|fs| fs.read_only())
}
}
impl VfsRouter {
async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
let mut entries = Vec::new();
let mut seen_names = std::collections::HashSet::new();
for mount_path in self.mounts.keys() {
let mount_str = mount_path.to_string_lossy();
if mount_str == "/" {
if let Some(fs) = self.mounts.get(mount_path)
&& let Ok(root_entries) = fs.list(Path::new("")).await {
for entry in root_entries {
if seen_names.insert(entry.name.clone()) {
entries.push(entry);
}
}
}
} else {
let first_component = mount_str
.trim_start_matches('/')
.split('/')
.next()
.unwrap_or("");
if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
entries.push(DirEntry::directory(first_component));
}
}
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::MemoryFs;
#[tokio::test]
async fn symlink_absolute_target_on_the_same_mount_is_stored_relative() {
let mut router = VfsRouter::new();
let data = MemoryFs::new();
data.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
data.mkdir(Path::new("home")).await.unwrap();
router.mount("/data", data);
router
.symlink(Path::new("/data/etc/hosts"), Path::new("/data/home/link"))
.await
.unwrap();
assert_eq!(
router.read_link(Path::new("/data/home/link")).await.unwrap(),
PathBuf::from("../etc/hosts")
);
assert_eq!(router.read(Path::new("/data/home/link")).await.unwrap(), b"hosts");
}
#[tokio::test]
async fn symlink_absolute_target_beside_the_link_is_a_bare_name() {
let mut router = VfsRouter::new();
let root = MemoryFs::new();
root.write(Path::new("a/target"), b"t").await.unwrap();
router.mount("/", root);
router
.symlink(Path::new("/a/target"), Path::new("/a/link"))
.await
.unwrap();
assert_eq!(
router.read_link(Path::new("/a/link")).await.unwrap(),
PathBuf::from("target")
);
}
#[tokio::test]
async fn symlink_absolute_target_with_dotdot_is_normalized_first() {
let mut router = VfsRouter::new();
let root = MemoryFs::new();
root.write(Path::new("etc/hosts"), b"hosts").await.unwrap();
root.mkdir(Path::new("home")).await.unwrap();
router.mount("/", root);
router
.symlink(Path::new("/home/../etc/./hosts"), Path::new("/home/link"))
.await
.unwrap();
assert_eq!(
router.read_link(Path::new("/home/link")).await.unwrap(),
PathBuf::from("../etc/hosts")
);
assert_eq!(router.read(Path::new("/home/link")).await.unwrap(), b"hosts");
}
#[tokio::test]
async fn symlink_relative_target_is_stored_verbatim() {
let mut router = VfsRouter::new();
router.mount("/data", MemoryFs::new());
router
.symlink(Path::new("../x/../y"), Path::new("/data/d/link"))
.await
.unwrap();
assert_eq!(
router.read_link(Path::new("/data/d/link")).await.unwrap(),
PathBuf::from("../x/../y")
);
}
#[tokio::test]
async fn symlink_across_mounts_is_refused_and_creates_nothing() {
let mut router = VfsRouter::new();
router.mount("/data", MemoryFs::new());
let scratch = MemoryFs::new();
scratch.write(Path::new("x"), b"x").await.unwrap();
router.mount("/scratch", scratch);
let error = router
.symlink(Path::new("/scratch/x"), Path::new("/data/link"))
.await
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
let message = error.to_string();
assert!(message.contains("/scratch") && message.contains("/data"), "{message}");
assert!(router.lstat(Path::new("/data/link")).await.is_err(), "nothing created");
}
#[test]
fn relative_path_from_walks_up_then_down() {
let rel = |from: &str, to: &str| relative_path_from(Path::new(from), Path::new(to));
assert_eq!(rel("/a/b", "/a/c/d"), PathBuf::from("../c/d"));
assert_eq!(rel("/a", "/a/x"), PathBuf::from("x"));
assert_eq!(rel("/", "/x/y"), PathBuf::from("x/y"));
assert_eq!(rel("/a/b/c", "/"), PathBuf::from("../../.."));
assert_eq!(rel("/a/b", "/a/b"), PathBuf::from("."));
}
#[tokio::test]
async fn test_basic_mount() {
let mut router = VfsRouter::new();
let scratch = MemoryFs::new();
scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
router.mount("/scratch", scratch);
let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
assert_eq!(data, b"hello");
}
#[tokio::test]
async fn test_multiple_mounts() {
let mut router = VfsRouter::new();
let scratch = MemoryFs::new();
scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
router.mount("/scratch", scratch);
let data = MemoryFs::new();
data.write(Path::new("b.txt"), b"data").await.unwrap();
router.mount("/data", data);
assert_eq!(
router.read(Path::new("/scratch/a.txt")).await.unwrap(),
b"scratch"
);
assert_eq!(
router.read(Path::new("/data/b.txt")).await.unwrap(),
b"data"
);
}
#[tokio::test]
async fn test_nested_mount() {
let mut router = VfsRouter::new();
let outer = MemoryFs::new();
outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
router.mount("/mnt", outer);
let inner = MemoryFs::new();
inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
router.mount("/mnt/project", inner);
assert_eq!(
router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
b"outer"
);
assert_eq!(
router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
b"inner"
);
}
#[tokio::test]
async fn test_list_root() {
let mut router = VfsRouter::new();
router.mount("/scratch", MemoryFs::new());
router.mount("/mnt/a", MemoryFs::new());
router.mount("/mnt/b", MemoryFs::new());
let entries = router.list(Path::new("/")).await.unwrap();
let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&"scratch".to_string()));
assert!(names.contains(&&"mnt".to_string()));
}
#[tokio::test]
async fn test_unmount() {
let mut router = VfsRouter::new();
let fs = MemoryFs::new();
fs.write(Path::new("test.txt"), b"data").await.unwrap();
router.mount("/scratch", fs);
assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
router.unmount("/scratch");
assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
}
#[tokio::test]
async fn test_list_mounts() {
let mut router = VfsRouter::new();
router.mount("/scratch", MemoryFs::new());
router.mount("/data", MemoryFs::new());
let mounts = router.list_mounts();
assert_eq!(mounts.len(), 2);
let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
assert!(paths.contains(&&PathBuf::from("/scratch")));
assert!(paths.contains(&&PathBuf::from("/data")));
}
#[tokio::test]
async fn test_no_mount_error() {
let router = VfsRouter::new();
let result = router.read(Path::new("/nothing/here.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[tokio::test]
async fn test_root_mount() {
let mut router = VfsRouter::new();
let root = MemoryFs::new();
root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
router.mount("/", root);
let data = router.read(Path::new("/at-root.txt")).await.unwrap();
assert_eq!(data, b"root file");
}
#[tokio::test]
async fn test_write_through_router() {
let mut router = VfsRouter::new();
router.mount("/scratch", MemoryFs::new());
router
.write(Path::new("/scratch/new.txt"), b"created")
.await
.unwrap();
let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
assert_eq!(data, b"created");
}
#[tokio::test]
async fn test_stat_mount_point() {
let mut router = VfsRouter::new();
router.mount("/scratch", MemoryFs::new());
let entry = router.stat(Path::new("/scratch")).await.unwrap();
assert!(entry.is_dir());
}
#[tokio::test]
async fn test_stat_root() {
let router = VfsRouter::new();
let entry = router.stat(Path::new("/")).await.unwrap();
assert!(entry.is_dir());
}
#[tokio::test]
async fn test_rename_same_mount() {
let mut router = VfsRouter::new();
let mem = MemoryFs::new();
mem.write(Path::new("old.txt"), b"data").await.unwrap();
router.mount("/scratch", mem);
router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
assert_eq!(data, b"data");
assert!(!router.exists(Path::new("/scratch/old.txt")).await);
}
#[tokio::test]
async fn test_rename_cross_mount_fails() {
let mut router = VfsRouter::new();
let mem1 = MemoryFs::new();
mem1.write(Path::new("file.txt"), b"data").await.unwrap();
router.mount("/mount1", mem1);
router.mount("/mount2", MemoryFs::new());
let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
}
#[tokio::test]
async fn path_access_agrees_with_stat_on_synthesized_directories() {
let mut router = VfsRouter::new();
router.mount("/v/docs", MemoryFs::new());
for path in ["/", "/v"] {
let path = Path::new(path);
assert!(
router.stat(path).await.is_ok(),
"{} is synthesized by stat",
path.display()
);
let access = router
.path_access(path)
.await
.unwrap_or_else(|e| panic!("path_access must not error where stat succeeds: {e}"));
assert!(access.readable, "{} must be readable", path.display());
assert!(access.executable, "{} must be searchable", path.display());
assert!(
!access.writable,
"the router creates nothing in {}",
path.display()
);
}
}
#[tokio::test]
async fn path_access_errors_on_a_path_with_no_mount() {
let mut router = VfsRouter::new();
router.mount("/v/docs", MemoryFs::new());
assert!(router.path_access(Path::new("/nope")).await.is_err());
assert!(router.path_access(Path::new("/v/docs/absent")).await.is_err());
}
#[tokio::test]
async fn path_access_at_a_mount_point_asks_the_mount() {
let mut router = VfsRouter::new();
router.mount("/rw", MemoryFs::new());
router.mount("/ro", BuiltinFsStub);
assert!(router.path_access(Path::new("/rw")).await.unwrap().writable);
assert!(!router.path_access(Path::new("/ro")).await.unwrap().writable);
assert!(router.path_access(Path::new("/ro")).await.unwrap().readable);
}
struct BuiltinFsStub;
#[async_trait]
impl Filesystem for BuiltinFsStub {
async fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
Ok(Vec::new())
}
async fn write(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
}
async fn list(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
Ok(Vec::new())
}
async fn stat(&self, _path: &Path) -> io::Result<DirEntry> {
Ok(DirEntry::directory("."))
}
async fn mkdir(&self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
}
async fn remove(&self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
}
fn read_only(&self) -> bool {
true
}
}
#[tokio::test]
async fn read_only_empty_router_returns_false() {
let router = VfsRouter::new();
assert!(!router.read_only());
}
#[cfg(feature = "localfs")]
#[tokio::test]
async fn read_only_all_read_only_mounts_returns_true() {
use crate::vfs::LocalFs;
let t1 = tempfile::tempdir().unwrap();
let t2 = tempfile::tempdir().unwrap();
let mut router = VfsRouter::new();
router.mount("/a", LocalFs::read_only(t1.path().to_path_buf()));
router.mount("/b", LocalFs::read_only(t2.path().to_path_buf()));
assert!(router.read_only());
}
#[cfg(feature = "localfs")]
#[tokio::test]
async fn read_only_mixed_mounts_returns_false() {
use crate::vfs::LocalFs;
let t1 = tempfile::tempdir().unwrap();
let mut router = VfsRouter::new();
router.mount("/ro", LocalFs::read_only(t1.path().to_path_buf()));
router.mount("/rw", MemoryFs::new());
assert!(!router.read_only());
}
#[tokio::test]
async fn test_list_synthesizes_intermediate_dir() {
let mut router = VfsRouter::new();
router.mount("/v/jobs", MemoryFs::new());
router.mount("/v/blobs", MemoryFs::new());
let entries = router.list(Path::new("/v")).await.unwrap();
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["blobs", "jobs"]); }
#[tokio::test]
async fn test_stat_intermediate_dir_is_directory() {
let mut router = VfsRouter::new();
router.mount("/v/jobs", MemoryFs::new());
assert!(router.stat(Path::new("/v")).await.unwrap().is_dir());
assert!(router.lstat(Path::new("/v")).await.unwrap().is_dir());
}
#[tokio::test]
async fn test_deep_intermediate_dir() {
let mut router = VfsRouter::new();
router.mount("/v/etc/rc", MemoryFs::new());
let v: Vec<_> = router.list(Path::new("/v")).await.unwrap();
assert_eq!(v.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["etc"]);
let etc: Vec<_> = router.list(Path::new("/v/etc")).await.unwrap();
assert_eq!(etc.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), vec!["rc"]);
assert!(router.stat(Path::new("/v/etc")).await.unwrap().is_dir());
}
#[tokio::test]
async fn test_has_mount_under() {
let mut router = VfsRouter::new();
router.mount("/v/jobs", MemoryFs::new());
assert!(router.has_mount_under(Path::new("/v")));
assert!(router.has_mount_under(Path::new("/")));
assert!(!router.has_mount_under(Path::new("/v/jobs")));
assert!(!router.has_mount_under(Path::new("/other")));
}
#[tokio::test]
async fn test_nonexistent_ancestor_still_notfound() {
let mut router = VfsRouter::new();
router.mount("/v/jobs", MemoryFs::new());
assert_eq!(
router.list(Path::new("/nope")).await.unwrap_err().kind(),
io::ErrorKind::NotFound
);
assert_eq!(
router.stat(Path::new("/nope")).await.unwrap_err().kind(),
io::ErrorKind::NotFound
);
}
}