mirror of
https://github.com/lexmount/moli.git
synced 2026-10-25 00:01:45 +00:00
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:
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user