use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RuntimePolicy {
pub allowed_paths: Vec<PathBuf>,
pub read_only_paths: Vec<PathBuf>,
pub allowed_hosts: Vec<String>,
pub memory_limit: Option<usize>,
pub time_limit: Option<Duration>,
pub output_limit: Option<usize>,
}
impl RuntimePolicy {
pub fn unrestricted() -> Self {
Self {
allowed_paths: Vec::new(),
read_only_paths: Vec::new(),
allowed_hosts: Vec::new(),
memory_limit: None,
time_limit: None,
output_limit: None,
}
}
pub fn is_path_readable(&self, path: &Path) -> bool {
if self.allowed_paths.is_empty() && self.read_only_paths.is_empty() {
return true;
}
self.path_matches_any(path, &self.allowed_paths)
|| self.path_matches_any(path, &self.read_only_paths)
}
pub fn is_path_writable(&self, path: &Path) -> bool {
if self.allowed_paths.is_empty() && self.read_only_paths.is_empty() {
return true;
}
if self.path_matches_any(path, &self.read_only_paths) {
return false;
}
self.path_matches_any(path, &self.allowed_paths)
}
pub fn is_host_allowed(&self, host: &str) -> bool {
if self.allowed_hosts.is_empty() {
return true;
}
self.allowed_hosts
.iter()
.any(|pattern| host_matches(host, pattern))
}
fn path_matches_any(&self, path: &Path, patterns: &[PathBuf]) -> bool {
patterns.iter().any(|allowed| path.starts_with(allowed))
}
}
fn host_matches(host: &str, pattern: &str) -> bool {
if let Some(suffix) = pattern.strip_prefix("*.") {
host.ends_with(suffix) && host.len() > suffix.len()
} else {
host == pattern
}
}
#[derive(Debug, Clone)]
pub struct FileMetadata {
pub size: u64,
pub is_dir: bool,
pub is_file: bool,
pub readonly: bool,
}
#[derive(Debug, Clone)]
pub struct PathEntry {
pub path: PathBuf,
pub is_dir: bool,
}
pub trait FileSystemProvider: Send + Sync {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>>;
fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()>;
fn append(&self, path: &Path, data: &[u8]) -> std::io::Result<()>;
fn exists(&self, path: &Path) -> bool;
fn remove(&self, path: &Path) -> std::io::Result<()>;
fn list_dir(&self, path: &Path) -> std::io::Result<Vec<PathEntry>>;
fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata>;
fn create_dir_all(&self, path: &Path) -> std::io::Result<()>;
}
#[derive(Debug, Clone, Copy)]
pub struct RealFileSystem;
impl FileSystemProvider for RealFileSystem {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
}
fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
std::fs::write(path, data)
}
fn append(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(path)?;
f.write_all(data)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn remove(&self, path: &Path) -> std::io::Result<()> {
std::fs::remove_file(path)
}
fn list_dir(&self, path: &Path) -> std::io::Result<Vec<PathEntry>> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
entries.push(PathEntry {
path: entry.path(),
is_dir: entry.file_type()?.is_dir(),
});
}
Ok(entries)
}
fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
let m = std::fs::metadata(path)?;
Ok(FileMetadata {
size: m.len(),
is_dir: m.is_dir(),
is_file: m.is_file(),
readonly: m.permissions().readonly(),
})
}
fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(path)
}
}
pub struct PolicyEnforcedFs {
inner: Arc<dyn FileSystemProvider>,
policy: Arc<RuntimePolicy>,
}
impl PolicyEnforcedFs {
pub fn new(inner: Arc<dyn FileSystemProvider>, policy: Arc<RuntimePolicy>) -> Self {
Self { inner, policy }
}
fn check_readable(&self, path: &Path) -> std::io::Result<()> {
if self.policy.is_path_readable(path) {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("policy denies read access to {}", path.display()),
))
}
}
fn check_writable(&self, path: &Path) -> std::io::Result<()> {
if self.policy.is_path_writable(path) {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("policy denies write access to {}", path.display()),
))
}
}
}
impl FileSystemProvider for PolicyEnforcedFs {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
self.check_readable(path)?;
self.inner.read(path)
}
fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
self.check_writable(path)?;
self.inner.write(path, data)
}
fn append(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
self.check_writable(path)?;
self.inner.append(path, data)
}
fn exists(&self, path: &Path) -> bool {
self.policy.is_path_readable(path) && self.inner.exists(path)
}
fn remove(&self, path: &Path) -> std::io::Result<()> {
self.check_writable(path)?;
self.inner.remove(path)
}
fn list_dir(&self, path: &Path) -> std::io::Result<Vec<PathEntry>> {
self.check_readable(path)?;
self.inner.list_dir(path)
}
fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
self.check_readable(path)?;
self.inner.metadata(path)
}
fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
self.check_writable(path)?;
self.inner.create_dir_all(path)
}
}
pub struct RoutingFileSystem {
routes: Vec<(PathBuf, Arc<dyn FileSystemProvider>)>,
fallback: Arc<dyn FileSystemProvider>,
}
impl RoutingFileSystem {
pub fn new(
routes: Vec<(PathBuf, Arc<dyn FileSystemProvider>)>,
fallback: Arc<dyn FileSystemProvider>,
) -> Self {
Self { routes, fallback }
}
fn resolve(&self, path: &Path) -> &dyn FileSystemProvider {
for (prefix, provider) in &self.routes {
if path.starts_with(prefix) {
return provider.as_ref();
}
}
self.fallback.as_ref()
}
}
impl FileSystemProvider for RoutingFileSystem {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
self.resolve(path).read(path)
}
fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
self.resolve(path).write(path, data)
}
fn append(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
self.resolve(path).append(path, data)
}
fn exists(&self, path: &Path) -> bool {
self.resolve(path).exists(path)
}
fn remove(&self, path: &Path) -> std::io::Result<()> {
self.resolve(path).remove(path)
}
fn list_dir(&self, path: &Path) -> std::io::Result<Vec<PathEntry>> {
self.resolve(path).list_dir(path)
}
fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
self.resolve(path).metadata(path)
}
fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
self.resolve(path).create_dir_all(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unrestricted_allows_everything() {
let policy = RuntimePolicy::unrestricted();
assert!(policy.is_path_readable(Path::new("/any/path")));
assert!(policy.is_path_writable(Path::new("/any/path")));
assert!(policy.is_host_allowed("any.host.com"));
}
#[test]
fn allowed_paths_restrict_read() {
let policy = RuntimePolicy {
allowed_paths: vec![PathBuf::from("/data"), PathBuf::from("/tmp")],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_path_readable(Path::new("/data/file.txt")));
assert!(policy.is_path_readable(Path::new("/tmp/scratch")));
assert!(!policy.is_path_readable(Path::new("/etc/passwd")));
}
#[test]
fn allowed_paths_restrict_write() {
let policy = RuntimePolicy {
allowed_paths: vec![PathBuf::from("/data")],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_path_writable(Path::new("/data/out.txt")));
assert!(!policy.is_path_writable(Path::new("/etc/shadow")));
}
#[test]
fn read_only_paths_deny_writes() {
let policy = RuntimePolicy {
allowed_paths: vec![PathBuf::from("/data")],
read_only_paths: vec![PathBuf::from("/data/config")],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_path_readable(Path::new("/data/file.txt")));
assert!(policy.is_path_readable(Path::new("/data/config/app.toml")));
assert!(policy.is_path_writable(Path::new("/data/file.txt")));
assert!(!policy.is_path_writable(Path::new("/data/config/app.toml")));
}
#[test]
fn read_only_paths_are_readable_even_without_allowed_paths() {
let policy = RuntimePolicy {
read_only_paths: vec![PathBuf::from("/docs")],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_path_readable(Path::new("/docs/readme.md")));
assert!(!policy.is_path_writable(Path::new("/docs/readme.md")));
assert!(!policy.is_path_readable(Path::new("/other")));
}
#[test]
fn exact_host_match() {
let policy = RuntimePolicy {
allowed_hosts: vec!["api.example.com".into()],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_host_allowed("api.example.com"));
assert!(!policy.is_host_allowed("evil.com"));
}
#[test]
fn wildcard_host_match() {
let policy = RuntimePolicy {
allowed_hosts: vec!["*.example.com".into()],
..RuntimePolicy::unrestricted()
};
assert!(policy.is_host_allowed("api.example.com"));
assert!(policy.is_host_allowed("sub.example.com"));
assert!(!policy.is_host_allowed("example.com"));
assert!(!policy.is_host_allowed("evil.com"));
}
#[test]
fn empty_allowed_hosts_allows_all() {
let policy = RuntimePolicy::unrestricted();
assert!(policy.is_host_allowed("anything.com"));
}
#[test]
fn real_fs_exists() {
let fs = RealFileSystem;
assert!(fs.exists(Path::new("/")));
}
#[test]
fn policy_enforced_fs_denies_unauthorized_read() {
let inner: Arc<dyn FileSystemProvider> = Arc::new(RealFileSystem);
let policy = Arc::new(RuntimePolicy {
allowed_paths: vec![PathBuf::from("/allowed")],
..RuntimePolicy::unrestricted()
});
let enforced = PolicyEnforcedFs::new(inner, policy);
let result = enforced.read(Path::new("/forbidden/file.txt"));
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied
);
}
#[test]
fn policy_enforced_fs_denies_unauthorized_write() {
let inner: Arc<dyn FileSystemProvider> = Arc::new(RealFileSystem);
let policy = Arc::new(RuntimePolicy {
allowed_paths: vec![PathBuf::from("/allowed")],
..RuntimePolicy::unrestricted()
});
let enforced = PolicyEnforcedFs::new(inner, policy);
let result = enforced.write(Path::new("/forbidden/file.txt"), b"data");
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied
);
}
#[test]
fn policy_enforced_fs_hides_existence() {
let inner: Arc<dyn FileSystemProvider> = Arc::new(RealFileSystem);
let policy = Arc::new(RuntimePolicy {
allowed_paths: vec![PathBuf::from("/nonexistent_prefix")],
..RuntimePolicy::unrestricted()
});
let enforced = PolicyEnforcedFs::new(inner, policy);
assert!(!enforced.exists(Path::new("/")));
}
struct ConstFs {
data: Vec<u8>,
}
impl FileSystemProvider for ConstFs {
fn read(&self, _path: &Path) -> std::io::Result<Vec<u8>> {
Ok(self.data.clone())
}
fn write(&self, _path: &Path, _data: &[u8]) -> std::io::Result<()> {
Ok(())
}
fn append(&self, _path: &Path, _data: &[u8]) -> std::io::Result<()> {
Ok(())
}
fn exists(&self, _path: &Path) -> bool {
true
}
fn remove(&self, _path: &Path) -> std::io::Result<()> {
Ok(())
}
fn list_dir(&self, _path: &Path) -> std::io::Result<Vec<PathEntry>> {
Ok(Vec::new())
}
fn metadata(&self, _path: &Path) -> std::io::Result<FileMetadata> {
Ok(FileMetadata {
size: self.data.len() as u64,
is_dir: false,
is_file: true,
readonly: false,
})
}
fn create_dir_all(&self, _path: &Path) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn routing_fs_dispatches_by_prefix() {
let a: Arc<dyn FileSystemProvider> = Arc::new(ConstFs {
data: vec![1, 2, 3],
});
let b: Arc<dyn FileSystemProvider> = Arc::new(ConstFs {
data: vec![4, 5, 6],
});
let fallback: Arc<dyn FileSystemProvider> = Arc::new(ConstFs { data: vec![0] });
let router = RoutingFileSystem::new(
vec![(PathBuf::from("/a"), a), (PathBuf::from("/b"), b)],
fallback,
);
assert_eq!(router.read(Path::new("/a/file")).unwrap(), vec![1, 2, 3]);
assert_eq!(router.read(Path::new("/b/file")).unwrap(), vec![4, 5, 6]);
assert_eq!(router.read(Path::new("/c/file")).unwrap(), vec![0]);
}
#[test]
fn routing_fs_first_match_wins() {
let first: Arc<dyn FileSystemProvider> = Arc::new(ConstFs { data: vec![1] });
let second: Arc<dyn FileSystemProvider> = Arc::new(ConstFs { data: vec![2] });
let fallback: Arc<dyn FileSystemProvider> = Arc::new(ConstFs { data: vec![0] });
let router = RoutingFileSystem::new(
vec![
(PathBuf::from("/data"), first),
(PathBuf::from("/data"), second),
],
fallback,
);
assert_eq!(router.read(Path::new("/data/x")).unwrap(), vec![1]);
}
}