use std::collections::HashMap;
use std::sync::Arc;
#[cfg(windows)]
use cap_fs_ext::DirExt as _;
use wasmtime::component::ResourceTable;
use wasmtime_wasi::{DirPerms, FilePerms};
use crate::storage::VfsStorage;
use crate::wasi_impl::VfsDescriptor;
#[derive(Clone)]
pub struct RestrictedDir {
inner: Arc<cap_std::fs::Dir>,
file_map: Option<HashMap<String, String>>,
}
impl std::fmt::Debug for RestrictedDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RestrictedDir")
.field("restricted", &self.file_map.is_some())
.finish_non_exhaustive()
}
}
impl RestrictedDir {
pub fn new(dir: cap_std::fs::Dir) -> Self {
Self {
inner: Arc::new(dir),
file_map: None,
}
}
pub fn with_file_map(dir: cap_std::fs::Dir, file_map: HashMap<String, String>) -> Self {
Self {
inner: Arc::new(dir),
file_map: Some(file_map),
}
}
pub fn open_ambient(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let dir = cap_std::fs::Dir::open_ambient_dir(path, cap_std::ambient_authority())?;
Ok(Self::new(dir))
}
fn check_allowed(&self, guest_rel_path: &str) -> Result<(), std::io::Error> {
match &self.file_map {
None => Ok(()),
Some(map) => {
let normalized = guest_rel_path.strip_prefix("./").unwrap_or(guest_rel_path);
if map.contains_key(normalized) {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("access denied: {guest_rel_path}"),
))
}
}
}
}
fn translate<'a>(&'a self, guest_rel_path: &'a str) -> &'a str {
match &self.file_map {
None => guest_rel_path,
Some(map) => {
let normalized = guest_rel_path.strip_prefix("./").unwrap_or(guest_rel_path);
map.get(normalized)
.map(String::as_str)
.unwrap_or(guest_rel_path)
}
}
}
pub fn open_with(
&self,
guest_path: &str,
opts: &cap_std::fs::OpenOptions,
) -> std::io::Result<cap_std::fs::File> {
self.check_allowed(guest_path)?;
self.inner.open_with(self.translate(guest_path), opts)
}
pub fn open_dir(&self, guest_path: &str) -> std::io::Result<RestrictedDir> {
self.check_allowed(guest_path)?;
let sub = self.inner.open_dir(self.translate(guest_path))?;
Ok(RestrictedDir {
inner: Arc::new(sub),
file_map: None,
})
}
pub fn create_dir(&self, guest_path: &str) -> std::io::Result<()> {
self.check_allowed(guest_path)?;
self.inner.create_dir(self.translate(guest_path))
}
pub fn metadata(&self, guest_path: &str) -> std::io::Result<cap_std::fs::Metadata> {
self.check_allowed(guest_path)?;
self.inner.metadata(self.translate(guest_path))
}
pub fn dir_metadata(&self) -> std::io::Result<cap_std::fs::Metadata> {
self.inner.dir_metadata()
}
pub fn read_link(&self, guest_path: &str) -> std::io::Result<std::path::PathBuf> {
self.check_allowed(guest_path)?;
self.inner.read_link(self.translate(guest_path))
}
pub fn remove_dir(&self, guest_path: &str) -> std::io::Result<()> {
self.check_allowed(guest_path)?;
self.inner.remove_dir(self.translate(guest_path))
}
pub fn remove_file(&self, guest_path: &str) -> std::io::Result<()> {
self.check_allowed(guest_path)?;
self.inner.remove_file(self.translate(guest_path))
}
pub fn rename(
&self,
old_guest: &str,
dest: &RestrictedDir,
new_guest: &str,
) -> std::io::Result<()> {
self.check_allowed(old_guest)?;
dest.check_allowed(new_guest)?;
self.inner.rename(
self.translate(old_guest),
&dest.inner,
dest.translate(new_guest),
)
}
pub fn symlink(&self, src_path: &str, dest_guest: &str) -> std::io::Result<()> {
self.check_allowed(dest_guest)?;
self.inner.symlink(src_path, self.translate(dest_guest))
}
pub fn entries(&self) -> std::io::Result<Vec<cap_std::fs::DirEntry>> {
match &self.file_map {
None => self.inner.entries()?.collect::<Result<Vec<_>, _>>(),
Some(map) => {
let reverse: HashMap<&str, &str> = map
.iter()
.map(|(guest, host)| (host.as_str(), guest.as_str()))
.collect();
let mut result = Vec::new();
for entry in self.inner.entries()? {
let entry = entry?;
let host_name = entry.file_name().to_string_lossy().into_owned();
if reverse.contains_key(host_name.as_str()) {
result.push(entry);
}
}
Ok(result)
}
}
}
pub fn guest_name(&self, entry: &cap_std::fs::DirEntry) -> String {
let host_name = entry.file_name().to_string_lossy().into_owned();
match &self.file_map {
None => host_name,
Some(map) => {
for (guest, host) in map {
if host == &host_name {
return guest.clone();
}
}
host_name
}
}
}
}
#[derive(Clone)]
pub struct RealDir {
pub dir: RestrictedDir,
pub dir_perms: DirPerms,
pub file_perms: FilePerms,
pub allow_blocking: bool,
}
impl std::fmt::Debug for RealDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RealDir")
.field("dir", &self.dir)
.field("dir_perms", &self.dir_perms)
.field("file_perms", &self.file_perms)
.finish_non_exhaustive()
}
}
impl RealDir {
pub fn new(dir: cap_std::fs::Dir, dir_perms: DirPerms, file_perms: FilePerms) -> Self {
Self {
dir: RestrictedDir::new(dir),
dir_perms,
file_perms,
allow_blocking: false,
}
}
pub fn open_ambient(
path: impl AsRef<std::path::Path>,
dir_perms: DirPerms,
file_perms: FilePerms,
) -> std::io::Result<Self> {
let dir = cap_std::fs::Dir::open_ambient_dir(path, cap_std::ambient_authority())?;
Ok(Self::new(dir, dir_perms, file_perms))
}
}
pub struct RealFile {
pub file: Arc<cap_std::fs::File>,
pub perms: FilePerms,
pub readable: bool,
pub writable: bool,
pub allow_blocking: bool,
}
impl std::fmt::Debug for RealFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RealFile")
.field("perms", &self.perms)
.field("readable", &self.readable)
.field("writable", &self.writable)
.finish_non_exhaustive()
}
}
pub enum HybridDescriptor {
Vfs(VfsDescriptor),
RealDir {
dir: RealDir,
guest_path: String,
},
RealFile {
file: RealFile,
guest_path: String,
},
}
impl std::fmt::Debug for HybridDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HybridDescriptor::Vfs(d) => f.debug_tuple("Vfs").field(d).finish(),
HybridDescriptor::RealDir { guest_path, .. } => f
.debug_struct("RealDir")
.field("guest_path", guest_path)
.finish_non_exhaustive(),
HybridDescriptor::RealFile { guest_path, .. } => f
.debug_struct("RealFile")
.field("guest_path", guest_path)
.finish_non_exhaustive(),
}
}
}
impl HybridDescriptor {
pub fn path(&self) -> &str {
match self {
HybridDescriptor::Vfs(d) => &d.path,
HybridDescriptor::RealDir { guest_path, .. } => guest_path,
HybridDescriptor::RealFile { guest_path, .. } => guest_path,
}
}
pub fn is_dir(&self) -> bool {
match self {
HybridDescriptor::Vfs(d) => d.is_dir,
HybridDescriptor::RealDir { .. } => true,
HybridDescriptor::RealFile { .. } => false,
}
}
pub fn as_vfs(&self) -> Option<&VfsDescriptor> {
match self {
HybridDescriptor::Vfs(d) => Some(d),
_ => None,
}
}
pub fn as_real_dir(&self) -> Option<&RealDir> {
match self {
HybridDescriptor::RealDir { dir, .. } => Some(dir),
_ => None,
}
}
pub fn as_real_file(&self) -> Option<&RealFile> {
match self {
HybridDescriptor::RealFile { file, .. } => Some(file),
_ => None,
}
}
}
pub enum HybridPreopen {
Vfs {
guest_path: String,
dir_perms: DirPerms,
file_perms: FilePerms,
},
Real {
guest_path: String,
dir: RealDir,
},
}
impl std::fmt::Debug for HybridPreopen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HybridPreopen::Vfs {
guest_path,
dir_perms,
file_perms,
} => f
.debug_struct("Vfs")
.field("guest_path", guest_path)
.field("dir_perms", dir_perms)
.field("file_perms", file_perms)
.finish(),
HybridPreopen::Real { guest_path, dir } => f
.debug_struct("Real")
.field("guest_path", guest_path)
.field("dir", dir)
.finish(),
}
}
}
impl HybridPreopen {
pub fn guest_path(&self) -> &str {
match self {
HybridPreopen::Vfs { guest_path, .. } => guest_path,
HybridPreopen::Real { guest_path, .. } => guest_path,
}
}
}
pub struct HybridVfsCtx<S: VfsStorage + Clone> {
pub storage: S,
pub vfs_prefixes: Vec<String>,
pub preopens: Vec<HybridPreopen>,
pub allow_blocking_current_thread: bool,
}
impl<S: VfsStorage + Clone> std::fmt::Debug for HybridVfsCtx<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HybridVfsCtx")
.field("vfs_prefixes", &self.vfs_prefixes)
.field("preopens", &self.preopens.len())
.field(
"allow_blocking_current_thread",
&self.allow_blocking_current_thread,
)
.finish_non_exhaustive()
}
}
impl<S: VfsStorage + Clone> HybridVfsCtx<S> {
pub fn new(storage: S) -> Self {
Self {
storage,
vfs_prefixes: Vec::new(),
preopens: Vec::new(),
allow_blocking_current_thread: false,
}
}
pub fn add_vfs_preopen(
&mut self,
guest_path: impl Into<String>,
dir_perms: DirPerms,
file_perms: FilePerms,
) {
let guest_path = guest_path.into();
if let Err(e) = self.storage.mkdir_sync(&guest_path) {
tracing::warn!(
"Failed to create VFS preopen directory {}: {}",
guest_path,
e
);
}
self.vfs_prefixes.push(guest_path.clone());
self.preopens.push(HybridPreopen::Vfs {
guest_path,
dir_perms,
file_perms,
});
}
pub fn add_real_preopen(&mut self, guest_path: impl Into<String>, dir: RealDir) {
self.preopens.push(HybridPreopen::Real {
guest_path: guest_path.into(),
dir,
});
}
pub fn add_real_preopen_path(
&mut self,
guest_path: impl Into<String>,
host_path: impl AsRef<std::path::Path>,
dir_perms: DirPerms,
file_perms: FilePerms,
) -> std::io::Result<()> {
let dir = RealDir::open_ambient(host_path, dir_perms, file_perms)?;
self.add_real_preopen(guest_path, dir);
Ok(())
}
pub fn add_real_file_preopen_path(
&mut self,
guest_path: impl Into<String>,
host_path: impl AsRef<std::path::Path>,
dir_perms: DirPerms,
file_perms: FilePerms,
) -> std::io::Result<()> {
let host_path = host_path.as_ref();
let guest_path = guest_path.into();
let host_parent = host_path.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("file has no parent directory: {}", host_path.display()),
)
})?;
let file_name = host_path
.file_name()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("file has no name: {}", host_path.display()),
)
})?
.to_string_lossy()
.into_owned();
let guest_parent = guest_path
.rsplit_once('/')
.map(|(parent, _)| {
if parent.is_empty() {
"/".to_string()
} else {
parent.to_string()
}
})
.unwrap_or_else(|| "/".to_string());
let guest_file_name = guest_path
.rsplit_once('/')
.map(|(_, name)| name.to_string())
.unwrap_or_else(|| guest_path.clone());
let raw_dir =
cap_std::fs::Dir::open_ambient_dir(host_parent, cap_std::ambient_authority())?;
let restricted =
RestrictedDir::with_file_map(raw_dir, HashMap::from([(guest_file_name, file_name)]));
let dir = RealDir {
dir: restricted,
dir_perms,
file_perms,
allow_blocking: false,
};
self.add_real_preopen(guest_parent, dir);
Ok(())
}
pub fn is_vfs_path(&self, path: &str) -> bool {
self.vfs_prefixes
.iter()
.any(|prefix| path == prefix || path.starts_with(&format!("{}/", prefix)))
}
pub fn allow_blocking_current_thread(&mut self, allow: bool) {
self.allow_blocking_current_thread = allow;
}
}
pub struct HybridVfsState<'a, S: VfsStorage + Clone> {
pub ctx: &'a mut HybridVfsCtx<S>,
pub table: &'a mut ResourceTable,
}
impl<S: VfsStorage + Clone> std::fmt::Debug for HybridVfsState<'_, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HybridVfsState")
.field("ctx", &self.ctx)
.finish_non_exhaustive()
}
}
impl<'a, S: VfsStorage + Clone> HybridVfsState<'a, S> {
pub fn new(ctx: &'a mut HybridVfsCtx<S>, table: &'a mut ResourceTable) -> Self {
Self { ctx, table }
}
pub fn is_vfs_path(&self, path: &str) -> bool {
self.ctx.is_vfs_path(path)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::{ArcStorage, InMemoryStorage};
#[test]
fn test_is_vfs_path() {
let storage = ArcStorage::new(Arc::new(InMemoryStorage::new()));
let mut ctx = HybridVfsCtx::new(storage);
ctx.add_vfs_preopen("/data", DirPerms::all(), FilePerms::all());
assert!(ctx.is_vfs_path("/data"));
assert!(ctx.is_vfs_path("/data/foo"));
assert!(ctx.is_vfs_path("/data/foo/bar"));
assert!(!ctx.is_vfs_path("/python-stdlib"));
assert!(!ctx.is_vfs_path("/datafile")); assert!(!ctx.is_vfs_path("/"));
}
#[test]
fn test_hybrid_preopen_guest_path() {
let preopen = HybridPreopen::Vfs {
guest_path: "/data".to_string(),
dir_perms: DirPerms::all(),
file_perms: FilePerms::all(),
};
assert_eq!(preopen.guest_path(), "/data");
}
}