1use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::time::{Duration, Instant};
18
19use ferrijs_permissions::{Container, Permissions};
20use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module, Value};
21
22use crate::console::{ConsoleCapture, ConsoleSink};
23use crate::console_fmt::install_console;
24use crate::error::{ScriptError, ScriptErrorKind};
25use crate::extension::Extension;
26use crate::limits::{AppliedLimits, Deadline, Limits, NeverParked, PauseClock, RunOptions, TimeoutState, run_within};
27use crate::modules::{
28 BoxLoader, BoxResolver, FileLoader, FileResolver, LoaderChain, ModulePolicy, ModuleRegistry, RequireHook,
29 ResolverChain,
30};
31use crate::realm::RealmOptions;
32use crate::redact::Redactor;
33use crate::result::ConsoleEntry;
34use crate::source_map::{CompiledModule, SourceMapper};
35use crate::vm::{VmHandle, VmShutdown, spawn_vm_loop};
36use crate::vm_with;
37
38pub const DEFAULT_MAX_CONSOLE_ENTRIES: usize = 1_000;
40pub const DEFAULT_MAX_CONSOLE_BYTES: usize = 1_048_576;
41pub const DEFAULT_MAX_CONSOLE_ENTRY_BYTES: usize = 8_192;
42
43#[derive(Clone)]
45pub struct ConsoleOptions {
46 pub max_entries: usize,
47 pub max_bytes: usize,
48 pub max_entry_bytes: usize,
49 pub sink: Option<Arc<dyn ConsoleSink>>,
59}
60
61impl Default for ConsoleOptions {
62 fn default() -> Self {
63 Self {
64 max_entries: DEFAULT_MAX_CONSOLE_ENTRIES,
65 max_bytes: DEFAULT_MAX_CONSOLE_BYTES,
66 max_entry_bytes: DEFAULT_MAX_CONSOLE_ENTRY_BYTES,
67 sink: None,
68 }
69 }
70}
71
72impl std::fmt::Debug for ConsoleOptions {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("ConsoleOptions")
75 .field("max_entries", &self.max_entries)
76 .field("max_bytes", &self.max_bytes)
77 .field("max_entry_bytes", &self.max_entry_bytes)
78 .field("sink", &self.sink.is_some())
79 .finish()
80 }
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct ProcessOptions {
86 pub cwd: Option<String>,
88 pub argv: Vec<String>,
90}
91
92pub struct Config {
94 pub limits: Limits,
95 pub console: ConsoleOptions,
96 pub realm: RealmOptions,
97 pub modules: ModulePolicy,
98 pub process: ProcessOptions,
99 pub identity: ferrijs_std::identity::Identity,
100 pub permissions: Arc<Container>,
101 pub redactor: Option<Arc<dyn Redactor>>,
102 pub pause_clock: Arc<dyn PauseClock>,
103 pub fs_global: bool,
106 pub timers: bool,
109 #[cfg(feature = "fetch")]
112 pub fetch: Option<Arc<dyn crate::fetch::FetchBackend>>,
113 pub extensions: Vec<Arc<dyn Extension>>,
114}
115
116impl std::fmt::Debug for Config {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("Config")
119 .field("limits", &self.limits)
120 .field("console", &self.console)
121 .field("realm", &self.realm)
122 .field("modules", &self.modules)
123 .field("process", &self.process)
124 .field("identity", &self.identity)
125 .field("permissions", &self.permissions)
126 .field("fs_global", &self.fs_global)
127 .field("timers", &self.timers)
128 .field(
129 "extensions",
130 &self.extensions.iter().map(|e| e.name().to_string()).collect::<Vec<_>>(),
131 )
132 .finish_non_exhaustive()
133 }
134}
135
136pub struct Builder {
139 config: Config,
140}
141
142impl Default for Builder {
143 fn default() -> Self {
144 Self {
145 config: Config {
146 limits: Limits::default(),
147 console: ConsoleOptions::default(),
148 realm: RealmOptions::default(),
149 modules: ModulePolicy::default(),
150 process: ProcessOptions::default(),
151 identity: ferrijs_std::identity::Identity::default(),
152 permissions: Arc::new(Container::new(Permissions::none())),
153 redactor: None,
154 pause_clock: Arc::new(NeverParked),
155 fs_global: false,
156 timers: true,
157 #[cfg(feature = "fetch")]
158 fetch: Some(Arc::new(crate::fetch::Client::new())),
159 extensions: Vec::new(),
160 },
161 }
162 }
163}
164
165impl Builder {
166 #[must_use]
167 pub fn limits(mut self, limits: Limits) -> Self {
168 self.config.limits = limits;
169 self
170 }
171
172 #[must_use]
173 pub fn console(mut self, console: ConsoleOptions) -> Self {
174 self.config.console = console;
175 self
176 }
177
178 #[must_use]
179 pub fn realm(mut self, realm: RealmOptions) -> Self {
180 self.config.realm = realm;
181 self
182 }
183
184 #[must_use]
185 pub fn modules(mut self, policy: ModulePolicy) -> Self {
186 self.config.modules = policy;
187 self
188 }
189
190 #[must_use]
191 pub fn process(mut self, process: ProcessOptions) -> Self {
192 self.config.process = process;
193 self
194 }
195
196 #[must_use]
197 pub fn identity(mut self, identity: ferrijs_std::identity::Identity) -> Self {
198 self.config.identity = identity;
199 self
200 }
201
202 #[must_use]
204 pub fn permissions(mut self, permissions: Permissions) -> Self {
205 self.config.permissions = Arc::new(Container::new(permissions));
206 self
207 }
208
209 #[must_use]
211 pub fn permission_container(mut self, container: Container) -> Self {
212 self.config.permissions = Arc::new(container);
213 self
214 }
215
216 #[must_use]
217 pub fn redactor(mut self, redactor: Arc<dyn Redactor>) -> Self {
218 self.config.redactor = Some(redactor);
219 self
220 }
221
222 #[must_use]
223 pub fn pause_clock(mut self, clock: Arc<dyn PauseClock>) -> Self {
224 self.config.pause_clock = clock;
225 self
226 }
227
228 #[must_use]
229 pub fn fs_global(mut self, on: bool) -> Self {
230 self.config.fs_global = on;
231 self
232 }
233
234 #[must_use]
237 pub fn timers(mut self, on: bool) -> Self {
238 self.config.timers = on;
239 self
240 }
241
242 #[cfg(feature = "fetch")]
246 #[must_use]
247 pub fn fetch(mut self, backend: Arc<dyn crate::fetch::FetchBackend>) -> Self {
248 self.config.fetch = Some(backend);
249 self
250 }
251
252 #[cfg(feature = "fetch")]
255 #[must_use]
256 pub fn without_fetch(mut self) -> Self {
257 self.config.fetch = None;
258 self
259 }
260
261 #[must_use]
262 pub fn extension(mut self, extension: impl Extension + 'static) -> Self {
263 self.config.extensions.push(Arc::new(extension));
264 self
265 }
266
267 #[must_use]
268 pub fn extension_arc(mut self, extension: Arc<dyn Extension>) -> Self {
269 self.config.extensions.push(extension);
270 self
271 }
272
273 pub async fn build(self) -> Result<Runtime, ScriptError> {
281 Runtime::create(self.config).await
282 }
283}
284
285struct VmHandleUd(VmHandle);
289
290#[allow(unsafe_code)]
293unsafe impl rquickjs::JsLifetime<'_> for VmHandleUd {
294 type Changed<'to> = VmHandleUd;
295}
296
297#[must_use]
299pub fn vm_handle(ctx: &Ctx<'_>) -> Option<VmHandle> {
300 ctx.userdata::<VmHandleUd>().map(|ud| ud.0.clone())
301}
302
303struct RegistryUd(Arc<ModuleRegistry>);
305
306#[allow(unsafe_code)]
308unsafe impl rquickjs::JsLifetime<'_> for RegistryUd {
309 type Changed<'to> = RegistryUd;
310}
311
312#[must_use]
314pub fn registry(ctx: &Ctx<'_>) -> Option<Arc<ModuleRegistry>> {
315 ctx.userdata::<RegistryUd>().map(|ud| Arc::clone(&ud.0))
316}
317
318#[derive(Debug)]
321pub struct Run<T> {
322 pub result: Result<T, ScriptError>,
323 pub duration_ms: u64,
324 pub console: Vec<ConsoleEntry>,
325 pub poisoned: bool,
330}
331
332impl<T> Run<T> {
333 #[must_use]
334 pub fn is_ok(&self) -> bool {
335 self.result.is_ok()
336 }
337
338 #[must_use]
340 pub fn err(&self) -> Option<&ScriptError> {
341 self.result.as_ref().err()
342 }
343
344 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Run<U> {
345 Run {
346 result: self.result.map(f),
347 duration_ms: self.duration_ms,
348 console: self.console,
349 poisoned: self.poisoned,
350 }
351 }
352}
353
354impl Run<serde_json::Value> {
355 #[must_use]
357 pub fn into_result(self) -> crate::result::ScriptResult {
358 match self.result {
359 Ok(value) => crate::result::ScriptResult::ok(value, self.duration_ms, self.console),
360 Err(error) => crate::result::ScriptResult::err(error, self.duration_ms, self.console),
361 }
362 }
363}
364
365pub type RunBody<T> =
371 Box<dyn for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = Result<T, ScriptError>> + 'js>> + Send>;
372
373pub struct Runtime {
375 engine: AsyncRuntime,
376 vm: VmHandle,
384 _vm_shutdown: VmShutdown,
387 config: Config,
388 registry: Arc<ModuleRegistry>,
389 applied: AppliedLimits,
390 timeout: Arc<TimeoutState>,
391 poisoned: AtomicBool,
392 base_console: Arc<ConsoleCapture>,
398}
399
400impl std::fmt::Debug for Runtime {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 f.debug_struct("Runtime")
403 .field("config", &self.config)
404 .field("poisoned", &self.poisoned())
405 .finish_non_exhaustive()
406 }
407}
408
409impl Runtime {
410 #[must_use]
411 pub fn builder() -> Builder {
412 Builder::default()
413 }
414
415 async fn create(config: Config) -> Result<Self, ScriptError> {
416 let runtime = AsyncRuntime::new().map_err(|e| ScriptError::internal(format!("rquickjs runtime init: {e}")))?;
417
418 runtime.set_memory_limit(config.limits.memory).await;
419 runtime.set_max_stack_size(config.limits.stack).await;
420 runtime.set_gc_threshold(config.limits.gc_threshold).await;
421
422 let timeout = Arc::new(TimeoutState::new(Arc::clone(&config.pause_clock)));
427 {
428 let state = Arc::clone(&timeout);
429 runtime
430 .set_interrupt_handler(Some(Box::new(move || {
431 if state.expired() {
432 state.timed_out.store(true, Ordering::Relaxed);
433 true
434 } else {
435 false
436 }
437 })))
438 .await;
439 }
440
441 let mut registry = ModuleRegistry::with_std();
449 registry.retain(|specifier| config.modules.serves_builtin(specifier));
450 let mut resolvers: Vec<BoxResolver> = Vec::new();
451 let mut loaders: Vec<BoxLoader> = Vec::new();
452 let mut require_hooks: Vec<Arc<dyn RequireHook>> = Vec::new();
453 for extension in &config.extensions {
454 extension
455 .modules(&mut registry)
456 .map_err(|e| ScriptError::internal(format!("extension `{}`: {e}", extension.name())))?;
457 for (resolver, loader) in extension.loaders() {
458 resolvers.push(resolver);
459 loaders.push(loader);
460 }
461 if let Some(hook) = extension.require_hook() {
462 require_hooks.push(hook);
463 }
464 }
465 let registry = Arc::new(registry);
466 let (native_resolver, native_loader) = registry.loader();
467 runtime
468 .set_loader(
469 (
470 native_resolver,
471 ResolverChain(resolvers),
472 FileResolver::new(config.modules.clone()),
473 ),
474 (native_loader, LoaderChain(loaders), FileLoader::new(&config.modules)),
475 )
476 .await;
477
478 let ctx = AsyncContext::full(&runtime)
479 .await
480 .map_err(|e| ScriptError::internal(format!("rquickjs context init: {e}")))?;
481
482 let (vm, vm_shutdown) = spawn_vm_loop(&ctx);
483
484 let base_console = Arc::new(Self::console_capture(&config));
485 let install_console_capture = Arc::clone(&base_console);
486 let kept_console = Arc::clone(&base_console);
487 let install_registry = Arc::clone(®istry);
488 let ud_vm = vm.clone();
489 let permissions = Arc::clone(&config.permissions);
490 let identity = config.identity.clone();
491 let process = ferrijs_std::node::process::ProcessOptions {
492 env: permissions.permissions().env_snapshot(),
493 cwd: config
494 .process
495 .cwd
496 .clone()
497 .unwrap_or_else(|| config.modules.root.to_string_lossy().into_owned()),
498 argv: config.process.argv.clone(),
499 };
500 let fs_global = config.fs_global;
501 let timers = config.timers;
502 #[cfg(feature = "fetch")]
503 let fetch_backend = config.fetch.clone();
504 let realm = config.realm.clone();
505 let extensions = config.extensions.clone();
506
507 let installed: Result<Result<(), ScriptError>, ScriptError> = vm_with!(vm => |ctx| {
508 let _ = ctx.store_userdata(VmHandleUd(ud_vm));
509 let _ = ctx.store_userdata(RegistryUd(Arc::clone(&install_registry)));
510 ferrijs_std::permissions::install(&ctx, permissions);
511 ferrijs_std::identity::set(&ctx, identity);
512
513 let fail = |what: &str, e: rquickjs::Error| ScriptError::internal(format!("failed to install {what}: {e}"));
514 ferrijs_std::init(&ctx).map_err(|e| fail("the standard library", e))?;
515 if timers {
516 crate::timers::install(&ctx).map_err(|e| fail("timers", e))?;
517 }
518 ferrijs_std::node::process::install(&ctx, &process).map_err(|e| fail("process", e))?;
519 if fs_global {
520 ferrijs_std::fs::init(&ctx).map_err(|e| fail("fs", e))?;
521 }
522 crate::modules::require::install(&ctx, install_registry, require_hooks).map_err(|e| fail("require", e))?;
523 #[cfg(feature = "fetch")]
524 if let Some(backend) = fetch_backend {
525 crate::fetch::install(&ctx, backend).map_err(|e| fail("fetch", e))?;
526 }
527 install_console(&ctx, base_console).map_err(|e| fail("console", e))?;
530
531 for extension in &extensions {
532 extension
533 .install_async(ctx.clone())
534 .await
535 .map_err(|e| ScriptError::internal(format!("extension `{}` failed to install: {e}", extension.name())))?;
536 }
537 for entry in install_console_capture.drain() {
541 tracing::info!(target: "ferrijs::extensions", "{}", entry.message);
542 }
543
544 crate::realm::lockdown(&ctx, &realm).map_err(|e| fail("the realm lockdown", e))?;
545 Ok(())
546 })
547 .await;
548 installed??;
549
550 let applied = AppliedLimits::new(&config.limits);
551 Ok(Self {
552 engine: runtime,
553 vm,
554 _vm_shutdown: vm_shutdown,
555 config,
556 registry,
557 applied,
558 timeout,
559 poisoned: AtomicBool::new(false),
560 base_console: kept_console,
561 })
562 }
563
564 fn console_capture(config: &Config) -> ConsoleCapture {
565 let capture = ConsoleCapture::new(
566 config.console.max_entries,
567 config.console.max_bytes,
568 config.console.max_entry_bytes,
569 );
570 let capture = match &config.redactor {
571 Some(redactor) => capture.with_redactor(Arc::clone(redactor)),
572 None => capture,
573 };
574 match &config.console.sink {
575 Some(sink) => capture.with_sink(Arc::clone(sink)),
576 None => capture,
577 }
578 }
579
580 #[must_use]
583 pub fn handle(&self) -> VmHandle {
584 self.vm.clone()
585 }
586
587 #[must_use]
588 pub fn config(&self) -> &Config {
589 &self.config
590 }
591
592 #[must_use]
593 pub fn registry(&self) -> &Arc<ModuleRegistry> {
594 &self.registry
595 }
596
597 #[must_use]
598 pub fn permissions(&self) -> &Arc<Container> {
599 &self.config.permissions
600 }
601
602 #[must_use]
605 pub fn poisoned(&self) -> bool {
606 self.poisoned.load(Ordering::Relaxed)
607 }
608
609 #[must_use]
612 pub fn deadline(&self) -> Deadline {
613 Deadline(Arc::clone(&self.timeout))
614 }
615
616 pub async fn with<R, F>(&self, f: F) -> Result<R, ScriptError>
624 where
625 R: Send + 'static,
626 F: for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = R> + Send + 'js>> + Send + 'static,
627 {
628 self.vm.with(f).await
629 }
630
631 async fn apply_limits(&self, memory: usize, stack: usize, gc: usize) {
634 if self.applied.memory.swap(memory, Ordering::Relaxed) != memory {
635 self.engine.set_memory_limit(memory).await;
636 }
637 if self.applied.stack.swap(stack, Ordering::Relaxed) != stack {
638 self.engine.set_max_stack_size(stack).await;
639 }
640 if self.applied.gc.swap(gc, Ordering::Relaxed) != gc {
641 self.engine.set_gc_threshold(gc).await;
642 }
643 }
644
645 async fn apply_run_options(&self, options: &RunOptions) -> Duration {
646 let limits = &self.config.limits;
647 self
648 .apply_limits(
649 options.memory.unwrap_or(limits.memory),
650 options.stack.unwrap_or(limits.stack),
651 options.gc_threshold.unwrap_or(limits.gc_threshold),
652 )
653 .await;
654 options.timeout.unwrap_or(limits.timeout)
655 }
656
657 pub async fn run<T: Send + 'static>(&self, options: RunOptions, body: RunBody<T>) -> Run<T> {
665 let started = Instant::now();
666 let streaming = self.config.console.sink.is_some();
671 let console = if streaming {
672 Arc::clone(&self.base_console)
673 } else {
674 Arc::new(Self::console_capture(&self.config))
675 };
676 if self.poisoned() {
677 return Run {
678 result: Err(ScriptError::internal(
679 "this realm is poisoned by an earlier timeout or allocation fault; build a new one",
680 )),
681 duration_ms: 0,
682 console: Vec::new(),
683 poisoned: true,
684 };
685 }
686 let timeout = self.apply_run_options(&options).await;
687 let token = self.timeout.arm(started + timeout);
688 let run_console = Arc::clone(&console);
689
690 let fut = vm_with!(self.vm => |ctx| {
691 if !streaming
692 && let Err(e) = install_console(&ctx, run_console)
693 {
694 return Err(ScriptError::internal(format!("failed to install console: {e}")));
695 }
696 body(ctx).await
697 });
698
699 let backstop = timeout.saturating_add(self.config.limits.backstop_grace);
700 let outcome = match run_within(self.timeout.clock(), backstop, fut).await {
701 Ok(r) => r.and_then(|inner| inner),
702 Err(_) => return self.finish_backstop(token, started, &console, timeout),
703 };
704 self.finish(token, outcome, started, &console, timeout)
705 }
706
707 fn finish<T>(
709 &self,
710 token: crate::limits::ArmToken,
711 outcome: Result<T, ScriptError>,
712 started: Instant,
713 console: &ConsoleCapture,
714 timeout: Duration,
715 ) -> Run<T> {
716 self.timeout.disarm(token);
717 let duration_ms = elapsed_ms(started);
718 let drained = console.drain();
719 match outcome {
720 Ok(value) => Run {
721 result: Ok(value),
722 duration_ms,
723 console: drained,
724 poisoned: false,
725 },
726 Err(mut err) => {
727 let timed_out = self.timeout.timed_out.load(Ordering::Relaxed);
728 let oom = is_oom(&err);
729 let poisoned = timed_out || oom;
730 if timed_out {
731 err = ScriptError::timeout(duration_ms, timeout.as_millis().try_into().unwrap_or(u64::MAX));
732 } else if oom {
733 err.kind = ScriptErrorKind::MemoryLimit;
734 }
735 if let Some(redactor) = &self.config.redactor {
736 err.redact(redactor.as_ref());
737 }
738 if poisoned {
739 self.poisoned.store(true, Ordering::Relaxed);
740 }
741 Run {
742 result: Err(err),
743 duration_ms,
744 console: drained,
745 poisoned,
746 }
747 },
748 }
749 }
750
751 fn finish_backstop<T>(
757 &self,
758 token: crate::limits::ArmToken,
759 started: Instant,
760 console: &ConsoleCapture,
761 timeout: Duration,
762 ) -> Run<T> {
763 self.timeout.disarm(token);
764 let poisoned = self.config.limits.backstop_poisons;
765 if poisoned {
766 self.poisoned.store(true, Ordering::Relaxed);
767 }
768 let duration_ms = elapsed_ms(started);
769 Run {
770 result: Err(ScriptError::timeout(
771 duration_ms,
772 timeout.as_millis().try_into().unwrap_or(u64::MAX),
773 )),
774 duration_ms,
775 console: console.drain(),
776 poisoned,
777 }
778 }
779
780 pub async fn eval_script(
788 &self,
789 source: &str,
790 args: &[serde_json::Value],
791 options: RunOptions,
792 ) -> Run<serde_json::Value> {
793 let source = source.to_string();
794 let args = args.to_vec();
795 let run = self
796 .run(
797 options,
798 Box::new(move |ctx| Box::pin(async move { script_body(&ctx, &source, &args).await })),
799 )
800 .await;
801 self.redact_run(run)
802 }
803
804 pub async fn eval_module(
809 &self,
810 module: &CompiledModule,
811 args: &[serde_json::Value],
812 options: RunOptions,
813 ) -> Run<serde_json::Value> {
814 let bytecode = Arc::clone(&module.bytecode);
815 let mapper = module.mapper();
816 let args = args.to_vec();
817 let run = self
818 .run(
819 options,
820 Box::new(move |ctx| Box::pin(async move { module_body(&ctx, &bytecode, mapper, &args).await })),
821 )
822 .await;
823 let run = run.map_err_pos(|e| {
824 if let Some(line) = e.line
825 && let Some((src, sl, sc)) = module.remap(line, e.column.unwrap_or(1))
826 {
827 e.message = format!("{} (at {src}:{sl}:{sc})", e.message);
828 }
829 });
830 self.redact_run(run)
831 }
832
833 pub fn redact_value(&self, value: &mut serde_json::Value) {
836 if let Some(redactor) = &self.config.redactor {
837 crate::redact::redact_json(redactor.as_ref(), value);
838 }
839 }
840
841 fn redact_run(&self, mut run: Run<serde_json::Value>) -> Run<serde_json::Value> {
842 if let Ok(value) = &mut run.result {
846 self.redact_value(value);
847 }
848 run
849 }
850
851 pub async fn eval_module_source(
854 &self,
855 name: &str,
856 source: &str,
857 args: &[serde_json::Value],
858 options: RunOptions,
859 ) -> Run<serde_json::Value> {
860 let name = name.to_string();
861 let source = source.to_string();
862 let args = args.to_vec();
863 let run = self
864 .run(
865 options,
866 Box::new(move |ctx| {
867 Box::pin(async move {
868 install_args(&ctx, &args)?;
869 let declared = match Module::declare(ctx.clone(), name.as_str(), source.as_bytes()).catch(&ctx) {
870 Ok(m) => m,
871 Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
872 };
873 let (evaluated, promise) = match declared.eval().catch(&ctx) {
874 Ok(v) => v,
875 Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
876 };
877 if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
878 return Err(ScriptError::from_caught(&ctx, e, &source));
879 }
880 let default = evaluated
881 .namespace()
882 .and_then(|ns| ns.get::<_, Value<'_>>("default"))
883 .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
884 Ok(crate::value::value_to_json(&ctx, default).unwrap_or(serde_json::Value::Null))
885 })
886 }),
887 )
888 .await;
889 self.redact_run(run)
890 }
891}
892
893pub async fn script_body(
902 ctx: &Ctx<'_>,
903 source: &str,
904 args: &[serde_json::Value],
905) -> Result<serde_json::Value, ScriptError> {
906 install_args(ctx, args)?;
907 let wrapped = format!("(async () => {{\n{source}\n}})()");
910 let promise: rquickjs::Promise<'_> = ctx
911 .eval(wrapped.as_bytes())
912 .map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
913 let value: Value<'_> = promise
914 .into_future::<Value<'_>>()
915 .await
916 .map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
917 Ok(crate::value::value_to_json(ctx, value).unwrap_or(serde_json::Value::Null))
918}
919
920pub async fn module_body(
928 ctx: &Ctx<'_>,
929 bytecode: &[u8],
930 mapper: SourceMapper,
931 args: &[serde_json::Value],
932) -> Result<serde_json::Value, ScriptError> {
933 let label = mapper.module_name.clone();
934 crate::source_map::register_bundle(ctx, mapper);
935 install_args(ctx, args)?;
936 let evaluated = eval_bytecode(ctx, bytecode, &label).await?;
937 let default = evaluated
938 .namespace()
939 .and_then(|ns| ns.get::<_, Value<'_>>("default"))
940 .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
941 Ok(crate::value::value_to_json(ctx, default).unwrap_or(serde_json::Value::Null))
942}
943
944pub async fn eval_bytecode<'js>(
952 ctx: &Ctx<'js>,
953 bytecode: &[u8],
954 label: &str,
955) -> Result<Module<'js, rquickjs::module::Evaluated>, ScriptError> {
956 #[allow(unsafe_code)]
962 let declared = match (unsafe { Module::load(ctx.clone(), bytecode) }).catch(ctx) {
963 Ok(m) => m,
964 Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
965 };
966 let (evaluated, promise) = match declared.eval().catch(ctx) {
967 Ok(v) => v,
968 Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
969 };
970 if let Err(e) = promise.into_future::<()>().await.catch(ctx) {
971 return Err(ScriptError::from_caught(ctx, e, label));
972 }
973 Ok(evaluated)
974}
975
976impl<T> Run<T> {
977 fn map_err_pos(mut self, f: impl FnOnce(&mut ScriptError)) -> Self {
978 if let Err(e) = &mut self.result {
979 f(e);
980 }
981 self
982 }
983}
984
985pub fn install_args(ctx: &Ctx<'_>, args: &[serde_json::Value]) -> Result<(), ScriptError> {
994 let array = rquickjs::Array::new(ctx.clone()).map_err(|e| ScriptError::internal(format!("args: {e}")))?;
995 for (i, a) in args.iter().enumerate() {
996 let v = crate::value::json_to_js(ctx, a).map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
997 array
998 .set(i, v)
999 .map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
1000 }
1001 ctx
1002 .globals()
1003 .set("args", array)
1004 .map_err(|e| ScriptError::internal(format!("args: {e}")))
1005}
1006
1007fn is_oom(err: &ScriptError) -> bool {
1011 err.kind == ScriptErrorKind::MemoryLimit || err.message.to_ascii_lowercase().contains("out of memory")
1012}
1013
1014fn elapsed_ms(started: Instant) -> u64 {
1015 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
1016}