mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
refactor(renderer): unify realm intrinsic interface state
Publish realm-owned interface objects only after finalization succeeds and route shared binding helpers through the canonical registry. Keep ECMAScript intrinsics separate and check native realm exposure when deserializing CryptoKey objects.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use super::*;
|
||||
use crate::util::{
|
||||
callback_data_index_value, get_private_value, global_constructor_object,
|
||||
serialize_v8_iter_array, set_private_value,
|
||||
callback_data_index_value, get_private_value, serialize_v8_iter_array, set_private_value,
|
||||
};
|
||||
use crate::web_api_interfaces;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
@@ -295,7 +294,13 @@ pub(crate) fn crypto_key_object_from_clone_payload<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
payload: CryptoKeyClonePayload,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
global_constructor_object(scope, "CryptoKey")?;
|
||||
// Like Blink's ExecutionContextExposesInterface(kCryptoKeyTag), check
|
||||
// native exposure before resolving the intrinsic identity. Public globals
|
||||
// can be deleted or replaced independently of secure-context policy.
|
||||
if !super::super::exposed_interfaces::is_realm_interface_exposed(scope, "CryptoKey") {
|
||||
return None;
|
||||
}
|
||||
super::super::ensure_intrinsic_interface_constructor(scope, "CryptoKey").ok()?;
|
||||
if !clone_payload_algorithm_is_supported(&payload.key_type, &payload.algorithm) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -100,10 +100,7 @@ pub(super) fn document_create_event_callback<'s>(
|
||||
// belongs to the callee realm even when its receiver is from another one.
|
||||
let exposed = {
|
||||
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
|
||||
super::exposed_interfaces::is_window_interface_exposed(
|
||||
target_scope,
|
||||
kind.constructor_name(),
|
||||
)
|
||||
super::exposed_interfaces::is_realm_interface_exposed(target_scope, kind.constructor_name())
|
||||
};
|
||||
if !exposed {
|
||||
throw_not_supported_dom_exception(
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
|
||||
use super::materialize::{
|
||||
ensure_intrinsic_interface_constructor, ensure_intrinsic_interface_prototype,
|
||||
};
|
||||
use crate::context_bootstrap::{
|
||||
crypto::finalize_crypto_realm_bindings, events::finalize_pointer_event_realm_bindings,
|
||||
notification_runtime::finalize_notification_realm_bindings,
|
||||
@@ -73,20 +70,22 @@ fn realm_dependent_finalizer(interface_name: &str) -> Option<RealmDependentFinal
|
||||
.find_map(|(name, finalizer)| (*name == interface_name).then_some(*finalizer))
|
||||
}
|
||||
|
||||
/// Completes bindings which still require a concrete realm-local prototype.
|
||||
/// Completes bindings on unpublished realm-local objects. Taking the objects
|
||||
/// directly avoids reentrant lookups of the interface being initialized.
|
||||
///
|
||||
/// The dispatch is deliberately interface-local: it must never scan public
|
||||
/// global constructor properties, because reading one of those properties is
|
||||
/// itself the lazy-materialization trigger. Context-independent declarations
|
||||
/// should continue moving to the reusable FunctionTemplate installer.
|
||||
pub(super) fn finalize_materialized_interface(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pub(super) fn finalize_materialized_interface<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
interface_name: &str,
|
||||
constructor: v8::Local<'s, v8::Function>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
) -> Result<()> {
|
||||
let Some(finalizer) = realm_dependent_finalizer(interface_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let prototype = ensure_intrinsic_interface_prototype(scope, interface_name)?;
|
||||
match finalizer {
|
||||
RealmDependentFinalizer::NodeMixinUnscopables => {
|
||||
finalize_node_mixin_unscopables(scope, prototype)
|
||||
@@ -101,14 +100,12 @@ pub(super) fn finalize_materialized_interface(
|
||||
finalize_xml_http_request_event_target_realm_bindings(scope, prototype)
|
||||
}
|
||||
RealmDependentFinalizer::NotificationPermission => {
|
||||
let constructor = ensure_intrinsic_interface_constructor(scope, "Notification")?;
|
||||
finalize_notification_realm_bindings(scope, constructor.into())
|
||||
}
|
||||
RealmDependentFinalizer::PointerEventSecureContextSurface => {
|
||||
finalize_pointer_event_realm_bindings(scope, prototype)
|
||||
}
|
||||
RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes => {
|
||||
let constructor = ensure_intrinsic_interface_constructor(scope, "PerformanceObserver")?;
|
||||
finalize_performance_observer_realm_bindings(scope, constructor.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use moli_v8_util::install_web_api_intrinsic_resolver;
|
||||
|
||||
use super::materialize::exposed_interface_lazy_getter;
|
||||
use super::materialize::{exposed_interface_lazy_getter, resolve_web_api_intrinsic};
|
||||
#[cfg(test)]
|
||||
use super::metadata::STORAGE_INTERFACE_NAMES;
|
||||
use super::metadata::{GlobalInstallation, RealmKind, TemplateBuildProfile};
|
||||
use super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceState};
|
||||
use super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceEntry};
|
||||
use super::template_registry::ExposedInterfaceTemplateRegistry;
|
||||
use crate::context_bootstrap::specs::ConstructorSpec;
|
||||
use crate::util::{
|
||||
constructor_object, constructor_prototype_object, get_private_value,
|
||||
initialize_intrinsic_interface_registry, register_intrinsic_interface,
|
||||
registered_intrinsic_constructor, registered_intrinsic_prototype, v8str,
|
||||
constructor_object, constructor_prototype_object, initialize_ecmascript_intrinsic_registry,
|
||||
register_ecmascript_intrinsic, registered_ecmascript_constructor,
|
||||
registered_ecmascript_prototype, v8str,
|
||||
};
|
||||
|
||||
const LEGACY_WINDOW_INTERFACE_ALIASES: &[(&str, &str)] = &[
|
||||
@@ -155,24 +156,32 @@ pub(crate) fn filter_window_exposed_interfaces(
|
||||
|
||||
/// Exposure is determined by the realm's native metadata, independently of
|
||||
/// author changes to the corresponding global property.
|
||||
pub(in crate::context_bootstrap) fn is_window_interface_exposed(
|
||||
pub(in crate::context_bootstrap) fn is_realm_interface_exposed(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
name: &str,
|
||||
) -> bool {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let secure = get_private_value(
|
||||
scope,
|
||||
global,
|
||||
crate::context_bootstrap::runtime_state::WINDOW_SECURE_CONTEXT_AVAILABLE_SLOT,
|
||||
)
|
||||
.is_some_and(|value| value.boolean_value(scope));
|
||||
let Some(registry) = ExposedInterfaceTemplateRegistry::current(scope) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(realm) = IntrinsicInterfaceRegistry::for_current_context(scope, registry.len()) else {
|
||||
return false;
|
||||
};
|
||||
let realm_kind = realm.realm_kind();
|
||||
let secure = match realm_kind {
|
||||
RealmKind::Window => {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
crate::context_bootstrap::runtime_state::window_realm_secure_context_available(
|
||||
scope, global,
|
||||
)
|
||||
}
|
||||
RealmKind::DedicatedWorker | RealmKind::SharedWorker | RealmKind::ServiceWorker => {
|
||||
crate::worker::worker_realm_secure_context_available(scope)
|
||||
}
|
||||
};
|
||||
registry
|
||||
.id_by_name(name)
|
||||
.and_then(|id| registry.metadata(id))
|
||||
.is_some_and(|metadata| metadata.is_exposed(RealmKind::Window, secure))
|
||||
.is_some_and(|metadata| metadata.is_exposed(realm_kind, secure))
|
||||
}
|
||||
|
||||
pub(crate) fn initialize_realm_interface_registry(
|
||||
@@ -194,7 +203,7 @@ pub(crate) fn capture_eager_intrinsic_interfaces<'s>(
|
||||
) -> Result<()> {
|
||||
let registry = ExposedInterfaceTemplateRegistry::current(scope)
|
||||
.ok_or_else(|| anyhow!("exposed interface template registry is unavailable"))?;
|
||||
initialize_intrinsic_interface_registry(scope, global);
|
||||
initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
capture_ecmascript_intrinsic(scope, global, "Error")?;
|
||||
let realm = IntrinsicInterfaceRegistry::initialize_for_current_context(
|
||||
scope,
|
||||
@@ -203,34 +212,21 @@ pub(crate) fn capture_eager_intrinsic_interfaces<'s>(
|
||||
)?;
|
||||
|
||||
for metadata in registry.metadata_entries() {
|
||||
let captured_constructor = registered_intrinsic_constructor(scope, global, metadata.name);
|
||||
let captured_prototype = registered_intrinsic_prototype(scope, global, metadata.name);
|
||||
if captured_constructor.is_some() != captured_prototype.is_some() {
|
||||
realm.set_state(metadata.id, RealmInterfaceState::Failed)?;
|
||||
return Err(anyhow!(
|
||||
"eager intrinsic `{}` has partial constructor/prototype state",
|
||||
metadata.name
|
||||
));
|
||||
}
|
||||
if let Some(constructor) = captured_constructor {
|
||||
let prototype = captured_prototype.expect("captured intrinsic pair was validated");
|
||||
let public_interface = realm
|
||||
.public_interface(scope, metadata.id)
|
||||
.unwrap_or(constructor);
|
||||
realm.register_intrinsic_objects(
|
||||
scope,
|
||||
global,
|
||||
metadata.id,
|
||||
metadata.name,
|
||||
constructor,
|
||||
prototype,
|
||||
public_interface,
|
||||
)?;
|
||||
realm.set_state(metadata.id, RealmInterfaceState::Ready)?;
|
||||
continue;
|
||||
match &*realm
|
||||
.entry(metadata.id)
|
||||
.ok_or_else(|| anyhow!("interface id is out of range"))?
|
||||
{
|
||||
RealmInterfaceEntry::Ready(_) => continue,
|
||||
RealmInterfaceEntry::Failed | RealmInterfaceEntry::Materializing => {
|
||||
return Err(anyhow!(
|
||||
"eager intrinsic `{}` is not ready for capture",
|
||||
metadata.name
|
||||
));
|
||||
}
|
||||
RealmInterfaceEntry::Uninitialized => {}
|
||||
}
|
||||
// Reading a public lazy property here would defeat lazy
|
||||
// materialization. Lazy callbacks register their own intrinsic pair.
|
||||
// materialization. Lazy callbacks publish their completed realm entry.
|
||||
//
|
||||
// Worker bootstrap still constructs interfaces outside the shared
|
||||
// registry in a few cohorts. Those entries have metadata, but no
|
||||
@@ -248,17 +244,9 @@ pub(crate) fn capture_eager_intrinsic_interfaces<'s>(
|
||||
let Some(prototype) = constructor_prototype_object(scope, constructor) else {
|
||||
continue;
|
||||
};
|
||||
realm.register_intrinsic_objects(
|
||||
scope,
|
||||
global,
|
||||
metadata.id,
|
||||
metadata.name,
|
||||
constructor,
|
||||
prototype,
|
||||
constructor,
|
||||
)?;
|
||||
realm.set_state(metadata.id, RealmInterfaceState::Ready)?;
|
||||
realm.publish_ready(scope, metadata.id, constructor, prototype, constructor)?;
|
||||
}
|
||||
install_web_api_intrinsic_resolver(scope, resolve_web_api_intrinsic);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -268,8 +256,8 @@ pub(super) fn capture_ecmascript_intrinsic<'s>(
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
match (
|
||||
registered_intrinsic_constructor(scope, global, name),
|
||||
registered_intrinsic_prototype(scope, global, name),
|
||||
registered_ecmascript_constructor(scope, global, name),
|
||||
registered_ecmascript_prototype(scope, global, name),
|
||||
) {
|
||||
(Some(_), Some(_)) => return Ok(()),
|
||||
(None, None) => {}
|
||||
@@ -283,7 +271,7 @@ pub(super) fn capture_ecmascript_intrinsic<'s>(
|
||||
.ok_or_else(|| anyhow!("missing ECMAScript intrinsic constructor `{name}`"))?;
|
||||
let prototype = constructor_prototype_object(scope, constructor)
|
||||
.ok_or_else(|| anyhow!("ECMAScript intrinsic `{name}` has no object prototype"))?;
|
||||
if !register_intrinsic_interface(scope, global, name, constructor, prototype) {
|
||||
if !register_ecmascript_intrinsic(scope, global, name, constructor, prototype) {
|
||||
return Err(anyhow!("failed to capture ECMAScript intrinsic `{name}`"));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use moli_v8_util::{WebApiIntrinsicKind, WebApiIntrinsicLookup};
|
||||
|
||||
use super::finalize::finalize_materialized_interface;
|
||||
use super::metadata::{InterfaceId, ResolvedPrototypeProperty};
|
||||
use super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceState};
|
||||
use super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceEntry};
|
||||
use super::template_registry::ExposedInterfaceTemplateRegistry;
|
||||
use crate::context_bootstrap::constructors::html_element_constructor_with_early_sanity_trap;
|
||||
use crate::context_bootstrap::runtime_state::set_interface_prototype_constructor;
|
||||
use crate::context_bootstrap::shared::throw_error;
|
||||
use crate::context_bootstrap::specs::ConstructorKind;
|
||||
use crate::util::{
|
||||
constructor_prototype_object, registered_intrinsic_constructor, registered_intrinsic_prototype,
|
||||
v8str,
|
||||
};
|
||||
use crate::util::{constructor_prototype_object, v8str};
|
||||
|
||||
/// Adapter for shared declaration helpers. Recognized Web APIs always resolve
|
||||
/// through the realm entry, even when a public binding was deleted or replaced.
|
||||
pub(super) fn resolve_web_api_intrinsic<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
name: &str,
|
||||
kind: WebApiIntrinsicKind,
|
||||
) -> WebApiIntrinsicLookup<'s> {
|
||||
let Some(registry) = ExposedInterfaceTemplateRegistry::current(scope) else {
|
||||
return WebApiIntrinsicLookup::Unmanaged;
|
||||
};
|
||||
if registry.id_by_name(name).is_none() {
|
||||
return WebApiIntrinsicLookup::Unmanaged;
|
||||
}
|
||||
WebApiIntrinsicLookup::Managed(match kind {
|
||||
WebApiIntrinsicKind::Constructor => ensure_intrinsic_interface_constructor(scope, name)
|
||||
.ok()
|
||||
.map(Into::into),
|
||||
WebApiIntrinsicKind::Prototype => ensure_intrinsic_interface_prototype(scope, name).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn exposed_interface_lazy_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
@@ -56,11 +75,11 @@ pub(super) fn materialize_interface<'s>(
|
||||
.ok_or_else(|| anyhow!("unknown exposed interface id {}", id.index()))?;
|
||||
let realm = IntrinsicInterfaceRegistry::for_current_context(scope, registry.len())?;
|
||||
|
||||
match realm
|
||||
.state(id)
|
||||
match &*realm
|
||||
.entry(id)
|
||||
.ok_or_else(|| anyhow!("interface state is out of range"))?
|
||||
{
|
||||
RealmInterfaceState::Ready => {
|
||||
RealmInterfaceEntry::Ready(_) => {
|
||||
return realm
|
||||
.public_interface(scope, id)
|
||||
.map(Into::into)
|
||||
@@ -71,34 +90,23 @@ pub(super) fn materialize_interface<'s>(
|
||||
)
|
||||
});
|
||||
}
|
||||
RealmInterfaceState::Materializing => {
|
||||
RealmInterfaceEntry::Materializing => {
|
||||
return Err(anyhow!("materialization cycle reached `{}`", metadata.name));
|
||||
}
|
||||
RealmInterfaceState::Finalizing => {
|
||||
return realm
|
||||
.public_interface(scope, id)
|
||||
.map(Into::into)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"finalizing realm-owned public interface object `{}` is missing",
|
||||
metadata.name
|
||||
)
|
||||
});
|
||||
}
|
||||
RealmInterfaceState::Failed => {
|
||||
RealmInterfaceEntry::Failed => {
|
||||
return Err(anyhow!(
|
||||
"a previous materialization of `{}` failed",
|
||||
metadata.name
|
||||
));
|
||||
}
|
||||
RealmInterfaceState::Uninitialized => {}
|
||||
RealmInterfaceEntry::Uninitialized => {}
|
||||
}
|
||||
|
||||
realm.set_state(id, RealmInterfaceState::Materializing)?;
|
||||
realm.begin_materialization(id)?;
|
||||
match materialize_uninitialized_interface(scope, ®istry, &realm, id) {
|
||||
Ok(interface) => Ok(interface),
|
||||
Err(error) => {
|
||||
realm.set_state(id, RealmInterfaceState::Failed)?;
|
||||
realm.fail(id)?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -186,46 +194,6 @@ fn materialize_uninitialized_interface<'s>(
|
||||
let metadata = registry
|
||||
.metadata(id)
|
||||
.ok_or_else(|| anyhow!("unknown exposed interface id {}", id.index()))?;
|
||||
let context = scope.get_current_context();
|
||||
let global = context.global(scope);
|
||||
match (
|
||||
registered_intrinsic_constructor(scope, global, metadata.name),
|
||||
registered_intrinsic_prototype(scope, global, metadata.name),
|
||||
) {
|
||||
(Some(constructor), Some(prototype)) => {
|
||||
// A complete native-only pair can outlive an interrupted eager
|
||||
// capture. Reuse its identities without consulting author bindings.
|
||||
if constructor.get_creation_context(scope) != Some(context)
|
||||
|| prototype.get_creation_context(scope) != Some(context)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"registered intrinsic interface `{}` belongs to another realm",
|
||||
metadata.name
|
||||
));
|
||||
}
|
||||
let constructor = v8::Local::<v8::Function>::try_from(constructor).map_err(|_| {
|
||||
anyhow!(
|
||||
"intrinsic constructor `{}` is not a Function",
|
||||
metadata.name
|
||||
)
|
||||
})?;
|
||||
return finish_materialized_interface(
|
||||
scope,
|
||||
registry,
|
||||
realm,
|
||||
id,
|
||||
constructor,
|
||||
prototype,
|
||||
);
|
||||
}
|
||||
(None, None) => {}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"uninitialized interface `{}` has partial registry state",
|
||||
metadata.name
|
||||
));
|
||||
}
|
||||
}
|
||||
let parent = metadata
|
||||
.parent
|
||||
.map(|parent_id| intrinsic_parent(scope, registry, parent_id))
|
||||
@@ -322,19 +290,16 @@ fn finish_materialized_interface<'s>(
|
||||
}
|
||||
_ => constructor.into(),
|
||||
};
|
||||
let global = scope.get_current_context().global(scope);
|
||||
realm.register_intrinsic_objects(
|
||||
// Like Blink's ConstructorForTypeSlowCase, finish every fallible binding
|
||||
// installation before publishing the interface in the per-context cache.
|
||||
finalize_materialized_interface(scope, metadata.name, constructor, constructor_prototype)?;
|
||||
realm.publish_ready(
|
||||
scope,
|
||||
global,
|
||||
id,
|
||||
metadata.name,
|
||||
constructor.into(),
|
||||
constructor_prototype,
|
||||
public_interface,
|
||||
)?;
|
||||
realm.set_state(id, RealmInterfaceState::Finalizing)?;
|
||||
finalize_materialized_interface(scope, metadata.name)?;
|
||||
realm.set_state(id, RealmInterfaceState::Ready)?;
|
||||
registry.record_materialization(id);
|
||||
Ok(public_interface.into())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) use install::capture_eager_intrinsic_interfaces;
|
||||
pub(super) use install::{
|
||||
filter_window_exposed_interfaces, initialize_realm_interface_registry,
|
||||
install_interface_template_metadata, install_window_exposed_interfaces,
|
||||
install_worker_exposed_interfaces, is_lazy_exposed_interface, is_window_interface_exposed,
|
||||
install_worker_exposed_interfaces, is_lazy_exposed_interface, is_realm_interface_exposed,
|
||||
prepare_worker_event_target_template,
|
||||
};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
|
||||
use super::metadata::{InterfaceId, RealmKind};
|
||||
use crate::util::{
|
||||
register_intrinsic_interface, registered_intrinsic_constructor, registered_intrinsic_prototype,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum RealmInterfaceState {
|
||||
/// Only a completely initialized interface retains intrinsic handles and is
|
||||
/// visible to callers. State and identities share one realm-owned entry.
|
||||
pub(super) enum RealmInterfaceEntry {
|
||||
Uninitialized,
|
||||
Materializing,
|
||||
Finalizing,
|
||||
Ready,
|
||||
Ready(RealmInterfaceObjects),
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub(super) struct IntrinsicInterfaceRegistry {
|
||||
realm_kind: RealmKind,
|
||||
states: RefCell<Vec<RealmInterfaceState>>,
|
||||
objects: RefCell<Vec<Option<RealmInterfaceObjects>>>,
|
||||
entries: RefCell<Vec<RealmInterfaceEntry>>,
|
||||
}
|
||||
|
||||
struct RealmInterfaceObjects {
|
||||
pub(super) struct RealmInterfaceObjects {
|
||||
constructor: v8::Global<v8::Object>,
|
||||
prototype: v8::Global<v8::Object>,
|
||||
public_interface: v8::Global<v8::Object>,
|
||||
@@ -33,8 +29,11 @@ impl IntrinsicInterfaceRegistry {
|
||||
fn new(interface_count: usize, realm_kind: RealmKind) -> Self {
|
||||
Self {
|
||||
realm_kind,
|
||||
states: RefCell::new(vec![RealmInterfaceState::Uninitialized; interface_count]),
|
||||
objects: RefCell::new((0..interface_count).map(|_| None).collect()),
|
||||
entries: RefCell::new(
|
||||
(0..interface_count)
|
||||
.map(|_| RealmInterfaceEntry::Uninitialized)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +44,7 @@ impl IntrinsicInterfaceRegistry {
|
||||
) -> Result<Rc<Self>> {
|
||||
let context = scope.get_current_context();
|
||||
if let Some(registry) = context.get_slot::<Self>() {
|
||||
if registry.states.borrow().len() != interface_count {
|
||||
return Err(anyhow!(
|
||||
"realm interface registry size changed from {} to {interface_count}",
|
||||
registry.states.borrow().len()
|
||||
));
|
||||
}
|
||||
registry.validate_size(interface_count)?;
|
||||
if registry.realm_kind != realm_kind {
|
||||
return Err(anyhow!(
|
||||
"realm interface registry kind changed from {:?} to {realm_kind:?}",
|
||||
@@ -60,18 +54,8 @@ impl IntrinsicInterfaceRegistry {
|
||||
return Ok(registry);
|
||||
}
|
||||
let registry = Rc::new(Self::new(interface_count, realm_kind));
|
||||
if let Some(previous) = context.set_slot(registry.clone()) {
|
||||
if previous.states.borrow().len() != interface_count
|
||||
|| previous.realm_kind != realm_kind
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"realm interface registry was concurrently initialized with incompatible metadata"
|
||||
));
|
||||
}
|
||||
Ok(previous)
|
||||
} else {
|
||||
Ok(registry)
|
||||
}
|
||||
context.set_slot(registry.clone());
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
pub(super) fn for_current_context(
|
||||
@@ -82,127 +66,96 @@ impl IntrinsicInterfaceRegistry {
|
||||
.get_current_context()
|
||||
.get_slot::<Self>()
|
||||
.ok_or_else(|| anyhow!("realm interface registry is not initialized"))?;
|
||||
if registry.states.borrow().len() != interface_count {
|
||||
return Err(anyhow!(
|
||||
"realm interface registry size changed from {} to {interface_count}",
|
||||
registry.states.borrow().len()
|
||||
));
|
||||
}
|
||||
registry.validate_size(interface_count)?;
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
pub(super) fn state(&self, id: InterfaceId) -> Option<RealmInterfaceState> {
|
||||
self.states.borrow().get(id.index()).copied()
|
||||
}
|
||||
|
||||
pub(super) fn set_state(&self, id: InterfaceId, state: RealmInterfaceState) -> Result<()> {
|
||||
let interface_count = self.states.borrow().len();
|
||||
let mut states = self.states.borrow_mut();
|
||||
let slot = states.get_mut(id.index()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"interface state id {} is out of range for {interface_count} entries",
|
||||
id.index()
|
||||
)
|
||||
})?;
|
||||
*slot = state;
|
||||
if state == RealmInterfaceState::Failed {
|
||||
self.objects.borrow_mut()[id.index()] = None;
|
||||
fn validate_size(&self, interface_count: usize) -> Result<()> {
|
||||
let current_count = self.entries.borrow().len();
|
||||
if current_count != interface_count {
|
||||
return Err(anyhow!(
|
||||
"realm interface registry size changed from {current_count} to {interface_count}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publishes the same intrinsic identities to both registries. Mark the
|
||||
/// interface in progress before either store changes, and make any failed
|
||||
/// publication terminal even if V8 has retained an immutable partial pair.
|
||||
/// Successful publication stays Materializing: eager capture then marks it
|
||||
/// Ready, while lazy materialization exposes it only when Finalizing starts.
|
||||
pub(super) fn register_intrinsic_objects<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
global: v8::Local<'s, v8::Object>,
|
||||
id: InterfaceId,
|
||||
name: &str,
|
||||
constructor: v8::Local<'s, v8::Object>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
public_interface: v8::Local<'s, v8::Object>,
|
||||
) -> Result<()> {
|
||||
if self.state(id) == Some(RealmInterfaceState::Failed) {
|
||||
return Err(anyhow!("a previous materialization of `{name}` failed"));
|
||||
}
|
||||
self.set_state(id, RealmInterfaceState::Materializing)?;
|
||||
let result = (|| {
|
||||
// Retain the Rust objects before writing the immutable JS entries.
|
||||
// Keep them hidden until the caller starts finalization or
|
||||
// completes eager capture.
|
||||
self.register_objects(scope, id, constructor, prototype, public_interface)?;
|
||||
match (
|
||||
registered_intrinsic_constructor(scope, global, name),
|
||||
registered_intrinsic_prototype(scope, global, name),
|
||||
) {
|
||||
(Some(existing_constructor), Some(existing_prototype)) => {
|
||||
if !existing_constructor.strict_equals(constructor.into())
|
||||
|| !existing_prototype.strict_equals(prototype.into())
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"intrinsic interface `{name}` was replaced after registration"
|
||||
));
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
if !register_intrinsic_interface(scope, global, name, constructor, prototype) {
|
||||
return Err(anyhow!("failed to register intrinsic interface `{name}`"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"intrinsic interface `{name}` has partial registry state"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
if result.is_err() {
|
||||
self.set_state(id, RealmInterfaceState::Failed)?;
|
||||
}
|
||||
result
|
||||
pub(super) const fn realm_kind(&self) -> RealmKind {
|
||||
self.realm_kind
|
||||
}
|
||||
|
||||
fn register_objects<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
id: InterfaceId,
|
||||
constructor: v8::Local<'s, v8::Object>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
public_interface: v8::Local<'s, v8::Object>,
|
||||
) -> Result<()> {
|
||||
let interface_count = self.objects.borrow().len();
|
||||
{
|
||||
let objects = self.objects.borrow();
|
||||
if let Some(existing) = objects.get(id.index()).and_then(Option::as_ref) {
|
||||
let same_constructor =
|
||||
v8::Local::new(scope, &existing.constructor).strict_equals(constructor.into());
|
||||
let same_prototype =
|
||||
v8::Local::new(scope, &existing.prototype).strict_equals(prototype.into());
|
||||
let same_public_interface = v8::Local::new(scope, &existing.public_interface)
|
||||
.strict_equals(public_interface.into());
|
||||
if same_constructor && same_prototype && same_public_interface {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"interface objects for id {} were replaced after registration",
|
||||
id.index()
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut objects = self.objects.borrow_mut();
|
||||
let slot = objects.get_mut(id.index()).ok_or_else(|| {
|
||||
pub(super) fn entry(&self, id: InterfaceId) -> Option<Ref<'_, RealmInterfaceEntry>> {
|
||||
Ref::filter_map(self.entries.borrow(), |entries| entries.get(id.index())).ok()
|
||||
}
|
||||
|
||||
pub(super) fn begin_materialization(&self, id: InterfaceId) -> Result<()> {
|
||||
self.set_pending_entry(id, RealmInterfaceEntry::Materializing)
|
||||
}
|
||||
|
||||
pub(super) fn fail(&self, id: InterfaceId) -> Result<()> {
|
||||
self.set_pending_entry(id, RealmInterfaceEntry::Failed)
|
||||
}
|
||||
|
||||
fn set_pending_entry(&self, id: InterfaceId, entry: RealmInterfaceEntry) -> Result<()> {
|
||||
let mut entries = self.entries.borrow_mut();
|
||||
let interface_count = entries.len();
|
||||
let slot = entries.get_mut(id.index()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"interface object id {} is out of range for {interface_count} entries",
|
||||
"interface id {} is out of range for {interface_count} entries",
|
||||
id.index()
|
||||
)
|
||||
})?;
|
||||
debug_assert!(slot.is_none());
|
||||
*slot = Some(RealmInterfaceObjects {
|
||||
if !matches!(
|
||||
(&*slot, &entry),
|
||||
(
|
||||
RealmInterfaceEntry::Uninitialized,
|
||||
RealmInterfaceEntry::Materializing
|
||||
) | (
|
||||
RealmInterfaceEntry::Materializing,
|
||||
RealmInterfaceEntry::Failed
|
||||
)
|
||||
) {
|
||||
return Err(anyhow!(
|
||||
"invalid materialization transition for interface id {}",
|
||||
id.index()
|
||||
));
|
||||
}
|
||||
*slot = entry;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The only publication point, shared by completed lazy materialization
|
||||
/// and eager capture after bootstrap. Keep the prototype handle because
|
||||
/// legacy factories share prototypes and author code can mutate properties.
|
||||
pub(super) fn publish_ready<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
id: InterfaceId,
|
||||
constructor: v8::Local<'s, v8::Object>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
public_interface: v8::Local<'s, v8::Object>,
|
||||
) -> Result<()> {
|
||||
let mut entries = self.entries.borrow_mut();
|
||||
let interface_count = entries.len();
|
||||
let slot = entries.get_mut(id.index()).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"interface id {} is out of range for {interface_count} entries",
|
||||
id.index()
|
||||
)
|
||||
})?;
|
||||
match slot {
|
||||
RealmInterfaceEntry::Ready(_) => {
|
||||
return Err(anyhow!("interface id {} is already published", id.index()));
|
||||
}
|
||||
RealmInterfaceEntry::Failed => {
|
||||
return Err(anyhow!(
|
||||
"a previous materialization of interface id {} failed",
|
||||
id.index()
|
||||
));
|
||||
}
|
||||
RealmInterfaceEntry::Uninitialized | RealmInterfaceEntry::Materializing => {}
|
||||
}
|
||||
*slot = RealmInterfaceEntry::Ready(RealmInterfaceObjects {
|
||||
constructor: v8::Global::new(scope, constructor),
|
||||
prototype: v8::Global::new(scope, prototype),
|
||||
public_interface: v8::Global::new(scope, public_interface),
|
||||
@@ -215,15 +168,11 @@ impl IntrinsicInterfaceRegistry {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
id: InterfaceId,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if !matches!(
|
||||
self.state(id)?,
|
||||
RealmInterfaceState::Ready | RealmInterfaceState::Finalizing
|
||||
) {
|
||||
let entries = self.entries.borrow();
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
}
|
||||
let objects = self.objects.borrow();
|
||||
let object = objects.get(id.index())?.as_ref()?;
|
||||
Some(v8::Local::new(scope, &object.constructor))
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.constructor))
|
||||
}
|
||||
|
||||
pub(super) fn prototype<'s>(
|
||||
@@ -231,15 +180,11 @@ impl IntrinsicInterfaceRegistry {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
id: InterfaceId,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if !matches!(
|
||||
self.state(id)?,
|
||||
RealmInterfaceState::Ready | RealmInterfaceState::Finalizing
|
||||
) {
|
||||
let entries = self.entries.borrow();
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
}
|
||||
let objects = self.objects.borrow();
|
||||
let object = objects.get(id.index())?.as_ref()?;
|
||||
Some(v8::Local::new(scope, &object.prototype))
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.prototype))
|
||||
}
|
||||
|
||||
pub(super) fn public_interface<'s>(
|
||||
@@ -247,15 +192,11 @@ impl IntrinsicInterfaceRegistry {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
id: InterfaceId,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if !matches!(
|
||||
self.state(id)?,
|
||||
RealmInterfaceState::Ready | RealmInterfaceState::Finalizing
|
||||
) {
|
||||
let entries = self.entries.borrow();
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
}
|
||||
let objects = self.objects.borrow();
|
||||
let object = objects.get(id.index())?.as_ref()?;
|
||||
Some(v8::Local::new(scope, &object.public_interface))
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.public_interface))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,30 +207,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_state_updates_a_registered_interface() {
|
||||
let registry = IntrinsicInterfaceRegistry::new(1, RealmKind::Window);
|
||||
let id = InterfaceId::from_callback_data(0);
|
||||
|
||||
registry
|
||||
.set_state(id, RealmInterfaceState::Ready)
|
||||
.expect("registered interface state should update");
|
||||
|
||||
assert_eq!(registry.state(id), Some(RealmInterfaceState::Ready));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_state_rejects_an_out_of_range_interface() {
|
||||
fn materialization_rejects_an_out_of_range_interface() {
|
||||
let registry = IntrinsicInterfaceRegistry::new(1, RealmKind::Window);
|
||||
let error = registry
|
||||
.set_state(
|
||||
InterfaceId::from_callback_data(1),
|
||||
RealmInterfaceState::Ready,
|
||||
)
|
||||
.expect_err("out-of-range interface state must fail");
|
||||
.begin_materialization(InterfaceId::from_callback_data(1))
|
||||
.expect_err("out-of-range interface must fail");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"interface state id 1 is out of range for 1 entries"
|
||||
"interface id 1 is out of range for 1 entries"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -308,11 +234,8 @@ mod tests {
|
||||
let public_interface = v8::Object::new(scope);
|
||||
|
||||
registry
|
||||
.register_objects(scope, id, constructor, prototype, public_interface)
|
||||
.expect("realm interface objects should register");
|
||||
registry
|
||||
.set_state(id, RealmInterfaceState::Ready)
|
||||
.expect("realm interface objects should become ready");
|
||||
.publish_ready(scope, id, constructor, prototype, public_interface)
|
||||
.expect("completed realm interface should publish");
|
||||
context.detach_global();
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -300,7 +300,7 @@ fn entered_context_template_build_is_isolate_cached_and_realm_neutral() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninitialized_dom_string_map_adopts_its_registered_intrinsics() {
|
||||
fn intrinsic_dom_string_map_materializes_without_reading_the_public_binding() {
|
||||
crate::ensure_v8_for_test();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
@@ -330,13 +330,10 @@ fn uninitialized_dom_string_map_adopts_its_registered_intrinsics() {
|
||||
let prototype = crate::util::constructor_prototype_object(scope, constructor.into())
|
||||
.expect("dataset prototype");
|
||||
let global = context.global(scope);
|
||||
assert!(crate::util::register_intrinsic_interface(
|
||||
moli_v8_util::install_web_api_intrinsic_resolver(
|
||||
scope,
|
||||
global,
|
||||
"DOMStringMap",
|
||||
constructor.into(),
|
||||
prototype,
|
||||
));
|
||||
super::materialize::resolve_web_api_intrinsic,
|
||||
);
|
||||
assert_eq!(
|
||||
global.set_lazy_data_property(
|
||||
scope,
|
||||
@@ -347,17 +344,17 @@ fn uninitialized_dom_string_map_adopts_its_registered_intrinsics() {
|
||||
);
|
||||
reset_lazy_getter_calls();
|
||||
|
||||
let recovered = super::ensure_intrinsic_interface_prototype(scope, "DOMStringMap")
|
||||
.expect("a complete trusted registration must recover the dataset prototype");
|
||||
let intrinsic = crate::util::global_constructor_prototype(scope, "DOMStringMap")
|
||||
.expect("the shared helper must materialize the trusted dataset prototype");
|
||||
|
||||
assert!(recovered.strict_equals(prototype.into()));
|
||||
assert_eq!(
|
||||
realm.state(id),
|
||||
Some(super::realm_registry::RealmInterfaceState::Ready)
|
||||
);
|
||||
assert!(intrinsic.strict_equals(prototype.into()));
|
||||
assert!(matches!(
|
||||
&*realm.entry(id).unwrap(),
|
||||
super::realm_registry::RealmInterfaceEntry::Ready(_)
|
||||
));
|
||||
assert!(
|
||||
super::ensure_intrinsic_interface_constructor(scope, "DOMStringMap")
|
||||
.expect("recovered dataset constructor")
|
||||
.expect("trusted dataset constructor")
|
||||
.strict_equals(constructor.into())
|
||||
);
|
||||
assert!(
|
||||
@@ -369,10 +366,45 @@ fn uninitialized_dom_string_map_adopts_its_registered_intrinsics() {
|
||||
assert_eq!(
|
||||
lazy_getter_calls(),
|
||||
0,
|
||||
"recovery must not read the public binding"
|
||||
"materialization must not read the public binding"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_cycle_keeps_intrinsic_objects_unpublished() {
|
||||
crate::ensure_v8_for_test();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let registry = super::template_registry::ExposedInterfaceTemplateRegistry::install(
|
||||
scope,
|
||||
crate::context_bootstrap::specs::constructor_specs(),
|
||||
super::metadata::TemplateBuildProfile::Window,
|
||||
)
|
||||
.expect("template registry");
|
||||
let realm = super::realm_registry::IntrinsicInterfaceRegistry::initialize_for_current_context(
|
||||
scope,
|
||||
registry.len(),
|
||||
super::RealmKind::Window,
|
||||
)
|
||||
.expect("realm registry");
|
||||
let id = registry.id_by_name("Crypto").expect("Crypto interface");
|
||||
realm
|
||||
.begin_materialization(id)
|
||||
.expect("start materialization");
|
||||
|
||||
let error = super::materialize::materialize_interface(scope, id)
|
||||
.expect_err("a reentrant lookup must report a cycle");
|
||||
|
||||
assert!(error.to_string().contains("materialization cycle"));
|
||||
assert!(realm.constructor(scope, id).is_none());
|
||||
assert!(realm.prototype(scope, id).is_none());
|
||||
assert!(realm.public_interface(scope, id).is_none());
|
||||
assert_eq!(registry.materialization_count(id), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecmascript_intrinsic_capture_rejects_partial_registration_on_retry() {
|
||||
crate::ensure_v8_for_test();
|
||||
@@ -382,8 +414,8 @@ fn ecmascript_intrinsic_capture_rejects_partial_registration_on_retry() {
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(scope);
|
||||
crate::util::initialize_intrinsic_interface_registry(scope, global);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliIntrinsicPrototypes")
|
||||
crate::util::initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliEcmascriptPrototypes")
|
||||
.expect("private prototype registry");
|
||||
assert_eq!(
|
||||
prototypes.set_integrity_level(scope, v8::IntegrityLevel::Frozen),
|
||||
@@ -392,8 +424,8 @@ fn ecmascript_intrinsic_capture_rejects_partial_registration_on_retry() {
|
||||
|
||||
super::install::capture_ecmascript_intrinsic(scope, global, "Error")
|
||||
.expect_err("the first capture must report its failed prototype publication");
|
||||
assert!(crate::util::registered_intrinsic_constructor(scope, global, "Error").is_some());
|
||||
assert!(crate::util::registered_intrinsic_prototype(scope, global, "Error").is_none());
|
||||
assert!(crate::util::registered_ecmascript_constructor(scope, global, "Error").is_some());
|
||||
assert!(crate::util::registered_ecmascript_prototype(scope, global, "Error").is_none());
|
||||
let error = super::install::capture_ecmascript_intrinsic(scope, global, "Error")
|
||||
.expect_err("retrying a partial Error pair must not report success");
|
||||
assert!(error.to_string().contains("partial registry state"));
|
||||
@@ -408,8 +440,8 @@ fn ecmascript_intrinsic_capture_rejects_prototype_without_constructor() {
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(scope);
|
||||
crate::util::initialize_intrinsic_interface_registry(scope, global);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliIntrinsicPrototypes")
|
||||
crate::util::initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliEcmascriptPrototypes")
|
||||
.expect("private prototype registry");
|
||||
let constructor = crate::util::constructor_object(scope, global, "Error")
|
||||
.expect("ECMAScript Error constructor");
|
||||
@@ -424,11 +456,18 @@ fn ecmascript_intrinsic_capture_rejects_prototype_without_constructor() {
|
||||
.expect_err("a prototype-only Error registration must not be completed from the global");
|
||||
|
||||
assert!(error.to_string().contains("partial registry state"));
|
||||
assert!(crate::util::registered_intrinsic_constructor(scope, global, "Error").is_none());
|
||||
assert!(crate::util::registered_ecmascript_constructor(scope, global, "Error").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_intrinsic_registration_marks_materialization_failed() {
|
||||
fn repeated_eager_capture_preserves_intrinsics_and_lazy_public_proxies() {
|
||||
fn eager_constructor<'s>(
|
||||
_scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
}
|
||||
|
||||
crate::ensure_v8_for_test();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
@@ -441,116 +480,46 @@ fn incomplete_intrinsic_registration_marks_materialization_failed() {
|
||||
super::metadata::TemplateBuildProfile::Window,
|
||||
)
|
||||
.expect("template registry");
|
||||
let realm = super::realm_registry::IntrinsicInterfaceRegistry::initialize_for_current_context(
|
||||
scope,
|
||||
registry.len(),
|
||||
super::RealmKind::Window,
|
||||
)
|
||||
.expect("realm registry");
|
||||
let id = registry
|
||||
.id_by_name("DOMStringMap")
|
||||
.expect("dataset interface");
|
||||
let global = context.global(scope);
|
||||
crate::util::initialize_intrinsic_interface_registry(scope, global);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliIntrinsicPrototypes")
|
||||
.expect("private prototype registry");
|
||||
assert_eq!(
|
||||
prototypes.set_integrity_level(scope, v8::IntegrityLevel::Frozen),
|
||||
Some(true)
|
||||
);
|
||||
let constructor = v8::Object::new(scope);
|
||||
let prototype = v8::Object::new(scope);
|
||||
assert!(!crate::util::register_intrinsic_interface(
|
||||
scope,
|
||||
global,
|
||||
"DOMStringMap",
|
||||
constructor,
|
||||
prototype,
|
||||
));
|
||||
assert!(crate::util::registered_intrinsic_constructor(scope, global, "DOMStringMap").is_some());
|
||||
assert!(crate::util::registered_intrinsic_prototype(scope, global, "DOMStringMap").is_none());
|
||||
|
||||
let error = super::ensure_intrinsic_interface_prototype(scope, "DOMStringMap")
|
||||
.expect_err("an incomplete pair must not supply a dataset prototype");
|
||||
|
||||
assert!(error.to_string().contains("partial registry state"));
|
||||
assert_eq!(
|
||||
realm.state(id),
|
||||
Some(super::realm_registry::RealmInterfaceState::Failed)
|
||||
);
|
||||
let repeated_error = super::ensure_intrinsic_interface_prototype(scope, "DOMStringMap")
|
||||
.expect_err("a failed materialization must not retry the partial registration");
|
||||
assert!(
|
||||
repeated_error
|
||||
.to_string()
|
||||
.contains("previous materialization")
|
||||
);
|
||||
assert_eq!(registry.build_count(id), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_eager_intrinsic_capture_does_not_leave_an_uninitialized_interface() {
|
||||
crate::ensure_v8_for_test();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let registry = super::template_registry::ExposedInterfaceTemplateRegistry::install(
|
||||
scope,
|
||||
crate::context_bootstrap::specs::constructor_specs(),
|
||||
super::metadata::TemplateBuildProfile::Window,
|
||||
)
|
||||
.expect("template registry");
|
||||
let realm = super::realm_registry::IntrinsicInterfaceRegistry::initialize_for_current_context(
|
||||
scope,
|
||||
registry.len(),
|
||||
super::RealmKind::Window,
|
||||
)
|
||||
.expect("realm registry");
|
||||
let global = context.global(scope);
|
||||
let error_constructor = crate::util::constructor_object(scope, global, "Error")
|
||||
.expect("ECMAScript Error constructor");
|
||||
let error_prototype = crate::util::constructor_prototype_object(scope, error_constructor)
|
||||
.expect("ECMAScript Error prototype");
|
||||
assert!(crate::util::register_intrinsic_interface(
|
||||
scope,
|
||||
global,
|
||||
"Error",
|
||||
error_constructor,
|
||||
error_prototype,
|
||||
));
|
||||
let id = registry
|
||||
.id_by_name("Window")
|
||||
.expect("eager Window interface");
|
||||
let constructor = v8::Object::new(scope);
|
||||
let prototype = v8::Object::new(scope);
|
||||
assert_eq!(
|
||||
constructor.set(scope, v8str(scope, "prototype").into(), prototype.into()),
|
||||
Some(true)
|
||||
);
|
||||
let constructor = v8::Function::new(scope, eager_constructor).expect("eager constructor");
|
||||
let prototype = crate::util::constructor_prototype_object(scope, constructor.into())
|
||||
.expect("eager prototype");
|
||||
assert_eq!(
|
||||
global.set(scope, v8str(scope, "Window").into(), constructor.into()),
|
||||
Some(true)
|
||||
);
|
||||
let prototypes = crate::util::get_private_object(scope, global, "__moliIntrinsicPrototypes")
|
||||
.expect("private prototype registry");
|
||||
super::capture_eager_intrinsic_interfaces(scope, global, super::RealmKind::Window)
|
||||
.expect("first eager capture");
|
||||
let id = registry
|
||||
.id_by_name("HTMLDivElement")
|
||||
.expect("HTML interface");
|
||||
let public = super::materialize::materialize_interface(scope, id)
|
||||
.expect("HTML interface materialization");
|
||||
assert!(public.is_proxy());
|
||||
assert_eq!(
|
||||
prototypes.set_integrity_level(scope, v8::IntegrityLevel::Frozen),
|
||||
global.set(scope, v8str(scope, "Window").into(), v8::null(scope).into()),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
super::capture_eager_intrinsic_interfaces(scope, global, super::RealmKind::Window)
|
||||
.expect_err("a failed private registration must abort eager capture");
|
||||
.expect("repeat eager capture must preserve existing entries");
|
||||
|
||||
assert_eq!(
|
||||
realm.state(id),
|
||||
Some(super::realm_registry::RealmInterfaceState::Failed)
|
||||
assert!(
|
||||
crate::util::global_constructor_object(scope, "Window")
|
||||
.unwrap()
|
||||
.strict_equals(constructor.into())
|
||||
);
|
||||
assert!(realm.constructor(scope, id).is_none());
|
||||
assert!(realm.prototype(scope, id).is_none());
|
||||
assert!(realm.public_interface(scope, id).is_none());
|
||||
assert!(super::ensure_intrinsic_interface_prototype(scope, "Window").is_err());
|
||||
assert!(
|
||||
crate::util::global_constructor_prototype(scope, "Window")
|
||||
.unwrap()
|
||||
.strict_equals(prototype.into())
|
||||
);
|
||||
assert!(
|
||||
super::materialize::materialize_interface(scope, id)
|
||||
.unwrap()
|
||||
.strict_equals(public)
|
||||
);
|
||||
assert_eq!(registry.materialization_count(id), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -582,13 +551,10 @@ fn failed_intrinsic_finalization_does_not_expose_registered_objects() {
|
||||
let prototype = crate::util::constructor_prototype_object(scope, constructor.into())
|
||||
.expect("Crypto prototype");
|
||||
let global = context.global(scope);
|
||||
assert!(crate::util::register_intrinsic_interface(
|
||||
moli_v8_util::install_web_api_intrinsic_resolver(
|
||||
scope,
|
||||
global,
|
||||
"Crypto",
|
||||
constructor.into(),
|
||||
prototype,
|
||||
));
|
||||
super::materialize::resolve_web_api_intrinsic,
|
||||
);
|
||||
let subtle_crypto_available = v8::Boolean::new(scope, true);
|
||||
crate::util::set_private_value(
|
||||
scope,
|
||||
@@ -604,10 +570,10 @@ fn failed_intrinsic_finalization_does_not_expose_registered_objects() {
|
||||
super::ensure_intrinsic_interface_prototype(scope, "Crypto")
|
||||
.expect_err("a frozen prototype must reject secure-context finalization");
|
||||
|
||||
assert_eq!(
|
||||
realm.state(id),
|
||||
Some(super::realm_registry::RealmInterfaceState::Failed)
|
||||
);
|
||||
assert!(matches!(
|
||||
&*realm.entry(id).unwrap(),
|
||||
super::realm_registry::RealmInterfaceEntry::Failed
|
||||
));
|
||||
assert!(realm.constructor(scope, id).is_none());
|
||||
assert!(realm.prototype(scope, id).is_none());
|
||||
assert!(realm.public_interface(scope, id).is_none());
|
||||
@@ -615,14 +581,10 @@ fn failed_intrinsic_finalization_does_not_expose_registered_objects() {
|
||||
assert!(super::ensure_intrinsic_interface_prototype(scope, "Crypto").is_err());
|
||||
assert!(super::materialized_intrinsic_interface_prototype(scope, "Crypto").is_none());
|
||||
assert_eq!(registry.materialization_count(id), 0);
|
||||
assert!(
|
||||
crate::util::registered_intrinsic_constructor(scope, global, "Crypto")
|
||||
.is_some_and(|value| value.strict_equals(constructor.into()))
|
||||
);
|
||||
assert!(
|
||||
crate::util::registered_intrinsic_prototype(scope, global, "Crypto")
|
||||
.is_some_and(|value| value.strict_equals(prototype.into()))
|
||||
);
|
||||
assert!(crate::util::global_constructor_object(scope, "Crypto").is_none());
|
||||
assert!(crate::util::global_constructor_prototype(scope, "Crypto").is_none());
|
||||
assert!(crate::util::registered_ecmascript_constructor(scope, global, "Crypto").is_none());
|
||||
assert!(crate::util::registered_ecmascript_prototype(scope, global, "Crypto").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,15 +6,15 @@ use super::super::materialize::{
|
||||
materialize_interface,
|
||||
};
|
||||
use super::super::metadata::{InterfaceId, RealmKind, TemplateBuildProfile};
|
||||
use super::super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceState};
|
||||
use super::super::realm_registry::{IntrinsicInterfaceRegistry, RealmInterfaceEntry};
|
||||
use super::super::template_registry::ExposedInterfaceTemplateRegistry;
|
||||
use super::{counting_lazy_getter, lazy_getter_calls, reset_lazy_getter_calls};
|
||||
use crate::util::{
|
||||
constructor_prototype_object, get_private_value, register_intrinsic_interface,
|
||||
registered_intrinsic_constructor, registered_intrinsic_prototype, v8str,
|
||||
constructor_prototype_object, get_private_value, registered_ecmascript_constructor,
|
||||
registered_ecmascript_prototype, v8str,
|
||||
};
|
||||
|
||||
struct RegisteredInterface<'s> {
|
||||
struct UnpublishedInterface<'s> {
|
||||
registry: Rc<ExposedInterfaceTemplateRegistry>,
|
||||
realm: Rc<IntrinsicInterfaceRegistry>,
|
||||
id: InterfaceId,
|
||||
@@ -23,12 +23,15 @@ struct RegisteredInterface<'s> {
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
impl RegisteredInterface<'_> {
|
||||
impl UnpublishedInterface<'_> {
|
||||
fn assert_failed(&self, scope: &mut v8::PinScope<'_, '_>, cause: &str) {
|
||||
let error = materialize_interface(scope, self.id)
|
||||
.expect_err("failed recovery must return an error without panicking");
|
||||
.expect_err("failed materialization must return an error without panicking");
|
||||
assert!(format!("{error:#}").contains(cause), "{error:#}");
|
||||
assert_eq!(self.realm.state(self.id), Some(RealmInterfaceState::Failed));
|
||||
assert!(matches!(
|
||||
&*self.realm.entry(self.id).unwrap(),
|
||||
RealmInterfaceEntry::Failed
|
||||
));
|
||||
assert!(self.realm.constructor(scope, self.id).is_none());
|
||||
assert!(self.realm.prototype(scope, self.id).is_none());
|
||||
assert!(self.realm.public_interface(scope, self.id).is_none());
|
||||
@@ -37,22 +40,29 @@ impl RegisteredInterface<'_> {
|
||||
assert_eq!(self.registry.materialization_count(self.id), 0);
|
||||
assert_eq!(lazy_getter_calls(), 0);
|
||||
|
||||
// Immutable private entries survive, but Failed prevents their reuse.
|
||||
assert!(crate::util::global_constructor_object(scope, self.name).is_none());
|
||||
assert!(crate::util::global_constructor_prototype(scope, self.name).is_none());
|
||||
// No intrinsic identity survives in a second registry or public lookup.
|
||||
let global = scope.get_current_context().global(scope);
|
||||
assert!(
|
||||
registered_intrinsic_constructor(scope, global, self.name)
|
||||
.is_some_and(|value| value.strict_equals(self.constructor.into()))
|
||||
);
|
||||
assert!(
|
||||
registered_intrinsic_prototype(scope, global, self.name)
|
||||
.is_some_and(|value| value.strict_equals(self.prototype.into()))
|
||||
assert!(registered_ecmascript_constructor(scope, global, self.name).is_none());
|
||||
assert!(registered_ecmascript_prototype(scope, global, self.name).is_none());
|
||||
assert_eq!(
|
||||
lazy_getter_calls(),
|
||||
0,
|
||||
"failed lookups must not read public bindings"
|
||||
);
|
||||
super::super::install::capture_eager_intrinsic_interfaces(scope, global, RealmKind::Window)
|
||||
.expect_err("eager capture cannot revive a failed interface");
|
||||
assert!(matches!(
|
||||
&*self.realm.entry(self.id).unwrap(),
|
||||
RealmInterfaceEntry::Failed
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn with_registered_interface(
|
||||
fn with_unpublished_interface(
|
||||
name: &'static str,
|
||||
test: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>, RegisteredInterface<'s>),
|
||||
test: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>, UnpublishedInterface<'s>),
|
||||
) {
|
||||
crate::ensure_v8_for_test();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
@@ -81,13 +91,10 @@ fn with_registered_interface(
|
||||
let prototype =
|
||||
constructor_prototype_object(scope, constructor.into()).expect("interface prototype");
|
||||
let global = context.global(scope);
|
||||
assert!(register_intrinsic_interface(
|
||||
moli_v8_util::install_web_api_intrinsic_resolver(
|
||||
scope,
|
||||
global,
|
||||
name,
|
||||
constructor.into(),
|
||||
prototype,
|
||||
));
|
||||
super::super::materialize::resolve_web_api_intrinsic,
|
||||
);
|
||||
assert_eq!(
|
||||
global.set_lazy_data_property(scope, v8str(scope, name).into(), counting_lazy_getter),
|
||||
Some(true)
|
||||
@@ -95,7 +102,7 @@ fn with_registered_interface(
|
||||
reset_lazy_getter_calls();
|
||||
test(
|
||||
scope,
|
||||
RegisteredInterface {
|
||||
UnpublishedInterface {
|
||||
registry,
|
||||
realm,
|
||||
id,
|
||||
@@ -108,7 +115,7 @@ fn with_registered_interface(
|
||||
|
||||
#[test]
|
||||
fn failed_unscopables_finalization_is_terminal() {
|
||||
with_registered_interface("Document", |scope, interface| {
|
||||
with_unpublished_interface("Document", |scope, interface| {
|
||||
let unscopables = interface
|
||||
.prototype
|
||||
.get(scope, v8::Symbol::get_unscopables(scope).into())
|
||||
@@ -125,7 +132,7 @@ fn failed_unscopables_finalization_is_terminal() {
|
||||
|
||||
#[test]
|
||||
fn missing_unscopables_finalization_is_terminal() {
|
||||
with_registered_interface("DocumentFragment", |scope, interface| {
|
||||
with_unpublished_interface("DocumentFragment", |scope, interface| {
|
||||
assert_eq!(
|
||||
interface
|
||||
.prototype
|
||||
@@ -138,7 +145,15 @@ fn missing_unscopables_finalization_is_terminal() {
|
||||
|
||||
#[test]
|
||||
fn failed_notification_finalization_is_terminal() {
|
||||
with_registered_interface("Notification", |scope, interface| {
|
||||
with_unpublished_interface("Notification", |scope, interface| {
|
||||
// Freeze after constructor inheritance is linked so the failure still
|
||||
// exercises the permission finalizer, rather than an earlier step.
|
||||
let parent = ensure_intrinsic_interface_constructor(scope, "EventTarget")
|
||||
.expect("Notification parent constructor");
|
||||
assert_eq!(
|
||||
interface.constructor.set_prototype(scope, parent.into()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
interface
|
||||
.constructor
|
||||
@@ -151,7 +166,7 @@ fn failed_notification_finalization_is_terminal() {
|
||||
|
||||
#[test]
|
||||
fn failed_performance_observer_finalization_is_terminal() {
|
||||
with_registered_interface("PerformanceObserver", |scope, interface| {
|
||||
with_unpublished_interface("PerformanceObserver", |scope, interface| {
|
||||
assert_eq!(
|
||||
interface
|
||||
.constructor
|
||||
@@ -164,7 +179,7 @@ fn failed_performance_observer_finalization_is_terminal() {
|
||||
|
||||
#[test]
|
||||
fn failed_html_constructor_link_is_terminal() {
|
||||
with_registered_interface("HTMLDivElement", |scope, interface| {
|
||||
with_unpublished_interface("HTMLDivElement", |scope, interface| {
|
||||
assert_eq!(
|
||||
interface
|
||||
.prototype
|
||||
@@ -176,54 +191,78 @@ fn failed_html_constructor_link_is_terminal() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn published_intrinsics_stay_hidden_until_the_caller_exposes_them() {
|
||||
with_registered_interface("DOMStringMap", |scope, interface| {
|
||||
fn shared_binding_helpers_preserve_intrinsics_after_author_overrides() {
|
||||
with_unpublished_interface("DOMStringMap", |scope, interface| {
|
||||
let public = materialize_interface(scope, interface.id).expect("completed materialization");
|
||||
let global = scope.get_current_context().global(scope);
|
||||
interface
|
||||
.realm
|
||||
.register_intrinsic_objects(
|
||||
scope,
|
||||
global,
|
||||
interface.id,
|
||||
interface.name,
|
||||
interface.constructor.into(),
|
||||
interface.prototype,
|
||||
interface.constructor.into(),
|
||||
)
|
||||
.expect("intrinsic publication");
|
||||
let replacement = v8::Object::new(scope);
|
||||
let replacement_prototype = v8::Object::new(scope);
|
||||
assert_eq!(
|
||||
interface.realm.state(interface.id),
|
||||
Some(RealmInterfaceState::Materializing)
|
||||
replacement.set(
|
||||
scope,
|
||||
v8str(scope, "prototype").into(),
|
||||
replacement_prototype.into(),
|
||||
),
|
||||
Some(true)
|
||||
);
|
||||
assert!(interface.realm.constructor(scope, interface.id).is_none());
|
||||
assert!(interface.realm.prototype(scope, interface.id).is_none());
|
||||
assert!(
|
||||
interface
|
||||
.realm
|
||||
.public_interface(scope, interface.id)
|
||||
.is_none()
|
||||
assert_eq!(
|
||||
global.set(
|
||||
scope,
|
||||
v8str(scope, interface.name).into(),
|
||||
replacement.into()
|
||||
),
|
||||
Some(true)
|
||||
);
|
||||
interface
|
||||
.realm
|
||||
.set_state(interface.id, RealmInterfaceState::Finalizing)
|
||||
.expect("caller starts finalization");
|
||||
assert!(interface.realm.constructor(scope, interface.id).is_some());
|
||||
assert!(interface.realm.prototype(scope, interface.id).is_some());
|
||||
|
||||
assert!(
|
||||
interface
|
||||
.realm
|
||||
.public_interface(scope, interface.id)
|
||||
.is_some()
|
||||
crate::util::global_constructor_object(scope, interface.name)
|
||||
.unwrap()
|
||||
.strict_equals(interface.constructor.into())
|
||||
);
|
||||
assert!(
|
||||
crate::util::global_constructor_prototype(scope, interface.name)
|
||||
.unwrap()
|
||||
.strict_equals(interface.prototype.into())
|
||||
);
|
||||
let instance = v8::Object::new(scope);
|
||||
moli_webapi_declare::set_required_interface_prototype(scope, instance, interface.name)
|
||||
.expect("shared declarations must use the realm intrinsic");
|
||||
assert!(
|
||||
instance
|
||||
.get_prototype(scope)
|
||||
.unwrap()
|
||||
.strict_equals(interface.prototype.into())
|
||||
);
|
||||
assert!(
|
||||
materialize_interface(scope, interface.id)
|
||||
.unwrap()
|
||||
.strict_equals(public)
|
||||
);
|
||||
assert_eq!(interface.registry.materialization_count(interface.id), 1);
|
||||
assert_eq!(
|
||||
global.delete(scope, v8str(scope, interface.name).into()),
|
||||
Some(true)
|
||||
);
|
||||
scope.get_current_context().detach_global();
|
||||
assert!(
|
||||
crate::util::global_constructor_object(scope, interface.name)
|
||||
.unwrap()
|
||||
.strict_equals(interface.constructor.into())
|
||||
);
|
||||
assert!(
|
||||
crate::util::global_constructor_prototype(scope, interface.name)
|
||||
.unwrap()
|
||||
.strict_equals(interface.prototype.into())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_xhr_event_target_initializes_its_private_state() {
|
||||
with_registered_interface("XMLHttpRequestEventTarget", |scope, interface| {
|
||||
fn materialized_xhr_event_target_initializes_its_private_state() {
|
||||
with_unpublished_interface("XMLHttpRequestEventTarget", |scope, interface| {
|
||||
assert!(
|
||||
ensure_intrinsic_interface_prototype(scope, interface.name)
|
||||
.expect("event target recovery")
|
||||
.expect("event target materialization")
|
||||
.strict_equals(interface.prototype.into())
|
||||
);
|
||||
let listener_slot = get_private_value(
|
||||
@@ -246,27 +285,27 @@ fn recovered_xhr_event_target_initializes_its_private_state() {
|
||||
.is_true()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
interface.realm.state(interface.id),
|
||||
Some(RealmInterfaceState::Ready)
|
||||
);
|
||||
assert!(matches!(
|
||||
&*interface.realm.entry(interface.id).unwrap(),
|
||||
RealmInterfaceEntry::Ready(_)
|
||||
));
|
||||
assert_eq!(lazy_getter_calls(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninitialized_crypto_adopts_registered_intrinsics_and_finalizes_secure_surface() {
|
||||
assert_registered_crypto_recovery(false);
|
||||
fn crypto_finalizes_its_secure_surface_before_publication() {
|
||||
assert_crypto_materialization(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninitialized_crypto_adopts_registered_intrinsics_and_refinalizes_secure_surface() {
|
||||
assert_registered_crypto_recovery(true);
|
||||
fn crypto_finalization_preserves_existing_secure_surface() {
|
||||
assert_crypto_materialization(true);
|
||||
}
|
||||
|
||||
fn assert_registered_crypto_recovery(already_finalized: bool) {
|
||||
with_registered_interface("Crypto", |scope, interface| {
|
||||
let RegisteredInterface {
|
||||
fn assert_crypto_materialization(already_finalized: bool) {
|
||||
with_unpublished_interface("Crypto", |scope, interface| {
|
||||
let UnpublishedInterface {
|
||||
registry,
|
||||
realm,
|
||||
id,
|
||||
@@ -292,15 +331,21 @@ fn assert_registered_crypto_recovery(already_finalized: bool) {
|
||||
let get_random_values = prototype
|
||||
.get(scope, v8str(scope, "getRandomValues").into())
|
||||
.expect("template-defined getRandomValues method");
|
||||
assert_eq!(realm.state(id), Some(RealmInterfaceState::Uninitialized));
|
||||
assert!(matches!(
|
||||
&*realm.entry(id).unwrap(),
|
||||
RealmInterfaceEntry::Uninitialized
|
||||
));
|
||||
|
||||
let recovered = ensure_intrinsic_interface_prototype(scope, "Crypto")
|
||||
.expect("adoption must allow secure-context finalization to run again");
|
||||
assert!(recovered.strict_equals(prototype.into()));
|
||||
assert_eq!(realm.state(id), Some(RealmInterfaceState::Ready));
|
||||
let intrinsic = ensure_intrinsic_interface_prototype(scope, "Crypto")
|
||||
.expect("materialization must finalize the secure surface");
|
||||
assert!(intrinsic.strict_equals(prototype.into()));
|
||||
assert!(matches!(
|
||||
&*realm.entry(id).unwrap(),
|
||||
RealmInterfaceEntry::Ready(_)
|
||||
));
|
||||
assert!(
|
||||
ensure_intrinsic_interface_constructor(scope, "Crypto")
|
||||
.expect("adopted Crypto constructor")
|
||||
.expect("trusted Crypto constructor")
|
||||
.strict_equals(constructor.into())
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -77,7 +77,7 @@ fn error_name_message_snapshot<'s>(
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let error_constructor = crate::util::registered_intrinsic_constructor(scope, global, "Error")?;
|
||||
let error_constructor = crate::util::registered_ecmascript_constructor(scope, global, "Error")?;
|
||||
if !value.instance_of(scope, error_constructor).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ pub use moli_v8_util::{
|
||||
define_non_enumerable_static_property, define_non_enumerable_static_string_property,
|
||||
get_own_static_property, get_private_object, get_private_value, get_property,
|
||||
global_constructor_object, global_constructor_prototype,
|
||||
initialize_intrinsic_interface_registry, new_null_prototype_object, object_bool_property,
|
||||
initialize_ecmascript_intrinsic_registry, new_null_prototype_object, object_bool_property,
|
||||
object_chain_contains, object_defined_string_property, object_non_empty_string_property,
|
||||
object_number_property, object_own_static_bool_property, object_own_static_property_as_array,
|
||||
object_own_static_string_property, object_property_as_array, object_property_as_object,
|
||||
object_string_property, private_key, register_intrinsic_interface,
|
||||
registered_intrinsic_constructor, registered_intrinsic_prototype, set_null_prototype,
|
||||
object_string_property, private_key, register_ecmascript_intrinsic,
|
||||
registered_ecmascript_constructor, registered_ecmascript_prototype, set_null_prototype,
|
||||
set_private_value, throw_range_error, throw_type_error, v8_json_parse, v8_string, v8str,
|
||||
walk_object_chain,
|
||||
};
|
||||
|
||||
@@ -242,8 +242,8 @@ pub(super) use service_worker_results::{
|
||||
use state::worker_close_callback;
|
||||
pub(crate) use state::{
|
||||
WORKER_STATE_SLOT, WorkerGlobalState, get_worker_state, worker_current_script_url,
|
||||
worker_exception_report_target, worker_global_is_closed, worker_service_worker_control_state,
|
||||
worker_storage_key, worker_storage_partition_identity, worker_termination_requested,
|
||||
worker_uses_shared_worker_agent_cluster,
|
||||
worker_exception_report_target, worker_global_is_closed, worker_realm_secure_context_available,
|
||||
worker_service_worker_control_state, worker_storage_key, worker_storage_partition_identity,
|
||||
worker_termination_requested, worker_uses_shared_worker_agent_cluster,
|
||||
};
|
||||
pub(super) use timers::{TimerInfo, WorkerIsolateTimerQueues, worker_isolate_timer_queues};
|
||||
|
||||
@@ -245,6 +245,11 @@ pub(crate) fn worker_service_worker_control_state(
|
||||
runtime.matching_controller_for_client(client_id)
|
||||
}
|
||||
|
||||
/// Returns the native secure-context policy, independently of public bindings.
|
||||
pub(crate) fn worker_realm_secure_context_available(scope: &mut v8::PinScope<'_, '_>) -> bool {
|
||||
get_worker_state(scope).is_some_and(|state| state.borrow().secure_context)
|
||||
}
|
||||
|
||||
/// Retrieve the `WorkerGlobalState` from a callback scope.
|
||||
pub(crate) fn get_worker_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
|
||||
@@ -42,9 +42,9 @@ pub(crate) use global_scope::{
|
||||
worker_global_origin, worker_message_port_registry, worker_message_port_wake_sender,
|
||||
worker_message_port_wrapper, worker_notification_permission_state,
|
||||
worker_opfs_directory_iterator_registry, worker_opfs_handle_registry,
|
||||
worker_requires_trusted_types_for_script, worker_service_worker_control_state,
|
||||
worker_storage_key, worker_storage_partition_identity, worker_termination_requested,
|
||||
worker_uses_shared_worker_agent_cluster,
|
||||
worker_realm_secure_context_available, worker_requires_trusted_types_for_script,
|
||||
worker_service_worker_control_state, worker_storage_key, worker_storage_partition_identity,
|
||||
worker_termination_requested, worker_uses_shared_worker_agent_cluster,
|
||||
};
|
||||
pub(crate) use handle::WorkerMessage;
|
||||
pub(crate) use handle::{
|
||||
|
||||
@@ -785,6 +785,48 @@ async fn worker_postmessage_cryptokey_to_nonsecure_worker_fires_messageerror() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_postmessage_cryptokey_exposure_ignores_public_constructor_overrides() {
|
||||
ensure_v8();
|
||||
for (url, setup, expected) in [
|
||||
(
|
||||
"https://worker-crypto.test/receive-cryptokey-override.js",
|
||||
"Object.defineProperty(globalThis, 'CryptoKey', {get() {throw new Error('public constructor read');}, configurable: true});",
|
||||
r#"["message","[object CryptoKey]"]"#,
|
||||
),
|
||||
(
|
||||
"http://worker-crypto.test/receive-cryptokey-override.js",
|
||||
"globalThis.CryptoKey = function FakeCryptoKey() {};",
|
||||
r#"["messageerror",true]"#,
|
||||
),
|
||||
] {
|
||||
let callbacks = r#"
|
||||
onmessage = event => {
|
||||
postMessage([event.type, Object.prototype.toString.call(event.data.key)]);
|
||||
};
|
||||
onmessageerror = event => {
|
||||
postMessage([event.type, event.data === null]);
|
||||
};
|
||||
"#;
|
||||
let mut handle = spawn_worker(format!("{setup}\n{callbacks}"), url.into());
|
||||
let payload = serialize_test_crypto_value(
|
||||
r#"
|
||||
(async () => ({key: await crypto.subtle.importKey(
|
||||
"raw", new Uint8Array([1, 2, 3, 4]),
|
||||
{name: "HMAC", hash: "SHA-256"}, true, ["sign"]
|
||||
)}))()
|
||||
"#,
|
||||
);
|
||||
handle.post_message(payload);
|
||||
|
||||
let msg = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("channel closed");
|
||||
assert_eq!(expect_post_json(msg), expected, "receiver {url}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_crypto_subtle_kdf_and_x25519_derivation_match_wpt_any_tests() {
|
||||
ensure_v8();
|
||||
|
||||
@@ -1,11 +1,43 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use v8::{Local, Object, PinScope, PropertyAttribute};
|
||||
|
||||
use crate::properties::new_null_prototype_object;
|
||||
use crate::strings::{v8_string, v8str};
|
||||
use crate::symbols::{get_private_object, set_private_value};
|
||||
|
||||
const INTRINSIC_CONSTRUCTORS_SLOT: &str = "__moliIntrinsicConstructors";
|
||||
const INTRINSIC_PROTOTYPES_SLOT: &str = "__moliIntrinsicPrototypes";
|
||||
const ECMASCRIPT_CONSTRUCTORS_SLOT: &str = "__moliEcmascriptConstructors";
|
||||
const ECMASCRIPT_PROTOTYPES_SLOT: &str = "__moliEcmascriptPrototypes";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WebApiIntrinsicKind {
|
||||
Constructor,
|
||||
Prototype,
|
||||
}
|
||||
|
||||
pub enum WebApiIntrinsicLookup<'s> {
|
||||
/// This name is outside the embedder's Web API registry.
|
||||
Unmanaged,
|
||||
/// A managed name must never fall back to an author-controlled global,
|
||||
/// including when materialization failed or the interface is unavailable.
|
||||
Managed(Option<Local<'s, Object>>),
|
||||
}
|
||||
|
||||
pub type WebApiIntrinsicResolver =
|
||||
for<'s, 'i> fn(&mut PinScope<'s, 'i>, &str, WebApiIntrinsicKind) -> WebApiIntrinsicLookup<'s>;
|
||||
|
||||
struct RealmWebApiIntrinsicResolver(WebApiIntrinsicResolver);
|
||||
|
||||
/// Connects shared binding helpers to the embedder's canonical realm cache.
|
||||
/// Install after eager bootstrap; the resolver stores no intrinsic identities.
|
||||
pub fn install_web_api_intrinsic_resolver(
|
||||
scope: &mut PinScope<'_, '_>,
|
||||
resolver: WebApiIntrinsicResolver,
|
||||
) {
|
||||
scope
|
||||
.get_current_context()
|
||||
.set_slot(Rc::new(RealmWebApiIntrinsicResolver(resolver)));
|
||||
}
|
||||
|
||||
fn intrinsic_registry_object<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
@@ -21,18 +53,18 @@ fn intrinsic_registry_object<'s>(
|
||||
registry
|
||||
}
|
||||
|
||||
/// Ensures that a realm global owns its native-only intrinsic interface maps.
|
||||
/// Ensures that a realm global owns its native-only ECMAScript intrinsic maps.
|
||||
///
|
||||
/// The maps are stored under V8 private symbols, so author code cannot observe
|
||||
/// or replace them through JavaScript reflection. They intentionally contain
|
||||
/// realm-local V8 objects rather than Rust `Global` handles, allowing V8 to
|
||||
/// reclaim the whole realm graph when its context becomes unreachable.
|
||||
pub fn initialize_intrinsic_interface_registry<'s>(
|
||||
pub fn initialize_ecmascript_intrinsic_registry<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
global: Local<'s, Object>,
|
||||
) {
|
||||
let _ = intrinsic_registry_object(scope, global, INTRINSIC_CONSTRUCTORS_SLOT);
|
||||
let _ = intrinsic_registry_object(scope, global, INTRINSIC_PROTOTYPES_SLOT);
|
||||
let _ = intrinsic_registry_object(scope, global, ECMASCRIPT_CONSTRUCTORS_SLOT);
|
||||
let _ = intrinsic_registry_object(scope, global, ECMASCRIPT_PROTOTYPES_SLOT);
|
||||
}
|
||||
|
||||
fn define_intrinsic<'s>(
|
||||
@@ -56,21 +88,22 @@ fn define_intrinsic<'s>(
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Records the trusted constructor and prototype for one realm-local Web API.
|
||||
/// Records a trusted ECMAScript constructor and prototype. Web API identities
|
||||
/// belong to the embedder's realm cache, accessed through its resolver.
|
||||
///
|
||||
/// Registrations are immutable. Returning `false` means allocation failed or
|
||||
/// an entry with the same name was already finalized; callers should treat
|
||||
/// that as a bootstrap/materialization error rather than silently replacing an
|
||||
/// intrinsic identity.
|
||||
pub fn register_intrinsic_interface<'s>(
|
||||
pub fn register_ecmascript_intrinsic<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
global: Local<'s, Object>,
|
||||
name: &str,
|
||||
constructor: Local<'s, Object>,
|
||||
prototype: Local<'s, Object>,
|
||||
) -> bool {
|
||||
let constructors = intrinsic_registry_object(scope, global, INTRINSIC_CONSTRUCTORS_SLOT);
|
||||
let prototypes = intrinsic_registry_object(scope, global, INTRINSIC_PROTOTYPES_SLOT);
|
||||
let constructors = intrinsic_registry_object(scope, global, ECMASCRIPT_CONSTRUCTORS_SLOT);
|
||||
let prototypes = intrinsic_registry_object(scope, global, ECMASCRIPT_PROTOTYPES_SLOT);
|
||||
|
||||
let Some(key) = v8_string(scope, name) else {
|
||||
return false;
|
||||
@@ -107,20 +140,20 @@ fn registered_intrinsic<'s>(
|
||||
.and_then(|value| Local::<Object>::try_from(value).ok())
|
||||
}
|
||||
|
||||
pub fn registered_intrinsic_constructor<'s>(
|
||||
pub fn registered_ecmascript_constructor<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
global: Local<'s, Object>,
|
||||
name: &str,
|
||||
) -> Option<Local<'s, Object>> {
|
||||
registered_intrinsic(scope, global, INTRINSIC_CONSTRUCTORS_SLOT, name)
|
||||
registered_intrinsic(scope, global, ECMASCRIPT_CONSTRUCTORS_SLOT, name)
|
||||
}
|
||||
|
||||
pub fn registered_intrinsic_prototype<'s>(
|
||||
pub fn registered_ecmascript_prototype<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
global: Local<'s, Object>,
|
||||
name: &str,
|
||||
) -> Option<Local<'s, Object>> {
|
||||
registered_intrinsic(scope, global, INTRINSIC_PROTOTYPES_SLOT, name)
|
||||
registered_intrinsic(scope, global, ECMASCRIPT_PROTOTYPES_SLOT, name)
|
||||
}
|
||||
|
||||
pub fn constructor_object<'s>(
|
||||
@@ -157,8 +190,16 @@ pub fn global_constructor_object<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
name: &str,
|
||||
) -> Option<Local<'s, Object>> {
|
||||
if let Some(resolver) = scope
|
||||
.get_current_context()
|
||||
.get_slot::<RealmWebApiIntrinsicResolver>()
|
||||
&& let WebApiIntrinsicLookup::Managed(constructor) =
|
||||
(resolver.0)(scope, name, WebApiIntrinsicKind::Constructor)
|
||||
{
|
||||
return constructor;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
registered_intrinsic_constructor(scope, global, name)
|
||||
registered_ecmascript_constructor(scope, global, name)
|
||||
.or_else(|| constructor_object(scope, global, name))
|
||||
}
|
||||
|
||||
@@ -166,8 +207,16 @@ pub fn global_constructor_prototype<'s>(
|
||||
scope: &mut PinScope<'s, '_>,
|
||||
name: &str,
|
||||
) -> Option<Local<'s, Object>> {
|
||||
if let Some(resolver) = scope
|
||||
.get_current_context()
|
||||
.get_slot::<RealmWebApiIntrinsicResolver>()
|
||||
&& let WebApiIntrinsicLookup::Managed(prototype) =
|
||||
(resolver.0)(scope, name, WebApiIntrinsicKind::Prototype)
|
||||
{
|
||||
return prototype;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
registered_intrinsic_prototype(scope, global, name)
|
||||
registered_ecmascript_prototype(scope, global, name)
|
||||
.or_else(|| constructor_prototype(scope, global, name))
|
||||
}
|
||||
|
||||
@@ -178,10 +227,10 @@ mod tests {
|
||||
use moli_v8_test_util::ensure_v8;
|
||||
|
||||
use super::{
|
||||
INTRINSIC_CONSTRUCTORS_SLOT, INTRINSIC_PROTOTYPES_SLOT, global_constructor_object,
|
||||
global_constructor_prototype, initialize_intrinsic_interface_registry,
|
||||
register_intrinsic_interface, registered_intrinsic_constructor,
|
||||
registered_intrinsic_prototype,
|
||||
ECMASCRIPT_CONSTRUCTORS_SLOT, ECMASCRIPT_PROTOTYPES_SLOT, global_constructor_object,
|
||||
global_constructor_prototype, initialize_ecmascript_intrinsic_registry,
|
||||
register_ecmascript_intrinsic, registered_ecmascript_constructor,
|
||||
registered_ecmascript_prototype,
|
||||
};
|
||||
use crate::strings::v8str;
|
||||
|
||||
@@ -195,10 +244,10 @@ mod tests {
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(scope);
|
||||
|
||||
initialize_intrinsic_interface_registry(scope, global);
|
||||
initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
let intrinsic_constructor = v8::Object::new(scope);
|
||||
let intrinsic_prototype = v8::Object::new(scope);
|
||||
assert!(register_intrinsic_interface(
|
||||
assert!(register_ecmascript_intrinsic(
|
||||
scope,
|
||||
global,
|
||||
"Sample",
|
||||
@@ -226,11 +275,11 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(
|
||||
registered_intrinsic_constructor(scope, global, "Sample")
|
||||
registered_ecmascript_constructor(scope, global, "Sample")
|
||||
.is_some_and(|value| value.strict_equals(intrinsic_constructor.into()))
|
||||
);
|
||||
assert!(
|
||||
registered_intrinsic_prototype(scope, global, "Sample")
|
||||
registered_ecmascript_prototype(scope, global, "Sample")
|
||||
.is_some_and(|value| value.strict_equals(intrinsic_prototype.into()))
|
||||
);
|
||||
assert!(
|
||||
@@ -253,7 +302,7 @@ mod tests {
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(scope);
|
||||
|
||||
initialize_intrinsic_interface_registry(scope, global);
|
||||
initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
let constructor = v8::Object::new(scope);
|
||||
let prototype = v8::Object::new(scope);
|
||||
assert_eq!(
|
||||
@@ -287,7 +336,7 @@ mod tests {
|
||||
|
||||
let constructor = v8::Object::new(scope);
|
||||
let prototype = v8::Object::new(scope);
|
||||
assert!(register_intrinsic_interface(
|
||||
assert!(register_ecmascript_intrinsic(
|
||||
scope,
|
||||
global,
|
||||
"Sample",
|
||||
@@ -296,7 +345,7 @@ mod tests {
|
||||
));
|
||||
let replacement_constructor = v8::Object::new(scope);
|
||||
let replacement_prototype = v8::Object::new(scope);
|
||||
assert!(!register_intrinsic_interface(
|
||||
assert!(!register_ecmascript_intrinsic(
|
||||
scope,
|
||||
global,
|
||||
"Sample",
|
||||
@@ -312,8 +361,8 @@ mod tests {
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| value.to_string(scope))
|
||||
.map(|value| value.to_rust_string_lossy(scope));
|
||||
assert_ne!(name.as_deref(), Some(INTRINSIC_CONSTRUCTORS_SLOT));
|
||||
assert_ne!(name.as_deref(), Some(INTRINSIC_PROTOTYPES_SLOT));
|
||||
assert_ne!(name.as_deref(), Some(ECMASCRIPT_CONSTRUCTORS_SLOT));
|
||||
assert_ne!(name.as_deref(), Some(ECMASCRIPT_PROTOTYPES_SLOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user