mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
feat(eme): add native key status map members
This commit is contained in:
@@ -39,6 +39,7 @@ mod geometry_clone;
|
||||
mod geometry_runtime;
|
||||
mod history_mutation;
|
||||
mod history_runtime;
|
||||
pub(crate) mod media_key_status_map;
|
||||
mod media_metadata;
|
||||
mod media_recorder;
|
||||
mod media_streams;
|
||||
|
||||
@@ -28,6 +28,7 @@ use super::super::{
|
||||
indexed_db::install_indexed_db_template_bindings,
|
||||
media_cues::install_media_cue_template_bindings,
|
||||
media_file_template::install_media_file_template_bindings,
|
||||
media_key_status_map::install as install_media_key_status_map_template_bindings,
|
||||
media_queries::install_media_query_list_template_bindings,
|
||||
media_source::install_media_source_template_bindings,
|
||||
message_ports::install_message_port_template_bindings,
|
||||
@@ -435,6 +436,7 @@ pub(super) fn install_constructor_template_bindings<'s>(
|
||||
install_media_cue_template_bindings(scope, template, spec.interface.name());
|
||||
install_media_source_template_bindings(scope, template, spec.interface.name());
|
||||
install_remote_playback_template_bindings(scope, template, spec.interface.name());
|
||||
install_media_key_status_map_template_bindings(scope, template, spec.interface.name());
|
||||
super::super::media_streams::install(scope, template, spec.interface.name());
|
||||
super::super::picture_in_picture::install(scope, template, spec.interface.name());
|
||||
super::super::media_recorder::install(scope, template, spec.interface.name());
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
//! EME's readonly binary-key collection. CDM/session production is separate.
|
||||
//! Its pair iterator reads the current sorted list on every next() call, as
|
||||
//! required by Web IDL. BufferSource keys retain their existing object identity;
|
||||
//! only iterator result objects and entry arrays are newly allocated.
|
||||
|
||||
use crate::{
|
||||
util::{
|
||||
get_private_object, get_private_value, materialize_hidden_function_template_prototype,
|
||||
set_private_value,
|
||||
},
|
||||
web_api_interfaces, webidl,
|
||||
webidl_iterator::{
|
||||
invoke_webidl_collection_for_each_callback, prepare_webidl_collection_for_each_callback,
|
||||
},
|
||||
};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
const BACKING: &str = "__moliMediaKeyStatusMapBacking";
|
||||
const TARGET: &str = "__moliMediaKeyStatusMapIteratorTarget";
|
||||
const INDEX: &str = "__moliMediaKeyStatusMapIteratorIndex";
|
||||
const KIND: &str = "__moliMediaKeyStatusMapIteratorKind";
|
||||
const ITERATOR_PROTOTYPE: &str = "__moliMediaKeyStatusMapIteratorPrototype";
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::MediaKeyStatusMap, receiver, enumerable)]
|
||||
struct Prototype {
|
||||
#[webapi(accessor_property, getter = size)]
|
||||
size: (),
|
||||
#[webapi(method, length = 1, callback = has)]
|
||||
has: (),
|
||||
#[webapi(method, length = 1, callback = get)]
|
||||
get: (),
|
||||
#[webapi(method, length = 0, callback = entries)]
|
||||
entries: (),
|
||||
#[webapi(method, length = 0, callback = keys)]
|
||||
keys: (),
|
||||
#[webapi(method, length = 0, callback = values)]
|
||||
values: (),
|
||||
#[webapi(method, length = 1, callback = for_each)]
|
||||
for_each: (),
|
||||
#[webapi(alias = "entries", symbol = "iterator")]
|
||||
iterator: (),
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "MediaKeyStatusMap lookup")]
|
||||
struct KeyArgs<'s> {
|
||||
#[webidl(required, converter = "raw")]
|
||||
key_id: webidl::NonSharedBufferSource<'s>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "MediaKeyStatusMap.forEach")]
|
||||
struct ForEachArgs<'s> {
|
||||
#[webidl(required)]
|
||||
callback: v8::Local<'s, v8::Value>,
|
||||
#[webidl(default = v8::undefined(scope).into())]
|
||||
this_arg: v8::Local<'s, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::MediaKeyStatusMapIterator, prototype = "Object")]
|
||||
struct IteratorObject<'s> {
|
||||
#[webapi(prototype)]
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
#[webapi(slot = TARGET)]
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
#[webapi(slot = INDEX, init = 0)]
|
||||
index: (),
|
||||
#[webapi(slot = KIND)]
|
||||
kind: i32,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::MediaKeyStatusMapIterator, receiver,
|
||||
intrinsic_prototype_parent = v8::Intrinsic::IteratorPrototype,
|
||||
prototype_to_string_tag = "MediaKeyStatusMap Iterator", readonly_prototype, enumerable)]
|
||||
struct IteratorPrototype {
|
||||
#[webapi(method, length = 0, callback = next)]
|
||||
next: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct IteratorResult<'s> {
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
pub(super) fn install<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
name: &str,
|
||||
) {
|
||||
if name == "MediaKeyStatusMap" {
|
||||
Prototype::initialize_prototype_template(scope, template.prototype_template(scope));
|
||||
}
|
||||
}
|
||||
|
||||
fn target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
moli_webapi_declare::web_api_object_target(scope, receiver)
|
||||
.expect("EME collection receiver was validated")
|
||||
}
|
||||
|
||||
fn backing<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Map> {
|
||||
let object = target(scope, receiver);
|
||||
get_private_object(scope, object, BACKING)
|
||||
.and_then(|value| v8::Local::<v8::Map>::try_from(value).ok())
|
||||
.expect("native key status map retains its backing")
|
||||
}
|
||||
|
||||
fn size<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
rv.set_uint32(backing(scope, args.this()).size() as u32);
|
||||
}
|
||||
|
||||
fn lookup<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
bytes: &[u8],
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let map = backing(scope, receiver);
|
||||
let entries = map.as_array(scope);
|
||||
for index in 0..map.size() as u32 {
|
||||
let key =
|
||||
v8::Local::<v8::ArrayBuffer>::try_from(entries.get_index(scope, index * 2)?).ok()?;
|
||||
if key.byte_length() == bytes.len()
|
||||
&& webidl::AllowSharedBufferSource::Buffer(key).to_vec(scope) == bytes
|
||||
{
|
||||
return entries.get_index(scope, index * 2 + 1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn has<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<KeyArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let bytes = parsed.key_id.to_vec(scope);
|
||||
rv.set_bool(lookup(scope, args.this(), &bytes).is_some());
|
||||
}
|
||||
|
||||
fn get<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<KeyArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let bytes = parsed.key_id.to_vec(scope);
|
||||
rv.set(lookup(scope, args.this(), &bytes).unwrap_or_else(|| v8::undefined(scope).into()));
|
||||
}
|
||||
|
||||
struct KeyStatusPair<'s> {
|
||||
bytes: Vec<u8>,
|
||||
key: v8::Local<'s, v8::ArrayBuffer>,
|
||||
status: v8::Local<'s, v8::Value>,
|
||||
}
|
||||
|
||||
fn sorted_pairs<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> Vec<KeyStatusPair<'s>> {
|
||||
let map = backing(scope, receiver);
|
||||
let array = map.as_array(scope);
|
||||
let mut pairs = Vec::with_capacity(map.size());
|
||||
for index in 0..map.size() as u32 {
|
||||
let key = v8::Local::<v8::ArrayBuffer>::try_from(
|
||||
array.get_index(scope, index * 2).expect("native key"),
|
||||
)
|
||||
.expect("native key id is an ArrayBuffer");
|
||||
let source = webidl::AllowSharedBufferSource::Buffer(key);
|
||||
let status = array
|
||||
.get_index(scope, index * 2 + 1)
|
||||
.expect("native key status");
|
||||
pairs.push(KeyStatusPair {
|
||||
bytes: source.to_vec(scope),
|
||||
key,
|
||||
status,
|
||||
});
|
||||
}
|
||||
pairs.sort_by(|a, b| a.bytes.cmp(&b.bytes));
|
||||
pairs
|
||||
}
|
||||
|
||||
fn iterator<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
kind: i32,
|
||||
) {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let prototype = get_private_object(scope, global, ITERATOR_PROTOTYPE).unwrap_or_else(|| {
|
||||
let template = IteratorPrototype::build(scope);
|
||||
let prototype = materialize_hidden_function_template_prototype(scope, template)
|
||||
.expect("EME iterator prototype");
|
||||
set_private_value(scope, global, ITERATOR_PROTOTYPE, prototype.into());
|
||||
prototype
|
||||
});
|
||||
let object = IteratorObject::new(prototype, target(scope, args.this()), kind)
|
||||
.bind(scope)
|
||||
.expect("EME iterator should bind");
|
||||
rv.set(object.into());
|
||||
}
|
||||
|
||||
fn entries<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
iterator(scope, args, rv, 0);
|
||||
}
|
||||
fn keys<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
iterator(scope, args, rv, 1);
|
||||
}
|
||||
fn values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
iterator(scope, args, rv, 2);
|
||||
}
|
||||
|
||||
fn next<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let iterator = target(scope, args.this());
|
||||
let owner = get_private_object(scope, iterator, TARGET).expect("EME iterator target");
|
||||
let index = get_private_value(scope, iterator, INDEX)
|
||||
.and_then(|value| value.uint32_value(scope))
|
||||
.expect("EME iterator index");
|
||||
let kind = get_private_value(scope, iterator, KIND)
|
||||
.and_then(|value| value.int32_value(scope))
|
||||
.expect("EME iterator kind");
|
||||
let pairs = sorted_pairs(scope, owner);
|
||||
let (value, done) = if let Some(pair) = pairs.into_iter().nth(index as usize) {
|
||||
set_private_value(
|
||||
scope,
|
||||
iterator,
|
||||
INDEX,
|
||||
v8::Integer::new_from_unsigned(scope, index.saturating_add(1)).into(),
|
||||
);
|
||||
let value = match kind {
|
||||
0 => v8::Array::new_with_elements(scope, &[pair.key.into(), pair.status]).into(),
|
||||
1 => pair.key.into(),
|
||||
2 => pair.status,
|
||||
_ => unreachable!("native EME iterator kind"),
|
||||
};
|
||||
(value, false)
|
||||
} else {
|
||||
(v8::undefined(scope).into(), true)
|
||||
};
|
||||
rv.set(
|
||||
IteratorResult::new(value, done)
|
||||
.bind(scope)
|
||||
.expect("EME iterator result")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn for_each<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<ForEachArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let Some(callback) = prepare_webidl_collection_for_each_callback(
|
||||
scope,
|
||||
parsed.callback,
|
||||
"MediaKeyStatusMap.forEach",
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let owner = args.this();
|
||||
let mut index = 0;
|
||||
loop {
|
||||
let pairs = sorted_pairs(scope, owner);
|
||||
let Some(pair) = pairs.into_iter().nth(index) else {
|
||||
break;
|
||||
};
|
||||
if invoke_webidl_collection_for_each_callback(
|
||||
scope,
|
||||
&callback,
|
||||
parsed.this_arg,
|
||||
pair.status,
|
||||
pair.key.into(),
|
||||
owner,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn map_for_test<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
map: v8::Local<'s, v8::Map>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::MediaKeyStatusMap, require_prototype)]
|
||||
struct NativeStatusMap<'s> {
|
||||
#[webapi(slot = BACKING)]
|
||||
backing: v8::Local<'s, v8::Map>,
|
||||
}
|
||||
NativeStatusMap::new(map)
|
||||
.bind(scope)
|
||||
.expect("native EME test collection")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
(() => {
|
||||
const checks = [];
|
||||
const check = (name, action) => {
|
||||
try { checks.push({name, passed: action() === true}); }
|
||||
catch (error) { checks.push({name, passed: false, error: String(error)}); }
|
||||
};
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const popup = window.open();
|
||||
const realms = [['main', window], ['iframe', frame.contentWindow], ['popup', popup]];
|
||||
const methods = [['has', 1], ['get', 1], ['entries', 0], ['keys', 0], ['values', 0], ['forEach', 1]];
|
||||
try {
|
||||
for (const [realm, w] of realms) {
|
||||
const C = w?.MediaKeyStatusMap, P = C?.prototype;
|
||||
const prefix = realm + ': ';
|
||||
const throws = action => {
|
||||
try { action(); } catch (error) { return error instanceof w.TypeError; }
|
||||
return false;
|
||||
};
|
||||
check(prefix + 'interface object', () => typeof C === 'function' && C.name === 'MediaKeyStatusMap' && C.length === 0);
|
||||
check(prefix + 'prototype inheritance', () => Object.getPrototypeOf(P) === w.Object.prototype);
|
||||
check(prefix + 'illegal constructor', () => throws(() => new C()));
|
||||
check(prefix + 'illegal function call', () => throws(() => C()));
|
||||
check(prefix + 'prototype tag', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(P, Symbol.toStringTag);
|
||||
return d.value === 'MediaKeyStatusMap' && !d.writable && !d.enumerable && d.configurable;
|
||||
});
|
||||
const size = Object.getOwnPropertyDescriptor(P ?? {}, 'size');
|
||||
check(prefix + 'size descriptor', () => typeof size.get === 'function' && size.set === undefined && size.enumerable && size.configurable && size.get.length === 0);
|
||||
check(prefix + 'iterator alias', () => typeof P.entries === 'function' && P[Symbol.iterator] === P.entries);
|
||||
check(prefix + 'iterator descriptor', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(P, Symbol.iterator);
|
||||
return d.writable && !d.enumerable && d.configurable;
|
||||
});
|
||||
for (const [name, length] of methods) {
|
||||
check(prefix + name + ' descriptor and length', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(P, name);
|
||||
return typeof d.value === 'function' && d.value.name === name && d.value.length === length && d.writable && d.enumerable && d.configurable;
|
||||
});
|
||||
}
|
||||
let traps = 0, conversions = 0;
|
||||
const argument = new Proxy({}, {get() {traps++; throw Error('argument trap');}});
|
||||
const fake = Object.create(P ?? null);
|
||||
const revoked = Proxy.revocable({}, {}); revoked.revoke();
|
||||
const receivers = [{}, fake, Object.create(fake), new Proxy(fake, {get() {traps++;}}), revoked.proxy, new w.EventTarget()];
|
||||
for (const [index, receiver] of receivers.entries()) {
|
||||
check(prefix + 'size rejects receiver ' + index, () => throws(() => w.Reflect.apply(size?.get, receiver, [])));
|
||||
for (const [name] of methods) {
|
||||
check(prefix + name + ' rejects receiver ' + index, () => throws(() => w.Reflect.apply(P?.[name], receiver, [argument, {valueOf() {conversions++;}}])));
|
||||
}
|
||||
}
|
||||
check(prefix + 'brand checks precede conversion and traps', () => traps === 0 && conversions === 0);
|
||||
}
|
||||
} finally {
|
||||
popup?.close(); frame.remove();
|
||||
}
|
||||
globalThis.__uiEventResults = {complete: true, checks, passed: checks.filter(row => row.passed).length, total: checks.length};
|
||||
return __uiEventResults;
|
||||
})()
|
||||
@@ -0,0 +1,182 @@
|
||||
use super::*;
|
||||
|
||||
fn native_status_map_vm() -> crate::runtime::PageVmTaskExecutorTestHarness {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://native-eme-map.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe></iframe>'")
|
||||
.unwrap();
|
||||
let context_ptr = &vm.page_default_runtime.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 map = v8::Map::new(scope);
|
||||
let object = crate::context_bootstrap::media_key_status_map::map_for_test(scope, map);
|
||||
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();
|
||||
for (name, value) in [
|
||||
("statuses", object.into()),
|
||||
("backing", map.into()),
|
||||
("nativeStatuses", proxy.into()),
|
||||
] {
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, crate::util::v8str(scope, name).into(), value),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
vm
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_key_status_map_public_members_use_derived_receivers_and_window_exposure() {
|
||||
for url in ["https://eme-members.test/", "http://localhost/"] {
|
||||
let mut vm = new_storage_page_task_executor_test_vm(url);
|
||||
vm.eval(include_str!("media_key_status_map.js")).unwrap();
|
||||
assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "171");
|
||||
}
|
||||
let mut vm = new_storage_page_task_executor_test_vm("http://eme-insecure.test/");
|
||||
assert_eq!(
|
||||
vm.eval("!isSecureContext && !('MediaKeyStatusMap' in globalThis)")
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_key_status_map_compares_binary_keys_and_preserves_sorted_key_identities() {
|
||||
let mut vm = native_status_map_vm();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const assert=(ok,name)=>{if(!ok)throw Error(name)};
|
||||
const bytes=b=>Array.from(new Uint8Array(b)).join('-');
|
||||
const entries=[[[2],'expired'],[[1,0],'usable'],[[1],'status-pending'],[[255],'released']];
|
||||
for(const [key,value] of entries)backing.set(new Uint8Array(key).buffer,value);
|
||||
assert(statuses.size===4 && nativeStatuses.size===4,'native size identity');
|
||||
const padded=new Uint8Array([7,1,0,8]);
|
||||
assert(statuses.get(padded.subarray(1,3))==='usable','typed array byte range');
|
||||
assert(statuses.has(new DataView(padded.buffer,1,2)),'DataView byte range');
|
||||
assert(statuses.get(new Uint16Array(new Uint8Array([1,0]).buffer))==='usable','bytes rather than elements');
|
||||
assert(statuses.get(new Uint8Array([1]))==='status-pending'&&!statuses.has(new Uint8Array([0,1])),'prefix and order distinct');
|
||||
assert(statuses.get(new Uint8Array([3]))===undefined&&!statuses.has(new ArrayBuffer(0)),'unknown keys');
|
||||
assert([...statuses.keys()].map(bytes).join()==='1,1-0,2,255','octet lexicographic and prefix sorting');
|
||||
assert([...statuses.values()].join()==='status-pending,usable,expired,released','associated statuses');
|
||||
const pair=statuses.entries().next().value;
|
||||
assert(pair instanceof Array&&pair[0] instanceof ArrayBuffer&&pair[1]==='status-pending','pair representation');
|
||||
const key1=statuses.keys().next().value,key2=statuses.keys().next().value;
|
||||
assert(key1===key2&&key1===pair[0],'BufferSource keys preserve object identity');
|
||||
assert(Object.getPrototypeOf(statuses.entries())===Object.getPrototypeOf(statuses.keys()),'shared interface iterator prototype');
|
||||
const iterator=statuses.entries(),proto=Object.getPrototypeOf(iterator);
|
||||
assert(Object.prototype.toString.call(iterator)==='[object MediaKeyStatusMap Iterator]','iterator tag');
|
||||
assert(!Object.hasOwn(iterator,Symbol.toStringTag)&&iterator[Symbol.iterator]()===iterator,'iterator inherited surface');
|
||||
assert(Object.getPrototypeOf(proto)===Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())),'IteratorPrototype parent');
|
||||
assert(iterator.next().value[0] instanceof ArrayBuffer,'first item');
|
||||
backing.clear();backing.set(new Uint8Array([0]).buffer,'released');backing.set(new Uint8Array([9]).buffer,'expired');
|
||||
assert(bytes(iterator.next().value[0])==='9','next reads the current sorted list at its index');
|
||||
assert(iterator.next().done,'current list exhausted');
|
||||
backing.set(new Uint8Array([10]).buffer,'usable');
|
||||
assert(bytes(iterator.next().value[0])==='10','IDL pair iterator reads a later list after done');
|
||||
return true;
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_key_status_map_preserves_callback_realms_order_and_conversion_boundaries() {
|
||||
let mut vm = native_status_map_vm();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const assert=(ok,name)=>{if(!ok)throw Error(name)};
|
||||
const other=document.querySelector('iframe').contentWindow;
|
||||
const marker={},thisArg={};
|
||||
const expect=(C,action,name)=>{let error;try{action()}catch(caught){error=caught}assert(error instanceof C,name)};
|
||||
for(const w of [window,other]) {
|
||||
const P=w.MediaKeyStatusMap.prototype;
|
||||
backing.clear();backing.set(new Uint8Array([2]).buffer,'expired');backing.set(new Uint8Array([1]).buffer,'usable');
|
||||
assert(P.get.call(nativeStatuses,new other.Uint8Array([1]))==='usable','cross-realm native key lookup');
|
||||
const iterator=P.entries.call(nativeStatuses),first=iterator.next();
|
||||
assert(first instanceof w.Object&&first.value instanceof w.Array&&first.value[0] instanceof ArrayBuffer,'iterator result realm and existing key realm');
|
||||
const keys=[];
|
||||
assert(P.forEach.call(nativeStatuses,function(value,key,owner){
|
||||
assert(this===thisArg&&owner===nativeStatuses,'callback receiver and owner');
|
||||
keys.push(new Uint8Array(key)[0]);
|
||||
if(keys.length===1){backing.clear();backing.set(new Uint8Array([0]).buffer,'expired');backing.set(new Uint8Array([9]).buffer,'usable');}
|
||||
},thisArg)===undefined&&keys.join()==='1,9','forEach refreshes the sorted pairs after callback');
|
||||
let caught;try{P.forEach.call(statuses,()=>{throw marker})}catch(error){caught=error}assert(caught===marker,'callback exception identity');
|
||||
for(const bad of [null,{},1])expect(w.TypeError,()=>P.forEach.call(statuses,bad),'callback callable check');
|
||||
for(const name of ['get','has']) {
|
||||
expect(w.TypeError,()=>P[name].call(statuses),'required key argument');
|
||||
for(const value of [null,undefined,{},[],1,'1',Symbol(),new Proxy(new Uint8Array([1]),{}),new SharedArrayBuffer(0),new Uint8Array(new SharedArrayBuffer(0)),new SharedArrayBuffer(1),new Uint8Array(new SharedArrayBuffer(1)),new ArrayBuffer(1,{maxByteLength:2}),new Uint8Array(new ArrayBuffer(1,{maxByteLength:2}))])expect(w.TypeError,()=>P[name].call(statuses,value),'fixed nonshared BufferSource');
|
||||
const detached=new ArrayBuffer(1),detachedView=new Uint8Array(detached);
|
||||
structuredClone(detached,{transfer:[detached]});
|
||||
assert(P[name].call(statuses,detached)===(name==='has'?false:undefined)&&P[name].call(statuses,detachedView)===(name==='has'?false:undefined),'detached fixed buffers are empty lookups');
|
||||
const resizable=new ArrayBuffer(1,{maxByteLength:2}),resizableView=new Uint8Array(resizable);
|
||||
structuredClone(resizable,{transfer:[resizable]});
|
||||
for(const value of [resizable,resizableView])expect(w.TypeError,()=>P[name].call(statuses,value),'detached resizable BufferSource');
|
||||
let traps=0;const proxy=new Proxy(new Uint8Array([1]),{get(){traps++;throw marker}});
|
||||
expect(w.TypeError,()=>P[name].call(statuses,proxy),'author buffer Proxy');assert(traps===0,'no author buffer traps');
|
||||
const revoked=Proxy.revocable(statuses,{});revoked.revoke();
|
||||
for(const receiver of [{},Object.create(statuses),new Proxy(nativeStatuses,{}),revoked.proxy])expect(w.TypeError,()=>P[name].call(receiver,proxy),'native brand before argument conversion');
|
||||
assert(traps===0,'receiver and argument Proxies do not trigger traps');
|
||||
}
|
||||
const p=Object.getPrototypeOf(iterator);
|
||||
for(const receiver of [{},Object.create(iterator),new Proxy(iterator,{})])expect(w.TypeError,()=>p.next.call(receiver),'iterator native brand');
|
||||
}
|
||||
const foreign=other.Function('value','key','owner', 'globalThis.callbackKey = key; globalThis.callbackOwner = owner;');
|
||||
statuses.forEach(foreign);
|
||||
assert(other.callbackKey instanceof ArrayBuffer&&!(other.callbackKey instanceof other.ArrayBuffer)&&other.callbackOwner===statuses,'callback preserves existing BufferSource realm');
|
||||
Object.setPrototypeOf(statuses,null);
|
||||
assert(other.MediaKeyStatusMap.prototype.get.call(nativeStatuses,new Uint8Array([9]))==='usable','brand survives prototype replacement');
|
||||
return true;
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_key_status_map_iterators_support_native_proxies_and_borrowed_next_realms() {
|
||||
let mut vm = native_status_map_vm();
|
||||
vm.eval(r#"
|
||||
backing.set(new Uint8Array([1]).buffer,'usable');
|
||||
backing.set(new Uint8Array([2]).buffer,'expired');
|
||||
Object.defineProperty(globalThis,'MediaKeyStatusMap Iterator',{get(){throw Error('author hidden constructor getter')}});
|
||||
globalThis.iterator=statuses.entries();
|
||||
"#).unwrap();
|
||||
let context_ptr = &vm.page_default_runtime.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 object = v8::Local::<v8::Object>::try_from(
|
||||
global
|
||||
.get(scope, crate::util::v8str(scope, "iterator").into())
|
||||
.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_eq!(
|
||||
global.create_data_property(
|
||||
scope,
|
||||
crate::util::v8str(scope, "nativeIterator").into(),
|
||||
proxy.into()
|
||||
),
|
||||
Some(true)
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const assert=(ok,name)=>{if(!ok)throw Error(name)};
|
||||
const other=document.querySelector('iframe').contentWindow;
|
||||
const foreignNext=Object.getPrototypeOf(other.MediaKeyStatusMap.prototype.entries.call(statuses)).next;
|
||||
const row=foreignNext.call(nativeIterator);
|
||||
assert(row instanceof other.Object&&row.value instanceof other.Array&&row.value[0] instanceof ArrayBuffer,'borrowed next creates result containers in its callee realm and retains the key realm');
|
||||
assert(row.value[1]==='usable','registered iterator native identity');
|
||||
let traps=0,error;try{foreignNext.call(new Proxy(nativeIterator,{get(){traps++;}}))}catch(caught){error=caught}
|
||||
assert(error instanceof other.TypeError&&traps===0,'author wrapper is rejected without traps');
|
||||
Object.setPrototypeOf(iterator,null);
|
||||
assert(foreignNext.call(nativeIterator).value[1]==='expired'&&foreignNext.call(nativeIterator).done,'native iterator brand survives prototype changes');
|
||||
return true;
|
||||
})()"#).unwrap(),"true");
|
||||
}
|
||||
@@ -2304,6 +2304,7 @@ mod lazy_window_surfaces;
|
||||
mod legacy_performance;
|
||||
mod location_put_forwards;
|
||||
mod media_device_interfaces;
|
||||
mod media_key_status_map;
|
||||
mod media_metadata;
|
||||
mod media_owner_playback_interfaces;
|
||||
mod media_recorder;
|
||||
|
||||
@@ -39,6 +39,7 @@ interfaces! {
|
||||
MediaKeyMessageEvent: Event;
|
||||
MediaKeySession: EventTarget;
|
||||
MediaKeyStatusMap;
|
||||
MediaKeyStatusMapIterator = "MediaKeyStatusMap Iterator";
|
||||
MediaKeySystemAccess;
|
||||
MediaKeys;
|
||||
PaymentMethodChangeEvent: PaymentRequestUpdateEvent;
|
||||
|
||||
@@ -12,6 +12,56 @@ pub enum AllowSharedBufferSource<'s> {
|
||||
View(v8::Local<'s, v8::ArrayBufferView>),
|
||||
}
|
||||
|
||||
/// A fixed-length, non-shared `BufferSource`. Keep the native identity until
|
||||
/// the consuming algorithm reads its bytes; conversion does not call author
|
||||
/// getters or unwrap author Proxies.
|
||||
pub struct NonSharedBufferSource<'s>(AllowSharedBufferSource<'s>);
|
||||
|
||||
impl<'s> WebIdlConverter<'s> for NonSharedBufferSource<'s> {
|
||||
type Options = ();
|
||||
|
||||
fn convert(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
context: Context,
|
||||
options: &Self::Options,
|
||||
) -> Result<Self, WebIdlError> {
|
||||
let source = AllowSharedBufferSource::convert(scope, value, context, options)?;
|
||||
if matches!(&source, AllowSharedBufferSource::Shared(_))
|
||||
|| source
|
||||
.backing_store(scope)
|
||||
.is_some_and(|backing| backing.is_shared())
|
||||
{
|
||||
return Err(WebIdlError::cannot_convert(
|
||||
context,
|
||||
"non-shared BufferSource",
|
||||
));
|
||||
}
|
||||
// A detached resizable ArrayBuffer retains its resizable identity,
|
||||
// even when its replacement empty backing store is fixed-length.
|
||||
let resizable = match &source {
|
||||
AllowSharedBufferSource::Buffer(buffer) => buffer.is_resizable_by_user_javascript(),
|
||||
AllowSharedBufferSource::View(view) => view
|
||||
.buffer(scope)
|
||||
.is_some_and(|buffer| buffer.is_resizable_by_user_javascript()),
|
||||
AllowSharedBufferSource::Shared(_) => unreachable!("shared sources were rejected"),
|
||||
};
|
||||
if resizable {
|
||||
return Err(WebIdlError::cannot_convert(
|
||||
context,
|
||||
"non-resizable BufferSource",
|
||||
));
|
||||
}
|
||||
Ok(Self(source))
|
||||
}
|
||||
}
|
||||
|
||||
impl NonSharedBufferSource<'_> {
|
||||
pub fn to_vec(&self, scope: &mut v8::PinScope<'_, '_>) -> Vec<u8> {
|
||||
self.0.to_vec(scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> WebIdlConverter<'s> for AllowSharedBufferSource<'s> {
|
||||
type Options = ();
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ mod helpers;
|
||||
mod traits;
|
||||
mod types;
|
||||
|
||||
pub use buffer_source::AllowSharedBufferSource;
|
||||
pub use buffer_source::{AllowSharedBufferSource, NonSharedBufferSource};
|
||||
pub use convert::{
|
||||
argument, argument_with_options, convert, convert_optional_sequence, convert_with_options,
|
||||
legacy_bool_member_or, legacy_number_member_or, legacy_optional_member,
|
||||
|
||||
Reference in New Issue
Block a user