1use std::collections::HashMap;
4use std::path::Path;
5
6use rhai::{Dynamic, Engine, Scope, AST};
7
8use crate::config::{PluginConfig, PluginMetadata};
9use crate::hooks::{Hook, HookContext, HookResult};
10use crate::runtime::{BoxFuture, IsolatedContext, PluginHandle, PluginRuntime};
11use crate::sandbox::SandboxConfig;
12use crate::types::{PluginError, PluginResult, Value};
13
14struct LoadedRhaiPlugin {
16 name: String,
18
19 ast: AST,
21
22 metadata: PluginMetadata,
24
25 hooks: Vec<String>,
27}
28
29pub struct RhaiRuntime {
31 engine: Engine,
33
34 plugins: HashMap<PluginHandle, LoadedRhaiPlugin>,
36
37 next_handle: usize,
39
40 config: Option<PluginConfig>,
42
43 initialized: bool,
45}
46
47impl RhaiRuntime {
48 pub fn new() -> PluginResult<Self> {
50 let mut engine = Engine::new();
51
52 engine.set_max_expr_depths(64, 64);
54 engine.set_max_call_levels(64);
55 engine.set_max_operations(1_000_000);
56 engine.set_max_modules(100);
57 engine.set_max_string_size(1024 * 1024); engine.set_max_array_size(10_000);
59 engine.set_max_map_size(10_000);
60
61 Ok(Self {
62 engine,
63 plugins: HashMap::new(),
64 next_handle: 0,
65 config: None,
66 initialized: false,
67 })
68 }
69
70 fn init_api(&mut self) -> PluginResult<()> {
72 self.engine.register_fn("log_info", |msg: &str| {
74 tracing::info!(target: "plugin", "{}", msg);
75 });
76
77 self.engine.register_fn("log_warn", |msg: &str| {
78 tracing::warn!(target: "plugin", "{}", msg);
79 });
80
81 self.engine.register_fn("log_error", |msg: &str| {
82 tracing::error!(target: "plugin", "{}", msg);
83 });
84
85 self.engine.register_fn("notify", |msg: &str| {
86 tracing::info!(target: "plugin_notify", "{}", msg);
87 });
88
89 self.engine
91 .register_fn("fs_exists", |path: &str| -> bool {
92 std::path::Path::new(path).exists()
93 });
94
95 self.engine
96 .register_fn("fs_is_dir", |path: &str| -> bool {
97 std::path::Path::new(path).is_dir()
98 });
99
100 self.engine
101 .register_fn("fs_is_file", |path: &str| -> bool {
102 std::path::Path::new(path).is_file()
103 });
104
105 self.engine
106 .register_fn("fs_read", |path: &str| -> Dynamic {
107 match std::fs::read_to_string(path) {
108 Ok(content) => Dynamic::from(content),
109 Err(_) => Dynamic::UNIT,
110 }
111 });
112
113 self.engine
114 .register_fn("fs_extension", |path: &str| -> Dynamic {
115 let p = std::path::Path::new(path);
116 match p.extension().and_then(|e| e.to_str()) {
117 Some(ext) => Dynamic::from(ext.to_string()),
118 None => Dynamic::UNIT,
119 }
120 });
121
122 self.engine
123 .register_fn("fs_filename", |path: &str| -> Dynamic {
124 let p = std::path::Path::new(path);
125 match p.file_name().and_then(|n| n.to_str()) {
126 Some(name) => Dynamic::from(name.to_string()),
127 None => Dynamic::UNIT,
128 }
129 });
130
131 self.engine
132 .register_fn("fs_parent", |path: &str| -> Dynamic {
133 let p = std::path::Path::new(path);
134 match p.parent().and_then(|p| p.to_str()) {
135 Some(parent) => Dynamic::from(parent.to_string()),
136 None => Dynamic::UNIT,
137 }
138 });
139
140 self.engine
141 .register_fn("fs_size", |path: &str| -> Dynamic {
142 match std::fs::metadata(path) {
143 Ok(meta) => Dynamic::from(meta.len() as i64),
144 Err(_) => Dynamic::from(-1_i64),
145 }
146 });
147
148 self.engine.register_fn(
150 "ui_span",
151 |text: &str, fg: &str| -> rhai::Map {
152 let mut map = rhai::Map::new();
153 map.insert("type".into(), Dynamic::from("span"));
154 map.insert("text".into(), Dynamic::from(text.to_string()));
155 map.insert("fg".into(), Dynamic::from(fg.to_string()));
156 map
157 },
158 );
159
160 self.engine.register_fn("ui_line", |spans: rhai::Array| -> rhai::Map {
161 let mut map = rhai::Map::new();
162 map.insert("type".into(), Dynamic::from("line"));
163 map.insert("spans".into(), Dynamic::from(spans));
164 map
165 });
166
167 Ok(())
168 }
169
170 fn dynamic_to_value(val: &Dynamic) -> Value {
172 if val.is_unit() {
173 Value::Null
174 } else if val.is_bool() {
175 Value::Bool(val.as_bool().unwrap_or(false))
176 } else if val.is_int() {
177 Value::Integer(val.as_int().unwrap_or(0))
178 } else if val.is_float() {
179 Value::Float(val.as_float().unwrap_or(0.0))
180 } else if val.is_string() {
181 Value::String(val.clone().into_string().unwrap_or_default())
182 } else if val.is_array() {
183 let arr = val.clone().into_array().unwrap_or_default();
184 Value::Array(arr.iter().map(Self::dynamic_to_value).collect())
185 } else if val.is_map() {
186 let map = val.clone().cast::<rhai::Map>();
187 let obj: std::collections::HashMap<String, Value> = map
188 .into_iter()
189 .map(|(k, v)| (k.to_string(), Self::dynamic_to_value(&v)))
190 .collect();
191 Value::Object(obj)
192 } else {
193 Value::Null
194 }
195 }
196
197 fn value_to_dynamic(val: &Value) -> Dynamic {
199 match val {
200 Value::Null => Dynamic::UNIT,
201 Value::Bool(b) => Dynamic::from(*b),
202 Value::Integer(i) => Dynamic::from(*i),
203 Value::Float(f) => Dynamic::from(*f),
204 Value::String(s) => Dynamic::from(s.clone()),
205 Value::Array(arr) => {
206 let rhai_arr: rhai::Array = arr.iter().map(Self::value_to_dynamic).collect();
207 Dynamic::from(rhai_arr)
208 }
209 Value::Object(obj) => {
210 let mut map = rhai::Map::new();
211 for (k, v) in obj {
212 map.insert(k.clone().into(), Self::value_to_dynamic(v));
213 }
214 Dynamic::from(map)
215 }
216 Value::Bytes(b) => Dynamic::from(b.clone()),
217 }
218 }
219
220 fn hook_to_dynamic(&self, hook: &Hook) -> Dynamic {
222 let json = serde_json::to_value(hook).unwrap_or(serde_json::Value::Null);
224
225 fn json_to_dynamic(val: &serde_json::Value) -> Dynamic {
226 match val {
227 serde_json::Value::Null => Dynamic::UNIT,
228 serde_json::Value::Bool(b) => Dynamic::from(*b),
229 serde_json::Value::Number(n) => {
230 if let Some(i) = n.as_i64() {
231 Dynamic::from(i)
232 } else {
233 Dynamic::from(n.as_f64().unwrap_or(0.0))
234 }
235 }
236 serde_json::Value::String(s) => Dynamic::from(s.clone()),
237 serde_json::Value::Array(arr) => {
238 let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect();
239 Dynamic::from(rhai_arr)
240 }
241 serde_json::Value::Object(obj) => {
242 let mut map = rhai::Map::new();
243 for (k, v) in obj {
244 map.insert(k.clone().into(), json_to_dynamic(v));
245 }
246 Dynamic::from(map)
247 }
248 }
249 }
250
251 json_to_dynamic(&json)
252 }
253}
254
255impl Default for RhaiRuntime {
256 fn default() -> Self {
257 Self::new().expect("Failed to create Rhai runtime")
258 }
259}
260
261impl PluginRuntime for RhaiRuntime {
262 fn name(&self) -> &'static str {
263 "rhai"
264 }
265
266 fn file_extensions(&self) -> &'static [&'static str] {
267 &[".rhai"]
268 }
269
270 fn init(&mut self, config: &PluginConfig) -> PluginResult<()> {
271 if self.initialized {
272 return Ok(());
273 }
274
275 self.config = Some(config.clone());
276 self.init_api()?;
277 self.initialized = true;
278
279 Ok(())
280 }
281
282 fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle> {
283 let code = std::fs::read_to_string(source)?;
285
286 let ast = self.engine.compile(&code).map_err(|e| PluginError::LoadError {
287 name: id.to_string(),
288 message: e.to_string(),
289 })?;
290
291 let mut hooks = vec![];
293 for func in ast.iter_functions() {
294 let name = func.name.to_string();
295 if name.starts_with("on_") {
296 hooks.push(name);
297 }
298 }
299
300 let handle = PluginHandle::new(self.next_handle);
301 self.next_handle += 1;
302
303 let metadata = PluginMetadata {
304 name: id.to_string(),
305 runtime: "rhai".to_string(),
306 ..Default::default()
307 };
308
309 self.plugins.insert(
310 handle,
311 LoadedRhaiPlugin {
312 name: id.to_string(),
313 ast,
314 metadata,
315 hooks,
316 },
317 );
318
319 Ok(handle)
320 }
321
322 fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()> {
323 self.plugins.remove(&handle);
324 Ok(())
325 }
326
327 fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata> {
328 self.plugins.get(&handle).map(|p| &p.metadata)
329 }
330
331 fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool {
332 self.plugins
333 .get(&handle)
334 .map(|p| p.hooks.contains(&hook_name.to_string()))
335 .unwrap_or(false)
336 }
337
338 fn call_hook_sync(
339 &self,
340 handle: PluginHandle,
341 hook: &Hook,
342 _ctx: &HookContext,
343 ) -> PluginResult<HookResult> {
344 let plugin = self.plugins.get(&handle).ok_or_else(|| PluginError::NotFound {
345 path: std::path::PathBuf::new(),
346 })?;
347
348 let hook_name = hook.name();
349 if !plugin.hooks.contains(&hook_name.to_string()) {
350 return Ok(HookResult::default());
351 }
352
353 let mut scope = Scope::new();
355 scope.push("hook", self.hook_to_dynamic(hook));
356
357 let result = self
359 .engine
360 .call_fn::<Dynamic>(&mut scope, &plugin.ast, hook_name, ())
361 .map_err(|e| PluginError::ExecutionError {
362 name: plugin.name.clone(),
363 message: e.to_string(),
364 })?;
365
366 let mut hook_result = HookResult::ok();
368 if result.is_map() {
369 if let Some(map) = result.try_cast::<rhai::Map>() {
370 if let Some(prevent) = map.get("prevent_default") {
371 if prevent.as_bool().unwrap_or(false) {
372 hook_result = hook_result.prevent_default();
373 }
374 }
375 if let Some(stop) = map.get("stop_propagation") {
376 if stop.as_bool().unwrap_or(false) {
377 hook_result = hook_result.stop_propagation();
378 }
379 }
380 if let Some(val) = map.get("value") {
381 hook_result.value = Some(Self::dynamic_to_value(val));
382 }
383 }
384 }
385
386 Ok(hook_result)
387 }
388
389 fn call_hook_async<'a>(
390 &'a self,
391 handle: PluginHandle,
392 hook: &'a Hook,
393 ctx: &'a HookContext,
394 ) -> BoxFuture<'a, PluginResult<HookResult>> {
395 Box::pin(async move { self.call_hook_sync(handle, hook, ctx) })
396 }
397
398 fn call_method<'a>(
399 &'a self,
400 handle: PluginHandle,
401 method: &'a str,
402 args: Vec<Value>,
403 ) -> BoxFuture<'a, PluginResult<Value>> {
404 Box::pin(async move {
405 let plugin = self.plugins.get(&handle).ok_or_else(|| PluginError::NotFound {
406 path: std::path::PathBuf::new(),
407 })?;
408
409 let mut scope = Scope::new();
410
411 let rhai_args: Vec<Dynamic> = args.iter().map(Self::value_to_dynamic).collect();
413
414 scope.push("args", rhai_args);
416
417 let result = self
418 .engine
419 .call_fn::<Dynamic>(&mut scope, &plugin.ast, method, ())
420 .map_err(|e| PluginError::ExecutionError {
421 name: plugin.name.clone(),
422 message: e.to_string(),
423 })?;
424
425 Ok(Self::dynamic_to_value(&result))
426 })
427 }
428
429 fn create_isolated_context(
430 &self,
431 sandbox: &SandboxConfig,
432 ) -> PluginResult<Box<dyn IsolatedContext>> {
433 Ok(Box::new(RhaiIsolatedContext::new(sandbox.clone())?))
434 }
435
436 fn loaded_plugins(&self) -> Vec<PluginHandle> {
437 self.plugins.keys().copied().collect()
438 }
439
440 fn shutdown(&mut self) -> PluginResult<()> {
441 self.plugins.clear();
442 Ok(())
443 }
444}
445
446struct RhaiIsolatedContext {
448 engine: Engine,
449 #[allow(dead_code)]
450 sandbox: SandboxConfig,
451}
452
453impl RhaiIsolatedContext {
454 fn new(sandbox: SandboxConfig) -> PluginResult<Self> {
455 let mut engine = Engine::new();
456
457 engine.set_max_expr_depths(32, 32);
459 engine.set_max_call_levels(32);
460 engine.set_max_operations(100_000);
461 engine.set_max_modules(10);
462 engine.set_max_string_size(100 * 1024); engine.set_max_array_size(1000);
464 engine.set_max_map_size(1000);
465
466 engine.disable_symbol("eval");
468
469 Ok(Self { engine, sandbox })
470 }
471}
472
473impl IsolatedContext for RhaiIsolatedContext {
474 fn execute<'a>(
475 &'a self,
476 code: &'a [u8],
477 cancel: tokio_util::sync::CancellationToken,
478 ) -> BoxFuture<'a, PluginResult<Value>> {
479 Box::pin(async move {
480 if cancel.is_cancelled() {
481 return Err(PluginError::Cancelled {
482 name: "isolate".into(),
483 });
484 }
485
486 let code_str = std::str::from_utf8(code).map_err(|e| PluginError::ExecutionError {
487 name: "isolate".into(),
488 message: format!("Invalid UTF-8: {}", e),
489 })?;
490
491 let result = self.engine.eval::<Dynamic>(code_str).map_err(|e| {
492 PluginError::ExecutionError {
493 name: "isolate".into(),
494 message: e.to_string(),
495 }
496 })?;
497
498 Ok(RhaiRuntime::dynamic_to_value(&result))
499 })
500 }
501
502 fn call_function<'a>(
503 &'a self,
504 name: &'a str,
505 args: Vec<Value>,
506 cancel: tokio_util::sync::CancellationToken,
507 ) -> BoxFuture<'a, PluginResult<Value>> {
508 Box::pin(async move {
509 if cancel.is_cancelled() {
510 return Err(PluginError::Cancelled {
511 name: "isolate".into(),
512 });
513 }
514
515 let args_str: Vec<String> = args
517 .iter()
518 .map(|v| match v {
519 Value::String(s) => format!("\"{}\"", s.replace("\"", "\\\"")),
520 Value::Integer(i) => i.to_string(),
521 Value::Float(f) => f.to_string(),
522 Value::Bool(b) => b.to_string(),
523 _ => "()".to_string(),
524 })
525 .collect();
526
527 let code = format!("{}({})", name, args_str.join(", "));
528
529 let result = self.engine.eval::<Dynamic>(&code).map_err(|e| {
530 PluginError::ExecutionError {
531 name: "isolate".into(),
532 message: e.to_string(),
533 }
534 })?;
535
536 Ok(RhaiRuntime::dynamic_to_value(&result))
537 })
538 }
539
540 fn set_global(&mut self, _name: &str, _value: Value) -> PluginResult<()> {
541 Ok(())
544 }
545
546 fn get_global(&self, _name: &str) -> PluginResult<Value> {
547 Ok(Value::Null)
548 }
549}