Skip to main content

brainwires_code_interpreters/
types.rs

1//! Common types for code execution
2
3use serde::{Deserialize, Serialize};
4
5/// Maximum string length for the relaxed execution limits profile (100MB).
6const RELAXED_MAX_STRING_LENGTH: usize = 104_857_600;
7
8/// Supported programming languages
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Language {
12    /// Rhai - Native Rust scripting language (fastest, lightweight)
13    Rhai,
14    /// Lua 5.4 - Small, fast scripting language
15    Lua,
16    /// JavaScript - ECMAScript via Boa engine
17    JavaScript,
18    /// Python - CPython 3.12 compatible via RustPython
19    Python,
20}
21
22impl Language {
23    /// Get the language name as a string
24    pub fn as_str(&self) -> &'static str {
25        match self {
26            Language::Rhai => "rhai",
27            Language::Lua => "lua",
28            Language::JavaScript => "javascript",
29            Language::Python => "python",
30        }
31    }
32
33    /// Parse a language from string (case-insensitive)
34    pub fn parse(s: &str) -> Option<Self> {
35        match s.to_lowercase().as_str() {
36            "rhai" => Some(Language::Rhai),
37            "lua" => Some(Language::Lua),
38            "javascript" | "js" => Some(Language::JavaScript),
39            "python" | "py" => Some(Language::Python),
40            _ => None,
41        }
42    }
43
44    /// Get the typical file extension for this language
45    pub fn extension(&self) -> &'static str {
46        match self {
47            Language::Rhai => "rhai",
48            Language::Lua => "lua",
49            Language::JavaScript => "js",
50            Language::Python => "py",
51        }
52    }
53}
54
55impl std::fmt::Display for Language {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.as_str())
58    }
59}
60
61/// Request to execute code
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ExecutionRequest {
64    /// Programming language to use
65    pub language: Language,
66
67    /// Source code to execute
68    pub code: String,
69
70    /// Standard input to provide (optional)
71    #[serde(default)]
72    pub stdin: Option<String>,
73
74    /// Execution timeout in milliseconds (default: 30000)
75    #[serde(default = "default_timeout_ms")]
76    pub timeout_ms: u64,
77
78    /// Memory limit in MB (default: 256)
79    #[serde(default = "default_memory_mb")]
80    pub memory_limit_mb: u32,
81
82    /// Context variables to inject as globals (optional)
83    #[serde(default)]
84    pub context: Option<serde_json::Value>,
85
86    /// Execution limits profile (optional, overrides individual limits)
87    #[serde(default)]
88    pub limits: Option<ExecutionLimits>,
89}
90
91fn default_timeout_ms() -> u64 {
92    30_000
93}
94
95fn default_memory_mb() -> u32 {
96    256
97}
98
99impl Default for ExecutionRequest {
100    fn default() -> Self {
101        Self {
102            language: Language::Rhai,
103            code: String::new(),
104            stdin: None,
105            timeout_ms: default_timeout_ms(),
106            memory_limit_mb: default_memory_mb(),
107            context: None,
108            limits: None,
109        }
110    }
111}
112
113/// Result of code execution
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ExecutionResult {
116    /// Whether execution completed successfully
117    pub success: bool,
118
119    /// Standard output from the program
120    pub stdout: String,
121
122    /// Standard error from the program
123    pub stderr: String,
124
125    /// Return value of the code (if any), serialized as JSON
126    #[serde(default)]
127    pub result: Option<serde_json::Value>,
128
129    /// Error message if execution failed
130    #[serde(default)]
131    pub error: Option<String>,
132
133    /// Execution time in milliseconds
134    pub timing_ms: u64,
135
136    /// Memory used in bytes (if available)
137    #[serde(default)]
138    pub memory_used_bytes: Option<u64>,
139
140    /// Number of operations executed (if tracked)
141    #[serde(default)]
142    pub operations_count: Option<u64>,
143}
144
145impl ExecutionResult {
146    /// Create a successful result
147    pub fn success(stdout: String, result: Option<serde_json::Value>, timing_ms: u64) -> Self {
148        Self {
149            success: true,
150            stdout,
151            stderr: String::new(),
152            result,
153            error: None,
154            timing_ms,
155            memory_used_bytes: None,
156            operations_count: None,
157        }
158    }
159
160    /// Create a failed result
161    pub fn error(error: String, timing_ms: u64) -> Self {
162        Self {
163            success: false,
164            stdout: String::new(),
165            stderr: String::new(),
166            result: None,
167            error: Some(error),
168            timing_ms,
169            memory_used_bytes: None,
170            operations_count: None,
171        }
172    }
173
174    /// Create a failed result with captured output
175    pub fn error_with_output(
176        error: String,
177        stdout: String,
178        stderr: String,
179        timing_ms: u64,
180    ) -> Self {
181        Self {
182            success: false,
183            stdout,
184            stderr,
185            result: None,
186            error: Some(error),
187            timing_ms,
188            memory_used_bytes: None,
189            operations_count: None,
190        }
191    }
192}
193
194impl Default for ExecutionResult {
195    fn default() -> Self {
196        Self::error("No execution performed".to_string(), 0)
197    }
198}
199
200/// Execution limits for sandboxing
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ExecutionLimits {
203    /// Maximum execution time in milliseconds
204    #[serde(default = "ExecutionLimits::default_timeout_ms")]
205    pub max_timeout_ms: u64,
206
207    /// Maximum memory usage in MB
208    #[serde(default = "ExecutionLimits::default_memory_mb")]
209    pub max_memory_mb: u32,
210
211    /// Maximum output size in bytes
212    #[serde(default = "ExecutionLimits::default_output_bytes")]
213    pub max_output_bytes: usize,
214
215    /// Maximum number of operations (for loop prevention)
216    #[serde(default = "ExecutionLimits::default_operations")]
217    pub max_operations: u64,
218
219    /// Maximum call stack depth
220    #[serde(default = "ExecutionLimits::default_call_depth")]
221    pub max_call_depth: u32,
222
223    /// Maximum string length
224    #[serde(default = "ExecutionLimits::default_string_length")]
225    pub max_string_length: usize,
226
227    /// Maximum array/list length
228    #[serde(default = "ExecutionLimits::default_array_length")]
229    pub max_array_length: usize,
230
231    /// Maximum map/dict entries
232    #[serde(default = "ExecutionLimits::default_map_size")]
233    pub max_map_size: usize,
234}
235
236impl ExecutionLimits {
237    fn default_timeout_ms() -> u64 {
238        30_000
239    }
240    fn default_memory_mb() -> u32 {
241        256
242    }
243    fn default_output_bytes() -> usize {
244        1_048_576 // 1MB
245    }
246    fn default_operations() -> u64 {
247        1_000_000
248    }
249    fn default_call_depth() -> u32 {
250        64
251    }
252    fn default_string_length() -> usize {
253        10_485_760 // 10MB
254    }
255    fn default_array_length() -> usize {
256        100_000
257    }
258    fn default_map_size() -> usize {
259        10_000
260    }
261
262    /// Create strict limits for untrusted code
263    pub fn strict() -> Self {
264        Self {
265            max_timeout_ms: 5_000,
266            max_memory_mb: 64,
267            max_output_bytes: 65_536, // 64KB
268            max_operations: 100_000,
269            max_call_depth: 32,
270            max_string_length: 1_048_576, // 1MB
271            max_array_length: 10_000,
272            max_map_size: 1_000,
273        }
274    }
275
276    /// Create relaxed limits for trusted code
277    pub fn relaxed() -> Self {
278        Self {
279            max_timeout_ms: 120_000, // 2 minutes
280            max_memory_mb: 512,
281            max_output_bytes: 10_485_760, // 10MB
282            max_operations: 10_000_000,
283            max_call_depth: 128,
284            max_string_length: RELAXED_MAX_STRING_LENGTH,
285            max_array_length: 1_000_000,
286            max_map_size: 100_000,
287        }
288    }
289}
290
291impl Default for ExecutionLimits {
292    fn default() -> Self {
293        Self {
294            max_timeout_ms: Self::default_timeout_ms(),
295            max_memory_mb: Self::default_memory_mb(),
296            max_output_bytes: Self::default_output_bytes(),
297            max_operations: Self::default_operations(),
298            max_call_depth: Self::default_call_depth(),
299            max_string_length: Self::default_string_length(),
300            max_array_length: Self::default_array_length(),
301            max_map_size: Self::default_map_size(),
302        }
303    }
304}
305
306/// Error types for code execution
307#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
308pub enum ExecutionError {
309    /// The requested language is not supported or not enabled.
310    #[error("Language '{0}' is not supported or not enabled")]
311    UnsupportedLanguage(String),
312
313    /// Execution timed out after the given number of milliseconds.
314    #[error("Execution timed out after {0}ms")]
315    Timeout(u64),
316
317    /// Memory limit exceeded (in MB).
318    #[error("Memory limit exceeded: {0}MB")]
319    MemoryLimitExceeded(u32),
320
321    /// Operation limit exceeded.
322    #[error("Operation limit exceeded: {0} operations")]
323    OperationLimitExceeded(u64),
324
325    /// Output exceeded the maximum size (in bytes).
326    #[error("Output too large: {0} bytes")]
327    OutputTooLarge(usize),
328
329    /// Syntax error in the submitted code.
330    #[error("Syntax error: {0}")]
331    SyntaxError(String),
332
333    /// Runtime error during execution.
334    #[error("Runtime error: {0}")]
335    RuntimeError(String),
336
337    /// Internal executor error.
338    #[error("Internal error: {0}")]
339    InternalError(String),
340}
341
342impl ExecutionError {
343    /// Convert this error into an [`ExecutionResult`] with the given timing.
344    pub fn to_result(&self, timing_ms: u64) -> ExecutionResult {
345        ExecutionResult::error(self.to_string(), timing_ms)
346    }
347}
348
349/// Sandbox profile presets
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[derive(Default)]
352pub enum SandboxProfile {
353    /// Minimal - No I/O, basic math only
354    Minimal,
355    /// Standard - Console output, JSON, basic stdlib
356    #[default]
357    Standard,
358    /// Extended - More stdlib, regex, datetime
359    Extended,
360}
361
362impl SandboxProfile {
363    /// Get allowed modules for this profile
364    pub fn allowed_modules(&self) -> Vec<&'static str> {
365        match self {
366            SandboxProfile::Minimal => vec!["math"],
367            SandboxProfile::Standard => vec!["math", "json", "string", "array", "print"],
368            SandboxProfile::Extended => vec![
369                "math", "json", "string", "array", "print", "datetime", "regex", "base64",
370            ],
371        }
372    }
373}
374
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_language_parsing() {
382        assert_eq!(Language::parse("python"), Some(Language::Python));
383        assert_eq!(Language::parse("py"), Some(Language::Python));
384        assert_eq!(Language::parse("JAVASCRIPT"), Some(Language::JavaScript));
385        assert_eq!(Language::parse("js"), Some(Language::JavaScript));
386        assert_eq!(Language::parse("lua"), Some(Language::Lua));
387        assert_eq!(Language::parse("rhai"), Some(Language::Rhai));
388        assert_eq!(Language::parse("unknown"), None);
389    }
390
391    #[test]
392    fn test_execution_limits_profiles() {
393        let strict = ExecutionLimits::strict();
394        let relaxed = ExecutionLimits::relaxed();
395
396        assert!(strict.max_timeout_ms < relaxed.max_timeout_ms);
397        assert!(strict.max_memory_mb < relaxed.max_memory_mb);
398        assert!(strict.max_operations < relaxed.max_operations);
399    }
400
401    #[test]
402    fn test_execution_result_creation() {
403        let success = ExecutionResult::success(
404            "Hello".to_string(),
405            Some(serde_json::json!(42)),
406            100,
407        );
408        assert!(success.success);
409        assert_eq!(success.stdout, "Hello");
410
411        let error = ExecutionResult::error("Failed".to_string(), 50);
412        assert!(!error.success);
413        assert_eq!(error.error, Some("Failed".to_string()));
414    }
415}