use std::cell::RefCell; use v8::{ValueDeserializerHelper, ValueSerializerHelper}; struct Clone { classify: v8::Global, clone_host: v8::Global, error: v8::Global, prepare_transfers: Option>, validate_transfers: Option>, commit_transfers: Option>, hosts: RefCell>>, shared: RefCell>>, modules: RefCell>, } impl Clone { fn fail<'s>(&self, scope: &mut v8::PinScope<'s, '_>, message: &str) { let message = v8::String::new(scope, message).unwrap(); self.throw(scope, message); } fn throw<'s>(&self, scope: &mut v8::PinScope<'s, '_>, message: v8::Local<'s, v8::String>) { let error = v8::Local::new(scope, &self.error); let receiver = v8::undefined(scope).into(); if let Some(exception) = error.call(scope, receiver, &[message.into()]) { scope.throw_exception(exception); } } fn kind<'s>( &self, scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, ) -> Option { let classify = v8::Local::new(scope, &self.classify); let receiver = v8::undefined(scope).into(); classify .call(scope, receiver, &[value])? .uint32_value(scope) } } impl v8::ValueSerializerImpl for &Clone { fn throw_data_clone_error<'s>( &self, scope: &mut v8::PinScope<'s, '_>, message: v8::Local<'s, v8::String>, ) { self.throw(scope, message); } fn has_custom_host_object(&self, _: &v8::Isolate) -> bool { true } fn is_host_object<'s>( &self, scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) -> Option { self.kind(scope, object.into()).map(|kind| kind != 0) } fn write_host_object<'s>( &self, scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, serializer: &dyn v8::ValueSerializerHelper, ) -> Option { let clone_host = v8::Local::new(scope, &self.clone_host); let receiver = v8::undefined(scope).into(); let value = clone_host.call(scope, receiver, &[object.into()])?; let Ok(value) = v8::Local::::try_from(value) else { self.fail(scope, "This object cannot be cloned"); return None; }; let mut hosts = self.hosts.borrow_mut(); let Ok(id) = u32::try_from(hosts.len()) else { drop(hosts); self.fail(scope, "Too many serialized host objects"); return None; }; hosts.push(v8::Global::new(scope, value)); serializer.write_uint32(id); Some(true) } fn get_shared_array_buffer_id<'s>( &self, scope: &mut v8::PinScope<'s, '_>, buffer: v8::Local<'s, v8::SharedArrayBuffer>, ) -> Option { let mut shared = self.shared.borrow_mut(); let Ok(id) = u32::try_from(shared.len()) else { drop(shared); self.fail(scope, "Too many serialized shared buffers"); return None; }; shared.push(buffer.get_backing_store()); Some(id) } fn get_wasm_module_transfer_id( &self, scope: &mut v8::PinScope<'_, '_>, module: v8::Local, ) -> Option { let mut modules = self.modules.borrow_mut(); let Ok(id) = u32::try_from(modules.len()) else { drop(modules); self.fail(scope, "Too many serialized WebAssembly modules"); return None; }; modules.push(module.get_compiled_module()); Some(id) } } impl v8::ValueDeserializerImpl for &Clone { fn read_host_object<'s>( &self, scope: &mut v8::PinScope<'s, '_>, deserializer: &dyn v8::ValueDeserializerHelper, ) -> Option> { let mut id = 0; if !deserializer.read_uint32(&mut id) { return None; } self.hosts .borrow() .get(id as usize) .map(|object| v8::Local::new(scope, object)) } fn get_shared_array_buffer_from_id<'s>( &self, scope: &mut v8::PinScope<'s, '_>, id: u32, ) -> Option> { self.shared .borrow() .get(id as usize) .map(|store| v8::SharedArrayBuffer::with_backing_store(scope, store)) } fn get_wasm_module_from_id<'s>( &self, scope: &mut v8::PinScope<'s, '_>, id: u32, ) -> Option> { v8::WasmModuleObject::from_compiled_module(scope, self.modules.borrow().get(id as usize)?) } } pub(crate) fn clone<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut result: v8::ReturnValue, ) { if let Some(value) = run(scope, args) { result.set(value); } } fn run<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, ) -> Option> { let (Ok(transfer), Ok(classify), Ok(clone_host), Ok(error)) = ( v8::Local::::try_from(args.get(1)), v8::Local::::try_from(args.get(2)), v8::Local::::try_from(args.get(3)), v8::Local::::try_from(args.get(4)), ) else { let message = v8::String::new( scope, "Clone requires a transfer array and policy callbacks", )?; let error = v8::Exception::type_error(scope, message); scope.throw_exception(error); return None; }; let hook_values = [args.get(5), args.get(6), args.get(7)]; let mut hooks = if hook_values.iter().all(|value| value.is_undefined()) { [None, None, None] } else { let Ok(prepare) = v8::Local::::try_from(hook_values[0]) else { let error = v8::Exception::type_error( scope, v8::String::new(scope, "Invalid transfer hooks").unwrap(), ); scope.throw_exception(error); return None; }; let Ok(validate) = v8::Local::::try_from(hook_values[1]) else { let error = v8::Exception::type_error( scope, v8::String::new(scope, "Invalid transfer hooks").unwrap(), ); scope.throw_exception(error); return None; }; let Ok(commit) = v8::Local::::try_from(hook_values[2]) else { let error = v8::Exception::type_error( scope, v8::String::new(scope, "Invalid transfer hooks").unwrap(), ); scope.throw_exception(error); return None; }; [ Some(v8::Global::new(scope, prepare)), Some(v8::Global::new(scope, validate)), Some(v8::Global::new(scope, commit)), ] }; let policy = Clone { classify: v8::Global::new(scope, classify), clone_host: v8::Global::new(scope, clone_host), error: v8::Global::new(scope, error), prepare_transfers: hooks[0].take(), validate_transfers: hooks[1].take(), commit_transfers: hooks[2].take(), hosts: RefCell::new(Vec::new()), shared: RefCell::new(Vec::new()), modules: RefCell::new(Vec::new()), }; let mut buffers = Vec::new(); let seen = v8::Set::new(scope); for index in 0..transfer.length() { let value = transfer.get_index(scope, index)?; if seen.has(scope, value)? { policy.fail(scope, "Transfer list contains a duplicate value"); return None; } seen.add(scope, value)?; if let Ok(buffer) = v8::Local::::try_from(value) { buffers.push(buffer); } else if policy.kind(scope, value)? != 2 { policy.fail(scope, "Transfer list contains a non-transferable value"); return None; } } let receiver = v8::undefined(scope).into(); if let Some(callback) = &policy.prepare_transfers { let prepare_transfers = v8::Local::new(scope, callback); prepare_transfers.call(scope, receiver, &[transfer.into()])?; } if let Some(callback) = &policy.validate_transfers { let validate_transfers = v8::Local::new(scope, callback); validate_transfers.call(scope, receiver, &[transfer.into()])?; } let serializer = v8::ValueSerializer::new(scope, Box::new(&policy)); for (id, buffer) in buffers.iter().enumerate() { serializer.transfer_array_buffer(id as u32, *buffer); } serializer.write_header(); let context = scope.get_current_context(); if serializer.write_value(context, args.get(0)) != Some(true) { return None; } if let Some(callback) = &policy.validate_transfers { let validate_transfers = v8::Local::new(scope, callback); validate_transfers.call(scope, receiver, &[transfer.into()])?; } let bytes = serializer.release(); let deserializer = v8::ValueDeserializer::new(scope, Box::new(&policy), &bytes); // Detachment follows successful serialization, in transfer-list order. Getters may // have resized or detached later buffers, so this phase can fail after earlier moves. for (id, buffer) in buffers.iter().enumerate() { if buffer.was_detached() { policy.fail(scope, "Cannot transfer a detached ArrayBuffer"); return None; } // V8's Detach silently succeeds on non-detachable buffers (e.g. Wasm memory). if !buffer.is_detachable() { let message = v8::String::new(scope, "Cannot detach this ArrayBuffer")?; let error = v8::Exception::type_error(scope, message); scope.throw_exception(error); return None; } let store = buffer.get_backing_store(); if buffer.detach(None) != Some(true) { return None; } let moved = v8::ArrayBuffer::with_backing_store(scope, &store); deserializer.transfer_array_buffer(id as u32, moved); } let value = { v8::tc_scope!(let caught, scope); let value = if deserializer.read_header(context) == Some(true) { deserializer.read_value(context) } else { None }; if value.is_none() { caught.reset(); } value }; if value.is_none() { // V8 cannot reconstruct views invalidated by a transfer-time resize. policy.fail( scope, "not yet implemented: deserializing this cloned value", ); return None; } if let Some(callback) = &policy.commit_transfers { let commit_transfers = v8::Local::new(scope, callback); commit_transfers.call(scope, receiver, &[transfer.into()])?; } value }