1use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
13use std::time::{Duration, Instant};
14
15use rquickjs::function::{Async, Func};
16use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module, Object, Value, async_with};
17
18use crate::console::{ConsoleCapture, strip_ansi};
19use crate::error::{ScriptError, ScriptErrorKind};
20use crate::fs::PathSandbox;
21use crate::result::{ConsoleLevel, ScriptResult};
22use crate::vars::VarsStore;
23
24pub const DEFAULT_MAX_CONSOLE_ENTRIES: usize = 1_000;
26pub const DEFAULT_MAX_CONSOLE_BYTES: usize = 1_048_576;
27pub const DEFAULT_MAX_CONSOLE_ENTRY_BYTES: usize = 8_192;
28
29pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300);
31
32pub const DEFAULT_MEMORY_LIMIT: usize = 256 * 1024 * 1024;
34
35pub const DEFAULT_STACK_SIZE: usize = 1024 * 1024;
37
38pub const DEFAULT_GC_THRESHOLD: usize = 64 * 1024 * 1024;
49
50pub const DEFAULT_MAX_SESSION_VMS: usize = 64;
54
55pub const DEFAULT_SESSION_IDLE_TTL: Duration = Duration::from_secs(30 * 60);
59
60#[derive(Debug, Clone)]
62pub struct ScriptEngineConfig {
63 pub default_timeout: Duration,
64 pub default_memory_limit: usize,
65 pub default_stack_size: usize,
66 pub default_gc_threshold: usize,
68 pub max_console_entries: usize,
69 pub max_console_bytes: usize,
70 pub max_console_entry_bytes: usize,
71 pub max_session_vms: usize,
73 pub session_idle_ttl: Option<Duration>,
76}
77
78impl Default for ScriptEngineConfig {
79 fn default() -> Self {
80 Self {
81 default_timeout: DEFAULT_TIMEOUT,
82 default_memory_limit: DEFAULT_MEMORY_LIMIT,
83 default_stack_size: DEFAULT_STACK_SIZE,
84 default_gc_threshold: DEFAULT_GC_THRESHOLD,
85 max_console_entries: DEFAULT_MAX_CONSOLE_ENTRIES,
86 max_console_bytes: DEFAULT_MAX_CONSOLE_BYTES,
87 max_console_entry_bytes: DEFAULT_MAX_CONSOLE_ENTRY_BYTES,
88 max_session_vms: DEFAULT_MAX_SESSION_VMS,
89 session_idle_ttl: Some(DEFAULT_SESSION_IDLE_TTL),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct RunOptions {
97 pub timeout: Option<Duration>,
98 pub memory_limit: Option<usize>,
99 pub stack_size: Option<usize>,
100 pub gc_threshold: Option<usize>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub enum ExtensionHost {
110 Mcp,
112 Bdd,
114 #[default]
116 Script,
117}
118
119impl ExtensionHost {
120 #[must_use]
121 pub fn as_str(self) -> &'static str {
122 match self {
123 Self::Mcp => "mcp",
124 Self::Bdd => "bdd",
125 Self::Script => "script",
126 }
127 }
128}
129
130#[derive(Clone)]
136pub struct RunContext {
137 pub vars: Arc<dyn VarsStore>,
138 pub sandbox: Arc<PathSandbox>,
139 pub artifacts: Option<Arc<PathSandbox>>,
142 pub page: Option<Arc<ferridriver::Page>>,
143 pub browser_context: Option<Arc<ferridriver::context::ContextRef>>,
144 pub request: Option<Arc<ferridriver::http_client::HttpClient>>,
145 pub browser: Option<Arc<ferridriver::Browser>>,
150 pub plugins: Vec<crate::bindings::PluginBinding>,
153 pub trusted_modules: bool,
159 pub host: ExtensionHost,
162 pub caps: ScriptCaps,
165}
166
167#[derive(Debug, Clone, Default)]
171pub struct ScriptCaps {
172 pub env: std::collections::BTreeMap<String, String>,
176}
177
178impl ScriptCaps {
179 #[must_use]
184 pub fn resolve(allow_env: &[String]) -> Self {
185 let env = allow_env
186 .iter()
187 .filter_map(|k| std::env::var(k).ok().map(|v| (k.clone(), v)))
188 .collect();
189 Self { env }
190 }
191}
192
193pub(crate) struct SessionAsyncCtx(pub(crate) AsyncContext);
199
200#[allow(unsafe_code)]
203unsafe impl rquickjs::JsLifetime<'_> for SessionAsyncCtx {
204 type Changed<'to> = SessionAsyncCtx;
205}
206
207pub(crate) struct SessionProcsUd(pub(crate) std::sync::Arc<crate::session_procs::SessionProcs>);
214
215#[allow(unsafe_code)]
217unsafe impl rquickjs::JsLifetime<'_> for SessionProcsUd {
218 type Changed<'to> = SessionProcsUd;
219}
220
221pub struct ScriptEngine {
223 config: ScriptEngineConfig,
224}
225
226impl ScriptEngine {
227 #[must_use]
228 pub fn new(config: ScriptEngineConfig) -> Self {
229 Self { config }
230 }
231
232 #[must_use]
233 pub fn config(&self) -> &ScriptEngineConfig {
234 &self.config
235 }
236
237 pub async fn run(
246 &self,
247 source: &str,
248 args: &[serde_json::Value],
249 options: RunOptions,
250 context: RunContext,
251 ) -> ScriptResult {
252 match Session::create(self.config.clone(), &context).await {
253 Ok(session) => session.execute(source, args, options, &context).await.result,
254 Err(e) => ScriptResult::err(e, 0, Vec::new()),
255 }
256 }
257}
258
259#[derive(Debug)]
265pub struct SessionRun {
266 pub result: ScriptResult,
267 pub poisoned: bool,
268}
269
270pub struct Session {
284 runtime: AsyncRuntime,
285 ctx: AsyncContext,
286 config: ScriptEngineConfig,
287 default_request: Arc<ferridriver::http_client::HttpClient>,
288 applied: AppliedLimits,
295}
296
297struct AppliedLimits {
300 memory: AtomicUsize,
301 stack: AtomicUsize,
302 gc: AtomicUsize,
303}
304
305impl Session {
306 pub async fn create(config: ScriptEngineConfig, context: &RunContext) -> Result<Self, ScriptError> {
311 let runtime = AsyncRuntime::new().map_err(|e| ScriptError::internal(format!("rquickjs runtime init: {e}")))?;
312
313 runtime.set_memory_limit(config.default_memory_limit).await;
314 runtime.set_max_stack_size(config.default_stack_size).await;
315 runtime.set_gc_threshold(config.default_gc_threshold).await;
319
320 if context.trusted_modules {
326 let mut resolver = rquickjs::loader::FileResolver::default();
330 resolver.add_path(".");
331 resolver.add_path(context.sandbox.root().to_string_lossy().as_ref());
332 runtime
333 .set_loader(resolver, rquickjs::loader::ScriptLoader::default())
334 .await;
335 } else {
336 runtime
337 .set_loader(
338 crate::modules::SandboxResolver::new(context.sandbox.clone()),
339 crate::modules::SandboxLoader::new(context.sandbox.clone()),
340 )
341 .await;
342 }
343
344 let ctx = AsyncContext::full(&runtime)
345 .await
346 .map_err(|e| ScriptError::internal(format!("rquickjs context init: {e}")))?;
347
348 let plugins = context.plugins.clone();
353 let vars = context.vars.clone();
356 let sandbox = context.sandbox.clone();
357 let sandbox_root = context.sandbox.root().to_string_lossy().into_owned();
358 let artifacts = context.artifacts.clone();
359 let host = context.host;
360 let caps = context.caps.clone();
361 let ud_ctx = ctx.clone();
362 let install: Result<(), ScriptError> = async_with!(ctx => |ctx| {
363 let _ = ctx.store_userdata(SessionAsyncCtx(ud_ctx));
368 let _ = ctx.store_userdata(crate::bindings::fetch::NetPolicyUd(
373 crate::bindings::fetch::NetPolicy::default(),
374 ));
375 crate::bindings::page::ensure_page_callbacks(&ctx);
380 install_runtime_shims(&ctx).map_err(|e| ScriptError::internal(format!("failed to install runtime shims: {e}")))?;
381
382 crate::bindings::define_classes(&ctx)
389 .map_err(|e| ScriptError::internal(format!("failed to define classes: {e}")))?;
390 install_vars(&ctx, vars).map_err(|e| ScriptError::internal(format!("failed to install vars: {e}")))?;
391 install_fs(&ctx, sandbox).map_err(|e| ScriptError::internal(format!("failed to install fs: {e}")))?;
392 crate::bindings::process::install(&ctx, &caps, &sandbox_root)
393 .map_err(|e| ScriptError::internal(format!("failed to install process: {e}")))?;
394 if let Some(artifacts) = artifacts {
395 crate::bindings::install_artifacts(&ctx, artifacts)
396 .map_err(|e| ScriptError::internal(format!("failed to install artifacts: {e}")))?;
397 }
398 crate::bindings::install_browser_type(&ctx)
399 .map_err(|e| ScriptError::internal(format!("failed to install browser_type: {e}")))?;
400
401 crate::bindings::expect::install_expect(&ctx)
406 .map_err(|e| ScriptError::internal(format!("failed to install expect: {e}")))?;
407
408 crate::bindings::install_bdd(&ctx)
414 .map_err(|e| ScriptError::internal(format!("failed to install extension registry: {e}")))?;
415
416 let fd = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(format!("ferridriver global: {e}")))?;
419 fd.set("host", host.as_str())
420 .map_err(|e| ScriptError::internal(format!("ferridriver.host: {e}")))?;
421 ctx
422 .globals()
423 .set("ferridriver", fd)
424 .map_err(|e| ScriptError::internal(format!("install ferridriver global: {e}")))?;
425
426 crate::bindings::install_plugins(&ctx, &plugins)
427 .map_err(|e| ScriptError::internal(format!("failed to install plugins: {e}")))
428 })
429 .await;
430 install?;
431
432 let applied = AppliedLimits {
433 memory: AtomicUsize::new(config.default_memory_limit),
434 stack: AtomicUsize::new(config.default_stack_size),
435 gc: AtomicUsize::new(config.default_gc_threshold),
436 };
437 Ok(Self {
438 runtime,
439 ctx,
440 config,
441 default_request: Arc::new(ferridriver::http_client::HttpClient::new(
442 ferridriver::http_client::HttpClientOptions::default(),
443 )),
444 applied,
445 })
446 }
447
448 #[must_use]
452 pub fn async_context(&self) -> AsyncContext {
453 self.ctx.clone()
454 }
455
456 pub async fn install_session_procs(&self, procs: std::sync::Arc<crate::session_procs::SessionProcs>) {
461 async_with!(self.ctx => |ctx| {
462 let _ = ctx.store_userdata(SessionProcsUd(procs));
463 })
464 .await;
465 }
466
467 async fn apply_limits(&self, memory: usize, stack: usize, gc: usize) {
471 if self.applied.memory.swap(memory, Ordering::Relaxed) != memory {
472 self.runtime.set_memory_limit(memory).await;
473 }
474 if self.applied.stack.swap(stack, Ordering::Relaxed) != stack {
475 self.runtime.set_max_stack_size(stack).await;
476 }
477 if self.applied.gc.swap(gc, Ordering::Relaxed) != gc {
478 self.runtime.set_gc_threshold(gc).await;
479 }
480 }
481
482 fn new_console(&self) -> Arc<ConsoleCapture> {
484 Arc::new(ConsoleCapture::new(
485 self.config.max_console_entries,
486 self.config.max_console_bytes,
487 self.config.max_console_entry_bytes,
488 ))
489 }
490
491 async fn arm_timeout(&self, deadline: Instant) -> Arc<AtomicBool> {
495 let timed_out = Arc::new(AtomicBool::new(false));
496 let flag = timed_out.clone();
497 self
498 .runtime
499 .set_interrupt_handler(Some(Box::new(move || {
500 if Instant::now() >= deadline {
501 flag.store(true, Ordering::Relaxed);
502 true
503 } else {
504 false
505 }
506 })))
507 .await;
508 timed_out
509 }
510
511 fn globals_install(&self, context: &RunContext, console: &Arc<ConsoleCapture>) -> GlobalsInstall {
513 GlobalsInstall {
514 console: console.clone(),
515 page: context.page.clone(),
516 browser_context: context.browser_context.clone(),
517 request: context.request.clone(),
518 default_request: self.default_request.clone(),
519 browser: context.browser.clone(),
520 async_ctx: self.ctx.clone(),
521 }
522 }
523
524 fn finish(
528 &self,
529 eval_result: Result<serde_json::Value, ScriptError>,
530 started: Instant,
531 console: &Arc<ConsoleCapture>,
532 timed_out: &Arc<AtomicBool>,
533 timeout: Duration,
534 ) -> SessionRun {
535 let duration = elapsed_ms(started);
536 let drained = console.drain();
537 match eval_result {
538 Ok(value) => SessionRun {
539 result: ScriptResult::ok(value, duration, drained),
540 poisoned: false,
541 },
542 Err(mut err) => {
543 let timed_out = timed_out.load(Ordering::Relaxed);
544 let oom = is_oom(&err);
545 let poisoned = timed_out || oom;
546 if timed_out {
547 err = ScriptError::timeout(duration, timeout.as_millis() as u64);
548 }
549 SessionRun {
550 result: ScriptResult::err(err, duration, drained),
551 poisoned,
552 }
553 },
554 }
555 }
556
557 async fn apply_call_limits(&self, options: &RunOptions) -> Duration {
560 self
561 .apply_limits(
562 options.memory_limit.unwrap_or(self.config.default_memory_limit),
563 options.stack_size.unwrap_or(self.config.default_stack_size),
564 options.gc_threshold.unwrap_or(self.config.default_gc_threshold),
565 )
566 .await;
567 options.timeout.unwrap_or(self.config.default_timeout)
568 }
569
570 pub async fn execute(
578 &self,
579 source: &str,
580 args: &[serde_json::Value],
581 options: RunOptions,
582 context: &RunContext,
583 ) -> SessionRun {
584 let started = Instant::now();
585 let console = self.new_console();
586 let timeout = self.apply_call_limits(&options).await;
587 let timed_out = self.arm_timeout(started + timeout).await;
588 let install = self.globals_install(context, &console);
589 let source_owned = source.to_string();
590
591 let eval_result: Result<serde_json::Value, ScriptError> = async_with!(self.ctx => |ctx| {
592 if let Err(e) = install_call_globals(&ctx, args, install) {
593 return Err(ScriptError::internal(format!("failed to install globals: {e}")));
594 }
595
596 let wrapped = wrap_source(&source_owned);
597
598 let promise: rquickjs::Promise<'_> = match ctx.eval(wrapped.as_bytes()) {
599 Ok(v) => v,
600 Err(e) => return Err(caught_to_script_error(rquickjs::CaughtError::from_error(&ctx, e), &source_owned)),
601 };
602
603 let result: Value<'_> = match promise.into_future::<Value<'_>>().await {
604 Ok(v) => v,
605 Err(e) => return Err(caught_to_script_error(rquickjs::CaughtError::from_error(&ctx, e), &source_owned)),
606 };
607
608 Ok(value_to_json(&ctx, result).unwrap_or(serde_json::Value::Null))
609 })
610 .await;
611
612 self.finish(eval_result, started, &console, &timed_out, timeout)
613 }
614
615 pub async fn execute_module(
625 &self,
626 bundle: &crate::bundle::CompiledBundle,
627 args: &[serde_json::Value],
628 options: RunOptions,
629 context: &RunContext,
630 ) -> SessionRun {
631 let started = Instant::now();
632 let console = self.new_console();
633 let timeout = self.apply_call_limits(&options).await;
634 let timed_out = self.arm_timeout(started + timeout).await;
635 let install = self.globals_install(context, &console);
636 let bytecode = Arc::clone(&bundle.bytecode);
637 let label = bundle.module_name.clone();
638
639 let eval_result: Result<serde_json::Value, ScriptError> = async_with!(self.ctx => |ctx| {
640 if let Err(e) = install_call_globals(&ctx, args, install) {
641 return Err(ScriptError::internal(format!("failed to install globals: {e}")));
642 }
643
644 #[allow(unsafe_code)]
650 let module = match (unsafe { Module::load(ctx.clone(), &bytecode) }).catch(&ctx) {
651 Ok(m) => m,
652 Err(e) => return Err(caught_to_script_error(e, &label)),
653 };
654 let (evaluated, promise) = match module.eval().catch(&ctx) {
655 Ok(v) => v,
656 Err(e) => return Err(caught_to_script_error(e, &label)),
657 };
658 if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
659 return Err(caught_to_script_error(e, &label));
660 }
661
662 let default = evaluated
664 .namespace()
665 .and_then(|ns| ns.get::<_, Value<'_>>("default"))
666 .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
667 Ok(value_to_json(&ctx, default).unwrap_or(serde_json::Value::Null))
668 })
669 .await;
670
671 let eval_result = eval_result.map_err(|mut e| {
673 if let Some(line) = e.line {
674 if let Some((src, sl, sc)) = bundle.remap(line, e.column.unwrap_or(1)) {
675 e.message = format!("{} (at {src}:{sl}:{sc})", e.message);
676 }
677 }
678 e
679 });
680
681 self.finish(eval_result, started, &console, &timed_out, timeout)
682 }
683}
684
685fn wrap_source(source: &str) -> String {
688 format!("(async () => {{\n{source}\n}})()")
689}
690
691fn is_oom(err: &ScriptError) -> bool {
696 err.message.to_ascii_lowercase().contains("out of memory")
697}
698
699struct GlobalsInstall {
703 console: Arc<ConsoleCapture>,
704 page: Option<Arc<ferridriver::Page>>,
705 browser_context: Option<Arc<ferridriver::context::ContextRef>>,
706 request: Option<Arc<ferridriver::http_client::HttpClient>>,
707 default_request: Arc<ferridriver::http_client::HttpClient>,
708 browser: Option<Arc<ferridriver::Browser>>,
709 async_ctx: AsyncContext,
713}
714
715fn install_call_globals(ctx: &Ctx<'_>, args: &[serde_json::Value], inst: GlobalsInstall) -> rquickjs::Result<()> {
722 let globals = ctx.globals();
723
724 let args_arr = rquickjs::Array::new(ctx.clone())?;
728 for (i, a) in args.iter().enumerate() {
729 args_arr.set(i, crate::bindings::convert::json_to_js(ctx, a)?)?;
730 }
731 globals.set("args", args_arr)?;
732
733 install_console(ctx, inst.console)?;
734
735 if let Some(page) = inst.page {
736 crate::bindings::install_page(ctx, page, inst.async_ctx.clone())?;
737 }
738 if let Some(bcx) = inst.browser_context {
739 crate::bindings::install_browser_context(ctx, bcx)?;
740 }
741 if let Some(browser) = inst.browser {
742 crate::bindings::install_browser(ctx, browser)?;
743 }
744 if let Some(req) = inst.request {
745 crate::bindings::fetch::install(ctx, req.clone())?;
746 crate::bindings::install_request(ctx, req)?;
747 } else {
748 crate::bindings::fetch::install(ctx, inst.default_request)?;
752 }
753
754 Ok(())
755}
756
757fn install_console(ctx: &Ctx<'_>, capture: Arc<ConsoleCapture>) -> rquickjs::Result<()> {
758 use std::fmt::Write as _;
759
760 use rquickjs::function::Rest;
761
762 let formatter = rquickjs_extra_console::Formatter::builder().max_depth(3).build();
769 let console = Object::new(ctx.clone())?;
770
771 for (name, level) in [
772 ("log", ConsoleLevel::Log),
773 ("info", ConsoleLevel::Info),
774 ("warn", ConsoleLevel::Warn),
775 ("error", ConsoleLevel::Error),
776 ("debug", ConsoleLevel::Debug),
777 ] {
778 let cap = capture.clone();
779 let fmt = formatter.clone();
780 console.set(
781 name,
782 Func::from(move |args: Rest<Value<'_>>| -> rquickjs::Result<()> {
783 let mut msg = String::new();
784 for (i, v) in args.0.into_iter().enumerate() {
785 if i > 0 {
786 let _ = msg.write_char(' ');
787 }
788 fmt.format(&mut msg, v)?;
789 }
790 cap.push(level, strip_ansi(&msg));
791 Ok(())
792 }),
793 )?;
794 }
795
796 ctx.globals().set("console", console)?;
797 Ok(())
798}
799
800fn install_runtime_shims(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
809 rquickjs_extra_timers::init(ctx)?;
812 rquickjs_extra_url::init(ctx)?;
813 crate::bindings::webapi::install(ctx)?;
816 Ok(())
817}
818
819fn install_vars(ctx: &Ctx<'_>, vars: Arc<dyn VarsStore>) -> rquickjs::Result<()> {
820 let obj = Object::new(ctx.clone())?;
821
822 {
823 let v = vars.clone();
824 obj.set("get", Func::from(move |name: String| v.get(&name)))?;
825 }
826 {
827 let v = vars.clone();
828 obj.set(
829 "set",
830 Func::from(move |name: String, value: String| {
831 v.set(&name, value);
832 }),
833 )?;
834 }
835 {
836 let v = vars.clone();
837 obj.set("has", Func::from(move |name: String| v.has(&name)))?;
838 }
839 {
840 let v = vars.clone();
841 obj.set(
842 "delete",
843 Func::from(move |name: String| {
844 v.delete(&name);
845 }),
846 )?;
847 }
848 {
849 let v = vars.clone();
850 obj.set("keys", Func::from(move || v.keys()))?;
851 }
852
853 ctx.globals().set("vars", obj)?;
854 Ok(())
855}
856
857fn install_fs(ctx: &Ctx<'_>, sandbox: Arc<PathSandbox>) -> rquickjs::Result<()> {
858 let obj = Object::new(ctx.clone())?;
859
860 {
861 let sb = sandbox.clone();
862 obj.set(
863 "readFile",
864 Func::from(Async(move |path: String| {
865 let sb = sb.clone();
866 async move {
867 let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
868 tokio::fs::read_to_string(&resolved)
869 .await
870 .map_err(|e| rquickjs::Error::new_from_js_message("fs", "readFile", e.to_string()))
871 }
872 })),
873 )?;
874 }
875 {
876 let sb = sandbox.clone();
877 obj.set(
878 "readFileBytes",
879 Func::from(Async(move |path: String| {
880 let sb = sb.clone();
881 async move {
882 let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
883 tokio::fs::read(&resolved)
884 .await
885 .map_err(|e| rquickjs::Error::new_from_js_message("fs", "readFileBytes", e.to_string()))
886 }
887 })),
888 )?;
889 }
890 {
891 let sb = sandbox.clone();
892 obj.set(
893 "writeFile",
894 Func::from(Async(move |path: String, contents: String| {
895 let sb = sb.clone();
896 async move {
897 let resolved = sb.resolve_write(&path).map_err(|e| to_rq_error(&e))?;
898 tokio::fs::write(&resolved, contents)
899 .await
900 .map_err(|e| rquickjs::Error::new_from_js_message("fs", "writeFile", e.to_string()))
901 }
902 })),
903 )?;
904 }
905 {
906 let sb = sandbox.clone();
907 obj.set(
908 "readdir",
909 Func::from(Async(move |path: String| {
910 let sb = sb.clone();
911 async move {
912 let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
913 let mut entries = tokio::fs::read_dir(&resolved)
914 .await
915 .map_err(|e| rquickjs::Error::new_from_js_message("fs", "readdir", e.to_string()))?;
916 let mut names: Vec<String> = Vec::new();
917 while let Some(entry) = entries
918 .next_entry()
919 .await
920 .map_err(|e| rquickjs::Error::new_from_js_message("fs", "readdir", e.to_string()))?
921 {
922 names.push(entry.file_name().to_string_lossy().into_owned());
923 }
924 Ok::<_, rquickjs::Error>(names)
925 }
926 })),
927 )?;
928 }
929 {
930 let sb = sandbox.clone();
931 obj.set(
932 "exists",
933 Func::from(Async(move |path: String| {
934 let sb = sb.clone();
935 async move {
936 match sb.resolve_read(&path) {
938 Ok(resolved) => Ok::<bool, rquickjs::Error>(tokio::fs::try_exists(&resolved).await.unwrap_or(false)),
939 Err(_) => Ok(false),
940 }
941 }
942 })),
943 )?;
944 }
945
946 obj.set("root", sandbox.root().to_string_lossy().into_owned())?;
948
949 ctx.globals().set("fs", obj)?;
950 Ok(())
951}
952
953fn to_rq_error(err: &ScriptError) -> rquickjs::Error {
954 rquickjs::Error::new_from_js_message("fs", "sandbox", err.message.clone())
958}
959
960fn value_to_json<'js>(_ctx: &Ctx<'js>, value: Value<'js>) -> Option<serde_json::Value> {
976 rquickjs_serde::from_value::<JsonInter>(value)
977 .ok()
978 .map(JsonInter::into_json)
979}
980
981enum JsonInter {
985 Null,
986 Bool(bool),
987 I64(i64),
988 U64(u64),
989 F64(f64),
990 Str(String),
991 Arr(Vec<JsonInter>),
992 Obj(Vec<(String, JsonInter)>),
993}
994
995impl JsonInter {
996 fn into_json(self) -> serde_json::Value {
997 use serde_json::Value;
998 match self {
999 Self::Null => Value::Null,
1000 Self::Bool(b) => Value::Bool(b),
1001 Self::I64(n) => Value::Number(n.into()),
1002 Self::U64(n) => Value::Number(n.into()),
1003 Self::F64(f) => serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number),
1004 Self::Str(s) => Value::String(s),
1005 Self::Arr(a) => Value::Array(a.into_iter().map(Self::into_json).collect()),
1006 Self::Obj(o) => Value::Object(o.into_iter().map(|(k, v)| (k, v.into_json())).collect()),
1007 }
1008 }
1009}
1010
1011impl<'de> serde::Deserialize<'de> for JsonInter {
1012 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1013 struct V;
1014 impl<'de> serde::de::Visitor<'de> for V {
1015 type Value = JsonInter;
1016 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1017 f.write_str("any JSON value")
1018 }
1019 fn visit_unit<E>(self) -> Result<JsonInter, E> {
1020 Ok(JsonInter::Null)
1021 }
1022 fn visit_none<E>(self) -> Result<JsonInter, E> {
1023 Ok(JsonInter::Null)
1024 }
1025 fn visit_bool<E>(self, v: bool) -> Result<JsonInter, E> {
1026 Ok(JsonInter::Bool(v))
1027 }
1028 fn visit_i64<E>(self, v: i64) -> Result<JsonInter, E> {
1029 Ok(JsonInter::I64(v))
1030 }
1031 fn visit_u64<E>(self, v: u64) -> Result<JsonInter, E> {
1032 Ok(JsonInter::U64(v))
1033 }
1034 fn visit_f64<E>(self, v: f64) -> Result<JsonInter, E> {
1035 Ok(JsonInter::F64(v))
1036 }
1037 fn visit_str<E>(self, v: &str) -> Result<JsonInter, E> {
1038 Ok(JsonInter::Str(v.to_owned()))
1039 }
1040 fn visit_string<E>(self, v: String) -> Result<JsonInter, E> {
1041 Ok(JsonInter::Str(v))
1042 }
1043 fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<JsonInter, A::Error> {
1044 let mut out = Vec::new();
1045 while let Some(e) = a.next_element()? {
1046 out.push(e);
1047 }
1048 Ok(JsonInter::Arr(out))
1049 }
1050 fn visit_map<A: serde::de::MapAccess<'de>>(self, mut m: A) -> Result<JsonInter, A::Error> {
1051 let mut out = Vec::new();
1052 while let Some((k, v)) = m.next_entry()? {
1053 out.push((k, v));
1054 }
1055 Ok(JsonInter::Obj(out))
1056 }
1057 }
1058 d.deserialize_any(V)
1059 }
1060}
1061
1062pub(crate) fn caught_to_script_error(caught: rquickjs::CaughtError<'_>, source: &str) -> ScriptError {
1063 let (message, stack, line, column) = match caught {
1064 rquickjs::CaughtError::Exception(ex) => {
1065 let message = ex.message().unwrap_or_else(|| "exception".to_string());
1066 let stack = ex.stack();
1067 let obj = ex.as_object();
1070 let line = obj.get::<_, u32>("lineNumber").ok();
1071 let column = obj.get::<_, u32>("columnNumber").ok();
1072 (message, stack, line, column)
1073 },
1074 rquickjs::CaughtError::Value(v) => (format!("{v:?}"), None, None, None),
1075 rquickjs::CaughtError::Error(e) => (format!("{e}"), None, None, None),
1076 };
1077
1078 ScriptError {
1079 kind: ScriptErrorKind::Runtime,
1080 message,
1081 stack,
1082 line,
1083 column,
1084 source_snippet: line.and_then(|l| snippet_around_line(source, l, 2)),
1085 }
1086}
1087
1088fn snippet_around_line(source: &str, line_1based: u32, context_lines: u32) -> Option<String> {
1091 use std::fmt::Write as _;
1092 let lines: Vec<&str> = source.lines().collect();
1093 if lines.is_empty() {
1094 return None;
1095 }
1096 let target = line_1based.saturating_sub(1) as usize;
1097 let start = target.saturating_sub(context_lines as usize);
1098 let end = (target + context_lines as usize + 1).min(lines.len());
1099 let mut out = String::new();
1100 for (i, text) in lines[start..end].iter().enumerate() {
1101 let ln = start + i + 1;
1102 let marker = if ln == line_1based as usize { ">>>" } else { " " };
1103 let _ = writeln!(out, "{marker} {ln:>4}: {text}");
1104 }
1105 Some(out)
1106}
1107
1108fn elapsed_ms(started: Instant) -> u64 {
1109 started.elapsed().as_millis() as u64
1110}