feat(eme): add native key status map members

This commit is contained in:
ldm0
2026-10-11 18:06:49 +08:00
parent 816e82b450
commit 3c282deb23
9 changed files with 632 additions and 1 deletions
@@ -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;
+50
View File
@@ -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 = ();
+1 -1
View File
@@ -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,