mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 00:01:06 +00:00
fix(structured-clone): preserve FileList objects
This commit is contained in:
@@ -252,8 +252,8 @@ pub(crate) use self::file_api::{
|
||||
};
|
||||
pub(crate) use self::file_api::{apply_drag_modifier_drop_effect, build_data_transfer_object};
|
||||
pub(crate) use self::file_api::{
|
||||
build_file_list_object, build_file_object, flush_one_pending_file_reader,
|
||||
selected_file_from_object,
|
||||
build_file_list_object, build_file_object, file_list_files_from_object,
|
||||
flush_one_pending_file_reader, is_file_list_object, selected_file_from_object,
|
||||
};
|
||||
pub(crate) use self::form_data_runtime::form_data_request_body;
|
||||
pub(crate) use self::geometry_clone::{
|
||||
|
||||
@@ -37,7 +37,10 @@ pub(crate) use file_entry_file::{
|
||||
file_system_file_entry_file_callback,
|
||||
};
|
||||
pub(super) use file_list::file_list_item_callback;
|
||||
pub(crate) use file_list::{build_file_list_object, sync_file_list_contents};
|
||||
pub(crate) use file_list::{
|
||||
build_file_list_object, file_list_files_from_object, is_file_list_object,
|
||||
sync_file_list_contents,
|
||||
};
|
||||
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,
|
||||
|
||||
@@ -5,13 +5,11 @@ use crate::webidl;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
const FILE_LIST_LENGTH_SLOT: &str = "__lmFileListLength";
|
||||
const FILE_LIST_BRAND_SLOT: &str = "__lmFileListBrand";
|
||||
const FILE_LIST_FILES_SLOT: &str = "__lmFileListFiles";
|
||||
|
||||
#[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,
|
||||
@@ -49,12 +47,18 @@ pub(crate) fn build_file_list_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
files: &[v8::Local<'s, v8::Object>],
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let object = FileListObjectDeclaration::new(files.len() as f64)
|
||||
let length = u32::try_from(files.len()).ok()?;
|
||||
let contents = build_file_list_contents_array(scope, files)?;
|
||||
let object = FileListObjectDeclaration::new(f64::from(length))
|
||||
.bind(scope)
|
||||
.ok()?;
|
||||
for (index, file) in files.iter().enumerate() {
|
||||
let _ = object.set_index(scope, index as u32, (*file).into());
|
||||
let index = u32::try_from(index).ok()?;
|
||||
if object.set_index(scope, index, (*file).into()) != Some(true) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
set_private_value(scope, object, FILE_LIST_FILES_SLOT, contents.into());
|
||||
Some(object)
|
||||
}
|
||||
|
||||
@@ -63,8 +67,19 @@ pub(crate) fn sync_file_list_contents<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
files: &[v8::Local<'s, v8::Object>],
|
||||
) {
|
||||
let Ok(length) = u32::try_from(files.len()) else {
|
||||
return;
|
||||
};
|
||||
let Some(contents) = build_file_list_contents_array(scope, files) else {
|
||||
return;
|
||||
};
|
||||
let previous_length = file_list_length_from_object(scope, object)
|
||||
.filter(|value| value.is_finite() && *value >= 0.0)
|
||||
.filter(|value| {
|
||||
value.is_finite()
|
||||
&& *value >= 0.0
|
||||
&& value.fract() == 0.0
|
||||
&& *value <= f64::from(u32::MAX)
|
||||
})
|
||||
.map(|value| value as u32)
|
||||
.unwrap_or(0);
|
||||
for index in 0..previous_length {
|
||||
@@ -73,23 +88,74 @@ pub(crate) fn sync_file_list_contents<'s>(
|
||||
for (index, file) in files.iter().enumerate() {
|
||||
let _ = object.set_index(scope, index as u32, (*file).into());
|
||||
}
|
||||
let length = v8::Number::new(scope, files.len() as f64);
|
||||
set_private_value(scope, object, FILE_LIST_FILES_SLOT, contents.into());
|
||||
let length = v8::Number::new(scope, f64::from(length));
|
||||
set_private_value(scope, object, FILE_LIST_LENGTH_SLOT, length.into());
|
||||
}
|
||||
|
||||
fn build_file_list_contents_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
files: &[v8::Local<'s, v8::Object>],
|
||||
) -> Option<v8::Local<'s, v8::Array>> {
|
||||
let contents = v8::Array::new(scope, 0);
|
||||
for (index, file) in files.iter().enumerate() {
|
||||
let index = u32::try_from(index).ok()?;
|
||||
if contents.set_index(scope, index, (*file).into()) != Some(true) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(contents)
|
||||
}
|
||||
|
||||
pub(crate) fn is_file_list_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
web_api_interfaces::FileList::is_instance(scope, object)
|
||||
}
|
||||
|
||||
pub(crate) fn file_list_files_from_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<Vec<v8::Local<'s, v8::Object>>> {
|
||||
if !is_file_list_object(scope, object) {
|
||||
return None;
|
||||
}
|
||||
let length = file_list_length_from_object(scope, object)?;
|
||||
if !length.is_finite() || length < 0.0 || length.fract() != 0.0 || length > f64::from(u32::MAX)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let contents = get_private_value(scope, object, FILE_LIST_FILES_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())?;
|
||||
let length = length as u32;
|
||||
let mut files = Vec::new();
|
||||
files.try_reserve_exact(length as usize).ok()?;
|
||||
for index in 0..length {
|
||||
let file = contents
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
|
||||
files.push(file);
|
||||
}
|
||||
Some(files)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn file_list_item_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !file_list_receiver_branded(scope, args.this()) {
|
||||
if !is_file_list_object(scope, args.this()) {
|
||||
throw_type_error(scope, "Illegal invocation");
|
||||
return;
|
||||
}
|
||||
let Some(parsed) = webidl::parse_args::<FileListItemArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let Some(value) = args.this().get_index(scope, parsed.index) else {
|
||||
let Some(value) = get_private_value(scope, args.this(), FILE_LIST_FILES_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
|
||||
.and_then(|files| files.get_index(scope, parsed.index))
|
||||
else {
|
||||
rv.set(v8::null(scope).into());
|
||||
return;
|
||||
};
|
||||
@@ -105,7 +171,7 @@ 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()) {
|
||||
if !is_file_list_object(scope, args.this()) {
|
||||
throw_type_error(scope, "Illegal invocation");
|
||||
return;
|
||||
}
|
||||
@@ -122,11 +188,3 @@ fn file_list_length_from_object<'s>(
|
||||
get_private_value(scope, object, FILE_LIST_LENGTH_SLOT)
|
||||
.and_then(|value| value.number_value(scope))
|
||||
}
|
||||
|
||||
fn file_list_receiver_branded<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
get_private_value(scope, receiver, FILE_LIST_BRAND_SLOT)
|
||||
.is_some_and(|value| value.boolean_value(scope))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,69 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
context_bootstrap::{
|
||||
FileSystemHandleDurablePayload, build_file_system_handle_from_durable_payload,
|
||||
FileSystemHandleDurablePayload, build_file_list_object,
|
||||
build_file_system_handle_from_durable_payload, file_list_files_from_object,
|
||||
file_system_handle_clone_payload_from_object,
|
||||
file_system_handle_durable_payload_from_object,
|
||||
},
|
||||
dom::native::SelectedFile,
|
||||
structured_clone::{
|
||||
BlobClonePayload, HOST_OBJECT_TAG_BLOB, HOST_OBJECT_TAG_CRYPTO_KEY,
|
||||
HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE, blob_clone_payload_from_object,
|
||||
build_blob_object_from_clone_payload, read_crypto_key_payload, write_crypto_key_payload,
|
||||
HOST_OBJECT_TAG_FILE_LIST, HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE,
|
||||
blob_clone_payload_from_object, build_blob_object_from_clone_payload,
|
||||
read_crypto_key_payload, write_crypto_key_payload,
|
||||
},
|
||||
};
|
||||
use moli_indexeddb::{IndexedDbFileSystemHandleBucket, IndexedDbFileSystemHandleKind};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
struct IndexedDbStructuredCloneSerializer {
|
||||
external_objects: Rc<RefCell<Vec<IndexedDbExternalObject>>>,
|
||||
external_object_sources: Rc<RefCell<Vec<IndexedDbExternalObjectSource>>>,
|
||||
}
|
||||
|
||||
struct IndexedDbExternalObjectSource {
|
||||
index: u32,
|
||||
object: v8::Global<v8::Object>,
|
||||
}
|
||||
|
||||
impl IndexedDbStructuredCloneSerializer {
|
||||
fn store_blob_external_object<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
payload: BlobClonePayload,
|
||||
) -> Option<u32> {
|
||||
if let Some(source) = self
|
||||
.external_object_sources
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|source| v8::Local::new(scope, &source.object).strict_equals(object.into()))
|
||||
{
|
||||
return Some(source.index);
|
||||
}
|
||||
|
||||
let mut external_objects = self.external_objects.borrow_mut();
|
||||
let Ok(index) = u32::try_from(external_objects.len()) else {
|
||||
drop(external_objects);
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Too many external objects in IndexedDB structured clone.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
external_objects.push(indexed_db_external_object_from_blob_payload(payload));
|
||||
drop(external_objects);
|
||||
self.external_object_sources
|
||||
.borrow_mut()
|
||||
.push(IndexedDbExternalObjectSource {
|
||||
index,
|
||||
object: v8::Global::new(scope, object),
|
||||
});
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer {
|
||||
@@ -54,20 +101,50 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer {
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
Some("Blob" | "File") => {
|
||||
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
|
||||
let mut external_objects = self.external_objects.borrow_mut();
|
||||
let Ok(index) = u32::try_from(external_objects.len()) else {
|
||||
drop(external_objects);
|
||||
Some("FileList") => {
|
||||
let Some(files) = file_list_files_from_object(scope, object) else {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Invalid FileList during IndexedDB structured clone.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
let Ok(length) = u32::try_from(files.len()) else {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Too many Files in IndexedDB structured clone FileList.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
let mut indices = Vec::with_capacity(files.len());
|
||||
for file in files {
|
||||
let Some(payload @ BlobClonePayload::File { .. }) =
|
||||
blob_clone_payload_from_object(scope, file)
|
||||
else {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Too many external objects in IndexedDB structured clone.",
|
||||
"FileList contains an invalid File during IndexedDB structured clone.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
external_objects.push(indexed_db_external_object_from_blob_payload(payload));
|
||||
indices.push(self.store_blob_external_object(scope, file, payload)?);
|
||||
}
|
||||
serializer.write_uint32(HOST_OBJECT_TAG_FILE_LIST);
|
||||
serializer.write_uint32(length);
|
||||
for index in indices {
|
||||
serializer.write_uint32(index);
|
||||
}
|
||||
return Some(true);
|
||||
}
|
||||
Some("Blob" | "File") => {
|
||||
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
|
||||
let index = self.store_blob_external_object(scope, object, payload)?;
|
||||
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
|
||||
serializer.write_uint32(index);
|
||||
return Some(true);
|
||||
@@ -144,6 +221,28 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer {
|
||||
|
||||
struct IndexedDbStructuredCloneDeserializer {
|
||||
external_objects: Vec<IndexedDbExternalObject>,
|
||||
external_object_cache: RefCell<HashMap<u32, v8::Global<v8::Object>>>,
|
||||
}
|
||||
|
||||
impl IndexedDbStructuredCloneDeserializer {
|
||||
fn blob_object_for_external_index<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
index: u32,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if let Some(object) = self.external_object_cache.borrow().get(&index) {
|
||||
return Some(v8::Local::new(scope, object));
|
||||
}
|
||||
let payload = self
|
||||
.external_objects
|
||||
.get(index as usize)
|
||||
.and_then(blob_payload_from_indexed_db_external_object)?;
|
||||
let object = build_blob_object_from_clone_payload(scope, &payload)?;
|
||||
self.external_object_cache
|
||||
.borrow_mut()
|
||||
.insert(index, v8::Global::new(scope, object));
|
||||
Some(object)
|
||||
}
|
||||
}
|
||||
|
||||
impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer {
|
||||
@@ -185,11 +284,7 @@ impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer {
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
}
|
||||
let Some(payload) = self
|
||||
.external_objects
|
||||
.get(index as usize)
|
||||
.and_then(blob_payload_from_indexed_db_external_object)
|
||||
else {
|
||||
let Some(object) = self.blob_object_for_external_index(scope, index) else {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Missing external Blob during IndexedDB structured clone.",
|
||||
@@ -198,7 +293,65 @@ impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer {
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
build_blob_object_from_clone_payload(scope, &payload)
|
||||
Some(object)
|
||||
}
|
||||
HOST_OBJECT_TAG_FILE_LIST => {
|
||||
let mut length = 0;
|
||||
if !deserializer.read_uint32(&mut length) {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Failed to deserialize IndexedDB FileList length.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
if files.try_reserve_exact(length as usize).is_err() {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"IndexedDB FileList is too large to deserialize.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
}
|
||||
for _ in 0..length {
|
||||
let mut index = 0;
|
||||
if !deserializer.read_uint32(&mut index)
|
||||
|| !matches!(
|
||||
self.external_objects.get(index as usize),
|
||||
Some(IndexedDbExternalObject::File { .. })
|
||||
)
|
||||
{
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Missing external File during IndexedDB FileList clone.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
}
|
||||
let Some(file) = self.blob_object_for_external_index(scope, index) else {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Failed to deserialize File in IndexedDB FileList.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
return None;
|
||||
};
|
||||
files.push(file);
|
||||
}
|
||||
build_file_list_object(scope, &files).or_else(|| {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"Failed to deserialize IndexedDB FileList.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
None
|
||||
})
|
||||
}
|
||||
HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE => {
|
||||
let mut index = 0;
|
||||
@@ -336,6 +489,7 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value(
|
||||
) -> Option<IndexedDbValue> {
|
||||
let mut should_throw_data_clone_error = false;
|
||||
let external_objects = Rc::new(RefCell::new(Vec::new()));
|
||||
let external_object_sources = Rc::new(RefCell::new(Vec::new()));
|
||||
let serialized = {
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let scope = try_catch.init();
|
||||
@@ -344,6 +498,7 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value(
|
||||
&scope,
|
||||
Box::new(IndexedDbStructuredCloneSerializer {
|
||||
external_objects: Rc::clone(&external_objects),
|
||||
external_object_sources: Rc::clone(&external_object_sources),
|
||||
}),
|
||||
);
|
||||
serializer.write_header();
|
||||
@@ -380,6 +535,7 @@ pub(in crate::context_bootstrap::indexed_db) fn deserialize_js_value<'s>(
|
||||
scope,
|
||||
Box::new(IndexedDbStructuredCloneDeserializer {
|
||||
external_objects: value.external_objects().to_vec(),
|
||||
external_object_cache: RefCell::new(HashMap::new()),
|
||||
}),
|
||||
value.wire_bytes(),
|
||||
);
|
||||
|
||||
@@ -103,6 +103,75 @@ fn structured_clone_preserves_webassembly_module() {
|
||||
assert_eq!(result, "true|false|0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_clone_preserves_file_list_brand_contents_and_graph_identity() {
|
||||
let mut vm = new_storage_test_vm("https://file-list-structured-clone.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const transfer = new DataTransfer();
|
||||
const file = new File(["file bytes"], "note.txt", {
|
||||
type: "text/custom",
|
||||
lastModified: 42
|
||||
});
|
||||
file.expando = "not serialized";
|
||||
transfer.items.add(file);
|
||||
const list = transfer.files;
|
||||
list.expando = "not serialized";
|
||||
|
||||
let indexedGetterHits = 0;
|
||||
Object.defineProperty(list, "0", {
|
||||
configurable: true,
|
||||
get() {
|
||||
indexedGetterHits++;
|
||||
throw new Error("FileList serialization invoked an indexed getter");
|
||||
}
|
||||
});
|
||||
|
||||
const listFirst = structuredClone({ list, alias: list, file });
|
||||
const fileFirst = structuredClone({ file, list });
|
||||
const empty = structuredClone(new DataTransfer().files);
|
||||
|
||||
return JSON.stringify({
|
||||
listBrand: listFirst.list instanceof FileList,
|
||||
listPrototype: Object.getPrototypeOf(listFirst.list) === FileList.prototype,
|
||||
listDistinct: listFirst.list !== list,
|
||||
listAlias: listFirst.list === listFirst.alias,
|
||||
listLength: listFirst.list.length,
|
||||
itemMatchesFile: listFirst.list.item(0) === listFirst.file,
|
||||
indexedMatchesFile: listFirst.list[0] === listFirst.file,
|
||||
fileFirstIdentity: fileFirst.list[0] === fileFirst.file,
|
||||
fileBrand: listFirst.file instanceof File && listFirst.file instanceof Blob,
|
||||
fileDistinct: listFirst.file !== file,
|
||||
fileMetadata: [
|
||||
listFirst.file.name,
|
||||
listFirst.file.type,
|
||||
listFirst.file.lastModified,
|
||||
listFirst.file.size
|
||||
],
|
||||
expandosExcluded:
|
||||
listFirst.list.expando === undefined && listFirst.file.expando === undefined,
|
||||
indexedGetterHits,
|
||||
empty: [
|
||||
empty instanceof FileList,
|
||||
Object.getPrototypeOf(empty) === FileList.prototype,
|
||||
empty.length,
|
||||
empty.item(0) === null
|
||||
]
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("FileList structuredClone probe should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"listBrand":true,"listPrototype":true,"listDistinct":true,"listAlias":true,"listLength":1,"itemMatchesFile":true,"indexedMatchesFile":true,"fileFirstIdentity":true,"fileBrand":true,"fileDistinct":true,"fileMetadata":["note.txt","text/custom",42,10],"expandosExcluded":true,"indexedGetterHits":0,"empty":[true,true,0,true]}"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_clone_rejects_native_dom_nodes() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/dom-node-clone");
|
||||
|
||||
@@ -1179,6 +1179,90 @@ fn indexed_db_roundtrips_blob_file_and_array_buffer_values() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_db_roundtrips_file_list_with_file_graph_identity() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-file-list-value.test/");
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__indexedDbFileListResult = "pending";
|
||||
const dbName = `file-list-${Math.random()}`;
|
||||
const open = indexedDB.open(dbName, 1);
|
||||
open.onerror = () => {
|
||||
globalThis.__indexedDbFileListResult = `open-error:${open.error && open.error.name}`;
|
||||
};
|
||||
open.onupgradeneeded = () => {
|
||||
open.result.createObjectStore("values", { keyPath: "id" });
|
||||
};
|
||||
open.onsuccess = () => {
|
||||
const db = open.result;
|
||||
const transfer = new DataTransfer();
|
||||
const file = new File(["FILE_LIST_IDB"], "list.txt", {
|
||||
type: "text/list",
|
||||
lastModified: 77
|
||||
});
|
||||
file.expando = "not serialized";
|
||||
transfer.items.add(file);
|
||||
const list = transfer.files;
|
||||
list.expando = "not serialized";
|
||||
|
||||
const writeTx = db.transaction("values", "readwrite");
|
||||
writeTx.onerror = () => {
|
||||
globalThis.__indexedDbFileListResult =
|
||||
`write-error:${writeTx.error && writeTx.error.name}`;
|
||||
};
|
||||
writeTx.objectStore("values").put({
|
||||
id: 1,
|
||||
list,
|
||||
listAlias: list,
|
||||
file
|
||||
});
|
||||
writeTx.oncomplete = () => {
|
||||
const get = db.transaction("values").objectStore("values").get(1);
|
||||
get.onerror = () => {
|
||||
globalThis.__indexedDbFileListResult = `get-error:${get.error && get.error.name}`;
|
||||
};
|
||||
get.onsuccess = () => {
|
||||
const row = get.result;
|
||||
row.file.text().then(text => {
|
||||
globalThis.__indexedDbFileListResult = JSON.stringify({
|
||||
listBrand: row.list instanceof FileList,
|
||||
listPrototype: Object.getPrototypeOf(row.list) === FileList.prototype,
|
||||
listDistinct: row.list !== list,
|
||||
listAlias: row.list === row.listAlias,
|
||||
listLength: row.list.length,
|
||||
itemMatchesFile: row.list.item(0) === row.file,
|
||||
indexedMatchesFile: row.list[0] === row.file,
|
||||
fileBrand: row.file instanceof File && row.file instanceof Blob,
|
||||
fileDistinct: row.file !== file,
|
||||
fileMetadata: [row.file.name, row.file.type, row.file.lastModified, row.file.size],
|
||||
text,
|
||||
expandosExcluded:
|
||||
row.list.expando === undefined && row.file.expando === undefined
|
||||
});
|
||||
}, error => {
|
||||
globalThis.__indexedDbFileListResult = `file-error:${error && error.name}`;
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
return "scheduled";
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("indexeddb FileList workflow should schedule");
|
||||
|
||||
let result = vm
|
||||
.eval_after_selected_page_tasks("String(globalThis.__indexedDbFileListResult)")
|
||||
.expect("indexeddb FileList result should be readable");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"listBrand":true,"listPrototype":true,"listDistinct":true,"listAlias":true,"listLength":1,"itemMatchesFile":true,"indexedMatchesFile":true,"fileBrand":true,"fileDistinct":true,"fileMetadata":["list.txt","text/list",77,13],"text":"FILE_LIST_IDB","expandosExcluded":true}"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_db_roundtrips_opfs_handles_with_durable_external_objects() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-opfs-handle.test/");
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
FileSystemHandleClonePayload, GeometryClonePayload, ImageDataClonePayload,
|
||||
ReadableStreamClonePayload, TransformStreamClonePayload, WritableStreamClonePayload,
|
||||
attach_file_system_file_snapshot_clone_payload, build_file_object,
|
||||
build_file_list_object, file_list_files_from_object,
|
||||
build_file_system_handle_from_clone_payload, build_image_data_object_from_clone_payload,
|
||||
build_readable_stream_clone_shell, build_transform_stream_clone_shell,
|
||||
build_writable_stream_clone_shell, crypto_key_clone_payload_from_object,
|
||||
@@ -46,6 +47,7 @@ const HOST_OBJECT_TAG_QUOTA_EXCEEDED_ERROR: u32 = 8;
|
||||
const HOST_OBJECT_TAG_WRITABLE_STREAM: u32 = 9;
|
||||
const HOST_OBJECT_TAG_TRANSFORM_STREAM: u32 = 10;
|
||||
const HOST_OBJECT_TAG_GEOMETRY: u32 = 11;
|
||||
pub(crate) const HOST_OBJECT_TAG_FILE_LIST: u32 = 12;
|
||||
|
||||
const GEOMETRY_KIND_DOM_POINT_READONLY: u32 = 0;
|
||||
const GEOMETRY_KIND_DOM_POINT: u32 = 1;
|
||||
@@ -283,10 +285,16 @@ struct WireSerializer {
|
||||
file_system_handles: Rc<RefCell<ClonedFileSystemHandleStore>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Default)]
|
||||
struct ClonedBlobStore {
|
||||
next_id: u32,
|
||||
blobs: Vec<ClonedBlob>,
|
||||
sources: Vec<ClonedBlobSource>,
|
||||
}
|
||||
|
||||
struct ClonedBlobSource {
|
||||
clone_id: u32,
|
||||
object: v8::Global<v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -295,6 +303,38 @@ struct ClonedFileSystemHandleStore {
|
||||
handles: Vec<ClonedFileSystemHandle>,
|
||||
}
|
||||
|
||||
impl WireSerializer {
|
||||
fn store_blob_clone<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
payload: BlobClonePayload,
|
||||
) -> Option<u32> {
|
||||
let mut store = self.blobs.borrow_mut();
|
||||
if let Some(source) = store
|
||||
.sources
|
||||
.iter()
|
||||
.find(|source| v8::Local::new(scope, &source.object).strict_equals(object.into()))
|
||||
{
|
||||
return Some(source.clone_id);
|
||||
}
|
||||
|
||||
let clone_id = store.next_id;
|
||||
let Some(next_id) = store.next_id.checked_add(1) else {
|
||||
drop(store);
|
||||
throw_data_clone_exception(scope, "Too many Blob or File objects in structured clone.");
|
||||
return None;
|
||||
};
|
||||
store.next_id = next_id;
|
||||
store.blobs.push(ClonedBlob { clone_id, payload });
|
||||
store.sources.push(ClonedBlobSource {
|
||||
clone_id,
|
||||
object: v8::Global::new(scope, object),
|
||||
});
|
||||
Some(clone_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl v8::ValueSerializerImpl for WireSerializer {
|
||||
fn throw_data_clone_error<'s>(
|
||||
&self,
|
||||
@@ -419,19 +459,40 @@ impl v8::ValueSerializerImpl for WireSerializer {
|
||||
serializer.write_uint32(clone_id);
|
||||
return Some(true);
|
||||
}
|
||||
Some("Blob" | "File") => {
|
||||
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
|
||||
let mut store = self.blobs.borrow_mut();
|
||||
let clone_id = store.next_id;
|
||||
let Some(next_id) = store.next_id.checked_add(1) else {
|
||||
drop(store);
|
||||
throw_data_clone_exception(scope, "Too many Blobs in structured clone.");
|
||||
Some("FileList") => {
|
||||
let Some(files) = file_list_files_from_object(scope, object) else {
|
||||
throw_data_clone_exception(scope, "Invalid FileList during structured clone.");
|
||||
return None;
|
||||
};
|
||||
let Ok(length) = u32::try_from(files.len()) else {
|
||||
throw_data_clone_exception(scope, "Too many Files in structured clone FileList.");
|
||||
return None;
|
||||
};
|
||||
let mut clone_ids = Vec::with_capacity(files.len());
|
||||
for file in files {
|
||||
let Some(payload @ BlobClonePayload::File { .. }) =
|
||||
blob_clone_payload_from_object(scope, file)
|
||||
else {
|
||||
throw_data_clone_exception(
|
||||
scope,
|
||||
"FileList contains an invalid File during structured clone.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
store.next_id = next_id;
|
||||
store.blobs.push(ClonedBlob { clone_id, payload });
|
||||
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
|
||||
clone_ids.push(self.store_blob_clone(scope, file, payload)?);
|
||||
}
|
||||
serializer.write_uint32(HOST_OBJECT_TAG_FILE_LIST);
|
||||
serializer.write_uint32(length);
|
||||
for clone_id in clone_ids {
|
||||
serializer.write_uint32(clone_id);
|
||||
}
|
||||
return Some(true);
|
||||
}
|
||||
Some("Blob" | "File") => {
|
||||
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
|
||||
let index = self.store_blob_clone(scope, object, payload)?;
|
||||
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
|
||||
serializer.write_uint32(index);
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
@@ -531,9 +592,28 @@ struct WireDeserializer {
|
||||
transform_streams: HashMap<u32, TransformStreamClonePayload>,
|
||||
deferred_streams: Rc<RefCell<Vec<DeferredStreamMaterialization>>>,
|
||||
blobs: HashMap<u32, BlobClonePayload>,
|
||||
blob_objects: RefCell<HashMap<u32, v8::Global<v8::Object>>>,
|
||||
file_system_handles: HashMap<u32, FileSystemHandleClonePayload>,
|
||||
}
|
||||
|
||||
impl WireDeserializer {
|
||||
fn blob_object_for_clone_id<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
clone_id: u32,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if let Some(object) = self.blob_objects.borrow().get(&clone_id) {
|
||||
return Some(v8::Local::new(scope, object));
|
||||
}
|
||||
let payload = self.blobs.get(&clone_id)?;
|
||||
let object = build_blob_object_from_clone_payload(scope, payload)?;
|
||||
self.blob_objects
|
||||
.borrow_mut()
|
||||
.insert(clone_id, v8::Global::new(scope, object));
|
||||
Some(object)
|
||||
}
|
||||
}
|
||||
|
||||
impl v8::ValueDeserializerImpl for WireDeserializer {
|
||||
fn read_host_object<'s>(
|
||||
&self,
|
||||
@@ -639,14 +719,50 @@ impl v8::ValueDeserializerImpl for WireDeserializer {
|
||||
}
|
||||
HOST_OBJECT_TAG_BLOB => {
|
||||
let clone_id = read_u32(deserializer)?;
|
||||
let Some(payload) = self.blobs.get(&clone_id) else {
|
||||
let Some(object) = self.blob_object_for_clone_id(scope, clone_id) else {
|
||||
throw_data_clone_exception(
|
||||
scope,
|
||||
"Missing Blob payload during structured clone.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
build_blob_object_from_clone_payload(scope, payload)
|
||||
Some(object)
|
||||
}
|
||||
HOST_OBJECT_TAG_FILE_LIST => {
|
||||
let length = read_u32(deserializer)?;
|
||||
let mut files = Vec::new();
|
||||
if files.try_reserve_exact(length as usize).is_err() {
|
||||
throw_data_clone_exception(
|
||||
scope,
|
||||
"FileList is too large to deserialize from structured clone.",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
for _ in 0..length {
|
||||
let clone_id = read_u32(deserializer)?;
|
||||
if !matches!(
|
||||
self.blobs.get(&clone_id),
|
||||
Some(BlobClonePayload::File { .. })
|
||||
) {
|
||||
throw_data_clone_exception(
|
||||
scope,
|
||||
"Missing File payload during FileList structured clone.",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(file) = self.blob_object_for_clone_id(scope, clone_id) else {
|
||||
throw_data_clone_exception(
|
||||
scope,
|
||||
"Failed to deserialize File in structured clone FileList.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
files.push(file);
|
||||
}
|
||||
build_file_list_object(scope, &files).or_else(|| {
|
||||
throw_data_clone_exception(scope, "Failed to deserialize FileList.");
|
||||
None
|
||||
})
|
||||
}
|
||||
HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE => {
|
||||
let clone_id = read_u32(deserializer)?;
|
||||
@@ -1405,6 +1521,7 @@ fn deserialize_from_wire_impl<'s>(
|
||||
transform_streams,
|
||||
deferred_streams: Rc::clone(&deferred_streams),
|
||||
blobs,
|
||||
blob_objects: RefCell::new(HashMap::new()),
|
||||
file_system_handles,
|
||||
}),
|
||||
&payload.base.wire_bytes,
|
||||
|
||||
Reference in New Issue
Block a user