use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use rquickjs::function::{Async, Func};
use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module, Object, Value, async_with};
use crate::console::{ConsoleCapture, strip_ansi};
use crate::error::{ScriptError, ScriptErrorKind};
use crate::fs::PathSandbox;
use crate::result::{ConsoleLevel, ScriptResult};
use crate::vars::VarsStore;
pub const DEFAULT_MAX_CONSOLE_ENTRIES: usize = 1_000;
pub const DEFAULT_MAX_CONSOLE_BYTES: usize = 1_048_576;
pub const DEFAULT_MAX_CONSOLE_ENTRY_BYTES: usize = 8_192;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300);
pub const DEFAULT_MEMORY_LIMIT: usize = 256 * 1024 * 1024;
pub const DEFAULT_STACK_SIZE: usize = 1024 * 1024;
pub const DEFAULT_GC_THRESHOLD: usize = 64 * 1024 * 1024;
pub const DEFAULT_MAX_SESSION_VMS: usize = 64;
pub const DEFAULT_SESSION_IDLE_TTL: Duration = Duration::from_secs(30 * 60);
#[derive(Debug, Clone)]
pub struct ScriptEngineConfig {
pub default_timeout: Duration,
pub default_memory_limit: usize,
pub default_stack_size: usize,
pub default_gc_threshold: usize,
pub max_console_entries: usize,
pub max_console_bytes: usize,
pub max_console_entry_bytes: usize,
pub max_session_vms: usize,
pub session_idle_ttl: Option<Duration>,
}
impl Default for ScriptEngineConfig {
fn default() -> Self {
Self {
default_timeout: DEFAULT_TIMEOUT,
default_memory_limit: DEFAULT_MEMORY_LIMIT,
default_stack_size: DEFAULT_STACK_SIZE,
default_gc_threshold: DEFAULT_GC_THRESHOLD,
max_console_entries: DEFAULT_MAX_CONSOLE_ENTRIES,
max_console_bytes: DEFAULT_MAX_CONSOLE_BYTES,
max_console_entry_bytes: DEFAULT_MAX_CONSOLE_ENTRY_BYTES,
max_session_vms: DEFAULT_MAX_SESSION_VMS,
session_idle_ttl: Some(DEFAULT_SESSION_IDLE_TTL),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RunOptions {
pub timeout: Option<Duration>,
pub memory_limit: Option<usize>,
pub stack_size: Option<usize>,
pub gc_threshold: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExtensionHost {
Mcp,
Bdd,
#[default]
Script,
}
impl ExtensionHost {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Mcp => "mcp",
Self::Bdd => "bdd",
Self::Script => "script",
}
}
}
#[derive(Clone)]
pub struct RunContext {
pub vars: Arc<dyn VarsStore>,
pub sandbox: Arc<PathSandbox>,
pub artifacts: Option<Arc<PathSandbox>>,
pub page: Option<Arc<ferridriver::Page>>,
pub browser_context: Option<Arc<ferridriver::context::ContextRef>>,
pub request: Option<Arc<ferridriver::http_client::HttpClient>>,
pub browser: Option<Arc<ferridriver::Browser>>,
pub plugins: Vec<crate::bindings::PluginBinding>,
pub trusted_modules: bool,
pub host: ExtensionHost,
pub caps: ScriptCaps,
}
#[derive(Debug, Clone, Default)]
pub struct ScriptCaps {
pub env: std::collections::BTreeMap<String, String>,
}
impl ScriptCaps {
#[must_use]
pub fn resolve(allow_env: &[String]) -> Self {
let env = allow_env
.iter()
.filter_map(|k| std::env::var(k).ok().map(|v| (k.clone(), v)))
.collect();
Self { env }
}
}
pub(crate) struct SessionAsyncCtx(pub(crate) AsyncContext);
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for SessionAsyncCtx {
type Changed<'to> = SessionAsyncCtx;
}
pub(crate) struct SessionProcsUd(pub(crate) std::sync::Arc<crate::session_procs::SessionProcs>);
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for SessionProcsUd {
type Changed<'to> = SessionProcsUd;
}
pub struct ScriptEngine {
config: ScriptEngineConfig,
}
impl ScriptEngine {
#[must_use]
pub fn new(config: ScriptEngineConfig) -> Self {
Self { config }
}
#[must_use]
pub fn config(&self) -> &ScriptEngineConfig {
&self.config
}
pub async fn run(
&self,
source: &str,
args: &[serde_json::Value],
options: RunOptions,
context: RunContext,
) -> ScriptResult {
match Session::create(self.config.clone(), &context).await {
Ok(session) => session.execute(source, args, options, &context).await.result,
Err(e) => ScriptResult::err(e, 0, Vec::new()),
}
}
}
#[derive(Debug)]
pub struct SessionRun {
pub result: ScriptResult,
pub poisoned: bool,
}
pub struct Session {
runtime: AsyncRuntime,
ctx: AsyncContext,
config: ScriptEngineConfig,
default_request: Arc<ferridriver::http_client::HttpClient>,
applied: AppliedLimits,
}
struct AppliedLimits {
memory: AtomicUsize,
stack: AtomicUsize,
gc: AtomicUsize,
}
impl Session {
pub async fn create(config: ScriptEngineConfig, context: &RunContext) -> Result<Self, ScriptError> {
let runtime = AsyncRuntime::new().map_err(|e| ScriptError::internal(format!("rquickjs runtime init: {e}")))?;
runtime.set_memory_limit(config.default_memory_limit).await;
runtime.set_max_stack_size(config.default_stack_size).await;
runtime.set_gc_threshold(config.default_gc_threshold).await;
if context.trusted_modules {
let mut resolver = rquickjs::loader::FileResolver::default();
resolver.add_path(".");
resolver.add_path(context.sandbox.root().to_string_lossy().as_ref());
runtime
.set_loader(resolver, rquickjs::loader::ScriptLoader::default())
.await;
} else {
runtime
.set_loader(
crate::modules::SandboxResolver::new(context.sandbox.clone()),
crate::modules::SandboxLoader::new(context.sandbox.clone()),
)
.await;
}
let ctx = AsyncContext::full(&runtime)
.await
.map_err(|e| ScriptError::internal(format!("rquickjs context init: {e}")))?;
let plugins = context.plugins.clone();
let vars = context.vars.clone();
let sandbox = context.sandbox.clone();
let sandbox_root = context.sandbox.root().to_string_lossy().into_owned();
let artifacts = context.artifacts.clone();
let host = context.host;
let caps = context.caps.clone();
let ud_ctx = ctx.clone();
let install: Result<(), ScriptError> = async_with!(ctx => |ctx| {
let _ = ctx.store_userdata(SessionAsyncCtx(ud_ctx));
let _ = ctx.store_userdata(crate::bindings::fetch::NetPolicyUd(
crate::bindings::fetch::NetPolicy::default(),
));
crate::bindings::page::ensure_page_callbacks(&ctx);
install_runtime_shims(&ctx).map_err(|e| ScriptError::internal(format!("failed to install runtime shims: {e}")))?;
crate::bindings::define_classes(&ctx)
.map_err(|e| ScriptError::internal(format!("failed to define classes: {e}")))?;
install_vars(&ctx, vars).map_err(|e| ScriptError::internal(format!("failed to install vars: {e}")))?;
install_fs(&ctx, sandbox).map_err(|e| ScriptError::internal(format!("failed to install fs: {e}")))?;
crate::bindings::process::install(&ctx, &caps, &sandbox_root)
.map_err(|e| ScriptError::internal(format!("failed to install process: {e}")))?;
if let Some(artifacts) = artifacts {
crate::bindings::install_artifacts(&ctx, artifacts)
.map_err(|e| ScriptError::internal(format!("failed to install artifacts: {e}")))?;
}
crate::bindings::install_browser_type(&ctx)
.map_err(|e| ScriptError::internal(format!("failed to install browser_type: {e}")))?;
crate::bindings::expect::install_expect(&ctx)
.map_err(|e| ScriptError::internal(format!("failed to install expect: {e}")))?;
crate::bindings::install_bdd(&ctx)
.map_err(|e| ScriptError::internal(format!("failed to install extension registry: {e}")))?;
let fd = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(format!("ferridriver global: {e}")))?;
fd.set("host", host.as_str())
.map_err(|e| ScriptError::internal(format!("ferridriver.host: {e}")))?;
ctx
.globals()
.set("ferridriver", fd)
.map_err(|e| ScriptError::internal(format!("install ferridriver global: {e}")))?;
crate::bindings::install_plugins(&ctx, &plugins)
.map_err(|e| ScriptError::internal(format!("failed to install plugins: {e}")))
})
.await;
install?;
let applied = AppliedLimits {
memory: AtomicUsize::new(config.default_memory_limit),
stack: AtomicUsize::new(config.default_stack_size),
gc: AtomicUsize::new(config.default_gc_threshold),
};
Ok(Self {
runtime,
ctx,
config,
default_request: Arc::new(ferridriver::http_client::HttpClient::new(
ferridriver::http_client::HttpClientOptions::default(),
)),
applied,
})
}
#[must_use]
pub fn async_context(&self) -> AsyncContext {
self.ctx.clone()
}
pub async fn install_session_procs(&self, procs: std::sync::Arc<crate::session_procs::SessionProcs>) {
async_with!(self.ctx => |ctx| {
let _ = ctx.store_userdata(SessionProcsUd(procs));
})
.await;
}
async fn apply_limits(&self, memory: usize, stack: usize, gc: usize) {
if self.applied.memory.swap(memory, Ordering::Relaxed) != memory {
self.runtime.set_memory_limit(memory).await;
}
if self.applied.stack.swap(stack, Ordering::Relaxed) != stack {
self.runtime.set_max_stack_size(stack).await;
}
if self.applied.gc.swap(gc, Ordering::Relaxed) != gc {
self.runtime.set_gc_threshold(gc).await;
}
}
fn new_console(&self) -> Arc<ConsoleCapture> {
Arc::new(ConsoleCapture::new(
self.config.max_console_entries,
self.config.max_console_bytes,
self.config.max_console_entry_bytes,
))
}
async fn arm_timeout(&self, deadline: Instant) -> Arc<AtomicBool> {
let timed_out = Arc::new(AtomicBool::new(false));
let flag = timed_out.clone();
self
.runtime
.set_interrupt_handler(Some(Box::new(move || {
if Instant::now() >= deadline {
flag.store(true, Ordering::Relaxed);
true
} else {
false
}
})))
.await;
timed_out
}
fn globals_install(&self, context: &RunContext, console: &Arc<ConsoleCapture>) -> GlobalsInstall {
GlobalsInstall {
console: console.clone(),
page: context.page.clone(),
browser_context: context.browser_context.clone(),
request: context.request.clone(),
default_request: self.default_request.clone(),
browser: context.browser.clone(),
async_ctx: self.ctx.clone(),
}
}
fn finish(
&self,
eval_result: Result<serde_json::Value, ScriptError>,
started: Instant,
console: &Arc<ConsoleCapture>,
timed_out: &Arc<AtomicBool>,
timeout: Duration,
) -> SessionRun {
let duration = elapsed_ms(started);
let drained = console.drain();
match eval_result {
Ok(value) => SessionRun {
result: ScriptResult::ok(value, duration, drained),
poisoned: false,
},
Err(mut err) => {
let timed_out = timed_out.load(Ordering::Relaxed);
let oom = is_oom(&err);
let poisoned = timed_out || oom;
if timed_out {
err = ScriptError::timeout(duration, timeout.as_millis() as u64);
}
SessionRun {
result: ScriptResult::err(err, duration, drained),
poisoned,
}
},
}
}
async fn apply_call_limits(&self, options: &RunOptions) -> Duration {
self
.apply_limits(
options.memory_limit.unwrap_or(self.config.default_memory_limit),
options.stack_size.unwrap_or(self.config.default_stack_size),
options.gc_threshold.unwrap_or(self.config.default_gc_threshold),
)
.await;
options.timeout.unwrap_or(self.config.default_timeout)
}
pub async fn execute(
&self,
source: &str,
args: &[serde_json::Value],
options: RunOptions,
context: &RunContext,
) -> SessionRun {
let started = Instant::now();
let console = self.new_console();
let timeout = self.apply_call_limits(&options).await;
let timed_out = self.arm_timeout(started + timeout).await;
let install = self.globals_install(context, &console);
let source_owned = source.to_string();
let eval_result: Result<serde_json::Value, ScriptError> = async_with!(self.ctx => |ctx| {
if let Err(e) = install_call_globals(&ctx, args, install) {
return Err(ScriptError::internal(format!("failed to install globals: {e}")));
}
let wrapped = wrap_source(&source_owned);
let promise: rquickjs::Promise<'_> = match ctx.eval(wrapped.as_bytes()) {
Ok(v) => v,
Err(e) => return Err(caught_to_script_error(rquickjs::CaughtError::from_error(&ctx, e), &source_owned)),
};
let result: Value<'_> = match promise.into_future::<Value<'_>>().await {
Ok(v) => v,
Err(e) => return Err(caught_to_script_error(rquickjs::CaughtError::from_error(&ctx, e), &source_owned)),
};
Ok(value_to_json(&ctx, result).unwrap_or(serde_json::Value::Null))
})
.await;
self.finish(eval_result, started, &console, &timed_out, timeout)
}
pub async fn execute_module(
&self,
bundle: &crate::bundle::CompiledBundle,
args: &[serde_json::Value],
options: RunOptions,
context: &RunContext,
) -> SessionRun {
let started = Instant::now();
let console = self.new_console();
let timeout = self.apply_call_limits(&options).await;
let timed_out = self.arm_timeout(started + timeout).await;
let install = self.globals_install(context, &console);
let bytecode = Arc::clone(&bundle.bytecode);
let label = bundle.module_name.clone();
let eval_result: Result<serde_json::Value, ScriptError> = async_with!(self.ctx => |ctx| {
if let Err(e) = install_call_globals(&ctx, args, install) {
return Err(ScriptError::internal(format!("failed to install globals: {e}")));
}
#[allow(unsafe_code)]
let module = match (unsafe { Module::load(ctx.clone(), &bytecode) }).catch(&ctx) {
Ok(m) => m,
Err(e) => return Err(caught_to_script_error(e, &label)),
};
let (evaluated, promise) = match module.eval().catch(&ctx) {
Ok(v) => v,
Err(e) => return Err(caught_to_script_error(e, &label)),
};
if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
return Err(caught_to_script_error(e, &label));
}
let default = evaluated
.namespace()
.and_then(|ns| ns.get::<_, Value<'_>>("default"))
.unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
Ok(value_to_json(&ctx, default).unwrap_or(serde_json::Value::Null))
})
.await;
let eval_result = eval_result.map_err(|mut e| {
if let Some(line) = e.line {
if let Some((src, sl, sc)) = bundle.remap(line, e.column.unwrap_or(1)) {
e.message = format!("{} (at {src}:{sl}:{sc})", e.message);
}
}
e
});
self.finish(eval_result, started, &console, &timed_out, timeout)
}
}
fn wrap_source(source: &str) -> String {
format!("(async () => {{\n{source}\n}})()")
}
fn is_oom(err: &ScriptError) -> bool {
err.message.to_ascii_lowercase().contains("out of memory")
}
struct GlobalsInstall {
console: Arc<ConsoleCapture>,
page: Option<Arc<ferridriver::Page>>,
browser_context: Option<Arc<ferridriver::context::ContextRef>>,
request: Option<Arc<ferridriver::http_client::HttpClient>>,
default_request: Arc<ferridriver::http_client::HttpClient>,
browser: Option<Arc<ferridriver::Browser>>,
async_ctx: AsyncContext,
}
fn install_call_globals(ctx: &Ctx<'_>, args: &[serde_json::Value], inst: GlobalsInstall) -> rquickjs::Result<()> {
let globals = ctx.globals();
let args_arr = rquickjs::Array::new(ctx.clone())?;
for (i, a) in args.iter().enumerate() {
args_arr.set(i, crate::bindings::convert::json_to_js(ctx, a)?)?;
}
globals.set("args", args_arr)?;
install_console(ctx, inst.console)?;
if let Some(page) = inst.page {
crate::bindings::install_page(ctx, page, inst.async_ctx.clone())?;
}
if let Some(bcx) = inst.browser_context {
crate::bindings::install_browser_context(ctx, bcx)?;
}
if let Some(browser) = inst.browser {
crate::bindings::install_browser(ctx, browser)?;
}
if let Some(req) = inst.request {
crate::bindings::fetch::install(ctx, req.clone())?;
crate::bindings::install_request(ctx, req)?;
} else {
crate::bindings::fetch::install(ctx, inst.default_request)?;
}
Ok(())
}
fn install_console(ctx: &Ctx<'_>, capture: Arc<ConsoleCapture>) -> rquickjs::Result<()> {
use std::fmt::Write as _;
use rquickjs::function::Rest;
let formatter = rquickjs_extra_console::Formatter::builder().max_depth(3).build();
let console = Object::new(ctx.clone())?;
for (name, level) in [
("log", ConsoleLevel::Log),
("info", ConsoleLevel::Info),
("warn", ConsoleLevel::Warn),
("error", ConsoleLevel::Error),
("debug", ConsoleLevel::Debug),
] {
let cap = capture.clone();
let fmt = formatter.clone();
console.set(
name,
Func::from(move |args: Rest<Value<'_>>| -> rquickjs::Result<()> {
let mut msg = String::new();
for (i, v) in args.0.into_iter().enumerate() {
if i > 0 {
let _ = msg.write_char(' ');
}
fmt.format(&mut msg, v)?;
}
cap.push(level, strip_ansi(&msg));
Ok(())
}),
)?;
}
ctx.globals().set("console", console)?;
Ok(())
}
fn install_runtime_shims(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
rquickjs_extra_timers::init(ctx)?;
rquickjs_extra_url::init(ctx)?;
crate::bindings::webapi::install(ctx)?;
Ok(())
}
fn install_vars(ctx: &Ctx<'_>, vars: Arc<dyn VarsStore>) -> rquickjs::Result<()> {
let obj = Object::new(ctx.clone())?;
{
let v = vars.clone();
obj.set("get", Func::from(move |name: String| v.get(&name)))?;
}
{
let v = vars.clone();
obj.set(
"set",
Func::from(move |name: String, value: String| {
v.set(&name, value);
}),
)?;
}
{
let v = vars.clone();
obj.set("has", Func::from(move |name: String| v.has(&name)))?;
}
{
let v = vars.clone();
obj.set(
"delete",
Func::from(move |name: String| {
v.delete(&name);
}),
)?;
}
{
let v = vars.clone();
obj.set("keys", Func::from(move || v.keys()))?;
}
ctx.globals().set("vars", obj)?;
Ok(())
}
fn install_fs(ctx: &Ctx<'_>, sandbox: Arc<PathSandbox>) -> rquickjs::Result<()> {
let obj = Object::new(ctx.clone())?;
{
let sb = sandbox.clone();
obj.set(
"readFile",
Func::from(Async(move |path: String| {
let sb = sb.clone();
async move {
let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
tokio::fs::read_to_string(&resolved)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("fs", "readFile", e.to_string()))
}
})),
)?;
}
{
let sb = sandbox.clone();
obj.set(
"readFileBytes",
Func::from(Async(move |path: String| {
let sb = sb.clone();
async move {
let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
tokio::fs::read(&resolved)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("fs", "readFileBytes", e.to_string()))
}
})),
)?;
}
{
let sb = sandbox.clone();
obj.set(
"writeFile",
Func::from(Async(move |path: String, contents: String| {
let sb = sb.clone();
async move {
let resolved = sb.resolve_write(&path).map_err(|e| to_rq_error(&e))?;
tokio::fs::write(&resolved, contents)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("fs", "writeFile", e.to_string()))
}
})),
)?;
}
{
let sb = sandbox.clone();
obj.set(
"readdir",
Func::from(Async(move |path: String| {
let sb = sb.clone();
async move {
let resolved = sb.resolve_read(&path).map_err(|e| to_rq_error(&e))?;
let mut entries = tokio::fs::read_dir(&resolved)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("fs", "readdir", e.to_string()))?;
let mut names: Vec<String> = Vec::new();
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| rquickjs::Error::new_from_js_message("fs", "readdir", e.to_string()))?
{
names.push(entry.file_name().to_string_lossy().into_owned());
}
Ok::<_, rquickjs::Error>(names)
}
})),
)?;
}
{
let sb = sandbox.clone();
obj.set(
"exists",
Func::from(Async(move |path: String| {
let sb = sb.clone();
async move {
match sb.resolve_read(&path) {
Ok(resolved) => Ok::<bool, rquickjs::Error>(tokio::fs::try_exists(&resolved).await.unwrap_or(false)),
Err(_) => Ok(false),
}
}
})),
)?;
}
obj.set("root", sandbox.root().to_string_lossy().into_owned())?;
ctx.globals().set("fs", obj)?;
Ok(())
}
fn to_rq_error(err: &ScriptError) -> rquickjs::Error {
rquickjs::Error::new_from_js_message("fs", "sandbox", err.message.clone())
}
fn value_to_json<'js>(_ctx: &Ctx<'js>, value: Value<'js>) -> Option<serde_json::Value> {
rquickjs_serde::from_value::<JsonInter>(value)
.ok()
.map(JsonInter::into_json)
}
enum JsonInter {
Null,
Bool(bool),
I64(i64),
U64(u64),
F64(f64),
Str(String),
Arr(Vec<JsonInter>),
Obj(Vec<(String, JsonInter)>),
}
impl JsonInter {
fn into_json(self) -> serde_json::Value {
use serde_json::Value;
match self {
Self::Null => Value::Null,
Self::Bool(b) => Value::Bool(b),
Self::I64(n) => Value::Number(n.into()),
Self::U64(n) => Value::Number(n.into()),
Self::F64(f) => serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number),
Self::Str(s) => Value::String(s),
Self::Arr(a) => Value::Array(a.into_iter().map(Self::into_json).collect()),
Self::Obj(o) => Value::Object(o.into_iter().map(|(k, v)| (k, v.into_json())).collect()),
}
}
}
impl<'de> serde::Deserialize<'de> for JsonInter {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = JsonInter;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("any JSON value")
}
fn visit_unit<E>(self) -> Result<JsonInter, E> {
Ok(JsonInter::Null)
}
fn visit_none<E>(self) -> Result<JsonInter, E> {
Ok(JsonInter::Null)
}
fn visit_bool<E>(self, v: bool) -> Result<JsonInter, E> {
Ok(JsonInter::Bool(v))
}
fn visit_i64<E>(self, v: i64) -> Result<JsonInter, E> {
Ok(JsonInter::I64(v))
}
fn visit_u64<E>(self, v: u64) -> Result<JsonInter, E> {
Ok(JsonInter::U64(v))
}
fn visit_f64<E>(self, v: f64) -> Result<JsonInter, E> {
Ok(JsonInter::F64(v))
}
fn visit_str<E>(self, v: &str) -> Result<JsonInter, E> {
Ok(JsonInter::Str(v.to_owned()))
}
fn visit_string<E>(self, v: String) -> Result<JsonInter, E> {
Ok(JsonInter::Str(v))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut a: A) -> Result<JsonInter, A::Error> {
let mut out = Vec::new();
while let Some(e) = a.next_element()? {
out.push(e);
}
Ok(JsonInter::Arr(out))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut m: A) -> Result<JsonInter, A::Error> {
let mut out = Vec::new();
while let Some((k, v)) = m.next_entry()? {
out.push((k, v));
}
Ok(JsonInter::Obj(out))
}
}
d.deserialize_any(V)
}
}
pub(crate) fn caught_to_script_error(caught: rquickjs::CaughtError<'_>, source: &str) -> ScriptError {
let (message, stack, line, column) = match caught {
rquickjs::CaughtError::Exception(ex) => {
let message = ex.message().unwrap_or_else(|| "exception".to_string());
let stack = ex.stack();
let obj = ex.as_object();
let line = obj.get::<_, u32>("lineNumber").ok();
let column = obj.get::<_, u32>("columnNumber").ok();
(message, stack, line, column)
},
rquickjs::CaughtError::Value(v) => (format!("{v:?}"), None, None, None),
rquickjs::CaughtError::Error(e) => (format!("{e}"), None, None, None),
};
ScriptError {
kind: ScriptErrorKind::Runtime,
message,
stack,
line,
column,
source_snippet: line.and_then(|l| snippet_around_line(source, l, 2)),
}
}
fn snippet_around_line(source: &str, line_1based: u32, context_lines: u32) -> Option<String> {
use std::fmt::Write as _;
let lines: Vec<&str> = source.lines().collect();
if lines.is_empty() {
return None;
}
let target = line_1based.saturating_sub(1) as usize;
let start = target.saturating_sub(context_lines as usize);
let end = (target + context_lines as usize + 1).min(lines.len());
let mut out = String::new();
for (i, text) in lines[start..end].iter().enumerate() {
let ln = start + i + 1;
let marker = if ln == line_1based as usize { ">>>" } else { " " };
let _ = writeln!(out, "{marker} {ln:>4}: {text}");
}
Some(out)
}
fn elapsed_ms(started: Instant) -> u64 {
started.elapsed().as_millis() as u64
}