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