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