mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
feat(media): add closed MediaSource frontend and buffer lists
This commit is contained in:
+1
@@ -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<v8::Local<'s, v8::Value>> {
|
||||
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?;
|
||||
|
||||
@@ -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<EndOfStreamError>,
|
||||
}
|
||||
|
||||
#[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::<DurationArgs>(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::<AddArgs>(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::<RemoveArgs>(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::<EndArgs>(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::<RangeArgs>(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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<v8::Array>::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<v8::Local<'s, v8::Value>> {
|
||||
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::<v8::Object>::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::<u32>()
|
||||
.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::<v8::Object>::try_from(value).ok())
|
||||
.unwrap_or_else(|| crate::util::new_null_prototype_object(scope))
|
||||
} else {
|
||||
object
|
||||
};
|
||||
let reflect_set =
|
||||
v8::Local::<v8::Function>::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::<Vec<_>>();
|
||||
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
|
||||
}
|
||||
@@ -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<RefCell<MediaSourceObjects>>;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
})()
|
||||
@@ -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 = '<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 owner = v8::Local::<v8::Object>::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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<v8::Object>::try_from(reflect).ok()?;
|
||||
let construct = reflect.get(scope, v8str(scope, "construct").into())?;
|
||||
let construct = v8::Local::<v8::Function>::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::<v8::Function>::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<v8::Local<'s, v8::Function>> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
get_private_value(scope, global, REFLECT_SET)
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
}
|
||||
|
||||
/// Keep the native function as the intrinsic constructor; publish this entry
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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::<v8::Object>::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"));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user