1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::process::Output;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ExecutionResult {
12 pub exit_code: i32,
14 pub stdout: String,
16 pub stderr: String,
18 pub execution_time_ms: u64,
20 pub timed_out: bool,
22 pub output_files: Vec<OutputFile>,
24}
25
26impl ExecutionResult {
27 pub fn success(&self) -> bool {
29 self.exit_code == 0 && !self.timed_out
30 }
31
32 pub fn output(&self) -> String {
34 if self.stderr.is_empty() {
35 self.stdout.clone()
36 } else if self.stdout.is_empty() {
37 self.stderr.clone()
38 } else {
39 format!("{}\n{}", self.stdout, self.stderr)
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct OutputFile {
47 pub path: String,
49 pub content: String,
51 pub is_binary: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SandboxConfig {
58 #[serde(with = "humantime_serde")]
60 pub timeout: Duration,
61 pub max_memory: usize,
63 pub max_cpu_time: u64,
65 pub working_dir: Option<String>,
67 pub env: HashMap<String, String>,
69 pub allow_network: bool,
71 pub allow_fs_write: bool,
73 pub allowed_read_paths: Vec<String>,
75 pub allowed_write_paths: Vec<String>,
77}
78
79impl Default for SandboxConfig {
80 fn default() -> Self {
81 Self {
82 timeout: Duration::from_secs(30),
83 max_memory: 256 * 1024 * 1024, max_cpu_time: 10,
85 working_dir: None,
86 env: HashMap::new(),
87 allow_network: false,
88 allow_fs_write: false,
89 allowed_read_paths: Vec::new(),
90 allowed_write_paths: Vec::new(),
91 }
92 }
93}
94
95impl SandboxConfig {
96 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn timeout(mut self, timeout: Duration) -> Self {
103 self.timeout = timeout;
104 self
105 }
106
107 pub fn max_memory(mut self, bytes: usize) -> Self {
109 self.max_memory = bytes;
110 self
111 }
112
113 pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
115 self.working_dir = Some(dir.into());
116 self
117 }
118
119 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
121 self.env.insert(key.into(), value.into());
122 self
123 }
124
125 pub fn allow_network(mut self, allow: bool) -> Self {
127 self.allow_network = allow;
128 self
129 }
130
131 pub fn allow_fs_write(mut self, allow: bool) -> Self {
133 self.allow_fs_write = allow;
134 self
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140pub enum Language {
141 Python,
142 JavaScript,
143 TypeScript,
144 Ruby,
145 Rust,
146 Go,
147 Shell,
148}
149
150impl Language {
151 pub fn extension(&self) -> &'static str {
153 match self {
154 Language::Python => "py",
155 Language::JavaScript => "js",
156 Language::TypeScript => "ts",
157 Language::Ruby => "rb",
158 Language::Rust => "rs",
159 Language::Go => "go",
160 Language::Shell => "sh",
161 }
162 }
163
164 pub fn command(&self) -> &'static str {
166 match self {
167 Language::Python => "python3",
168 Language::JavaScript => "node",
169 Language::TypeScript => "npx ts-node",
170 Language::Ruby => "ruby",
171 Language::Rust => "rustc",
172 Language::Go => "go run",
173 Language::Shell => "bash",
174 }
175 }
176
177 pub fn detect(code: &str) -> Option<Language> {
179 let code = code.trim();
180
181 if code.starts_with("#!/usr/bin/env python") || code.starts_with("#!/usr/bin/python") {
183 return Some(Language::Python);
184 }
185 if code.starts_with("#!/usr/bin/env node") || code.starts_with("#!/usr/bin/node") {
186 return Some(Language::JavaScript);
187 }
188 if code.starts_with("#!/bin/bash") || code.starts_with("#!/bin/sh") {
189 return Some(Language::Shell);
190 }
191
192 if code.contains("def ") && code.contains(":") && !code.contains("{") {
194 return Some(Language::Python);
195 }
196 if code.contains("import ") && code.contains("from ") && code.contains(":") {
197 return Some(Language::Python);
198 }
199 if code.contains("function ") || code.contains("const ") || code.contains("let ") {
200 if code.contains(": string") || code.contains(": number") || code.contains(": boolean")
201 {
202 return Some(Language::TypeScript);
203 }
204 return Some(Language::JavaScript);
205 }
206 if code.contains("fn ") && code.contains("->") {
207 return Some(Language::Rust);
208 }
209 if code.contains("func ") && code.contains("package ") {
210 return Some(Language::Go);
211 }
212
213 None
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SandboxError {
220 pub message: String,
222 pub kind: SandboxErrorKind,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227pub enum SandboxErrorKind {
228 Timeout,
230 MemoryLimit,
232 SecurityViolation,
234 UnsupportedLanguage,
236 ExecutionFailed,
238 ConfigError,
240}
241
242impl std::fmt::Display for SandboxError {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 write!(f, "{:?}: {}", self.kind, self.message)
245 }
246}
247
248impl std::error::Error for SandboxError {}
249
250impl SandboxError {
251 pub fn new(kind: SandboxErrorKind, message: impl Into<String>) -> Self {
252 Self {
253 kind,
254 message: message.into(),
255 }
256 }
257
258 pub fn timeout(message: impl Into<String>) -> Self {
259 Self::new(SandboxErrorKind::Timeout, message)
260 }
261
262 pub fn security(message: impl Into<String>) -> Self {
263 Self::new(SandboxErrorKind::SecurityViolation, message)
264 }
265
266 pub fn unsupported(message: impl Into<String>) -> Self {
267 Self::new(SandboxErrorKind::UnsupportedLanguage, message)
268 }
269
270 pub fn execution(message: impl Into<String>) -> Self {
271 Self::new(SandboxErrorKind::ExecutionFailed, message)
272 }
273}
274
275#[async_trait]
277pub trait Sandbox: Send + Sync {
278 async fn execute(
280 &self,
281 code: &str,
282 language: Language,
283 config: &SandboxConfig,
284 ) -> Result<ExecutionResult, SandboxError>;
285
286 fn supports_language(&self, language: Language) -> bool;
288
289 fn supported_languages(&self) -> Vec<Language>;
291}
292
293#[derive(Debug, Clone, Default)]
298pub struct ProcessSandbox {
299 pub temp_dir: Option<String>,
301}
302
303impl ProcessSandbox {
304 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn with_temp_dir(mut self, dir: impl Into<String>) -> Self {
311 self.temp_dir = Some(dir.into());
312 self
313 }
314
315 async fn run_command(
316 &self,
317 cmd: &str,
318 args: &[&str],
319 config: &SandboxConfig,
320 ) -> Result<Output, SandboxError> {
321 use std::process::Command;
322
323 let mut command = Command::new(cmd);
324 command.args(args);
325
326 if let Some(ref dir) = config.working_dir {
328 command.current_dir(dir);
329 }
330
331 command.env_clear();
333 for (key, value) in &config.env {
334 command.env(key, value);
335 }
336
337 command.env("PATH", "/usr/local/bin:/usr/bin:/bin");
339 command.env("HOME", "/tmp");
340
341 let output = command
342 .output()
343 .map_err(|e| SandboxError::execution(format!("Failed to execute: {}", e)))?;
344
345 Ok(output)
346 }
347}
348
349#[async_trait]
350impl Sandbox for ProcessSandbox {
351 async fn execute(
352 &self,
353 code: &str,
354 language: Language,
355 config: &SandboxConfig,
356 ) -> Result<ExecutionResult, SandboxError> {
357 use std::io::Write;
358 use std::time::Instant;
359
360 let temp_dir = self.temp_dir.as_deref().unwrap_or("/tmp");
362 let file_name = format!(
363 "{}/sandbox_code_{}.{}",
364 temp_dir,
365 std::process::id(),
366 language.extension()
367 );
368
369 let mut file = std::fs::File::create(&file_name)
371 .map_err(|e| SandboxError::execution(format!("Failed to create temp file: {}", e)))?;
372 file.write_all(code.as_bytes())
373 .map_err(|e| SandboxError::execution(format!("Failed to write code: {}", e)))?;
374
375 let start = Instant::now();
376
377 let output = match language {
379 Language::Python => self.run_command("python3", &[&file_name], config).await?,
380 Language::JavaScript => self.run_command("node", &[&file_name], config).await?,
381 Language::Shell => self.run_command("bash", &[&file_name], config).await?,
382 Language::Ruby => self.run_command("ruby", &[&file_name], config).await?,
383 _ => {
384 let _ = std::fs::remove_file(&file_name);
386 return Err(SandboxError::unsupported(format!(
387 "{:?} is not supported by ProcessSandbox",
388 language
389 )));
390 }
391 };
392
393 let execution_time = start.elapsed();
394
395 let _ = std::fs::remove_file(&file_name);
397
398 Ok(ExecutionResult {
399 exit_code: output.status.code().unwrap_or(-1),
400 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
401 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
402 execution_time_ms: execution_time.as_millis() as u64,
403 timed_out: false,
404 output_files: Vec::new(),
405 })
406 }
407
408 fn supports_language(&self, language: Language) -> bool {
409 matches!(
410 language,
411 Language::Python | Language::JavaScript | Language::Shell | Language::Ruby
412 )
413 }
414
415 fn supported_languages(&self) -> Vec<Language> {
416 vec![
417 Language::Python,
418 Language::JavaScript,
419 Language::Shell,
420 Language::Ruby,
421 ]
422 }
423}
424
425#[derive(Debug, Clone, Default)]
427pub struct MockSandbox {
428 pub result: Option<ExecutionResult>,
430}
431
432impl MockSandbox {
433 pub fn new() -> Self {
434 Self::default()
435 }
436
437 pub fn with_result(mut self, result: ExecutionResult) -> Self {
438 self.result = Some(result);
439 self
440 }
441}
442
443#[async_trait]
444impl Sandbox for MockSandbox {
445 async fn execute(
446 &self,
447 _code: &str,
448 _language: Language,
449 _config: &SandboxConfig,
450 ) -> Result<ExecutionResult, SandboxError> {
451 Ok(self.result.clone().unwrap_or(ExecutionResult {
452 exit_code: 0,
453 stdout: "Mock execution successful".to_string(),
454 stderr: String::new(),
455 execution_time_ms: 1,
456 timed_out: false,
457 output_files: Vec::new(),
458 }))
459 }
460
461 fn supports_language(&self, _language: Language) -> bool {
462 true
463 }
464
465 fn supported_languages(&self) -> Vec<Language> {
466 vec![
467 Language::Python,
468 Language::JavaScript,
469 Language::TypeScript,
470 Language::Ruby,
471 Language::Rust,
472 Language::Go,
473 Language::Shell,
474 ]
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_execution_result() {
484 let result = ExecutionResult {
485 exit_code: 0,
486 stdout: "Hello".to_string(),
487 stderr: String::new(),
488 execution_time_ms: 10,
489 timed_out: false,
490 output_files: Vec::new(),
491 };
492
493 assert!(result.success());
494 assert_eq!(result.output(), "Hello");
495 }
496
497 #[test]
498 fn test_execution_result_failed() {
499 let result = ExecutionResult {
500 exit_code: 1,
501 stdout: String::new(),
502 stderr: "Error".to_string(),
503 execution_time_ms: 10,
504 timed_out: false,
505 output_files: Vec::new(),
506 };
507
508 assert!(!result.success());
509 assert_eq!(result.output(), "Error");
510 }
511
512 #[test]
513 fn test_sandbox_config() {
514 let config = SandboxConfig::new()
515 .timeout(Duration::from_secs(60))
516 .max_memory(512 * 1024 * 1024)
517 .working_dir("/tmp/sandbox")
518 .env("FOO", "bar")
519 .allow_network(true);
520
521 assert_eq!(config.timeout, Duration::from_secs(60));
522 assert_eq!(config.max_memory, 512 * 1024 * 1024);
523 assert_eq!(config.working_dir, Some("/tmp/sandbox".to_string()));
524 assert_eq!(config.env.get("FOO"), Some(&"bar".to_string()));
525 assert!(config.allow_network);
526 }
527
528 #[test]
529 fn test_language_extension() {
530 assert_eq!(Language::Python.extension(), "py");
531 assert_eq!(Language::JavaScript.extension(), "js");
532 assert_eq!(Language::Rust.extension(), "rs");
533 }
534
535 #[test]
536 fn test_language_detection() {
537 assert_eq!(
538 Language::detect("def foo():\n pass"),
539 Some(Language::Python)
540 );
541 assert_eq!(
542 Language::detect("function foo() { }"),
543 Some(Language::JavaScript)
544 );
545 assert_eq!(
546 Language::detect("fn main() -> () { }"),
547 Some(Language::Rust)
548 );
549 assert_eq!(
550 Language::detect("#!/bin/bash\necho hello"),
551 Some(Language::Shell)
552 );
553 }
554
555 #[tokio::test]
556 async fn test_mock_sandbox() {
557 let sandbox = MockSandbox::new().with_result(ExecutionResult {
558 exit_code: 0,
559 stdout: "42".to_string(),
560 stderr: String::new(),
561 execution_time_ms: 5,
562 timed_out: false,
563 output_files: Vec::new(),
564 });
565
566 let config = SandboxConfig::default();
567 let result = sandbox
568 .execute("print(42)", Language::Python, &config)
569 .await
570 .unwrap();
571
572 assert!(result.success());
573 assert_eq!(result.stdout, "42");
574 }
575
576 #[test]
577 fn test_sandbox_error() {
578 let err = SandboxError::timeout("Execution took too long");
579 assert_eq!(err.kind, SandboxErrorKind::Timeout);
580 assert!(err.to_string().contains("Execution took too long"));
581 }
582}