//! Rhai engine setup: sandboxed engine with registered types and host API. use rhai::Engine; use crate::host_api; use crate::types::{self, RhaiExportContext, RhaiSampleInfo}; /// Create a sandboxed Rhai engine with all custom types and host functions registered. pub fn create_engine() -> Engine { let mut engine = Engine::new(); // Sandbox limits engine.set_max_operations(100_000); engine.set_max_call_levels(32); engine.set_max_string_size(10_000); engine.set_max_array_size(1_000); engine.set_max_map_size(100); engine.set_max_expr_depths(64, 32); // Suppress print/debug output from scripts engine.on_print(|_| {}); engine.on_debug(|_, _, _| {}); // Disable dynamic eval and module imports (principle of least privilege) engine.disable_symbol("eval"); engine.set_max_modules(0); // Register custom types engine .register_type_with_name::("SampleInfo") .register_type_with_name::("ExportContext"); // Register getter modules engine.register_global_module(rhai::exported_module!(types::sample_info_module).into()); engine.register_global_module(rhai::exported_module!(types::export_context_module).into()); // Register host API functions host_api::register(&mut engine); engine } #[cfg(test)] mod tests { use super::*; #[test] fn engine_creates_without_panic() { let _engine = create_engine(); } #[test] fn engine_enforces_operation_limit() { let engine = create_engine(); let result = engine.eval::<()>("let x = 0; loop { x += 1; }"); assert!(result.is_err(), "infinite loop should be stopped by operation limit"); } #[test] fn engine_sample_info_getters_work() { let engine = create_engine(); let mut scope = rhai::Scope::new(); let info = RhaiSampleInfo { hash: "abc123".to_string(), name: "kick.wav".to_string(), extension: "wav".to_string(), sample_rate: 44100, bit_depth: 16, channels: 1, duration: 0.5, file_size: 44100, }; scope.push("info", info); let result: String = engine.eval_with_scope(&mut scope, "info.name").unwrap(); assert_eq!(result, "kick.wav"); let rate: i64 = engine.eval_with_scope(&mut scope, "info.sample_rate").unwrap(); assert_eq!(rate, 44100); } #[test] fn engine_export_context_getters_work() { let engine = create_engine(); let mut scope = rhai::Scope::new(); let ctx = RhaiExportContext { device_name: "SP-404 MKII".to_string(), destination: "/tmp/export".to_string(), filename: "kick".to_string(), extension: "wav".to_string(), index: 0, total: 10, }; scope.push("ctx", ctx); let result: String = engine.eval_with_scope(&mut scope, "ctx.device_name").unwrap(); assert_eq!(result, "SP-404 MKII"); let total: i64 = engine.eval_with_scope(&mut scope, "ctx.total").unwrap(); assert_eq!(total, 10); } }