use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::fs;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result, SandboxError};
use crate::security::{
Capabilities, NetworkCapability, FilesystemCapability,
EnvironmentCapability, ProcessCapability, PortRange, HostSpec
};
use crate::runtime::RuntimeConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxManifest {
pub name: String,
pub version: String,
pub description: Option<String>,
#[serde(default)]
pub runtime: ManifestRuntime,
#[serde(default)]
pub capabilities: ManifestCapabilities,
#[serde(default)]
pub resource_limits: ManifestResourceLimits,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestRuntime {
pub engine: String,
#[serde(default)]
pub debug: bool,
#[serde(default = "default_true")]
pub cache_modules: bool,
#[serde(default = "default_threads")]
pub compilation_threads: usize,
}
fn default_true() -> bool {
true
}
fn default_threads() -> usize {
num_cpus::get()
}
impl Default for ManifestRuntime {
fn default() -> Self {
Self {
engine: "wasmtime".to_string(),
debug: false,
cache_modules: true,
compilation_threads: num_cpus::get(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestNetworkCapabilities {
#[serde(default)]
pub mode: String,
#[serde(default)]
pub allowed_hosts: Vec<String>,
#[serde(default)]
pub allowed_ports: Vec<String>,
}
impl Default for ManifestNetworkCapabilities {
fn default() -> Self {
Self {
mode: "none".to_string(),
allowed_hosts: Vec::new(),
allowed_ports: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestFilesystemCapabilities {
#[serde(default)]
pub readable_dirs: Vec<String>,
#[serde(default)]
pub writable_dirs: Vec<String>,
#[serde(default)]
pub allow_create: bool,
#[serde(default)]
pub allow_delete: bool,
pub max_file_size: Option<String>,
}
impl Default for ManifestFilesystemCapabilities {
fn default() -> Self {
Self {
readable_dirs: Vec::new(),
writable_dirs: Vec::new(),
allow_create: false,
allow_delete: false,
max_file_size: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestEnvironmentCapabilities {
#[serde(default)]
pub mode: String,
#[serde(default)]
pub vars: Vec<String>,
}
impl Default for ManifestEnvironmentCapabilities {
fn default() -> Self {
Self {
mode: "none".to_string(),
vars: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestProcessCapabilities {
#[serde(default)]
pub allow_execution: bool,
#[serde(default)]
pub allowed_commands: Vec<String>,
}
impl Default for ManifestProcessCapabilities {
fn default() -> Self {
Self {
allow_execution: false,
allowed_commands: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestCapabilities {
#[serde(default)]
pub network: ManifestNetworkCapabilities,
#[serde(default)]
pub filesystem: ManifestFilesystemCapabilities,
#[serde(default)]
pub environment: ManifestEnvironmentCapabilities,
#[serde(default)]
pub process: ManifestProcessCapabilities,
#[serde(default)]
pub time_mode: String,
#[serde(default)]
pub random_mode: String,
#[serde(default)]
pub custom: HashMap<String, String>,
}
impl Default for ManifestCapabilities {
fn default() -> Self {
Self {
network: ManifestNetworkCapabilities::default(),
filesystem: ManifestFilesystemCapabilities::default(),
environment: ManifestEnvironmentCapabilities::default(),
process: ManifestProcessCapabilities::default(),
time_mode: "readonly".to_string(),
random_mode: "pseudo".to_string(),
custom: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestMemoryLimits {
pub max_memory: Option<String>,
pub reserved_memory: Option<String>,
}
impl Default for ManifestMemoryLimits {
fn default() -> Self {
Self {
max_memory: Some("64MB".to_string()),
reserved_memory: Some("16MB".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestCpuLimits {
pub max_execution_time: Option<String>,
pub cpu_usage_percentage: Option<u8>,
pub max_threads: Option<u32>,
}
impl Default for ManifestCpuLimits {
fn default() -> Self {
Self {
max_execution_time: Some("10s".to_string()),
cpu_usage_percentage: Some(50),
max_threads: Some(1),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestIoLimits {
pub max_read_bytes: Option<String>,
pub max_write_bytes: Option<String>,
pub max_open_files: Option<u32>,
}
impl Default for ManifestIoLimits {
fn default() -> Self {
Self {
max_read_bytes: Some("10MB".to_string()),
max_write_bytes: Some("5MB".to_string()),
max_open_files: Some(10),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestResourceLimits {
#[serde(default)]
pub memory: ManifestMemoryLimits,
#[serde(default)]
pub cpu: ManifestCpuLimits,
#[serde(default)]
pub io: ManifestIoLimits,
}
impl Default for ManifestResourceLimits {
fn default() -> Self {
Self {
memory: ManifestMemoryLimits::default(),
cpu: ManifestCpuLimits::default(),
io: ManifestIoLimits::default(),
}
}
}
impl SandboxManifest {
pub fn from_path(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.map_err(|e| Error::Filesystem {
operation: "read_manifest".to_string(),
path: path.to_path_buf(),
reason: e.to_string()
})?;
Self::from_str(&content)
}
pub fn from_str(content: &str) -> Result<Self> {
if let Ok(manifest) = toml::from_str::<SandboxManifest>(content) {
return Ok(manifest);
}
serde_json::from_str::<SandboxManifest>(content)
.map_err(|e| SandboxError::Configuration {
message: format!("Failed to parse manifest: {}", e),
suggestion: Some("Check manifest syntax - supports both TOML and JSON".to_string()),
field: Some("manifest".to_string()),
})
}
pub fn to_runtime_config(&self) -> RuntimeConfig {
RuntimeConfig {
enable_fuel: true,
enable_memory_limits: true,
native_stack_trace: self.runtime.debug,
debug_info: self.runtime.debug,
compilation_threads: self.runtime.compilation_threads,
cache_modules: self.runtime.cache_modules,
cache_directory: None,
}
}
pub fn to_capabilities(&self) -> Result<Capabilities> {
let network = match self.capabilities.network.mode.as_str() {
"none" => NetworkCapability::None,
"loopback" => NetworkCapability::Loopback,
"allowed_hosts" => {
let mut hosts = Vec::new();
for host_spec in &self.capabilities.network.allowed_hosts {
let parts: Vec<&str> = host_spec.split(':').collect();
if parts.len() != 2 {
return Err(SandboxError::config_error(format!("Invalid host spec: {}", host_spec), None));
}
let host = parts[0].to_string();
let port_spec = parts[1];
let ports = if port_spec.contains('-') {
let port_parts: Vec<&str> = port_spec.split('-').collect();
if port_parts.len() != 2 {
return Err(SandboxError::config_error(format!("Invalid port range: {}", port_spec), None));
}
let start = port_parts[0].parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_parts[0]), None))?;
let end = port_parts[1].parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_parts[1]), None))?;
Some(PortRange::new(start, end))
} else {
let port = port_spec.parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_spec), None))?;
Some(PortRange::single(port))
};
hosts.push(HostSpec {
host,
ports,
secure: true, });
}
NetworkCapability::AllowedHosts(hosts)
},
"allowed_ports" => {
let mut ports = Vec::new();
for port_spec in &self.capabilities.network.allowed_ports {
if port_spec.contains('-') {
let port_parts: Vec<&str> = port_spec.split('-').collect();
if port_parts.len() != 2 {
return Err(SandboxError::config_error(format!("Invalid port range: {}", port_spec), None));
}
let start = port_parts[0].parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_parts[0]), None))?;
let end = port_parts[1].parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_parts[1]), None))?;
ports.push(PortRange::new(start, end));
} else {
let port = port_spec.parse::<u16>()
.map_err(|_| SandboxError::config_error(format!("Invalid port: {}", port_spec), None))?;
ports.push(PortRange::single(port));
}
}
NetworkCapability::AllowedPorts(ports)
},
"full" => NetworkCapability::Full,
_ => {
return Err(SandboxError::config_error(format!("Invalid network mode: {}", self.capabilities.network.mode), None));
}
};
let filesystem = FilesystemCapability {
readable_dirs: self.capabilities.filesystem.readable_dirs.iter()
.map(|s| PathBuf::from(s))
.collect(),
writable_dirs: self.capabilities.filesystem.writable_dirs.iter()
.map(|s| PathBuf::from(s))
.collect(),
max_file_size: self.capabilities.filesystem.max_file_size.as_ref()
.and_then(|s| parse_size(s).ok()),
allow_create: self.capabilities.filesystem.allow_create,
allow_delete: self.capabilities.filesystem.allow_delete,
};
let environment = match self.capabilities.environment.mode.as_str() {
"none" => EnvironmentCapability::None,
"allowlist" => EnvironmentCapability::Allowlist(
self.capabilities.environment.vars.clone()
),
"denylist" => EnvironmentCapability::Denylist(
self.capabilities.environment.vars.clone()
),
"full" => EnvironmentCapability::Full,
_ => {
return Err(SandboxError::config_error(format!("Invalid environment mode: {}", self.capabilities.environment.mode), None));
}
};
let process = if self.capabilities.process.allow_execution {
if self.capabilities.process.allowed_commands.is_empty() {
ProcessCapability::Full
} else {
ProcessCapability::AllowedCommands(
self.capabilities.process.allowed_commands.clone()
)
}
} else {
ProcessCapability::None
};
Ok(Capabilities {
network,
filesystem,
environment,
process,
time: match self.capabilities.time_mode.as_str() {
"readonly" => crate::security::TimeCapability::ReadOnly,
"full" => crate::security::TimeCapability::Full,
_ => crate::security::TimeCapability::ReadOnly,
},
random: match self.capabilities.random_mode.as_str() {
"none" => crate::security::RandomCapability::None,
"pseudo" => crate::security::RandomCapability::PseudoOnly,
"full" => crate::security::RandomCapability::Full,
_ => crate::security::RandomCapability::PseudoOnly,
},
custom: HashMap::new(), })
}
}
fn parse_size(size: &str) -> Result<u64> {
let size = size.trim();
if size.is_empty() {
return Err(SandboxError::config_error("Empty size string".to_string(), None));
}
let mut num_str = String::new();
let mut suffix = String::new();
for c in size.chars() {
if c.is_digit(10) || c == '.' {
num_str.push(c);
} else {
suffix.push(c);
}
}
if num_str.is_empty() {
return Err(SandboxError::config_error(format!("Invalid size format: {}", size), None));
}
let num: f64 = num_str.parse()
.map_err(|_| SandboxError::config_error(format!("Invalid number: {}", num_str), None))?;
let multiplier = match suffix.trim().to_uppercase().as_str() {
"" | "B" => 1,
"K" | "KB" => 1024,
"M" | "MB" => 1024 * 1024,
"G" | "GB" => 1024 * 1024 * 1024,
_ => return Err(SandboxError::config_error(format!("Invalid size suffix: {}", suffix), None)),
};
Ok((num * multiplier as f64) as u64)
}