mirror of
https://github.com/lexmount/moli.git
synced 2026-11-15 00:08:03 +00:00
fix(webidl): defer constructor prototype selection until after conversion
This commit is contained in:
@@ -754,9 +754,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>(
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcPeerConnectionIceEvent => {
|
||||
v8::FunctionTemplate::builder(rtc_peer_connection_ice_event_constructor_callback)
|
||||
.length(1)
|
||||
.build(scope)
|
||||
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
|
||||
web_api_interfaces::RTCPeerConnectionIceEvent,
|
||||
rtc_peer_connection_ice_event_constructor_callback
|
||||
))
|
||||
.length(1)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcPeerConnectionIceErrorEvent => {
|
||||
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
|
||||
@@ -767,9 +770,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>(
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcDataChannelEvent => {
|
||||
v8::FunctionTemplate::builder(rtc_data_channel_event_constructor_callback)
|
||||
.length(2)
|
||||
.build(scope)
|
||||
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
|
||||
web_api_interfaces::RTCDataChannelEvent,
|
||||
rtc_data_channel_event_constructor_callback
|
||||
))
|
||||
.length(2)
|
||||
.build(scope)
|
||||
}
|
||||
ConstructorKind::RtcPeerConnectionIceErrorEvent => {
|
||||
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::util::apply_webidl_constructor_prototype_fallback;
|
||||
use moli_webapi_declare::initialize_web_api_constructor_receiver;
|
||||
use webidl::WebIdlDictionarySerialize;
|
||||
|
||||
const ERROR_DETAIL: &str = "__moliRtcErrorDetail";
|
||||
@@ -91,6 +91,9 @@ pub(crate) fn rtc_error_constructor_callback<'s>(
|
||||
let Some(parsed) = webidl::parse_args::<ConstructorArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCError") {
|
||||
return;
|
||||
}
|
||||
let Some(message) =
|
||||
v8::String::new_from_two_byte(scope, &parsed.message.0, v8::NewStringType::Normal)
|
||||
else {
|
||||
@@ -104,12 +107,6 @@ pub(crate) fn rtc_error_constructor_callback<'s>(
|
||||
&& declaration.initialize(scope, args.this()).is_ok()
|
||||
{
|
||||
capture_dom_exception_stack(scope, args.this());
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCError",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
use moli_webapi_declare::{WebApiObject, initialize_web_api_constructor_receiver};
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain)]
|
||||
@@ -48,6 +48,9 @@ pub(in crate::context_bootstrap) fn image_constructor_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLImageElement") {
|
||||
return;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let Some(document) = global.get(scope, v8str(scope, "document").into()) else {
|
||||
rv.set_undefined();
|
||||
@@ -85,7 +88,7 @@ pub(in crate::context_bootstrap) fn image_constructor_callback<'s>(
|
||||
let value = v8::Integer::new_from_unsigned(scope, height);
|
||||
let _ = image.set(scope, v8str(scope, "height").into(), value.into());
|
||||
}
|
||||
apply_legacy_factory_new_target_prototype(scope, &args, image, "HTMLImageElement");
|
||||
copy_legacy_factory_receiver_prototype(scope, &args, image);
|
||||
rv.set(image.into());
|
||||
}
|
||||
|
||||
@@ -102,6 +105,9 @@ pub(in crate::context_bootstrap) fn audio_constructor_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLAudioElement") {
|
||||
return;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let Some(document) = global.get(scope, v8str(scope, "document").into()) else {
|
||||
rv.set_undefined();
|
||||
@@ -155,7 +161,7 @@ pub(in crate::context_bootstrap) fn audio_constructor_callback<'s>(
|
||||
);
|
||||
}
|
||||
}
|
||||
apply_legacy_factory_new_target_prototype(scope, &args, audio, "HTMLAudioElement");
|
||||
copy_legacy_factory_receiver_prototype(scope, &args, audio);
|
||||
rv.set(audio.into());
|
||||
}
|
||||
|
||||
@@ -172,6 +178,9 @@ pub(in crate::context_bootstrap) fn option_constructor_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLOptionElement") {
|
||||
return;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let Some(document) = global.get(scope, v8str(scope, "document").into()) else {
|
||||
rv.set_undefined();
|
||||
@@ -246,23 +255,16 @@ pub(in crate::context_bootstrap) fn option_constructor_callback<'s>(
|
||||
false,
|
||||
);
|
||||
}
|
||||
apply_legacy_factory_new_target_prototype(scope, &args, option, "HTMLOptionElement");
|
||||
copy_legacy_factory_receiver_prototype(scope, &args, option);
|
||||
rv.set(option.into());
|
||||
}
|
||||
|
||||
fn apply_legacy_factory_new_target_prototype<'s>(
|
||||
fn copy_legacy_factory_receiver_prototype<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
result: v8::Local<'s, v8::Object>,
|
||||
default_constructor_name: &str,
|
||||
) {
|
||||
let receiver = args.this();
|
||||
crate::util::apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
receiver,
|
||||
args.new_target(),
|
||||
default_constructor_name,
|
||||
);
|
||||
if let Some(prototype) = receiver.get_prototype(scope) {
|
||||
let _ = result.set_prototype(scope, prototype);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,8 @@ pub(crate) fn capture_eager_intrinsic_interfaces<'s>(
|
||||
let registry = ExposedInterfaceTemplateRegistry::current(scope)
|
||||
.ok_or_else(|| anyhow!("exposed interface template registry is unavailable"))?;
|
||||
initialize_ecmascript_intrinsic_registry(scope, global);
|
||||
moli_webapi_declare::capture_web_api_constructor_intrinsics(scope)
|
||||
.ok_or_else(|| anyhow!("native Reflect.construct is unavailable during realm bootstrap"))?;
|
||||
capture_ecmascript_intrinsic(scope, global, "Error")?;
|
||||
capture_ecmascript_intrinsic(scope, global, "Promise")?;
|
||||
let realm = IntrinsicInterfaceRegistry::initialize_for_current_context(
|
||||
|
||||
@@ -278,6 +278,33 @@ fn finish_materialized_interface<'s>(
|
||||
set_interface_prototype_constructor(scope, constructor_prototype, proxy.into())?;
|
||||
v8::Local::<v8::Object>::from(proxy)
|
||||
}
|
||||
ConstructorKind::DomParser
|
||||
| ConstructorKind::VideoColorSpace
|
||||
| ConstructorKind::RtcIceCandidate
|
||||
| ConstructorKind::RtcSessionDescription
|
||||
| ConstructorKind::RtcError
|
||||
| ConstructorKind::RtcErrorEvent
|
||||
| ConstructorKind::RtcDataChannelEvent
|
||||
| ConstructorKind::RtcPeerConnectionIceEvent
|
||||
| ConstructorKind::RtcPeerConnectionIceErrorEvent
|
||||
| ConstructorKind::Image
|
||||
| ConstructorKind::Audio
|
||||
| ConstructorKind::Option => {
|
||||
let proxy = moli_webapi_declare::web_api_constructor_with_deferred_prototype(
|
||||
scope,
|
||||
constructor,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"failed to create WebIDL constructor entry for `{}`",
|
||||
metadata.name
|
||||
)
|
||||
})?;
|
||||
if metadata.prototype_property == ResolvedPrototypeProperty::TemplateReadOnly {
|
||||
set_interface_prototype_constructor(scope, constructor_prototype, proxy.into())?;
|
||||
}
|
||||
proxy.into()
|
||||
}
|
||||
_ => constructor.into(),
|
||||
};
|
||||
// Like Blink's ConstructorForTypeSlowCase, finish every fallible binding
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Immutable color metadata, independent of video decoding and rendering.
|
||||
|
||||
use crate::{
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, get_private_value, throw_type_error, v8str,
|
||||
},
|
||||
util::{get_private_value, throw_type_error, v8str},
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
use moli_webapi_declare::{
|
||||
WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver,
|
||||
};
|
||||
|
||||
const PRIMARIES_SLOT: &str = "__moliVideoColorSpacePrimaries";
|
||||
const TRANSFER_SLOT: &str = "__moliVideoColorSpaceTransfer";
|
||||
@@ -185,6 +185,9 @@ pub(in crate::context_bootstrap) fn video_color_space_constructor_callback<'s>(
|
||||
let Some(parsed) = webidl::parse_args::<VideoColorSpaceConstructorArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "VideoColorSpace") {
|
||||
return;
|
||||
}
|
||||
let init = parsed.init;
|
||||
let primaries = nullable_string(scope, init.primaries.map(VideoColorPrimaries::label));
|
||||
let transfer = nullable_string(
|
||||
@@ -199,12 +202,6 @@ pub(in crate::context_bootstrap) fn video_color_space_constructor_callback<'s>(
|
||||
VideoColorSpaceObjectDeclaration::new(primaries, transfer, matrix, full_range)
|
||||
.initialize(scope, args.this())
|
||||
.expect("VideoColorSpace slots initialize");
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"VideoColorSpace",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ use crate::{
|
||||
EventInit, event_private_value, initialize_event_object_with_type,
|
||||
initialize_event_wrapper, new_event_state,
|
||||
},
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item,
|
||||
throw_type_error,
|
||||
},
|
||||
util::{callback_data_index_value, callback_data_item, throw_type_error},
|
||||
webidl::{self, WebIdlDictionarySerialize},
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiInterfaceDescriptor};
|
||||
use moli_webapi_declare::{
|
||||
WebApiFunctionTemplate, WebApiInterfaceDescriptor, initialize_web_api_constructor_receiver,
|
||||
};
|
||||
|
||||
const ICE_EVENT_CANDIDATE_SLOT: &str = "__moliRtcIceEventCandidate";
|
||||
const ICE_EVENT_URL_SLOT: &str = "__moliRtcIceEventUrl";
|
||||
@@ -148,12 +147,6 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_event_constructor_ca
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCPeerConnectionIceEvent",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
}
|
||||
@@ -180,12 +173,6 @@ pub(in crate::context_bootstrap) fn rtc_data_channel_event_constructor_callback<
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCDataChannelEvent",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
}
|
||||
@@ -212,12 +199,6 @@ pub(in crate::context_bootstrap) fn rtc_error_event_constructor_callback<'s>(
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCErrorEvent",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
}
|
||||
@@ -230,6 +211,9 @@ fn initialize_event<'s, T: WebIdlDictionarySerialize<'s>>(
|
||||
payload: &T,
|
||||
interface: WebApiInterfaceDescriptor,
|
||||
) -> Option<()> {
|
||||
if !initialize_web_api_constructor_receiver(scope, wrapper, interface.name()) {
|
||||
return None;
|
||||
}
|
||||
let event_type =
|
||||
v8::String::new_from_two_byte(scope, &event_type.0, v8::NewStringType::Normal)?;
|
||||
let state = new_event_state(scope);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use super::ice_candidate_parser::parse_ice_candidate;
|
||||
use crate::web_api_interfaces;
|
||||
use crate::{
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, callback_data_index_value, get_private_value,
|
||||
throw_type_error, v8str,
|
||||
},
|
||||
util::{callback_data_index_value, get_private_value, throw_type_error, v8str},
|
||||
webidl::{self, WebIdlConverter},
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
use moli_webapi_declare::{
|
||||
WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver,
|
||||
};
|
||||
|
||||
const CANDIDATE_VALUES_SLOT: &str = "__moliRtcIceCandidateValues";
|
||||
|
||||
@@ -98,6 +97,9 @@ pub(in crate::context_bootstrap) fn rtc_ice_candidate_constructor_callback<'s>(
|
||||
let Some(mut values) = parse_candidate_init(scope, dictionary) else {
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCIceCandidate") {
|
||||
return;
|
||||
}
|
||||
if values[1].is_null() && values[2].is_null() {
|
||||
throw_type_error(scope, "RTCIceCandidate requires sdpMid or sdpMLineIndex.");
|
||||
return;
|
||||
@@ -130,12 +132,6 @@ pub(in crate::context_bootstrap) fn rtc_ice_candidate_constructor_callback<'s>(
|
||||
{
|
||||
return;
|
||||
}
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCIceCandidate",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,10 @@ use crate::{
|
||||
EventInit, event_private_value, initialize_event_object_with_type,
|
||||
initialize_event_wrapper, new_event_state,
|
||||
},
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item,
|
||||
throw_type_error,
|
||||
},
|
||||
util::{callback_data_index_value, callback_data_item, throw_type_error},
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, initialize_web_api_constructor_receiver};
|
||||
use webidl::WebIdlDictionarySerialize;
|
||||
|
||||
const ADDRESS_SLOT: &str = "__moliRtcIceErrorAddress";
|
||||
@@ -91,6 +88,13 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_error_event_construc
|
||||
let Some(parsed) = webidl::parse_args::<Args>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(
|
||||
scope,
|
||||
args.this(),
|
||||
"RTCPeerConnectionIceErrorEvent",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let Some(event_type) =
|
||||
v8::String::new_from_two_byte(scope, &parsed.event_type.0, v8::NewStringType::Normal)
|
||||
else {
|
||||
@@ -110,12 +114,6 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_error_event_construc
|
||||
{
|
||||
return;
|
||||
}
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCPeerConnectionIceErrorEvent",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ use super::RtcSessionDescriptionInitDeclaration;
|
||||
use crate::web_api_interfaces;
|
||||
use crate::{
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item,
|
||||
get_private_value, throw_type_error, v8str,
|
||||
callback_data_index_value, callback_data_item, get_private_value, throw_type_error, v8str,
|
||||
},
|
||||
webidl,
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
use moli_webapi_declare::{
|
||||
WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver,
|
||||
};
|
||||
|
||||
const DESCRIPTION_TYPE_SLOT: &str = "__moliRtcSessionDescriptionType";
|
||||
const DESCRIPTION_SDP_SLOT: &str = "__moliRtcSessionDescriptionSdp";
|
||||
@@ -105,18 +106,15 @@ pub(in crate::context_bootstrap) fn rtc_session_description_constructor_callback
|
||||
throw_type_error(scope, "Invalid RTCSdpType.");
|
||||
return;
|
||||
}
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCSessionDescription") {
|
||||
return;
|
||||
}
|
||||
if SessionDescriptionObjectDeclaration::new(kind, sdp)
|
||||
.initialize(scope, args.this())
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
apply_webidl_constructor_prototype_fallback(
|
||||
scope,
|
||||
args.this(),
|
||||
args.new_target(),
|
||||
"RTCSessionDescription",
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::web_api_interfaces;
|
||||
use moli_web_mime::{is_dom_parser_xml_mime, is_html_document_mime, is_xml_document_mime};
|
||||
use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
use moli_webapi_declare::initialize_web_api_constructor_receiver;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
@@ -19,8 +20,8 @@ use super::{
|
||||
},
|
||||
},
|
||||
util::{
|
||||
apply_webidl_constructor_prototype_fallback, context_host_ptr_from_global_bridge,
|
||||
get_private_object, get_private_value, set_private_value, throw_type_error,
|
||||
context_host_ptr_from_global_bridge, get_private_object, get_private_value,
|
||||
set_private_value, throw_type_error,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -132,7 +133,9 @@ pub(super) fn dom_parser_constructor_callback<'s>(
|
||||
return;
|
||||
}
|
||||
let parser = args.this();
|
||||
apply_webidl_constructor_prototype_fallback(scope, parser, args.new_target(), "DOMParser");
|
||||
if !initialize_web_api_constructor_receiver(scope, parser, "DOMParser") {
|
||||
return;
|
||||
}
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
throw_type_error(scope, "DOMParser constructor has no associated Document");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
(async () => {
|
||||
const rows = [];
|
||||
const check = (name, ok, detail = null) => rows.push({name, ok: !!ok, detail});
|
||||
const child = document.querySelector('iframe');
|
||||
await new Promise(resolve => {
|
||||
if (child.contentDocument.readyState === 'complete') resolve();
|
||||
else child.addEventListener('load', resolve, {once: true});
|
||||
});
|
||||
const other = child.contentWindow;
|
||||
const definitions = [
|
||||
['DOMParser', () => [], 'DOMParser'],
|
||||
['VideoColorSpace', () => [{matrix: 'bt709'}], 'VideoColorSpace'],
|
||||
['RTCIceCandidate', () => [{candidate: '', sdpMid: '0'}], 'RTCIceCandidate'],
|
||||
['RTCSessionDescription', () => [{type: 'offer', sdp: ''}], 'RTCSessionDescription'],
|
||||
['RTCError', () => [{errorDetail: 'sdp-syntax-error'}, 'reason'], 'RTCError'],
|
||||
['RTCDataChannelEvent', w => ['x', {channel: channels.get(w)}], 'RTCDataChannelEvent'],
|
||||
['RTCErrorEvent', w => ['x', {error: new w.RTCError({errorDetail: 'sdp-syntax-error'})}], 'RTCErrorEvent'],
|
||||
['RTCPeerConnectionIceEvent', () => ['x', {candidate: null}], 'RTCPeerConnectionIceEvent'],
|
||||
['RTCPeerConnectionIceErrorEvent', () => ['x', {errorCode: 701}], 'RTCPeerConnectionIceErrorEvent'],
|
||||
['Image', () => [1, 2], 'HTMLImageElement'],
|
||||
['Audio', () => [''], 'HTMLAudioElement'],
|
||||
['Option', () => ['label', 'value', true, true], 'HTMLOptionElement']
|
||||
];
|
||||
const connections = [];
|
||||
const channels = new Map();
|
||||
try {
|
||||
for (const w of [window, other]) {
|
||||
const realm = w === window ? 'main' : 'child';
|
||||
const peer = new w.RTCPeerConnection(); connections.push(peer);
|
||||
channels.set(w, peer.createDataChannel('constructor-test'));
|
||||
for (const [name, args, defaultName] of definitions) {
|
||||
try {
|
||||
const C = w[name], defaults = w[defaultName].prototype;
|
||||
check(`${realm}/${name}/metadata`, typeof C === 'function' && C.name === name && C.prototype === defaults);
|
||||
const explicit = [w.Object.prototype, Object.create(null), new Proxy({}, {})];
|
||||
for (const [i, prototype] of explicit.entries()) {
|
||||
const N = new Proxy(w.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') return prototype;
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const value = Reflect.construct(C, args(w), N);
|
||||
check(`${realm}/${name}/explicit-${i}`, Object.getPrototypeOf(value) === prototype);
|
||||
}
|
||||
for (const nw of [window, other]) {
|
||||
let reads = 0;
|
||||
const N = new Proxy(nw.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') { reads++; return 42; }
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const value = Reflect.construct(C, args(w), N);
|
||||
check(`${realm}/${name}/fallback-${nw === window ? 'main' : 'child'}`, Object.getPrototypeOf(value) === nw[defaultName].prototype && reads === 1, {reads});
|
||||
}
|
||||
const prototype = Object.create(null);
|
||||
let reads = 0, nested = null;
|
||||
const N = new Proxy(w.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') {
|
||||
reads++;
|
||||
if (reads === 1) nested = Reflect.construct(C, args(w), N);
|
||||
return prototype;
|
||||
}
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const outer = Reflect.construct(C, args(w), N);
|
||||
check(`${realm}/${name}/reentry`, reads === 2 && Object.getPrototypeOf(outer) === prototype && Object.getPrototypeOf(nested) === prototype, {reads});
|
||||
const sentinel = {};
|
||||
const throwing = new Proxy(w.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') throw sentinel;
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
let thrown;
|
||||
try { Reflect.construct(C, args(w), throwing); } catch (error) { thrown = error; }
|
||||
check(`${realm}/${name}/prototype-exception`, thrown === sentinel);
|
||||
} catch (error) { check(`${realm}/${name}/setup`, false, String(error)); }
|
||||
}
|
||||
for (const kind of ['object', 'primitive']) {
|
||||
let revoke, reads = 0;
|
||||
const prototype = Object.create(null);
|
||||
const pair = Proxy.revocable(w.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') { reads++; revoke(); return kind === 'object' ? prototype : 7; }
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
revoke = pair.revoke;
|
||||
let value, error;
|
||||
try { value = Reflect.construct(w.VideoColorSpace, [], pair.proxy); } catch (e) { error = e; }
|
||||
check(`${realm}/revoked-after-get-${kind}`, reads === 1 && (kind === 'object' ? value && Object.getPrototypeOf(value) === prototype : error instanceof w.TypeError), {reads, error: String(error)});
|
||||
}
|
||||
const log = [];
|
||||
const N = new Proxy(w.Function(), {get(target, key, receiver) {
|
||||
if (key === 'prototype') { log.push('prototype'); return w.Object.prototype; }
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const init = {get fullRange() { log.push('fullRange'); return true; }, get matrix() { log.push('matrix'); return 'bt709'; }, get primaries() { log.push('primaries'); return 'bt709'; }, get transfer() { log.push('transfer'); return 'bt709'; }};
|
||||
Reflect.construct(w.VideoColorSpace, [init], N);
|
||||
check(`${realm}/conversion-before-prototype`, log.join(',') === 'fullRange,matrix,primaries,transfer,prototype', log);
|
||||
log.length = 0;
|
||||
const sentinel = {};
|
||||
try { Reflect.construct(w.VideoColorSpace, [{get matrix() { log.push('matrix'); throw sentinel; }}], N); } catch (error) { check(`${realm}/conversion-exception-identity`, error === sentinel); }
|
||||
check(`${realm}/failed-conversion-no-prototype`, log.join(',') === 'matrix', log);
|
||||
}
|
||||
// A lazily materialized constructor must use captured native construction,
|
||||
// even when an author has replaced Reflect.construct and polluted traps.
|
||||
const intrinsicConstruct = Reflect.construct;
|
||||
const prototypeGet = Object.prototype.get;
|
||||
const inheritedConstruct = Object.prototype.construct;
|
||||
try {
|
||||
Reflect.construct = () => { throw Error('author Reflect.construct'); };
|
||||
Object.prototype.get = () => { throw Error('inherited get trap'); };
|
||||
Object.prototype.construct = () => { throw Error('inherited construct trap'); };
|
||||
check('intrinsic-capture-and-handler-prototype', Object.getPrototypeOf(new VideoColorSpace()) === VideoColorSpace.prototype);
|
||||
} finally {
|
||||
Reflect.construct = intrinsicConstruct;
|
||||
if (prototypeGet === undefined) delete Object.prototype.get; else Object.prototype.get = prototypeGet;
|
||||
if (inheritedConstruct === undefined) delete Object.prototype.construct; else Object.prototype.construct = inheritedConstruct;
|
||||
}
|
||||
} finally { for (const connection of connections) connection.close(); }
|
||||
globalThis.__uiEventResults = {rows, total: rows.length, passed: rows.filter(row => row.ok).length, failed: rows.filter(row => !row.ok)};
|
||||
return true;
|
||||
})()
|
||||
@@ -0,0 +1,43 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn webidl_constructors_preserve_explicit_prototypes_and_conversion_order_across_realms() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://constructor-entry.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(include_str!("constructor_entry.js")).unwrap();
|
||||
assert_eq!(
|
||||
vm.eval_after_selected_page_tasks("JSON.stringify(__uiEventResults.failed)")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "203");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy_constructor_materialization_keeps_captured_intrinsics_and_public_metadata() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://constructor-intrinsic.test/");
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"(() => {
|
||||
const reflectConstruct = Reflect.construct;
|
||||
const functionToString = Function.prototype.toString;
|
||||
Reflect.construct = () => { throw Error('author Reflect.construct'); };
|
||||
Reflect = null;
|
||||
Object.prototype.get = () => { throw Error('inherited get trap'); };
|
||||
Object.prototype.construct = () => { throw Error('inherited construct trap'); };
|
||||
const C = VideoColorSpace;
|
||||
const value = new C({matrix: 'rgb'});
|
||||
const descriptor = Object.getOwnPropertyDescriptor(C, 'prototype');
|
||||
return C.prototype.constructor === C && Object.getPrototypeOf(value) === C.prototype
|
||||
&& value.matrix === 'rgb' && C.length === 0 && C.name === 'VideoColorSpace'
|
||||
&& !descriptor.writable && !descriptor.enumerable && !descriptor.configurable
|
||||
&& JSON.stringify(Object.getOwnPropertyNames(C)) === '["length","name","prototype"]'
|
||||
&& functionToString.call(C).includes('[native code]')
|
||||
&& Object.getPrototypeOf(reflectConstruct(C, [])) === C.prototype;
|
||||
})()"#
|
||||
)
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ mod visual_viewport_receiver;
|
||||
mod writable_stream_native_receivers;
|
||||
mod dom_rect_factory_descriptors;
|
||||
mod offline_audio_float;
|
||||
mod constructor_entry;
|
||||
mod webtransport_stream_interface_exposure;
|
||||
|
||||
use super::post_parse::dynamic_script_execute_is_runnable_before_dom_content_loaded;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::document_runtime::DomHandle;
|
||||
use super::native_bridge::JsContextHost;
|
||||
pub use moli_v8_util::{
|
||||
array_contains_strict, array_push_value, call_object_method, callback_data_index_value,
|
||||
callable_relevant_context, array_contains_strict, array_push_value, call_object_method, callback_data_index_value,
|
||||
callback_data_item, constructor_object, constructor_prototype, constructor_prototype_object,
|
||||
define_non_enumerable_static_bool_property, define_non_enumerable_static_number_property,
|
||||
define_non_enumerable_static_property, define_non_enumerable_static_string_property,
|
||||
@@ -346,35 +346,6 @@ where
|
||||
serialize_v8_array(scope, values.as_slice())
|
||||
}
|
||||
|
||||
pub(crate) fn callable_relevant_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
mut callable: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
// GetFunctionRealm follows both bound targets and Proxy targets. A bound
|
||||
// Proxy's creation context need not be the underlying function's realm.
|
||||
// Read internal targets without invoking author-defined property traps.
|
||||
loop {
|
||||
if let Ok(proxy) = v8::Local::<v8::Proxy>::try_from(callable) {
|
||||
if proxy.is_revoked() {
|
||||
throw_type_error(scope, "Cannot determine the realm of a revoked Proxy");
|
||||
return None;
|
||||
}
|
||||
callable = proxy.get_target(scope);
|
||||
continue;
|
||||
}
|
||||
if let Ok(function) = v8::Local::<v8::Function>::try_from(callable) {
|
||||
let target = function.get_bound_function(scope);
|
||||
if !target.is_undefined() {
|
||||
callable = target;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return v8::Local::<v8::Object>::try_from(callable)
|
||||
.ok()?
|
||||
.get_creation_context(scope);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_target_realm_constructor_prototype<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
new_target: v8::Local<'s, v8::Value>,
|
||||
@@ -405,28 +376,6 @@ pub(crate) fn receiver_uses_new_target_realm_object_fallback<'s>(
|
||||
receiver_prototype.strict_equals(object_prototype.into())
|
||||
}
|
||||
|
||||
pub(crate) fn apply_webidl_constructor_prototype_fallback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
new_target: v8::Local<'s, v8::Value>,
|
||||
default_constructor_name: &str,
|
||||
) {
|
||||
// V8 has already read `NewTarget.prototype` while preparing the native
|
||||
// constructor receiver. Do not read it again: a Proxy getter must run only
|
||||
// once. V8 represents a non-object result with NewTarget's realm-local
|
||||
// Object prototype, which lets us replace precisely that fallback with
|
||||
// the WebIDL interface's default prototype from the same realm.
|
||||
if !receiver_uses_new_target_realm_object_fallback(scope, receiver, new_target) {
|
||||
return;
|
||||
}
|
||||
let Some(prototype) =
|
||||
new_target_realm_constructor_prototype(scope, new_target, default_constructor_name)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _ = receiver.set_prototype(scope, prototype.into());
|
||||
}
|
||||
|
||||
pub(super) fn create_script_origin_with_base_url<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
resource_name: &str,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::throw_type_error;
|
||||
|
||||
/// ECMAScript GetFunctionRealm follows bound and Proxy targets without calling
|
||||
/// author property traps. In particular, a bound Proxy's creation context need
|
||||
/// not be the underlying function's realm.
|
||||
pub fn callable_relevant_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
mut callable: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
loop {
|
||||
if let Ok(proxy) = v8::Local::<v8::Proxy>::try_from(callable) {
|
||||
if proxy.is_revoked() {
|
||||
throw_type_error(scope, "Cannot determine the realm of a revoked Proxy");
|
||||
return None;
|
||||
}
|
||||
callable = proxy.get_target(scope);
|
||||
continue;
|
||||
}
|
||||
if let Ok(function) = v8::Local::<v8::Function>::try_from(callable) {
|
||||
let target = function.get_bound_function(scope);
|
||||
if !target.is_undefined() {
|
||||
callable = target;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return v8::Local::<v8::Object>::try_from(callable)
|
||||
.ok()?
|
||||
.get_creation_context(scope);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod callables;
|
||||
mod callbacks;
|
||||
mod constructors;
|
||||
mod exceptions;
|
||||
@@ -9,6 +10,7 @@ mod properties;
|
||||
mod strings;
|
||||
mod symbols;
|
||||
|
||||
pub use callables::*;
|
||||
pub use callbacks::*;
|
||||
pub use constructors::*;
|
||||
pub use exceptions::*;
|
||||
|
||||
@@ -20,7 +20,14 @@ pub fn invoke_web_api_constructor<'s>(
|
||||
let receiver = args.this();
|
||||
let construct = args.is_construct_call();
|
||||
v8::tc_scope!(let scope, scope);
|
||||
let Some((args, deferred)) = crate::constructor::restore_constructor_new_target(scope, args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
callback(scope, args, rv);
|
||||
if deferred && scope.can_continue() {
|
||||
crate::constructor::clear_constructor_invocation(scope, receiver);
|
||||
}
|
||||
if scope.has_caught() {
|
||||
scope.rethrow();
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//! WebIDL argument conversion precedes the single Get(NewTarget, "prototype").
|
||||
//! V8 allocates API constructor receivers before entering their callbacks. A
|
||||
//! private NewTarget lets the native callback perform that Get at the specified
|
||||
//! point, without guessing from V8's already selected Object.prototype fallback.
|
||||
|
||||
use moli_v8_util::{
|
||||
callable_relevant_context, get_private_object, get_private_value, global_constructor_prototype,
|
||||
new_null_prototype_object, private_key, throw_type_error, v8str,
|
||||
};
|
||||
|
||||
const REFLECT_CONSTRUCT: &str = "moli.webidl.reflectConstruct";
|
||||
const INVOCATION: &str = "moli.webidl.constructorInvocation";
|
||||
const NEW_TARGET: &str = "moli.webidl.originalNewTarget";
|
||||
const SELECTED_PROTOTYPE: &str = "moli.webidl.selectedConstructorPrototype";
|
||||
|
||||
/// Capture before author script runs, including before lazy interface access.
|
||||
pub fn capture_web_api_constructor_intrinsics<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<()> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
if get_private_value(scope, global, REFLECT_CONSTRUCT).is_some() {
|
||||
return Some(());
|
||||
}
|
||||
let reflect = global.get(scope, v8str(scope, "Reflect").into())?;
|
||||
let reflect = v8::Local::<v8::Object>::try_from(reflect).ok()?;
|
||||
let construct = reflect.get(scope, v8str(scope, "construct").into())?;
|
||||
let construct = v8::Local::<v8::Function>::try_from(construct).ok()?;
|
||||
let key = private_key(scope, REFLECT_CONSTRUCT)?;
|
||||
global
|
||||
.set_private(scope, key, construct.into())?
|
||||
.then_some(())
|
||||
}
|
||||
|
||||
/// Keep the native function as the intrinsic constructor; publish this entry
|
||||
/// for JavaScript construction. Native callback data and instance templates are
|
||||
/// preserved by invoking the original function with a private NewTarget.
|
||||
pub fn web_api_constructor_with_deferred_prototype<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
constructor: v8::Local<'s, v8::Function>,
|
||||
) -> Option<v8::Local<'s, v8::Proxy>> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let construct = get_private_value(scope, global, REFLECT_CONSTRUCT)?;
|
||||
let trap = v8::Function::builder(construct_trap)
|
||||
.data(construct)
|
||||
.length(3)
|
||||
.build(scope)?;
|
||||
let handler = new_null_prototype_object(scope);
|
||||
handler
|
||||
.define_own_property(
|
||||
scope,
|
||||
v8str(scope, "construct").into(),
|
||||
trap.into(),
|
||||
v8::PropertyAttribute::NONE,
|
||||
)?
|
||||
.then_some(())?;
|
||||
v8::Proxy::new(scope, constructor.into(), handler)
|
||||
}
|
||||
|
||||
fn construct_trap<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
let construct =
|
||||
v8::Local::<v8::Function>::try_from(args.data()).expect("captured Reflect.construct");
|
||||
let Some(hidden_new_target) = v8::Function::new(scope, private_constructor) else {
|
||||
return;
|
||||
};
|
||||
let prototype = new_null_prototype_object(scope);
|
||||
if !hidden_new_target
|
||||
.define_own_property(
|
||||
scope,
|
||||
v8str(scope, "prototype").into(),
|
||||
prototype.into(),
|
||||
v8::PropertyAttribute::READ_ONLY
|
||||
| v8::PropertyAttribute::DONT_ENUM
|
||||
| v8::PropertyAttribute::DONT_DELETE,
|
||||
)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let invocation = new_null_prototype_object(scope);
|
||||
let Some(new_target_key) = private_key(scope, NEW_TARGET) else {
|
||||
return;
|
||||
};
|
||||
let Some(invocation_key) = private_key(scope, INVOCATION) else {
|
||||
return;
|
||||
};
|
||||
if !invocation
|
||||
.set_private(scope, new_target_key, args.get(2))
|
||||
.unwrap_or(false)
|
||||
|| !hidden_new_target
|
||||
.set_private(scope, invocation_key, invocation.into())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(result) = construct.call(
|
||||
scope,
|
||||
v8::undefined(scope).into(),
|
||||
&[args.get(0), args.get(1), hidden_new_target.into()],
|
||||
) {
|
||||
rv.set(result);
|
||||
}
|
||||
}
|
||||
|
||||
fn private_constructor(
|
||||
_scope: &mut v8::PinScope<'_, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'_>,
|
||||
_rv: v8::ReturnValue<'_>,
|
||||
) {
|
||||
}
|
||||
|
||||
pub(crate) fn restore_constructor_new_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
) -> Option<(v8::FunctionCallbackArguments<'s>, bool)> {
|
||||
if !args.is_construct_call() || args.new_target().is_proxy() {
|
||||
return Some((args, false));
|
||||
}
|
||||
let Ok(new_target) = v8::Local::<v8::Object>::try_from(args.new_target()) else {
|
||||
return Some((args, false));
|
||||
};
|
||||
let Some(invocation) = get_private_object(scope, new_target, INVOCATION) else {
|
||||
return Some((args, false));
|
||||
};
|
||||
let original = get_private_value(scope, invocation, NEW_TARGET)?;
|
||||
let key = private_key(scope, INVOCATION)?;
|
||||
args.this()
|
||||
.set_private(scope, key, invocation.into())?
|
||||
.then_some((args.with_new_target(original), true))
|
||||
}
|
||||
|
||||
pub(crate) fn clear_constructor_invocation(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
receiver: v8::Local<'_, v8::Object>,
|
||||
) {
|
||||
if let Some(key) = private_key(scope, INVOCATION) {
|
||||
let _ = receiver.delete_private(scope, key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Call after overload resolution and all WebIDL argument conversion, before
|
||||
/// constructor initialization. Internal construction through the cached native
|
||||
/// function retains its V8 receiver and does not inspect author properties.
|
||||
pub fn initialize_web_api_constructor_receiver<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
default_interface_name: &str,
|
||||
) -> bool {
|
||||
let Some(invocation) = get_private_object(scope, receiver, INVOCATION) else {
|
||||
return true;
|
||||
};
|
||||
if get_private_value(scope, invocation, SELECTED_PROTOTYPE).is_some() {
|
||||
return true;
|
||||
}
|
||||
let Some(new_target) = get_private_object(scope, invocation, NEW_TARGET) else {
|
||||
return false;
|
||||
};
|
||||
let Some(prototype) = new_target.get(scope, v8str(scope, "prototype").into()) else {
|
||||
return false;
|
||||
};
|
||||
let prototype = if let Ok(prototype) = v8::Local::<v8::Object>::try_from(prototype) {
|
||||
prototype
|
||||
} else {
|
||||
let Some(context) = callable_relevant_context(scope, new_target.into()) else {
|
||||
return false;
|
||||
};
|
||||
let prototype = {
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let Some(prototype) = global_constructor_prototype(scope, default_interface_name)
|
||||
else {
|
||||
throw_type_error(scope, "WebIDL constructor prototype is unavailable");
|
||||
return false;
|
||||
};
|
||||
v8::Global::new(scope, prototype)
|
||||
};
|
||||
v8::Local::new(scope, &prototype)
|
||||
};
|
||||
let Some(key) = private_key(scope, SELECTED_PROTOTYPE) else {
|
||||
return false;
|
||||
};
|
||||
receiver
|
||||
.set_prototype(scope, prototype.into())
|
||||
.unwrap_or(false)
|
||||
&& invocation
|
||||
.set_private(scope, key, prototype.into())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -257,6 +257,7 @@ extern crate self as moli_webapi_declare;
|
||||
mod brand;
|
||||
mod callback;
|
||||
mod cross_origin;
|
||||
mod constructor;
|
||||
mod declaration;
|
||||
mod error;
|
||||
mod interface;
|
||||
@@ -266,6 +267,10 @@ mod value;
|
||||
|
||||
pub mod __private;
|
||||
|
||||
pub use constructor::{
|
||||
capture_web_api_constructor_intrinsics, initialize_web_api_constructor_receiver,
|
||||
web_api_constructor_with_deferred_prototype,
|
||||
};
|
||||
pub use interface::WebApiInterfaceDescriptor;
|
||||
pub use moli_webapi_declare_derive::{WebApiCrossOrigin, WebApiFunctionTemplate, WebApiObject};
|
||||
pub use v8;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
use std::pin::pin;
|
||||
|
||||
use moli_v8_test_util::ensure_v8;
|
||||
use moli_v8_util::{
|
||||
WebApiIntrinsicKind, WebApiIntrinsicLookup, get_private_object,
|
||||
install_web_api_intrinsic_resolver, set_private_value, v8str,
|
||||
};
|
||||
use moli_webapi_declare::{
|
||||
capture_web_api_constructor_intrinsics, initialize_web_api_constructor_receiver,
|
||||
web_api_constructor_with_deferred_prototype,
|
||||
};
|
||||
|
||||
moli_webapi_declare::declare_web_api_interfaces! {
|
||||
TestThing;
|
||||
}
|
||||
|
||||
fn intrinsic<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
name: &str,
|
||||
kind: WebApiIntrinsicKind,
|
||||
) -> WebApiIntrinsicLookup<'s> {
|
||||
if name != "TestThing" {
|
||||
return WebApiIntrinsicLookup::Unmanaged;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
WebApiIntrinsicLookup::Managed(get_private_object(
|
||||
scope,
|
||||
global,
|
||||
match kind {
|
||||
WebApiIntrinsicKind::Constructor => "test.constructor",
|
||||
WebApiIntrinsicKind::Prototype => "test.prototype",
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn install<'s>(scope: &mut v8::PinScope<'s, '_>) {
|
||||
capture_web_api_constructor_intrinsics(scope).unwrap();
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let template = v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
|
||||
TestThing,
|
||||
constructor
|
||||
))
|
||||
.data(v8str(scope, "native callback data").into())
|
||||
.length(2)
|
||||
.build(scope);
|
||||
template.set_class_name(v8str(scope, "TestThing"));
|
||||
let native = template.get_function(scope).unwrap();
|
||||
let prototype = native.get(scope, v8str(scope, "prototype").into()).unwrap();
|
||||
let prototype = v8::Local::<v8::Object>::try_from(prototype).unwrap();
|
||||
set_private_value(scope, global, "test.constructor", native.into());
|
||||
set_private_value(scope, global, "test.prototype", prototype.into());
|
||||
install_web_api_intrinsic_resolver(scope, intrinsic);
|
||||
let public = web_api_constructor_with_deferred_prototype(scope, native).unwrap();
|
||||
prototype
|
||||
.set(scope, v8str(scope, "constructor").into(), public.into())
|
||||
.unwrap();
|
||||
global
|
||||
.set(scope, v8str(scope, "TestThing").into(), public.into())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn constructor<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
assert!(args.is_construct_call());
|
||||
let Some(converted) = args.get(0).to_string(scope) else {
|
||||
return;
|
||||
};
|
||||
if !initialize_web_api_constructor_receiver(scope, args.this(), "TestThing") {
|
||||
return;
|
||||
}
|
||||
// A second native initialization must not invoke the author's getter twice.
|
||||
assert!(initialize_web_api_constructor_receiver(
|
||||
scope,
|
||||
args.this(),
|
||||
"TestThing"
|
||||
));
|
||||
for (name, value) in [
|
||||
("converted", converted.into()),
|
||||
("originalNewTarget", args.new_target()),
|
||||
("callbackData", args.data()),
|
||||
(
|
||||
"argumentCount",
|
||||
v8::Integer::new(scope, args.length()).into(),
|
||||
),
|
||||
] {
|
||||
args.this()
|
||||
.define_own_property(
|
||||
scope,
|
||||
v8str(scope, name).into(),
|
||||
value,
|
||||
v8::PropertyAttribute::NONE,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> {
|
||||
let source = v8::String::new(scope, source).unwrap();
|
||||
v8::Script::compile(scope, source, None)
|
||||
.unwrap()
|
||||
.run(scope)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_defers_prototype_get_and_preserves_native_callback_information() {
|
||||
ensure_v8();
|
||||
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);
|
||||
install(scope);
|
||||
assert!(
|
||||
eval(
|
||||
scope,
|
||||
r#"(() => {
|
||||
const log = [];
|
||||
const prototype = Object.prototype;
|
||||
const N = new Proxy(function() {}, {get(target, key, receiver) {
|
||||
if (key === 'prototype') { log.push('prototype'); return prototype; }
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const argument = {toString() { log.push('conversion'); return 'value'; }};
|
||||
const value = Reflect.construct(TestThing, [argument, 1, 2], N);
|
||||
if (log.join(',') !== 'conversion,prototype'
|
||||
|| Object.getPrototypeOf(value) !== prototype
|
||||
|| value.converted !== 'value' || value.originalNewTarget !== N
|
||||
|| value.callbackData !== 'native callback data' || value.argumentCount !== 3
|
||||
|| TestThing.length !== 2 || TestThing.name !== 'TestThing'
|
||||
|| TestThing.prototype.constructor !== TestThing) return false;
|
||||
log.length = 0;
|
||||
const sentinel = {};
|
||||
try { Reflect.construct(TestThing, [{toString() { throw sentinel; }}], N); }
|
||||
catch (error) { if (error !== sentinel) return false; }
|
||||
if (log.length !== 0) return false;
|
||||
const badN = new Proxy(function() {}, {get(target, key) {
|
||||
if (key === 'prototype') throw sentinel;
|
||||
return Reflect.get(target, key);
|
||||
}});
|
||||
try { Reflect.construct(TestThing, [argument], badN); return false; }
|
||||
catch (error) { if (error !== sentinel) return false; }
|
||||
const nativeConstruct = Reflect.construct;
|
||||
Reflect.construct = () => { throw Error('public Reflect'); };
|
||||
Object.prototype.get = () => { throw Error('inherited trap'); };
|
||||
Object.prototype.construct = () => { throw Error('inherited construct'); };
|
||||
const C = TestThing;
|
||||
TestThing = {};
|
||||
const ordinary = new C('normal');
|
||||
return Object.getPrototypeOf(ordinary) === C.prototype
|
||||
&& ordinary.originalNewTarget === C
|
||||
&& nativeConstruct(C, ['again']).converted === 'again';
|
||||
})()"#,
|
||||
)
|
||||
.is_true()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_uses_new_target_realm_and_keeps_reentrant_invocations_independent() {
|
||||
ensure_v8();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let first = v8::Context::new(scope, Default::default());
|
||||
let second = v8::Context::new(scope, Default::default());
|
||||
let token = v8str(scope, "same-origin constructor test");
|
||||
for context in [first, second] {
|
||||
context.set_security_token(token.into());
|
||||
install(&mut v8::ContextScope::new(scope, context));
|
||||
}
|
||||
let foreign = second.global(scope);
|
||||
let scope = &mut v8::ContextScope::new(scope, first);
|
||||
first
|
||||
.global(scope)
|
||||
.set(scope, v8str(scope, "other").into(), foreign.into())
|
||||
.unwrap();
|
||||
assert!(
|
||||
eval(
|
||||
scope,
|
||||
r#"(() => {
|
||||
const foreignPrototype = other.TestThing.prototype;
|
||||
const foreignTarget = other.Function();
|
||||
foreignTarget.prototype = 7;
|
||||
const bound = foreignTarget.bind(null);
|
||||
const proxiedBound = new Proxy(bound, {});
|
||||
const boundProxy = new Proxy(foreignTarget, {}).bind(null);
|
||||
for (const N of [foreignTarget, bound, proxiedBound, boundProxy]) {
|
||||
const value = Reflect.construct(TestThing, ['x'], N);
|
||||
if (Object.getPrototypeOf(value) !== foreignPrototype
|
||||
|| value.originalNewTarget !== N) return false;
|
||||
}
|
||||
const C = TestThing;
|
||||
other.TestThing = {prototype: {poisoned: true}};
|
||||
if (Object.getPrototypeOf(Reflect.construct(C, ['x'], foreignTarget))
|
||||
!== foreignPrototype) return false;
|
||||
let reads = 0, nested;
|
||||
const prototype = Object.create(null);
|
||||
const N = new Proxy(function() {}, {get(target, key, receiver) {
|
||||
if (key === 'prototype') {
|
||||
if (++reads === 1) nested = Reflect.construct(C, ['nested'], N);
|
||||
return prototype;
|
||||
}
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
const value = Reflect.construct(C, ['outer'], N);
|
||||
if (reads !== 2 || Object.getPrototypeOf(value) !== prototype
|
||||
|| Object.getPrototypeOf(nested) !== prototype
|
||||
|| value.converted !== 'outer' || nested.converted !== 'nested') return false;
|
||||
for (const primitive of [true, false]) {
|
||||
let revoke;
|
||||
const pair = Proxy.revocable(function() {}, {get(target, key, receiver) {
|
||||
if (key === 'prototype') { revoke(); return primitive ? 1 : prototype; }
|
||||
return Reflect.get(target, key, receiver);
|
||||
}});
|
||||
revoke = pair.revoke;
|
||||
try {
|
||||
const instance = Reflect.construct(C, ['x'], pair.proxy);
|
||||
if (primitive || Object.getPrototypeOf(instance) !== prototype) return false;
|
||||
} catch (error) { if (!primitive || !(error instanceof TypeError)) return false; }
|
||||
}
|
||||
return true;
|
||||
})()"#,
|
||||
)
|
||||
.is_true()
|
||||
);
|
||||
}
|
||||
Vendored
+13
-1
@@ -385,6 +385,7 @@ pub struct FunctionCallbackArguments<'s> {
|
||||
info: &'s FunctionCallbackInfo,
|
||||
data: Option<Local<'s, Value>>,
|
||||
length: Option<int>,
|
||||
new_target: Option<Local<'s, Value>>,
|
||||
}
|
||||
|
||||
impl<'s> FunctionCallbackArguments<'s> {
|
||||
@@ -394,6 +395,7 @@ impl<'s> FunctionCallbackArguments<'s> {
|
||||
info,
|
||||
data: None,
|
||||
length: None,
|
||||
new_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +410,7 @@ impl<'s> FunctionCallbackArguments<'s> {
|
||||
info,
|
||||
data: Some(parts.data),
|
||||
length: Some(parts.length),
|
||||
new_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +428,16 @@ impl<'s> FunctionCallbackArguments<'s> {
|
||||
/// For construct calls, this returns the "new.target" value.
|
||||
#[inline(always)]
|
||||
pub fn new_target(&self) -> Local<'s, Value> {
|
||||
self.info.new_target()
|
||||
self.new_target.unwrap_or_else(|| self.info.new_target())
|
||||
}
|
||||
|
||||
/// Restores the caller's NewTarget when a native binding deferred receiver
|
||||
/// allocation through an internal constructor. Other callback information,
|
||||
/// including whether this is a construct call, is preserved.
|
||||
#[inline(always)]
|
||||
pub fn with_new_target(mut self, new_target: Local<'s, Value>) -> Self {
|
||||
self.new_target = Some(new_target);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns true if this is a construct call, i.e., if the function was
|
||||
|
||||
Reference in New Issue
Block a user