1use std::cell::{Cell, RefCell};
4use std::collections::HashMap;
5use std::convert::TryFrom;
6use std::env;
7use std::ffi::OsStr;
8use std::fs;
9use std::io::Read;
10use std::path::{Component, Path, PathBuf};
11use std::rc::Rc;
12use std::sync::Arc;
13
14use crate::asset_store::{Asset, AssetStore};
15use crate::bundle::Bundle;
16use crate::error::{PyRunnerError, Result};
17use bytes::Bytes;
18use once_cell::sync::{Lazy, OnceCell};
19use parking_lot::RwLock;
20use serde::Deserialize;
21use serde_json::Value as JsonValue;
22use tracing::{debug, info, warn};
23use url::Url;
24use v8::{
25 self, script_compiler, Array, Context, ContextScope, FixedArray, Function,
26 FunctionCallbackArguments, Local, Module, ModuleRequest, Object, PinScope, Promise,
27 PromiseState, ReturnValue, String as V8String, Uint8Array, Value,
28};
29use walkdir::WalkDir;
30static V8_PLATFORM: OnceCell<v8::SharedRef<v8::Platform>> = OnceCell::new();
31static PACKAGE_ROOT: Lazy<RwLock<Option<PathBuf>>> = Lazy::new(|| RwLock::new(None));
32
33#[derive(Debug, Clone)]
34struct HostPattern {
35 kind: HostPatternKind,
36 port: Option<u16>,
37}
38
39#[derive(Debug, Clone)]
40enum HostPatternKind {
41 Exact(String),
42 WildcardSuffix(String),
43}
44
45#[derive(Debug, Clone)]
46pub struct NetworkContactRecord {
47 pub host: String,
48 pub port: Option<u16>,
49 pub https: bool,
50}
51
52#[derive(Debug, Clone)]
53pub struct NetworkDeniedRecord {
54 pub host: String,
55 pub port: Option<u16>,
56 pub reason: String,
57 pub https_required: bool,
58}
59
60#[derive(Debug, Clone)]
61struct NetworkPolicy {
62 entries: Vec<HostPattern>,
63 https_only: bool,
64}
65
66#[derive(Debug, Clone)]
67pub struct FilesystemViolationRecord {
68 pub path: Option<String>,
69 pub message: String,
70}
71
72impl Default for NetworkPolicy {
73 fn default() -> Self {
74 Self {
75 entries: Vec::new(),
76 https_only: true,
77 }
78 }
79}
80
81impl NetworkPolicy {
82 fn new(allow: &[String], https_only: bool) -> Self {
83 let entries = allow
84 .iter()
85 .filter_map(|value| HostPattern::from_pattern(value))
86 .collect();
87 Self {
88 entries,
89 https_only,
90 }
91 }
92
93 fn evaluate(&self, host: &str, port: Option<u16>, is_https: bool) -> NetworkDecision {
94 if self.entries.is_empty() {
95 return NetworkDecision::Denied(NetworkDenyReason::NoAllowlist);
96 }
97 if self.https_only && !is_https {
98 return NetworkDecision::Denied(NetworkDenyReason::SchemeNotAllowed);
99 }
100 let host_lc = host.to_ascii_lowercase();
101 for pattern in &self.entries {
102 if pattern.matches(&host_lc, port) {
103 return NetworkDecision::Allowed;
104 }
105 }
106 NetworkDecision::Denied(NetworkDenyReason::HostNotAllowed)
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum NetworkDecision {
112 Allowed,
113 Denied(NetworkDenyReason),
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117enum NetworkDenyReason {
118 NoAllowlist,
119 SchemeNotAllowed,
120 HostNotAllowed,
121}
122
123impl NetworkDenyReason {
124 fn as_str(&self) -> &'static str {
125 match self {
126 NetworkDenyReason::NoAllowlist => "no-allowlist",
127 NetworkDenyReason::SchemeNotAllowed => "scheme-not-allowed",
128 NetworkDenyReason::HostNotAllowed => "host-not-allowed",
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134pub enum FilesystemModeConfig {
135 Read,
136 ReadWrite,
137}
138
139impl HostPattern {
140 fn from_pattern(value: &str) -> Option<Self> {
141 let trimmed = value.trim();
142 if trimmed.is_empty() {
143 return None;
144 }
145 let lowered = trimmed.to_ascii_lowercase();
146 let (host_part, port) = split_host_and_port(&lowered);
147 if host_part.is_empty() {
148 return None;
149 }
150 if host_part.starts_with("*.") {
151 let suffix = host_part.trim_start_matches("*.").to_owned();
152 if suffix.is_empty() {
153 return None;
154 }
155 Some(Self {
156 kind: HostPatternKind::WildcardSuffix(suffix),
157 port,
158 })
159 } else {
160 Some(Self {
161 kind: HostPatternKind::Exact(host_part),
162 port,
163 })
164 }
165 }
166
167 fn matches(&self, host: &str, port: Option<u16>) -> bool {
168 let port_allowed = match (self.port, port) {
169 (Some(expected), Some(actual)) => expected == actual,
170 (Some(_), None) => false,
171 _ => true,
172 };
173 if !port_allowed {
174 return false;
175 }
176 match &self.kind {
177 HostPatternKind::Exact(expected) => host == expected,
178 HostPatternKind::WildcardSuffix(suffix) => host.ends_with(suffix),
179 }
180 }
181}
182
183fn split_host_and_port(value: &str) -> (String, Option<u16>) {
184 if let Some(idx) = value.rfind(':') {
185 let (host_part, port_part) = value.split_at(idx);
186 let port_str = &port_part[1..];
187 if !port_str.is_empty() && port_str.chars().all(|c| c.is_ascii_digit()) {
188 if let Ok(port) = port_str.parse::<u16>() {
189 return (host_part.to_owned(), Some(port));
190 }
191 }
192 }
193 (value.to_owned(), None)
194}
195
196pub struct PyodideLoadOptions<'a> {
198 pub snapshot: Option<&'a [u8]>,
199 pub make_snapshot: bool,
200}
201
202pub struct OverlayBlob {
204 pub key: String,
205 pub digest: Option<String>,
206 pub bytes: Vec<u8>,
207}
208
209pub struct OverlayExport {
211 pub metadata: Vec<u8>,
212 pub blobs: Vec<OverlayBlob>,
213}
214
215fn package_root_dir() -> Option<PathBuf> {
216 if let Some(path) = PACKAGE_ROOT.read().as_ref() {
217 return Some(path.clone());
218 }
219 let resolved = env::var_os("AARDVARK_PYODIDE_PACKAGE_DIR")
220 .map(PathBuf::from)
221 .map(normalize_package_root);
222 if let Some(ref path) = resolved {
223 tracing::debug!(
224 target = "aardvark::packages",
225 path = %path.display(),
226 "initialised package root"
227 );
228 *PACKAGE_ROOT.write() = Some(path.clone());
229 }
230 resolved
231}
232
233pub(crate) fn set_package_root_override(path: Option<PathBuf>) {
234 let normalized = path.map(normalize_package_root);
235 {
236 let mut guard = PACKAGE_ROOT.write();
237 *guard = normalized.clone();
238 }
239 match normalized {
240 Some(ref path) => tracing::debug!(
241 target = "aardvark::packages",
242 path = %path.display(),
243 "package root override set"
244 ),
245 None => tracing::debug!(
246 target = "aardvark::packages",
247 "cleared package root override"
248 ),
249 }
250}
251
252fn normalize_package_root(path: PathBuf) -> PathBuf {
253 if path.is_relative() {
254 if let Ok(cwd) = env::current_dir() {
255 return cwd.join(path);
256 }
257 }
258 path
259}
260
261#[cfg(test)]
262pub(crate) fn reset_package_root_for_tests() {
263 *PACKAGE_ROOT.write() = None;
264}
265
266fn resolve_local_package_path(url: &str) -> Option<PathBuf> {
267 let root = package_root_dir()?;
268 let scheme_split = url.find("://").map(|idx| idx + 3).unwrap_or(0);
269 let remainder = &url[scheme_split..];
270 let path_part = remainder.split_once('/').map_or("", |(_, rest)| rest);
271 if path_part.is_empty() {
272 return None;
273 }
274 let trimmed = path_part
275 .split(['?', '#'])
276 .next()
277 .unwrap_or("")
278 .trim_start_matches('/');
279 if trimmed.is_empty() {
280 return None;
281 }
282 let as_path = Path::new(trimmed);
283 let mut attempts: Vec<PathBuf> = Vec::new();
284
285 if let Some(file_name) = as_path.file_name() {
286 push_unique(&mut attempts, root.join(file_name));
287 }
288
289 if let Some(variant_relative) = strip_variant_prefix(as_path) {
290 push_unique(&mut attempts, root.join(&variant_relative));
291 if let Some(last) = variant_relative.file_name() {
292 push_unique(&mut attempts, root.join(last));
293 }
294 }
295
296 push_unique(&mut attempts, root.join(as_path));
297 push_unique(&mut attempts, root.join("pyodide").join(as_path));
298
299 if let Some(file_name) = as_path.file_name() {
300 push_unique(&mut attempts, root.join("full").join(file_name));
301 }
302
303 for candidate in attempts {
304 tracing::debug!(
305 target = "aardvark::packages",
306 path = %candidate.display(),
307 exists = candidate.exists(),
308 "checking local package candidate"
309 );
310 if candidate.exists() {
311 tracing::debug!(
312 target = "aardvark::packages",
313 path = %candidate.display(),
314 "resolved local package path"
315 );
316 return Some(candidate);
317 }
318 }
319 if let Some(file_name) = as_path.file_name() {
320 if let Some(found) = walk_for_file(&root, file_name) {
321 tracing::debug!(
322 target = "aardvark::packages",
323 path = %found.display(),
324 "resolved local package path via search"
325 );
326 return Some(found);
327 }
328 }
329 None
330}
331
332fn strip_variant_prefix(path: &Path) -> Option<PathBuf> {
333 let mut components = path.components();
334 match (components.next()?, components.next(), components.next()) {
335 (
336 Component::Normal(first),
337 Some(Component::Normal(_version)),
338 Some(Component::Normal(_variant)),
339 ) if first == OsStr::new("pyodide") => {
340 let remaining = components.as_path();
341 if remaining.as_os_str().is_empty() {
342 None
343 } else {
344 Some(remaining.to_path_buf())
345 }
346 }
347 _ => None,
348 }
349}
350
351fn push_unique(list: &mut Vec<PathBuf>, candidate: PathBuf) {
352 if !list.iter().any(|existing| existing == &candidate) {
353 list.push(candidate);
354 }
355}
356
357fn walk_for_file(root: &Path, needle: &OsStr) -> Option<PathBuf> {
358 let walker = WalkDir::new(root).into_iter();
359 for entry in walker.filter_map(|e| e.ok()) {
360 let path = entry.path();
361 if path.file_name() == Some(needle) {
362 return Some(path.to_path_buf());
363 }
364 }
365 None
366}
367
368fn guess_content_type(path: &Path) -> &'static str {
369 match path
370 .extension()
371 .and_then(|ext| ext.to_str())
372 .map(|s| s.to_ascii_lowercase())
373 .as_deref()
374 {
375 Some("json") => "application/json",
376 Some("js") => "application/javascript",
377 Some("wasm") => "application/wasm",
378 Some("txt") => "text/plain; charset=utf-8",
379 Some("py") => "text/x-python",
380 Some("data") => "application/octet-stream",
381 Some("whl") => "application/octet-stream",
382 Some("zip") => "application/zip",
383 Some("gz") => "application/gzip",
384 Some("bz2") => "application/x-bzip2",
385 Some("tar") => "application/x-tar",
386 _ => "application/octet-stream",
387 }
388}
389
390fn copy_typed_array(array: Local<Uint8Array>) -> Vec<u8> {
391 let length = array.length();
392 let mut data = vec![0u8; length];
393 array.copy_contents(&mut data);
394 data
395}
396
397thread_local! {
398 static TLS_RUNTIME_CONTEXT: Cell<*const RuntimeContext> = const { Cell::new(std::ptr::null()) };
399 static TLS_SCOPE: Cell<*mut std::ffi::c_void> = const { Cell::new(std::ptr::null_mut()) };
400}
401
402fn init_v8() {
404 V8_PLATFORM.get_or_init(|| {
405 let platform = v8::new_default_platform(0, false);
406 let shared = platform.make_shared();
407 v8::V8::initialize_platform(shared.clone());
408 v8::V8::initialize();
409 shared
410 });
411}
412
413pub struct JsRuntime {
415 isolate: v8::OwnedIsolate,
416 context: v8::Global<v8::Context>,
417 context_state: Rc<RuntimeContext>,
418}
419
420struct RuntimeContext {
421 assets: AssetStore,
422 modules: RefCell<HashMap<String, v8::Global<Module>>>,
423 module_by_hash: RefCell<HashMap<i32, String>>,
424 module_namespaces: RefCell<HashMap<String, v8::Global<Object>>>,
425 pyodide_instance: RefCell<Option<v8::Global<Object>>>,
426 stdout_log: RefCell<String>,
427 stderr_log: RefCell<String>,
428 network_policy: RwLock<NetworkPolicy>,
429 network_contacts: RwLock<Vec<NetworkContactRecord>>,
430 network_denied: RwLock<Vec<NetworkDeniedRecord>>,
431 filesystem_violations: RwLock<Vec<FilesystemViolationRecord>>,
432}
433
434enum ConsoleStream {
435 Stdout,
436 Stderr,
437}
438
439impl JsRuntime {
440 pub fn new() -> Result<Self> {
442 init_v8();
443 let context_state = Rc::new(RuntimeContext::new());
444 let create_params =
445 v8::CreateParams::default().array_buffer_allocator(v8::new_default_allocator());
446 let mut isolate = v8::Isolate::new(create_params);
447 isolate.set_slot(context_state.clone());
448 let global = {
449 v8::scope!(let scope, &mut isolate);
450 let context = v8::Context::new(scope, v8::ContextOptions::default());
451 v8::Global::new(scope, context)
452 };
453
454 let mut runtime = Self {
455 isolate,
456 context: global,
457 context_state,
458 };
459 runtime.install_polyfills()?;
460 Ok(runtime)
461 }
462
463 pub fn reset(&mut self) -> Result<()> {
465 let new_state = Rc::new(RuntimeContext::new());
467 self.context_state = new_state.clone();
468 self.isolate.set_slot(new_state);
469
470 self.isolate.low_memory_notification();
472
473 let global = {
474 v8::scope!(let scope, &mut self.isolate);
475 let context = v8::Context::new(scope, v8::ContextOptions::default());
476 v8::Global::new(scope, context)
477 };
478 self.context = global;
479 self.install_polyfills()
480 }
481
482 pub fn set_network_policy(&self, allow: &[String], https_only: bool) {
484 self.context_state.set_network_policy(allow, https_only);
485 }
486
487 pub fn clear_network_contacts(&self) {
489 self.context_state.clear_network_contacts();
490 }
491
492 pub fn drain_network_contacts(&self) -> Vec<NetworkContactRecord> {
494 self.context_state.take_network_contacts()
495 }
496
497 pub fn clear_network_denied(&self) {
499 self.context_state.clear_network_denied();
500 }
501
502 pub fn drain_network_denied(&self) -> Vec<NetworkDeniedRecord> {
504 self.context_state.take_network_denied()
505 }
506
507 pub fn clear_filesystem_events(&self) {
509 self.context_state.clear_filesystem_violations();
510 }
511
512 pub fn drain_filesystem_violations(&self) -> Vec<FilesystemViolationRecord> {
514 self.context_state.take_filesystem_violations()
515 }
516
517 pub fn set_filesystem_policy(
519 &mut self,
520 mode: FilesystemModeConfig,
521 quota_bytes: Option<u64>,
522 ) -> Result<()> {
523 self.with_context(|scope, _| {
524 let global = scope.get_current_context().global(scope);
525 let key = v8::String::new(scope, "__aardvarkFilesystemSetPolicy").ok_or_else(|| {
526 PyRunnerError::Execution("filesystem policy hook unavailable".into())
527 })?;
528 let value = global.get(scope, key.into()).ok_or_else(|| {
529 PyRunnerError::Execution("__aardvarkFilesystemSetPolicy missing".into())
530 })?;
531 let func = Local::<Function>::try_from(value).map_err(|_| {
532 PyRunnerError::Execution("__aardvarkFilesystemSetPolicy is not a function".into())
533 })?;
534 let policy = v8::Object::new(scope);
535 let mode_key = v8::String::new(scope, "mode").unwrap();
536 let mode_value = v8::String::new(
537 scope,
538 match mode {
539 FilesystemModeConfig::Read => "read",
540 FilesystemModeConfig::ReadWrite => "readWrite",
541 },
542 )
543 .unwrap();
544 let _ = policy.set(scope, mode_key.into(), mode_value.into());
545 if let Some(quota) = quota_bytes {
546 let quota_key = v8::String::new(scope, "quotaBytes").unwrap();
547 let quota_value = v8::Number::new(scope, quota as f64);
548 let _ = policy.set(scope, quota_key.into(), quota_value.into());
549 }
550 func.call(scope, global.into(), &[policy.into()])
551 .ok_or_else(|| {
552 PyRunnerError::Execution("filesystem policy update failed".into())
553 })?;
554 Ok(())
555 })
556 }
557
558 pub fn run_js_entrypoint(&mut self, entrypoint: &str) -> Result<ExecutionOutput> {
560 let entry_trimmed = entrypoint.trim();
561 let (module_part, export_part) = entry_trimmed
562 .split_once(':')
563 .map(|(module, export)| (module.trim(), export.trim()))
564 .unwrap_or((entry_trimmed, "default"));
565
566 if module_part.is_empty() {
567 return Err(PyRunnerError::Execution(
568 "entrypoint must include a module name".into(),
569 ));
570 }
571
572 let export_name = if export_part.is_empty() {
573 "default"
574 } else {
575 export_part
576 };
577
578 let mut specifier = module_part.replace('.', "/");
579 if !specifier.ends_with(".js") {
580 specifier.push_str(".js");
581 }
582 let specifier = normalize_specifier(&specifier);
583 self.ensure_module(&specifier)?;
584
585 enum InvocationResult {
586 Immediate(v8::Global<v8::Value>),
587 Promise(v8::Global<Promise>),
588 Exception {
589 typ: String,
590 value: String,
591 stack: Option<String>,
592 },
593 }
594
595 let ctx_state = self.context_state.clone();
596 ctx_state.clear_console();
597
598 self.with_context(|scope, _| {
599 let global = scope.get_current_context().global(scope);
600 let reset_key = v8::String::new(scope, "__aardvarkResetSharedBuffers").unwrap();
601 if let Some(reset_value) = global.get(scope, reset_key.into()) {
602 if let Ok(reset_fn) = Local::<Function>::try_from(reset_value) {
603 let _ = reset_fn.call(scope, global.into(), &[]);
604 }
605 }
606 Ok(())
607 })?;
608
609 let mut invocation: Option<InvocationResult> = None;
610
611 self.with_context(|scope, _| {
612 v8::tc_scope!(let try_catch, scope);
613 let Some(namespace) = ctx_state.module_namespace(try_catch, &specifier) else {
614 return Err(PyRunnerError::Execution(format!(
615 "module '{specifier}' not loaded"
616 )));
617 };
618
619 let export_key = v8::String::new(try_catch, export_name)
620 .ok_or_else(|| PyRunnerError::Execution("failed to allocate export name".into()))?;
621 let export_value = namespace.get(try_catch, export_key.into()).ok_or_else(|| {
622 PyRunnerError::Execution(format!(
623 "module '{specifier}' missing export '{export_name}'"
624 ))
625 })?;
626
627 let Ok(function) = v8::Local::<Function>::try_from(export_value) else {
628 return Err(PyRunnerError::Execution(format!(
629 "export '{export_name}' is not callable"
630 )));
631 };
632
633 let global = try_catch.get_current_context().global(try_catch);
634 let wrap_key = v8::String::new(try_catch, "__aardvarkWrapRawctxFunction").unwrap();
635 let mut callable = function;
636 if let Some(wrap_value) = global.get(try_catch, wrap_key.into()) {
637 if let Ok(wrap_fn) = v8::Local::<Function>::try_from(wrap_value) {
638 let module_name = v8::String::new(try_catch, module_part).ok_or_else(|| {
639 PyRunnerError::Execution("failed to allocate module name".into())
640 })?;
641 let export_js = v8::String::new(try_catch, export_name).ok_or_else(|| {
642 PyRunnerError::Execution("failed to allocate export name".into())
643 })?;
644 let wrapped = wrap_fn.call(
645 try_catch,
646 global.into(),
647 &[callable.into(), module_name.into(), export_js.into()],
648 );
649 if let Some(value) = wrapped {
650 if let Ok(func) = v8::Local::<Function>::try_from(value) {
651 callable = func;
652 }
653 }
654 }
655 }
656
657 let call_result = callable.call(try_catch, namespace.into(), &[]);
658 let Some(value) = call_result else {
659 let exception = try_catch.exception();
660 let mut typ = "JavaScriptError".to_string();
661 let mut message = "javascript execution failed".to_string();
662 let mut stack: Option<String> = None;
663
664 if let Some(object) = exception.and_then(|value| value.to_object(try_catch)) {
665 if let Some(name_value) = object.get(
666 try_catch,
667 v8::String::new(try_catch, "name")
668 .ok_or_else(|| {
669 PyRunnerError::Execution(
670 "failed to allocate error name string".into(),
671 )
672 })?
673 .into(),
674 ) {
675 if let Some(name_str) = name_value.to_string(try_catch) {
676 typ = name_str.to_rust_string_lossy(try_catch);
677 }
678 }
679 if let Some(message_value) = object.get(
680 try_catch,
681 v8::String::new(try_catch, "message")
682 .ok_or_else(|| {
683 PyRunnerError::Execution(
684 "failed to allocate error message string".into(),
685 )
686 })?
687 .into(),
688 ) {
689 if let Some(msg_str) = message_value.to_string(try_catch) {
690 message = msg_str.to_rust_string_lossy(try_catch);
691 }
692 }
693 } else if let Some(value) = exception {
694 if let Some(msg) = value.to_string(try_catch) {
695 message = msg.to_rust_string_lossy(try_catch);
696 }
697 }
698
699 if let Some(stack_value) = try_catch.stack_trace() {
700 if let Some(stack_str) = stack_value.to_string(try_catch) {
701 stack = Some(stack_str.to_rust_string_lossy(try_catch));
702 }
703 }
704
705 invocation = Some(InvocationResult::Exception {
706 typ,
707 value: message,
708 stack,
709 });
710 return Ok(());
711 };
712
713 if let Ok(promise) = v8::Local::<Promise>::try_from(value) {
714 invocation = Some(InvocationResult::Promise(v8::Global::new(
715 try_catch, promise,
716 )));
717 } else {
718 invocation = Some(InvocationResult::Immediate(v8::Global::new(
719 try_catch, value,
720 )));
721 }
722 Ok(())
723 })?;
724
725 let invocation = invocation.unwrap_or_else(|| InvocationResult::Exception {
726 typ: "JavaScriptError".to_string(),
727 value: "javascript execution failed".to_string(),
728 stack: None,
729 });
730
731 let mut execution = ExecutionOutput {
732 stdout: String::new(),
733 stderr: String::new(),
734 result: None,
735 exception_type: None,
736 exception_value: None,
737 traceback: None,
738 json: None,
739 shared_buffers: Vec::new(),
740 };
741
742 match invocation {
743 InvocationResult::Exception { typ, value, stack } => {
744 execution.exception_type = Some(typ);
745 execution.exception_value = Some(value);
746 execution.traceback = stack;
747 }
748 InvocationResult::Immediate(value_global) => {
749 populate_execution_output(self, value_global, &mut execution)?;
750 }
751 InvocationResult::Promise(promise_global) => {
752 enum PromiseOutcome {
753 Pending,
754 Fulfilled(v8::Global<v8::Value>),
755 Rejected {
756 typ: String,
757 value: String,
758 stack: Option<String>,
759 },
760 }
761
762 let mut resolved_value: Option<v8::Global<v8::Value>> = None;
763
764 loop {
765 let outcome = self.with_context(|scope, _| -> Result<PromiseOutcome> {
766 let promise = v8::Local::new(scope, &promise_global);
767 match promise.state() {
768 PromiseState::Pending => Ok(PromiseOutcome::Pending),
769 PromiseState::Fulfilled => {
770 let value = promise.result(scope);
771 Ok(PromiseOutcome::Fulfilled(v8::Global::new(scope, value)))
772 }
773 PromiseState::Rejected => {
774 let reason = promise.result(scope);
775 let mut typ = "JavaScriptError".to_string();
776 let mut message = reason
777 .to_string(scope)
778 .map(|s| s.to_rust_string_lossy(scope))
779 .unwrap_or_else(|| "javascript promise rejected".into());
780 let mut stack: Option<String> = None;
781 if let Some(object) = reason.to_object(scope) {
782 if let Some(name_value) = object.get(
783 scope,
784 v8::String::new(scope, "name")
785 .ok_or_else(|| {
786 PyRunnerError::Execution(
787 "failed to allocate error name string".into(),
788 )
789 })?
790 .into(),
791 ) {
792 if let Some(name_str) = name_value.to_string(scope) {
793 typ = name_str.to_rust_string_lossy(scope);
794 }
795 }
796 if let Some(message_value) = object.get(
797 scope,
798 v8::String::new(scope, "message")
799 .ok_or_else(|| {
800 PyRunnerError::Execution(
801 "failed to allocate error message string"
802 .into(),
803 )
804 })?
805 .into(),
806 ) {
807 if let Some(msg_str) = message_value.to_string(scope) {
808 message = msg_str.to_rust_string_lossy(scope);
809 }
810 }
811 if let Some(stack_value) = object.get(
812 scope,
813 v8::String::new(scope, "stack")
814 .ok_or_else(|| {
815 PyRunnerError::Execution(
816 "failed to allocate error stack string".into(),
817 )
818 })?
819 .into(),
820 ) {
821 if let Some(stack_str) = stack_value.to_string(scope) {
822 stack = Some(stack_str.to_rust_string_lossy(scope));
823 }
824 }
825 }
826 Ok(PromiseOutcome::Rejected {
827 typ,
828 value: message,
829 stack,
830 })
831 }
832 }
833 })?;
834
835 match outcome {
836 PromiseOutcome::Pending => {
837 self.isolate.perform_microtask_checkpoint();
838 }
839 PromiseOutcome::Fulfilled(value) => {
840 resolved_value = Some(value);
841 break;
842 }
843 PromiseOutcome::Rejected { typ, value, stack } => {
844 execution.exception_type = Some(typ);
845 execution.exception_value = Some(value);
846 execution.traceback = stack;
847 break;
848 }
849 }
850 }
851
852 if let Some(value_global) = resolved_value {
853 populate_execution_output(self, value_global, &mut execution)?;
854 }
855 }
856 }
857
858 let shared_buffers = self.with_context(|scope, _| -> Result<Vec<SharedBuffer>> {
859 let global = scope.get_current_context().global(scope);
860 let buffers = collect_shared_buffers(scope, global)?;
861 if !buffers.is_empty() {
862 let release_ids: Vec<String> =
863 buffers.iter().map(|buffer| buffer.id.clone()).collect();
864 release_shared_buffers(scope, global, &release_ids)?;
865 }
866 Ok(buffers)
867 })?;
868 execution.shared_buffers = shared_buffers;
869
870 execution.stdout = ctx_state.take_stdout();
871 execution.stderr = ctx_state.take_stderr();
872
873 Ok(execution)
874 }
875
876 pub fn reset_shared_buffers(&mut self) -> Result<()> {
878 self.with_context(|scope, _| {
879 let global = scope.get_current_context().global(scope);
880 let key = v8::String::new(scope, "__aardvarkResetSharedBuffers").ok_or_else(|| {
881 PyRunnerError::Execution("shared buffer reset hook unavailable".into())
882 })?;
883 if let Some(value) = global.get(scope, key.into()) {
884 if let Ok(func) = Local::<Function>::try_from(value) {
885 func.call(scope, global.into(), &[]).ok_or_else(|| {
886 PyRunnerError::Execution("shared buffer reset failed".into())
887 })?;
888 }
889 }
890 Ok(())
891 })
892 }
893
894 pub fn reset_filesystem(&mut self) -> Result<()> {
896 self.with_context(|scope, _| {
897 let global = scope.get_current_context().global(scope);
898 let key = v8::String::new(scope, "__aardvarkFilesystemReset").ok_or_else(|| {
899 PyRunnerError::Execution("filesystem reset hook unavailable".into())
900 })?;
901 let value = global.get(scope, key.into()).ok_or_else(|| {
902 PyRunnerError::Execution("__aardvarkFilesystemReset missing".into())
903 })?;
904 let func = Local::<Function>::try_from(value).map_err(|_| {
905 PyRunnerError::Execution("__aardvarkFilesystemReset is not a function".into())
906 })?;
907 func.call(scope, global.into(), &[])
908 .ok_or_else(|| PyRunnerError::Execution("filesystem reset failed".into()))?;
909 Ok(())
910 })
911 }
912
913 pub fn filesystem_usage_bytes(&mut self) -> Result<u64> {
915 self.with_context(|scope, _| {
916 let global = scope.get_current_context().global(scope);
917 let key = v8::String::new(scope, "__aardvarkFilesystemGetUsage").ok_or_else(|| {
918 PyRunnerError::Execution("filesystem usage hook unavailable".into())
919 })?;
920 let value = global.get(scope, key.into()).ok_or_else(|| {
921 PyRunnerError::Execution("__aardvarkFilesystemGetUsage missing".into())
922 })?;
923 let func = Local::<Function>::try_from(value).map_err(|_| {
924 PyRunnerError::Execution("__aardvarkFilesystemGetUsage is not a function".into())
925 })?;
926 let usage_value = func
927 .call(scope, global.into(), &[])
928 .ok_or_else(|| PyRunnerError::Execution("filesystem usage query failed".into()))?;
929 let number = usage_value
930 .to_number(scope)
931 .ok_or_else(|| PyRunnerError::Execution("filesystem usage not a number".into()))?;
932 let value = number.value();
933 Ok(if value <= 0.0 { 0 } else { value as u64 })
934 })
935 }
936
937 pub fn set_host_capabilities(&mut self, capabilities: &[String]) -> Result<()> {
939 self.with_context(|scope, _| {
940 let global = scope.get_current_context().global(scope);
941 let key = v8::String::new(scope, "__aardvarkSetHostCapabilities").ok_or_else(|| {
942 PyRunnerError::Execution("host capability hook unavailable".into())
943 })?;
944 let value = global.get(scope, key.into()).ok_or_else(|| {
945 PyRunnerError::Execution("__aardvarkSetHostCapabilities missing".into())
946 })?;
947 let func = Local::<Function>::try_from(value).map_err(|_| {
948 PyRunnerError::Execution("__aardvarkSetHostCapabilities is not a function".into())
949 })?;
950 let array = v8::Array::new(scope, capabilities.len() as i32);
951 for (index, capability) in capabilities.iter().enumerate() {
952 if let Some(value) = v8::String::new(scope, capability) {
953 array.set_index(scope, index as u32, value.into());
954 }
955 }
956 func.call(scope, global.into(), &[array.into()])
957 .ok_or_else(|| {
958 PyRunnerError::Execution("applying host capabilities failed".into())
959 })?;
960 Ok(())
961 })
962 }
963
964 pub(crate) fn isolate_handle(&mut self) -> v8::IsolateHandle {
965 self.isolate.thread_safe_handle()
966 }
967
968 pub(crate) fn cancel_terminate_execution(&mut self) {
969 let _ = self.isolate.cancel_terminate_execution();
970 }
971
972 pub(crate) fn heap_used_bytes(&mut self) -> usize {
973 let stats = self.isolate.get_heap_statistics();
974 stats.used_heap_size()
975 }
976
977 fn install_polyfills(&mut self) -> Result<()> {
978 self.with_context(|scope, context| {
979 let global = context.global(scope);
980 let source = include_str!("js/polyfills.js");
981 let script_name = "polyfills.js";
982 exec_script(scope, script_name, source)
983 .map_err(|msg| PyRunnerError::Init(format!("polyfill error: {msg}")))?;
984 let fetch_name = v8::String::new(scope, "__pyRunnerFetchAsset")
986 .ok_or_else(|| PyRunnerError::Init("failed to allocate v8 string".into()))?;
987 let template = v8::FunctionTemplate::new(scope, asset_fetch_callback);
988 let function = template
989 .get_function(scope)
990 .ok_or_else(|| PyRunnerError::Init("failed to create asset hook".into()))?;
991 let _ = global.set(scope, fetch_name.into(), function.into());
992
993 let log_name = v8::String::new(scope, "__pyRunnerNativeLog")
994 .ok_or_else(|| PyRunnerError::Init("failed to allocate log string".into()))?;
995 let log_template = v8::FunctionTemplate::new(scope, native_log_callback);
996 let log_function = log_template
997 .get_function(scope)
998 .ok_or_else(|| PyRunnerError::Init("failed to create log hook".into()))?;
999 let _ = global.set(scope, log_name.into(), log_function.into());
1000
1001 let native_fetch_name =
1002 v8::String::new(scope, "__pyRunnerNativeFetch").ok_or_else(|| {
1003 PyRunnerError::Init("failed to allocate native fetch string".into())
1004 })?;
1005 let native_fetch_template = v8::FunctionTemplate::new(scope, native_fetch_callback);
1006 let native_fetch_function = native_fetch_template
1007 .get_function(scope)
1008 .ok_or_else(|| PyRunnerError::Init("failed to create native fetch hook".into()))?;
1009 let _ = global.set(
1010 scope,
1011 native_fetch_name.into(),
1012 native_fetch_function.into(),
1013 );
1014
1015 let record_name =
1016 v8::String::new(scope, "__aardvarkRecordBufferEvent").ok_or_else(|| {
1017 PyRunnerError::Init("failed to allocate buffer event string".into())
1018 })?;
1019 let record_template = v8::FunctionTemplate::new(scope, record_buffer_event_callback);
1020 let record_function = record_template
1021 .get_function(scope)
1022 .ok_or_else(|| PyRunnerError::Init("failed to create buffer event hook".into()))?;
1023 let _ = global.set(scope, record_name.into(), record_function.into());
1024
1025 let fs_violation_name = v8::String::new(scope, "__aardvarkFilesystemRecordViolation")
1026 .ok_or_else(|| {
1027 PyRunnerError::Init("failed to allocate fs violation string".into())
1028 })?;
1029 let fs_violation_template =
1030 v8::FunctionTemplate::new(scope, filesystem_violation_callback);
1031 let fs_violation_function =
1032 fs_violation_template.get_function(scope).ok_or_else(|| {
1033 PyRunnerError::Init("failed to create filesystem violation hook".into())
1034 })?;
1035 let _ = global.set(
1036 scope,
1037 fs_violation_name.into(),
1038 fs_violation_function.into(),
1039 );
1040 Ok(())
1041 })
1042 }
1043
1044 pub fn execute_script(&mut self, name: &str, source: &str) -> Result<()> {
1046 self.with_context(|scope, _context| {
1047 exec_script(scope, name, source)
1048 .map_err(|msg| PyRunnerError::Execution(format!("javascript error: {msg}")))
1049 })
1050 }
1051
1052 pub fn with_context<F, R>(&mut self, f: F) -> Result<R>
1054 where
1055 F: for<'a> FnOnce(&mut PinScope<'a, '_>, Local<'a, v8::Context>) -> Result<R>,
1056 {
1057 let context_global = self.context.clone();
1058 v8::scope!(let isolate_scope, &mut self.isolate);
1059 let context = v8::Local::new(isolate_scope, context_global);
1060 let mut context_scope = ContextScope::new(isolate_scope, context);
1061 f(&mut context_scope, context)
1062 }
1063
1064 pub fn insert_text_asset<S>(&self, name: &str, contents: S)
1066 where
1067 S: Into<Arc<str>>,
1068 {
1069 self.context_state.assets.insert_text(name, contents.into());
1070 }
1071
1072 pub fn insert_binary_asset(&self, name: &str, bytes: &'static [u8]) {
1074 self.context_state
1075 .assets
1076 .insert_bytes(name, Arc::<[u8]>::from(bytes));
1077 }
1078
1079 pub fn insert_binary_asset_owned(&self, name: &str, bytes: Vec<u8>) {
1081 self.context_state
1082 .assets
1083 .insert_bytes(name, Arc::<[u8]>::from(bytes.into_boxed_slice()));
1084 }
1085
1086 pub fn load_pyodide(&mut self, options: PyodideLoadOptions<'_>) -> Result<()> {
1088 if self.context_state.pyodide_instance.borrow().is_some() {
1089 return Ok(());
1090 }
1091 let ctx_state = self.context_state.clone();
1092 self.ensure_module("pyodide.mjs")?;
1093 self.ensure_module("pyodide_bootstrap.js")?;
1094 let mut promise_handle: Option<v8::Global<Promise>> = None;
1095 self.with_context(|scope, _| {
1096 let bootstrap = ctx_state
1097 .module_namespace(scope, "pyodide_bootstrap.js")
1098 .ok_or_else(|| {
1099 PyRunnerError::Execution("pyodide_bootstrap.js namespace unavailable".into())
1100 })?;
1101 let load_key = v8::String::new(scope, "loadPyRunnerPyodide").unwrap();
1102 let load_value = bootstrap.get(scope, load_key.into());
1103 let load_fn = load_value
1104 .and_then(|value| Local::<Function>::try_from(value).ok())
1105 .ok_or_else(|| {
1106 PyRunnerError::Execution(
1107 "pyodide_bootstrap.js does not export loadPyRunnerPyodide".into(),
1108 )
1109 })?;
1110 let js_options = v8::Object::new(scope);
1111 let index_key = v8::String::new(scope, "indexURL").unwrap();
1112 let index_value = v8::String::new(scope, ".").unwrap();
1113 let _ = js_options.set(scope, index_key.into(), index_value.into());
1114 if let Some(snapshot) = options.snapshot {
1115 let snapshot_key = v8::String::new(scope, "snapshot").unwrap();
1116 let backing = v8::ArrayBuffer::new_backing_store_from_vec(snapshot.to_vec());
1117 let shared = backing.make_shared();
1118 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &shared);
1119 let length = array_buffer.byte_length();
1120 let typed = Uint8Array::new(scope, array_buffer, 0, length).unwrap();
1121 let _ = js_options.set(scope, snapshot_key.into(), typed.into());
1122 }
1123 if options.make_snapshot {
1124 let make_key = v8::String::new(scope, "makeSnapshot").unwrap();
1125 let make_value = v8::Boolean::new(scope, true);
1126 let _ = js_options.set(scope, make_key.into(), make_value.into());
1127 }
1128 let value = load_fn
1129 .call(scope, bootstrap.into(), &[js_options.into()])
1130 .ok_or_else(|| PyRunnerError::Execution("loadPyodide invocation failed".into()))?;
1131 let promise = v8::Local::<Promise>::try_from(value).map_err(|_| {
1132 PyRunnerError::Execution("loadPyodide did not return a Promise".into())
1133 })?;
1134 promise_handle = Some(v8::Global::new(scope, promise));
1135 Ok(())
1136 })?;
1137
1138 let promise_global = promise_handle
1139 .ok_or_else(|| PyRunnerError::Execution("missing loadPyodide promise handle".into()))?;
1140
1141 loop {
1142 let done = self.with_context(|scope, _| -> Result<Option<()>> {
1143 let promise = v8::Local::new(scope, &promise_global);
1144 match promise.state() {
1145 PromiseState::Pending => Ok(None),
1146 PromiseState::Fulfilled => {
1147 let result = promise.result(scope);
1148 let obj = v8::Local::<Object>::try_from(result).map_err(|_| {
1149 PyRunnerError::Execution(
1150 "loadPyodide fulfilled with non-object result".into(),
1151 )
1152 })?;
1153 ctx_state
1154 .pyodide_instance
1155 .replace(Some(v8::Global::new(scope, obj)));
1156 Ok(Some(()))
1157 }
1158 PromiseState::Rejected => {
1159 let reason = promise.result(scope);
1160 let message = reason
1161 .to_string(scope)
1162 .map(|s| s.to_rust_string_lossy(scope))
1163 .unwrap_or_else(|| "unknown rejection".to_string());
1164 let detailed = reason
1165 .to_object(scope)
1166 .and_then(|obj| {
1167 let stack_key = v8::String::new(scope, "stack")?;
1168 obj.get(scope, stack_key.into())
1169 })
1170 .and_then(|value| value.to_string(scope))
1171 .map(|s| s.to_rust_string_lossy(scope));
1172 let message = detailed
1173 .map(|stack| format!("{message}\n{stack}"))
1174 .unwrap_or(message);
1175 Err(PyRunnerError::Execution(format!(
1176 "loadPyodide rejected: {message}"
1177 )))
1178 }
1179 }
1180 })?;
1181 if done.is_some() {
1182 break;
1183 }
1184 self.isolate.perform_microtask_checkpoint();
1185 }
1186
1187 self.prepare_dynlibs()?;
1188 Ok(())
1189 }
1190
1191 pub fn load_packages(&mut self, packages: &[String]) -> Result<()> {
1193 if packages.is_empty() {
1194 return Ok(());
1195 }
1196
1197 let mut promise_handle: Option<v8::Global<Promise>> = None;
1198 self.with_context(|scope, _| {
1199 let global = scope.get_current_context().global(scope);
1200 let key = v8::String::new(scope, "__pyRunnerLoadPackages").ok_or_else(|| {
1201 PyRunnerError::Execution("failed to allocate package loader key".into())
1202 })?;
1203 let value = global.get(scope, key.into()).ok_or_else(|| {
1204 PyRunnerError::Execution("__pyRunnerLoadPackages is not defined".into())
1205 })?;
1206 let func = Local::<Function>::try_from(value).map_err(|_| {
1207 PyRunnerError::Execution("__pyRunnerLoadPackages is not a function".into())
1208 })?;
1209
1210 let array = v8::Array::new(scope, packages.len() as i32);
1211 for (index, name) in packages.iter().enumerate() {
1212 let value = v8::String::new(scope, name).ok_or_else(|| {
1213 PyRunnerError::Execution("failed to allocate package name".into())
1214 })?;
1215 array.set_index(scope, index as u32, value.into());
1216 }
1217
1218 let promise_value = func
1219 .call(scope, global.into(), &[array.into()])
1220 .ok_or_else(|| {
1221 PyRunnerError::Execution("package loader invocation failed".into())
1222 })?;
1223 let promise = v8::Local::<Promise>::try_from(promise_value).map_err(|_| {
1224 PyRunnerError::Execution("package loader did not return a Promise".into())
1225 })?;
1226 promise_handle = Some(v8::Global::new(scope, promise));
1227 Ok(())
1228 })?;
1229
1230 let promise_global = promise_handle.ok_or_else(|| {
1231 PyRunnerError::Execution("missing package loader promise handle".into())
1232 })?;
1233
1234 loop {
1235 let done = self.with_context(|scope, _| -> Result<Option<()>> {
1236 let promise = v8::Local::new(scope, &promise_global);
1237 match promise.state() {
1238 PromiseState::Pending => Ok(None),
1239 PromiseState::Fulfilled => Ok(Some(())),
1240 PromiseState::Rejected => {
1241 let reason = promise.result(scope);
1242 let message = reason
1243 .to_string(scope)
1244 .map(|s| s.to_rust_string_lossy(scope))
1245 .unwrap_or_else(|| "unknown rejection".to_string());
1246 let detailed = reason
1247 .to_object(scope)
1248 .and_then(|obj| {
1249 let stack_key = v8::String::new(scope, "stack")?;
1250 obj.get(scope, stack_key.into())
1251 })
1252 .and_then(|value| value.to_string(scope))
1253 .map(|s| s.to_rust_string_lossy(scope));
1254 let message = detailed
1255 .map(|stack| format!("{message}\n{stack}"))
1256 .unwrap_or(message);
1257 Err(PyRunnerError::Execution(format!(
1258 "loadPackages rejected: {message}"
1259 )))
1260 }
1261 }
1262 })?;
1263 if done.is_some() {
1264 break;
1265 }
1266 self.isolate.perform_microtask_checkpoint();
1267 }
1268
1269 Ok(())
1270 }
1271
1272 pub fn collect_snapshot(&mut self) -> Result<Vec<u8>> {
1274 let mut snapshot: Option<Vec<u8>> = None;
1275 self.with_context(|scope, _| {
1276 let global = scope.get_current_context().global(scope);
1277 let key = v8::String::new(scope, "__pyRunnerMakeSnapshot").ok_or_else(|| {
1278 PyRunnerError::Execution("failed to allocate snapshot key".into())
1279 })?;
1280 let value = global.get(scope, key.into()).ok_or_else(|| {
1281 PyRunnerError::Execution("__pyRunnerMakeSnapshot is not defined".into())
1282 })?;
1283 let func = Local::<Function>::try_from(value).map_err(|_| {
1284 PyRunnerError::Execution("__pyRunnerMakeSnapshot is not a function".into())
1285 })?;
1286 let result = func
1287 .call(scope, global.into(), &[])
1288 .ok_or_else(|| PyRunnerError::Execution("snapshot invocation failed".into()))?;
1289 let array = Local::<Uint8Array>::try_from(result).map_err(|_| {
1290 PyRunnerError::Execution(
1291 "__pyRunnerMakeSnapshot did not return a Uint8Array".into(),
1292 )
1293 })?;
1294 snapshot = Some(copy_typed_array(array));
1295 Ok(())
1296 })?;
1297
1298 snapshot.ok_or_else(|| PyRunnerError::Execution("snapshot helper returned no data".into()))
1299 }
1300
1301 pub fn export_overlay(&mut self) -> Result<OverlayExport> {
1303 let mut metadata: Option<Vec<u8>> = None;
1304 let mut blobs: Vec<OverlayBlob> = Vec::new();
1305 self.with_context(|scope, _| {
1306 let global = scope.get_current_context().global(scope);
1307 let key_meta = v8::String::new(scope, "__pyRunnerExportOverlay").ok_or_else(|| {
1308 PyRunnerError::Execution("failed to allocate overlay export key".into())
1309 })?;
1310 let value_meta = global.get(scope, key_meta.into()).ok_or_else(|| {
1311 PyRunnerError::Execution("__pyRunnerExportOverlay is not defined".into())
1312 })?;
1313 let func_meta = Local::<Function>::try_from(value_meta).map_err(|_| {
1314 PyRunnerError::Execution("__pyRunnerExportOverlay is not a function".into())
1315 })?;
1316 let meta_result = func_meta
1317 .call(scope, global.into(), &[])
1318 .ok_or_else(|| PyRunnerError::Execution("overlay export failed".into()))?;
1319 if meta_result.is_null_or_undefined() {
1320 metadata = Some(Vec::new());
1321 } else {
1322 let array = Local::<Uint8Array>::try_from(meta_result).map_err(|_| {
1323 PyRunnerError::Execution(
1324 "__pyRunnerExportOverlay did not return a Uint8Array".into(),
1325 )
1326 })?;
1327 metadata = Some(copy_typed_array(array));
1328 }
1329
1330 let key_blobs =
1331 v8::String::new(scope, "__pyRunnerExportOverlayBlobs").ok_or_else(|| {
1332 PyRunnerError::Execution("failed to allocate overlay blob export key".into())
1333 })?;
1334 let value_blobs = global.get(scope, key_blobs.into()).ok_or_else(|| {
1335 PyRunnerError::Execution("__pyRunnerExportOverlayBlobs is not defined".into())
1336 })?;
1337 let func_blobs = Local::<Function>::try_from(value_blobs).map_err(|_| {
1338 PyRunnerError::Execution("__pyRunnerExportOverlayBlobs is not a function".into())
1339 })?;
1340 let blobs_result = func_blobs
1341 .call(scope, global.into(), &[])
1342 .ok_or_else(|| PyRunnerError::Execution("overlay blob export failed".into()))?;
1343 if !blobs_result.is_null_or_undefined() {
1344 let array = Local::<v8::Array>::try_from(blobs_result).map_err(|_| {
1345 PyRunnerError::Execution(
1346 "__pyRunnerExportOverlayBlobs did not return an Array".into(),
1347 )
1348 })?;
1349 let key_prop = v8::String::new(scope, "key").ok_or_else(|| {
1350 PyRunnerError::Execution("failed to allocate blob key".into())
1351 })?;
1352 let digest_prop = v8::String::new(scope, "digest").ok_or_else(|| {
1353 PyRunnerError::Execution("failed to allocate blob digest".into())
1354 })?;
1355 let data_prop = v8::String::new(scope, "data").ok_or_else(|| {
1356 PyRunnerError::Execution("failed to allocate blob data".into())
1357 })?;
1358 let length = array.length();
1359 for index in 0..length {
1360 let value = array
1361 .get_index(scope, index)
1362 .unwrap_or_else(|| v8::undefined(scope).into());
1363 if value.is_null_or_undefined() {
1364 continue;
1365 }
1366 let object = Local::<Object>::try_from(value).map_err(|_| {
1367 PyRunnerError::Execution("overlay blob entry is not an object".into())
1368 })?;
1369 let key_value = object
1370 .get(scope, key_prop.into())
1371 .unwrap_or_else(|| v8::undefined(scope).into());
1372 let key = if key_value.is_null_or_undefined() {
1373 String::new()
1374 } else {
1375 key_value
1376 .to_string(scope)
1377 .ok_or_else(|| {
1378 PyRunnerError::Execution(
1379 "failed to convert overlay blob key to string".into(),
1380 )
1381 })?
1382 .to_rust_string_lossy(scope)
1383 };
1384 let digest_value = object
1385 .get(scope, digest_prop.into())
1386 .unwrap_or_else(|| v8::undefined(scope).into());
1387 let digest = if digest_value.is_null_or_undefined() {
1388 None
1389 } else {
1390 Some(
1391 digest_value
1392 .to_string(scope)
1393 .ok_or_else(|| {
1394 PyRunnerError::Execution(
1395 "failed to convert overlay blob digest to string".into(),
1396 )
1397 })?
1398 .to_rust_string_lossy(scope),
1399 )
1400 };
1401 let data_value = object.get(scope, data_prop.into()).ok_or_else(|| {
1402 PyRunnerError::Execution(
1403 "overlay blob entry missing 'data' property".into(),
1404 )
1405 })?;
1406 let data_array = Local::<Uint8Array>::try_from(data_value).map_err(|_| {
1407 PyRunnerError::Execution("overlay blob 'data' is not a Uint8Array".into())
1408 })?;
1409 let bytes = copy_typed_array(data_array);
1410 blobs.push(OverlayBlob { key, digest, bytes });
1411 }
1412 }
1413 Ok(())
1414 })?;
1415
1416 Ok(OverlayExport {
1417 metadata: metadata.ok_or_else(|| {
1418 PyRunnerError::Execution("overlay export returned no data".into())
1419 })?,
1420 blobs,
1421 })
1422 }
1423
1424 pub fn import_overlay(&mut self, metadata: &[u8], blobs: &[OverlayBlob]) -> Result<()> {
1426 self.with_context(|scope, _| {
1427 let global = scope.get_current_context().global(scope);
1428 let key = v8::String::new(scope, "__pyRunnerImportOverlay").ok_or_else(|| {
1429 PyRunnerError::Execution("failed to allocate overlay import key".into())
1430 })?;
1431 let value = global.get(scope, key.into()).ok_or_else(|| {
1432 PyRunnerError::Execution("__pyRunnerImportOverlay is not defined".into())
1433 })?;
1434 let func = Local::<Function>::try_from(value).map_err(|_| {
1435 PyRunnerError::Execution("__pyRunnerImportOverlay is not a function".into())
1436 })?;
1437 let meta_backing = v8::ArrayBuffer::new_backing_store_from_vec(metadata.to_vec());
1438 let meta_shared = meta_backing.make_shared();
1439 let meta_buffer = v8::ArrayBuffer::with_backing_store(scope, &meta_shared);
1440 let meta_typed =
1441 Uint8Array::new(scope, meta_buffer, 0, metadata.len()).ok_or_else(|| {
1442 PyRunnerError::Execution("failed to allocate overlay metadata buffer".into())
1443 })?;
1444
1445 let blob_array = v8::Array::new(scope, blobs.len() as i32);
1446 let key_prop = v8::String::new(scope, "key")
1447 .ok_or_else(|| PyRunnerError::Execution("failed to allocate blob key".into()))?;
1448 let digest_prop = v8::String::new(scope, "digest")
1449 .ok_or_else(|| PyRunnerError::Execution("failed to allocate blob digest".into()))?;
1450 let data_prop = v8::String::new(scope, "data")
1451 .ok_or_else(|| PyRunnerError::Execution("failed to allocate blob data".into()))?;
1452 for (index, blob) in blobs.iter().enumerate() {
1453 let object = v8::Object::new(scope);
1454 let key_string = v8::String::new(scope, blob.key.as_str())
1455 .ok_or_else(|| PyRunnerError::Execution("failed to convert blob key".into()))?;
1456 object.set(scope, key_prop.into(), key_string.into());
1457 if let Some(digest) = &blob.digest {
1458 let digest_string =
1459 v8::String::new(scope, digest.as_str()).ok_or_else(|| {
1460 PyRunnerError::Execution("failed to convert blob digest".into())
1461 })?;
1462 object.set(scope, digest_prop.into(), digest_string.into());
1463 } else {
1464 let null_value = v8::null(scope);
1465 object.set(scope, digest_prop.into(), null_value.into());
1466 }
1467 let data_backing = v8::ArrayBuffer::new_backing_store_from_vec(blob.bytes.clone());
1468 let data_shared = data_backing.make_shared();
1469 let data_buffer = v8::ArrayBuffer::with_backing_store(scope, &data_shared);
1470 let data_array = Uint8Array::new(scope, data_buffer, 0, blob.bytes.len())
1471 .ok_or_else(|| {
1472 PyRunnerError::Execution(
1473 "failed to allocate overlay blob data buffer".into(),
1474 )
1475 })?;
1476 object.set(scope, data_prop.into(), data_array.into());
1477 blob_array.set_index(scope, index as u32, object.into());
1478 }
1479
1480 let _ = func.call(
1481 scope,
1482 global.into(),
1483 &[meta_typed.into(), blob_array.into()],
1484 );
1485 Ok(())
1486 })?;
1487 self.prepare_dynlibs()
1488 }
1489
1490 pub fn prepare_dynlibs(&mut self) -> Result<()> {
1492 self.with_context(|scope, _| {
1493 let global = scope.get_current_context().global(scope);
1494 let key = v8::String::new(scope, "__pyRunnerPrepareDynlibs").ok_or_else(|| {
1495 PyRunnerError::Execution("failed to allocate dynlib preparation key".into())
1496 })?;
1497 let value = global.get(scope, key.into()).ok_or_else(|| {
1498 PyRunnerError::Execution("__pyRunnerPrepareDynlibs is not defined".into())
1499 })?;
1500 let func = Local::<Function>::try_from(value).map_err(|_| {
1501 PyRunnerError::Execution("__pyRunnerPrepareDynlibs is not a function".into())
1502 })?;
1503 let _ = func.call(scope, global.into(), &[]);
1504 Ok(())
1505 })
1506 }
1507
1508 pub fn mount_bundle(&mut self, bundle: &Bundle, root: &str) -> Result<()> {
1510 let ctx_state = self.context_state.clone();
1511 let root_owned = root.to_owned();
1512 self.with_context(|scope, _| {
1513 let pyodide = ctx_state
1514 .pyodide_local(scope)
1515 .ok_or_else(|| PyRunnerError::Execution("Pyodide is not loaded".into()))?;
1516
1517 let files = v8::Array::new(scope, bundle.entries().len() as i32);
1518 for (index, entry) in bundle.entries().iter().enumerate() {
1519 let obj = v8::Object::new(scope);
1520 let rel_path = v8::String::new(scope, entry.path()).ok_or_else(|| {
1521 PyRunnerError::Execution("failed to allocate file path string".into())
1522 })?;
1523 let path_key = v8::String::new(scope, "path").unwrap();
1524 let data_key = v8::String::new(scope, "data").unwrap();
1525 let size_key = v8::String::new(scope, "size").unwrap();
1526
1527 let buffer = entry.contents().to_vec();
1528 let backing = v8::ArrayBuffer::new_backing_store_from_bytes(buffer);
1529 let shared = backing.make_shared();
1530 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &shared);
1531 let uint8 = Uint8Array::new(scope, array_buffer, 0, entry.contents().len())
1532 .ok_or_else(|| {
1533 PyRunnerError::Execution("failed to allocate typed array".into())
1534 })?;
1535 let size_value = v8::Number::new(scope, entry.contents().len() as f64);
1536
1537 let _ = obj.set(scope, path_key.into(), rel_path.into());
1538 let _ = obj.set(scope, data_key.into(), uint8.into());
1539 let _ = obj.set(scope, size_key.into(), size_value.into());
1540 files.set_index(scope, index as u32, obj.into());
1541 }
1542
1543 let global = scope.get_current_context().global(scope);
1544 let mount_fn_key = v8::String::new(scope, "__pyRunnerMountFiles").unwrap();
1545 let mount_fn_value = global
1546 .get(scope, mount_fn_key.into())
1547 .ok_or_else(|| {
1548 PyRunnerError::Execution("__pyRunnerMountFiles is not defined".into())
1549 })?;
1550 let mount_fn = Local::<Function>::try_from(mount_fn_value).map_err(|_| {
1551 PyRunnerError::Execution("__pyRunnerMountFiles is not a function".into())
1552 })?;
1553 let root_value = v8::String::new(scope, &root_owned).ok_or_else(|| {
1554 PyRunnerError::Execution("failed to allocate mount root string".into())
1555 })?;
1556 mount_fn
1557 .call(scope, global.into(), &[pyodide.into(), files.into(), root_value.into()])
1558 .ok_or_else(|| PyRunnerError::Execution("mount files call failed".into()))?;
1559
1560 let run_key = v8::String::new(scope, "runPython").unwrap();
1561 let run_value = pyodide.get(scope, run_key.into()).ok_or_else(|| {
1562 PyRunnerError::Execution("pyodide.runPython is not available".into())
1563 })?;
1564 let run_fn = Local::<Function>::try_from(run_value).map_err(|_| {
1565 PyRunnerError::Execution("pyodide.runPython is not a function".into())
1566 })?;
1567 let script = v8::String::new(
1568 scope,
1569 "import sys\nfrom pathlib import Path\napp = Path('/app')\nif str(app) not in sys.path:\n sys.path.insert(0, str(app))\n",
1570 )
1571 .ok_or_else(|| PyRunnerError::Execution("failed to allocate sys.path script".into()))?;
1572 let _ = run_fn.call(scope, pyodide.into(), &[script.into()]);
1573
1574 Ok(())
1575 })?;
1576 Ok(())
1577 }
1578
1579 pub fn run_python_entrypoint(&mut self, entrypoint: &str) -> Result<ExecutionOutput> {
1581 let ctx_state = self.context_state.clone();
1582 let entry_literal = serde_json::to_string(entrypoint).map_err(|err| {
1583 PyRunnerError::Execution(format!("failed to encode entrypoint: {err}"))
1584 })?;
1585 let script = format!(
1586 "import io, sys, importlib, json, traceback\nfrom js import globalThis as __aardvark_js\nif '__aardvark_publish_buffer' not in globals():\n def __aardvark_publish_buffer(buffer_id, data, metadata=None):\n return __aardvark_js.__aardvarkPublishBuffer(buffer_id, data, metadata)\nentrypoint = {entry}\n_stdout = io.StringIO()\n_stderr = io.StringIO()\n_old_out, _old_err = sys.stdout, sys.stderr\nresult_repr = None\nexc_type = None\nexc_value = None\nexc_traceback = None\ntry:\n sys.stdout = _stdout\n sys.stderr = _stderr\n module_name, sep, func_name = entrypoint.partition(':')\n if not module_name:\n raise ValueError('entrypoint must specify a module')\n module = importlib.import_module(module_name)\n if sep:\n target = getattr(module, func_name)\n value = target()\n elif hasattr(module, 'main'):\n value = module.main()\n else:\n value = None\n result_repr = repr(value)\nexcept Exception as exc: # noqa: BLE001\n exc_type = exc.__class__.__name__\n exc_value = repr(exc)\n exc_traceback = traceback.format_exc()\nfinally:\n sys.stdout = _old_out\n sys.stderr = _old_err\njson.dumps({{\"stdout\": _stdout.getvalue(), \"stderr\": _stderr.getvalue(), \"result\": result_repr, \"exception_type\": exc_type, \"exception_value\": exc_value, \"traceback\": exc_traceback}})\n",
1587 entry = entry_literal
1588 );
1589
1590 self.with_context(|scope, _| {
1591 let pyodide = ctx_state
1592 .pyodide_local(scope)
1593 .ok_or_else(|| PyRunnerError::Execution("Pyodide is not loaded".into()))?;
1594 let global = scope.get_current_context().global(scope);
1595 let request_key = v8::String::new(scope, "__pyRunnerEnterRequestContext").unwrap();
1596 if let Some(request_value) = global.get(scope, request_key.into()) {
1597 if let Ok(request_fn) = Local::<Function>::try_from(request_value) {
1598 let _ = request_fn.call(scope, global.into(), &[]);
1599 }
1600 }
1601 let reset_key = v8::String::new(scope, "__aardvarkResetSharedBuffers").unwrap();
1602 if let Some(reset_value) = global.get(scope, reset_key.into()) {
1603 if let Ok(reset_fn) = Local::<Function>::try_from(reset_value) {
1604 let _ = reset_fn.call(scope, global.into(), &[]);
1605 }
1606 }
1607 let run_key = v8::String::new(scope, "runPython").unwrap();
1608 let run_value = pyodide.get(scope, run_key.into()).ok_or_else(|| {
1609 PyRunnerError::Execution("pyodide.runPython is not available".into())
1610 })?;
1611 let run_fn = Local::<Function>::try_from(run_value).map_err(|_| {
1612 PyRunnerError::Execution("pyodide.runPython is not a function".into())
1613 })?;
1614 let script_value = v8::String::new(scope, &script).ok_or_else(|| {
1615 PyRunnerError::Execution("failed to allocate execution script".into())
1616 })?;
1617 let result_value = run_fn
1618 .call(scope, pyodide.into(), &[script_value.into()])
1619 .ok_or_else(|| PyRunnerError::Execution("running entrypoint failed".into()))?;
1620 let json_str = result_value.to_rust_string_lossy(scope);
1621 let parsed: PythonCallResult = serde_json::from_str(&json_str).map_err(|err| {
1622 PyRunnerError::Execution(format!("failed to parse execution output: {err}"))
1623 })?;
1624 let mut execution: ExecutionOutput = parsed.into();
1625 let shared_buffers = collect_shared_buffers(scope, global)?;
1626 if !shared_buffers.is_empty() {
1627 let release_ids: Vec<String> = shared_buffers
1628 .iter()
1629 .map(|buffer| buffer.id.clone())
1630 .collect();
1631 release_shared_buffers(scope, global, &release_ids)?;
1632 }
1633 execution.shared_buffers = shared_buffers;
1634 Ok(execution)
1635 })
1636 }
1637
1638 pub fn run_python_snippet(&mut self, code: &str) -> Result<()> {
1640 let ctx_state = self.context_state.clone();
1641 self.with_context(|scope, _| {
1642 let pyodide = ctx_state
1643 .pyodide_local(scope)
1644 .ok_or_else(|| PyRunnerError::Execution("Pyodide is not loaded".into()))?;
1645 let run_key = v8::String::new(scope, "runPython").unwrap();
1646 let run_value = pyodide.get(scope, run_key.into()).ok_or_else(|| {
1647 PyRunnerError::Execution("pyodide.runPython is not available".into())
1648 })?;
1649 let run_fn = Local::<Function>::try_from(run_value).map_err(|_| {
1650 PyRunnerError::Execution("pyodide.runPython is not a function".into())
1651 })?;
1652 let script_value = v8::String::new(scope, code).ok_or_else(|| {
1653 PyRunnerError::Execution("failed to allocate python snippet".into())
1654 })?;
1655 run_fn
1656 .call(scope, pyodide.into(), &[script_value.into()])
1657 .ok_or_else(|| {
1658 PyRunnerError::Execution("python snippet execution failed".into())
1659 })?;
1660 Ok(())
1661 })
1662 }
1663
1664 pub fn ensure_module(&mut self, specifier: &str) -> Result<()> {
1666 let specifier = normalize_specifier(specifier);
1667 if self.context_state.modules.borrow().contains_key(&specifier) {
1668 return Ok(());
1669 }
1670 let ctx_state = self.context_state.clone();
1671 self.with_context(|scope, _context| {
1672 let module = compile_module_from_assets(scope, &ctx_state, &specifier)?;
1673 instantiate_module(scope, &ctx_state, module)?;
1674 evaluate_module(scope, &ctx_state, module, &specifier)?;
1675 Ok(())
1676 })
1677 }
1678}
1679
1680fn populate_execution_output(
1681 runtime: &mut JsRuntime,
1682 value_global: v8::Global<v8::Value>,
1683 execution: &mut ExecutionOutput,
1684) -> Result<()> {
1685 runtime.with_context(|scope, _| {
1686 let value = v8::Local::new(scope, &value_global);
1687 if value.is_null_or_undefined() {
1688 execution.result = None;
1689 execution.json = None;
1690 return Ok(());
1691 }
1692
1693 if let Some(json_value) = v8::json::stringify(scope, value) {
1694 let json_str = json_value.to_rust_string_lossy(scope);
1695 if let Ok(parsed) = serde_json::from_str(&json_str) {
1696 execution.json = Some(parsed);
1697 }
1698 }
1699
1700 if let Some(string_value) = value.to_string(scope) {
1701 execution.result = Some(string_value.to_rust_string_lossy(scope));
1702 } else {
1703 execution.result = Some("<unprintable>".to_string());
1704 }
1705
1706 Ok(())
1707 })
1708}
1709
1710fn exec_script(
1711 scope: &mut PinScope<'_, '_>,
1712 name: &str,
1713 source: &str,
1714) -> std::result::Result<(), String> {
1715 let code = v8::String::new(scope, source).ok_or_else(|| "source too large".to_owned())?;
1716 let resource_name = v8::String::new(scope, name).ok_or_else(|| "name too long".to_owned())?;
1717 let origin = v8::ScriptOrigin::new(
1718 scope,
1719 resource_name.into(),
1720 0,
1721 0,
1722 false,
1723 0,
1724 None,
1725 false,
1726 false,
1727 false,
1728 None,
1729 );
1730 v8::tc_scope!(let try_catch, scope);
1731 let script = v8::Script::compile(try_catch, code, Some(&origin))
1732 .ok_or_else(|| format!("failed to compile {name}"))?;
1733 if script.run(try_catch).is_some() {
1734 Ok(())
1735 } else {
1736 let message = try_catch
1737 .exception()
1738 .and_then(|value| value.to_string(try_catch))
1739 .map(|s| s.to_rust_string_lossy(try_catch))
1740 .unwrap_or_else(|| "script execution failed".into());
1741 Err(message)
1742 }
1743}
1744
1745fn asset_fetch_callback(
1746 scope: &mut PinScope<'_, '_>,
1747 args: FunctionCallbackArguments<'_>,
1748 mut rv: ReturnValue<'_, Value>,
1749) {
1750 let name = if args.length() > 0 {
1751 args.get(0)
1752 .to_string(scope)
1753 .map(|s| s.to_rust_string_lossy(scope))
1754 .unwrap_or_default()
1755 } else {
1756 String::new()
1757 };
1758
1759 let Some(context_state) = scope.get_slot::<Rc<RuntimeContext>>() else {
1760 rv.set(v8::undefined(scope).into());
1761 return;
1762 };
1763
1764 let Some(asset) = context_state.assets.get(&name) else {
1765 debug!("asset request for '{name}' not found");
1766 rv.set(v8::undefined(scope).into());
1767 return;
1768 };
1769
1770 let result = v8::Object::new(scope);
1771 let kind_key = v8::String::new(scope, "kind").unwrap();
1772 let data_key = v8::String::new(scope, "data").unwrap();
1773 let size_key = v8::String::new(scope, "size").unwrap();
1774
1775 match asset {
1776 Asset::Text(text) => {
1777 let kind_value = v8::String::new(scope, "text").unwrap();
1778 let data_value = v8::String::new(scope, &text).unwrap();
1779 let size_value = v8::Number::new(scope, text.len() as f64);
1780 let _ = result.set(scope, kind_key.into(), kind_value.into());
1781 let _ = result.set(scope, data_key.into(), data_value.into());
1782 let _ = result.set(scope, size_key.into(), size_value.into());
1783 }
1784 Asset::Binary(bytes) => {
1785 let kind_value = v8::String::new(scope, "binary").unwrap();
1786 let vec = bytes.as_ref().to_vec();
1787 let backing = v8::ArrayBuffer::new_backing_store_from_vec(vec);
1788 let shared = backing.make_shared();
1789 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &shared);
1790 let length = array_buffer.byte_length();
1791 let typed = Uint8Array::new(scope, array_buffer, 0, length).unwrap();
1792 let size_value = v8::Number::new(scope, length as f64);
1793 let _ = result.set(scope, kind_key.into(), kind_value.into());
1794 let _ = result.set(scope, data_key.into(), typed.into());
1795 let _ = result.set(scope, size_key.into(), size_value.into());
1796 }
1797 }
1798
1799 rv.set(result.into());
1800}
1801
1802fn record_buffer_event_callback(
1803 scope: &mut PinScope<'_, '_>,
1804 args: FunctionCallbackArguments<'_>,
1805 mut rv: ReturnValue<'_, Value>,
1806) {
1807 let event = args
1808 .get(0)
1809 .to_string(scope)
1810 .map(|s| s.to_rust_string_lossy(scope))
1811 .unwrap_or_default();
1812 if event.is_empty() {
1813 warn!(
1814 target = "aardvark::buffers",
1815 "shared buffer event missing name"
1816 );
1817 rv.set(v8::undefined(scope).into());
1818 return;
1819 }
1820
1821 let buffer_id = args
1822 .get(1)
1823 .to_string(scope)
1824 .map(|s| s.to_rust_string_lossy(scope))
1825 .unwrap_or_default();
1826 if buffer_id.is_empty() {
1827 warn!(
1828 target = "aardvark::buffers",
1829 buffers.event = event.as_str(),
1830 "shared buffer event missing id"
1831 );
1832 rv.set(v8::undefined(scope).into());
1833 return;
1834 }
1835
1836 let size = if args.length() > 2 {
1837 args.get(2).number_value(scope).unwrap_or(0.0).max(0.0)
1838 } else {
1839 0.0
1840 } as u64;
1841
1842 let mut metadata_json: Option<String> = None;
1843 if args.length() > 3 {
1844 let meta_value = args.get(3);
1845 if !meta_value.is_null_or_undefined() {
1846 if let Some(stringified) = v8::json::stringify(scope, meta_value) {
1847 metadata_json = Some(stringified.to_rust_string_lossy(scope));
1848 } else {
1849 warn!(
1850 target = "aardvark::buffers",
1851 buffers.event = event.as_str(),
1852 buffers.id = buffer_id.as_str(),
1853 "shared buffer metadata stringify failed"
1854 );
1855 }
1856 }
1857 }
1858
1859 info!(
1860 target = "aardvark::buffers",
1861 buffers.event = event.as_str(),
1862 buffers.id = buffer_id.as_str(),
1863 buffers.size = size,
1864 buffers.metadata = metadata_json.as_deref(),
1865 "shared buffer event"
1866 );
1867
1868 rv.set(v8::undefined(scope).into());
1869}
1870
1871fn filesystem_violation_callback(
1872 scope: &mut PinScope<'_, '_>,
1873 args: FunctionCallbackArguments<'_>,
1874 mut rv: ReturnValue<'_, Value>,
1875) {
1876 let message = args
1877 .get(0)
1878 .to_string(scope)
1879 .map(|s| s.to_rust_string_lossy(scope))
1880 .unwrap_or_else(|| "filesystem violation".to_string());
1881 let path_value = if args.length() > 1 {
1882 let value = args.get(1);
1883 if value.is_null_or_undefined() {
1884 None
1885 } else {
1886 value
1887 .to_string(scope)
1888 .map(|s| s.to_rust_string_lossy(scope))
1889 }
1890 } else {
1891 None
1892 };
1893
1894 if let Some(context_state) = scope.get_slot::<Rc<RuntimeContext>>() {
1895 context_state.record_filesystem_violation(path_value, message);
1896 } else {
1897 warn!(
1898 target = "aardvark::sandbox",
1899 "filesystem violation reported without runtime context"
1900 );
1901 }
1902
1903 rv.set(v8::undefined(scope).into());
1904}
1905
1906fn native_log_callback(
1907 scope: &mut PinScope<'_, '_>,
1908 args: FunctionCallbackArguments<'_>,
1909 mut rv: ReturnValue<'_, Value>,
1910) {
1911 let mut parts = Vec::with_capacity(args.length() as usize);
1912 for index in 0..args.length() {
1913 let value = args.get(index);
1914 if let Some(text) = value.to_string(scope) {
1915 parts.push(text.to_rust_string_lossy(scope));
1916 }
1917 }
1918
1919 let mut stream = ConsoleStream::Stdout;
1920 let mut start_index = 0;
1921 if let Some(first) = parts.first() {
1922 match first.as_str() {
1923 "__stderr__" => {
1924 stream = ConsoleStream::Stderr;
1925 start_index = 1;
1926 }
1927 "__stdout__" => {
1928 stream = ConsoleStream::Stdout;
1929 start_index = 1;
1930 }
1931 _ => {}
1932 }
1933 }
1934
1935 let message = if start_index >= parts.len() {
1936 String::new()
1937 } else {
1938 parts[start_index..].join(" ")
1939 };
1940
1941 if let Some(context_state) = scope.get_slot::<Rc<RuntimeContext>>() {
1942 match stream {
1943 ConsoleStream::Stdout => context_state.append_stdout(&message),
1944 ConsoleStream::Stderr => context_state.append_stderr(&message),
1945 }
1946 }
1947
1948 if !message.is_empty() {
1949 match stream {
1950 ConsoleStream::Stdout => {
1951 info!(target = "aardvark::js", "{}", message);
1952 }
1953 ConsoleStream::Stderr => {
1954 warn!(target = "aardvark::js", "{}", message);
1955 }
1956 }
1957 }
1958 rv.set(v8::undefined(scope).into());
1959}
1960
1961fn native_fetch_callback(
1962 scope: &mut PinScope<'_, '_>,
1963 args: FunctionCallbackArguments<'_>,
1964 mut rv: ReturnValue<'_, Value>,
1965) {
1966 let url = if args.length() > 0 {
1967 args.get(0)
1968 .to_string(scope)
1969 .map(|s| s.to_rust_string_lossy(scope))
1970 .unwrap_or_default()
1971 } else {
1972 String::new()
1973 };
1974
1975 if !(url.starts_with("http://") || url.starts_with("https://")) {
1976 rv.set(v8::undefined(scope).into());
1977 return;
1978 }
1979
1980 if let Some(local_path) = resolve_local_package_path(&url) {
1981 match fs::read(&local_path) {
1982 Ok(body) => {
1983 info!(
1984 target = "aardvark::js",
1985 %url,
1986 path = %local_path.display(),
1987 "serving package from local directory"
1988 );
1989 let backing = v8::ArrayBuffer::new_backing_store_from_vec(body);
1990 let byte_length = backing.len();
1991 let backing_shared = backing.make_shared();
1992 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_shared);
1993 let uint8 = Uint8Array::new(scope, array_buffer, 0, byte_length)
1994 .expect("failed to create typed array for local fetch");
1995
1996 let result = v8::Object::new(scope);
1997 let status_key = v8::String::new(scope, "status").unwrap();
1998 let status_value = v8::Integer::new(scope, 200);
1999 result.set(scope, status_key.into(), status_value.into());
2000
2001 let status_text_key = v8::String::new(scope, "statusText").unwrap();
2002 let status_text_value = v8::String::new(scope, "OK").unwrap();
2003 result.set(scope, status_text_key.into(), status_text_value.into());
2004
2005 let ok_key = v8::String::new(scope, "ok").unwrap();
2006 let ok_value = v8::Boolean::new(scope, true);
2007 result.set(scope, ok_key.into(), ok_value.into());
2008
2009 let url_key = v8::String::new(scope, "url").unwrap();
2010 let url_value = v8::String::new(scope, &url).unwrap();
2011 result.set(scope, url_key.into(), url_value.into());
2012
2013 let binary_key = v8::String::new(scope, "binary").unwrap();
2014 let binary_value = v8::Boolean::new(scope, true);
2015 result.set(scope, binary_key.into(), binary_value.into());
2016
2017 let body_key = v8::String::new(scope, "body").unwrap();
2018 result.set(scope, body_key.into(), uint8.into());
2019
2020 let headers_array = v8::Array::new(scope, 1);
2021 let header_pair = v8::Array::new(scope, 2);
2022 let name_value = v8::String::new(scope, "content-type").unwrap();
2023 let content_type = guess_content_type(&local_path);
2024 let value_value = v8::String::new(scope, content_type).unwrap();
2025 header_pair.set_index(scope, 0, name_value.into());
2026 header_pair.set_index(scope, 1, value_value.into());
2027 headers_array.set_index(scope, 0, header_pair.into());
2028 let headers_key = v8::String::new(scope, "headers").unwrap();
2029 result.set(scope, headers_key.into(), headers_array.into());
2030
2031 let content_type_key = v8::String::new(scope, "contentType").unwrap();
2032 let content_type_value = v8::String::new(scope, content_type).unwrap();
2033 result.set(scope, content_type_key.into(), content_type_value.into());
2034
2035 rv.set(result.into());
2036 return;
2037 }
2038 Err(err) => {
2039 tracing::warn!(
2040 %url,
2041 path = %local_path.display(),
2042 error = ?err,
2043 "failed to read local package asset"
2044 );
2045 }
2046 }
2047 }
2048
2049 let Some(context_state) = scope.get_slot::<Rc<RuntimeContext>>() else {
2050 rv.set(v8::undefined(scope).into());
2051 return;
2052 };
2053
2054 let parsed = match Url::parse(&url) {
2055 Ok(value) => value,
2056 Err(err) => {
2057 warn!(target = "aardvark::sandbox", %url, error = ?err, "network request rejected: invalid url");
2058 let message = v8::String::new(scope, "network access denied").unwrap();
2059 scope.throw_exception(message.into());
2060 return;
2061 }
2062 };
2063
2064 let host = match parsed.host_str() {
2065 Some(value) if !value.is_empty() => value.to_ascii_lowercase(),
2066 _ => {
2067 warn!(target = "aardvark::sandbox", %url, "network request rejected: missing host");
2068 let message = v8::String::new(scope, "network access denied").unwrap();
2069 scope.throw_exception(message.into());
2070 return;
2071 }
2072 };
2073 let port = parsed.port();
2074 let is_https = parsed.scheme().eq_ignore_ascii_case("https");
2075
2076 let decision = {
2077 let policy = context_state.network_policy.read();
2078 policy.evaluate(&host, port, is_https)
2079 };
2080
2081 if let NetworkDecision::Denied(reason) = decision {
2082 context_state.record_network_denial(&host, port, reason);
2083 let message_text = match reason {
2084 NetworkDenyReason::SchemeNotAllowed => {
2085 if let Some(p) = port {
2086 format!("network access to '{}:{}' requires https", host, p)
2087 } else {
2088 format!("network access to '{}' requires https", host)
2089 }
2090 }
2091 _ => {
2092 if let Some(p) = port {
2093 format!("network access to '{}:{}' is not permitted", host, p)
2094 } else {
2095 format!("network access to '{}' is not permitted", host)
2096 }
2097 }
2098 };
2099 warn!(
2100 target = "aardvark::sandbox",
2101 network.allowed = false,
2102 %url,
2103 host = host.as_str(),
2104 port,
2105 reason = ?reason,
2106 "network request blocked"
2107 );
2108 let message = v8::String::new(scope, &message_text).unwrap();
2109 scope.throw_exception(message.into());
2110 return;
2111 }
2112
2113 info!(
2114 target = "aardvark::sandbox",
2115 network.allowed = true,
2116 %url,
2117 host = host.as_str(),
2118 port,
2119 https = is_https,
2120 "network request allowed"
2121 );
2122
2123 context_state.record_network_contact(&host, port, is_https);
2124
2125 let response = match ureq::get(&url).call() {
2126 Ok(resp) => resp,
2127 Err(err) => {
2128 tracing::warn!(%url, error = ?err, "native fetch failed");
2129 rv.set(v8::undefined(scope).into());
2130 return;
2131 }
2132 };
2133
2134 let status = response.status();
2135 let status_text = response.status_text().to_string();
2136 let mut headers_list = Vec::new();
2137 for name in response.headers_names() {
2138 if let Some(value) = response.header(&name) {
2139 headers_list.push((name.to_ascii_lowercase(), value.to_string()));
2140 }
2141 }
2142
2143 let mut body = Vec::new();
2144 if let Err(err) = response.into_reader().read_to_end(&mut body) {
2145 tracing::warn!(%url, error = ?err, "native fetch read failed");
2146 rv.set(v8::undefined(scope).into());
2147 return;
2148 }
2149
2150 let is_binary = true;
2151 let backing = v8::ArrayBuffer::new_backing_store_from_vec(body);
2152 let byte_length = backing.len();
2153 let backing_shared = backing.make_shared();
2154 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_shared);
2155 let uint8 = Uint8Array::new(scope, array_buffer, 0, byte_length)
2156 .expect("failed to create typed array for native fetch");
2157
2158 let result = v8::Object::new(scope);
2159 let status_key = v8::String::new(scope, "status").unwrap();
2160 let status_value = v8::Integer::new(scope, status as i32);
2161 result.set(scope, status_key.into(), status_value.into());
2162
2163 let status_text_key = v8::String::new(scope, "statusText").unwrap();
2164 let status_text_value = v8::String::new(scope, &status_text).unwrap();
2165 result.set(scope, status_text_key.into(), status_text_value.into());
2166
2167 let ok_key = v8::String::new(scope, "ok").unwrap();
2168 let ok_value = v8::Boolean::new(scope, (200..300).contains(&status));
2169 result.set(scope, ok_key.into(), ok_value.into());
2170
2171 let url_key = v8::String::new(scope, "url").unwrap();
2172 let url_value = v8::String::new(scope, &url).unwrap();
2173 result.set(scope, url_key.into(), url_value.into());
2174
2175 let binary_key = v8::String::new(scope, "binary").unwrap();
2176 let binary_value = v8::Boolean::new(scope, is_binary);
2177 result.set(scope, binary_key.into(), binary_value.into());
2178
2179 let body_key = v8::String::new(scope, "body").unwrap();
2180 result.set(scope, body_key.into(), uint8.into());
2181
2182 let headers_array = v8::Array::new(scope, headers_list.len() as i32);
2183 for (index, (name, value)) in headers_list.iter().enumerate() {
2184 let pair = v8::Array::new(scope, 2);
2185 let name_value = v8::String::new(scope, name).unwrap();
2186 let value_value = v8::String::new(scope, value).unwrap();
2187 pair.set_index(scope, 0, name_value.into());
2188 pair.set_index(scope, 1, value_value.into());
2189 headers_array.set_index(scope, index as u32, pair.into());
2190 }
2191 let headers_key = v8::String::new(scope, "headers").unwrap();
2192 result.set(scope, headers_key.into(), headers_array.into());
2193
2194 if let Some((_, value)) = headers_list
2196 .iter()
2197 .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
2198 {
2199 let content_type_key = v8::String::new(scope, "contentType").unwrap();
2200 let content_type_value = v8::String::new(scope, value).unwrap();
2201 result.set(scope, content_type_key.into(), content_type_value.into());
2202 }
2203
2204 rv.set(result.into());
2205}
2206
2207fn compile_module_from_assets<'a>(
2208 scope: &mut PinScope<'a, '_>,
2209 ctx: &Rc<RuntimeContext>,
2210 specifier: &str,
2211) -> Result<Local<'a, Module>> {
2212 if let Some(existing) = ctx.modules.borrow().get(specifier) {
2213 return Ok(Local::new(scope, existing));
2214 }
2215 let asset = ctx
2216 .assets
2217 .get(specifier)
2218 .ok_or_else(|| PyRunnerError::Execution(format!("module asset not found: {specifier}")))?;
2219 let source_text = match asset {
2220 Asset::Text(text) => text,
2221 Asset::Binary(_) => {
2222 return Err(PyRunnerError::Execution(format!(
2223 "module asset '{specifier}' is binary"
2224 )))
2225 }
2226 };
2227 let source_str: &str = &source_text;
2228 let source_string = v8::String::new(scope, source_str).ok_or_else(|| {
2229 PyRunnerError::Execution(format!("failed to allocate source string for {specifier}"))
2230 })?;
2231 let resource_name = v8::String::new(scope, specifier).ok_or_else(|| {
2232 PyRunnerError::Execution(format!("failed to allocate resource name for {specifier}"))
2233 })?;
2234 let origin = v8::ScriptOrigin::new(
2235 scope,
2236 resource_name.into(),
2237 0,
2238 0,
2239 false,
2240 0,
2241 None,
2242 false,
2243 false,
2244 true,
2245 None,
2246 );
2247 let mut source = script_compiler::Source::new(source_string, Some(&origin));
2248 let module = script_compiler::compile_module(scope, &mut source)
2249 .ok_or_else(|| PyRunnerError::Execution(format!("failed to compile module {specifier}")))?;
2250 let global = v8::Global::new(scope, module);
2251 ctx.modules
2252 .borrow_mut()
2253 .insert(specifier.to_owned(), global);
2254 ctx.module_by_hash
2255 .borrow_mut()
2256 .insert(module.get_identity_hash().get(), specifier.to_owned());
2257 let requests = module.get_module_requests();
2259 let len = requests.length();
2260 for i in 0..len {
2261 if let Some(data) = requests.get(scope, i) {
2262 if let Ok(request) = Local::<ModuleRequest>::try_from(data) {
2263 let request_spec = request.get_specifier().to_rust_string_lossy(scope);
2264 let resolved = resolve_specifier(specifier, &request_spec);
2265 compile_module_from_assets(scope, ctx, &resolved)?;
2266 }
2267 }
2268 }
2269
2270 Ok(module)
2271}
2272
2273fn instantiate_module(
2274 scope: &mut PinScope<'_, '_>,
2275 ctx: &Rc<RuntimeContext>,
2276 module: Local<Module>,
2277) -> Result<()> {
2278 let scope_ptr = scope as *mut _ as *mut std::ffi::c_void;
2279 TLS_RUNTIME_CONTEXT.with(|cell| cell.set(Rc::as_ptr(ctx)));
2280 TLS_SCOPE.with(|cell| cell.set(scope_ptr));
2281 let instantiated = module.instantiate_module(scope, resolve_module_callback);
2282 TLS_SCOPE.with(|cell| cell.set(std::ptr::null_mut()));
2283 TLS_RUNTIME_CONTEXT.with(|cell| cell.set(std::ptr::null()));
2284 if instantiated.is_some() {
2285 Ok(())
2286 } else {
2287 Err(PyRunnerError::Execution(
2288 "failed to instantiate module".into(),
2289 ))
2290 }
2291}
2292
2293fn evaluate_module(
2294 scope: &mut PinScope<'_, '_>,
2295 ctx: &Rc<RuntimeContext>,
2296 module: Local<Module>,
2297 specifier: &str,
2298) -> Result<()> {
2299 module
2300 .evaluate(scope)
2301 .ok_or_else(|| PyRunnerError::Execution("module evaluation failed".into()))?;
2302 let namespace_value = module.get_module_namespace();
2303 let namespace_obj = v8::Local::<Object>::try_from(namespace_value).map_err(|_| {
2304 PyRunnerError::Execution(format!(
2305 "module namespace for {specifier} was not an object"
2306 ))
2307 })?;
2308 ctx.module_namespaces
2309 .borrow_mut()
2310 .insert(specifier.to_owned(), v8::Global::new(scope, namespace_obj));
2311 Ok(())
2312}
2313
2314fn resolve_module_callback<'a>(
2315 _context: Local<'a, Context>,
2316 specifier: Local<'a, V8String>,
2317 _import_assertions: Local<'a, FixedArray>,
2318 referencing_module: Local<'a, Module>,
2319) -> Option<Local<'a, Module>> {
2320 let scope_ptr = TLS_SCOPE.with(|cell| cell.get()) as *mut PinScope<'static, 'static>;
2321 let ctx_ptr = TLS_RUNTIME_CONTEXT.with(|cell| cell.get());
2322 if scope_ptr.is_null() || ctx_ptr.is_null() {
2323 return None;
2324 }
2325 let scope_ref = unsafe { &mut *scope_ptr };
2326 let ctx_ref = unsafe { &*ctx_ptr };
2327 let request = specifier.to_rust_string_lossy(scope_ref);
2328 let parent_hash = referencing_module.get_identity_hash().get();
2329 let base = ctx_ref
2330 .module_by_hash
2331 .borrow()
2332 .get(&parent_hash)
2333 .cloned()
2334 .unwrap_or_default();
2335 let resolved = resolve_specifier(&base, &request);
2336 let maybe_module = ctx_ref
2337 .modules
2338 .borrow()
2339 .get(&resolved)
2340 .cloned()
2341 .map(|global| Local::new(scope_ref, &global));
2342 if let Some(module) = maybe_module {
2343 Some(module)
2344 } else {
2345 if let Some(message) = v8::String::new(
2346 scope_ref,
2347 &format!("unresolved module specifier: {resolved}"),
2348 ) {
2349 scope_ref.throw_exception(message.into());
2350 }
2351 None
2352 }
2353}
2354
2355fn resolve_specifier(base: &str, request: &str) -> String {
2356 if request.starts_with("./") || request.starts_with("../") {
2357 let mut parts: Vec<&str> = if base.is_empty() {
2358 Vec::new()
2359 } else {
2360 base.rsplit_once('/')
2361 .map(|(prefix, _)| prefix.split('/').collect())
2362 .unwrap_or_default()
2363 };
2364 for segment in request.split('/') {
2365 match segment {
2366 "." | "" => {}
2367 ".." => {
2368 parts.pop();
2369 }
2370 other => parts.push(other),
2371 }
2372 }
2373 return parts.join("/");
2374 }
2375 request.trim_start_matches("./").to_owned()
2376}
2377
2378fn normalize_specifier(spec: &str) -> String {
2379 if spec.starts_with("./") {
2380 spec.trim_start_matches("./").to_owned()
2381 } else {
2382 spec.to_owned()
2383 }
2384}
2385
2386impl RuntimeContext {
2387 fn new() -> Self {
2388 Self {
2389 assets: AssetStore::new(),
2390 modules: RefCell::new(HashMap::new()),
2391 module_by_hash: RefCell::new(HashMap::new()),
2392 module_namespaces: RefCell::new(HashMap::new()),
2393 pyodide_instance: RefCell::new(None),
2394 stdout_log: RefCell::new(String::new()),
2395 stderr_log: RefCell::new(String::new()),
2396 network_policy: RwLock::new(NetworkPolicy::default()),
2397 network_contacts: RwLock::new(Vec::new()),
2398 network_denied: RwLock::new(Vec::new()),
2399 filesystem_violations: RwLock::new(Vec::new()),
2400 }
2401 }
2402
2403 fn clear_console(&self) {
2404 self.stdout_log.borrow_mut().clear();
2405 self.stderr_log.borrow_mut().clear();
2406 }
2407
2408 fn append_stdout(&self, message: &str) {
2409 if message.is_empty() {
2410 return;
2411 }
2412 let mut stdout = self.stdout_log.borrow_mut();
2413 stdout.push_str(message);
2414 stdout.push('\n');
2415 }
2416
2417 fn append_stderr(&self, message: &str) {
2418 if message.is_empty() {
2419 return;
2420 }
2421 let mut stderr = self.stderr_log.borrow_mut();
2422 stderr.push_str(message);
2423 stderr.push('\n');
2424 }
2425
2426 fn take_stdout(&self) -> String {
2427 let mut stdout = self.stdout_log.borrow_mut();
2428 std::mem::take(&mut *stdout)
2429 }
2430
2431 fn take_stderr(&self) -> String {
2432 let mut stderr = self.stderr_log.borrow_mut();
2433 std::mem::take(&mut *stderr)
2434 }
2435
2436 fn module_namespace<'a>(
2437 &self,
2438 scope: &mut PinScope<'a, '_>,
2439 specifier: &str,
2440 ) -> Option<Local<'a, Object>> {
2441 self.module_namespaces
2442 .borrow()
2443 .get(specifier)
2444 .map(|global| Local::new(scope, global))
2445 }
2446
2447 fn pyodide_local<'a>(&self, scope: &mut PinScope<'a, '_>) -> Option<Local<'a, Object>> {
2448 self.pyodide_instance
2449 .borrow()
2450 .as_ref()
2451 .map(|global| Local::new(scope, global))
2452 }
2453
2454 fn set_network_policy(&self, allow: &[String], https_only: bool) {
2455 let mut policy = self.network_policy.write();
2456 *policy = NetworkPolicy::new(allow, https_only);
2457 self.clear_network_contacts();
2458 self.clear_network_denied();
2459 }
2460
2461 fn clear_network_contacts(&self) {
2462 self.network_contacts.write().clear();
2463 }
2464
2465 fn clear_network_denied(&self) {
2466 self.network_denied.write().clear();
2467 }
2468
2469 fn record_network_contact(&self, host: &str, port: Option<u16>, https: bool) {
2470 let mut contacts = self.network_contacts.write();
2471 if !contacts
2472 .iter()
2473 .any(|entry| entry.host == host && entry.port == port && entry.https == https)
2474 {
2475 contacts.push(NetworkContactRecord {
2476 host: host.to_owned(),
2477 port,
2478 https,
2479 });
2480 }
2481 }
2482
2483 fn take_network_contacts(&self) -> Vec<NetworkContactRecord> {
2484 let mut contacts = self.network_contacts.write();
2485 std::mem::take(&mut *contacts)
2486 }
2487
2488 fn record_network_denial(&self, host: &str, port: Option<u16>, reason: NetworkDenyReason) {
2489 let reason_str = reason.as_str().to_string();
2490 let https_required = matches!(reason, NetworkDenyReason::SchemeNotAllowed);
2491 let mut denied = self.network_denied.write();
2492 if !denied.iter().any(|entry| {
2493 entry.host == host
2494 && entry.port == port
2495 && entry.reason == reason_str
2496 && entry.https_required == https_required
2497 }) {
2498 denied.push(NetworkDeniedRecord {
2499 host: host.to_owned(),
2500 port,
2501 reason: reason_str,
2502 https_required,
2503 });
2504 }
2505 }
2506
2507 fn take_network_denied(&self) -> Vec<NetworkDeniedRecord> {
2508 let mut denied = self.network_denied.write();
2509 std::mem::take(&mut *denied)
2510 }
2511
2512 fn record_filesystem_violation(&self, path: Option<String>, message: String) {
2513 let mut violations = self.filesystem_violations.write();
2514 violations.push(FilesystemViolationRecord { path, message });
2515 }
2516
2517 fn clear_filesystem_violations(&self) {
2518 self.filesystem_violations.write().clear();
2519 }
2520
2521 fn take_filesystem_violations(&self) -> Vec<FilesystemViolationRecord> {
2522 let mut violations = self.filesystem_violations.write();
2523 std::mem::take(&mut *violations)
2524 }
2525}
2526
2527#[derive(Debug, Deserialize)]
2528struct PythonCallResult {
2529 stdout: String,
2530 stderr: String,
2531 result: Option<String>,
2532 #[serde(default)]
2533 exception_type: Option<String>,
2534 #[serde(default)]
2535 exception_value: Option<String>,
2536 #[serde(default)]
2537 traceback: Option<String>,
2538}
2539
2540#[derive(Debug, Clone)]
2541pub struct SharedBuffer {
2542 pub id: String,
2543 pub length: usize,
2544 pub metadata: Option<JsonValue>,
2545 pub backing: Option<Arc<SharedBufferBacking>>,
2546 pub bytes: Option<Bytes>,
2547}
2548
2549#[derive(Debug)]
2550pub struct SharedBufferBacking {
2551 store: v8::SharedRef<v8::BackingStore>,
2552 offset: usize,
2553 length: usize,
2554}
2555
2556impl SharedBufferBacking {
2557 fn new(store: v8::SharedRef<v8::BackingStore>, offset: usize, length: usize) -> Self {
2558 Self {
2559 store,
2560 offset,
2561 length,
2562 }
2563 }
2564
2565 pub(crate) fn as_slice(&self) -> &[u8] {
2566 if self.length == 0 {
2567 return &[];
2568 }
2569 let Some(ptr) = self.store.data() else {
2570 return &[];
2571 };
2572 let store_size = self.store.byte_length();
2573 if self.offset > store_size || self.length > store_size {
2574 return &[];
2575 }
2576 if let Some(end) = self.offset.checked_add(self.length) {
2577 if end > store_size {
2578 return &[];
2579 }
2580 } else {
2581 return &[];
2582 }
2583 unsafe {
2584 let data = ptr.as_ptr().add(self.offset) as *const u8;
2585 std::slice::from_raw_parts(data, self.length)
2586 }
2587 }
2588}
2589
2590#[derive(Debug, Clone)]
2591pub struct ExecutionOutput {
2592 pub stdout: String,
2593 pub stderr: String,
2594 pub result: Option<String>,
2595 pub exception_type: Option<String>,
2596 pub exception_value: Option<String>,
2597 pub traceback: Option<String>,
2598 pub json: Option<JsonValue>,
2599 pub shared_buffers: Vec<SharedBuffer>,
2600}
2601
2602impl From<PythonCallResult> for ExecutionOutput {
2603 fn from(value: PythonCallResult) -> Self {
2604 Self {
2605 stdout: value.stdout,
2606 stderr: value.stderr,
2607 result: value.result,
2608 exception_type: value.exception_type,
2609 exception_value: value.exception_value,
2610 traceback: value.traceback,
2611 json: None,
2612 shared_buffers: Vec::new(),
2613 }
2614 }
2615}
2616
2617fn collect_shared_buffers<'a>(
2618 scope: &mut PinScope<'a, '_>,
2619 global: Local<'a, Object>,
2620) -> Result<Vec<SharedBuffer>> {
2621 let mut buffers = Vec::new();
2622 let collect_key = v8::String::new(scope, "__aardvarkCollectSharedBuffers").unwrap();
2623 let Some(collect_value) = global.get(scope, collect_key.into()) else {
2624 return Ok(buffers);
2625 };
2626 let Ok(collect_fn) = Local::<Function>::try_from(collect_value) else {
2627 return Ok(buffers);
2628 };
2629 let result = collect_fn
2630 .call(scope, global.into(), &[])
2631 .ok_or_else(|| PyRunnerError::Execution("collect shared buffers call failed".into()))?;
2632 let Ok(array) = Local::<Array>::try_from(result) else {
2633 return Ok(buffers);
2634 };
2635 let length = array.length();
2636 if length == 0 {
2637 return Ok(buffers);
2638 }
2639
2640 let id_key = v8::String::new(scope, "id").unwrap();
2641 let buffer_key = v8::String::new(scope, "buffer").unwrap();
2642 let metadata_key = v8::String::new(scope, "metadata").unwrap();
2643
2644 for index in 0..length {
2645 let entry_value = array
2646 .get_index(scope, index)
2647 .ok_or_else(|| PyRunnerError::Execution("shared buffer entry missing".into()))?;
2648 let entry_obj = entry_value
2649 .to_object(scope)
2650 .ok_or_else(|| PyRunnerError::Execution("shared buffer entry not an object".into()))?;
2651 let id_value = entry_obj
2652 .get(scope, id_key.into())
2653 .ok_or_else(|| PyRunnerError::Execution("shared buffer missing id".into()))?;
2654 let id = id_value
2655 .to_string(scope)
2656 .ok_or_else(|| PyRunnerError::Execution("failed to stringify buffer id".into()))?
2657 .to_rust_string_lossy(scope);
2658
2659 let buffer_value = entry_obj
2660 .get(scope, buffer_key.into())
2661 .ok_or_else(|| PyRunnerError::Execution("shared buffer missing payload".into()))?;
2662 let typed_array = Local::<Uint8Array>::try_from(buffer_value).map_err(|_| {
2663 PyRunnerError::Execution("shared buffer payload is not a Uint8Array".into())
2664 })?;
2665 let byte_len = typed_array.byte_length();
2666 let array_buffer = typed_array.buffer(scope).ok_or_else(|| {
2667 PyRunnerError::Execution("shared buffer missing backing store".into())
2668 })?;
2669 let backing_store = array_buffer.get_backing_store();
2670 let offset = typed_array.byte_offset();
2671
2672 let metadata = match entry_obj.get(scope, metadata_key.into()) {
2673 Some(value) if !value.is_null_or_undefined() => {
2674 let json_value = v8::json::stringify(scope, value).ok_or_else(|| {
2675 PyRunnerError::Execution("failed to stringify shared buffer metadata".into())
2676 })?;
2677 let json_str = json_value.to_rust_string_lossy(scope);
2678 Some(serde_json::from_str(&json_str).map_err(|err| {
2679 PyRunnerError::Execution(format!(
2680 "failed to parse shared buffer metadata: {err}"
2681 ))
2682 })?)
2683 }
2684 _ => None,
2685 };
2686
2687 buffers.push(SharedBuffer {
2688 id,
2689 length: byte_len,
2690 metadata,
2691 backing: Some(Arc::new(SharedBufferBacking::new(
2692 backing_store,
2693 offset,
2694 byte_len,
2695 ))),
2696 bytes: None,
2697 });
2698 }
2699
2700 Ok(buffers)
2701}
2702
2703fn release_shared_buffers<'a>(
2704 scope: &mut PinScope<'a, '_>,
2705 global: Local<'a, Object>,
2706 ids: &[String],
2707) -> Result<()> {
2708 let release_key = v8::String::new(scope, "__aardvarkReleaseSharedBuffers").unwrap();
2709 let Some(release_value) = global.get(scope, release_key.into()) else {
2710 return Ok(());
2711 };
2712 let Ok(release_fn) = Local::<Function>::try_from(release_value) else {
2713 return Ok(());
2714 };
2715
2716 let mut args: Vec<Local<Value>> = Vec::new();
2717 if !ids.is_empty() {
2718 let id_array = Array::new(scope, ids.len() as i32);
2719 for (index, id) in ids.iter().enumerate() {
2720 let id_value = v8::String::new(scope, id).ok_or_else(|| {
2721 PyRunnerError::Execution("failed to allocate buffer id string".into())
2722 })?;
2723 id_array.set_index(scope, index as u32, id_value.into());
2724 }
2725 args.push(id_array.into());
2726 }
2727
2728 release_fn
2729 .call(scope, global.into(), &args)
2730 .ok_or_else(|| PyRunnerError::Execution("release shared buffers call failed".into()))?;
2731 Ok(())
2732}
2733
2734#[cfg(test)]
2735mod tests {
2736 use super::*;
2737 use std::ffi::OsString;
2738 use std::fs;
2739 use tempfile::tempdir;
2740
2741 struct EnvGuard {
2742 original: Option<OsString>,
2743 }
2744
2745 impl EnvGuard {
2746 fn clear() -> Self {
2747 let original = env::var_os("AARDVARK_PYODIDE_PACKAGE_DIR");
2748 env::remove_var("AARDVARK_PYODIDE_PACKAGE_DIR");
2749 Self { original }
2750 }
2751 }
2752
2753 impl Drop for EnvGuard {
2754 fn drop(&mut self) {
2755 match self.original.take() {
2756 Some(value) => env::set_var("AARDVARK_PYODIDE_PACKAGE_DIR", value),
2757 None => env::remove_var("AARDVARK_PYODIDE_PACKAGE_DIR"),
2758 }
2759 }
2760 }
2761
2762 #[test]
2763 fn package_root_override_takes_precedence() {
2764 reset_package_root_for_tests();
2765 let temp = tempdir().expect("create tempdir");
2766 let cache_dir = temp.path().join("cache");
2767 fs::create_dir(&cache_dir).expect("create cache dir");
2768
2769 let guard = EnvGuard::clear();
2770 set_package_root_override(Some(cache_dir.clone()));
2771 let resolved = package_root_dir().expect("package root available");
2772 assert_eq!(resolved, cache_dir);
2773
2774 drop(guard);
2775 reset_package_root_for_tests();
2776 }
2777
2778 #[test]
2779 fn package_root_falls_back_to_env() {
2780 reset_package_root_for_tests();
2781 let temp = tempdir().expect("create tempdir");
2782 let cache_dir = temp.path().join("env-cache");
2783 fs::create_dir(&cache_dir).expect("create env cache dir");
2784
2785 std::env::set_var("AARDVARK_PYODIDE_PACKAGE_DIR", &cache_dir);
2786 let resolved = package_root_dir().expect("package root from env");
2787 assert_eq!(resolved, cache_dir);
2788
2789 std::env::remove_var("AARDVARK_PYODIDE_PACKAGE_DIR");
2790 reset_package_root_for_tests();
2791 }
2792}