outl_exec/runtimes/
lua.rs1use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12use mlua::{Lua, MultiValue, Value, Variadic};
13
14use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
15
16pub struct LuaRuntime;
18
19impl Runtime for LuaRuntime {
20 fn language(&self) -> &'static str {
21 "lua"
22 }
23
24 fn execute(&self, source: &str, _ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
25 let start = Instant::now();
26 let lua = Lua::new();
27 let buffer: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
28
29 let sink = buffer.clone();
31 let print_fn = lua
32 .create_function(move |_, args: Variadic<Value>| {
33 let mut s = sink.lock().unwrap();
34 for (i, v) in args.iter().enumerate() {
35 if i > 0 {
36 s.push('\t');
37 }
38 s.push_str(&lua_value_tostring(v));
39 }
40 s.push('\n');
41 Ok(())
42 })
43 .map_err(|e| ExecError::Sandbox(format!("install print: {e}")))?;
44 lua.globals()
45 .set("print", print_fn)
46 .map_err(|e| ExecError::Sandbox(format!("set global print: {e}")))?;
47
48 match lua.load(source).eval::<MultiValue>() {
49 Ok(values) => {
50 let mut stdout = buffer.lock().unwrap().clone();
51 if stdout.is_empty() && !values.is_empty() {
52 if let Some(last) = values.iter().last() {
55 stdout.push_str(&lua_value_tostring(last));
56 }
57 }
58 Ok(ExecOutput {
59 stdout,
60 stderr: String::new(),
61 duration: start.elapsed(),
62 exit: ExitStatus::Ok,
63 format: OutputFormat::Text,
64 })
65 }
66 Err(e) => Ok(ExecOutput {
67 stdout: buffer.lock().unwrap().clone(),
68 stderr: e.to_string(),
69 duration: start.elapsed(),
70 exit: ExitStatus::Trap("lua-error".into()),
71 format: OutputFormat::Text,
72 }),
73 }
74 }
75}
76
77fn lua_value_tostring(v: &Value) -> String {
78 match v {
79 Value::Nil => "nil".into(),
80 Value::Boolean(b) => b.to_string(),
81 Value::Integer(i) => i.to_string(),
82 Value::Number(n) => n.to_string(),
83 Value::String(s) => s.to_str().map(|s| s.to_string()).unwrap_or_default(),
84 other => format!("{other:?}"),
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn run(src: &str) -> String {
93 LuaRuntime
94 .execute(src, &ExecContext::default())
95 .unwrap()
96 .stdout
97 }
98
99 #[test]
100 fn print_writes_stdout() {
101 assert_eq!(run("print(1 + 2)"), "3\n");
102 }
103
104 #[test]
105 fn return_value_auto_printed() {
106 assert_eq!(run("return 6 * 7"), "42");
107 }
108
109 #[test]
110 fn string_concat() {
111 assert_eq!(run("print('hello ' .. 'world')"), "hello world\n");
112 }
113
114 #[test]
115 fn tables_and_loops() {
116 let out = run("local s=0; for i=1,5 do s=s+i end; print(s)");
117 assert_eq!(out, "15\n");
118 }
119
120 #[test]
121 fn syntax_error_returns_trap() {
122 let out = LuaRuntime
123 .execute("function (", &ExecContext::default())
124 .unwrap();
125 assert!(matches!(out.exit, ExitStatus::Trap(_)));
126 }
127}