feat(webapi): implement media and transport error values

This commit is contained in:
ldm0
2026-10-11 18:06:48 +08:00
parent 16ba5c733e
commit 5ffd83fedd
14 changed files with 797 additions and 24 deletions
+5 -2
View File
@@ -291,9 +291,12 @@ pub(crate) use self::canvas::{
};
use self::constructors::illegal_constructor_callback;
pub(crate) use self::constructors::{
dom_exception_clone_fields, ensure_dom_implementation_singleton, new_dom_error_value,
new_dom_exception_value, new_most_derived_dom_exception_value, new_quota_exceeded_error_value,
WebTransportErrorClonePayload, WebTransportErrorSource,
build_web_transport_error_from_clone_payload, dom_exception_clone_fields,
ensure_dom_implementation_singleton, new_dom_error_value, new_dom_exception_value,
new_most_derived_dom_exception_value, new_quota_exceeded_error_value,
quota_exceeded_error_clone_fields, throw_dom_exception_value,
web_transport_error_clone_payload_from_object,
};
pub(crate) use self::css_stylesheet_runtime::{
adopted_style_sheet_installations_from_value, bind_css_style_sheet_to_live_stylesheet,
@@ -311,6 +311,22 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>(
.length(1)
.build(scope)
}
ConstructorKind::OverconstrainedError => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::OverconstrainedError,
overconstrained_error_constructor_callback
))
.length(1)
.build(scope)
}
ConstructorKind::WebTransportError => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::WebTransportError,
web_transport_error_constructor_callback
))
.length(0)
.build(scope)
}
ConstructorKind::RtcErrorEvent => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::RTCErrorEvent,
@@ -33,12 +33,16 @@ pub(super) use document_nodes::{
document_fragment_constructor_callback, text_constructor_callback,
};
pub(crate) use dom_exception::{
dom_error_constructor_callback, dom_exception_clone_fields, dom_exception_constructor_callback,
initialize_websocket_error, install_dom_exception_template_bindings, new_dom_error_value,
new_dom_exception_value, new_most_derived_dom_exception_value, new_quota_exceeded_error_value,
new_websocket_error_value, quota_exceeded_error_clone_fields,
quota_exceeded_error_constructor_callback, rtc_error_constructor_callback,
throw_dom_exception_value, websocket_error_close_info,
WebTransportErrorClonePayload, WebTransportErrorSource,
build_web_transport_error_from_clone_payload, dom_error_constructor_callback,
dom_exception_clone_fields, dom_exception_constructor_callback, initialize_websocket_error,
install_dom_exception_template_bindings, new_dom_error_value, new_dom_exception_value,
new_most_derived_dom_exception_value, new_quota_exceeded_error_value,
new_websocket_error_value, overconstrained_error_constructor_callback,
quota_exceeded_error_clone_fields, quota_exceeded_error_constructor_callback,
rtc_error_constructor_callback, throw_dom_exception_value,
web_transport_error_clone_payload_from_object, web_transport_error_constructor_callback,
websocket_error_close_info,
};
pub(crate) use dom_implementation::ensure_dom_implementation_singleton;
pub(super) use dom_implementation::install_dom_implementation_template_bindings;
@@ -4,8 +4,16 @@ use crate::{util::get_private_value, webidl};
use moli_web_errors::{DOM_EXCEPTION_CONSTANTS, dom_exception_legacy_code};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject, WebApiTemplateValue};
mod overconstrained_error;
mod rtc_error;
mod web_transport_error;
pub(crate) use overconstrained_error::overconstrained_error_constructor_callback;
pub(crate) use rtc_error::rtc_error_constructor_callback;
pub(crate) use web_transport_error::{
WebTransportErrorClonePayload, WebTransportErrorSource,
build_web_transport_error_from_clone_payload, web_transport_error_clone_payload_from_object,
web_transport_error_constructor_callback,
};
const DOM_EXCEPTION_MESSAGE_SLOT: &str = "__lmDomExceptionMessage";
const DOM_EXCEPTION_NAME_SLOT: &str = "__lmDomExceptionName";
@@ -191,6 +199,8 @@ pub(crate) fn install_dom_exception_template_bindings<'s>(
);
}
"RTCError" => rtc_error::install(scope, prototype),
"OverconstrainedError" => overconstrained_error::install(scope, prototype),
"WebTransportError" => web_transport_error::install(scope, prototype),
_ => {}
}
}
@@ -0,0 +1,86 @@
use super::*;
use moli_webapi_declare::initialize_web_api_constructor_receiver;
const CONSTRAINT_SLOT: &str = "__moliOverconstrainedErrorConstraint";
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "OverconstrainedError")]
struct ConstructorArgs {
#[webidl(required, converter = "raw")]
constraint: webidl::DomString16,
#[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")]
message: webidl::DomString16,
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::OverconstrainedError)]
struct ObjectDeclaration<'s> {
#[webapi(slot = CONSTRAINT_SLOT)]
constraint: v8::Local<'s, v8::String>,
#[webapi(slot = DOM_EXCEPTION_MESSAGE_SLOT)]
message: v8::Local<'s, v8::String>,
#[webapi(slot = DOM_EXCEPTION_NAME_SLOT, init = string("OverconstrainedError"))]
name: (),
#[webapi(slot = DOM_EXCEPTION_CODE_SLOT, init = 0)]
code: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::OverconstrainedError, receiver, enumerable)]
struct PrototypeDeclaration {
#[webapi(accessor_property, getter = constraint_getter)]
constraint: (),
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
prototype: v8::Local<'s, v8::ObjectTemplate>,
) {
PrototypeDeclaration::initialize_prototype_template(scope, prototype);
}
pub(crate) fn overconstrained_error_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if !args.is_construct_call() {
throw_type_error(scope, "OverconstrainedError constructor requires 'new'.");
return;
}
let Some(parsed) = webidl::parse_args::<ConstructorArgs>(scope, &args) else {
return;
};
if !initialize_web_api_constructor_receiver(scope, args.this(), "OverconstrainedError") {
return;
}
let Some(constraint) =
v8::String::new_from_two_byte(scope, &parsed.constraint.0, v8::NewStringType::Normal)
else {
return;
};
let Some(message) =
v8::String::new_from_two_byte(scope, &parsed.message.0, v8::NewStringType::Normal)
else {
return;
};
if ObjectDeclaration::new(constraint, message)
.initialize(scope, args.this())
.is_ok()
{
capture_dom_exception_stack(scope, args.this());
rv.set(args.this().into());
}
}
fn constraint_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = moli_webapi_declare::web_api_object_target(scope, args.this())
.expect("OverconstrainedError receiver was validated");
if let Some(value) = get_private_value(scope, receiver, CONSTRAINT_SLOT) {
rv.set(value);
}
}
@@ -0,0 +1,185 @@
use super::*;
use moli_webapi_declare::initialize_web_api_constructor_receiver;
use webidl::WebIdlEnum;
const SOURCE_SLOT: &str = "__moliWebTransportErrorSource";
const STREAM_ERROR_CODE_SLOT: &str = "__moliWebTransportErrorStreamErrorCode";
#[derive(Clone, Copy, Debug, webidl::WebIdlEnum)]
#[webidl(name = "WebTransportErrorSource")]
pub(crate) enum WebTransportErrorSource {
Stream,
Session,
}
impl WebTransportErrorSource {
fn token(self) -> &'static str {
match self {
Self::Stream => "stream",
Self::Session => "session",
}
}
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "WebTransportErrorOptions")]
struct Options {
#[webidl(converter = "enum", default = WebTransportErrorSource::Stream)]
source: WebTransportErrorSource,
#[webidl(nullable, converter = "clamped_unsigned_long")]
stream_error_code: Option<u32>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "WebTransportError")]
struct ConstructorArgs {
#[webidl(default = webidl::DomString16(Vec::new()), converter = "raw")]
message: webidl::DomString16,
#[webidl(dictionary)]
options: Options,
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::WebTransportError)]
struct ObjectDeclaration<'s> {
#[webapi(slot = DOM_EXCEPTION_MESSAGE_SLOT)]
message: v8::Local<'s, v8::String>,
#[webapi(slot = DOM_EXCEPTION_NAME_SLOT, init = string("WebTransportError"))]
name: (),
#[webapi(slot = DOM_EXCEPTION_CODE_SLOT, init = 0)]
code: (),
#[webapi(slot = SOURCE_SLOT)]
source: v8::Local<'s, v8::String>,
#[webapi(slot = STREAM_ERROR_CODE_SLOT)]
stream_error_code: v8::Local<'s, v8::Value>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::WebTransportError, receiver, enumerable)]
struct PrototypeDeclaration {
#[webapi(accessor_property, getter = slot_getter, data = v8str(scope, SOURCE_SLOT))]
source: (),
#[webapi(accessor_property, getter = slot_getter, data = v8str(scope, STREAM_ERROR_CODE_SLOT))]
stream_error_code: (),
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
prototype: v8::Local<'s, v8::ObjectTemplate>,
) {
PrototypeDeclaration::initialize_prototype_template(scope, prototype);
}
pub(crate) fn web_transport_error_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if !args.is_construct_call() {
throw_type_error(scope, "WebTransportError constructor requires 'new'.");
return;
}
let Some(parsed) = webidl::parse_args::<ConstructorArgs>(scope, &args) else {
return;
};
if !initialize_web_api_constructor_receiver(scope, args.this(), "WebTransportError") {
return;
}
let payload = WebTransportErrorClonePayload {
message: parsed.message.0,
source: parsed.options.source,
stream_error_code: parsed.options.stream_error_code,
};
let Some(declaration) = declaration(scope, &payload) else {
return;
};
if declaration.initialize(scope, args.this()).is_ok() {
capture_dom_exception_stack(scope, args.this());
rv.set(args.this().into());
}
}
fn slot_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let slot = args.data().to_rust_string_lossy(scope);
let receiver = moli_webapi_declare::web_api_object_target(scope, args.this())
.expect("WebTransportError receiver was validated");
if let Some(value) = get_private_value(scope, receiver, &slot) {
rv.set(value);
}
}
#[derive(Clone, Debug)]
pub(crate) struct WebTransportErrorClonePayload {
pub(crate) message: Vec<u16>,
pub(crate) source: WebTransportErrorSource,
pub(crate) stream_error_code: Option<u32>,
}
pub(crate) fn web_transport_error_clone_payload_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<WebTransportErrorClonePayload> {
web_api_interfaces::WebTransportError::is_instance(scope, object).then_some(())?;
let object = moli_webapi_declare::web_api_object_target(scope, object)?;
let message = get_private_value(scope, object, DOM_EXCEPTION_MESSAGE_SLOT)?;
let message = webidl::convert::<webidl::DomString16>(
scope,
message,
webidl::Context::member("WebTransportError", "message"),
)
.ok()?
.0;
let source = get_private_value(scope, object, SOURCE_SLOT)?.to_rust_string_lossy(scope);
let source = WebTransportErrorSource::parse_token(&source)?;
let code = get_private_value(scope, object, STREAM_ERROR_CODE_SLOT)?;
let stream_error_code = if code.is_null() {
None
} else {
Some(v8::Local::<v8::Number>::try_from(code).ok()?.value() as u32)
};
Some(WebTransportErrorClonePayload {
message,
source,
stream_error_code,
})
}
fn declaration<'s>(
scope: &mut v8::PinScope<'s, '_>,
payload: &WebTransportErrorClonePayload,
) -> Option<ObjectDeclaration<'s>> {
let message =
v8::String::new_from_two_byte(scope, &payload.message, v8::NewStringType::Normal)?;
let source = v8str(scope, payload.source.token());
let code = nullable_double_slot_value(scope, payload.stream_error_code.map(f64::from));
Some(ObjectDeclaration::new(message, source, code))
}
pub(crate) fn build_web_transport_error_from_clone_payload<'s>(
scope: &mut v8::PinScope<'s, '_>,
payload: &WebTransportErrorClonePayload,
) -> Option<v8::Local<'s, v8::Object>> {
if !crate::context_bootstrap::exposed_interfaces::is_realm_interface_exposed(
scope,
"WebTransportError",
) {
throw_dom_exception_value(
scope,
"WebTransportError is not exposed in this realm.",
"DataCloneError",
);
return None;
}
crate::context_bootstrap::exposed_interfaces::ensure_intrinsic_interface_prototype(
scope,
"WebTransportError",
)
.ok()?;
let object = declaration(scope, payload)?.bind(scope).ok()?;
capture_dom_exception_stack(scope, object);
Some(object)
}
@@ -3,18 +3,20 @@ use crate::{
context_bootstrap::{
FileSystemHandleDurablePayload, build_file_list_object,
build_file_system_handle_from_durable_payload, build_geometry_object_from_clone_payload,
file_list_files_from_object, file_system_handle_clone_payload_from_object,
build_web_transport_error_from_clone_payload, file_list_files_from_object,
file_system_handle_clone_payload_from_object,
file_system_handle_durable_payload_from_object, geometry_clone_payload_from_object,
image_data_clone_payload_from_object,
image_data_clone_payload_from_object, web_transport_error_clone_payload_from_object,
},
dom::native::SelectedFile,
structured_clone::{
BlobClonePayload, HOST_OBJECT_TAG_BLOB, HOST_OBJECT_TAG_CRYPTO_KEY,
HOST_OBJECT_TAG_FILE_LIST, HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE, HOST_OBJECT_TAG_GEOMETRY,
HOST_OBJECT_TAG_IMAGE_DATA, blob_clone_payload_from_object,
build_blob_object_from_clone_payload, read_crypto_key_payload, read_geometry_clone_payload,
read_image_data_payload, write_crypto_key_payload, write_geometry_clone_payload,
write_image_data_payload,
HOST_OBJECT_TAG_IMAGE_DATA, HOST_OBJECT_TAG_WEB_TRANSPORT_ERROR,
blob_clone_payload_from_object, build_blob_object_from_clone_payload,
read_crypto_key_payload, read_geometry_clone_payload, read_image_data_payload,
read_web_transport_error_payload, write_crypto_key_payload, write_geometry_clone_payload,
write_image_data_payload, write_web_transport_error_payload,
},
};
use moli_indexeddb::{IndexedDbFileSystemHandleBucket, IndexedDbFileSystemHandleKind};
@@ -99,6 +101,13 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer {
serializer: &dyn v8::ValueSerializerHelper,
) -> Option<bool> {
match moli_webapi_declare::web_api_object_type(scope, object).map(|kind| kind.name()) {
Some("WebTransportError") => {
if let Some(payload) = web_transport_error_clone_payload_from_object(scope, object)
{
write_web_transport_error_payload(serializer, &payload);
return Some(true);
}
}
Some("EncodedVideoChunk") => {
let exception = dom_exception_value(
scope,
@@ -289,6 +298,10 @@ impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer {
return None;
}
match tag {
HOST_OBJECT_TAG_WEB_TRANSPORT_ERROR => {
let payload = read_web_transport_error_payload(deserializer)?;
build_web_transport_error_from_clone_payload(scope, &payload)
}
HOST_OBJECT_TAG_IMAGE_DATA => read_image_data_payload(scope, deserializer),
HOST_OBJECT_TAG_CRYPTO_KEY => {
read_crypto_key_payload(scope, deserializer).or_else(|| {
@@ -146,7 +146,7 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
},
ConstructorSpec {
interface: web_api_interfaces::OverconstrainedError::DESCRIPTOR,
kind: ConstructorKind::Shell { length: 1 },
kind: ConstructorKind::OverconstrainedError,
},
ConstructorSpec {
interface: web_api_interfaces::PictureInPictureEvent::DESCRIPTOR,
@@ -314,7 +314,7 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
},
ConstructorSpec {
interface: web_api_interfaces::WebTransportError::DESCRIPTOR,
kind: ConstructorKind::Shell { length: 0 },
kind: ConstructorKind::WebTransportError,
},
ConstructorSpec {
interface: web_api_interfaces::External::DESCRIPTOR,
@@ -57,6 +57,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
HashChangeEvent,
PageTransitionEvent,
DomException,
OverconstrainedError,
WebTransportError,
RtcError,
RtcErrorEvent,
DomError,
@@ -104,6 +104,7 @@ mod trusted_types;
mod trusted_types_attributes;
mod ui_event_init;
mod user_activation;
mod value_errors;
mod value_events;
mod web_audio;
mod web_mcp;
@@ -0,0 +1,220 @@
(() => {
const checks = [];
const check = (name, action) => {
try { checks.push({name, passed: action() === true}); }
catch (error) { checks.push({name, passed: false, error: String(error)}); }
};
const throws = (action, realm, expected) => {
try { action(); } catch (error) {
return expected === undefined ? Object.getPrototypeOf(error) === realm.TypeError.prototype : error === expected;
}
return false;
};
const utf16 = 'a\u0000\ud800b\udfff\ud83d\ude00';
function run(realm, label) {
const test = (name, action) => check(label + ': ' + name, action);
const WT = realm.WebTransportError;
const OC = realm.OverconstrainedError;
const createWT = (message, options = {}) => new WT(message, options);
test('WebTransportError inheritance and defaults', () => {
const e = new WT();
return e instanceof WT && e instanceof realm.DOMException && e instanceof realm.Error &&
Object.getPrototypeOf(WT.prototype) === realm.DOMException.prototype && e.name === 'WebTransportError' &&
e.message === '' && e.code === 0 && e.source === 'stream' && e.streamErrorCode === null;
});
test('WebTransportError length and tag', () => WT.length === 0 &&
Object.prototype.toString.call(new WT()) === '[object WebTransportError]');
test('WebTransportError new before conversion', () => {
let reads = 0;
return throws(() => WT({toString() { reads++; return 'x'; }}), realm) && reads === 0;
});
for (const [index, [value, expected]] of [[undefined, ''], [null, 'null'], [true, 'true'],
[0, '0'], [-0, '0'], [1n, '1'], [utf16, utf16]].entries()) {
test('WebTransportError message ' + index + ': ' + String(value), () => createWT(value).message === expected);
}
test('WebTransportError Symbol message before dictionary', () => {
let reads = 0;
return throws(() => new WT(Symbol(), {get source() {reads++; return 'stream';}}), realm) && reads === 0;
});
for (const options of [undefined, null, {}, []]) {
test('WebTransportError empty options ' + String(options), () => {
const e = new WT(utf16, options);
return e.message === utf16 && e.source === 'stream' && e.streamErrorCode === null;
});
}
for (const options of [false, 0, '', 1n, Symbol('dictionary')]) {
test('WebTransportError invalid dictionary ' + String(options), () => throws(() => new WT('', options), realm));
}
for (const source of ['stream', 'session']) {
test('WebTransportError source ' + source, () => createWT(utf16, {source, streamErrorCode: 42}).source === source);
}
for (const source of [null, '', 'STREAM', 'other', 0, Symbol('source')]) {
test('WebTransportError rejects source ' + String(source), () => {
let reads = 0;
return throws(() => createWT('', {source, get streamErrorCode() {reads++; return 1;}}), realm) && reads === 0;
});
}
test('WebTransportError undefined source default', () => createWT('', {source: undefined}).source === 'stream');
const codes = [[undefined, null], [null, null], [NaN, 0], [Infinity, 4294967295], [-Infinity, 0],
[-4294967297, 0], [-1, 0], [-0, 0], [0, 0], [0.5, 0], [1.5, 2], [2.5, 2], [3.5, 4],
[42.4, 42], [42.6, 43], [65536, 65536], [4294967294.5, 4294967294],
[4294967295, 4294967295], [4294967295.5, 4294967295], [4294967296, 4294967295],
[Number.MAX_VALUE, 4294967295], [false, 0], [true, 1], ['3.5', 4], ['', 0], ['x', 0]];
for (const [index, [value, expected]] of codes.entries()) {
test('WebTransportError clamp ' + index + ': ' + String(value), () => Object.is(createWT('', {streamErrorCode: value}).streamErrorCode, expected));
}
for (const value of [1n, Symbol('code')]) {
test('WebTransportError rejects code ' + String(value), () => throws(() => createWT('', {streamErrorCode: value}), realm));
}
test('WebTransportError one numeric conversion', () => {
let reads = 0;
const e = createWT('', {streamErrorCode: {[Symbol.toPrimitive](hint) {reads++; if(hint !== 'number') throw 42; return 2.5;}}});
return e.streamErrorCode === 2 && reads === 1;
});
const sentinel = {};
for (const member of ['message', 'source', 'streamErrorCode']) {
test('WebTransportError exception ' + member, () => {
const order = [];
const message = {toString() {order.push('message'); if(member === 'message') throw sentinel; return utf16;}};
const options = new Proxy({}, {get(target, name) {order.push(name); if(name === member) throw sentinel; return undefined;}});
const expected = ['message', 'source', 'streamErrorCode'].slice(0, ['message', 'source', 'streamErrorCode'].indexOf(member)+1);
return throws(() => new WT(message, options), realm, sentinel) && JSON.stringify(order) === JSON.stringify(expected);
});
}
test('WebTransportError conversion order', () => {
const order = [];
const e = new WT({toString() {order.push('message'); return utf16;}}, new Proxy({}, {get(target, name) {
order.push(name); return name === 'source' ? {toString() {order.push('source string'); return 'session';}} : {valueOf() {order.push('code number'); return 4.5;}};
}}));
return e.message === utf16 && e.source === 'session' && e.streamErrorCode === 4 &&
JSON.stringify(order) === JSON.stringify(['message', 'source', 'source string', 'streamErrorCode', 'code number']);
});
test('WebTransportError null options avoid prototype getters', () => {
let reads = 0;
Object.defineProperty(realm.Object.prototype, 'source', {configurable:true, get() {reads++; throw sentinel;}});
try { return new WT('', null).source === 'stream' && new WT('', undefined).source === 'stream' && reads === 0; }
finally { delete realm.Object.prototype.source; }
});
for (const name of ['source', 'streamErrorCode']) {
test('WebTransportError descriptor ' + name, () => {
const d = Object.getOwnPropertyDescriptor(WT.prototype, name);
const e = createWT('', {streamErrorCode:42});
return d.get.length === 0 && d.get.name === 'get ' + name && d.set === undefined && d.enumerable && d.configurable &&
!Object.hasOwn(e, name) && !Reflect.set(e, name, 99);
});
for (const kind of ['plain', 'prototype', 'inherit', 'author proxy', 'revoked proxy', 'other error']) {
test('WebTransportError receiver ' + name + ' ' + kind, () => {
const e = createWT(); let traps = 0;
const proxy = new Proxy(e, {get() {traps++; throw sentinel;}, getPrototypeOf() {traps++; throw sentinel;}});
const revoked = Proxy.revocable(e, {}); revoked.revoke();
const bad = {'plain':{}, 'prototype':WT.prototype, 'inherit':Object.create(e), 'author proxy':proxy,
'revoked proxy':revoked.proxy, 'other error':new realm.DOMException()}[kind];
return throws(() => Object.getOwnPropertyDescriptor(WT.prototype, name).get.call(bad), realm) && traps === 0;
});
}
}
test('WebTransportError brand survives prototype removal', () => {
const e = createWT(utf16, {source:'session', streamErrorCode:3});
Object.setPrototypeOf(e, null);
return Object.getOwnPropertyDescriptor(WT.prototype, 'source').get.call(e) === 'session' &&
Object.getOwnPropertyDescriptor(realm.DOMException.prototype, 'message').get.call(e) === utf16;
});
test('WebTransportError subclass and Reflect.construct', () => {
class Derived extends WT {}
const e = new Derived(utf16, {streamErrorCode:1.5});
function NewTarget() {}
const other = Reflect.construct(WT, [utf16, {source:'session'}], NewTarget);
return e instanceof Derived && e.streamErrorCode === 2 && Object.getPrototypeOf(other) === NewTarget.prototype &&
Object.getOwnPropertyDescriptor(WT.prototype, 'source').get.call(other) === 'session';
});
for (const source of ['stream', 'session']) for (const code of [null, 0, 42, 4294967295]) {
test('WebTransportError structured clone ' + source + ' ' + code, () => {
const e = createWT(utf16, {source, streamErrorCode:code});
const sentinel = {};
for (const name of ['name','message','source','streamErrorCode']) Object.defineProperty(e, name, {enumerable:true, get() {throw sentinel;}});
const cloned = realm.structuredClone({a:e, b:e});
return cloned.a === cloned.b && cloned.a !== e && Object.getPrototypeOf(cloned.a) === realm.WebTransportError.prototype &&
cloned.a.name === 'WebTransportError' && cloned.a.message === utf16 && cloned.a.source === source &&
cloned.a.streamErrorCode === code && cloned.a.code === 0 && !Object.hasOwn(cloned.a, 'name');
});
}
test('WebTransportError author Proxy clone rejects without traps', () => {
let traps = 0, error;
const proxy = new Proxy(createWT(), {ownKeys() {traps++; throw sentinel;}, get() {traps++; throw sentinel;}});
try { realm.structuredClone(proxy); } catch(caught) {error = caught;}
return error instanceof realm.DOMException && error.name === 'DataCloneError' && traps === 0;
});
test('OverconstrainedError Window exposure', () => typeof OC === (typeof document === 'undefined' ? 'undefined' : 'function'));
if (typeof document !== 'undefined') {
test('OverconstrainedError inheritance and defaults', () => {
const e = new OC('width');
return e instanceof OC && e instanceof realm.DOMException && e instanceof realm.Error &&
Object.getPrototypeOf(OC.prototype) === realm.DOMException.prototype && e.name === 'OverconstrainedError' &&
e.constraint === 'width' && e.message === '' && e.code === 0;
});
test('OverconstrainedError length and tag', () => OC.length === 1 && Object.prototype.toString.call(new OC('')) === '[object OverconstrainedError]');
test('OverconstrainedError missing arity', () => throws(() => new OC(), realm));
test('OverconstrainedError new before conversion', () => {
let reads = 0; return throws(() => OC({toString(){reads++; return '';}}), realm) && reads === 0;
});
for (const [value, expected] of [[undefined,'undefined'], [null,'null'], [true,'true'], [0,'0'], [1n,'1'], [utf16,utf16]]) {
test('OverconstrainedError constraint ' + String(value), () => new OC(value).constraint === expected);
}
for (const [value, expected] of [[undefined,''], [null,'null'], [true,'true'], [0,'0'], [1n,'1'], [utf16,utf16]]) {
test('OverconstrainedError message ' + String(value), () => new OC(utf16,value).message === expected);
}
test('OverconstrainedError conversion order', () => {
const order=[]; const e=new OC({toString(){order.push('constraint');return utf16;}},{toString(){order.push('message');return utf16;}});
return e.constraint === utf16 && e.message === utf16 && JSON.stringify(order) === '["constraint","message"]';
});
test('OverconstrainedError Symbol constraint before message', () => {
let reads=0; return throws(() => new OC(Symbol(), {toString(){reads++;return '';}}),realm) && reads===0;
});
test('OverconstrainedError Symbol message', () => throws(() => new OC('',Symbol()),realm));
for(const member of ['constraint','message']) test('OverconstrainedError exception '+member, () => {
let reads=0;
return throws(() => new OC({toString(){reads++;if(member==='constraint')throw sentinel;return utf16;}},
{toString(){reads++;throw sentinel;}}),realm,sentinel) && reads===(member==='constraint'?1:2);
});
test('OverconstrainedError descriptor', () => {
const d=Object.getOwnPropertyDescriptor(OC.prototype,'constraint');const e=new OC(utf16);
return d.get.name === 'get constraint' && d.get.length === 0 && d.set === undefined && d.enumerable && d.configurable &&
!Object.hasOwn(e,'constraint') && !Reflect.set(e,'constraint','changed');
});
for(const kind of ['plain','prototype','inherit','author proxy','revoked proxy','other error']) test('OverconstrainedError receiver '+kind,() => {
const e=new OC(utf16);let traps=0;
const proxy=new Proxy(e,{get(){traps++;throw sentinel;},getPrototypeOf(){traps++;throw sentinel;}});
const revoked=Proxy.revocable(e,{});revoked.revoke();
const bad={'plain':{},'prototype':OC.prototype,'inherit':Object.create(e),'author proxy':proxy,'revoked proxy':revoked.proxy,'other error':new realm.DOMException()}[kind];
return throws(()=>Object.getOwnPropertyDescriptor(OC.prototype,'constraint').get.call(bad),realm)&&traps===0;
});
test('OverconstrainedError subclass and native brand', () => {
class Derived extends OC {} const e=new Derived(utf16,utf16); Object.setPrototypeOf(e,null);
return Object.getOwnPropertyDescriptor(OC.prototype,'constraint').get.call(e)===utf16 &&
Object.getOwnPropertyDescriptor(realm.DOMException.prototype,'message').get.call(e)===utf16;
});
test('OverconstrainedError nonserializable subclass', () => {
let error; try {realm.structuredClone(new OC('width'));}catch(caught){error=caught;}
return error instanceof realm.DOMException && error.name==='DataCloneError';
});
}
}
if (typeof document === 'undefined') run(globalThis, 'worker');
else {
run(globalThis, 'main');
run(document.querySelector('iframe').contentWindow, 'iframe');
check('cross realm error getters and clone', () => {
const child=document.querySelector('iframe').contentWindow;
const wt=new child.WebTransportError(utf16,{source:'session',streamErrorCode:4.5});
const oc=new child.OverconstrainedError(utf16,utf16);
return Object.getOwnPropertyDescriptor(WebTransportError.prototype,'streamErrorCode').get.call(wt)===4 &&
Object.getOwnPropertyDescriptor(OverconstrainedError.prototype,'constraint').get.call(oc)===utf16 &&
Object.getOwnPropertyDescriptor(DOMException.prototype,'message').get.call(wt)===utf16 &&
Object.getPrototypeOf(structuredClone(wt))===WebTransportError.prototype &&
Object.getPrototypeOf(child.structuredClone(new WebTransportError()))===child.WebTransportError.prototype;
});
}
globalThis.__uiEventResults={complete:true,total:checks.length,passed:checks.filter(row=>row.passed).length,checks};
if(typeof document==='undefined') postMessage(globalThis.__uiEventResults);
return true;
})()
@@ -0,0 +1,125 @@
use super::*;
#[test]
fn media_and_transport_errors_preserve_native_values_and_conversion_order() {
let mut vm = new_storage_page_task_executor_test_vm("https://value-errors.test/");
vm.eval("document.body.innerHTML = '<iframe></iframe>'")
.unwrap();
vm.eval(include_str!("value_errors.js")).unwrap();
assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true");
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "239");
assert_eq!(
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))")
.unwrap(),
"[]"
);
}
#[test]
fn media_and_transport_error_getters_accept_native_proxies_and_reject_author_wrappers() {
let mut vm = new_storage_page_task_executor_test_vm("https://value-error-proxies.test/");
vm.eval(r#"document.body.innerHTML='<iframe></iframe>';
globalThis.childWindow=document.querySelector('iframe').contentWindow;
globalThis.errors=[new childWindow.OverconstrainedError('w\ud800','m\udfff'),
new childWindow.WebTransportError('m\udfff',{source:'session',streamErrorCode:4294967295})];"#).unwrap();
let context_ptr = &vm.page_default_context as *const _;
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
let global = scope.get_current_context().global(scope);
let key = crate::util::v8str(scope, "errors");
let errors =
v8::Local::<v8::Array>::try_from(global.get(scope, key.into()).unwrap()).unwrap();
let proxies = v8::Array::new(scope, 2);
for index in 0..2 {
let object =
v8::Local::<v8::Object>::try_from(errors.get_index(scope, index).unwrap()).unwrap();
let handler = crate::util::new_null_prototype_object(scope);
let proxy = v8::Proxy::new(scope, object, handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
assert!(crate::web_api_interfaces::DOMException::is_instance(
scope,
proxy.into()
));
assert_eq!(proxies.set_index(scope, index, proxy.into()), Some(true));
}
let key = crate::util::v8str(scope, "nativeProxies");
assert_eq!(
global.create_data_property(scope, key.into(), proxies.into()),
Some(true)
);
Ok(())
})
.unwrap();
assert_eq!(vm.eval(r#"(() => {
for(const realm of [window,childWindow]) {
for(let index=0;index<errors.length;index++) {
const original=errors[index],proxy=nativeProxies[index];
const members=index===0?['constraint']:['source','streamErrorCode'];
const interfaceName=index===0?'OverconstrainedError':'WebTransportError';
for(const name of [...members,'message','name','code']) {
const prototype=members.includes(name)?realm[interfaceName].prototype:realm.DOMException.prototype;
const getter=Object.getOwnPropertyDescriptor(prototype,name).get;
if(getter.call(proxy)!==getter.call(original))throw Error('native value identity');
let traps=0;
const author=new Proxy(proxy,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 43;}});
const revoked=Proxy.revocable(proxy,{});revoked.revoke();
for(const receiver of [author,revoked.proxy,Object.create(original)]) {
let error;try{getter.call(receiver);}catch(caught){error=caught;}
if(Object.getPrototypeOf(error)!==realm.TypeError.prototype||traps)throw Error('author wrapper and callee realm');
}
}
}
}
return true;
})()"#).unwrap(), "true");
}
#[test]
fn web_transport_error_deserialization_respects_native_realm_exposure() {
let mut source = new_storage_page_task_executor_test_vm("https://transport-error-source.test/");
source.eval("globalThis.value=new WebTransportError('message',{source:'session',streamErrorCode:42})").unwrap();
let context_ptr = &source.page_default_context as *const _;
let mut payload = None;
source
.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
let global = scope.get_current_context().global(scope);
let key = crate::util::v8str(scope, "value");
let value = global.get(scope, key.into()).unwrap();
payload = crate::structured_clone::serialize_for_wire_for_storage(scope, value);
Ok(())
})
.unwrap();
let payload = payload.expect("WebTransportError should serialize in its exposed realm");
drop(source);
let mut target =
new_storage_page_task_executor_test_vm("http://transport-error-insecure.test/");
assert_eq!(
target
.eval("isSecureContext+'|'+typeof WebTransportError")
.unwrap(),
"false|undefined"
);
let context_ptr = &target.page_default_context as *const _;
target
.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
let scope = std::pin::pin!(v8::TryCatch::new(scope));
let scope = &mut scope.init();
assert!(crate::structured_clone::deserialize_from_wire(scope, &payload).is_none());
let exception = scope
.exception()
.expect("unexposed interface should throw DataCloneError");
let exception = v8::Local::<v8::Object>::try_from(exception).unwrap();
assert!(crate::web_api_interfaces::DOMException::is_instance(
scope, exception
));
let key = crate::util::v8str(scope, "name");
assert_eq!(
exception
.get(scope, key.into())
.unwrap()
.to_rust_string_lossy(scope),
"DataCloneError"
);
Ok(())
})
.unwrap();
}
@@ -49,6 +49,50 @@ fn indexed_db_roundtrips_image_data_with_graph_identity() {
use super::*;
use moli_url::origin_ascii_serialization;
#[test]
fn indexed_db_roundtrips_web_transport_error_native_payloads_without_author_access() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-transport-error.test/");
vm.eval(r#"(() => {
globalThis.transportErrorStorageResult='pending';
const WT=WebTransportError,message='m\u0000\ud800\udfff';
const originals=[];
for(const source of ['stream','session'])for(const streamErrorCode of [null,0,42,4294967295]) {
const error=new WT(message,{source,streamErrorCode});
Object.setPrototypeOf(error,null);
for(const name of ['message','source','streamErrorCode','stack'])Object.defineProperty(error,name,{enumerable:true,get(){throw 42;}});
originals.push({error,alias:error,source,streamErrorCode});
}
const value={rows:originals};value.self=value;
const open=indexedDB.open('transport-errors-'+Math.random(),1);
const fail=event=>{transportErrorStorageResult='error:'+event.target.error?.name;};
open.onerror=fail;open.onupgradeneeded=()=>open.result.createObjectStore('values');
open.onsuccess=()=>{
const db=open.result,write=db.transaction('values','readwrite');
write.onerror=fail;write.objectStore('values').put(value,'value');
write.oncomplete=()=>{
const descriptor=Object.getOwnPropertyDescriptor(window,'WebTransportError');
Object.defineProperty(window,'WebTransportError',{configurable:true,get(){throw 43;}});
const read=db.transaction('values').objectStore('values').get('value');read.onerror=fail;
read.onsuccess=()=>{
try {
const clone=read.result;
const valid=clone.self===clone&&clone.rows.length===8&&clone.rows.every((row,index)=>
Object.getPrototypeOf(row.error)===WT.prototype&&row.error!==originals[index].error&&row.error===row.alias&&
row.error.name==='WebTransportError'&&row.error.message===message&&row.error.source===row.source&&
row.error.streamErrorCode===row.streamErrorCode&&row.error.code===0&&!Object.hasOwn(row.error,'message'));
transportErrorStorageResult=String(valid);
} finally {Object.defineProperty(window,'WebTransportError',descriptor);db.close();}
};
};
};
})()"#).expect("WebTransportError IndexedDB round trip should schedule");
assert_eq!(
vm.eval_after_selected_page_tasks("String(transportErrorStorageResult)")
.unwrap(),
"true"
);
}
#[test]
fn indexed_db_first_use_ignores_public_constructor_overrides() {
let fixture = include_str!("../../../tests/fixtures/indexeddb-first-use.js");
+72 -8
View File
@@ -11,17 +11,17 @@ use crate::{
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, EncodedVideoChunkClonePayload,
EncodedVideoChunkType, FileSystemFileSnapshotClonePayload, FileSystemHandleClonePayload,
GeometryClonePayload, ImageDataClonePayload, ReadableStreamClonePayload,
TransformStreamClonePayload, WritableStreamClonePayload,
attach_file_system_file_snapshot_clone_payload,
TransformStreamClonePayload, WebTransportErrorClonePayload, WebTransportErrorSource,
WritableStreamClonePayload, attach_file_system_file_snapshot_clone_payload,
build_encoded_video_chunk_from_clone_payload, build_file_list_object, build_file_object,
build_file_system_handle_from_clone_payload, build_geometry_object_from_clone_payload,
build_image_data_object_from_clone_payload, build_readable_stream_clone_shell,
build_transform_stream_clone_shell, build_writable_stream_clone_shell,
crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload,
detach_message_port_owner_for_transfer, detach_transferred_message_port,
dom_exception_clone_fields, encoded_video_chunk_clone_payload_from_object,
ensure_message_port_wrapper_for_id, file_list_files_from_object,
file_system_file_snapshot_clone_payload_from_object,
build_transform_stream_clone_shell, build_web_transport_error_from_clone_payload,
build_writable_stream_clone_shell, crypto_key_clone_payload_from_object,
crypto_key_object_from_clone_payload, detach_message_port_owner_for_transfer,
detach_transferred_message_port, dom_exception_clone_fields,
encoded_video_chunk_clone_payload_from_object, ensure_message_port_wrapper_for_id,
file_list_files_from_object, file_system_file_snapshot_clone_payload_from_object,
file_system_handle_clone_payload_from_object, geometry_clone_payload_from_object,
image_data_clone_payload_from_object, initialize_readable_stream_clone_shell,
initialize_transform_stream_clone_shell, initialize_writable_stream_clone_shell,
@@ -29,6 +29,7 @@ use crate::{
prepare_readable_stream_transfer, prepare_transform_stream_transfer,
prepare_writable_stream_transfer, quota_exceeded_error_clone_fields,
require_internal_stream_value, selected_file_from_object,
web_transport_error_clone_payload_from_object,
},
dom::native::SelectedFile,
types::MessagePortId,
@@ -51,6 +52,7 @@ const HOST_OBJECT_TAG_TRANSFORM_STREAM: u32 = 10;
pub(crate) const HOST_OBJECT_TAG_GEOMETRY: u32 = 11;
pub(crate) const HOST_OBJECT_TAG_FILE_LIST: u32 = 12;
const HOST_OBJECT_TAG_ENCODED_VIDEO_CHUNK: u32 = 13;
pub(crate) const HOST_OBJECT_TAG_WEB_TRANSPORT_ERROR: u32 = 14;
const GEOMETRY_KIND_DOM_POINT_READONLY: u32 = 0;
const GEOMETRY_KIND_DOM_POINT: u32 = 1;
@@ -576,6 +578,13 @@ impl v8::ValueSerializerImpl for WireSerializer {
return Some(true);
}
}
Some("WebTransportError") => {
if let Some(payload) = web_transport_error_clone_payload_from_object(scope, object)
{
write_web_transport_error_payload(serializer, &payload);
return Some(true);
}
}
Some("QuotaExceededError") => {
if let Some((message, quota, requested)) =
quota_exceeded_error_clone_fields(scope, object)
@@ -846,6 +855,10 @@ impl v8::ValueDeserializerImpl for WireDeserializer {
None
})
}
HOST_OBJECT_TAG_WEB_TRANSPORT_ERROR => {
let payload = read_web_transport_error_payload(deserializer)?;
build_web_transport_error_from_clone_payload(scope, &payload)
}
HOST_OBJECT_TAG_DOM_EXCEPTION => {
let message = read_string(deserializer)?;
let name = read_string(deserializer)?;
@@ -1173,6 +1186,57 @@ pub(crate) fn read_crypto_key_payload<'s>(
)
}
pub(crate) fn write_web_transport_error_payload(
serializer: &dyn v8::ValueSerializerHelper,
payload: &WebTransportErrorClonePayload,
) {
serializer.write_uint32(HOST_OBJECT_TAG_WEB_TRANSPORT_ERROR);
// A new wire tag keeps lossless DOMString code units separate from the
// existing UTF-8 DOMException encoding retained by older stored values.
let bytes: Vec<_> = payload
.message
.iter()
.flat_map(|unit| unit.to_le_bytes())
.collect();
write_raw_vec(serializer, &bytes);
serializer.write_uint32(match payload.source {
WebTransportErrorSource::Stream => 0,
WebTransportErrorSource::Session => 1,
});
serializer.write_uint32(u32::from(payload.stream_error_code.is_some()));
if let Some(code) = payload.stream_error_code {
serializer.write_uint32(code);
}
}
pub(crate) fn read_web_transport_error_payload(
deserializer: &dyn v8::ValueDeserializerHelper,
) -> Option<WebTransportErrorClonePayload> {
let bytes = read_raw_vec(deserializer)?;
if !bytes.len().is_multiple_of(2) {
return None;
}
let message = bytes
.chunks_exact(2)
.map(|units| u16::from_le_bytes([units[0], units[1]]))
.collect();
let source = match read_u32(deserializer)? {
0 => WebTransportErrorSource::Stream,
1 => WebTransportErrorSource::Session,
_ => return None,
};
let stream_error_code = match read_u32(deserializer)? {
0 => None,
1 => Some(read_u32(deserializer)?),
_ => return None,
};
Some(WebTransportErrorClonePayload {
message,
source,
stream_error_code,
})
}
fn write_string(serializer: &dyn v8::ValueSerializerHelper, value: &str) {
write_raw_vec(serializer, value.as_bytes());
}