refactor(navigator): derive MediaCapabilities configuration fields

Derive required configuration arguments, nested audio/video/key-system dictionaries and session-type sequences. Preserve optional absence, validate required child members for null, and stop before later getters on child failures. Keep the three-state encryptionScheme converter and Promise rejection policy.

Validation on the combined tree: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (19639 passed, 16 skipped; CARGO_BUILD_JOBS=1).
This commit is contained in:
ldm0
2026-10-06 13:35:34 +08:00
committed by Donough Liu
parent 66b76317b0
commit 86ccaa5ee8
2 changed files with 60 additions and 140 deletions
@@ -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<Vec<String>>,
#[webidl(with = optional_key_system_track_member)]
#[webidl(dictionary)]
audio: Option<KeySystemTrackConfiguration>,
#[webidl(with = optional_key_system_track_member)]
#[webidl(dictionary)]
video: Option<KeySystemTrackConfiguration>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "MediaDecodingConfiguration")]
struct MediaDecodingConfiguration {
#[webidl(with = optional_audio_configuration_member)]
#[webidl(dictionary)]
audio: Option<AudioConfiguration>,
#[webidl(name = "keySystemConfiguration", with = optional_key_system_configuration_member)]
#[webidl(name = "keySystemConfiguration", dictionary)]
key_system_configuration: Option<MediaCapabilitiesKeySystemConfiguration>,
#[webidl(name = "type", required, converter = "enum")]
decoding_type: MediaDecodingType,
#[webidl(with = optional_video_configuration_member)]
#[webidl(dictionary)]
video: Option<VideoConfiguration>,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "MediaEncodingConfiguration")]
struct MediaEncodingConfiguration {
#[webidl(with = optional_audio_configuration_member)]
#[webidl(dictionary)]
audio: Option<AudioConfiguration>,
#[webidl(name = "type", required, converter = "enum")]
encoding_type: MediaEncodingType,
#[webidl(with = optional_video_configuration_member)]
#[webidl(dictionary)]
video: Option<VideoConfiguration>,
}
#[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<MediaDecodingConfiguration, webidl::WebIdlError> {
required_dictionary_argument::<MediaDecodingConfiguration>(
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<MediaEncodingConfiguration, webidl::WebIdlError> {
required_dictionary_argument::<MediaEncodingConfiguration>(
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<T, webidl::WebIdlError>
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::<T>(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<v8::Local<'s, v8::Object>, 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<Option<T>, 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::<T>(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<Option<AudioConfiguration>, 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<Option<VideoConfiguration>, 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<Option<MediaCapabilitiesKeySystemConfiguration>, 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<Option<KeySystemTrackConfiguration>, 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<Option<Vec<String>>, 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::<webidl::Sequence<webidl::DomString>>(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>,
@@ -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/");