From 3c282deb23dff001c49228cea1db61dead5e7cd3 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 10 Oct 2026 06:37:58 +0800 Subject: [PATCH] feat(eme): add native key status map members --- moli-renderer-v8/src/context_bootstrap.rs | 1 + .../assets/prototype_bindings.rs | 2 + .../context_bootstrap/media_key_status_map.rs | 335 ++++++++++++++++++ .../script_vm/tests/media_key_status_map.js | 59 +++ .../script_vm/tests/media_key_status_map.rs | 182 ++++++++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + moli-renderer-v8/src/web_api_interfaces.rs | 1 + moli-webidl/src/buffer_source.rs | 50 +++ moli-webidl/src/lib.rs | 2 +- 9 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/media_key_status_map.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/media_key_status_map.js create mode 100644 moli-renderer-v8/src/script_vm/tests/media_key_status_map.rs diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 720aaeed36..f3af549aa3 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs index 3457448c71..3fc0446b11 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs @@ -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()); diff --git a/moli-renderer-v8/src/context_bootstrap/media_key_status_map.rs b/moli-renderer-v8/src/context_bootstrap/media_key_status_map.rs new file mode 100644 index 0000000000..3f6de8db8b --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/media_key_status_map.rs @@ -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::::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> { + let map = backing(scope, receiver); + let entries = map.as_array(scope); + for index in 0..map.size() as u32 { + let key = + v8::Local::::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::(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::(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, + 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> { + 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::::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::(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") +} diff --git a/moli-renderer-v8/src/script_vm/tests/media_key_status_map.js b/moli-renderer-v8/src/script_vm/tests/media_key_status_map.js new file mode 100644 index 0000000000..07a5b6e583 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/media_key_status_map.js @@ -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; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/media_key_status_map.rs b/moli-renderer-v8/src/script_vm/tests/media_key_status_map.rs new file mode 100644 index 0000000000..d7772dd639 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/media_key_status_map.rs @@ -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 = ''") + .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::::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"); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index e822384040..176919fcf8 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index b5111fd9fd..48a200a21e 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -39,6 +39,7 @@ interfaces! { MediaKeyMessageEvent: Event; MediaKeySession: EventTarget; MediaKeyStatusMap; + MediaKeyStatusMapIterator = "MediaKeyStatusMap Iterator"; MediaKeySystemAccess; MediaKeys; PaymentMethodChangeEvent: PaymentRequestUpdateEvent; diff --git a/moli-webidl/src/buffer_source.rs b/moli-webidl/src/buffer_source.rs index 9fa4cea8c4..3e093800d2 100644 --- a/moli-webidl/src/buffer_source.rs +++ b/moli-webidl/src/buffer_source.rs @@ -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 { + 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 { + self.0.to_vec(scope) + } +} + impl<'s> WebIdlConverter<'s> for AllowSharedBufferSource<'s> { type Options = (); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 65c53a232d..7540cf9a7a 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -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,