diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/state.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/state.rs index f60dd2b2de..6441c90bcf 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/state.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/state.rs @@ -21,6 +21,7 @@ pub(in crate::context_bootstrap::media_queries::events::simple_event_target) fn target: v8::Local<'s, v8::Object>, slot: &str, ) -> Option> { + let target = moli_webapi_declare::web_api_object_target(scope, target).unwrap_or(target); let mut current = Some(target); for _ in 0..64 { let object = current?; diff --git a/moli-renderer-v8/src/context_bootstrap/media_source.rs b/moli-renderer-v8/src/context_bootstrap/media_source.rs index a3d9345db7..5a24c15633 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_source.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_source.rs @@ -1,11 +1,22 @@ -use crate::web_api_interfaces; -use crate::webidl; +//! MediaSource's initial closed state. Attachment, source buffers and decoding +//! require a media backend; this frontend does not synthesize an open source. + +use super::{constructors::throw_dom_exception_value, media_queries}; +use crate::{ + util::{get_private_object, get_private_value, set_private_value, throw_type_error, v8str}, + web_api_interfaces, webidl, +}; use moli_web_mime::is_media_source_type_supported; use moli_webapi_declare::WebApiFunctionTemplate; +pub(crate) mod source_buffer_list; mod state; pub(crate) use state::{MediaSourceObject, media_source_object}; +const SOURCE_BUFFERS: &str = "__moliMediaSourceBuffers"; +const ACTIVE_SOURCE_BUFFERS: &str = "__moliMediaSourceActiveBuffers"; +const LISTENERS: &str = "__moliMediaSourceListeners"; + #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "MediaSource.isTypeSupported")] struct MediaSourceIsTypeSupportedArgs { @@ -25,11 +36,88 @@ struct MediaSourceTemplateDeclaration { is_type_supported: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::MediaSource, receiver, enumerable)] +struct MediaSourcePrototype { + #[webapi(accessor_property, getter = buffers, data = v8str(scope, SOURCE_BUFFERS))] + source_buffers: (), + #[webapi(accessor_property, getter = buffers, data = v8str(scope, ACTIVE_SOURCE_BUFFERS))] + active_source_buffers: (), + #[webapi(accessor_property, getter = ready_state)] + ready_state: (), + #[webapi(accessor_property, getter = duration, setter = set_duration)] + duration: (), + #[webapi(accessor_property, getter = handler_getter, setter = handler_setter, data = v8str(scope, "sourceopen"))] + onsourceopen: (), + #[webapi(accessor_property, getter = handler_getter, setter = handler_setter, data = v8str(scope, "sourceended"))] + onsourceended: (), + #[webapi(accessor_property, getter = handler_getter, setter = handler_setter, data = v8str(scope, "sourceclose"))] + onsourceclose: (), + #[webapi(method, length = 1, callback = add_source_buffer)] + add_source_buffer: (), + #[webapi(method, length = 1, callback = remove_source_buffer)] + remove_source_buffer: (), + #[webapi(method, length = 0, callback = end_of_stream)] + end_of_stream: (), + #[webapi(method, length = 2, callback = set_live_seekable_range)] + set_live_seekable_range: (), + #[webapi(method, length = 0, callback = clear_live_seekable_range)] + clear_live_seekable_range: (), +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "MediaSource.duration")] +struct DurationArgs { + #[webidl(converter = "unrestricted_double", default = f64::NAN)] + value: f64, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "MediaSource.addSourceBuffer")] +struct AddArgs { + #[webidl(required, name = "type")] + media_type: String, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "MediaSource.removeSourceBuffer")] +struct RemoveArgs<'s> { + #[webidl(required, name = "sourceBuffer", interface = web_api_interfaces::SourceBuffer)] + _source_buffer: v8::Local<'s, v8::Object>, +} + +#[derive(webidl::WebIdlEnum)] +#[webidl(name = "EndOfStreamError", rename_all = "lowercase")] +enum EndOfStreamError { + Network, + Decode, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "MediaSource.endOfStream")] +struct EndArgs { + #[webidl(name = "error", converter = "enum")] + _error: Option, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "MediaSource.setLiveSeekableRange")] +struct RangeArgs { + #[webidl(required, name = "start", converter = "double")] + _start: f64, + #[webidl(required, name = "end", converter = "double")] + _end: f64, +} + pub(in crate::context_bootstrap) fn media_source_constructor_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !args.is_construct_call() { + throw_type_error(scope, "MediaSource constructor requires 'new'."); + return; + } state::initialize(scope, args.this()); rv.set(args.this().into()); } @@ -41,6 +129,12 @@ pub(in crate::context_bootstrap) fn install_media_source_template_bindings<'s>( ) { if interface_name == "MediaSource" { MediaSourceTemplateDeclaration::initialize_template(scope, template); + MediaSourcePrototype::initialize_prototype_template( + scope, + template.prototype_template(scope), + ); + } else if interface_name == "SourceBufferList" { + source_buffer_list::install(scope, template); } } @@ -54,3 +148,181 @@ fn media_source_is_type_supported_callback<'s>( }; rv.set(v8::Boolean::new(scope, is_media_source_type_supported(&parsed.media_type)).into()); } + +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("MSE receiver was validated") +} + +fn buffers<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let source = target(scope, args.this()); + let slot = args.data().to_rust_string_lossy(scope); + if let Some(list) = get_private_object(scope, source, &slot) { + rv.set(list.into()); + return; + } + // A borrowed getter still returns a list from its owner's creation realm. + let context = source + .get_creation_context(scope) + .expect("MediaSource has a creation realm"); + let scope = &mut v8::ContextScope::new(scope, context); + let values = v8::Array::new(scope, 0); + let list = source_buffer_list::build(scope, source, values); + set_private_value(scope, source, &slot, list.into()); + rv.set(list.into()); +} + +fn ready_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + rv.set(v8str(scope, "closed").into()); +} + +fn duration<'s>( + _scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + rv.set_double(f64::NAN); +} + +fn closed_error(scope: &mut v8::PinScope<'_, '_>) { + throw_dom_exception_value(scope, "The MediaSource is not open.", "InvalidStateError"); +} + +fn set_duration<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + if parsed.value.is_nan() || parsed.value < 0.0 { + throw_type_error( + scope, + "MediaSource duration must be nonnegative and not NaN.", + ); + return; + } + closed_error(scope); +} + +fn add_source_buffer<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + if parsed.media_type.is_empty() { + throw_type_error(scope, "Source buffer type must not be empty."); + } else if !is_media_source_type_supported(&parsed.media_type) { + throw_dom_exception_value( + scope, + "Unsupported source buffer type.", + "NotSupportedError", + ); + } else { + closed_error(scope); + } +} + +fn remove_source_buffer<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if webidl::parse_args::(scope, &args).is_some() { + // Closed sources have no members, even if the argument is a genuine + // SourceBuffer from another source. + throw_dom_exception_value( + scope, + "Source buffer is not in this source.", + "NotFoundError", + ); + } +} + +fn end_of_stream<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if webidl::parse_args::(scope, &args).is_some() { + closed_error(scope); + } +} + +fn set_live_seekable_range<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if webidl::parse_args::(scope, &args).is_some() { + // Finite double conversion precedes the algorithm; closed-state + // rejection precedes its nonnegative/ordered range validation. + closed_error(scope); + } +} + +fn clear_live_seekable_range<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + closed_error(scope); +} + +fn handler_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let object = target(scope, args.this()); + let event = args.data().to_rust_string_lossy(scope); + rv.set( + get_private_value(scope, object, handler_slot(&event)) + .unwrap_or_else(|| v8::null(scope).into()), + ); +} + +fn handler_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let object = target(scope, args.this()); + let event = args.data().to_rust_string_lossy(scope); + let slot = handler_slot(&event); + let active = args.get(0).is_object(); + let handler = if active { + args.get(0) + } else { + v8::null(scope).into() + }; + set_private_value(scope, object, slot, handler); + media_queries::simple_object_event_set_ordered_handler( + scope, object, LISTENERS, &event, slot, active, + ); +} + +fn handler_slot(event: &str) -> &'static str { + match event { + "sourceopen" => "__moliMediaSourceOnsourceopen", + "sourceended" => "__moliMediaSourceOnsourceended", + "sourceclose" => "__moliMediaSourceOnsourceclose", + "addsourcebuffer" => "__moliSourceBufferListOnaddsourcebuffer", + "removesourcebuffer" => "__moliSourceBufferListOnremovesourcebuffer", + _ => unreachable!("MSE event handler callback data"), + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/media_source/source_buffer_list.rs b/moli-renderer-v8/src/context_bootstrap/media_source/source_buffer_list.rs new file mode 100644 index 0000000000..641d22fa6e --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/media_source/source_buffer_list.rs @@ -0,0 +1,244 @@ +//! Native indexed SourceBufferList. The backing array is private; indexed writes +//! follow Web IDL's readonly indexed getter rules, including unsupported indices. + +use super::{LISTENERS, handler_getter, handler_setter, target}; +use crate::{ + context_bootstrap::shared, + util::{get_private_object, v8str}, + web_api_interfaces, +}; +use moli_webapi_declare::{ + DataPropertyDescriptorDeclaration, WebApiFunctionTemplate, WebApiObject, +}; + +const OWNER: &str = "__moliSourceBufferListOwner"; +const VALUES: &str = "__moliSourceBufferListValues"; + +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::SourceBufferList, require_prototype)] +struct ListObject<'s> { + #[webapi(slot = OWNER)] + owner: v8::Local<'s, v8::Object>, + #[webapi(slot = VALUES)] + values: v8::Local<'s, v8::Array>, + #[webapi(slot = shared::SIMPLE_EVENT_TARGET_SLOT, value = LISTENERS)] + event_target: (), + #[webapi(slot = shared::SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT, init = true)] + ordered_handlers: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SourceBufferList, receiver, enumerable)] +struct ListPrototype { + #[webapi(accessor_property, getter = length)] + length: (), + #[webapi(accessor_property, getter = handler_getter, setter = handler_setter, data = v8str(scope, "addsourcebuffer"))] + onaddsourcebuffer: (), + #[webapi(accessor_property, getter = handler_getter, setter = handler_setter, data = v8str(scope, "removesourcebuffer"))] + onremovesourcebuffer: (), + #[webapi(intrinsic_data_property = v8::Intrinsic::ArrayProtoValues, symbol = "iterator")] + iterator: (), +} + +#[derive(WebApiObject)] +#[webapi(plain)] +struct ListHandler<'s> { + #[webapi(prototype, value = v8::null(scope))] + prototype: (), + reflect_set: v8::Local<'s, v8::Function>, + #[webapi(method, length = 4, callback = set, data = self.reflect_set)] + set: (), +} + +pub(super) fn install<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + ListPrototype::initialize_prototype_template(scope, template.prototype_template(scope)); +} + +pub(crate) fn build<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + values: v8::Local<'s, v8::Array>, +) -> v8::Local<'s, v8::Object> { + let template = v8::ObjectTemplate::new(scope); + template.set_indexed_property_handler( + v8::IndexedPropertyHandlerConfiguration::new() + .getter(indexed_getter) + .query(indexed_query) + .deleter(indexed_deleter) + .definer(indexed_definer) + .enumerator(indexed_enumerator) + .descriptor(indexed_descriptor), + ); + let object = template + .new_instance(scope) + .expect("SourceBufferList should allocate"); + ListObject::new(owner, values) + .bind_into(scope, object) + .expect("SourceBufferList should bind"); + // Interceptors expose their holder but not the original [[Set]] receiver. + // A registered native Proxy retains brand identity while providing it. + let handler = ListHandler::new( + moli_webapi_declare::web_api_reflect_set(scope).expect("Reflect.set was captured"), + ) + .bind(scope) + .expect("SourceBufferList handler should bind"); + let proxy = + v8::Proxy::new(scope, object, handler).expect("SourceBufferList Proxy should allocate"); + moli_webapi_declare::register_web_api_proxy(scope, proxy) + .expect("SourceBufferList Proxy retains native identity"); + proxy.into() +} + +fn values<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Array> { + let object = target(scope, receiver); + get_private_object(scope, object, VALUES) + .and_then(|value| v8::Local::::try_from(value).ok()) + .expect("SourceBufferList retains its backing array") +} + +fn item<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + index: u32, +) -> Option> { + let values = values(scope, receiver); + (index < values.length()) + .then(|| values.get_index(scope, index)) + .flatten() +} + +fn length<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + rv.set_uint32(values(scope, args.this()).length()); +} + +fn indexed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = item(scope, args.holder(), index) else { + return v8::Intercepted::kNo; + }; + rv.set(value); + v8::Intercepted::kYes +} + +fn indexed_query<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Integer>, +) -> v8::Intercepted { + if index >= values(scope, args.holder()).length() { + return v8::Intercepted::kNo; + } + rv.set_int32(v8::PropertyAttribute::READ_ONLY.as_u32() as i32); + v8::Intercepted::kYes +} + +fn set<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let object = v8::Local::::try_from(args.get(0)).expect("native Proxy target"); + let key = args.get(1); + let index = key + .is_string() + .then(|| key.to_rust_string_lossy(scope)) + .and_then(|key| { + key.parse::() + .ok() + .filter(|index| *index != u32::MAX && index.to_string() == key) + }); + let target = if let Some(index) = index { + if index < values(scope, object).length() { + rv.set_bool(false); + return; + } + // Ordinary [[Set]] begins at the prototype for an unsupported index. + // Its eventual own creation uses this Proxy's [[DefineOwnProperty]], + // rather than V8's interceptor Set fast path which bypasses definer. + object + .get_prototype(scope) + .and_then(|value| v8::Local::::try_from(value).ok()) + .unwrap_or_else(|| crate::util::new_null_prototype_object(scope)) + } else { + object + }; + let reflect_set = + v8::Local::::try_from(args.data()).expect("captured Reflect.set"); + if let Some(result) = reflect_set.call( + scope, + v8::undefined(scope).into(), + &[target.into(), key, args.get(2), args.get(3)], + ) { + rv.set(result); + } +} + +fn indexed_deleter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if index >= values(scope, args.holder()).length() { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn indexed_definer( + _scope: &mut v8::PinScope<'_, '_>, + index: u32, + _descriptor: &v8::PropertyDescriptor, + _args: v8::PropertyCallbackArguments<'_>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if index == u32::MAX { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn indexed_enumerator<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let length = values(scope, args.holder()).length(); + let keys = (0..length) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn indexed_descriptor<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = item(scope, args.holder(), index) else { + return v8::Intercepted::kNo; + }; + let descriptor = DataPropertyDescriptorDeclaration::new(value, false, true) + .bind(scope) + .expect("SourceBufferList indexed descriptor should allocate"); + rv.set(descriptor.into()); + v8::Intercepted::kYes +} diff --git a/moli-renderer-v8/src/context_bootstrap/media_source/state.rs b/moli-renderer-v8/src/context_bootstrap/media_source/state.rs index 3268250481..9ce329fdc1 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_source/state.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_source/state.rs @@ -26,6 +26,10 @@ pub(crate) struct MediaSourceObject { struct MediaSourceInstanceDeclaration<'scope> { #[webapi(slot = MEDIA_SOURCE_ID_SLOT)] id: v8::Local<'scope, v8::BigInt>, + #[webapi(slot = crate::context_bootstrap::shared::SIMPLE_EVENT_TARGET_SLOT, value = super::LISTENERS)] + event_target: (), + #[webapi(slot = crate::context_bootstrap::shared::SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT, init = true)] + ordered_handlers: (), } type Store = Rc>; diff --git a/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs b/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs index da4480a296..622c453752 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs @@ -27,6 +27,9 @@ pub(super) fn shared_target_owner<'s>( scope: &mut v8::PinScope<'s, '_>, target: v8::Local<'s, v8::Object>, ) -> v8::Local<'s, v8::Object> { + // Registered native Proxies share the backing object's listener registry. + // Keep the original wrapper separately for callback `this` and Event.target. + let target = moli_webapi_declare::web_api_object_target(scope, target).unwrap_or(target); get_private_object(scope, target, OWNER).unwrap_or(target) } diff --git a/moli-renderer-v8/src/script_vm/tests/media_source_closed.js b/moli-renderer-v8/src/script_vm/tests/media_source_closed.js new file mode 100644 index 0000000000..dfcdf997a8 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/media_source_closed.js @@ -0,0 +1,64 @@ +(() => { + const checks=[],diagnostics=[]; + const check=(name,action)=>{try{checks.push({name,passed:action()===true})}catch(error){checks.push({name,passed:false,error:String(error)})}}; + const throws=(C,action,name)=>{let error;try{action()}catch(caught){error=caught}return error instanceof C&&(name===undefined||error.name===name)}; + const frame=document.createElement('iframe');document.body.appendChild(frame);const popup=window.open('about:blank'); + try { + for(const [label,w] of [['main',window],['iframe',frame.contentWindow],['popup',popup]]) { + const P=w.MediaSource.prototype,L=w.SourceBufferList.prototype,source=new w.MediaSource(); + const getters=['sourceBuffers','activeSourceBuffers','readyState','duration','onsourceopen','onsourceended','onsourceclose']; + const methods=[['addSourceBuffer',1],['removeSourceBuffer',1],['endOfStream',0],['setLiveSeekableRange',2],['clearLiveSeekableRange',0]]; + for(const [name,C] of [['MediaSource',w.MediaSource],['SourceBufferList',w.SourceBufferList]]){ + check(label+'/'+name+'/interface',()=>typeof C==='function'&&Object.getPrototypeOf(C.prototype)===w.EventTarget.prototype); + check(label+'/'+name+'/function call',()=>throws(w.TypeError,()=>C())); + } + check(label+'/SourceBufferList/illegal constructor',()=>throws(w.TypeError,()=>new w.SourceBufferList())); + for(const name of getters)check(label+'/MediaSource/descriptor/'+name,()=>{ + const d=Object.getOwnPropertyDescriptor(P,name),set=name==='duration'||name.startsWith('on'); + return d.enumerable&&d.configurable&&d.get.length===0&&d.get.name==='get '+name&&(set?d.set.length===1&&d.set.name==='set '+name:d.set===undefined); + }); + for(const [name,length] of methods)check(label+'/MediaSource/method/'+name,()=>{const d=Object.getOwnPropertyDescriptor(P,name);return d.enumerable&&d.configurable&&d.writable&&d.value.length===length&&d.value.name===name}); + for(const name of ['length','onaddsourcebuffer','onremovesourcebuffer'])check(label+'/SourceBufferList/descriptor/'+name,()=>{const d=Object.getOwnPropertyDescriptor(L,name);return d.enumerable&&d.configurable&&d.get.length===0&&(name==='length'?d.set===undefined:d.set.length===1)}); + check(label+'/SourceBufferList/iterator descriptor',()=>{const d=Object.getOwnPropertyDescriptor(L,Symbol.iterator);return d.value===w.Array.prototype.values&&!d.enumerable&&d.configurable&&d.writable}); + check(label+'/MediaSource/closed defaults',()=>source.readyState==='closed'&&Number.isNaN(source.duration)&&source.onsourceopen===null&&source.onsourceended===null&&source.onsourceclose===null); + for(const name of ['sourceBuffers','activeSourceBuffers'])check(label+'/MediaSource/native list/'+name,()=>{const list=source[name];return list instanceof w.SourceBufferList&&list instanceof w.EventTarget&&!w.Array.isArray(list)&&list===source[name]&&list.length===0&&list[0]===undefined&&Object.keys(list).length===0&&Object.prototype.toString.call(list)==='[object SourceBufferList]'&&[...list].length===0}); + check(label+'/MediaSource/distinct lists',()=>source.sourceBuffers!==source.activeSourceBuffers&&source.sourceBuffers!==new w.MediaSource().sourceBuffers); + for(const [name,args,C,errorName] of [ + ['addSourceBuffer',[],w.TypeError],['addSourceBuffer',[''],w.TypeError],['addSourceBuffer',['video/moli'],w.DOMException,'NotSupportedError'],['addSourceBuffer',['video/mp4;codecs="avc1.4d001e"'],w.DOMException,'InvalidStateError'], + ['removeSourceBuffer',[],w.TypeError],['removeSourceBuffer',[{}],w.TypeError],['endOfStream',[],w.DOMException,'InvalidStateError'],['endOfStream',['decode'],w.DOMException,'InvalidStateError'],['endOfStream',['network'],w.DOMException,'InvalidStateError'],['endOfStream',[undefined],w.DOMException,'InvalidStateError'],['endOfStream',[''],w.TypeError],['endOfStream',['other'],w.TypeError], + ['setLiveSeekableRange',[],w.TypeError],['setLiveSeekableRange',[0],w.TypeError],['setLiveSeekableRange',[NaN,1],w.TypeError],['setLiveSeekableRange',[0,Infinity],w.TypeError],['setLiveSeekableRange',[-1,0],w.DOMException,'InvalidStateError'],['setLiveSeekableRange',[2,1],w.DOMException,'InvalidStateError'],['setLiveSeekableRange',[0,1],w.DOMException,'InvalidStateError'],['clearLiveSeekableRange',[],w.DOMException,'InvalidStateError'] + ])check(label+'/MediaSource/error/'+name+'/'+args.map(String).join(','),()=>throws(C,()=>Reflect.apply(P[name],source,args),errorName)); + const setter=Object.getOwnPropertyDescriptor(P,'duration')?.set; + for(const [index,value] of [undefined,NaN,-1,-Infinity,0,-0,1,Infinity].entries())check(label+'/duration/value/'+index,()=>throws(value===undefined||Number.isNaN(value)||value<0?w.TypeError:w.DOMException,()=>setter.call(source,value),value!==undefined&&!Number.isNaN(value)&&value>=0?'InvalidStateError':undefined)); + for(const kind of ['addSourceBuffer','endOfStream','duration','setLiveSeekableRange'])check(label+'/conversion/'+kind,()=>{ + const order=[],marker={};const first={toString(){order.push('first');throw marker},valueOf(){order.push('first');throw marker}},second={valueOf(){order.push('second');return 1}}; + let error;try{if(kind==='duration')setter.call(source,first);else P[kind].call(source,first,second)}catch(caught){error=caught} + return error===marker&&order.join()==='first'; + }); + const bad=[{},Object.create(P),Object.create(source),new Proxy(source,{})];const revoked=Proxy.revocable(source,{});revoked.revoke();bad.push(revoked.proxy); + for(const [index,receiver] of bad.entries()) { + for(const name of getters)check(label+'/invalid getter/'+index+'/'+name,()=>throws(w.TypeError,()=>Object.getOwnPropertyDescriptor(P,name).get.call(receiver))); + for(const name of ['duration','onsourceopen','onsourceended','onsourceclose'])check(label+'/invalid setter/'+index+'/'+name,()=>{let conversions=0;return throws(w.TypeError,()=>Object.getOwnPropertyDescriptor(P,name).set.call(receiver,{valueOf(){conversions++;throw Error()}}))&&conversions===0}); + for(const [name] of methods)check(label+'/invalid method/'+index+'/'+name,()=>{let conversions=0;const argument={toString(){conversions++;throw Error()},valueOf(){conversions++;throw Error()}};return throws(w.TypeError,()=>P[name].call(receiver,argument,argument))&&conversions===0}); + } + for(const name of ['onsourceopen','onsourceended','onsourceclose'])check(label+'/handler order/'+name,()=>{ + const events=[],event=name.slice(2),handler=function(e){events.push(this===source&&e.target===source?'handler':'bad');return false}; + source.addEventListener(event,()=>events.push('first'));source[name]=()=>events.push('old');source.addEventListener(event,()=>events.push('last'));source[name]=handler; + const canceled=source.dispatchEvent(new w.Event(event,{cancelable:true}));source[name]=null; + return canceled===false&&events.join()==='first,handler,last'&&source[name]===null; + }); + for(const name of ['onaddsourcebuffer','onremovesourcebuffer'])check(label+'/list handler/'+name,()=>{ + const list=source.sourceBuffers,events=[],event=name.slice(2);list.addEventListener(event,()=>events.push('first'));list[name]=()=>events.push('old');list.addEventListener(event,()=>events.push('last'));list[name]=function(e){events.push(this===list&&e.target===list?'handler':'bad');return false}; + const result=list.dispatchEvent(new w.Event(event,{cancelable:true}));list[name]=null;return !result&&events.join()==='first,handler,last'&&list[name]===null; + }); + for(const name of ['length','onaddsourcebuffer','onremovesourcebuffer'])for(const [index,receiver] of [{},Object.create(source.sourceBuffers ?? null),new Proxy(source.sourceBuffers ?? {},{})].entries())check(label+'/list invalid receiver/'+name+'/'+index,()=>throws(w.TypeError,()=>Object.getOwnPropertyDescriptor(L,name).get.call(receiver))); + for(const index of ['0','4294967294'])check(label+'/list readonly index/'+index,()=>{const list=source.sourceBuffers;return Reflect.set(list,index,{})===false&&!Object.hasOwn(list,index)&&Reflect.defineProperty(list,index,{value:{},configurable:true})===false&&Reflect.deleteProperty(list,index)===true}); + for(const key of ['4294967295','01','-1'])check(label+'/list expando/'+key,()=>{const list=source.sourceBuffers;return Reflect.set(list,key,'value')&&list[key]==='value'&&Reflect.deleteProperty(list,key)}); + check(label+'/list prevent extensions',()=>Reflect.preventExtensions(source.sourceBuffers)===false&&Object.isExtensible(source.sourceBuffers)); + check(label+'/list intrinsic realm',()=>{const other=label==='main'?frame.contentWindow:window;return Object.getOwnPropertyDescriptor(other.MediaSource.prototype,'sourceBuffers').get.call(source) instanceof w.SourceBufferList}); + check(label+'/list intrinsic survives author global',()=>{const intrinsic=w.SourceBufferList;try{Object.defineProperty(w,'SourceBufferList',{get(){throw Error('author constructor getter')},configurable:true});const list=new w.MediaSource().sourceBuffers;return list instanceof intrinsic}finally{Object.defineProperty(w,'SourceBufferList',{value:intrinsic,writable:true,configurable:true})}}); + diagnostics.push({realm:label,sourceTag:Object.prototype.toString.call(source),state:source.readyState,listLength:source.sourceBuffers?.length}); + } + } finally {frame.remove();popup?.close();} + globalThis.__uiEventResults={complete:true,checks,diagnostics,passed:checks.filter(row=>row.passed).length,total:checks.length};return checks.every(row=>row.passed); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/media_source_closed.rs b/moli-renderer-v8/src/script_vm/tests/media_source_closed.rs new file mode 100644 index 0000000000..87ba4d34eb --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/media_source_closed.rs @@ -0,0 +1,150 @@ +use super::*; + +#[test] +fn media_source_closed_frontend_validates_receivers_realms_and_conversion_order() { + for url in ["https://mse-closed.test/", "http://mse-closed.test/"] { + let mut vm = new_storage_page_task_executor_test_vm(url); + vm.eval(include_str!("media_source_closed.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(), "477"); + } +} + +fn native_source_buffer_list_vm() -> crate::runtime::PageVmTaskExecutorTestHarness { + use moli_webapi_declare::WebApiObject; + + #[derive(WebApiObject)] + #[webapi(interface = crate::web_api_interfaces::SourceBuffer, require_prototype)] + struct TestSourceBuffer {} + + let mut vm = new_storage_page_task_executor_test_vm("https://mse-list.test/"); + vm.eval("globalThis.source = new MediaSource(); 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 owner = v8::Local::::try_from( + global + .get(scope, crate::util::v8str(scope, "source").into()) + .unwrap(), + ) + .unwrap(); + let first = TestSourceBuffer::new().bind(scope).unwrap(); + let second = TestSourceBuffer::new().bind(scope).unwrap(); + let backing = v8::Array::new_with_elements(scope, &[first.into(), second.into()]); + let list = crate::context_bootstrap::media_source::source_buffer_list::build( + scope, owner, backing, + ); + assert!(list.is_proxy(), "native lists use a registered Proxy"); + assert!(crate::web_api_interfaces::SourceBufferList::is_instance( + scope, list + )); + for (name, value) in [ + ("backing", backing.into()), + ("buffers", list.into()), + ("nativeBuffers", list.into()), + ("first", first.into()), + ("second", second.into()), + ] { + assert_eq!( + global.create_data_property(scope, crate::util::v8str(scope, name).into(), value), + Some(true) + ); + } + Ok(()) + }) + .unwrap(); + vm +} + +#[test] +fn media_source_buffer_list_indexes_and_array_iterator_observe_native_backing() { + let mut vm = native_source_buffer_list_vm(); + assert_eq!(vm.eval(r#"(() => { + const assert=(ok,name)=>{if(!ok)throw Error(name)}; + assert(buffers.length===2&&buffers[0]===first&&buffers[1]===second,'native indexed identity'); + assert(Object.keys(buffers).join()==='0,1','supported indices are enumerable'); + const d=Object.getOwnPropertyDescriptor(buffers,'0'); + assert(d.value===first&&d.writable===false&&d.enumerable&&d.configurable,'indexed descriptor'); + for(const key of ['0','1','2','4294967294']) { + assert(Reflect.set(buffers,key,{})===false,'readonly array index set '+key); + assert(Reflect.defineProperty(buffers,key,{value:first,configurable:true})===false,'readonly define '+key); + assert(Reflect.defineProperty(buffers,key,{get(){throw Error('getter')}})===false,'readonly accessor define '+key); + } + assert(Reflect.deleteProperty(buffers,'0')===false&&Reflect.deleteProperty(buffers,'2')===true,'delete supported and unsupported indices'); + const iterator=buffers[Symbol.iterator](); + assert(iterator.next().value===first,'ArrayProtoValues first member'); + backing[1]=first;backing.push(second); + assert(iterator.next().value===first&&iterator.next().value===second&&iterator.next().done,'iterator observes the live backing'); + backing.length=0; + assert(buffers.length===0&&!Object.hasOwn(buffers,0)&&Object.keys(buffers).length===0,'shrinking removes supported indices'); + const p=Object.create(SourceBufferList.prototype); + Object.defineProperty(p,'0',{get(){return 'prototype'},set(value){this.expando=value},configurable:true}); + Object.setPrototypeOf(buffers,p); + assert(buffers[0]==='prototype'&&Reflect.set(buffers,'0','setter')&&buffers.expando==='setter','unsupported index uses the prototype'); + backing.push(first); + assert(buffers[0]===first&&!Reflect.set(buffers,'0',second),'supported index masks the prototype'); + const other={};assert(Reflect.set(buffers,'4','ordinary',other)&&other[4]==='ordinary','ordinary set receiver'); + const reflectSet=Reflect.set; + try { + Reflect.set=()=>{throw Error('author Reflect.set')}; + assert(reflectSet(buffers,'4','ignored')===false,'captured intrinsic survives author replacement'); + } finally {Reflect.set=reflectSet} + Object.setPrototypeOf(buffers,new Proxy({}, {set(target,key,value,receiver){ + assert(key==='4'&&value==='primitive'&&receiver===42,'prototype Proxy receives the original arguments');return true; + }})); + assert(Reflect.set(buffers,'4','primitive',42),'prototype Proxy with a primitive receiver'); + Object.setPrototypeOf(buffers,null); + assert(Reflect.set(buffers,'4',1)===false&&Reflect.set(buffers,'4',1,42)===false,'null prototype readonly creation'); + assert(Reflect.preventExtensions(buffers)===false&&Object.isExtensible(buffers),'legacy object remains extensible'); + return true; + })()"#).unwrap(), "true"); +} + +#[test] +fn media_source_buffer_list_native_proxies_and_foreign_source_buffers_use_brands() { + let mut vm = native_source_buffer_list_vm(); + assert_eq!(vm.eval(r#"(() => { + const assert=(ok,name)=>{if(!ok)throw Error(name)}; + const other=document.querySelector('iframe').contentWindow; + const log=[],controller=new AbortController(); + const listener=function(e){assert(this===buffers&&e.target===buffers,'native wrapper event identity');log.push('listener')}; + buffers.addEventListener('custom',listener,{once:true}); + buffers.dispatchEvent(new Event('custom'));buffers.dispatchEvent(new Event('custom')); + assert(log.join()==='listener','once across the registered native Proxy'); + buffers.addEventListener('custom',listener,{signal:controller.signal});controller.abort(); + buffers.dispatchEvent(new Event('custom'));assert(log.length===1,'aborting removes the native proxy listener'); + buffers.addEventListener('custom',listener);buffers.removeEventListener('custom',listener); + buffers.dispatchEvent(new Event('custom'));assert(log.length===1,'remove uses the same backing registry'); + try { + Object.prototype.get=()=>{throw Error('polluted get trap')}; + Object.prototype.ownKeys=()=>{throw Error('polluted ownKeys trap')}; + assert(buffers[0]===first&&Object.keys(buffers).join()==='0,1','null prototype native handler'); + } finally {delete Object.prototype.get;delete Object.prototype.ownKeys} + for(const w of [window,other]) { + const getLength=Object.getOwnPropertyDescriptor(w.SourceBufferList.prototype,'length').get; + assert(getLength.call(nativeBuffers)===2,'registered native Proxy'); + let traps=0; + const proxy=new Proxy(nativeBuffers,{get(){traps++;throw Error('author trap')}}); + const revoked=Proxy.revocable(buffers,{});revoked.revoke(); + for(const receiver of [{},Object.create(buffers),proxy,revoked.proxy]) { + let error;try{getLength.call(receiver)}catch(caught){error=caught} + assert(error instanceof w.TypeError&&traps===0,'callee realm brand check without traps'); + } + let error;try{w.MediaSource.prototype.removeSourceBuffer.call(source,first)}catch(caught){error=caught} + assert(error instanceof w.DOMException&&error.name==='NotFoundError','genuine buffer missing from closed source'); + for(const argument of [Object.create(first),new Proxy(first,{})]) { + let error;try{w.MediaSource.prototype.removeSourceBuffer.call(source,argument)}catch(caught){error=caught} + assert(error instanceof w.TypeError,'buffer argument uses native interface conversion'); + } + } + Object.setPrototypeOf(buffers,null); + assert(Object.getOwnPropertyDescriptor(other.SourceBufferList.prototype,'length').get.call(nativeBuffers)===2,'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 176919fcf8..7ea4664131 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2308,6 +2308,7 @@ mod media_key_status_map; mod media_metadata; mod media_owner_playback_interfaces; mod media_recorder; +mod media_source_closed; mod media_streams; mod midi_frontend; mod midi_owner_interfaces; diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index 22481d22f7..45ac18c4cf 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -207,6 +207,14 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[ 2, DirectCallOwner::NativeForwardingOrScript, ), + // SourceBufferList's native Proxy forwards [[Set]] to Reflect.set captured + // before author script. Its private handler retains only that intrinsic; + // this is native forwarding, not a page-supplied Web IDL callback. + allowed( + "context_bootstrap/media_source/source_buffer_list.rs", + 1, + DirectCallOwner::NativeForwardingOrScript, + ), allowed( "context_bootstrap/runtime_state.rs", 1, diff --git a/moli-webapi-declare/src/constructor.rs b/moli-webapi-declare/src/constructor.rs index 58368f2e13..7a8f6e1d0a 100644 --- a/moli-webapi-declare/src/constructor.rs +++ b/moli-webapi-declare/src/constructor.rs @@ -9,6 +9,7 @@ use moli_v8_util::{ }; const REFLECT_CONSTRUCT: &str = "moli.webidl.reflectConstruct"; +const REFLECT_SET: &str = "moli.webidl.reflectSet"; const INVOCATION: &str = "moli.webidl.constructorInvocation"; const NEW_TARGET: &str = "moli.webidl.originalNewTarget"; const SELECTED_PROTOTYPE: &str = "moli.webidl.selectedConstructorPrototype"; @@ -16,17 +17,32 @@ const SELECTED_PROTOTYPE: &str = "moli.webidl.selectedConstructorPrototype"; /// Capture before author script runs, including before lazy interface access. pub fn capture_web_api_constructor_intrinsics<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<()> { let global = scope.get_current_context().global(scope); - if get_private_value(scope, global, REFLECT_CONSTRUCT).is_some() { + if get_private_value(scope, global, REFLECT_CONSTRUCT).is_some() + && get_private_value(scope, global, REFLECT_SET).is_some() + { return Some(()); } let reflect = global.get(scope, v8str(scope, "Reflect").into())?; let reflect = v8::Local::::try_from(reflect).ok()?; - let construct = reflect.get(scope, v8str(scope, "construct").into())?; - let construct = v8::Local::::try_from(construct).ok()?; - let key = private_key(scope, REFLECT_CONSTRUCT)?; - global - .set_private(scope, key, construct.into())? - .then_some(()) + for (name, slot) in [("construct", REFLECT_CONSTRUCT), ("set", REFLECT_SET)] { + let function = reflect.get(scope, v8str(scope, name).into())?; + let function = v8::Local::::try_from(function).ok()?; + let key = private_key(scope, slot)?; + global + .set_private(scope, key, function.into())? + .then_some(())?; + } + Some(()) +} + +/// The realm's captured Reflect.set, including its original receiver semantics. +/// Native exotic objects must not consult a possibly replaced author global. +pub fn web_api_reflect_set<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> Option> { + let global = scope.get_current_context().global(scope); + get_private_value(scope, global, REFLECT_SET) + .and_then(|value| v8::Local::::try_from(value).ok()) } /// Keep the native function as the intrinsic constructor; publish this entry diff --git a/moli-webapi-declare/src/declaration.rs b/moli-webapi-declare/src/declaration.rs index d52213a313..f59bcbb2fd 100644 --- a/moli-webapi-declare/src/declaration.rs +++ b/moli-webapi-declare/src/declaration.rs @@ -64,6 +64,10 @@ pub trait WebApiObjectDeclaration<'s> { #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable, no_dynamic_constructor)] pub struct DataPropertyDescriptorDeclaration<'scope> { + // V8 converts this internal record with ToPropertyDescriptor, which also + // reads inherited get/set fields. Author Object.prototype must not enter it. + #[webapi(prototype, value = v8::null(scope))] + prototype: (), value: v8::Local<'scope, v8::Value>, enumerable: bool, configurable: bool, @@ -73,6 +77,7 @@ pub struct DataPropertyDescriptorDeclaration<'scope> { impl<'scope> DataPropertyDescriptorDeclaration<'scope> { pub fn new(value: v8::Local<'scope, v8::Value>, writable: bool, enumerable: bool) -> Self { Self { + prototype: (), value, enumerable, configurable: true, diff --git a/moli-webapi-declare/src/lib.rs b/moli-webapi-declare/src/lib.rs index fc7c8f3f19..0c96cf2aa2 100644 --- a/moli-webapi-declare/src/lib.rs +++ b/moli-webapi-declare/src/lib.rs @@ -253,7 +253,7 @@ pub mod __private; pub use constructor::{ capture_web_api_constructor_intrinsics, initialize_web_api_constructor_receiver, - web_api_constructor_with_deferred_prototype, + web_api_constructor_with_deferred_prototype, web_api_reflect_set, }; pub use interface::WebApiInterfaceDescriptor; pub use moli_webapi_declare_derive::{WebApiFunctionTemplate, WebApiObject}; diff --git a/moli-webapi-declare/src/prototype.rs b/moli-webapi-declare/src/prototype.rs index c1ac56c28f..af6b36dc6e 100644 --- a/moli-webapi-declare/src/prototype.rs +++ b/moli-webapi-declare/src/prototype.rs @@ -68,11 +68,12 @@ where let prototype = prototype .to_v8_value(scope) .ok_or_else(|| BindError::new("failed to convert declared prototype"))?; - let prototype = v8::Local::::try_from(prototype) - .map_err(|_| BindError::new("declared prototype must be an object"))?; - let installed = object - .set_prototype(scope, prototype.into()) - .unwrap_or(false); + if !prototype.is_object() && !prototype.is_null() { + return Err(BindError::new( + "declared prototype must be an object or null", + )); + } + let installed = object.set_prototype(scope, prototype).unwrap_or(false); if !installed { return Err(BindError::new("failed to set declared prototype")); } diff --git a/moli-webapi-declare/tests/declare.rs b/moli-webapi-declare/tests/declare.rs index c77a5cbaf3..240aeadbe4 100644 --- a/moli-webapi-declare/tests/declare.rs +++ b/moli-webapi-declare/tests/declare.rs @@ -3094,3 +3094,55 @@ mod interfaces { pub(super) SampleTemplate; } } + +#[test] +fn null_prototype_declarations_ignore_object_prototype_pollution() { + #[derive(WebApiObject)] + #[webapi(plain)] + struct NullPrototype { + #[webapi(prototype, value = v8::null(scope))] + prototype: (), + } + + ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let object = NullPrototype::new().bind(scope).unwrap(); + assert!(object.get_prototype(scope).unwrap().is_null()); + let invalid = v8::Integer::new(scope, 1); + assert!(moli_webapi_declare::set_declared_prototype(scope, object, &invalid).is_err()); + assert!(object.get_prototype(scope).unwrap().is_null()); + run_script( + scope, + "Object.prototype.get = () => { throw Error('inherited getter') };", + ); + let value = v8::Integer::new(scope, 7); + let descriptor = + moli_webapi_declare::DataPropertyDescriptorDeclaration::new(value.into(), false, true) + .bind(scope) + .unwrap(); + assert!(descriptor.get_prototype(scope).unwrap().is_null()); + let global = context.global(scope); + assert_eq!( + global.create_data_property( + scope, + v8::String::new(scope, "descriptor").unwrap().into(), + descriptor.into() + ), + Some(true) + ); + assert!( + run_script( + scope, + r#"(() => { + const target={};Object.defineProperty(target,'value',descriptor); + const d=Object.getOwnPropertyDescriptor(target,'value'); + return target.value===7 && d.writable===false && d.enumerable && d.configurable; + })()"# + ) + .is_true() + ); +}