From 5ffd83fedd28f69c70eb31b1c8f280aa5db9d594 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 6 Oct 2026 21:54:06 +0800 Subject: [PATCH] feat(webapi): implement media and transport error values --- moli-renderer-v8/src/context_bootstrap.rs | 7 +- .../assets/constructor_templates.rs | 16 ++ .../src/context_bootstrap/constructors.rs | 16 +- .../constructors/dom_exception.rs | 10 + .../dom_exception/overconstrained_error.rs | 86 +++++++ .../dom_exception/web_transport_error.rs | 185 +++++++++++++++ .../indexed_db/core/clone.rs | 25 +- .../src/context_bootstrap/specs/registry.rs | 4 +- .../src/context_bootstrap/specs/types.rs | 2 + .../src/script_vm/tests/browser_api/mod.rs | 1 + .../tests/browser_api/value_errors.js | 220 ++++++++++++++++++ .../tests/browser_api/value_errors.rs | 125 ++++++++++ .../src/script_vm/tests/indexed_db.rs | 44 ++++ moli-renderer-v8/src/structured_clone.rs | 80 ++++++- 14 files changed, 797 insertions(+), 24 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/overconstrained_error.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/web_transport_error.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.js create mode 100644 moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.rs diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 4a6b018c57..9428a2414c 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs index dda7815cc3..0a246e74ab 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/constructors.rs b/moli-renderer-v8/src/context_bootstrap/constructors.rs index a666c66ec9..a8e97cd85b 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs index aa3c4a60e2..bc55c8b977 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs @@ -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), _ => {} } } diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/overconstrained_error.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/overconstrained_error.rs new file mode 100644 index 0000000000..185ca0c823 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/overconstrained_error.rs @@ -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::(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); + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/web_transport_error.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/web_transport_error.rs new file mode 100644 index 0000000000..a176eaf9cc --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/web_transport_error.rs @@ -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, +} + +#[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::(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, + pub(crate) source: WebTransportErrorSource, + pub(crate) stream_error_code: Option, +} + +pub(crate) fn web_transport_error_clone_payload_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option { + 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::( + 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::::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> { + 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> { + 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) +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs index 708b3cafb1..789e2c862a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs @@ -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 { 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(|| { diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 0419f9a3b5..83d7c1c839 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/specs/types.rs b/moli-renderer-v8/src/context_bootstrap/specs/types.rs index 95177d8679..e169e49f90 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/types.rs @@ -57,6 +57,8 @@ pub(in crate::context_bootstrap) enum ConstructorKind { HashChangeEvent, PageTransitionEvent, DomException, + OverconstrainedError, + WebTransportError, RtcError, RtcErrorEvent, DomError, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index 13b608c486..abecdc4f7d 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.js b/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.js new file mode 100644 index 0000000000..521b8e6f93 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.js @@ -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; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.rs new file mode 100644 index 0000000000..fdf141779d --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/value_errors.rs @@ -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 = ''") + .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=''; + 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::::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::::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::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(); +} diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 9de1b2ad3e..4c205fe247 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -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"); diff --git a/moli-renderer-v8/src/structured_clone.rs b/moli-renderer-v8/src/structured_clone.rs index 85a65280ee..0954ceab00 100644 --- a/moli-renderer-v8/src/structured_clone.rs +++ b/moli-renderer-v8/src/structured_clone.rs @@ -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 { + 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()); }