use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::platform::SystemTime;
use parking_lot::RwLock;
use super::{DirEntry, Metadata, NodeType, VirtualFs};
use crate::error::VfsError;
use crate::interpreter::pattern::glob_match;
struct MountPair {
src_fs: Arc<dyn VirtualFs>,
src_rel: PathBuf,
dst_fs: Arc<dyn VirtualFs>,
dst_rel: PathBuf,
same: bool,
}
pub struct MountableFs {
mounts: Arc<RwLock<BTreeMap<PathBuf, Arc<dyn VirtualFs>>>>,
}
impl MountableFs {
pub fn new() -> Self {
Self {
mounts: Arc::new(RwLock::new(BTreeMap::new())),
}
}
pub fn mount(self, path: impl Into<PathBuf>, fs: Arc<dyn VirtualFs>) -> Self {
let path = path.into();
assert!(
super::vfs_path_is_absolute(&path),
"mount path must be absolute: {path:?}"
);
self.mounts.write().insert(path, fs);
self
}
fn resolve_mount(&self, path: &Path) -> Result<(Arc<dyn VirtualFs>, PathBuf), VfsError> {
let mounts = self.mounts.read();
for (mount_point, fs) in mounts.iter().rev() {
if path.starts_with(mount_point) {
let relative = path.strip_prefix(mount_point).unwrap_or(Path::new(""));
let resolved = if relative.as_os_str().is_empty() {
PathBuf::from("/")
} else {
PathBuf::from("/").join(relative)
};
return Ok((Arc::clone(fs), resolved));
}
}
Err(VfsError::NotFound(path.to_path_buf()))
}
fn resolve_two(&self, src: &Path, dst: &Path) -> Result<MountPair, VfsError> {
let mounts = self.mounts.read();
let resolve_one =
|path: &Path| -> Result<(Arc<dyn VirtualFs>, PathBuf, PathBuf), VfsError> {
for (mount_point, fs) in mounts.iter().rev() {
if path.starts_with(mount_point) {
let relative = path.strip_prefix(mount_point).unwrap_or(Path::new(""));
let resolved = if relative.as_os_str().is_empty() {
PathBuf::from("/")
} else {
PathBuf::from("/").join(relative)
};
return Ok((Arc::clone(fs), resolved, mount_point.clone()));
}
}
Err(VfsError::NotFound(path.to_path_buf()))
};
let (src_fs, src_rel, src_mount) = resolve_one(src)?;
let (dst_fs, dst_rel, dst_mount) = resolve_one(dst)?;
let same = src_mount == dst_mount;
Ok(MountPair {
src_fs,
src_rel,
dst_fs,
dst_rel,
same,
})
}
fn synthetic_mount_entries(&self, dir_path: &Path) -> Vec<DirEntry> {
let mounts = self.mounts.read();
let mut entries = Vec::new();
let dir_str = dir_path.to_string_lossy();
let prefix = if dir_str == "/" {
"/".to_string()
} else {
format!("{}/", dir_str.trim_end_matches('/'))
};
for mount_point in mounts.keys() {
if mount_point == dir_path {
continue;
}
let mp_str = mount_point.to_string_lossy();
if let Some(rest) = mp_str.strip_prefix(&prefix)
&& !rest.is_empty()
{
let first_component = rest.split('/').next().unwrap();
if !entries.iter().any(|e: &DirEntry| e.name == first_component) {
entries.push(DirEntry {
name: first_component.to_string(),
node_type: NodeType::Directory,
});
}
}
}
entries
}
fn glob_walk(
&self,
dir: &Path,
components: &[&str],
current_path: PathBuf,
results: &mut Vec<PathBuf>,
max: usize,
) {
if results.len() >= max || components.is_empty() {
if components.is_empty() {
results.push(current_path);
}
return;
}
let pattern = components[0];
let rest = &components[1..];
let entries = self.merged_readdir_for_glob(dir);
if pattern == "**" {
self.glob_walk(dir, rest, current_path.clone(), results, max);
for entry in &entries {
if results.len() >= max {
return;
}
if entry.name.starts_with('.') {
continue;
}
let child_path = current_path.join(&entry.name);
let child_dir = dir.join(&entry.name);
if entry.node_type == NodeType::Directory || entry.node_type == NodeType::Symlink {
self.glob_walk(&child_dir, components, child_path, results, max);
}
}
} else {
for entry in &entries {
if results.len() >= max {
return;
}
if entry.name.starts_with('.') && !pattern.starts_with('.') {
continue;
}
if glob_match(pattern, &entry.name) {
let child_path = current_path.join(&entry.name);
let child_dir = dir.join(&entry.name);
if rest.is_empty() {
results.push(child_path);
} else if entry.node_type == NodeType::Directory
|| entry.node_type == NodeType::Symlink
{
self.glob_walk(&child_dir, rest, child_path, results, max);
}
}
}
}
}
fn merged_readdir_for_glob(&self, dir: &Path) -> Vec<DirEntry> {
let mut entries = match self.resolve_mount(dir) {
Ok((fs, rel)) => fs.readdir(&rel).unwrap_or_default(),
Err(_) => Vec::new(),
};
let synthetics = self.synthetic_mount_entries(dir);
let existing_names: std::collections::HashSet<String> =
entries.iter().map(|e| e.name.clone()).collect();
for s in synthetics {
if !existing_names.contains(&s.name) {
entries.push(s);
}
}
entries
}
}
impl Default for MountableFs {
fn default() -> Self {
Self::new()
}
}
impl VirtualFs for MountableFs {
fn read_file(&self, path: &Path) -> Result<Vec<u8>, VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.read_file(&rel)
}
fn write_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.write_file(&rel, content)
}
fn append_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.append_file(&rel, content)
}
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.remove_file(&rel)
}
fn mkdir(&self, path: &Path) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.mkdir(&rel)
}
fn mkdir_p(&self, path: &Path) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.mkdir_p(&rel)
}
fn readdir(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
let (mut entries, mount_ok) = match self.resolve_mount(path) {
Ok((fs, rel)) => match fs.readdir(&rel) {
Ok(e) => (e, true),
Err(_) => (Vec::new(), false),
},
Err(_) => (Vec::new(), false),
};
let synthetics = self.synthetic_mount_entries(path);
let existing_names: std::collections::HashSet<String> =
entries.iter().map(|e| e.name.clone()).collect();
for s in synthetics {
if !existing_names.contains(&s.name) {
entries.push(s);
}
}
if !mount_ok && entries.is_empty() {
return Err(VfsError::NotFound(path.to_path_buf()));
}
Ok(entries)
}
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.remove_dir(&rel)
}
fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.remove_dir_all(&rel)
}
fn exists(&self, path: &Path) -> bool {
if let Ok((fs, rel)) = self.resolve_mount(path)
&& fs.exists(&rel)
{
return true;
}
let mounts = self.mounts.read();
if mounts.contains_key(path) {
return true;
}
let prefix = if path == Path::new("/") {
"/".to_string()
} else {
format!("{}/", path.to_string_lossy().trim_end_matches('/'))
};
mounts
.keys()
.any(|mp| mp.to_string_lossy().starts_with(&prefix))
}
fn stat(&self, path: &Path) -> Result<Metadata, VfsError> {
if let Ok((fs, rel)) = self.resolve_mount(path)
&& let Ok(m) = fs.stat(&rel)
{
return Ok(m);
}
if self.is_mount_point_or_ancestor(path) {
return Ok(Metadata {
node_type: NodeType::Directory,
size: 0,
mode: 0o755,
mtime: SystemTime::UNIX_EPOCH,
});
}
Err(VfsError::NotFound(path.to_path_buf()))
}
fn lstat(&self, path: &Path) -> Result<Metadata, VfsError> {
if let Ok((fs, rel)) = self.resolve_mount(path)
&& let Ok(m) = fs.lstat(&rel)
{
return Ok(m);
}
if self.is_mount_point_or_ancestor(path) {
return Ok(Metadata {
node_type: NodeType::Directory,
size: 0,
mode: 0o755,
mtime: SystemTime::UNIX_EPOCH,
});
}
Err(VfsError::NotFound(path.to_path_buf()))
}
fn chmod(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.chmod(&rel, mode)
}
fn utimes(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
fs.utimes(&rel, mtime)
}
fn symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
let (link_fs, link_rel) = self.resolve_mount(link)?;
let remapped_target = if target.is_absolute() {
if let Ok((_, target_rel)) = self.resolve_mount(target) {
let link_mount = self.mount_point_for(link);
let target_mount = self.mount_point_for(target);
if link_mount == target_mount {
target_rel
} else {
target.to_path_buf()
}
} else {
target.to_path_buf()
}
} else {
target.to_path_buf()
};
link_fs.symlink(&remapped_target, &link_rel)
}
fn hardlink(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
let pair = self.resolve_two(src, dst)?;
if !pair.same {
return Err(VfsError::IoError(
"hard links across mount boundaries are not supported".to_string(),
));
}
pair.src_fs.hardlink(&pair.src_rel, &pair.dst_rel)
}
fn readlink(&self, path: &Path) -> Result<PathBuf, VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
let target = fs.readlink(&rel)?;
if target.is_absolute() {
let mount_point = self.mount_point_for(path);
if mount_point != Path::new("/") {
let inner_rel = target.strip_prefix("/").unwrap_or(&target);
if inner_rel.as_os_str().is_empty() {
return Ok(mount_point);
}
return Ok(mount_point.join(inner_rel));
}
}
Ok(target)
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf, VfsError> {
let (fs, rel) = self.resolve_mount(path)?;
let canonical_in_mount = fs.canonicalize(&rel)?;
let mounts = self.mounts.read();
for (mount_point, _) in mounts.iter().rev() {
if path.starts_with(mount_point) {
if mount_point == Path::new("/") {
return Ok(canonical_in_mount);
}
let inner_rel = canonical_in_mount
.strip_prefix("/")
.unwrap_or(&canonical_in_mount);
if inner_rel.as_os_str().is_empty() {
return Ok(mount_point.clone());
}
return Ok(mount_point.join(inner_rel));
}
}
Ok(canonical_in_mount)
}
fn copy(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
let pair = self.resolve_two(src, dst)?;
if pair.same {
pair.src_fs.copy(&pair.src_rel, &pair.dst_rel)
} else {
let content = pair.src_fs.read_file(&pair.src_rel)?;
pair.dst_fs.write_file(&pair.dst_rel, &content)
}
}
fn rename(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
let pair = self.resolve_two(src, dst)?;
if pair.same {
pair.src_fs.rename(&pair.src_rel, &pair.dst_rel)
} else {
if let Ok(m) = pair.src_fs.stat(&pair.src_rel)
&& m.node_type == NodeType::Directory
{
return Err(VfsError::IoError(
"rename of directories across mount boundaries is not supported".to_string(),
));
}
let content = pair.src_fs.read_file(&pair.src_rel)?;
pair.dst_fs.write_file(&pair.dst_rel, &content)?;
pair.src_fs.remove_file(&pair.src_rel)
}
}
fn glob(&self, pattern: &str, cwd: &Path) -> Result<Vec<PathBuf>, VfsError> {
let is_absolute = pattern.starts_with('/');
let abs_pattern = if is_absolute {
pattern.to_string()
} else {
let cwd_str = cwd.to_str().unwrap_or("/").trim_end_matches('/');
format!("{cwd_str}/{pattern}")
};
let components: Vec<&str> = abs_pattern.split('/').filter(|s| !s.is_empty()).collect();
let mut results = Vec::new();
let max = 100_000;
self.glob_walk(
Path::new("/"),
&components,
PathBuf::from("/"),
&mut results,
max,
);
results.sort();
results.dedup();
if !is_absolute {
results = results
.into_iter()
.filter_map(|p| p.strip_prefix(cwd).ok().map(|r| r.to_path_buf()))
.collect();
}
Ok(results)
}
fn deep_clone(&self) -> Arc<dyn VirtualFs> {
let mounts = self.mounts.read();
let cloned_mounts: BTreeMap<PathBuf, Arc<dyn VirtualFs>> = mounts
.iter()
.map(|(path, fs)| (path.clone(), fs.deep_clone()))
.collect();
Arc::new(MountableFs {
mounts: Arc::new(RwLock::new(cloned_mounts)),
})
}
}
impl MountableFs {
fn is_mount_point_or_ancestor(&self, path: &Path) -> bool {
let mounts = self.mounts.read();
if mounts.contains_key(path) {
return true;
}
let prefix = if path == Path::new("/") {
"/".to_string()
} else {
format!("{}/", path.to_string_lossy().trim_end_matches('/'))
};
mounts
.keys()
.any(|mp| mp.to_string_lossy().starts_with(&prefix))
}
fn mount_point_for(&self, path: &Path) -> PathBuf {
let mounts = self.mounts.read();
for mount_point in mounts.keys().rev() {
if path.starts_with(mount_point) {
return mount_point.clone();
}
}
PathBuf::from("/")
}
}