use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use crate::{Error, Extension}; use v8::MapFnTo; type Exports = Vec<(String, v8::Global)>; struct Modules { sources: HashMap<&'static str, &'static str>, builtins: HashMap, modules: RefCell>>, // Identity hashes collide, so each bucket holds candidates confirmed by Local == Global. by_identity: RefCell>>, origin: v8::Global, } fn valid_name(name: &str) -> bool { name.starts_with("ext:") && name != "ext:core" && name.len() > 4 && name[4..] .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') } #[derive(Clone, Copy, PartialEq)] enum Mark { Unvisited, Visiting, Done, } fn append_extension_in_dependency_order( index: usize, extensions: &[Extension], indices: &HashMap<&'static str, usize>, marks: &mut [Mark], stack: &mut Vec, order: &mut Vec, ) -> Result<(), Error> { match marks[index] { Mark::Done => return Ok(()), Mark::Visiting => { let start = stack.iter().position(|item| *item == index).unwrap(); let mut cycle = stack[start..] .iter() .map(|item| extensions[*item].name) .collect::>(); cycle.push(extensions[index].name); return Err(Error::Module(format!( "Extension dependency cycle: {}", cycle.join(" -> ") ))); } Mark::Unvisited => {} } marks[index] = Mark::Visiting; stack.push(index); for dependency in extensions[index].dependencies { if !valid_name(dependency) { return Err(Error::Module(format!( "Invalid extension dependency: {dependency}" ))); } let Some(dependency) = indices.get(dependency) else { return Err(Error::Module(format!( "Extension {} depends on missing extension {dependency}", extensions[index].name ))); }; append_extension_in_dependency_order( *dependency, extensions, indices, marks, stack, order, )?; } stack.pop(); marks[index] = Mark::Done; order.push(index); Ok(()) } pub(crate) fn validate(extensions: &[Extension]) -> Result, Error> { let mut names = std::collections::HashSet::new(); let mut sources = std::collections::HashSet::new(); for extension in extensions { let name = extension.name; if !valid_name(name) { return Err(Error::Module(format!("Invalid extension name: {name}"))); } if !names.insert(name) { return Err(Error::Module(format!("Duplicate extension: {name}"))); } for &(specifier, _) in extension.modules { if (specifier != name && !specifier.starts_with(&format!("{name}/"))) || normalize(specifier).as_deref() != Some(specifier) { return Err(Error::Module(format!( "Invalid extension module: {specifier}" ))); } if !sources.insert(specifier) { return Err(Error::Module(format!( "Duplicate extension module: {specifier}" ))); } } if let Some(entry) = extension.entry_point && !extension.modules.iter().any(|(name, _)| *name == entry) { return Err(Error::Module(format!( "Missing extension entry point: {entry}" ))); } } let indices = extensions .iter() .enumerate() .map(|(index, extension)| (extension.name, index)) .collect::>(); let mut marks = vec![Mark::Unvisited; extensions.len()]; let mut stack = Vec::new(); let mut order = Vec::with_capacity(extensions.len()); for index in 0..extensions.len() { append_extension_in_dependency_order( index, extensions, &indices, &mut marks, &mut stack, &mut order, )?; } Ok(order) } pub(crate) fn clear(isolate: &mut v8::Isolate) { isolate.remove_slot::>(); } pub(crate) const SOURCES: &[(&str, &str)] = &[ ("ext:core/promises", include_str!("promises.js")), ("ext:core/bootstrap", include_str!("bootstrap.js")), ( "ext:core/shared_array_buffer", include_str!("shared_array_buffer.js"), ), ("ext:core/stack_trace.js", include_str!("stack_trace.js")), ]; type NativeModule = (&'static str, Vec<(&'static str, v8::FunctionCallback)>); pub(crate) fn native_modules() -> Vec { vec![ ( "ext:core/tasks", vec![("enqueueTask", crate::tasks::enqueue.map_fn_to())], ), ( "ext:core/microtasks", vec![("enqueueMicrotask", crate::microtasks::enqueue.map_fn_to())], ), ( "ext:core/promise_hooks", vec![ ( "markPromiseHandled", crate::op::mark_promise_handled.map_fn_to(), ), ( "performPromiseThen", crate::op::perform_promise_then.map_fn_to(), ), ("promiseState", crate::op::promise_state.map_fn_to()), ], ), ( "ext:core/event_hooks", vec![ ( "setEventHandlers", crate::reporting::set_event_handlers.map_fn_to(), ), ("isPromise", crate::reporting::is_promise.map_fn_to()), ( "reportNonfatalException", crate::reporting::report_nonfatal_exception.map_fn_to(), ), ], ), ( "ext:core/structured_clone", vec![("clone", crate::structured_clone::clone.map_fn_to())], ), ( "ext:core/source_map", vec![( "mapSourceLocation", crate::modules::map_source_location_op.map_fn_to(), )], ), ] } pub(crate) fn external_references() -> Vec { let evaluate: v8::SyntheticModuleEvaluationSteps = evaluate_builtin.map_fn_to(); let mut references = vec![v8::ExternalReference { pointer: evaluate as *mut std::ffi::c_void, }]; for (_, exports) in native_modules() { references.extend( exports .into_iter() .map(|(_, function)| v8::ExternalReference { function }), ); } references } pub(crate) fn init( scope: &mut v8::PinScope, sources: HashMap<&'static str, &'static str>, primordials: v8::Local, ops: v8::Local, ) -> Option<()> { let exports = |scope: &mut v8::PinScope, object: v8::Local| -> Option { let keys = object.get_own_property_names(scope, Default::default())?; let mut exports = Vec::new(); for index in 0..keys.length() { let key = keys.get_index(scope, index)?; let value = object.get(scope, key)?; exports.push(( key.to_rust_string_lossy(scope), v8::Global::new(scope, value), )); } Some(exports) }; let mut builtins = HashMap::new(); builtins.insert("ext:core/primordials".into(), exports(scope, primordials)?); builtins.insert("ext:core/ops".into(), exports(scope, ops)?); builtins.insert( "ext:core".into(), vec![ ( "primordials".into(), v8::Global::new(scope, v8::Local::::from(primordials)), ), ( "ops".into(), v8::Global::new(scope, v8::Local::::from(ops)), ), ], ); for (module, functions) in native_modules() { let mut exports = Vec::new(); for (name, callback) in functions { let function = v8::Function::builder_raw(callback) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; exports.push(( name.into(), v8::Global::new(scope, v8::Local::::from(function)), )); } builtins.insert(module.into(), exports); } let origin = v8::PrimitiveArray::new(scope, 1); let token = v8::Symbol::new(scope, None); origin.set(scope, 0, token.into()); let origin = v8::Global::new(scope, origin); scope.set_slot(Rc::new(Modules { sources, builtins, modules: RefCell::new(HashMap::new()), by_identity: RefCell::new(HashMap::new()), origin, })); Some(()) } #[derive(Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct SnapshotState { origin: usize, builtins: std::collections::BTreeMap>, modules: std::collections::BTreeMap, } /// Records handles in name order: context data indices are part of the artifact bytes. pub(crate) fn capture(scope: &mut v8::PinScope) -> SnapshotState { let map = scope.get_slot::>().unwrap().clone(); let context = scope.get_current_context(); let marker = v8::Local::new(scope, &map.origin).get(scope, 0); let origin = scope.add_context_data(context, marker); let mut builtins = std::collections::BTreeMap::new(); for (name, exports) in map .builtins .iter() .collect::>() { let exports = exports .iter() .map(|(key, value)| { let value = v8::Local::new(scope, value); (key.clone(), scope.add_context_data(context, value)) }) .collect(); builtins.insert(name.clone(), exports); } let modules = map .modules .borrow() .iter() .map(|(name, module)| (name.clone(), module.clone())) .collect::>() .into_iter() .map(|(name, module)| { let module = v8::Local::new(scope, module); (name, scope.add_context_data(context, module)) }) .collect(); SnapshotState { origin, builtins, modules } } pub(crate) fn restore( scope: &mut v8::PinScope, sources: HashMap<&'static str, &'static str>, state: &SnapshotState, ) -> Result<(), crate::SnapshotError> { let marker = scope.get_context_data_from_snapshot_once::(state.origin)?; let origin = v8::PrimitiveArray::new(scope, 1); origin.set(scope, 0, marker.into()); let origin = v8::Global::new(scope, origin); let mut builtins = HashMap::new(); for (name, exports) in &state.builtins { let mut values = Vec::new(); for (key, index) in exports { let value = scope.get_context_data_from_snapshot_once::(*index)?; values.push((key.clone(), v8::Global::new(scope, value))); } builtins.insert(name.clone(), values); } let mut modules = HashMap::new(); let mut by_identity: HashMap> = HashMap::new(); for (name, &index) in &state.modules { let module = scope.get_context_data_from_snapshot_once::(index)?; // Identity hashes belong to the restored heap, not the snapshot-producing isolate. by_identity .entry(module.get_identity_hash().get()) .or_default() .push(name.clone()); modules.insert(name.clone(), v8::Global::new(scope, module)); } scope.set_slot(Rc::new(Modules { sources, builtins, modules: RefCell::new(modules), by_identity: RefCell::new(by_identity), origin, })); Ok(()) } pub(crate) fn dynamic_imports_disabled( scope: &mut v8::PinScope, origin: v8::Local, ) -> bool { let Some(map) = scope.get_slot::>().cloned() else { return false; }; let Ok(options) = v8::Local::::try_from(origin) else { return false; }; if options.length() != 1 { return false; } // Snapshot restoration can copy the options array; the private symbol retains identity. let marker = v8::Local::new(scope, &map.origin).get(scope, 0); options .get(scope, 0) .and_then(|token| v8::Local::::try_from(token).ok()) .is_some_and(|token| token.strict_equals(marker.into())) } pub(crate) fn module_name( scope: &mut v8::PinScope, module: v8::Local, ) -> Option { let map = scope.get_slot::>()?; let hash = module.get_identity_hash().get(); let by_identity = map.by_identity.borrow(); let modules = map.modules.borrow(); for name in by_identity.get(&hash).into_iter().flatten() { if let Some(global) = modules.get(name) && v8::Local::new(scope, global) == module { return Some(name.clone()); } } None } fn normalize(name: &str) -> Option { let (root, path) = name.split_once('/').unwrap_or((name, "")); if !root.starts_with("ext:") || root.len() == 4 || name.contains(['?', '#', '\\', '%']) { return None; } let mut components = Vec::new(); if !path.is_empty() { for part in path.split('/') { match part { "" => return None, "." => {} ".." => { components.pop()?; } part => components.push(part), } } } if components.is_empty() { Some(root.into()) } else { Some(format!("{root}/{}", components.join("/"))) } } fn fail(scope: &mut v8::PinScope, message: impl AsRef) -> Option { let message = v8::String::new(scope, message.as_ref())?; let error = v8::Exception::error(scope, message); scope.throw_exception(error); None } fn load<'s>(scope: &mut v8::PinScope<'s, '_>, name: &str) -> Option> { let map = scope.get_slot::>().unwrap().clone(); { let modules = map.modules.borrow(); if let Some(cached) = modules.get(name) { return Some(v8::Local::new(scope, cached)); } } let module = if let Some(exports) = map.builtins.get(name) { let name = v8::String::new(scope, name)?; let keys = exports .iter() .map(|(name, _)| v8::String::new(scope, name)) .collect::>>()?; v8::Module::create_synthetic_module(scope, name, &keys, evaluate_builtin) } else { let Some(code) = map.sources.get(name) else { return fail(scope, format!("Unknown extension module: {name}")); }; let origin = v8::Local::new(scope, &map.origin); let mut source = crate::source(scope, name, code, true, Some(origin.into())); v8::script_compiler::compile_module(scope, &mut source)? }; map.modules .borrow_mut() .insert(name.to_owned(), v8::Global::new(scope, module)); let hash = module.get_identity_hash().get(); let mut by_identity = map.by_identity.borrow_mut(); let bucket = by_identity.entry(hash).or_default(); if !bucket.iter().any(|candidate| candidate == name) { bucket.push(name.to_owned()); } Some(module) } fn evaluate_builtin<'s>( context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>, ) -> Option> { // SAFETY: V8 supplies this module's entered context. v8::callback_scope!(unsafe scope, context); let name = module_name(scope, module)?; let map = scope.get_slot::>().unwrap().clone(); for (key, value) in &map.builtins[&*name] { let key = v8::String::new(scope, key)?; let value = v8::Local::new(scope, value); module.set_synthetic_module_export(scope, key, value)?; } let resolver = v8::PromiseResolver::new(scope)?; resolver.resolve(scope, v8::undefined(scope).into())?; Some(resolver.get_promise(scope).into()) } fn resolve<'s>( context: v8::Local<'s, v8::Context>, specifier: v8::Local<'s, v8::String>, attributes: v8::Local<'s, v8::FixedArray>, referrer: v8::Local<'s, v8::Module>, ) -> Option> { // Trust module identity, never the origin string supplied by a guest loader. // SAFETY: V8 invokes module resolution with the supplied context entered. v8::callback_scope!(unsafe scope, context); let Some(referrer) = module_name(scope, referrer) else { return fail(scope, "Not an extension module"); }; if attributes.length() != 0 { return fail(scope, "not yet implemented: extension import attributes"); } let specifier = specifier.to_rust_string_lossy(scope); let name = resolve_name(scope, &specifier, &referrer)?; load(scope, &name) } pub(crate) fn resolve_name( scope: &mut v8::PinScope, specifier: &str, referrer: &str, ) -> Option { let candidate = if specifier.starts_with("ext:") { specifier.to_owned() } else if specifier.starts_with("./") || specifier.starts_with("../") { let Some((parent, _)) = referrer.rsplit_once('/') else { return fail(scope, "Relative import from an extension package root"); }; format!("{parent}/{specifier}") } else { return fail( scope, "Extensions can only import embedded extension modules", ); }; let Some(name) = normalize(&candidate) else { return fail(scope, format!("Invalid extension import: {candidate}")); }; Some(name) } pub(crate) fn execute<'s>( scope: &mut v8::PinScope<'s, '_>, entry: &str, ) -> Option> { let module = load(scope, entry)?; if module.get_status() == v8::ModuleStatus::Uninstantiated && module.instantiate_module(scope, resolve) != Some(true) { return None; } let evaluation = module.evaluate(scope)?; let promise = v8::Local::::try_from(evaluation).ok()?; promise.mark_as_handled(); crate::op::run_event_loop(scope, Some(promise), false)?; Some(module) }