diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs index 1474444022..20d808aecf 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs @@ -225,49 +225,49 @@ struct MediaCapabilitiesKeySystemConfiguration { distinctive_identifier: MediaKeysRequirement, #[webidl(converter = "enum", default = MediaKeysRequirement::Optional)] persistent_state: MediaKeysRequirement, - #[webidl(with = optional_dom_string_sequence_member)] + #[webidl(sequence)] session_types: Option>, - #[webidl(with = optional_key_system_track_member)] + #[webidl(dictionary)] audio: Option, - #[webidl(with = optional_key_system_track_member)] + #[webidl(dictionary)] video: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "MediaDecodingConfiguration")] struct MediaDecodingConfiguration { - #[webidl(with = optional_audio_configuration_member)] + #[webidl(dictionary)] audio: Option, - #[webidl(name = "keySystemConfiguration", with = optional_key_system_configuration_member)] + #[webidl(name = "keySystemConfiguration", dictionary)] key_system_configuration: Option, #[webidl(name = "type", required, converter = "enum")] decoding_type: MediaDecodingType, - #[webidl(with = optional_video_configuration_member)] + #[webidl(dictionary)] video: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "MediaEncodingConfiguration")] struct MediaEncodingConfiguration { - #[webidl(with = optional_audio_configuration_member)] + #[webidl(dictionary)] audio: Option, #[webidl(name = "type", required, converter = "enum")] encoding_type: MediaEncodingType, - #[webidl(with = optional_video_configuration_member)] + #[webidl(dictionary)] video: Option, } #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "MediaCapabilities.decodingInfo")] struct DecodingInfoArgs { - #[webidl(required, with = media_decoding_configuration_arg)] + #[webidl(required, dictionary)] configuration: MediaDecodingConfiguration, } #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "MediaCapabilities.encodingInfo")] struct EncodingInfoArgs { - #[webidl(required, with = media_encoding_configuration_arg)] + #[webidl(required, dictionary)] configuration: MediaEncodingConfiguration, } @@ -494,136 +494,6 @@ fn media_capabilities_is_worker<'s>( .is_some_and(|value| value.boolean_value(scope)) } -fn media_decoding_configuration_arg<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, -) -> std::result::Result { - required_dictionary_argument::( - scope, - args, - index, - "MediaCapabilities.decodingInfo", - ) -} - -fn media_encoding_configuration_arg<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, -) -> std::result::Result { - required_dictionary_argument::( - scope, - args, - index, - "MediaCapabilities.encodingInfo", - ) -} - -fn required_dictionary_argument<'s, T>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - index: i32, - prefix: &'static str, -) -> std::result::Result -where - T: webidl::WebIdlDictionary<'s>, -{ - if args.length() <= index { - return Err(webidl::WebIdlError::missing_required( - webidl::Context::argument(prefix, (index + 1) as usize), - )); - } - let context = webidl::Context::argument(prefix, (index + 1) as usize); - let object = dictionary_object_or_empty(scope, args.get(index), context)?; - webidl::parse_dictionary_object::(scope, object) -} - -fn dictionary_object_or_empty<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - context: webidl::Context, -) -> std::result::Result, webidl::WebIdlError> { - if value.is_null_or_undefined() { - return Ok(v8::Object::new(scope)); - } - webidl::dictionary_value(value, context)? - .ok_or_else(|| webidl::WebIdlError::custom_message("The value is not a dictionary")) -} - -fn optional_dictionary_member<'s, T>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, - prefix: &'static str, -) -> std::result::Result, webidl::WebIdlError> -where - T: webidl::WebIdlDictionary<'s>, -{ - let context = webidl::Context::member(prefix, name); - let Some(value) = webidl::property_result(scope, object, name, context)? else { - return Ok(None); - }; - if value.is_undefined() { - return Ok(None); - } - let value = dictionary_object_or_empty(scope, value, context)?; - webidl::parse_dictionary_object::(scope, value).map(Some) -} - -fn optional_audio_configuration_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result, webidl::WebIdlError> { - optional_dictionary_member(scope, object, name, "MediaConfiguration") -} - -fn optional_video_configuration_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result, webidl::WebIdlError> { - optional_dictionary_member(scope, object, name, "MediaConfiguration") -} - -fn optional_key_system_configuration_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result, webidl::WebIdlError> { - optional_dictionary_member(scope, object, name, "MediaDecodingConfiguration") -} - -fn optional_key_system_track_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result, webidl::WebIdlError> { - optional_dictionary_member( - scope, - object, - name, - "MediaCapabilitiesKeySystemConfiguration", - ) -} - -fn optional_dom_string_sequence_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result>, webidl::WebIdlError> { - let context = webidl::Context::member("MediaCapabilitiesKeySystemConfiguration", name); - let Some(value) = webidl::property_result(scope, object, name, context)? else { - return Ok(None); - }; - if value.is_undefined() { - return Ok(None); - } - webidl::convert::>(scope, value, context) - .map(|sequence| Some(sequence.0.into_iter().map(|value| value.0).collect())) -} - fn optional_nullable_dom_string_member<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs index 6b3e50871e..9445f99324 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs @@ -1,5 +1,55 @@ use super::*; +#[test] +fn media_capabilities_nested_dictionaries_validate_null_before_later_getters() { + let mut vm = new_storage_test_vm("https://media-capabilities-dictionary.test/"); + vm.eval(r#" + globalThis.__nestedMediaResult = 'pending'; + (async () => { + const assert = (ok, message) => { if (!ok) throw new Error(message); }; + const capabilities = navigator.mediaCapabilities; + const marker = {}; + let laterReads = 0; + for (const method of ['decodingInfo', 'encodingInfo']) { + const call = config => capabilities[method](config); + const config = { + audio: null, + get type() {laterReads++; return method === 'decodingInfo' ? 'file' : 'record';}, + get video() {laterReads++; return undefined;} + }; + const promise = call(config); + assert(promise instanceof Promise, 'conversion failure returns a Promise'); + let error; + try {await promise;} catch (caught) {error = caught;} + assert(error instanceof TypeError && error.message.includes('contentType'), 'null child still validates required contentType'); + assert(laterReads === 0, 'invalid child stops before later fields'); + try {await call({audio: {get contentType() {throw marker;}}});} catch (caught) {error = caught;} + assert(error === marker, 'child getter exception identity'); + } + let error; + try { + await capabilities.decodingInfo({ + keySystemConfiguration: null, + get type() {laterReads++; return 'file';} + }); + } catch (caught) {error = caught;} + assert(error instanceof TypeError && error.message.includes('keySystem'), 'null key system requires keySystem'); + assert(laterReads === 0, 'invalid key system stops before type'); + const result = await capabilities.encodingInfo({ + type: 'record', + audio: {contentType: 'audio/webm; codecs="opus"'}, + video: undefined + }); + assert(result.configuration.audio.contentType.includes('audio/webm'), 'present child and absent sibling'); + return 'ok'; + })().then( + result => globalThis.__nestedMediaResult = result, + error => globalThis.__nestedMediaResult = String(error) + ); + "#).unwrap(); + assert_eq!(vm.eval("globalThis.__nestedMediaResult").unwrap(), "ok"); +} + #[test] fn navigator_geolocation_is_same_object_and_rejects_insecure_requests_asynchronously() { let mut vm = new_storage_test_vm("http://insecure-geolocation.test/");