use serde::{Deserialize, Serialize};
const RELAXED_MAX_STRING_LENGTH: usize = 104_857_600;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
Rhai,
Lua,
JavaScript,
Python,
}
impl Language {
pub fn as_str(&self) -> &'static str {
match self {
Language::Rhai => "rhai",
Language::Lua => "lua",
Language::JavaScript => "javascript",
Language::Python => "python",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"rhai" => Some(Language::Rhai),
"lua" => Some(Language::Lua),
"javascript" | "js" => Some(Language::JavaScript),
"python" | "py" => Some(Language::Python),
_ => None,
}
}
pub fn extension(&self) -> &'static str {
match self {
Language::Rhai => "rhai",
Language::Lua => "lua",
Language::JavaScript => "js",
Language::Python => "py",
}
}
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionRequest {
pub language: Language,
pub code: String,
#[serde(default)]
pub stdin: Option<String>,
#[serde(default = "default_timeout_ms")]
pub timeout_ms: u64,
#[serde(default = "default_memory_mb")]
pub memory_limit_mb: u32,
#[serde(default)]
pub context: Option<serde_json::Value>,
#[serde(default)]
pub limits: Option<ExecutionLimits>,
}
fn default_timeout_ms() -> u64 {
30_000
}
fn default_memory_mb() -> u32 {
256
}
impl Default for ExecutionRequest {
fn default() -> Self {
Self {
language: Language::Rhai,
code: String::new(),
stdin: None,
timeout_ms: default_timeout_ms(),
memory_limit_mb: default_memory_mb(),
context: None,
limits: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
pub success: bool,
pub stdout: String,
pub stderr: String,
#[serde(default)]
pub result: Option<serde_json::Value>,
#[serde(default)]
pub error: Option<String>,
pub timing_ms: u64,
#[serde(default)]
pub memory_used_bytes: Option<u64>,
#[serde(default)]
pub operations_count: Option<u64>,
}
impl ExecutionResult {
pub fn success(stdout: String, result: Option<serde_json::Value>, timing_ms: u64) -> Self {
Self {
success: true,
stdout,
stderr: String::new(),
result,
error: None,
timing_ms,
memory_used_bytes: None,
operations_count: None,
}
}
pub fn error(error: String, timing_ms: u64) -> Self {
Self {
success: false,
stdout: String::new(),
stderr: String::new(),
result: None,
error: Some(error),
timing_ms,
memory_used_bytes: None,
operations_count: None,
}
}
pub fn error_with_output(
error: String,
stdout: String,
stderr: String,
timing_ms: u64,
) -> Self {
Self {
success: false,
stdout,
stderr,
result: None,
error: Some(error),
timing_ms,
memory_used_bytes: None,
operations_count: None,
}
}
}
impl Default for ExecutionResult {
fn default() -> Self {
Self::error("No execution performed".to_string(), 0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionLimits {
#[serde(default = "ExecutionLimits::default_timeout_ms")]
pub max_timeout_ms: u64,
#[serde(default = "ExecutionLimits::default_memory_mb")]
pub max_memory_mb: u32,
#[serde(default = "ExecutionLimits::default_output_bytes")]
pub max_output_bytes: usize,
#[serde(default = "ExecutionLimits::default_operations")]
pub max_operations: u64,
#[serde(default = "ExecutionLimits::default_call_depth")]
pub max_call_depth: u32,
#[serde(default = "ExecutionLimits::default_string_length")]
pub max_string_length: usize,
#[serde(default = "ExecutionLimits::default_array_length")]
pub max_array_length: usize,
#[serde(default = "ExecutionLimits::default_map_size")]
pub max_map_size: usize,
}
impl ExecutionLimits {
fn default_timeout_ms() -> u64 {
30_000
}
fn default_memory_mb() -> u32 {
256
}
fn default_output_bytes() -> usize {
1_048_576 }
fn default_operations() -> u64 {
1_000_000
}
fn default_call_depth() -> u32 {
64
}
fn default_string_length() -> usize {
10_485_760 }
fn default_array_length() -> usize {
100_000
}
fn default_map_size() -> usize {
10_000
}
pub fn strict() -> Self {
Self {
max_timeout_ms: 5_000,
max_memory_mb: 64,
max_output_bytes: 65_536, max_operations: 100_000,
max_call_depth: 32,
max_string_length: 1_048_576, max_array_length: 10_000,
max_map_size: 1_000,
}
}
pub fn relaxed() -> Self {
Self {
max_timeout_ms: 120_000, max_memory_mb: 512,
max_output_bytes: 10_485_760, max_operations: 10_000_000,
max_call_depth: 128,
max_string_length: RELAXED_MAX_STRING_LENGTH,
max_array_length: 1_000_000,
max_map_size: 100_000,
}
}
}
impl Default for ExecutionLimits {
fn default() -> Self {
Self {
max_timeout_ms: Self::default_timeout_ms(),
max_memory_mb: Self::default_memory_mb(),
max_output_bytes: Self::default_output_bytes(),
max_operations: Self::default_operations(),
max_call_depth: Self::default_call_depth(),
max_string_length: Self::default_string_length(),
max_array_length: Self::default_array_length(),
max_map_size: Self::default_map_size(),
}
}
}
#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
pub enum ExecutionError {
#[error("Language '{0}' is not supported or not enabled")]
UnsupportedLanguage(String),
#[error("Execution timed out after {0}ms")]
Timeout(u64),
#[error("Memory limit exceeded: {0}MB")]
MemoryLimitExceeded(u32),
#[error("Operation limit exceeded: {0} operations")]
OperationLimitExceeded(u64),
#[error("Output too large: {0} bytes")]
OutputTooLarge(usize),
#[error("Syntax error: {0}")]
SyntaxError(String),
#[error("Runtime error: {0}")]
RuntimeError(String),
#[error("Internal error: {0}")]
InternalError(String),
}
impl ExecutionError {
pub fn to_result(&self, timing_ms: u64) -> ExecutionResult {
ExecutionResult::error(self.to_string(), timing_ms)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SandboxProfile {
Minimal,
#[default]
Standard,
Extended,
}
impl SandboxProfile {
pub fn allowed_modules(&self) -> Vec<&'static str> {
match self {
SandboxProfile::Minimal => vec!["math"],
SandboxProfile::Standard => vec!["math", "json", "string", "array", "print"],
SandboxProfile::Extended => vec![
"math", "json", "string", "array", "print", "datetime", "regex", "base64",
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_language_parsing() {
assert_eq!(Language::parse("python"), Some(Language::Python));
assert_eq!(Language::parse("py"), Some(Language::Python));
assert_eq!(Language::parse("JAVASCRIPT"), Some(Language::JavaScript));
assert_eq!(Language::parse("js"), Some(Language::JavaScript));
assert_eq!(Language::parse("lua"), Some(Language::Lua));
assert_eq!(Language::parse("rhai"), Some(Language::Rhai));
assert_eq!(Language::parse("unknown"), None);
}
#[test]
fn test_execution_limits_profiles() {
let strict = ExecutionLimits::strict();
let relaxed = ExecutionLimits::relaxed();
assert!(strict.max_timeout_ms < relaxed.max_timeout_ms);
assert!(strict.max_memory_mb < relaxed.max_memory_mb);
assert!(strict.max_operations < relaxed.max_operations);
}
#[test]
fn test_execution_result_creation() {
let success =
ExecutionResult::success("Hello".to_string(), Some(serde_json::json!(42)), 100);
assert!(success.success);
assert_eq!(success.stdout, "Hello");
let error = ExecutionResult::error("Failed".to_string(), 50);
assert!(!error.success);
assert_eq!(error.error, Some("Failed".to_string()));
}
}