diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 87713361b6..7d8d7e0bb6 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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::{ diff --git a/moli-renderer-v8/src/context_bootstrap/file_api.rs b/moli-renderer-v8/src/context_bootstrap/file_api.rs index cfc4941b82..bcf915ab2d 100644 --- a/moli-renderer-v8/src/context_bootstrap/file_api.rs +++ b/moli-renderer-v8/src/context_bootstrap/file_api.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/file_list.rs b/moli-renderer-v8/src/context_bootstrap/file_api/file_list.rs index e6520ba01a..4fb57da035 100644 --- a/moli-renderer-v8/src/context_bootstrap/file_api/file_list.rs +++ b/moli-renderer-v8/src/context_bootstrap/file_api/file_list.rs @@ -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> { - 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> { + 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>> { + 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::::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::::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::(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::::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)) -} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs index b1d472ed4c..c32d0371bf 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs @@ -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>>, + external_object_sources: Rc>>, +} + +struct IndexedDbExternalObjectSource { + index: u32, + object: v8::Global, +} + +impl IndexedDbStructuredCloneSerializer { + fn store_blob_external_object<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + payload: BlobClonePayload, + ) -> Option { + 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, + external_object_cache: RefCell>>, +} + +impl IndexedDbStructuredCloneDeserializer { + fn blob_object_for_external_index<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + index: u32, + ) -> Option> { + 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 { 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(), ); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs index acd003a37a..178c971f52 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs @@ -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"); diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 1267dd5034..6bee3a8e1d 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -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/"); diff --git a/moli-renderer-v8/src/structured_clone.rs b/moli-renderer-v8/src/structured_clone.rs index 1be754572c..0b57cffb86 100644 --- a/moli-renderer-v8/src/structured_clone.rs +++ b/moli-renderer-v8/src/structured_clone.rs @@ -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>, } -#[derive(Clone, Debug, Default)] +#[derive(Default)] struct ClonedBlobStore { next_id: u32, blobs: Vec, + sources: Vec, +} + +struct ClonedBlobSource { + clone_id: u32, + object: v8::Global, } #[derive(Clone, Debug, Default)] @@ -295,6 +303,38 @@ struct ClonedFileSystemHandleStore { handles: Vec, } +impl WireSerializer { + fn store_blob_clone<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + payload: BlobClonePayload, + ) -> Option { + 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, deferred_streams: Rc>>, blobs: HashMap, + blob_objects: RefCell>>, file_system_handles: HashMap, } +impl WireDeserializer { + fn blob_object_for_clone_id<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + clone_id: u32, + ) -> Option> { + 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,