1use std::collections::HashMap;
4use std::path::Path;
5use std::sync::Arc;
6
7use rhai::{AST, Dynamic, Engine, Scope};
8
9use crate::config::{PluginConfig, PluginMetadata};
10use crate::hooks::{Hook, HookContext, HookResult};
11use crate::runtime::{BoxFuture, IsolatedContext, PluginHandle, PluginRuntime};
12use crate::sandbox::SandboxConfig;
13use crate::types::{PluginError, PluginResult, Value};
14
15struct LoadedRhaiPlugin {
17 name: String,
19
20 ast: AST,
22
23 metadata: PluginMetadata,
25
26 hooks: Vec<String>,
28}
29
30pub struct RhaiRuntime {
32 engine: Engine,
34
35 plugins: HashMap<PluginHandle, LoadedRhaiPlugin>,
37
38 next_handle: usize,
40
41 config: Option<PluginConfig>,
43
44 sandbox: Arc<SandboxConfig>,
46
47 initialized: bool,
49}
50
51impl RhaiRuntime {
52 pub fn new() -> PluginResult<Self> {
54 let mut engine = Engine::new();
55
56 engine.set_max_expr_depths(64, 64);
58 engine.set_max_call_levels(64);
59 engine.set_max_operations(1_000_000);
60 engine.set_max_modules(100);
61 engine.set_max_string_size(1024 * 1024); engine.set_max_array_size(10_000);
63 engine.set_max_map_size(10_000);
64
65 engine.disable_symbol("eval");
67
68 Ok(Self {
69 engine,
70 plugins: HashMap::new(),
71 next_handle: 0,
72 config: None,
73 sandbox: Arc::new(SandboxConfig::default()),
74 initialized: false,
75 })
76 }
77
78 fn init_api(&mut self) -> PluginResult<()> {
80 self.engine.register_fn("log_info", |msg: &str| {
82 tracing::info!(target: "plugin", "{}", msg);
83 });
84
85 self.engine.register_fn("log_warn", |msg: &str| {
86 tracing::warn!(target: "plugin", "{}", msg);
87 });
88
89 self.engine.register_fn("log_error", |msg: &str| {
90 tracing::error!(target: "plugin", "{}", msg);
91 });
92
93 self.engine.register_fn("notify", |msg: &str| {
94 tracing::info!(target: "plugin_notify", "{}", msg);
95 });
96
97 let sb = Arc::clone(&self.sandbox);
99 self.engine
100 .register_fn("fs_exists", move |path: &str| -> bool {
101 let p = std::path::Path::new(path);
102 if !sb.can_read(p) {
103 return false;
104 }
105 p.exists()
106 });
107
108 let sb = Arc::clone(&self.sandbox);
109 self.engine
110 .register_fn("fs_is_dir", move |path: &str| -> bool {
111 let p = std::path::Path::new(path);
112 if !sb.can_read(p) {
113 return false;
114 }
115 p.is_dir()
116 });
117
118 let sb = Arc::clone(&self.sandbox);
119 self.engine
120 .register_fn("fs_is_file", move |path: &str| -> bool {
121 let p = std::path::Path::new(path);
122 if !sb.can_read(p) {
123 return false;
124 }
125 p.is_file()
126 });
127
128 let sb = Arc::clone(&self.sandbox);
129 self.engine
130 .register_fn("fs_read", move |path: &str| -> Dynamic {
131 let p = std::path::Path::new(path);
132 if !sb.can_read(p) {
133 return Dynamic::UNIT;
134 }
135 match std::fs::read_to_string(path) {
136 Ok(content) => Dynamic::from(content),
137 Err(_) => Dynamic::UNIT,
138 }
139 });
140
141 self.engine
142 .register_fn("fs_extension", |path: &str| -> Dynamic {
143 let p = std::path::Path::new(path);
144 match p.extension().and_then(|e| e.to_str()) {
145 Some(ext) => Dynamic::from(ext.to_string()),
146 None => Dynamic::UNIT,
147 }
148 });
149
150 self.engine
151 .register_fn("fs_filename", |path: &str| -> Dynamic {
152 let p = std::path::Path::new(path);
153 match p.file_name().and_then(|n| n.to_str()) {
154 Some(name) => Dynamic::from(name.to_string()),
155 None => Dynamic::UNIT,
156 }
157 });
158
159 self.engine
160 .register_fn("fs_parent", |path: &str| -> Dynamic {
161 let p = std::path::Path::new(path);
162 match p.parent().and_then(|p| p.to_str()) {
163 Some(parent) => Dynamic::from(parent.to_string()),
164 None => Dynamic::UNIT,
165 }
166 });
167
168 let sb = Arc::clone(&self.sandbox);
169 self.engine
170 .register_fn("fs_size", move |path: &str| -> Dynamic {
171 let p = std::path::Path::new(path);
172 if !sb.can_read(p) {
173 return Dynamic::from(-1_i64);
174 }
175 match std::fs::metadata(path) {
176 Ok(meta) => Dynamic::from(meta.len() as i64),
177 Err(_) => Dynamic::from(-1_i64),
178 }
179 });
180
181 self.engine
183 .register_fn("ui_span", |text: &str, fg: &str| -> rhai::Map {
184 let mut map = rhai::Map::new();
185 map.insert("type".into(), Dynamic::from("span"));
186 map.insert("text".into(), Dynamic::from(text.to_string()));
187 map.insert("fg".into(), Dynamic::from(fg.to_string()));
188 map
189 });
190
191 self.engine
192 .register_fn("ui_line", |spans: rhai::Array| -> rhai::Map {
193 let mut map = rhai::Map::new();
194 map.insert("type".into(), Dynamic::from("line"));
195 map.insert("spans".into(), Dynamic::from(spans));
196 map
197 });
198
199 Ok(())
200 }
201
202 fn dynamic_to_value(val: &Dynamic) -> Value {
204 if val.is_unit() {
205 Value::Null
206 } else if val.is_bool() {
207 Value::Bool(val.as_bool().unwrap_or(false))
208 } else if val.is_int() {
209 Value::Integer(val.as_int().unwrap_or(0))
210 } else if val.is_float() {
211 Value::Float(val.as_float().unwrap_or(0.0))
212 } else if val.is_string() {
213 Value::String(val.clone().into_string().unwrap_or_default())
214 } else if val.is_array() {
215 let arr = val.clone().into_array().unwrap_or_default();
216 Value::Array(arr.iter().map(Self::dynamic_to_value).collect())
217 } else if val.is_map() {
218 let map = val.clone().cast::<rhai::Map>();
219 let obj: std::collections::HashMap<String, Value> = map
220 .into_iter()
221 .map(|(k, v)| (k.to_string(), Self::dynamic_to_value(&v)))
222 .collect();
223 Value::Object(obj)
224 } else {
225 Value::Null
226 }
227 }
228
229 fn value_to_dynamic(val: &Value) -> Dynamic {
231 match val {
232 Value::Null => Dynamic::UNIT,
233 Value::Bool(b) => Dynamic::from(*b),
234 Value::Integer(i) => Dynamic::from(*i),
235 Value::Float(f) => Dynamic::from(*f),
236 Value::String(s) => Dynamic::from(s.clone()),
237 Value::Array(arr) => {
238 let rhai_arr: rhai::Array = arr.iter().map(Self::value_to_dynamic).collect();
239 Dynamic::from(rhai_arr)
240 }
241 Value::Object(obj) => {
242 let mut map = rhai::Map::new();
243 for (k, v) in obj {
244 map.insert(k.clone().into(), Self::value_to_dynamic(v));
245 }
246 Dynamic::from(map)
247 }
248 Value::Bytes(b) => Dynamic::from(b.clone()),
249 }
250 }
251
252 fn hook_to_dynamic(&self, hook: &Hook) -> Dynamic {
254 let json = serde_json::to_value(hook).unwrap_or(serde_json::Value::Null);
256
257 fn json_to_dynamic(val: &serde_json::Value) -> Dynamic {
258 match val {
259 serde_json::Value::Null => Dynamic::UNIT,
260 serde_json::Value::Bool(b) => Dynamic::from(*b),
261 serde_json::Value::Number(n) => {
262 if let Some(i) = n.as_i64() {
263 Dynamic::from(i)
264 } else {
265 Dynamic::from(n.as_f64().unwrap_or(0.0))
266 }
267 }
268 serde_json::Value::String(s) => Dynamic::from(s.clone()),
269 serde_json::Value::Array(arr) => {
270 let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect();
271 Dynamic::from(rhai_arr)
272 }
273 serde_json::Value::Object(obj) => {
274 let mut map = rhai::Map::new();
275 for (k, v) in obj {
276 map.insert(k.clone().into(), json_to_dynamic(v));
277 }
278 Dynamic::from(map)
279 }
280 }
281 }
282
283 json_to_dynamic(&json)
284 }
285}
286
287impl Default for RhaiRuntime {
288 fn default() -> Self {
289 Self::new().expect("Failed to create Rhai runtime")
290 }
291}
292
293impl PluginRuntime for RhaiRuntime {
294 fn name(&self) -> &'static str {
295 "rhai"
296 }
297
298 fn file_extensions(&self) -> &'static [&'static str] {
299 &[".rhai"]
300 }
301
302 fn init(&mut self, config: &PluginConfig) -> PluginResult<()> {
303 if self.initialized {
304 return Ok(());
305 }
306
307 self.sandbox = Arc::new(SandboxConfig {
309 timeout_ms: config.default_timeout_ms,
310 max_memory: config.max_memory_mb * 1024 * 1024,
311 allow_network: config.allow_network,
312 ..SandboxConfig::default()
313 });
314
315 self.config = Some(config.clone());
316 self.init_api()?;
317 self.initialized = true;
318
319 Ok(())
320 }
321
322 fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle> {
323 let code = std::fs::read_to_string(source)?;
325
326 let ast = self
327 .engine
328 .compile(&code)
329 .map_err(|e| PluginError::LoadError {
330 name: id.to_string(),
331 message: e.to_string(),
332 })?;
333
334 let mut hooks = vec![];
336 for func in ast.iter_functions() {
337 let name = func.name.to_string();
338 if name.starts_with("on_") {
339 hooks.push(name);
340 }
341 }
342
343 let handle = PluginHandle::new(self.next_handle);
344 self.next_handle += 1;
345
346 let metadata = PluginMetadata {
347 name: id.to_string(),
348 runtime: "rhai".to_string(),
349 ..Default::default()
350 };
351
352 self.plugins.insert(
353 handle,
354 LoadedRhaiPlugin {
355 name: id.to_string(),
356 ast,
357 metadata,
358 hooks,
359 },
360 );
361
362 Ok(handle)
363 }
364
365 fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()> {
366 self.plugins.remove(&handle);
367 Ok(())
368 }
369
370 fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata> {
371 self.plugins.get(&handle).map(|p| &p.metadata)
372 }
373
374 fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool {
375 self.plugins
376 .get(&handle)
377 .map(|p| p.hooks.contains(&hook_name.to_string()))
378 .unwrap_or(false)
379 }
380
381 fn call_hook_sync(
382 &self,
383 handle: PluginHandle,
384 hook: &Hook,
385 _ctx: &HookContext,
386 ) -> PluginResult<HookResult> {
387 let plugin = self
388 .plugins
389 .get(&handle)
390 .ok_or_else(|| PluginError::NotFound {
391 path: std::path::PathBuf::new(),
392 })?;
393
394 let hook_name = hook.name();
395 if !plugin.hooks.contains(&hook_name.to_string()) {
396 return Ok(HookResult::default());
397 }
398
399 let mut scope = Scope::new();
401 scope.push("hook", self.hook_to_dynamic(hook));
402
403 let result = self
405 .engine
406 .call_fn::<Dynamic>(&mut scope, &plugin.ast, hook_name, ())
407 .map_err(|e| PluginError::ExecutionError {
408 name: plugin.name.clone(),
409 message: e.to_string(),
410 })?;
411
412 let mut hook_result = HookResult::ok();
414 if result.is_map()
415 && let Some(map) = result.try_cast::<rhai::Map>()
416 {
417 if let Some(prevent) = map.get("prevent_default")
418 && prevent.as_bool().unwrap_or(false)
419 {
420 hook_result = hook_result.prevent_default();
421 }
422 if let Some(stop) = map.get("stop_propagation")
423 && stop.as_bool().unwrap_or(false)
424 {
425 hook_result = hook_result.stop_propagation();
426 }
427 if let Some(val) = map.get("value") {
428 hook_result.value = Some(Self::dynamic_to_value(val));
429 }
430 }
431
432 Ok(hook_result)
433 }
434
435 fn call_hook_async<'a>(
436 &'a self,
437 handle: PluginHandle,
438 hook: &'a Hook,
439 ctx: &'a HookContext,
440 ) -> BoxFuture<'a, PluginResult<HookResult>> {
441 Box::pin(async move { self.call_hook_sync(handle, hook, ctx) })
442 }
443
444 fn call_method<'a>(
445 &'a self,
446 handle: PluginHandle,
447 method: &'a str,
448 args: Vec<Value>,
449 ) -> BoxFuture<'a, PluginResult<Value>> {
450 Box::pin(async move {
451 let plugin = self
452 .plugins
453 .get(&handle)
454 .ok_or_else(|| PluginError::NotFound {
455 path: std::path::PathBuf::new(),
456 })?;
457
458 let mut scope = Scope::new();
459
460 let rhai_args: Vec<Dynamic> = args.iter().map(Self::value_to_dynamic).collect();
462
463 scope.push("args", rhai_args);
465
466 let result = self
467 .engine
468 .call_fn::<Dynamic>(&mut scope, &plugin.ast, method, ())
469 .map_err(|e| PluginError::ExecutionError {
470 name: plugin.name.clone(),
471 message: e.to_string(),
472 })?;
473
474 Ok(Self::dynamic_to_value(&result))
475 })
476 }
477
478 fn create_isolated_context(
479 &self,
480 sandbox: &SandboxConfig,
481 ) -> PluginResult<Box<dyn IsolatedContext>> {
482 Ok(Box::new(RhaiIsolatedContext::new(sandbox.clone())?))
483 }
484
485 fn loaded_plugins(&self) -> Vec<PluginHandle> {
486 self.plugins.keys().copied().collect()
487 }
488
489 fn shutdown(&mut self) -> PluginResult<()> {
490 self.plugins.clear();
491 Ok(())
492 }
493}
494
495struct RhaiIsolatedContext {
497 engine: Engine,
498 #[allow(dead_code)]
501 sandbox: Arc<SandboxConfig>,
502 ast: std::sync::Mutex<Option<AST>>,
505}
506
507impl RhaiIsolatedContext {
508 fn new(sandbox: SandboxConfig) -> PluginResult<Self> {
509 let sandbox = Arc::new(sandbox);
510 let mut engine = Engine::new();
511
512 engine.set_max_expr_depths(32, 32);
514 engine.set_max_call_levels(32);
515 engine.set_max_operations(100_000);
516 engine.set_max_modules(10);
517 engine.set_max_string_size(100 * 1024); engine.set_max_array_size(1000);
519 engine.set_max_map_size(1000);
520
521 engine.disable_symbol("eval");
523
524 let sb = Arc::clone(&sandbox);
526 engine.register_fn("fs_exists", move |path: &str| -> bool {
527 let p = std::path::Path::new(path);
528 sb.can_read(p) && p.exists()
529 });
530
531 let sb = Arc::clone(&sandbox);
532 engine.register_fn("fs_is_dir", move |path: &str| -> bool {
533 let p = std::path::Path::new(path);
534 sb.can_read(p) && p.is_dir()
535 });
536
537 let sb = Arc::clone(&sandbox);
538 engine.register_fn("fs_is_file", move |path: &str| -> bool {
539 let p = std::path::Path::new(path);
540 sb.can_read(p) && p.is_file()
541 });
542
543 let sb = Arc::clone(&sandbox);
544 engine.register_fn("fs_read", move |path: &str| -> Dynamic {
545 let p = std::path::Path::new(path);
546 if !sb.can_read(p) {
547 return Dynamic::UNIT;
548 }
549 match std::fs::read_to_string(path) {
550 Ok(content) => Dynamic::from(content),
551 Err(_) => Dynamic::UNIT,
552 }
553 });
554
555 let sb = Arc::clone(&sandbox);
556 engine.register_fn("fs_size", move |path: &str| -> Dynamic {
557 let p = std::path::Path::new(path);
558 if !sb.can_read(p) {
559 return Dynamic::from(-1_i64);
560 }
561 match std::fs::metadata(path) {
562 Ok(meta) => Dynamic::from(meta.len() as i64),
563 Err(_) => Dynamic::from(-1_i64),
564 }
565 });
566
567 Ok(Self {
568 engine,
569 sandbox,
570 ast: std::sync::Mutex::new(None),
571 })
572 }
573}
574
575impl IsolatedContext for RhaiIsolatedContext {
576 fn execute<'a>(
577 &'a self,
578 code: &'a [u8],
579 cancel: tokio_util::sync::CancellationToken,
580 ) -> BoxFuture<'a, PluginResult<Value>> {
581 Box::pin(async move {
582 if cancel.is_cancelled() {
583 return Err(PluginError::Cancelled {
584 name: "isolate".into(),
585 });
586 }
587
588 let code_str = std::str::from_utf8(code).map_err(|e| PluginError::ExecutionError {
589 name: "isolate".into(),
590 message: format!("Invalid UTF-8: {}", e),
591 })?;
592
593 let compiled =
596 self.engine
597 .compile(code_str)
598 .map_err(|e| PluginError::ExecutionError {
599 name: "isolate".into(),
600 message: e.to_string(),
601 })?;
602
603 let mut scope = Scope::new();
604 let result = self
605 .engine
606 .eval_ast_with_scope::<Dynamic>(&mut scope, &compiled)
607 .map_err(|e| PluginError::ExecutionError {
608 name: "isolate".into(),
609 message: e.to_string(),
610 })?;
611
612 if let Ok(mut guard) = self.ast.lock() {
614 *guard = Some(compiled);
615 }
616
617 Ok(RhaiRuntime::dynamic_to_value(&result))
618 })
619 }
620
621 fn call_function<'a>(
622 &'a self,
623 name: &'a str,
624 args: Vec<Value>,
625 cancel: tokio_util::sync::CancellationToken,
626 ) -> BoxFuture<'a, PluginResult<Value>> {
627 Box::pin(async move {
628 if cancel.is_cancelled() {
629 return Err(PluginError::Cancelled {
630 name: "isolate".into(),
631 });
632 }
633
634 let rhai_args: Vec<Dynamic> = args.iter().map(RhaiRuntime::value_to_dynamic).collect();
636
637 let ast_guard = self.ast.lock().map_err(|_| PluginError::ExecutionError {
639 name: "isolate".into(),
640 message: "AST mutex poisoned".to_string(),
641 })?;
642
643 let ast = ast_guard
644 .as_ref()
645 .ok_or_else(|| PluginError::ExecutionError {
646 name: "isolate".into(),
647 message: format!(
648 "Cannot call '{}': no code has been executed in this context yet. \
649 Call execute() with the script source first.",
650 name
651 ),
652 })?;
653
654 let mut scope = Scope::new();
655 let result = self
656 .engine
657 .call_fn::<Dynamic>(&mut scope, ast, name, rhai_args)
658 .map_err(|e| PluginError::ExecutionError {
659 name: "isolate".into(),
660 message: e.to_string(),
661 })?;
662
663 Ok(RhaiRuntime::dynamic_to_value(&result))
664 })
665 }
666
667 fn set_global(&mut self, _name: &str, _value: Value) -> PluginResult<()> {
668 Ok(())
671 }
672
673 fn get_global(&self, _name: &str) -> PluginResult<Value> {
674 Ok(Value::Null)
675 }
676}