1use thiserror::Error;
4
5#[derive(Debug, Clone)]
7pub struct ExecLimits {
8 pub instruction_limit: u64,
10 pub wall_clock_ms: Option<u64>,
12 pub memory_limit_bytes: u64,
14}
15
16impl Default for ExecLimits {
17 fn default() -> Self {
18 Self {
19 instruction_limit: 1_000_000,
20 wall_clock_ms: Some(300_000), memory_limit_bytes: 128 * 1024 * 1024, }
23 }
24}
25
26#[derive(Error, Debug)]
28pub enum ScriptError {
29 #[error("syntax error: {0}")]
30 Syntax(String),
31
32 #[error("sandbox violation: attempted to access forbidden global `{name}`")]
33 SandboxViolation { name: String },
34
35 #[error("instruction limit exceeded (limit: {0})")]
36 InstructionLimitExceeded(u64),
37
38 #[error("wall-clock timeout ({0}ms)")]
39 WallClockTimeout(u64),
40
41 #[error("memory limit exceeded ({0} bytes)")]
42 MemoryLimitExceeded(u64),
43
44 #[error("agent error: {0}")]
45 AgentError(String),
46
47 #[error("serialization error: {0}")]
48 SerdeError(String),
49
50 #[error("internal error: {0}")]
51 Internal(String),
52
53 #[error("script is missing a `function main() ... end` entry point")]
54 MissingMain,
55}
56
57impl From<mlua::Error> for ScriptError {
58 fn from(e: mlua::Error) -> Self {
59 use mlua::Error::*;
60 match e {
61 SyntaxError { message, .. } => ScriptError::Syntax(message),
62 RuntimeError(msg) => {
63 if msg.contains("forbidden") || msg.contains("not allowed") {
65 ScriptError::SandboxViolation { name: msg.clone() }
66 } else if msg.contains("instruction limit") {
67 ScriptError::InstructionLimitExceeded(0)
68 } else if msg.contains("timeout") {
69 ScriptError::WallClockTimeout(0)
70 } else {
71 ScriptError::AgentError(msg)
72 }
73 }
74 _ => ScriptError::Internal(e.to_string()),
75 }
76 }
77}
78
79impl From<serde_json::Error> for ScriptError {
80 fn from(e: serde_json::Error) -> Self {
81 ScriptError::SerdeError(e.to_string())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
93 fn exec_limits_default() {
94 let limits = ExecLimits::default();
95 assert_eq!(limits.instruction_limit, 1_000_000);
96 assert_eq!(limits.wall_clock_ms, Some(300_000));
97 assert_eq!(limits.memory_limit_bytes, 128 * 1024 * 1024);
98 }
99
100 #[test]
101 fn exec_limits_debug_and_clone() {
102 let a = ExecLimits::default();
103 let b = a.clone();
104 assert_eq!(a.instruction_limit, b.instruction_limit);
105 let _ = format!("{:?}", a); }
107
108 #[test]
112 fn display_syntax() {
113 let err = ScriptError::Syntax("unexpected symbol".into());
114 assert_eq!(err.to_string(), "syntax error: unexpected symbol");
115 }
116
117 #[test]
118 fn display_sandbox_violation() {
119 let err = ScriptError::SandboxViolation { name: "os".into() };
120 assert_eq!(
121 err.to_string(),
122 "sandbox violation: attempted to access forbidden global `os`"
123 );
124 }
125
126 #[test]
127 fn display_instruction_limit() {
128 let err = ScriptError::InstructionLimitExceeded(50_000);
129 assert_eq!(err.to_string(), "instruction limit exceeded (limit: 50000)");
130 }
131
132 #[test]
133 fn display_wall_clock_timeout() {
134 let err = ScriptError::WallClockTimeout(300_000);
135 assert_eq!(err.to_string(), "wall-clock timeout (300000ms)");
136 }
137
138 #[test]
139 fn display_memory_limit() {
140 let err = ScriptError::MemoryLimitExceeded(134_217_728);
141 assert_eq!(err.to_string(), "memory limit exceeded (134217728 bytes)");
142 }
143
144 #[test]
145 fn display_agent_error() {
146 let err = ScriptError::AgentError("something went wrong".into());
147 assert_eq!(err.to_string(), "agent error: something went wrong");
148 }
149
150 #[test]
151 fn display_serde_error() {
152 let err = ScriptError::SerdeError("invalid type".into());
153 assert_eq!(err.to_string(), "serialization error: invalid type");
154 }
155
156 #[test]
157 fn display_internal() {
158 let err = ScriptError::Internal("unexpected state".into());
159 assert_eq!(err.to_string(), "internal error: unexpected state");
160 }
161
162 #[test]
166 fn debug_format() {
167 let variants: Vec<ScriptError> = vec![
168 ScriptError::Syntax("x".into()),
169 ScriptError::SandboxViolation { name: "x".into() },
170 ScriptError::InstructionLimitExceeded(1),
171 ScriptError::WallClockTimeout(1),
172 ScriptError::MemoryLimitExceeded(1),
173 ScriptError::AgentError("x".into()),
174 ScriptError::SerdeError("x".into()),
175 ScriptError::Internal("x".into()),
176 ];
177 for v in &variants {
178 let _ = format!("{:?}", v);
179 }
180 }
181
182 #[test]
186 fn from_mlua_syntax_error() {
187 let e = mlua::Error::SyntaxError {
188 message: "unexpected symbol near ')'".into(),
189 incomplete_input: false,
190 };
191 match ScriptError::from(e) {
192 ScriptError::Syntax(msg) => assert_eq!(msg, "unexpected symbol near ')'"),
193 other => panic!("expected Syntax, got {other:?}"),
194 }
195 }
196
197 #[test]
198 fn from_mlua_runtime_forbidden() {
199 let e = mlua::Error::RuntimeError("forbidden global 'os'".into());
200 match ScriptError::from(e) {
201 ScriptError::SandboxViolation { name } => {
202 assert_eq!(name, "forbidden global 'os'");
203 }
204 other => panic!("expected SandboxViolation, got {other:?}"),
205 }
206 }
207
208 #[test]
209 fn from_mlua_runtime_not_allowed() {
210 let e = mlua::Error::RuntimeError("this function is not allowed".into());
211 match ScriptError::from(e) {
212 ScriptError::SandboxViolation { name } => {
213 assert_eq!(name, "this function is not allowed");
214 }
215 other => panic!("expected SandboxViolation, got {other:?}"),
216 }
217 }
218
219 #[test]
220 fn from_mlua_runtime_instruction_limit() {
221 let e = mlua::Error::RuntimeError("instruction limit exceeded".into());
222 match ScriptError::from(e) {
223 ScriptError::InstructionLimitExceeded(limit) => assert_eq!(limit, 0),
224 other => panic!("expected InstructionLimitExceeded, got {other:?}"),
225 }
226 }
227
228 #[test]
229 fn from_mlua_runtime_timeout() {
230 let e = mlua::Error::RuntimeError("timeout reached".into());
231 match ScriptError::from(e) {
232 ScriptError::WallClockTimeout(ms) => assert_eq!(ms, 0),
233 other => panic!("expected WallClockTimeout, got {other:?}"),
234 }
235 }
236
237 #[test]
238 fn from_mlua_runtime_generic_agent_error() {
239 let e = mlua::Error::RuntimeError("some random Lua error".into());
240 match ScriptError::from(e) {
241 ScriptError::AgentError(msg) => assert_eq!(msg, "some random Lua error"),
242 other => panic!("expected AgentError, got {other:?}"),
243 }
244 }
245
246 #[test]
247 fn from_mlua_non_runtime_internal() {
248 let e = mlua::Error::MemoryError("OOM".into());
250 match ScriptError::from(e) {
251 ScriptError::Internal(msg) => assert!(msg.contains("memory error")),
252 other => panic!("expected Internal, got {other:?}"),
253 }
254 }
255
256 #[test]
257 fn from_mlua_safety_error_internal() {
258 let e = mlua::Error::SafetyError("unsafe operation".into());
259 match ScriptError::from(e) {
260 ScriptError::Internal(msg) => assert!(msg.contains("safety error")),
261 other => panic!("expected Internal, got {other:?}"),
262 }
263 }
264
265 #[test]
269 fn from_serde_json_error() {
270 let invalid: serde_json::Error = serde_json::from_str::<()>("!").unwrap_err();
271 match ScriptError::from(invalid) {
272 ScriptError::SerdeError(msg) => assert!(!msg.is_empty()),
273 other => panic!("expected SerdeError, got {other:?}"),
274 }
275 }
276}