#![allow(clippy::unwrap_used)]
use async_trait::async_trait;
use std::collections::BTreeMap;
use std::io::Error as IoError;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use super::limits::{FsLimits, FsUsage};
use super::traits::{DirEntry, FileSystem, FileSystemExt, FileType, Metadata};
use crate::error::Result;
use std::io::ErrorKind;
fn is_posix_absolute(path: &Path) -> bool {
path.has_root()
}
pub struct MountableFs {
root: Arc<dyn FileSystem>,
mounts: RwLock<BTreeMap<PathBuf, Arc<dyn FileSystem>>>,
}
impl MountableFs {
pub fn new(root: Arc<dyn FileSystem>) -> Self {
Self {
root,
mounts: RwLock::new(BTreeMap::new()),
}
}
pub fn mount(&self, path: impl AsRef<Path>, fs: Arc<dyn FileSystem>) -> Result<()> {
if !is_posix_absolute(path.as_ref()) {
return Err(IoError::other("mount path must be absolute").into());
}
let path = Self::normalize_path(path.as_ref());
let mut mounts = self.mounts.write().unwrap();
mounts.insert(path, fs);
Ok(())
}
pub fn unmount(&self, path: impl AsRef<Path>) -> Result<()> {
let path = Self::normalize_path(path.as_ref());
let mut mounts = self.mounts.write().unwrap();
mounts
.remove(&path)
.ok_or_else(|| IoError::other("mount not found"))?;
Ok(())
}
fn normalize_path(path: &Path) -> PathBuf {
super::normalize_path(path)
}
fn validate_path(&self, path: &Path) -> Result<()> {
self.root
.limits()
.validate_path(path)
.map_err(|e| IoError::new(ErrorKind::InvalidInput, e.to_string()))?;
Ok(())
}
fn resolve(&self, path: &Path) -> (Arc<dyn FileSystem>, PathBuf) {
let path = Self::normalize_path(path);
let mounts = self.mounts.read().unwrap();
let mut best_mount: Option<(&PathBuf, &Arc<dyn FileSystem>)> = None;
for (mount_path, fs) in mounts.iter() {
if path.starts_with(mount_path) {
match best_mount {
None => best_mount = Some((mount_path, fs)),
Some((best_path, _)) => {
if mount_path.components().count() > best_path.components().count() {
best_mount = Some((mount_path, fs));
}
}
}
}
}
match best_mount {
Some((mount_path, fs)) => {
let relative = path
.strip_prefix(mount_path)
.unwrap_or(Path::new(""))
.to_path_buf();
let resolved = if relative.as_os_str().is_empty() {
PathBuf::from("/")
} else {
PathBuf::from("/").join(relative)
};
(Arc::clone(fs), resolved)
}
None => {
(Arc::clone(&self.root), path)
}
}
}
}
#[async_trait]
impl FileSystem for MountableFs {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
let (fs, resolved) = self.resolve(path);
fs.read_file(&resolved).await
}
async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.write_file(&resolved, content).await
}
async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.append_file(&resolved, content).await
}
async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.mkdir(&resolved, recursive).await
}
async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.remove(&resolved, recursive).await
}
async fn stat(&self, path: &Path) -> Result<Metadata> {
let (fs, resolved) = self.resolve(path);
fs.stat(&resolved).await
}
async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
let path = Self::normalize_path(path);
let (fs, resolved) = self.resolve(&path);
let mut entries = fs.read_dir(&resolved).await?;
let mounts = self.mounts.read().unwrap();
for mount_path in mounts.keys() {
if mount_path.parent() == Some(&path)
&& let Some(name) = mount_path.file_name()
{
let name_str = name.to_string_lossy().to_string();
if !entries.iter().any(|e| e.name == name_str) {
entries.push(DirEntry {
name: name_str,
metadata: Metadata {
file_type: FileType::Directory,
size: 0,
mode: 0o755,
modified: std::time::SystemTime::now(),
created: std::time::SystemTime::now(),
},
});
}
}
}
Ok(entries)
}
async fn exists(&self, path: &Path) -> Result<bool> {
let path = Self::normalize_path(path);
{
let mounts = self.mounts.read().unwrap();
if mounts.contains_key(&path) {
return Ok(true);
}
}
let (fs, resolved) = self.resolve(&path);
fs.exists(&resolved).await
}
async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
self.validate_path(from)?;
self.validate_path(to)?;
let (from_fs, from_resolved) = self.resolve(from);
let (to_fs, to_resolved) = self.resolve(to);
if Arc::ptr_eq(&from_fs, &to_fs) {
from_fs.rename(&from_resolved, &to_resolved).await
} else {
let meta = from_fs.stat(&from_resolved).await?;
if meta.file_type == FileType::Symlink {
let target = from_fs.read_link(&from_resolved).await?;
to_fs.symlink(&target, &to_resolved).await?;
from_fs.remove(&from_resolved, false).await
} else {
let content = from_fs.read_file(&from_resolved).await?;
to_fs.write_file(&to_resolved, &content).await?;
from_fs.remove(&from_resolved, false).await
}
}
}
async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
self.validate_path(from)?;
self.validate_path(to)?;
let (from_fs, from_resolved) = self.resolve(from);
let (to_fs, to_resolved) = self.resolve(to);
if Arc::ptr_eq(&from_fs, &to_fs) {
from_fs.copy(&from_resolved, &to_resolved).await
} else {
let meta = from_fs.stat(&from_resolved).await?;
if meta.file_type == FileType::Symlink {
let target = from_fs.read_link(&from_resolved).await?;
to_fs.symlink(&target, &to_resolved).await
} else {
let content = from_fs.read_file(&from_resolved).await?;
to_fs.write_file(&to_resolved, &content).await
}
}
}
async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
self.validate_path(link)?;
let (fs, resolved) = self.resolve(link);
fs.symlink(target, &resolved).await
}
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
let (fs, resolved) = self.resolve(path);
fs.read_link(&resolved).await
}
async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.chmod(&resolved, mode).await
}
async fn set_modified_time(&self, path: &Path, time: SystemTime) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.set_modified_time(&resolved, time).await
}
}
#[async_trait]
impl FileSystemExt for MountableFs {
fn usage(&self) -> FsUsage {
let mut total = self.root.usage();
let mounts = self.mounts.read().unwrap();
for fs in mounts.values() {
let mount_usage = fs.usage();
total.total_bytes += mount_usage.total_bytes;
total.file_count += mount_usage.file_count;
total.dir_count += mount_usage.dir_count;
}
total
}
fn limits(&self) -> FsLimits {
self.root.limits()
}
async fn mkfifo(&self, path: &Path, mode: u32) -> Result<()> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.mkfifo(&resolved, mode).await
}
fn vfs_snapshot(&self) -> Option<super::VfsSnapshot> {
self.root.vfs_snapshot()
}
fn vfs_restore(&self, snapshot: &super::VfsSnapshot) -> bool {
self.root.vfs_restore(snapshot)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::InMemoryFs;
#[tokio::test]
async fn test_mount_and_access() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
mounted
.write_file(Path::new("/data.txt"), b"mounted data")
.await
.unwrap();
let mfs = MountableFs::new(root.clone());
mfs.mount("/mnt/data", mounted.clone()).unwrap();
let content = mfs
.read_file(Path::new("/mnt/data/data.txt"))
.await
.unwrap();
assert_eq!(content, b"mounted data");
}
#[tokio::test]
async fn test_write_to_mount() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
let mfs = MountableFs::new(root);
mfs.mount("/mnt", mounted.clone()).unwrap();
mfs.mkdir(Path::new("/mnt/subdir"), false).await.unwrap();
mfs.write_file(Path::new("/mnt/subdir/test.txt"), b"hello")
.await
.unwrap();
let content = mounted
.read_file(Path::new("/subdir/test.txt"))
.await
.unwrap();
assert_eq!(content, b"hello");
}
#[tokio::test]
async fn test_nested_mounts() {
let root = Arc::new(InMemoryFs::new());
let outer = Arc::new(InMemoryFs::new());
let inner = Arc::new(InMemoryFs::new());
outer
.write_file(Path::new("/outer.txt"), b"outer")
.await
.unwrap();
inner
.write_file(Path::new("/inner.txt"), b"inner")
.await
.unwrap();
let mfs = MountableFs::new(root);
mfs.mount("/mnt", outer).unwrap();
mfs.mount("/mnt/nested", inner).unwrap();
let content = mfs.read_file(Path::new("/mnt/outer.txt")).await.unwrap();
assert_eq!(content, b"outer");
let content = mfs
.read_file(Path::new("/mnt/nested/inner.txt"))
.await
.unwrap();
assert_eq!(content, b"inner");
}
#[tokio::test]
async fn test_root_fallback() {
let root = Arc::new(InMemoryFs::new());
root.write_file(Path::new("/root.txt"), b"root data")
.await
.unwrap();
let mfs = MountableFs::new(root);
let content = mfs.read_file(Path::new("/root.txt")).await.unwrap();
assert_eq!(content, b"root data");
}
#[tokio::test]
async fn test_mount_point_in_readdir() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
let mfs = MountableFs::new(root);
mfs.mount("/mnt", mounted).unwrap();
let entries = mfs.read_dir(Path::new("/")).await.unwrap();
let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
assert!(names.contains(&&"mnt".to_string()));
}
#[tokio::test]
async fn test_unmount() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
mounted
.write_file(Path::new("/data.txt"), b"data")
.await
.unwrap();
let mfs = MountableFs::new(root);
mfs.mount("/mnt", mounted).unwrap();
assert!(mfs.exists(Path::new("/mnt/data.txt")).await.unwrap());
mfs.unmount("/mnt").unwrap();
assert!(!mfs.exists(Path::new("/mnt/data.txt")).await.unwrap());
}
#[test]
fn test_is_posix_absolute_accepts_root_paths() {
assert!(is_posix_absolute(Path::new("/")));
assert!(is_posix_absolute(Path::new("/workspace")));
assert!(is_posix_absolute(Path::new("/data/sub")));
}
#[test]
fn test_is_posix_absolute_rejects_relative_paths() {
assert!(!is_posix_absolute(Path::new("relative")));
assert!(!is_posix_absolute(Path::new("relative/path")));
assert!(!is_posix_absolute(Path::new("./foo")));
assert!(!is_posix_absolute(Path::new("")));
}
#[test]
fn test_mount_accepts_posix_absolute_path_on_any_host() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
let mfs = MountableFs::new(root);
mfs.mount("/workspace", mounted.clone()).unwrap();
mfs.mount("/data/sub", mounted).unwrap();
}
#[tokio::test]
async fn test_mount_rejects_relative_path() {
let root = Arc::new(InMemoryFs::new());
let mounted = Arc::new(InMemoryFs::new());
let mfs = MountableFs::new(root);
let err = mfs.mount("relative/path", mounted).unwrap_err();
assert!(
err.to_string().contains("absolute"),
"expected 'absolute' in error, got: {err}"
);
}
}