assay/lua/builtins/
readonly.rs1use mlua::{Lua, MultiValue, Table, Value};
10
11use super::gated::{BLOCKED_FUNCTIONS, BLOCKED_TABLES, is_gated_http_verb};
12
13pub fn apply(lua: &Lua) -> mlua::Result<()> {
14 for path in BLOCKED_FUNCTIONS {
15 block_function(lua, path)?;
16 }
17 for name in BLOCKED_TABLES {
18 block_table(lua, name)?;
19 }
20 guard_http_client_request(lua)?;
21 guard_io_open(lua)?;
22 guard_io_output(lua)?;
23 Ok(())
24}
25
26fn blocked(name: &str) -> mlua::Error {
27 mlua::Error::runtime(format!(
28 "readonly: {name} blocked (write operations are disabled in read-only mode)"
29 ))
30}
31
32fn blocked_stub(lua: &Lua, name: &str) -> mlua::Result<mlua::Function> {
33 let name = name.to_string();
34 lua.create_function(move |_, _args: MultiValue| -> mlua::Result<Value> { Err(blocked(&name)) })
35}
36
37fn block_function(lua: &Lua, path: &str) -> mlua::Result<()> {
41 let Some((table_name, fn_name)) = path.split_once('.') else {
42 return Ok(());
43 };
44 let Some(table) = lua.globals().get::<Option<Table>>(table_name)? else {
45 return Ok(());
46 };
47 if table.get::<Value>(fn_name)?.is_function() {
48 table.set(fn_name, blocked_stub(lua, path)?)?;
49 }
50 Ok(())
51}
52
53fn block_table(lua: &Lua, name: &str) -> mlua::Result<()> {
55 let Some(table) = lua.globals().get::<Option<Table>>(name)? else {
56 return Ok(());
57 };
58 for pair in table.clone().pairs::<Value, Value>() {
59 let (key, value) = pair?;
60 if let (Value::String(key_str), true) = (&key, value.is_function()) {
61 let label = format!("{name}.{}", key_str.to_str()?);
62 table.set(key, blocked_stub(lua, &label)?)?;
63 }
64 }
65 Ok(())
66}
67
68fn guard_http_client_request(lua: &Lua) -> mlua::Result<()> {
73 let Some(http) = lua.globals().get::<Option<Table>>("http")? else {
74 return Ok(());
75 };
76 let Some(inner) = http.get::<Option<mlua::Function>>("_client_request")? else {
77 return Ok(());
78 };
79 let wrapper = lua.create_async_function(move |_, args: MultiValue| {
80 let inner = inner.clone();
81 async move {
82 let method = match args.iter().nth(1) {
83 Some(Value::String(s)) => Some(s.to_str()?.to_string()),
84 _ => None,
85 };
86 if let Some(method) = method
87 && is_gated_http_verb(&method)
88 {
89 return Err(blocked(&format!("http.{method}")));
90 }
91 inner.call_async::<MultiValue>(args).await
92 }
93 })?;
94 http.set("_client_request", wrapper)?;
95 Ok(())
96}
97
98fn guard_io_open(lua: &Lua) -> mlua::Result<()> {
100 let Some(io_table) = lua.globals().get::<Option<Table>>("io")? else {
101 return Ok(());
102 };
103 let Some(inner) = io_table.get::<Option<mlua::Function>>("open")? else {
104 return Ok(());
105 };
106 let wrapper = lua.create_function(move |_, args: MultiValue| {
107 let mode = match args.iter().nth(1) {
108 Some(Value::String(s)) => s.to_str()?.to_string(),
109 _ => "r".to_string(),
110 };
111 if mode.contains('w') || mode.contains('a') || mode.contains('+') {
112 return Err(blocked("io.open"));
113 }
114 inner.call::<MultiValue>(args)
115 })?;
116 io_table.set("open", wrapper)?;
117 Ok(())
118}
119
120fn guard_io_output(lua: &Lua) -> mlua::Result<()> {
123 let Some(io_table) = lua.globals().get::<Option<Table>>("io")? else {
124 return Ok(());
125 };
126 let Some(inner) = io_table.get::<Option<mlua::Function>>("output")? else {
127 return Ok(());
128 };
129 let wrapper = lua.create_function(move |_, args: MultiValue| {
130 if !args.is_empty() {
131 return Err(blocked("io.output"));
132 }
133 inner.call::<MultiValue>(args)
134 })?;
135 io_table.set("output", wrapper)?;
136 Ok(())
137}