use mlua::prelude::*;
fn registry_key(name: &str) -> String {
format!("gizmo_api_raw_{name}")
}
pub fn register_protected(
lua: &Lua,
name: &str,
build: impl FnOnce(&LuaTable) -> Result<(), LuaError>,
) -> Result<(), LuaError> {
let real = lua.create_table()?;
lua.globals().set(name, real.clone())?;
build(&real)?;
lua.set_named_registry_value(®istry_key(name), real.clone())?;
let proxy = lua.create_table()?;
let meta = lua.create_table()?;
meta.set("__index", real)?;
let owner = name.to_string();
meta.set(
"__newindex",
lua.create_function(move |_, (_, key, _): (LuaTable, LuaValue, LuaValue)| {
let key = match &key {
LuaValue::String(s) => s.to_str().unwrap_or("?").to_string(),
other => format!("{other:?}"),
};
Err::<(), _>(LuaError::RuntimeError(format!(
"`{owner}` is a read-only engine API: assigning to `{owner}.{key}` would change it \
for every script in the scene, not just this one"
)))
})?,
)?;
meta.set("__metatable", false)?;
proxy.set_metatable(Some(meta));
lua.globals().set(name, proxy)?;
Ok(())
}
pub fn raw<'lua>(lua: &'lua Lua, name: &str) -> Result<LuaTable<'lua>, LuaError> {
lua.named_registry_value(®istry_key(name))
}
#[cfg(test)]
mod tests {
use super::*;
fn engine_api(lua: &Lua) {
register_protected(lua, "demo", |t| {
t.set("value", 1)?;
lua.load("function demo.helper() return demo.value end").exec()
})
.unwrap();
}
#[test]
fn a_script_cannot_replace_an_api_function() {
let lua = Lua::new();
engine_api(&lua);
let err = lua
.load("demo.helper = function() return 99 end")
.exec()
.expect_err("overwriting an API function must fail");
assert!(format!("{err}").contains("read-only"), "unexpected error: {err}");
let got: i64 = lua.load("return demo.helper()").eval().unwrap();
assert_eq!(got, 1);
}
#[test]
fn an_existing_key_is_protected_too() {
let lua = Lua::new();
engine_api(&lua);
assert!(lua.load("demo.value = 42").exec().is_err(), "existing keys must be protected");
let got: i64 = lua.load("return demo.value").eval().unwrap();
assert_eq!(got, 1, "the write must not have landed");
}
#[test]
fn the_real_table_is_not_reachable_from_lua() {
let lua = Lua::new();
engine_api(&lua);
let meta: LuaValue = lua.load("return getmetatable(demo)").eval().unwrap();
assert_eq!(meta, LuaValue::Boolean(false), "the metatable must not be readable");
let found: bool = lua
.load("for k, v in pairs(_G) do if v ~= demo and type(v) == 'table' and rawget(v, 'value') == 1 then return true end end return false")
.eval()
.unwrap();
assert!(!found, "the real table is reachable through some global");
}
#[test]
fn the_engine_still_writes_through_the_registry() {
let lua = Lua::new();
engine_api(&lua);
raw(&lua, "demo").unwrap().raw_set("value", 7).unwrap();
let got: i64 = lua.load("return demo.value").eval().unwrap();
assert_eq!(got, 7, "a Rust-side raw_set must be visible through the proxy");
}
}