fix(file-api): enforce WebIDL receiver contracts

This commit is contained in:
ldm0
2026-09-27 07:55:59 +08:00
parent b1f2a1699e
commit 4e64dbddd8
19 changed files with 450 additions and 109 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ document.body.setAttribute("data-file-type", file.type);
document.body.setAttribute("data-file-last-modified", String(file.lastModified));
document.body.setAttribute("data-file-size", String(file.size));
const list = new FileList([file]);
const transfer = new DataTransfer();
transfer.items.add(file);
const list = transfer.files;
document.body.setAttribute("data-file-list-tag", Object.prototype.toString.call(list));
document.body.setAttribute("data-file-list-length", String(list.length));
document.body.setAttribute("data-file-list-item-same", String(list.item(0) === file));
+44
View File
@@ -110,6 +110,10 @@ fn blob_size_attribute_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let size = blob_bytes_from_object(scope, args.this())
.map(|bytes| bytes.len() as f64)
.unwrap_or(0.0);
@@ -121,6 +125,10 @@ fn blob_type_attribute_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let mime_type = blob_mime_type_from_object(scope, args.this()).unwrap_or_default();
if let Some(value) = v8_string(scope, &mime_type) {
rv.set(value.into());
@@ -134,6 +142,10 @@ pub(super) fn blob_text_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
set_rejected_type_error_promise(scope, &mut rv, "Illegal invocation");
return;
}
let text = blob_bytes_from_object(scope, args.this())
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
.unwrap_or_default();
@@ -149,6 +161,10 @@ pub(super) fn blob_array_buffer_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
set_rejected_type_error_promise(scope, &mut rv, "Illegal invocation");
return;
}
let bytes = blob_bytes_from_object(scope, args.this()).unwrap_or_default();
let value = array_buffer_from_bytes(scope, bytes)
.map(|buffer| buffer.into())
@@ -161,6 +177,10 @@ pub(super) fn blob_bytes_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
set_rejected_type_error_promise(scope, &mut rv, "Illegal invocation");
return;
}
let bytes = blob_bytes_from_object(scope, args.this()).unwrap_or_default();
let value = new_uint8_array_from_bytes(scope, bytes)
.map(|array| array.into())
@@ -173,6 +193,10 @@ pub(super) fn blob_stream_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(bytes) = blob_bytes_from_object(scope, args.this()) else {
rv.set(v8::undefined(scope).into());
return;
@@ -196,6 +220,10 @@ pub(super) fn blob_slice_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !is_blob_object(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let bytes = blob_bytes_from_object(scope, args.this()).unwrap_or_default();
let size = bytes.len();
let start = clamped_long_long_arg(scope, &args, 0).unwrap_or(0);
@@ -940,3 +968,19 @@ fn set_resolved_promise(
let _ = resolver.resolve(scope, value);
rv.set(promise.into());
}
fn set_rejected_type_error_promise(
scope: &mut v8::PinScope<'_, '_>,
rv: &mut v8::ReturnValue<'_, v8::Value>,
message: &str,
) {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
rv.set(v8::undefined(scope).into());
return;
};
let promise = resolver.get_promise(scope);
let message = v8_string(scope, message).unwrap_or_else(|| v8::String::empty(scope));
let reason = v8::Exception::type_error(scope, message);
let _ = resolver.reject(scope, reason);
rv.set(promise.into());
}
@@ -16,8 +16,7 @@ use super::super::{
exposed_interfaces::{TemplateBuildProfile, install_interface_template_metadata},
file_api::{
data_transfer_constructor_callback, file_constructor_callback,
file_list_constructor_callback, file_reader_constructor_callback,
file_reader_sync_constructor_callback,
file_reader_constructor_callback, file_reader_sync_constructor_callback,
},
form_data_runtime::build_form_data_constructor_template,
geometry_runtime::{
@@ -463,14 +462,6 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>(
.length(2)
.build(scope)
}
ConstructorKind::FileList => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::FileList,
file_list_constructor_callback
))
.length(0)
.build(scope)
}
ConstructorKind::FileReader => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::FileReader,
@@ -37,11 +37,11 @@ pub(crate) use file_entry_file::{
FileEntryFileCallbackTask, FileEntryFileCallbackTaskEffect,
file_system_file_entry_file_callback,
};
pub(super) use file_list::file_list_item_callback;
pub(crate) use file_list::{
build_file_list_object, file_list_files_from_object, is_file_list_object,
sync_file_list_contents,
};
pub(super) use file_list::{file_list_constructor_callback, file_list_item_callback};
pub(crate) use file_reader::flush_one_pending_file_reader;
pub(super) use file_reader::{
file_reader_abort_callback, file_reader_add_event_listener_callback,
@@ -86,6 +86,10 @@ fn file_name_attribute_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let name = file_name_from_object(scope, args.this()).unwrap_or_default();
if let Some(value) = v8_string(scope, &name) {
rv.set(value.into());
@@ -106,12 +110,23 @@ fn file_last_modified_attribute_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let last_modified = get_private_value(scope, args.this(), FILE_LAST_MODIFIED_SLOT)
.and_then(|value| value.number_value(scope))
.unwrap_or_else(unix_epoch_millis);
rv.set(v8::Number::new(scope, last_modified).into());
}
fn file_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, receiver, FILE_NAME_SLOT).is_some()
}
fn private_string_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -6,10 +6,14 @@ use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
const FILE_LIST_LENGTH_SLOT: &str = "__lmFileListLength";
const FILE_LIST_FILES_SLOT: &str = "__lmFileListFiles";
const FILE_LIST_BRAND_SLOT: &str = "__lmFileListBrand";
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::FileList, require_prototype)]
struct FileListObjectDeclaration {
#[webapi(slot = FILE_LIST_BRAND_SLOT, init = true)]
brand: (),
#[webapi(slot = FILE_LIST_LENGTH_SLOT)]
length: f64,
}
@@ -42,22 +46,6 @@ pub(super) fn install_file_list_template_bindings<'s>(
);
}
pub(in crate::context_bootstrap) fn file_list_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,
"Failed to construct 'FileList': Please use the 'new' operator.",
);
return;
}
initialize_file_list_object(scope, args.this(), args.get(0));
rv.set(args.this().into());
}
pub(crate) fn build_file_list_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
files: &[v8::Local<'s, v8::Object>],
@@ -153,6 +141,10 @@ pub(in crate::context_bootstrap) fn file_list_item_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_list_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(parsed) = webidl::parse_args::<FileListItemArgs>(scope, &args) else {
return;
};
@@ -172,6 +164,10 @@ fn file_list_length_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_list_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let length = file_list_length_from_object(scope, args.this())
.filter(|value| value.is_finite() && *value >= 0.0)
.unwrap_or(0.0);
@@ -186,33 +182,10 @@ fn file_list_length_from_object<'s>(
.and_then(|value| value.number_value(scope))
}
fn initialize_file_list_object<'s>(
fn file_list_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
files_value: v8::Local<'s, v8::Value>,
) {
let files = if files_value.is_null_or_undefined() {
None
} else {
files_value.to_object(scope)
};
let length = files
.and_then(|value| value.get(scope, v8str(scope, "length").into()))
.and_then(|value| value.number_value(scope))
.filter(|value| value.is_finite() && *value >= 0.0)
.map(|value| value as u32)
.unwrap_or(0);
let mut contents = Vec::new();
for index in 0..length {
let file = files.and_then(|files| files.get_index(scope, index));
contents.push(file.unwrap_or_else(|| v8::undefined(scope).into()));
if let Some(file) = file {
let _ = object.set_index(scope, index, file);
}
}
let contents = v8::Array::new_with_elements(scope, &contents);
set_private_value(scope, object, FILE_LIST_FILES_SLOT, contents.into());
FileListObjectDeclaration::new(length as f64)
.initialize(scope, object)
.expect("FileList declaration should initialize object");
receiver: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, receiver, FILE_LIST_BRAND_SLOT)
.is_some_and(|value| value.boolean_value(scope))
}
@@ -21,10 +21,11 @@ pub(in crate::context_bootstrap::file_api) use state::install_file_reader_templa
pub(in crate::context_bootstrap::file_api::file_reader) use events::dispatch_file_reader_event;
pub(in crate::context_bootstrap::file_api::file_reader) use state::{
file_reader_pending_result, file_reader_pending_total, file_reader_read_id,
file_reader_ready_state, file_reader_scheduled, file_reader_task_phase,
initialize_file_reader_object, set_file_reader_error, set_file_reader_pending_result,
set_file_reader_pending_total, set_file_reader_read_id, set_file_reader_ready_state,
set_file_reader_result, set_file_reader_scheduled, set_file_reader_task_phase,
file_reader_ready_state, file_reader_receiver_branded, file_reader_scheduled,
file_reader_task_phase, initialize_file_reader_object, set_file_reader_error,
set_file_reader_pending_result, set_file_reader_pending_total, set_file_reader_read_id,
set_file_reader_ready_state, set_file_reader_result, set_file_reader_scheduled,
set_file_reader_task_phase,
};
pub(in crate::context_bootstrap::file_api::file_reader) use task::file_reader_flush_callback;
pub(crate) use task::flush_one_pending_file_reader;
@@ -24,6 +24,10 @@ pub(in crate::context_bootstrap) fn file_reader_add_event_listener_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
simple_object_event_target_add_listener(scope, &args, FILE_READER_LISTENERS_SLOT);
rv.set_undefined();
}
@@ -33,6 +37,10 @@ pub(in crate::context_bootstrap) fn file_reader_remove_event_listener_callback<'
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
simple_object_event_target_remove_listener(scope, &args, FILE_READER_LISTENERS_SLOT);
rv.set_undefined();
}
@@ -126,6 +126,10 @@ pub(in crate::context_bootstrap) fn file_reader_abort_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
if file_reader_ready_state(scope, args.this()) != 1.0 {
rv.set_undefined();
return;
@@ -177,6 +181,10 @@ fn ensure_file_reader_can_start_read<'s>(
reader: v8::Local<'s, v8::Object>,
rv: &mut v8::ReturnValue<'_, v8::Value>,
) -> bool {
if !file_reader_receiver_branded(scope, reader) {
throw_type_error(scope, "Illegal invocation");
return false;
}
if file_reader_ready_state(scope, reader) != 1.0 {
return true;
}
@@ -1,18 +1,31 @@
use super::*;
use crate::util::{get_private_value, set_private_value};
use crate::util::{callback_data_item, get_private_value, set_private_value};
use crate::web_api_interfaces;
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
const FILE_READER_READY_STATE_SLOT: &str = "__lmFileReaderReadyState";
const FILE_READER_RESULT_SLOT: &str = "__lmFileReaderResult";
const FILE_READER_ERROR_SLOT: &str = "__lmFileReaderError";
const FILE_READER_BRAND_SLOT: &str = "__lmFileReaderBrand";
const FILE_READER_ONLOADSTART_SLOT: &str = "__lmFileReaderOnloadstart";
const FILE_READER_ONPROGRESS_SLOT: &str = "__lmFileReaderOnprogress";
const FILE_READER_ONLOAD_SLOT: &str = "__lmFileReaderOnload";
const FILE_READER_ONABORT_SLOT: &str = "__lmFileReaderOnabort";
const FILE_READER_ONERROR_SLOT: &str = "__lmFileReaderOnerror";
const FILE_READER_ONLOADEND_SLOT: &str = "__lmFileReaderOnloadend";
#[derive(Default, WebApiObject)]
#[webapi(interface = web_api_interfaces::FileReader)]
struct FileReaderObjectDeclaration {
#[webapi(slot = FILE_READER_BRAND_SLOT, init = true)]
brand: (),
#[webapi(slot = SIMPLE_EVENT_TARGET_SLOT, value = FILE_READER_LISTENERS_SLOT)]
event_target_slot: (),
#[webapi(slot = SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT, init = true)]
ordered_handlers: (),
#[webapi(slot = FILE_READER_READY_STATE_SLOT, init = 0)]
ready_state: (),
@@ -22,23 +35,23 @@ struct FileReaderObjectDeclaration {
#[webapi(slot = FILE_READER_ERROR_SLOT, init = "null")]
error: (),
#[webapi(data_property = "onloadstart", init = "null")]
#[webapi(slot = FILE_READER_ONLOADSTART_SLOT, init = "null")]
onloadstart: (),
#[webapi(data_property = "onprogress", init = "null")]
#[webapi(slot = FILE_READER_ONPROGRESS_SLOT, init = "null")]
onprogress: (),
#[webapi(data_property = "onload", init = "null")]
#[webapi(slot = FILE_READER_ONLOAD_SLOT, init = "null")]
onload: (),
#[webapi(data_property = "onloadend", init = "null")]
onloadend: (),
#[webapi(slot = FILE_READER_ONABORT_SLOT, init = "null")]
onabort: (),
#[webapi(data_property = "onerror", init = "null")]
#[webapi(slot = FILE_READER_ONERROR_SLOT, init = "null")]
onerror: (),
#[webapi(data_property = "onabort", init = "null")]
onabort: (),
#[webapi(slot = FILE_READER_ONLOADEND_SLOT, init = "null")]
onloadend: (),
#[webapi(slot = FILE_READER_LISTENERS_SLOT, init = "null_object")]
listeners: (),
@@ -74,8 +87,95 @@ struct FileReaderPrototypeAccessorsDeclaration {
#[webapi(accessor_property, getter = file_reader_error_getter_callback, enumerable)]
error: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 0),
enumerable
)]
onloadstart: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 1),
enumerable
)]
onprogress: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 2),
enumerable
)]
onload: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 3),
enumerable
)]
onabort: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 4),
enumerable
)]
onerror: (),
#[webapi(
accessor_property,
getter = file_reader_event_handler_getter_callback,
setter = file_reader_event_handler_setter_callback,
data = crate::util::callback_data_index_value(scope, 5),
enumerable
)]
onloadend: (),
}
#[derive(Clone, Copy)]
struct FileReaderEventHandler {
event_type: &'static str,
slot_name: &'static str,
}
const FILE_READER_EVENT_HANDLERS: &[FileReaderEventHandler] = &[
FileReaderEventHandler {
event_type: "loadstart",
slot_name: FILE_READER_ONLOADSTART_SLOT,
},
FileReaderEventHandler {
event_type: "progress",
slot_name: FILE_READER_ONPROGRESS_SLOT,
},
FileReaderEventHandler {
event_type: "load",
slot_name: FILE_READER_ONLOAD_SLOT,
},
FileReaderEventHandler {
event_type: "abort",
slot_name: FILE_READER_ONABORT_SLOT,
},
FileReaderEventHandler {
event_type: "error",
slot_name: FILE_READER_ONERROR_SLOT,
},
FileReaderEventHandler {
event_type: "loadend",
slot_name: FILE_READER_ONLOADEND_SLOT,
},
];
pub(in crate::context_bootstrap::file_api) fn install_file_reader_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -95,6 +195,10 @@ fn file_reader_ready_state_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let ready_state = file_reader_ready_state(scope, args.this());
rv.set(v8::Number::new(scope, ready_state).into());
}
@@ -104,6 +208,10 @@ fn file_reader_result_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let result = args.this();
let result = file_reader_slot_value(scope, result, FILE_READER_RESULT_SLOT)
.unwrap_or_else(|| v8::null(scope).into());
@@ -115,12 +223,77 @@ fn file_reader_error_getter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let error = args.this();
let error = file_reader_slot_value(scope, error, FILE_READER_ERROR_SLOT)
.unwrap_or_else(|| v8::null(scope).into());
rv.set(error);
}
fn file_reader_event_handler_getter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(handler) = callback_data_item(
scope,
&args,
FILE_READER_EVENT_HANDLERS,
"FileReader event handlers",
) else {
rv.set_null();
return;
};
let value = get_private_value(scope, args.this(), handler.slot_name)
.unwrap_or_else(|| v8::null(scope).into());
if value.is_null_or_undefined() {
rv.set_null();
} else {
rv.set(value);
}
}
fn file_reader_event_handler_setter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if !file_reader_receiver_branded(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(handler) = callback_data_item(
scope,
&args,
FILE_READER_EVENT_HANDLERS,
"FileReader event handlers",
) else {
return;
};
let value = args.get(0);
let stored = if value.is_function() {
value
} else {
v8::null(scope).into()
};
set_private_value(scope, args.this(), handler.slot_name, stored);
simple_object_event_set_ordered_handler(
scope,
args.this(),
FILE_READER_LISTENERS_SLOT,
handler.event_type,
handler.slot_name,
stored.is_function(),
);
}
pub(in crate::context_bootstrap::file_api::file_reader) fn initialize_file_reader_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
reader: v8::Local<'s, v8::Object>,
@@ -130,6 +303,14 @@ pub(in crate::context_bootstrap::file_api::file_reader) fn initialize_file_reade
.expect("FileReader declaration should initialize");
}
pub(in crate::context_bootstrap::file_api::file_reader) fn file_reader_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, receiver, FILE_READER_BRAND_SLOT)
.is_some_and(|value| value.boolean_value(scope))
}
pub(in crate::context_bootstrap::file_api::file_reader) fn file_reader_ready_state<'s>(
scope: &mut v8::PinScope<'s, '_>,
reader: v8::Local<'s, v8::Object>,
@@ -961,7 +961,7 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
},
ConstructorSpec {
interface: web_api_interfaces::FileList::DESCRIPTOR,
kind: ConstructorKind::FileList,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::FileSystem::DESCRIPTOR,
@@ -71,7 +71,6 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
WebGlDebugRendererInfo,
WebGlLoseContext,
File,
FileList,
FileReader,
FileReaderSync,
XmlSerializer,
@@ -62,6 +62,9 @@ fn file_reader_backing_state_is_not_own_property_surface() {
].join(":");
};
const reader = new FileReader();
const eventHandlerNames = [
"onloadstart", "onprogress", "onload", "onabort", "onerror", "onloadend"
];
const leaked = () => Object.getOwnPropertyNames(reader)
.filter(name => name.startsWith("__lmFileReader") ||
name.startsWith("__moliFileReader"))
@@ -74,6 +77,19 @@ fn file_reader_backing_state_is_not_own_property_surface() {
__lmFileReaderResult: "spoofed",
__lmFileReaderError: "spoofed"
};
const throwsTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof TypeError;
}
};
const handler = () => {};
reader.onload = handler;
const handlerRoundTrips = reader.onload === handler;
reader.onload = {};
const nonCallableHandlerBecomesNull = reader.onload === null;
const before = leaked();
reader.addEventListener("load", () => {});
reader.readAsText(new Blob(["abc"]));
@@ -82,7 +98,8 @@ fn file_reader_backing_state_is_not_own_property_surface() {
descriptors: [
descriptorReport("readyState"),
descriptorReport("result"),
descriptorReport("error")
descriptorReport("error"),
...eventHandlerNames.map(descriptorReport)
],
constructorConstants: ["EMPTY", "LOADING", "DONE"].map(name =>
constantDescriptor(FileReader, name)
@@ -93,12 +110,18 @@ fn file_reader_backing_state_is_not_own_property_surface() {
readerOwnConstants: ["EMPTY", "LOADING", "DONE"].filter(name =>
Object.prototype.hasOwnProperty.call(reader, name)
),
readerOwnHandlers: eventHandlerNames.filter(name =>
Object.prototype.hasOwnProperty.call(reader, name)
),
before,
during,
resultIsNull: reader.result === null,
fakeReadyState: readyStateDescriptor.get.call(fake),
fakeResult: String(resultDescriptor.get.call(fake)),
fakeError: String(errorDescriptor.get.call(fake))
handlerRoundTrips,
nonCallableHandlerBecomesNull,
fakeReadyStateThrows: throwsTypeError(() => readyStateDescriptor.get.call(fake)),
fakeResultThrows: throwsTypeError(() => resultDescriptor.get.call(fake)),
fakeErrorThrows: throwsTypeError(() => errorDescriptor.get.call(fake)),
fakeAbortThrows: throwsTypeError(() => FileReader.prototype.abort.call(fake))
});
})()
"#,
@@ -107,7 +130,7 @@ fn file_reader_backing_state_is_not_own_property_surface() {
assert_eq!(
result,
r#"{"descriptors":["readyState:function:get readyState:0:undefined:true:true","result:function:get result:0:undefined:true:true","error:function:get error:0:undefined:true:true"],"constructorConstants":["EMPTY:0:true:false:false","LOADING:1:true:false:false","DONE:2:true:false:false"],"prototypeConstants":["EMPTY:0:true:false:false","LOADING:1:true:false:false","DONE:2:true:false:false"],"readerOwnConstants":[],"before":[],"during":[],"resultIsNull":true,"fakeReadyState":0,"fakeResult":"null","fakeError":"null"}"#
r#"{"descriptors":["readyState:function:get readyState:0:undefined:true:true","result:function:get result:0:undefined:true:true","error:function:get error:0:undefined:true:true","onloadstart:function:get onloadstart:0:function:true:true","onprogress:function:get onprogress:0:function:true:true","onload:function:get onload:0:function:true:true","onabort:function:get onabort:0:function:true:true","onerror:function:get onerror:0:function:true:true","onloadend:function:get onloadend:0:function:true:true"],"constructorConstants":["EMPTY:0:true:false:false","LOADING:1:true:false:false","DONE:2:true:false:false"],"prototypeConstants":["EMPTY:0:true:false:false","LOADING:1:true:false:false","DONE:2:true:false:false"],"readerOwnConstants":[],"readerOwnHandlers":[],"before":[],"during":[],"resultIsNull":true,"handlerRoundTrips":true,"nonCallableHandlerBecomesNull":true,"fakeReadyStateThrows":true,"fakeResultThrows":true,"fakeErrorThrows":true,"fakeAbortThrows":true}"#
);
}
#[test]
@@ -1096,3 +1119,63 @@ fn removing_child_frame_revokes_only_its_blob_object_urls() {
"removing a child frame must revoke object URLs created by its realm"
);
}
#[test]
fn blob_members_enforce_webidl_receiver_contracts() {
let mut vm = new_storage_test_vm("https://blob-receiver-brand.test/");
let result = vm
.eval(
r#"
(() => {
const throwsTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof TypeError;
}
};
const rejectionName = callback => {
try {
return Promise.resolve(callback()).then(
() => "resolved",
error => error && error.name,
);
} catch (error) {
return Promise.resolve("threw:" + (error && error.name));
}
};
const sizeGetter = Object.getOwnPropertyDescriptor(Blob.prototype, "size").get;
const typeGetter = Object.getOwnPropertyDescriptor(Blob.prototype, "type").get;
globalThis.__blobReceiverProbe = {
syncThrows: [
throwsTypeError(() => sizeGetter.call({})),
throwsTypeError(() => typeGetter.call({})),
throwsTypeError(() => Blob.prototype.slice.call(null)),
throwsTypeError(() => Blob.prototype.stream.call(null)),
],
};
Promise.all([
rejectionName(() => Blob.prototype.text.call(null)),
rejectionName(() => Blob.prototype.arrayBuffer.call(null)),
rejectionName(() => Blob.prototype.bytes.call(null)),
]).then(names => {
__blobReceiverProbe.promiseRejections = names;
});
return "scheduled";
})()
"#,
)
.expect("Blob receiver contract probe should evaluate");
assert_eq!(result, "scheduled");
vm.eval("0")
.expect("Blob receiver rejection microtasks should drain");
assert_eq!(
vm.eval("JSON.stringify(globalThis.__blobReceiverProbe)")
.expect("Blob receiver contract result should evaluate"),
r#"{"syncThrows":[true,true,true,true],"promiseRejections":["TypeError","TypeError","TypeError"]}"#
);
}
@@ -4583,6 +4583,14 @@ fn file_constructor_preserves_declared_metadata_slots() {
__lmFileLastModified: 99
};
const fakeFileList = { __lmFileListLength: 99 };
const throwsTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof TypeError;
}
};
return JSON.stringify({
fileDescriptors: [
descriptorReport(File.prototype, "name"),
@@ -4604,9 +4612,11 @@ fn file_constructor_preserves_declared_metadata_slots() {
tag: Object.prototype.toString.call(explicitFile),
ctor: explicitFile instanceof File,
weirdLastModifiedFinite: Number.isFinite(weirdFile.lastModified),
fakeName: nameDescriptor.get.call(fakeFile),
fakeLastModifiedFinite: Number.isFinite(lastModifiedDescriptor.get.call(fakeFile)),
fakeLength: lengthDescriptor.get.call(fakeFileList)
constructFileListThrows: throwsTypeError(() => new FileList()),
fakeNameThrows: throwsTypeError(() => nameDescriptor.get.call(fakeFile)),
fakeLastModifiedThrows: throwsTypeError(() => lastModifiedDescriptor.get.call(fakeFile)),
fakeLengthThrows: throwsTypeError(() => lengthDescriptor.get.call(fakeFileList)),
fakeItemThrows: throwsTypeError(() => FileList.prototype.item.call(fakeFileList, 0))
});
})()
"#,
@@ -4615,7 +4625,7 @@ fn file_constructor_preserves_declared_metadata_slots() {
assert_eq!(
result,
r#"{"fileDescriptors":["name:function:get name:0:undefined:true:true","lastModified:function:get lastModified:0:undefined:true:true"],"fileListDescriptors":["length:function:get length:0:undefined:true:true"],"fileInternalNames":[],"fileListInternalNames":[],"defaultName":"default.txt","defaultLastModifiedFinite":true,"explicitName":"note.txt","explicitLastModified":7,"explicitType":"text/plain","roundTripName":"note.txt","roundTripLastModified":7,"roundTripType":"text/plain","tag":"[object File]","ctor":true,"weirdLastModifiedFinite":true,"fakeName":"","fakeLastModifiedFinite":true,"fakeLength":0}"#
r#"{"fileDescriptors":["name:function:get name:0:undefined:true:true","lastModified:function:get lastModified:0:undefined:true:true"],"fileListDescriptors":["length:function:get length:0:undefined:true:true"],"fileInternalNames":[],"fileListInternalNames":[],"defaultName":"default.txt","defaultLastModifiedFinite":true,"explicitName":"note.txt","explicitLastModified":7,"explicitType":"text/plain","roundTripName":"note.txt","roundTripLastModified":7,"roundTripType":"text/plain","tag":"[object File]","ctor":true,"weirdLastModifiedFinite":true,"constructFileListThrows":true,"fakeNameThrows":true,"fakeLastModifiedThrows":true,"fakeLengthThrows":true,"fakeItemThrows":true}"#
);
}
#[test]
@@ -2603,19 +2603,23 @@ async fn worker_filelist_interface_object_is_available() {
ensure_v8();
let mut handle = spawn_worker(
r#"
const file = new File(["hello"], "note.txt", { type: "text/plain" });
const list = new FileList([file]);
let constructError = null;
try {
new FileList();
} catch (error) {
constructError = error && error.name;
}
postMessage({
ctorOwn: Object.prototype.hasOwnProperty.call(self, "FileList"),
ctorType: typeof FileList,
ctorName: list.constructor && list.constructor.name,
tag: Object.prototype.toString.call(list),
instanceofFileList: list instanceof FileList,
length: list.length,
firstName: list.item(0) && list.item(0).name,
indexName: list[0] && list[0].name,
iterType: typeof list[Symbol.iterator],
iterName: Array.from(list).map(file => file.name).join(","),
ctorName: FileList.name,
constructError,
itemType: typeof FileList.prototype.item,
lengthGetterType: typeof Object.getOwnPropertyDescriptor(
FileList.prototype,
"length",
).get,
iterType: typeof FileList.prototype[Symbol.iterator],
});
close();
"#
@@ -2629,7 +2633,7 @@ async fn worker_filelist_interface_object_is_available() {
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
r#"{"ctorOwn":true,"ctorType":"function","ctorName":"FileList","tag":"[object FileList]","instanceofFileList":true,"length":1,"firstName":"note.txt","indexName":"note.txt","iterType":"function","iterName":"note.txt"}"#
r#"{"ctorOwn":true,"ctorType":"function","ctorName":"FileList","constructError":"TypeError","itemType":"function","lengthGetterType":"function","iterType":"function"}"#
);
}
@@ -14,6 +14,14 @@ function assert_throws_name(expectedName, callback, description) {
throw new Error(description || "expected exception");
}
function file_list(files) {
const transfer = new DataTransfer();
for (const file of files) {
transfer.items.add(file);
}
return transfer.files;
}
promise_test(async function () {
const file = new File(["hello ", new Uint8Array([0xe2, 0x99, 0xa5])], "note.txt", {
type: "TEXT/PLAIN",
@@ -33,7 +41,8 @@ test(function () {
const file = new File(["hello"], "note.txt");
const second = new File(["second"], "second.txt");
const third = new File(["third"], "third.txt");
const list = new FileList([file, second, third]);
assert_throws_name("TypeError", () => new FileList(), "FileList is not constructible");
const list = file_list([file, second, third]);
assert_true(list instanceof FileList, "FileList instance");
assert_equals(list.length, 3, "FileList length");
@@ -54,6 +54,14 @@ function append_target_frame(name) {
return frame;
}
function file_list(files) {
const transfer = new DataTransfer();
for (const file of files) {
transfer.items.add(file);
}
return transfer.files;
}
test(function () {
const text = document.createElement("input");
assert_equals(text.files, null, "files should not apply to non-file inputs");
@@ -66,7 +74,7 @@ test(function () {
assert_equals(input.files, empty, "files getter should return the same object until replaced");
const file = new File(["file-body"], "note.txt", { type: "text/plain" });
const files = new FileList([file]);
const files = file_list([file]);
input.files = files;
assert_equals(input.files, files, "files setter should install the selected FileList");
}, "HTMLInputElement.files exposes a replaceable FileList for file inputs");
@@ -80,7 +88,7 @@ test(function () {
upload.type = "file";
upload.name = "upload";
upload.multiple = true;
upload.files = new FileList([
upload.files = file_list([
new File(["alpha-body"], "alpha.txt", { type: "text/plain" }),
new File(["beta-body"], "beta.bin"),
]);
@@ -115,7 +123,7 @@ promise_test(async function () {
const upload = document.createElement("input");
upload.type = "file";
upload.name = "upload";
upload.files = new FileList([
upload.files = file_list([
new File(["file-body"], 'report"name.txt', { type: "text/custom" }),
]);
const emptyInput = document.createElement("input");
@@ -180,7 +188,7 @@ promise_test(async function () {
externalUpload.type = "file";
externalUpload.name = "upload";
externalUpload.multiple = true;
externalUpload.files = new FileList([
externalUpload.files = file_list([
new File(["external-alpha-body"], "external-alpha.txt", { type: "text/external-alpha" }),
new File(["external-beta-body"], "external-beta.bin"),
]);
@@ -2,17 +2,23 @@ self.onmessage = async function (event) {
const mode = event.data && event.data.mode;
if (mode === "filelist") {
const file = new File(["hello"], "note.txt", { type: "text/plain" });
const list = new FileList([file]);
let constructError = null;
try {
new FileList();
} catch (error) {
constructError = error && error.name;
}
postMessage({
ctorOwn: Object.prototype.hasOwnProperty.call(self, "FileList"),
ctorType: typeof FileList,
ctorName: list.constructor && list.constructor.name,
tag: Object.prototype.toString.call(list),
instanceofFileList: list instanceof FileList,
length: list.length,
firstName: list.item(0) && list.item(0).name,
indexName: list[0] && list[0].name,
ctorName: FileList.name,
constructError: constructError,
itemType: typeof FileList.prototype.item,
lengthGetterType: typeof Object.getOwnPropertyDescriptor(
FileList.prototype,
"length",
).get,
iterType: typeof FileList.prototype[Symbol.iterator],
});
close();
return;
@@ -42,11 +42,10 @@ promise_test(async function () {
assert_equals(message.data.ctorOwn, true, "worker should expose FileList");
assert_equals(message.data.ctorType, "function", "FileList constructor type");
assert_equals(message.data.ctorName, "FileList", "FileList constructor name");
assert_equals(message.data.tag, "[object FileList]", "FileList object brand");
assert_equals(message.data.instanceofFileList, true, "FileList instanceof");
assert_equals(message.data.length, 1, "FileList length");
assert_equals(message.data.firstName, "note.txt", "FileList.item(0)");
assert_equals(message.data.indexName, "note.txt", "FileList index getter");
assert_equals(message.data.constructError, "TypeError", "FileList is not constructible");
assert_equals(message.data.itemType, "function", "FileList.item is exposed");
assert_equals(message.data.lengthGetterType, "function", "FileList.length is exposed");
assert_equals(message.data.iterType, "function", "FileList iterator is exposed");
} finally {
worker.terminate();
}