use std::io;
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum NetworkPolicy {
#[default]
Deny,
Allow,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct SandboxSpec {
pub read_only_paths: Vec<PathBuf>,
pub writable_paths: Vec<PathBuf>,
pub network: NetworkPolicy,
pub env: Vec<(String, String)>,
pub timeout: Option<Duration>,
}
impl SandboxSpec {
pub fn deny_all() -> Self {
Self::default()
}
#[must_use]
pub fn with_read_only(mut self, path: impl Into<PathBuf>) -> Self {
self.read_only_paths.push(path.into());
self
}
#[must_use]
pub fn with_writable(mut self, path: impl Into<PathBuf>) -> Self {
self.writable_paths.push(path.into());
self
}
#[must_use]
pub fn with_network(mut self, network: NetworkPolicy) -> Self {
self.network = network;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn validate(&self) -> Result<(), SpecError> {
for path in &self.writable_paths {
if self.read_only_paths.contains(path) {
return Err(SpecError::ConflictingPath { path: path.clone() });
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SandboxCommand {
pub program: String,
pub args: Vec<String>,
pub cwd: Option<PathBuf>,
}
impl SandboxCommand {
pub fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
cwd: None,
}
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
#[must_use]
pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxOutput {
pub status: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl SandboxOutput {
pub fn success(&self) -> bool {
self.status == 0
}
}
#[derive(Debug)]
pub enum SandboxError {
Spec(SpecError),
Spawn(io::Error),
Unsupported(&'static str),
UnsupportedReadOnlyShare { path: PathBuf },
}
impl std::fmt::Display for SandboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Spec(err) => write!(f, "invalid sandbox policy: {err}"),
Self::Spawn(err) => write!(f, "sandbox spawn failed: {err}"),
Self::Unsupported(what) => write!(f, "sandbox backend does not support {what}"),
Self::UnsupportedReadOnlyShare { path, .. } => write!(
f,
"sandbox backend cannot express {} as read-only",
path.display()
),
}
}
}
impl std::error::Error for SandboxError {}
impl From<SpecError> for SandboxError {
fn from(err: SpecError) -> Self {
Self::Spec(err)
}
}
impl From<io::Error> for SandboxError {
fn from(err: io::Error) -> Self {
Self::Spawn(err)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum SpecError {
ConflictingPath { path: PathBuf },
}
impl std::fmt::Display for SpecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConflictingPath { path } => {
write!(f, "{} is both read-only and writable", path.display())
}
}
}
}
impl std::error::Error for SpecError {}
pub trait Sandbox: Send + Sync {
fn name(&self) -> &'static str;
fn is_isolated(&self) -> bool;
fn exec(
&self,
spec: &SandboxSpec,
command: &SandboxCommand,
) -> Result<SandboxOutput, SandboxError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DirectSandbox;
impl Sandbox for DirectSandbox {
fn name(&self) -> &'static str {
"direct"
}
fn is_isolated(&self) -> bool {
false
}
fn exec(
&self,
spec: &SandboxSpec,
command: &SandboxCommand,
) -> Result<SandboxOutput, SandboxError> {
spec.validate()?;
if spec.network == NetworkPolicy::Deny {
return Err(SandboxError::Unsupported("a network-denying policy"));
}
let mut cmd = std::process::Command::new(&command.program);
cmd.args(&command.args);
for (key, value) in &spec.env {
cmd.env(key, value);
}
if let Some(cwd) = &command.cwd {
cmd.current_dir(cwd);
}
cmd.stdin(std::process::Stdio::null());
let output = cmd.output()?;
Ok(SandboxOutput {
status: output.status.code().unwrap_or(-1),
stdout: output.stdout,
stderr: output.stderr,
})
}
}
#[cfg(feature = "vfkit")]
pub mod vfkit;