use std::path::PathBuf;
use std::time::Instant;
use async_trait::async_trait;
use tokio::io::AsyncWriteExt;
use tracing::{debug, info, instrument, warn};
use crate::harness::{
HARNESS_TEMPLATE, extract_structured_output, truncate_output, validate_rust_source,
};
use crate::{
BackendCapabilities, CodeExecutor, ExecutionError, ExecutionIsolation, ExecutionLanguage,
ExecutionPayload, ExecutionRequest, ExecutionResult, ExecutionStatus, validate_request,
};
#[derive(Debug, Clone)]
pub struct RustSandboxConfig {
pub rustc_path: String,
pub rustc_flags: Vec<String>,
pub serde_json_path: Option<PathBuf>,
}
impl Default for RustSandboxConfig {
fn default() -> Self {
Self { rustc_path: "rustc".to_string(), rustc_flags: vec![], serde_json_path: None }
}
}
#[derive(Debug, Clone)]
pub struct RustSandboxExecutor {
config: RustSandboxConfig,
}
impl RustSandboxExecutor {
pub fn new(config: RustSandboxConfig) -> Self {
Self { config }
}
}
impl Default for RustSandboxExecutor {
fn default() -> Self {
Self::new(RustSandboxConfig::default())
}
}
#[async_trait]
impl CodeExecutor for RustSandboxExecutor {
fn name(&self) -> &str {
"rust-sandbox"
}
fn capabilities(&self) -> BackendCapabilities {
BackendCapabilities {
isolation: ExecutionIsolation::HostLocal,
enforce_network_policy: false,
enforce_filesystem_policy: false,
enforce_environment_policy: false,
enforce_timeout: true,
supports_structured_output: true,
supports_process_execution: false,
supports_persistent_workspace: false,
supports_interactive_sessions: false,
}
}
fn supports_language(&self, lang: &ExecutionLanguage) -> bool {
matches!(lang, ExecutionLanguage::Rust)
}
#[instrument(skip_all, fields(backend = "rust-sandbox", language = "Rust"))]
async fn execute(&self, request: ExecutionRequest) -> Result<ExecutionResult, ExecutionError> {
validate_request(&self.capabilities(), &[ExecutionLanguage::Rust], &request)?;
let code = match &request.payload {
ExecutionPayload::Source { code } => code.clone(),
ExecutionPayload::GuestModule { .. } => {
return Err(ExecutionError::InvalidRequest(
"RustSandboxExecutor only accepts Source payloads".to_string(),
));
}
};
validate_rust_source(&code)?;
let start = Instant::now();
let tmp_dir = tempfile::tempdir().map_err(|e| {
ExecutionError::ExecutionFailed(format!("failed to create temp directory: {e}"))
})?;
let source_path = tmp_dir.path().join("main.rs");
let binary_path = tmp_dir.path().join("main");
let harnessed_source = HARNESS_TEMPLATE.replace("{user_code}", &code);
tokio::fs::write(&source_path, &harnessed_source).await.map_err(|e| {
ExecutionError::ExecutionFailed(format!("failed to write source file: {e}"))
})?;
debug!(source_path = %source_path.display(), "wrote harnessed source");
let compile_result = self.compile(&source_path, &binary_path, &request).await?;
if let Some(result) = compile_result {
return Ok(result);
}
info!("compilation succeeded, executing binary");
let result = self.run_binary(&binary_path, &request, start).await;
drop(tmp_dir);
result
}
}
impl RustSandboxExecutor {
async fn compile(
&self,
source_path: &std::path::Path,
binary_path: &std::path::Path,
request: &ExecutionRequest,
) -> Result<Option<ExecutionResult>, ExecutionError> {
let serde_json_dep = self.find_serde_json_dep().await?;
let mut cmd = tokio::process::Command::new(&self.config.rustc_path);
cmd.arg(source_path).arg("-o").arg(binary_path).arg("--edition").arg("2021");
if let Some(dep_path) = &serde_json_dep {
cmd.arg("--extern").arg(format!("serde_json={}", dep_path.display()));
if let Some(parent) = dep_path.parent() {
cmd.arg("-L").arg(format!("dependency={}", parent.display()));
}
}
for flag in &self.config.rustc_flags {
cmd.arg(flag);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let compile_timeout = request.sandbox.timeout;
let compile_output = match tokio::time::timeout(compile_timeout, cmd.output()).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
return Err(ExecutionError::CompileFailed(format!("failed to invoke rustc: {e}")));
}
Err(_) => {
return Ok(Some(ExecutionResult {
status: ExecutionStatus::Timeout,
stdout: String::new(),
stderr: "compilation timed out".to_string(),
output: None,
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms: compile_timeout.as_millis() as u64,
metadata: None,
}));
}
};
if !compile_output.status.success() {
let stderr = String::from_utf8_lossy(&compile_output.stderr).to_string();
let (stderr, stderr_truncated) =
truncate_output(stderr, request.sandbox.max_stderr_bytes);
debug!(exit_code = compile_output.status.code(), "compilation failed");
return Ok(Some(ExecutionResult {
status: ExecutionStatus::CompileFailed,
stdout: String::new(),
stderr,
output: None,
exit_code: compile_output.status.code(),
stdout_truncated: false,
stderr_truncated,
duration_ms: 0, metadata: None,
}));
}
Ok(None)
}
async fn run_binary(
&self,
binary_path: &std::path::Path,
request: &ExecutionRequest,
start: Instant,
) -> Result<ExecutionResult, ExecutionError> {
let mut cmd = tokio::process::Command::new(binary_path);
for arg in &request.argv {
cmd.arg(arg);
}
cmd.stdin(std::process::Stdio::piped());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd.kill_on_drop(true);
let mut child = cmd
.spawn()
.map_err(|e| ExecutionError::ExecutionFailed(format!("failed to spawn binary: {e}")))?;
if let Some(ref input) = request.input {
if let Some(mut stdin) = child.stdin.take() {
let json_bytes = serde_json::to_vec(input).unwrap_or_default();
let _ = stdin.write_all(&json_bytes).await;
drop(stdin);
}
} else if let Some(ref raw_stdin) = request.stdin {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(raw_stdin).await;
drop(stdin);
}
} else {
drop(child.stdin.take());
}
let output =
match tokio::time::timeout(request.sandbox.timeout, child.wait_with_output()).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
return Err(ExecutionError::ExecutionFailed(format!(
"failed to wait for binary: {e}"
)));
}
Err(_) => {
warn!("execution timed out");
let duration_ms = start.elapsed().as_millis() as u64;
return Ok(ExecutionResult {
status: ExecutionStatus::Timeout,
stdout: String::new(),
stderr: String::new(),
output: None,
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms,
metadata: None,
});
}
};
let duration_ms = start.elapsed().as_millis() as u64;
let raw_stdout = String::from_utf8_lossy(&output.stdout).to_string();
let raw_stderr = String::from_utf8_lossy(&output.stderr).to_string();
let (stdout, stdout_truncated) =
truncate_output(raw_stdout, request.sandbox.max_stdout_bytes);
let (stderr, stderr_truncated) =
truncate_output(raw_stderr, request.sandbox.max_stderr_bytes);
let (structured_output, display_stdout) = extract_structured_output(&stdout);
let status = if output.status.success() {
ExecutionStatus::Success
} else {
ExecutionStatus::Failed
};
debug!(
exit_code = output.status.code(),
duration_ms,
has_structured_output = structured_output.is_some(),
"execution completed"
);
Ok(ExecutionResult {
status,
stdout: display_stdout,
stderr,
output: structured_output,
exit_code: output.status.code(),
stdout_truncated,
stderr_truncated,
duration_ms,
metadata: None,
})
}
async fn find_serde_json_dep(&self) -> Result<Option<PathBuf>, ExecutionError> {
if let Some(ref path) = self.config.serde_json_path {
if path.exists() {
return Ok(Some(path.clone()));
}
return Err(ExecutionError::ExecutionFailed(format!(
"configured serde_json path does not exist: {}",
path.display()
)));
}
let output = tokio::process::Command::new("cargo")
.args(["metadata", "--format-version=1", "--no-deps"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
if let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&output.stdout) {
if let Some(target_dir) = metadata["target_directory"].as_str() {
let deps_dir = PathBuf::from(target_dir).join("debug").join("deps");
if let Some(rlib) = find_rlib_in_dir(&deps_dir, "serde_json").await {
return Ok(Some(rlib));
}
}
}
}
}
Ok(None)
}
}
async fn find_rlib_in_dir(dir: &std::path::Path, crate_name: &str) -> Option<PathBuf> {
let prefix = format!("lib{crate_name}-");
let mut entries = match tokio::fs::read_dir(dir).await {
Ok(entries) => entries,
Err(_) => return None,
};
let mut rlibs = Vec::new();
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(&prefix) && name_str.ends_with(".rlib") {
if let Ok(metadata) = entry.metadata().await {
if let Ok(modified) = metadata.modified() {
rlibs.push((entry.path(), modified));
}
}
}
}
rlibs.sort_by_key(|&(_, modified)| std::cmp::Reverse(modified));
rlibs.into_iter().next().map(|(path, _)| path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capabilities_are_honest() {
let executor = RustSandboxExecutor::default();
let caps = executor.capabilities();
assert_eq!(caps.isolation, ExecutionIsolation::HostLocal);
assert!(caps.enforce_timeout);
assert!(caps.supports_structured_output);
assert!(!caps.enforce_network_policy);
assert!(!caps.enforce_filesystem_policy);
assert!(!caps.enforce_environment_policy);
}
#[test]
fn supports_only_rust() {
let executor = RustSandboxExecutor::default();
assert!(executor.supports_language(&ExecutionLanguage::Rust));
assert!(!executor.supports_language(&ExecutionLanguage::JavaScript));
assert!(!executor.supports_language(&ExecutionLanguage::Python));
assert!(!executor.supports_language(&ExecutionLanguage::Wasm));
assert!(!executor.supports_language(&ExecutionLanguage::Command));
}
#[test]
fn default_config() {
let config = RustSandboxConfig::default();
assert_eq!(config.rustc_path, "rustc");
assert!(config.rustc_flags.is_empty());
assert!(config.serde_json_path.is_none());
}
}