fix(structured-clone): preserve FileList objects

This commit is contained in:
ldm0
2026-09-23 00:11:53 +08:00
parent 2fd9c1d574
commit d7deea56fe
7 changed files with 538 additions and 51 deletions
+2 -2
View File
@@ -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/");
+130 -13
View File
@@ -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,