use super::{Capability, CapabilityManifest};
use crate::Result;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub read_paths: HashSet<PathBuf>,
pub write_paths: HashSet<PathBuf>,
pub env_vars: HashSet<String>,
pub max_memory: usize,
pub max_time_ms: u64,
pub allow_network: bool,
pub allow_spawn: bool,
}
impl SandboxConfig {
pub fn restricted() -> Self {
Self {
read_paths: HashSet::new(),
write_paths: HashSet::new(),
env_vars: HashSet::new(),
max_memory: 64 * 1024 * 1024, max_time_ms: 5000, allow_network: false,
allow_spawn: false,
}
}
pub fn permissive() -> Self {
Self {
read_paths: HashSet::new(), write_paths: HashSet::new(),
env_vars: HashSet::new(),
max_memory: 512 * 1024 * 1024, max_time_ms: 30000, allow_network: true,
allow_spawn: true,
}
}
pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
self.read_paths.insert(path.into());
self
}
pub fn allow_write(mut self, path: impl Into<PathBuf>) -> Self {
self.write_paths.insert(path.into());
self
}
pub fn allow_env(mut self, var: impl Into<String>) -> Self {
self.env_vars.insert(var.into());
self
}
pub fn with_memory_limit(mut self, bytes: usize) -> Self {
self.max_memory = bytes;
self
}
pub fn with_time_limit(mut self, ms: u64) -> Self {
self.max_time_ms = ms;
self
}
pub fn to_manifest(&self) -> CapabilityManifest {
let mut manifest = CapabilityManifest::new();
if !self.read_paths.is_empty() {
manifest.require(Capability::FileRead);
}
if !self.write_paths.is_empty() {
manifest.require(Capability::FileWrite);
}
if !self.env_vars.is_empty() {
manifest.require(Capability::Environment);
}
if self.allow_network {
manifest.require(Capability::Network);
} else {
manifest.deny(Capability::Network);
}
if self.allow_spawn {
manifest.require(Capability::Process);
} else {
manifest.deny(Capability::Process);
}
manifest
}
}
impl Default for SandboxConfig {
fn default() -> Self {
Self::restricted()
}
}
#[derive(Debug)]
pub struct Sandbox {
config: SandboxConfig,
started: Option<std::time::Instant>,
memory_used: usize,
violations: Vec<SandboxViolation>,
}
#[derive(Debug, Clone)]
pub struct SandboxViolation {
pub kind: ViolationKind,
pub message: String,
pub timestamp: std::time::Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationKind {
UnauthorizedRead,
UnauthorizedWrite,
MemoryExceeded,
TimeExceeded,
NetworkDenied,
SpawnDenied,
EnvDenied,
}
impl Sandbox {
pub fn new(config: SandboxConfig) -> Self {
Self {
config,
started: None,
memory_used: 0,
violations: Vec::new(),
}
}
pub fn start(&mut self) {
self.started = Some(std::time::Instant::now());
self.memory_used = 0;
self.violations.clear();
}
pub fn check_time(&mut self) -> Result<bool> {
if let Some(started) = self.started {
if started.elapsed().as_millis() as u64 > self.config.max_time_ms {
self.record_violation(ViolationKind::TimeExceeded, "Time limit exceeded");
return Ok(false);
}
}
Ok(true)
}
pub fn check_memory(&mut self, bytes: usize) -> Result<bool> {
if self.memory_used + bytes > self.config.max_memory {
self.record_violation(ViolationKind::MemoryExceeded, "Memory limit exceeded");
return Ok(false);
}
self.memory_used += bytes;
Ok(true)
}
pub fn check_read(&mut self, path: &Path) -> bool {
if self.config.read_paths.is_empty() {
self.record_violation(
ViolationKind::UnauthorizedRead,
&format!("Read denied: {}", path.display()),
);
return false;
}
let allowed = self
.config
.read_paths
.iter()
.any(|allowed| path.starts_with(allowed) || path == allowed);
if !allowed {
self.record_violation(
ViolationKind::UnauthorizedRead,
&format!("Read denied: {}", path.display()),
);
}
allowed
}
pub fn check_write(&mut self, path: &Path) -> bool {
if self.config.write_paths.is_empty() {
self.record_violation(
ViolationKind::UnauthorizedWrite,
&format!("Write denied: {}", path.display()),
);
return false;
}
let allowed = self
.config
.write_paths
.iter()
.any(|allowed| path.starts_with(allowed) || path == allowed);
if !allowed {
self.record_violation(
ViolationKind::UnauthorizedWrite,
&format!("Write denied: {}", path.display()),
);
}
allowed
}
pub fn check_env(&mut self, var: &str) -> bool {
if self.config.env_vars.is_empty() {
self.record_violation(
ViolationKind::EnvDenied,
&format!("Env access denied: {}", var),
);
return false;
}
let allowed = self.config.env_vars.contains(var);
if !allowed {
self.record_violation(
ViolationKind::EnvDenied,
&format!("Env access denied: {}", var),
);
}
allowed
}
pub fn check_network(&mut self) -> bool {
if !self.config.allow_network {
self.record_violation(ViolationKind::NetworkDenied, "Network access denied");
return false;
}
true
}
pub fn check_spawn(&mut self) -> bool {
if !self.config.allow_spawn {
self.record_violation(ViolationKind::SpawnDenied, "Process spawn denied");
return false;
}
true
}
fn record_violation(&mut self, kind: ViolationKind, message: &str) {
self.violations.push(SandboxViolation {
kind,
message: message.to_string(),
timestamp: std::time::Instant::now(),
});
}
pub fn violations(&self) -> &[SandboxViolation] {
&self.violations
}
pub fn has_violations(&self) -> bool {
!self.violations.is_empty()
}
pub fn memory_used(&self) -> usize {
self.memory_used
}
pub fn elapsed(&self) -> Option<std::time::Duration> {
self.started.map(|s| s.elapsed())
}
pub fn reset(&mut self) {
self.started = None;
self.memory_used = 0;
self.violations.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config() {
let config = SandboxConfig::restricted()
.allow_read("/home/user")
.allow_write("/tmp")
.with_memory_limit(128 * 1024 * 1024);
assert!(config.read_paths.contains(Path::new("/home/user")));
assert!(config.write_paths.contains(Path::new("/tmp")));
assert_eq!(config.max_memory, 128 * 1024 * 1024);
}
#[test]
fn test_sandbox_file_access() {
let config = SandboxConfig::restricted().allow_read("/home/user/project");
let mut sandbox = Sandbox::new(config);
sandbox.start();
assert!(sandbox.check_read(Path::new("/home/user/project/src/main.rs")));
assert!(!sandbox.check_read(Path::new("/etc/passwd")));
assert!(sandbox.has_violations());
}
#[test]
fn test_sandbox_memory() {
let config = SandboxConfig::restricted().with_memory_limit(1024);
let mut sandbox = Sandbox::new(config);
sandbox.start();
assert!(sandbox.check_memory(512).unwrap());
assert!(sandbox.check_memory(512).unwrap());
assert!(!sandbox.check_memory(1).unwrap()); }
}