#[cfg(feature = "rhai-runtime")]
use rhai::Engine;
use uni_plugin::{Capability, CapabilitySet};
use crate::host_fns::RhaiHostFnRegistry;
pub const DEFAULT_MAX_CALL_LEVELS: usize = 64;
#[cfg(feature = "rhai-runtime")]
#[must_use]
pub fn build_engine(effective_caps: &CapabilitySet, host_fns: &RhaiHostFnRegistry) -> Engine {
let mut engine = Engine::new();
engine.disable_symbol("eval");
engine.set_module_resolver(rhai::module_resolvers::DummyModuleResolver::new());
engine.set_max_call_levels(DEFAULT_MAX_CALL_LEVELS);
apply_resource_limits(&mut engine, effective_caps);
crate::columns::register_column_types(&mut engine);
for spec in host_fns.iter() {
let granted = match &spec.required_capability {
None => true,
Some(required) => caps_grant(effective_caps, required),
};
if granted {
(spec.register)(&mut engine, effective_caps);
}
}
engine
}
#[cfg(not(feature = "rhai-runtime"))]
pub fn build_engine(_effective_caps: &CapabilitySet, _host_fns: &RhaiHostFnRegistry) -> () {}
#[cfg(feature = "rhai-runtime")]
fn apply_resource_limits(engine: &mut Engine, caps: &CapabilitySet) {
for cap in caps.iter() {
match cap {
Capability::FuelPerCall(n) => {
engine.set_max_operations(*n);
}
Capability::MemoryBytes(n) => {
let per_collection = (*n / 4).max(1024) as usize;
engine.set_max_string_size(per_collection);
engine.set_max_array_size(per_collection);
engine.set_max_map_size(per_collection);
}
_ => {}
}
}
}
fn caps_grant(effective: &CapabilitySet, required: &Capability) -> bool {
effective.contains_variant(required)
}
#[cfg(all(test, feature = "rhai-runtime"))]
mod tests {
use super::*;
use crate::host_fns::RhaiHostFnSpec;
use std::sync::Arc;
fn empty_caps() -> CapabilitySet {
CapabilitySet::new()
}
#[test]
fn eval_is_disabled() {
let engine = build_engine(&empty_caps(), &RhaiHostFnRegistry::new());
let result = engine.eval::<rhai::Dynamic>(r#"eval("1 + 1")"#);
assert!(result.is_err(), "eval should be disabled");
}
#[test]
fn import_is_denied() {
let engine = build_engine(&empty_caps(), &RhaiHostFnRegistry::new());
let script = r#"import "math" as m; m.pi"#;
let result = engine.eval::<rhai::Dynamic>(script);
assert!(
result.is_err(),
"import should be denied by module resolver"
);
}
#[test]
fn ungranted_host_fn_not_registered() {
let mut host_fns = RhaiHostFnRegistry::new();
host_fns.register(RhaiHostFnSpec {
name: "uni.fs.read".to_owned(),
required_capability: Some(Capability::Filesystem {
read: vec!["/data/**".into()],
write: vec![],
}),
docs: String::new(),
register: Arc::new(|engine: &mut Engine, _caps: &CapabilitySet| {
engine.register_fn("uni_fs_read", |_path: &str| "ok".to_string());
}),
});
let engine = build_engine(&empty_caps(), &host_fns);
let result = engine.eval::<String>(r#"uni_fs_read("/data/x")"#);
assert!(result.is_err(), "ungranted host fn must not resolve");
}
#[test]
fn granted_host_fn_callable() {
let mut host_fns = RhaiHostFnRegistry::new();
let cap = Capability::Filesystem {
read: vec!["/data/**".into()],
write: vec![],
};
host_fns.register(RhaiHostFnSpec {
name: "uni.fs.read".to_owned(),
required_capability: Some(cap.clone()),
docs: String::new(),
register: Arc::new(|engine: &mut Engine, _caps: &CapabilitySet| {
engine.register_fn("uni_fs_read", |_path: &str| "ok".to_string());
}),
});
let caps = CapabilitySet::from_iter_of([cap]);
let engine = build_engine(&caps, &host_fns);
let result: String = engine
.eval(r#"uni_fs_read("/data/x")"#)
.expect("should call");
assert_eq!(result, "ok");
}
#[test]
fn fuel_limit_trips() {
let caps = CapabilitySet::from_iter_of([Capability::FuelPerCall(1_000)]);
let engine = build_engine(&caps, &RhaiHostFnRegistry::new());
let script = r#"
let i = 0;
while i < 100000 {
i += 1;
}
i
"#;
let result = engine.eval::<i64>(script);
assert!(
result.is_err(),
"FuelPerCall(1000) should trip on a long loop"
);
}
#[test]
fn always_available_fn_registered_with_empty_caps() {
let mut host_fns = RhaiHostFnRegistry::new();
host_fns.register(RhaiHostFnSpec {
name: "uni.always".to_owned(),
required_capability: None,
docs: String::new(),
register: Arc::new(|engine: &mut Engine, _caps: &CapabilitySet| {
engine.register_fn("uni_always", || 42_i64);
}),
});
let engine = build_engine(&empty_caps(), &host_fns);
let result: i64 = engine.eval("uni_always()").expect("should call");
assert_eq!(result, 42);
}
}