use super::{
AtomicU32, AtomicUsize, Error, LUA_LOG_CHARACTER_LIMIT, Lua, MultiValue, Observation, Observer,
Ordering, Result, Scope, StoreRef, Value, detail,
};
pub(crate) fn install_log<'scope, 'env: 'scope>(
lua: &Lua,
scope: &'scope Scope<'scope, 'env>,
execution: &'env str,
observer: &'env dyn Observer,
section: &'env str,
log_budget: &'env AtomicU32,
log_byte_budget: &'env AtomicUsize,
) -> Result<()> {
let log = scope
.create_function(move |_, arguments: MultiValue| {
if arguments.len() != 1 {
return Err(mlua::Error::external("log expects exactly one argument"));
}
if log_budget
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1))
.is_err()
{
return Err(mlua::Error::external(crate::error::lua_quota::LOG_EVENT));
}
let Some(Value::String(message)) = arguments.into_iter().next() else {
return Err(mlua::Error::external("log message must be a UTF-8 string"));
};
let message = message
.to_str()
.map_err(|_| mlua::Error::external("log message must be a UTF-8 string"))?;
if message.chars().count() > LUA_LOG_CHARACTER_LIMIT {
return Err(mlua::Error::external(
"log message must be at most 256 characters",
));
}
if message.chars().any(is_log_line_break_or_control) {
return Err(mlua::Error::external(
"log message must not contain newline or control characters",
));
}
if log_byte_budget
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| {
remaining.checked_sub(message.len())
})
.is_err()
{
return Err(mlua::Error::external(crate::error::lua_quota::LOG_BYTE));
}
observer.observe(execution, section, Observation::Lua(message.to_owned()));
Ok(())
})
.map_err(Error::lua)?;
lua.globals().raw_set("log", log).map_err(Error::lua)
}
pub(crate) fn is_log_line_break_or_control(character: char) -> bool {
character.is_control() || matches!(character, '\u{2028}' | '\u{2029}')
}
pub(crate) fn finish_log_phase<T>(lua: &Lua, result: mlua::Result<T>) -> mlua::Result<T> {
let cleanup = lua.globals().raw_set("log", Value::Nil);
match (result, cleanup) {
(Err(error), _) | (Ok(_), Err(error)) => Err(error),
(Ok(value), Ok(())) => Ok(value),
}
}
pub(crate) fn observe_store_result(
execution: &str,
observer: &dyn Observer,
section: &str,
succeeded: bool,
success: Observation,
failure: Observation,
) {
observer.observe(
execution,
section,
if succeeded { success } else { failure },
);
}
#[expect(
clippy::too_many_lines,
reason = "one table installs all six store operations beside their matching observation outcomes"
)]
pub(crate) fn install_store_table<'scope, 'env: 'scope>(
lua: &Lua,
scope: &'scope Scope<'scope, 'env>,
globals: &mlua::Table,
store: &StoreRef,
execution: &'env str,
observer: &'env dyn Observer,
section: &'env str,
) -> Result<()> {
let table = lua.create_table().map_err(Error::lua)?;
let handle = store.clone();
let write = scope
.create_function(move |_, (path, contents): (String, String)| {
let result = handle.write(&path, &contents);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_WRITE_SUCCEEDED,
detail::STORE_WRITE_FAILED,
);
result.map_err(mlua::Error::external)?;
Ok(())
})
.map_err(Error::lua)?;
table.set("write", write).map_err(Error::lua)?;
let handle = store.clone();
let append = scope
.create_function(move |_, (path, contents): (String, String)| {
let result = handle.append(&path, &contents);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_APPEND_SUCCEEDED,
detail::STORE_APPEND_FAILED,
);
result.map_err(mlua::Error::external)?;
Ok(())
})
.map_err(Error::lua)?;
table.set("append", append).map_err(Error::lua)?;
let handle = store.clone();
let read_lines = scope
.create_function(move |_, path: String| {
let result = handle.read_lines(&path);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_READ_LINES_SUCCEEDED,
detail::STORE_READ_LINES_FAILED,
);
result.map_err(mlua::Error::external)
})
.map_err(Error::lua)?;
table.set("read_lines", read_lines).map_err(Error::lua)?;
let handle = store.clone();
let read = scope
.create_function(move |_, path: String| {
let result = handle.read(&path);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_READ_SUCCEEDED,
detail::STORE_READ_FAILED,
);
result.map_err(mlua::Error::external)
})
.map_err(Error::lua)?;
table.set("read", read).map_err(Error::lua)?;
let handle = store.clone();
let inject = scope
.create_function(move |_, path: String| {
let result = handle.inject(&path);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_INJECT_SUCCEEDED,
detail::STORE_INJECT_FAILED,
);
result.map_err(mlua::Error::external)
})
.map_err(Error::lua)?;
table.set("inject", inject).map_err(Error::lua)?;
let handle = store.clone();
let str_replace = scope
.create_function(move |_, (path, old, new): (String, String, String)| {
let result = handle.str_replace(&path, &old, &new);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_REPLACE_SUCCEEDED,
detail::STORE_REPLACE_FAILED,
);
result.map_err(mlua::Error::external)?;
Ok(())
})
.map_err(Error::lua)?;
table.set("str_replace", str_replace).map_err(Error::lua)?;
let handle = store.clone();
let delete = scope
.create_function(move |_, path: String| {
let result = handle.delete(&path);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_DELETE_SUCCEEDED,
detail::STORE_DELETE_FAILED,
);
result.map_err(mlua::Error::external)?;
Ok(())
})
.map_err(Error::lua)?;
table.set("delete", delete).map_err(Error::lua)?;
let handle = store.clone();
let glob = scope
.create_function(move |lua, pattern: String| {
let result = handle.glob(&pattern);
observe_store_result(
execution,
observer,
section,
result.is_ok(),
detail::STORE_GLOB_SUCCEEDED,
detail::STORE_GLOB_FAILED,
);
let paths = result.map_err(mlua::Error::external)?;
lua.create_sequence_from(paths)
})
.map_err(Error::lua)?;
table.set("glob", glob).map_err(Error::lua)?;
let handle = store.clone();
let exists = scope
.create_function(move |_, path: String| handle.exists(&path).map_err(mlua::Error::external))
.map_err(Error::lua)?;
table.set("exists", exists).map_err(Error::lua)?;
globals.raw_set("store", table).map_err(Error::lua)?;
Ok(())
}