1use std::sync::{Arc, Mutex};
2use std::time::Instant;
3
4use wasmtime::{Engine, Extern, Linker, Memory, Module, Store, TypedFunc};
5
6use crate::bridge::Bridge;
7use crate::config::SandboxConfig;
8use crate::context::{ContextId, MessageBus};
9use crate::error::{Error, Result};
10use crate::observation::{Observation, ResourceLimitKind};
11
12const QUICKJS_WASM: &[u8] = include_bytes!("../quickjs.wasm");
14
15pub struct CompiledModule {
20 engine: Engine,
21 module: Module,
22}
23
24impl CompiledModule {
25 pub fn new() -> Result<Self> {
34 let mut engine_config = wasmtime::Config::new();
35 engine_config.consume_fuel(true);
36 engine_config.wasm_bulk_memory(true);
37 engine_config.async_stack_size(64 * 1024 * 1024);
45 engine_config.max_wasm_stack(32 * 1024 * 1024);
46
47 let engine =
48 Engine::new(&engine_config).map_err(|e| Error::WasmInit(format!("engine: {e}")))?;
49
50 let module = Module::new(&engine, QUICKJS_WASM)
51 .map_err(|e| Error::WasmInit(format!("module: {e}")))?;
52
53 Ok(Self { engine, module })
54 }
55
56 pub fn serialize(&self) -> Result<Vec<u8>> {
65 self.module
66 .serialize()
67 .map_err(|e| Error::WasmInit(format!("serialize: {e}")))
68 }
69
70 pub fn load_cached(bytes: &[u8]) -> Result<Self> {
81 let mut engine_config = wasmtime::Config::new();
82 engine_config.consume_fuel(true);
83 engine_config.wasm_bulk_memory(true);
84 engine_config.async_stack_size(64 * 1024 * 1024);
92 engine_config.max_wasm_stack(32 * 1024 * 1024);
93
94 let engine =
95 Engine::new(&engine_config).map_err(|e| Error::WasmInit(format!("engine: {e}")))?;
96
97 let module = unsafe {
98 Module::deserialize(&engine, bytes)
99 .map_err(|e| Error::WasmInit(format!("deserialize: {e}")))?
100 };
101
102 Ok(Self { engine, module })
103 }
104
105 pub fn new_cached(cache_path: &std::path::Path) -> Result<Self> {
114 let module = Self::new()?;
120 if let Ok(serialized) = module.serialize() {
121 if let Some(parent) = cache_path.parent() {
122 let _ = std::fs::create_dir_all(parent);
123 }
124 let tagged = Self::tag_cache_bytes(&serialized);
125 let _ = std::fs::write(cache_path, &tagged);
126 Self::set_restrictive_permissions(cache_path);
127 }
128 Ok(module)
129 }
130
131 fn tag_cache_bytes(payload: &[u8]) -> Vec<u8> {
135 let tag = Self::fnv1a_hash(payload).to_le_bytes();
136 let mut out = Vec::with_capacity(8 + payload.len());
137 out.extend_from_slice(&tag);
138 out.extend_from_slice(payload);
139 out
140 }
141
142 fn fnv1a_hash(data: &[u8]) -> u64 {
144 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
145 for &byte in data {
146 hash ^= u64::from(byte);
147 hash = hash.wrapping_mul(0x0100_0000_01b3);
148 }
149 hash
150 }
151
152 fn set_restrictive_permissions(path: &std::path::Path) {
154 #[cfg(unix)]
155 {
156 use std::os::unix::fs::PermissionsExt;
157 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
158 }
159 let _ = path;
160 }
161
162 #[must_use]
164 pub fn engine(&self) -> &Engine {
165 &self.engine
166 }
167
168 #[must_use]
170 pub fn module_ref(&self) -> &Module {
171 &self.module
172 }
173
174 pub fn execute(
182 &self,
183 scripts: &[String],
184 bridge: Arc<dyn Bridge>,
185 config: &SandboxConfig,
186 ) -> Result<ExecutionResult> {
187 self.execute_in_context(scripts, bridge, config, &ContextId::background(), None)
188 }
189
190 #[allow(
196 clippy::too_many_lines,
197 clippy::needless_pass_by_value,
198 clippy::cast_possible_truncation,
199 clippy::cast_possible_wrap
200 )]
201 pub fn execute_in_context(
202 &self,
203 scripts: &[String],
204 bridge: Arc<dyn Bridge>,
205 config: &SandboxConfig,
206 context_id: &ContextId,
207 message_bus: Option<&Mutex<MessageBus>>,
208 ) -> Result<ExecutionResult> {
209 let start = Instant::now();
210 let observations: Arc<Mutex<Vec<Observation>>> = Arc::new(Mutex::new(Vec::new()));
211
212 validate_scripts(scripts, config)?;
213
214 let host = HostState {
216 bridge: bridge.clone(),
217 observations: observations.clone(),
218 context_id: context_id.clone(),
219 config: config.clone(),
220 };
221
222 let mut store = Store::new(&self.engine, host);
223
224 if config.max_fuel > 0 {
225 store
226 .set_fuel(config.max_fuel)
227 .map_err(|e| Error::WasmInit(format!("fuel: {e}")))?;
228 }
229
230 let linker = build_linker(&self.engine, &bridge, &observations)?;
232 let instance = linker
233 .instantiate(&mut store, &self.module)
234 .map_err(|e| Error::WasmInit(format!("instantiate: {e}")))?;
235
236 let jsdet_init: TypedFunc<(i32, i32), i32> = instance
238 .get_typed_func(&mut store, "jsdet_init")
239 .map_err(|e| Error::WasmInit(format!("missing jsdet_init export: {e}")))?;
240 let jsdet_eval: TypedFunc<(i32, i32), i32> = instance
241 .get_typed_func(&mut store, "jsdet_eval")
242 .map_err(|e| Error::WasmInit(format!("missing jsdet_eval export: {e}")))?;
243 let jsdet_alloc: TypedFunc<i32, i32> =
244 instance
245 .get_typed_func(&mut store, "jsdet_alloc")
246 .map_err(|e| Error::WasmInit(format!("missing jsdet_alloc export: {e}")))?;
247 let jsdet_free: TypedFunc<i32, ()> = instance
248 .get_typed_func(&mut store, "jsdet_free")
249 .map_err(|e| Error::WasmInit(format!("missing jsdet_free export: {e}")))?;
250 let jsdet_destroy: TypedFunc<(), ()> = instance
251 .get_typed_func(&mut store, "jsdet_destroy")
252 .map_err(|e| Error::WasmInit(format!("missing jsdet_destroy export: {e}")))?;
253
254 let memory = instance
255 .exports(&mut store)
256 .find_map(wasmtime::Export::into_memory)
257 .ok_or_else(|| Error::WasmInit("no memory export".into()))?;
258
259 let qjs_memory_limit = config.max_memory_bytes.min(i32::MAX as usize) as i32;
262 if qjs_memory_limit == 0 {
263 return Err(Error::MemoryExceeded { limit_bytes: 0 });
264 }
265 let qjs_stack_size = (config.max_memory_bytes / 4).min(i32::MAX as usize) as i32;
266 let init_result = jsdet_init
267 .call(&mut store, (qjs_memory_limit, qjs_stack_size))
268 .map_err(|e| Error::WasmInit(format!("jsdet_init trapped: {e}")))?;
269 if init_result != 0 {
270 return Err(Error::WasmInit(format!(
271 "jsdet_init returned error code {init_result}"
272 )));
273 }
274
275 let bootstrap = bridge.bootstrap_js();
277 if !bootstrap.is_empty() {
278 eval_in_wasm(
279 &mut store,
280 &memory,
281 &jsdet_eval,
282 &jsdet_alloc,
283 &jsdet_free,
284 &bootstrap,
285 )?;
286 }
287
288 if let Some(bus) = message_bus
290 && let Ok(mut bus) = bus.lock()
291 {
292 let pending = bus.receive(context_id);
293 for msg in pending {
294 let dispatch = format!(
295 "if(typeof __jsdet_dispatch_message==='function')__jsdet_dispatch_message({});",
296 serde_json::to_string(&msg.payload).unwrap_or_default()
297 );
298 let _ = eval_in_wasm(
299 &mut store,
300 &memory,
301 &jsdet_eval,
302 &jsdet_alloc,
303 &jsdet_free,
304 &dispatch,
305 );
306 }
307 }
308
309 let mut scripts_executed = 0;
311 let mut errors = Vec::new();
312
313 for (i, script) in scripts.iter().enumerate() {
314 if scripts_executed >= config.max_scripts {
315 push_observation(
316 &observations,
317 Observation::ResourceLimit {
318 kind: ResourceLimitKind::ScriptCount,
319 detail: format!("exceeded {} script limit", config.max_scripts),
320 },
321 );
322 break;
323 }
324
325 if u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX) >= config.timeout_ms {
326 push_observation(
327 &observations,
328 Observation::ResourceLimit {
329 kind: ResourceLimitKind::Timeout,
330 detail: format!("exceeded {}ms timeout", config.timeout_ms),
331 },
332 );
333 break;
334 }
335
336 let fuel_before = if config.max_fuel > 0 {
337 store.get_fuel().ok()
338 } else {
339 None
340 };
341
342 match eval_in_wasm(
343 &mut store,
344 &memory,
345 &jsdet_eval,
346 &jsdet_alloc,
347 &jsdet_free,
348 script,
349 ) {
350 Ok(()) => {
351 scripts_executed += 1;
352 if let (Some(before), Ok(after)) = (fuel_before, store.get_fuel()) {
353 let consumed = before.saturating_sub(after);
354 if consumed >= config.max_fuel.saturating_mul(9) / 10 {
355 push_observation(
356 &observations,
357 Observation::ResourceLimit {
358 kind: ResourceLimitKind::Fuel,
359 detail: format!(
360 "consumed {consumed} of {} fuel units without trap",
361 config.max_fuel
362 ),
363 },
364 );
365 }
366 }
367 if u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
368 >= config.timeout_ms
369 {
370 push_observation(
371 &observations,
372 Observation::ResourceLimit {
373 kind: ResourceLimitKind::Timeout,
374 detail: format!(
375 "single script exceeded {}ms timeout budget",
376 config.timeout_ms
377 ),
378 },
379 );
380 break;
381 }
382 }
383 Err(Error::FuelExhausted { budget }) => {
384 push_observation(
385 &observations,
386 Observation::ResourceLimit {
387 kind: ResourceLimitKind::Fuel,
388 detail: format!("exceeded {budget} fuel budget"),
389 },
390 );
391 break;
392 }
393 Err(Error::MemoryExceeded { limit_bytes }) => {
394 push_observation(
395 &observations,
396 Observation::ResourceLimit {
397 kind: ResourceLimitKind::Memory,
398 detail: format!("exceeded {limit_bytes} byte memory budget"),
399 },
400 );
401 break;
402 }
403 Err(e) => {
404 errors.push(format!("script[{i}]: {e}"));
405 push_observation(
406 &observations,
407 Observation::Error {
408 message: format!("{e}"),
409 script_index: Some(i),
410 },
411 );
412 scripts_executed += 1;
413 if u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
414 >= config.timeout_ms
415 {
416 push_observation(
417 &observations,
418 Observation::ResourceLimit {
419 kind: ResourceLimitKind::Timeout,
420 detail: format!(
421 "single script exceeded {}ms timeout budget",
422 config.timeout_ms
423 ),
424 },
425 );
426 break;
427 }
428 }
429 }
430 }
431
432 if config.drain_timers {
434 for _ in 0..config.max_timer_drains {
435 if u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
436 >= config.timeout_ms
437 {
438 break;
439 }
440 let drain_result = eval_in_wasm(
441 &mut store,
442 &memory,
443 &jsdet_eval,
444 &jsdet_alloc,
445 &jsdet_free,
446 "if(typeof __jsdet_drain_timer==='function')__jsdet_drain_timer()",
447 );
448 if drain_result.is_err() {
449 break;
450 }
451 }
452 }
453
454 let _ = eval_in_wasm(
457 &mut store,
458 &memory,
459 &jsdet_eval,
460 &jsdet_alloc,
461 &jsdet_free,
462 "if(typeof __jsdet_probe_forms==='function')__jsdet_probe_forms()",
463 );
464
465 if config.drain_timers {
469 for _ in 0..5 {
470 if u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
471 >= config.timeout_ms
472 {
473 break;
474 }
475 let drain_result = eval_in_wasm(
476 &mut store,
477 &memory,
478 &jsdet_eval,
479 &jsdet_alloc,
480 &jsdet_free,
481 "if(typeof __jsdet_drain_timer==='function')__jsdet_drain_timer()",
482 );
483 if drain_result.is_err() {
484 break;
485 }
486 }
487 }
488
489 let _ = jsdet_destroy.call(&mut store, ());
491
492 let duration_us = u64::try_from(start.elapsed().as_micros()).unwrap_or(u64::MAX);
493 let collected = observations
494 .lock()
495 .unwrap_or_else(std::sync::PoisonError::into_inner)
496 .clone();
497
498 Ok(ExecutionResult {
499 observations: collected,
500 scripts_executed,
501 errors,
502 duration_us,
503 timed_out: u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
504 >= config.timeout_ms,
505 })
506 }
507
508 #[must_use]
513 pub fn snapshot_memory(store: &Store<HostState>, memory: &Memory) -> Vec<u8> {
514 memory.data(store).to_vec()
515 }
516
517 pub fn restore_memory(store: &mut Store<HostState>, memory: &Memory, snapshot: &[u8]) {
519 let data = memory.data_mut(store);
520 let len = snapshot.len().min(data.len());
521 data[..len].copy_from_slice(&snapshot[..len]);
522 }
523}
524
525#[derive(Debug, Clone)]
527pub struct ExecutionResult {
528 pub observations: Vec<Observation>,
530 pub scripts_executed: usize,
532 pub errors: Vec<String>,
534 pub duration_us: u64,
536 pub timed_out: bool,
538}
539
540#[allow(dead_code)]
546pub struct HostState {
547 pub bridge: Arc<dyn Bridge>,
548 pub observations: Arc<Mutex<Vec<Observation>>>,
549 pub context_id: ContextId,
550 pub config: SandboxConfig,
551}
552
553fn validate_scripts(scripts: &[String], config: &SandboxConfig) -> Result<()> {
554 let total_bytes: usize = scripts.iter().map(String::len).sum();
555 if total_bytes > config.max_total_script_bytes {
556 return Err(Error::Internal(format!(
557 "total script size {} exceeds limit {}",
558 total_bytes, config.max_total_script_bytes
559 )));
560 }
561 for (i, script) in scripts.iter().enumerate() {
562 if script.len() > config.max_script_bytes {
563 return Err(Error::Internal(format!(
564 "script[{i}] size {} exceeds limit {}",
565 script.len(),
566 config.max_script_bytes
567 )));
568 }
569 }
570 Ok(())
571}
572
573pub fn push_observation(observations: &Arc<Mutex<Vec<Observation>>>, obs: Observation) {
574 if let Ok(mut guard) = observations.lock() {
575 guard.push(obs);
576 }
577}
578
579#[allow(
588 clippy::too_many_lines,
589 clippy::cast_sign_loss,
590 clippy::cast_possible_truncation
591)]
592pub fn build_linker(
593 engine: &Engine,
594 bridge: &Arc<dyn Bridge>,
595 observations: &Arc<Mutex<Vec<Observation>>>,
596) -> Result<Linker<HostState>> {
597 let mut linker = Linker::new(engine);
598
599 linker
603 .func_wrap(
604 "wasi_snapshot_preview1",
605 "clock_time_get",
606 |mut caller: wasmtime::Caller<'_, HostState>,
607 _clock_id: i32,
608 _precision: i64,
609 result_ptr: i32|
610 -> i32 {
611 const FAKE_TIME_NS: u64 = 1_735_689_600_000_000_000; if let Some(memory) = caller.get_export("memory").and_then(Extern::into_memory) {
617 let ptr = result_ptr as usize;
618 let data = memory.data_mut(&mut caller);
619 if ptr.checked_add(8).is_some_and(|end| end <= data.len()) {
620 data[ptr..ptr + 8].copy_from_slice(&FAKE_TIME_NS.to_le_bytes());
621 }
622 }
623 0 },
625 )
626 .map_err(|e| Error::WasmInit(format!("wasi clock: {e}")))?;
627
628 linker
630 .func_wrap(
631 "wasi_snapshot_preview1",
632 "fd_close",
633 |_caller: wasmtime::Caller<'_, HostState>, _fd: i32| -> i32 { 0 },
634 )
635 .map_err(|e| Error::WasmInit(format!("wasi fd_close: {e}")))?;
636
637 linker
639 .func_wrap(
640 "wasi_snapshot_preview1",
641 "fd_fdstat_get",
642 |_: wasmtime::Caller<'_, HostState>, _: i32, _: i32| -> i32 { 0 },
643 )
644 .map_err(|e| Error::WasmInit(format!("wasi fd_fdstat_get: {e}")))?;
645
646 linker
648 .func_wrap(
649 "wasi_snapshot_preview1",
650 "fd_seek",
651 |_caller: wasmtime::Caller<'_, HostState>,
652 _fd: i32,
653 _offset: i64,
654 _whence: i32,
655 _newoffset: i32|
656 -> i32 { 76 },
657 )
658 .map_err(|e| Error::WasmInit(format!("wasi fd_seek: {e}")))?;
659
660 linker
662 .func_wrap(
663 "wasi_snapshot_preview1",
664 "fd_write",
665 move |mut caller: wasmtime::Caller<'_, HostState>,
666 _fd: i32,
667 iovs_ptr: i32,
668 iovs_len: i32,
669 nwritten_ptr: i32|
670 -> i32 {
671 let memory = caller.get_export("memory").and_then(Extern::into_memory);
673 let Some(memory) = memory else { return 8 }; let data = memory.data(&caller);
675
676 let mut total = 0u32;
677 for i in 0..iovs_len {
678 let Some(iov_offset) =
679 (iovs_ptr as usize).checked_add((i as usize).saturating_mul(8))
680 else {
681 break;
682 };
683 let Some(iov_end) = iov_offset.checked_add(8) else {
684 break;
685 };
686 if iov_end > data.len() {
687 break;
688 }
689 let _buf_ptr = match data[iov_offset..iov_offset + 4].try_into() {
692 Ok(b) => u32::from_le_bytes(b),
693 Err(_) => return 22, };
695 let buf_len = match data[iov_offset + 4..iov_offset + 8].try_into() {
696 Ok(b) => u32::from_le_bytes(b),
697 Err(_) => return 22, };
699 total += buf_len;
700 }
701
702 let nw_offset = nwritten_ptr as usize;
704 if nw_offset
705 .checked_add(4)
706 .is_some_and(|end| end <= memory.data(&caller).len())
707 {
708 let bytes = total.to_le_bytes();
709 memory.data_mut(&mut caller)[nw_offset..nw_offset + 4].copy_from_slice(&bytes);
710 }
711
712 0 },
714 )
715 .map_err(|e| Error::WasmInit(format!("wasi fd_write: {e}")))?;
716
717 let bridge_clone = bridge.clone();
720 let obs_clone = observations.clone();
721 linker
722 .func_wrap(
723 "jsdet",
724 "bridge_call",
725 move |mut caller: wasmtime::Caller<'_, HostState>,
726 api_ptr: i32,
727 api_len: i32,
728 args_ptr: i32,
729 args_len: i32|
730 -> i32 {
731 let memory = caller.get_export("memory").and_then(Extern::into_memory);
732 let Some(memory) = memory else { return 0 };
733 let data = memory.data(&caller);
734
735 let api = read_string(data, api_ptr as usize, api_len as usize);
736 let args_json = read_string(data, args_ptr as usize, args_len as usize);
737
738 let args: Vec<crate::observation::Value> = serde_json::from_str(&args_json)
740 .unwrap_or_else(|_| {
741 if args_json.is_empty() {
742 vec![]
743 } else {
744 vec![crate::observation::Value::string(args_json.clone())]
745 }
746 });
747
748 let result = bridge_clone.call(&api, &args);
750
751 let result_value = match &result {
753 Ok(v) => v.clone(),
754 Err(e) => crate::observation::Value::string(format!("Error: {e}")),
755 };
756 if let Ok(mut guard) = obs_clone.lock() {
757 guard.push(Observation::ApiCall {
758 api: api.clone(),
759 args,
760 result: result_value.clone(),
761 });
762 }
763
764 if let Ok(ref value) = result {
767 let json = value_to_json(value);
768 let json_bytes = json.as_bytes();
769 let write_len = json_bytes.len();
770
771 if let Ok(alloc_ret) = caller
773 .get_export("jsdet_alloc_return")
774 .and_then(Extern::into_func)
775 .ok_or(())
776 .and_then(|f| f.typed::<i32, i32>(&caller).map_err(|_| ()))
777 && let Ok(buf_ptr) = alloc_ret.call(&mut caller, write_len as i32)
778 && buf_ptr != 0
779 {
780 let buf_ptr = buf_ptr as usize;
781 let mem = caller.get_export("memory").and_then(Extern::into_memory);
782 if let Some(mem) = mem {
783 let data = mem.data_mut(&mut caller);
784 if buf_ptr
785 .checked_add(write_len)
786 .is_some_and(|end| end < data.len())
787 {
788 data[buf_ptr..buf_ptr + write_len].copy_from_slice(json_bytes);
789 }
790 }
791
792 if let Ok(set_len) = caller
793 .get_export("jsdet_set_return_len")
794 .and_then(Extern::into_func)
795 .ok_or(())
796 .and_then(|f| f.typed::<i32, ()>(&caller).map_err(|_| ()))
797 {
798 let _ = set_len.call(&mut caller, write_len as i32);
799 }
800 }
801 }
802
803 if result.is_ok() { 0 } else { -1 }
804 },
805 )
806 .map_err(|e| Error::WasmInit(format!("bridge_call link: {e}")))?;
807
808 let obs_clone2 = observations.clone();
810 linker
811 .func_wrap(
812 "jsdet",
813 "observe",
814 move |mut caller: wasmtime::Caller<'_, HostState>,
815 kind: i32,
816 data_ptr: i32,
817 data_len: i32| {
818 let memory = caller.get_export("memory").and_then(Extern::into_memory);
819 let Some(memory) = memory else { return };
820 let mem_data = memory.data(&caller);
821 let detail = read_string(mem_data, data_ptr as usize, data_len as usize);
822
823 let observation = match kind {
824 9 => Observation::Error {
825 message: detail,
826 script_index: None,
827 },
828 _ => Observation::ApiCall {
829 api: format!("__observe_kind_{kind}"),
830 args: vec![crate::observation::Value::string(detail)],
831 result: crate::observation::Value::Undefined,
832 },
833 };
834
835 if let Ok(mut guard) = obs_clone2.lock() {
836 guard.push(observation);
837 }
838 },
839 )
840 .map_err(|e| Error::WasmInit(format!("observe link: {e}")))?;
841
842 Ok(linker)
843}
844
845pub fn eval_in_wasm(
847 store: &mut Store<HostState>,
848 memory: &Memory,
849 jsdet_eval: &TypedFunc<(i32, i32), i32>,
850 jsdet_alloc: &TypedFunc<i32, i32>,
851 jsdet_free: &TypedFunc<i32, ()>,
852 script: &str,
853) -> Result<()> {
854 let bytes = script.as_bytes();
855 if bytes.len() >= i32::MAX as usize {
856 return Err(Error::Trap(
857 "script exceeds WASM 32-bit address space".into(),
858 ));
859 }
860 let alloc_len = (bytes.len() as i32) + 1;
863
864 let ptr = jsdet_alloc
866 .call(&mut *store, alloc_len)
867 .map_err(|e| Error::Trap(format!("alloc: {e}")))?;
868 if ptr == 0 {
869 return Err(Error::MemoryExceeded {
870 limit_bytes: store.data().config.max_memory_bytes,
871 });
872 }
873
874 {
876 let mem_data = memory.data_mut(&mut *store);
877 let start = ptr as usize;
878 let Some(end) = start.checked_add(bytes.len()) else {
879 return Err(Error::MemoryExceeded {
880 limit_bytes: mem_data.len(),
881 });
882 };
883 if end < mem_data.len() {
884 mem_data[start..end].copy_from_slice(bytes);
885 mem_data[end] = 0; } else {
887 return Err(Error::MemoryExceeded {
888 limit_bytes: mem_data.len(),
889 });
890 }
891 }
892
893 let len = bytes.len() as i32;
895 let result = jsdet_eval.call(&mut *store, (ptr, len));
896
897 let _ = jsdet_free.call(&mut *store, ptr);
899
900 match result {
901 Ok(0) => Ok(()),
902 Ok(code) => Err(Error::Trap(format!("jsdet_eval returned error {code}"))),
903 Err(e) => {
904 let msg = e.to_string();
905 if msg.contains("fuel") {
906 Err(Error::FuelExhausted {
907 budget: store.data().config.max_fuel,
908 })
909 } else if msg.contains("js_array_constructor")
910 || msg.contains("out of bounds memory access")
911 || msg.contains("memory access out of bounds")
912 || msg.contains("allocation too large")
913 || msg.contains("memory allocation failed")
914 {
915 Err(Error::MemoryExceeded {
916 limit_bytes: store.data().config.max_memory_bytes,
917 })
918 } else {
919 Err(Error::Trap(msg))
920 }
921 }
922 }
923}
924
925fn value_to_json(value: &crate::observation::Value) -> String {
927 match value {
928 crate::observation::Value::Undefined => "undefined".into(),
929 crate::observation::Value::Null => "null".into(),
930 crate::observation::Value::Bool(b) => if *b { "true" } else { "false" }.into(),
931 crate::observation::Value::Int(n) => n.to_string(),
932 crate::observation::Value::Float(n) => n.to_string(),
933 crate::observation::Value::String(s, _) => serde_json::to_string(s).unwrap_or_default(),
934 crate::observation::Value::Json(j, _) => j.clone(),
935 crate::observation::Value::Bytes(_) => "null".into(),
936 }
937}
938
939fn read_string(data: &[u8], ptr: usize, len: usize) -> String {
941 let Some(end) = ptr.checked_add(len) else {
942 return String::new(); };
944 if end > data.len() {
945 return String::new();
946 }
947 String::from_utf8_lossy(&data[ptr..end]).into_owned()
948}