use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use std::time::Instant;
#[cfg(test)]
use jiff::SignedDuration;
use jiff::Timestamp;
use rhai::packages::{Package, StandardPackage};
use rhai::{Dynamic, Engine, EvalAltResult};
pub const MAX_OPERATIONS: u64 = 50_000;
pub const MAX_CALL_LEVELS: usize = 16;
pub const MAX_EXPR_DEPTH: usize = 32;
pub const MAX_STRING_SIZE: usize = 1024;
pub const MAX_ARRAY_SIZE: usize = 256;
pub const MAX_MAP_SIZE: usize = 256;
pub const DEFAULT_RENDER_DEADLINE_MS: u64 = 50;
#[derive(Clone)]
pub(crate) struct DeadlineAbortMarker;
const DEADLINE_CHECK_STRIDE: u64 = 256;
const _: () = assert!(DEADLINE_CHECK_STRIDE > 0);
const _: () = assert!(DEADLINE_CHECK_STRIDE < MAX_OPERATIONS);
thread_local! {
static RENDER_DEADLINE: Cell<Option<Instant>> = const { Cell::new(None) };
static CURRENT_PLUGIN_ID: RefCell<Option<String>> = const { RefCell::new(None) };
static LOG_EMITTED: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
}
pub const LOG_LINES_PER_PLUGIN: u32 = 1;
type WarnEmitter = Box<dyn Fn(&str) + Send + Sync>;
static WARN_EMITTER: OnceLock<WarnEmitter> = OnceLock::new();
pub fn install_warn_emitter(emitter: WarnEmitter) {
debug_assert!(
WARN_EMITTER.get().is_none(),
"install_warn_emitter called twice — first install wins, subsequent emitter is dropped"
);
let _ = WARN_EMITTER.set(emitter);
}
fn emit_warn(msg: &str) {
if let Some(emitter) = WARN_EMITTER.get() {
emitter(msg);
} else {
eprintln!("linesmith [warn] {msg}");
}
}
pub fn set_render_deadline(deadline: Option<Instant>) {
RENDER_DEADLINE.with(|d| d.set(deadline));
}
pub fn set_current_plugin_id(id: Option<&str>) {
CURRENT_PLUGIN_ID.with(|cell| {
*cell.borrow_mut() = id.map(str::to_owned);
});
}
#[cfg(test)]
pub(crate) fn reset_log_counts() {
LOG_EMITTED.with(|cell| cell.borrow_mut().clear());
}
pub fn render_deadline_snapshot() -> Option<Instant> {
RENDER_DEADLINE.with(Cell::get)
}
#[must_use]
pub fn is_deadline_abort(err: &EvalAltResult) -> bool {
if let EvalAltResult::ErrorTerminated(token, _) = err {
token.is::<DeadlineAbortMarker>()
} else {
false
}
}
pub fn current_plugin_id_snapshot() -> Option<String> {
CURRENT_PLUGIN_ID.with(|c| c.borrow().clone())
}
#[must_use]
pub fn build_engine() -> Arc<Engine> {
let mut engine = Engine::new_raw();
engine.register_global_module(StandardPackage::new().as_shared_module());
engine.on_print(|_| {});
engine.on_debug(|_, _, _| {});
install_deadline_callback(&mut engine);
configure_limits(&mut engine);
lock_down_symbols(&mut engine);
register_host_fns(&mut engine);
Arc::new(engine)
}
fn install_deadline_callback(engine: &mut Engine) {
engine.on_progress(|ops| {
if ops % DEADLINE_CHECK_STRIDE != 0 {
return None;
}
let deadline = RENDER_DEADLINE.with(Cell::get)?;
if Instant::now() >= deadline {
Some(Dynamic::from(DeadlineAbortMarker))
} else {
None
}
});
}
fn configure_limits(engine: &mut Engine) {
engine.set_max_operations(MAX_OPERATIONS);
engine.set_max_call_levels(MAX_CALL_LEVELS);
engine.set_max_expr_depths(MAX_EXPR_DEPTH, MAX_EXPR_DEPTH);
engine.set_max_string_size(MAX_STRING_SIZE);
engine.set_max_array_size(MAX_ARRAY_SIZE);
engine.set_max_map_size(MAX_MAP_SIZE);
}
fn lock_down_symbols(engine: &mut Engine) {
engine.disable_symbol("import");
engine.disable_symbol("eval");
}
fn register_host_fns(engine: &mut Engine) {
engine.register_fn("log", rhai_log);
engine.register_fn("format_duration", rhai_format_duration);
engine.register_fn("format_cost_usd", rhai_format_cost_usd);
engine.register_fn("format_cost_usd", |n: i64| rhai_format_cost_usd(n as f64));
engine.register_fn("format_tokens", rhai_format_tokens);
engine.register_fn("format_countdown_until", rhai_format_countdown_until);
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Arc<Engine>>();
};
fn rhai_log(msg: &str) {
const UNSCOPED: &str = "<unscoped>";
let allowed = LOG_EMITTED.with(|cell| {
let mut counts = cell.borrow_mut();
let id_str = CURRENT_PLUGIN_ID.with(|c| c.borrow().clone());
let key: &str = id_str.as_deref().unwrap_or(UNSCOPED);
match counts.get_mut(key) {
Some(n) if *n >= LOG_LINES_PER_PLUGIN => None,
Some(n) => {
*n += 1;
Some(key.to_owned())
}
None => {
counts.insert(key.to_owned(), 1);
Some(key.to_owned())
}
}
});
if let Some(id) = allowed {
emit_warn(&format!("plugin {id}: {msg}"));
}
}
fn rhai_format_duration(ms: i64) -> String {
if ms <= 0 {
return "0s".to_string();
}
let total_seconds = ms / 1000;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
if hours > 0 {
if minutes > 0 {
format!("{hours}h {minutes}m")
} else {
format!("{hours}h")
}
} else if minutes > 0 {
format!("{minutes}m")
} else {
format!("{seconds}s")
}
}
fn rhai_format_cost_usd(dollars: f64) -> String {
format!("${dollars:.2}")
}
fn rhai_format_tokens(count: i64) -> String {
let n = count.max(0);
if n >= 999_500 {
let m = n as f64 / 1_000_000.0;
format!("{m:.1}M")
} else if n >= 1_000 {
let k = n as f64 / 1_000.0;
format!("{k:.1}k")
} else {
format!("{n}")
}
}
fn rhai_format_countdown_until(rfc3339_ts: &str) -> String {
let Ok(target) = rfc3339_ts.parse::<Timestamp>() else {
return "?".to_string();
};
let total_minutes = (target.as_second() - Timestamp::now().as_second()) / 60;
if total_minutes <= 0 {
return "now".to_string();
}
let days = total_minutes / (24 * 60);
if days >= 2 {
return format!("{days}d");
}
let hours = total_minutes / 60;
if hours >= 1 {
let minutes = total_minutes - hours * 60;
return if minutes == 0 {
format!("{hours}h")
} else {
format!("{hours}h {minutes}m")
};
}
format!("{total_minutes}m")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_evaluates_basic_arithmetic() {
let engine = build_engine();
let n: i64 = engine.eval("1 + 2").expect("eval ok");
assert_eq!(n, 3);
}
#[test]
fn infinite_loop_trips_operation_limit() {
let engine = build_engine();
let err = engine.eval::<()>("loop {}").unwrap_err();
assert!(
format!("{err}").contains("operations"),
"expected operation-limit error, got: {err}"
);
}
struct ThreadLocalGuard;
impl ThreadLocalGuard {
fn install_deadline(at: Instant) -> Self {
set_render_deadline(Some(at));
Self
}
fn install_plugin_id(id: &str) -> Self {
set_current_plugin_id(Some(id));
Self
}
}
impl Drop for ThreadLocalGuard {
fn drop(&mut self) {
set_render_deadline(None);
set_current_plugin_id(None);
}
}
#[test]
fn past_deadline_aborts_long_running_script() {
let engine = build_engine();
let _guard = ThreadLocalGuard::install_deadline(Instant::now());
let err = engine.eval::<()>("loop {}").unwrap_err();
let msg = format!("{err}");
assert!(
msg.to_lowercase().contains("terminated"),
"expected `Script terminated` from on_progress abort, got: {msg}"
);
}
#[test]
fn far_future_deadline_does_not_abort_quick_script() {
let engine = build_engine();
let _guard = ThreadLocalGuard::install_deadline(
Instant::now() + std::time::Duration::from_secs(3600),
);
let n: i64 = engine.eval("1 + 2 + 3").expect("quick eval ok");
assert_eq!(n, 6);
}
#[test]
fn no_deadline_set_does_not_abort_quick_script() {
set_render_deadline(None);
let engine = build_engine();
let n: i64 = engine.eval("4 * 5").expect("eval ok");
assert_eq!(n, 20);
}
#[test]
fn log_emits_first_call_then_silences() {
reset_log_counts();
let engine = build_engine();
let _guard = ThreadLocalGuard::install_plugin_id("log_emits_first_call_then_silences");
engine
.eval::<()>(r#"log("first"); log("second"); log("third");"#)
.expect("eval ok");
let count = LOG_EMITTED.with(|cell| {
cell.borrow()
.get("log_emits_first_call_then_silences")
.copied()
.unwrap_or(0)
});
assert_eq!(
count, LOG_LINES_PER_PLUGIN,
"expected exactly {LOG_LINES_PER_PLUGIN} emission(s), counted {count}"
);
}
#[test]
fn log_under_distinct_plugin_ids_each_gets_its_own_quota() {
reset_log_counts();
let engine = build_engine();
for id in ["log_quota_a", "log_quota_b"] {
let _guard = ThreadLocalGuard::install_plugin_id(id);
engine.eval::<()>(r#"log("hi");"#).expect("eval ok");
}
let counts = LOG_EMITTED.with(|cell| {
let map = cell.borrow();
(
map.get("log_quota_a").copied().unwrap_or(0),
map.get("log_quota_b").copied().unwrap_or(0),
)
});
assert_eq!(counts, (LOG_LINES_PER_PLUGIN, LOG_LINES_PER_PLUGIN));
}
#[test]
fn log_outside_render_attributes_to_unscoped_bucket() {
reset_log_counts();
let engine = build_engine();
engine.eval::<()>(r#"log("from-eval");"#).expect("eval ok");
let count = LOG_EMITTED.with(|cell| cell.borrow().get("<unscoped>").copied());
assert_eq!(count, Some(LOG_LINES_PER_PLUGIN));
}
#[test]
fn import_is_disabled() {
let engine = build_engine();
let err = engine.eval::<()>(r#"import "foo" as bar;"#).unwrap_err();
assert!(
format!("{err}").to_lowercase().contains("import"),
"expected import-related error, got: {err}"
);
}
#[test]
fn eval_symbol_is_disabled() {
let engine = build_engine();
let err = engine.eval::<()>(r#"eval("1 + 1")"#).unwrap_err();
assert!(
format!("{err}").to_lowercase().contains("eval"),
"expected eval-related error, got: {err}"
);
}
#[test]
fn unregistered_fs_call_fails_at_runtime() {
let engine = build_engine();
let err = engine.eval::<()>(r#"fs::read("/etc/passwd")"#).unwrap_err();
let msg = format!("{err}").to_lowercase();
assert!(
msg.contains("fs::read") || msg.contains("not found") || msg.contains("function"),
"expected function-not-found error, got: {err}"
);
}
#[test]
fn print_and_debug_are_silent_no_ops() {
let engine = build_engine();
engine
.eval::<()>(
r#"print("this would leak to stdout under Engine::new"); debug("this too");"#,
)
.expect("print/debug call must succeed as a no-op");
}
#[test]
fn format_duration_sub_minute_renders_seconds() {
assert_eq!(rhai_format_duration(45_000), "45s");
}
#[test]
fn format_duration_negative_clamps_to_zero() {
assert_eq!(rhai_format_duration(-1), "0s");
}
#[test]
fn format_duration_renders_hours_and_minutes() {
assert_eq!(rhai_format_duration(3_600_000 + 23 * 60 * 1000), "1h 23m");
}
#[test]
fn format_duration_renders_minutes_only_under_an_hour() {
assert_eq!(rhai_format_duration(12 * 60 * 1000), "12m");
}
#[test]
fn format_duration_drops_minutes_on_round_hour() {
assert_eq!(rhai_format_duration(2 * 3_600_000), "2h");
}
#[test]
fn format_cost_usd_two_decimals() {
assert_eq!(rhai_format_cost_usd(1.234), "$1.23");
assert_eq!(rhai_format_cost_usd(0.0), "$0.00");
}
#[test]
fn format_tokens_under_1k_renders_literal() {
assert_eq!(rhai_format_tokens(42), "42");
assert_eq!(rhai_format_tokens(0), "0");
}
#[test]
fn format_tokens_thousands_get_k_suffix() {
assert_eq!(rhai_format_tokens(1200), "1.2k");
}
#[test]
fn format_tokens_millions_get_m_suffix() {
assert_eq!(rhai_format_tokens(3_500_000), "3.5M");
}
#[test]
fn format_tokens_negative_clamps_to_zero() {
assert_eq!(rhai_format_tokens(-5), "0");
}
#[test]
fn format_countdown_until_bad_rfc3339_renders_marker() {
assert_eq!(rhai_format_countdown_until("not a timestamp"), "?");
}
#[test]
fn format_countdown_until_past_timestamp_says_now() {
assert_eq!(rhai_format_countdown_until("2001-09-09T01:46:40Z"), "now");
}
#[test]
fn host_format_cost_usd_invokable_from_script() {
let engine = build_engine();
let s: String = engine.eval(r#"format_cost_usd(1.99)"#).expect("eval ok");
assert_eq!(s, "$1.99");
}
#[test]
fn host_format_tokens_invokable_from_script() {
let engine = build_engine();
let s: String = engine.eval(r#"format_tokens(1500)"#).expect("eval ok");
assert_eq!(s, "1.5k");
}
#[test]
fn host_log_invokable_from_script() {
let engine = build_engine();
engine
.eval::<()>(r#"log("hello from rhai");"#)
.expect("eval ok");
}
#[test]
fn host_format_duration_invokable_from_script() {
let engine = build_engine();
let s: String = engine.eval(r#"format_duration(45000)"#).expect("eval ok");
assert_eq!(s, "45s");
}
#[test]
fn host_format_countdown_until_invokable_from_script() {
let engine = build_engine();
let s: String = engine
.eval(r#"format_countdown_until("2001-09-09T01:46:40Z")"#)
.expect("eval ok");
assert_eq!(s, "now");
}
#[test]
fn host_format_cost_usd_accepts_integer_literal() {
let engine = build_engine();
let s: String = engine.eval(r#"format_cost_usd(2)"#).expect("eval ok");
assert_eq!(s, "$2.00");
}
#[test]
fn format_tokens_boundary_at_exactly_1000() {
assert_eq!(rhai_format_tokens(1_000), "1.0k");
}
#[test]
fn format_tokens_boundary_at_exactly_1_000_000() {
assert_eq!(rhai_format_tokens(1_000_000), "1.0M");
}
#[test]
fn standard_package_string_helpers_work() {
let engine = build_engine();
let len: i64 = engine.eval(r#""hello".len()"#).expect("eval ok");
assert_eq!(len, 5);
}
#[test]
fn standard_package_array_helpers_work() {
let engine = build_engine();
let n: i64 = engine
.eval(r#"let xs = [1, 2, 3]; xs.len()"#)
.expect("eval ok");
assert_eq!(n, 3);
}
#[test]
fn format_tokens_near_million_boundary_rolls_to_m() {
assert_eq!(rhai_format_tokens(999_950), "1.0M");
assert_eq!(rhai_format_tokens(999_999), "1.0M");
}
#[test]
fn format_tokens_just_below_rollover_boundary_stays_k() {
assert_eq!(rhai_format_tokens(999_499), "999.5k");
}
#[test]
fn format_countdown_until_future_timestamp_renders_duration() {
let target = Timestamp::now() + SignedDuration::from_hours(2);
let rendered = rhai_format_countdown_until(&target.to_string());
assert_ne!(rendered, "?", "expected successful parse + format");
assert_ne!(rendered, "now", "expected future-duration output");
assert!(!rendered.is_empty());
}
}