diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f569f5b2a1..a589a99be0 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -286,6 +286,25 @@ deno_core::extension!(deno_node, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, ops::v8::op_v8_get_heap_statistics, + ops::v8::op_v8_get_wire_format_version, + ops::v8::op_v8_new_deserializer, + ops::v8::op_v8_new_serializer, + ops::v8::op_v8_read_double, + ops::v8::op_v8_read_header, + ops::v8::op_v8_read_raw_bytes, + ops::v8::op_v8_read_uint32, + ops::v8::op_v8_read_uint64, + ops::v8::op_v8_read_value, + ops::v8::op_v8_release_buffer, + ops::v8::op_v8_set_treat_array_buffer_views_as_host_objects, + ops::v8::op_v8_transfer_array_buffer, + ops::v8::op_v8_transfer_array_buffer_de, + ops::v8::op_v8_write_double, + ops::v8::op_v8_write_header, + ops::v8::op_v8_write_raw_bytes, + ops::v8::op_v8_write_uint32, + ops::v8::op_v8_write_uint64, + ops::v8::op_v8_write_value, ops::vm::op_vm_create_script, ops::vm::op_vm_create_context, ops::vm::op_vm_script_run_in_context, diff --git a/ext/node/ops/v8.rs b/ext/node/ops/v8.rs index ebcf6b0802..8813d2e18e 100644 --- a/ext/node/ops/v8.rs +++ b/ext/node/ops/v8.rs @@ -1,6 +1,15 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; +use deno_core::FastString; +use deno_core::GarbageCollected; +use deno_core::ToJsBuffer; +use std::ptr::NonNull; +use v8::ValueDeserializerHelper; +use v8::ValueSerializerHelper; #[op2(fast)] pub fn op_v8_cached_data_version_tag() -> u32 { @@ -30,3 +39,355 @@ pub fn op_v8_get_heap_statistics( buffer[12] = stats.used_global_handles_size() as f64; buffer[13] = stats.external_memory() as f64; } + +pub struct Serializer<'a> { + inner: v8::ValueSerializer<'a>, +} + +pub struct SerializerDelegate { + obj: v8::Global, +} + +impl<'a> v8::cppgc::GarbageCollected for Serializer<'a> { + fn trace(&self, _visitor: &v8::cppgc::Visitor) {} +} + +impl SerializerDelegate { + fn obj<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + ) -> v8::Local<'s, v8::Object> { + v8::Local::new(scope, &self.obj) + } +} + +impl v8::ValueSerializerImpl for SerializerDelegate { + fn get_shared_array_buffer_id<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + shared_array_buffer: v8::Local<'s, v8::SharedArrayBuffer>, + ) -> Option { + let obj = self.obj(scope); + let key = FastString::from_static("_getSharedArrayBufferId") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + if let Ok(fun) = v.try_cast::() { + return fun + .call(scope, obj.into(), &[shared_array_buffer.into()]) + .and_then(|ret| ret.uint32_value(scope)); + } + } + None + } + fn has_custom_host_object(&self, _isolate: &mut v8::Isolate) -> bool { + false + } + fn throw_data_clone_error<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + message: v8::Local<'s, v8::String>, + ) { + let obj = self.obj(scope); + let key = FastString::from_static("_getDataCloneError") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + let fun = v + .try_cast::() + .expect("_getDataCloneError should be a function"); + if let Some(error) = fun.call(scope, obj.into(), &[message.into()]) { + scope.throw_exception(error); + return; + } + } + let error = v8::Exception::type_error(scope, message); + scope.throw_exception(error); + } + + fn write_host_object<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + object: v8::Local<'s, v8::Object>, + _value_serializer: &dyn ValueSerializerHelper, + ) -> Option { + let obj = self.obj(scope); + let key = FastString::from_static("_writeHostObject") + .v8_string(scope) + .into(); + if let Some(v) = obj.get(scope, key) { + if let Ok(v) = v.try_cast::() { + v.call(scope, obj.into(), &[object.into()])?; + return Some(true); + } + } + + None + } + + fn is_host_object<'s>( + &self, + _scope: &mut v8::HandleScope<'s>, + _object: v8::Local<'s, v8::Object>, + ) -> Option { + // should never be called because has_custom_host_object returns false + None + } +} + +#[op2] +#[cppgc] +pub fn op_v8_new_serializer( + scope: &mut v8::HandleScope, + obj: v8::Local, +) -> Serializer<'static> { + let obj = v8::Global::new(scope, obj); + let inner = + v8::ValueSerializer::new(scope, Box::new(SerializerDelegate { obj })); + Serializer { inner } +} + +#[op2(fast)] +pub fn op_v8_set_treat_array_buffer_views_as_host_objects( + #[cppgc] ser: &Serializer, + value: bool, +) { + ser + .inner + .set_treat_array_buffer_views_as_host_objects(value); +} + +#[op2] +#[serde] +pub fn op_v8_release_buffer(#[cppgc] ser: &Serializer) -> ToJsBuffer { + ser.inner.release().into() +} + +#[op2(fast)] +pub fn op_v8_transfer_array_buffer( + #[cppgc] ser: &Serializer, + #[smi] id: u32, + array_buffer: v8::Local, +) { + ser.inner.transfer_array_buffer(id, array_buffer); +} + +#[op2(fast)] +pub fn op_v8_write_double(#[cppgc] ser: &Serializer, double: f64) { + ser.inner.write_double(double); +} + +#[op2(fast)] +pub fn op_v8_write_header(#[cppgc] ser: &Serializer) { + ser.inner.write_header(); +} + +#[op2] +pub fn op_v8_write_raw_bytes( + #[cppgc] ser: &Serializer, + #[anybuffer] source: &[u8], +) { + ser.inner.write_raw_bytes(source); +} + +#[op2(fast)] +pub fn op_v8_write_uint32(#[cppgc] ser: &Serializer, num: u32) { + ser.inner.write_uint32(num); +} + +#[op2(fast)] +pub fn op_v8_write_uint64(#[cppgc] ser: &Serializer, hi: u32, lo: u32) { + let num = ((hi as u64) << 32) | (lo as u64); + ser.inner.write_uint64(num); +} + +#[op2(nofast, reentrant)] +pub fn op_v8_write_value( + scope: &mut v8::HandleScope, + #[cppgc] ser: &Serializer, + value: v8::Local, +) -> Result<(), AnyError> { + let context = scope.get_current_context(); + ser.inner.write_value(context, value); + Ok(()) +} + +struct DeserBuffer { + ptr: Option>, + // Hold onto backing store to keep the underlying buffer + // alive while we hold a reference to it. + _backing_store: v8::SharedRef, +} + +pub struct Deserializer<'a> { + buf: DeserBuffer, + inner: v8::ValueDeserializer<'a>, +} + +impl<'a> deno_core::GarbageCollected for Deserializer<'a> {} + +pub struct DeserializerDelegate { + obj: v8::Global, +} + +impl GarbageCollected for DeserializerDelegate { + fn trace(&self, _visitor: &v8::cppgc::Visitor) {} +} + +impl v8::ValueDeserializerImpl for DeserializerDelegate { + fn read_host_object<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + _value_deserializer: &dyn v8::ValueDeserializerHelper, + ) -> Option> { + let obj = v8::Local::new(scope, &self.obj); + let key = FastString::from_static("_readHostObject") + .v8_string(scope) + .into(); + let scope = &mut v8::AllowJavascriptExecutionScope::new(scope); + if let Some(v) = obj.get(scope, key) { + if let Ok(v) = v.try_cast::() { + let result = v.call(scope, obj.into(), &[])?; + match result.try_cast() { + Ok(res) => return Some(res), + Err(_) => { + let msg = + FastString::from_static("readHostObject must return an object") + .v8_string(scope); + let error = v8::Exception::type_error(scope, msg); + scope.throw_exception(error); + return None; + } + } + } + } + None + } +} + +#[op2] +#[cppgc] +pub fn op_v8_new_deserializer( + scope: &mut v8::HandleScope, + obj: v8::Local, + buffer: v8::Local, +) -> Result, AnyError> { + let offset = buffer.byte_offset(); + let len = buffer.byte_length(); + let backing_store = buffer.get_backing_store().ok_or_else(|| { + generic_error("deserialization buffer has no backing store") + })?; + let (buf_slice, buf_ptr) = if let Some(data) = backing_store.data() { + // SAFETY: the offset is valid for the underlying buffer because we're getting it directly from v8 + let data_ptr = unsafe { data.as_ptr().cast::().add(offset) }; + ( + // SAFETY: the len is valid, from v8, and the data_ptr is valid (as above) + unsafe { std::slice::from_raw_parts(data_ptr.cast_const().cast(), len) }, + Some(data.cast()), + ) + } else { + (&[] as &[u8], None::>) + }; + let obj = v8::Global::new(scope, obj); + let inner = v8::ValueDeserializer::new( + scope, + Box::new(DeserializerDelegate { obj }), + buf_slice, + ); + Ok(Deserializer { + inner, + buf: DeserBuffer { + _backing_store: backing_store, + ptr: buf_ptr, + }, + }) +} + +#[op2(fast)] +pub fn op_v8_transfer_array_buffer_de( + #[cppgc] deser: &Deserializer, + #[smi] id: u32, + array_buffer: v8::Local, +) { + // TODO(nathanwhit): also need binding for TransferSharedArrayBuffer, then call that if + // array_buffer is shared + deser.inner.transfer_array_buffer(id, array_buffer); +} + +#[op2(fast)] +pub fn op_v8_read_double( + #[cppgc] deser: &Deserializer, +) -> Result { + let mut double = 0f64; + if !deser.inner.read_double(&mut double) { + return Err(type_error("ReadDouble() failed")); + } + Ok(double) +} + +#[op2(nofast)] +pub fn op_v8_read_header( + scope: &mut v8::HandleScope, + #[cppgc] deser: &Deserializer, +) -> bool { + let context = scope.get_current_context(); + let res = deser.inner.read_header(context); + res.unwrap_or_default() +} + +#[op2(fast)] +#[number] +pub fn op_v8_read_raw_bytes( + #[cppgc] deser: &Deserializer, + #[number] length: usize, +) -> usize { + let Some(buf_ptr) = deser.buf.ptr else { + return 0; + }; + if let Some(buf) = deser.inner.read_raw_bytes(length) { + let ptr = buf.as_ptr(); + (ptr as usize) - (buf_ptr.as_ptr() as usize) + } else { + 0 + } +} + +#[op2(fast)] +pub fn op_v8_read_uint32( + #[cppgc] deser: &Deserializer, +) -> Result { + let mut value = 0; + if !deser.inner.read_uint32(&mut value) { + return Err(type_error("ReadUint32() failed")); + } + + Ok(value) +} + +#[op2] +#[serde] +pub fn op_v8_read_uint64( + #[cppgc] deser: &Deserializer, +) -> Result<(u32, u32), AnyError> { + let mut val = 0; + if !deser.inner.read_uint64(&mut val) { + return Err(type_error("ReadUint64() failed")); + } + + Ok(((val >> 32) as u32, val as u32)) +} + +#[op2(fast)] +pub fn op_v8_get_wire_format_version(#[cppgc] deser: &Deserializer) -> u32 { + deser.inner.get_wire_format_version() +} + +#[op2(reentrant)] +pub fn op_v8_read_value<'s>( + scope: &mut v8::HandleScope<'s>, + #[cppgc] deser: &Deserializer, +) -> v8::Local<'s, v8::Value> { + let context = scope.get_current_context(); + let val = deser.inner.read_value(context); + val.unwrap_or_else(|| v8::null(scope).into()) +} diff --git a/ext/node/polyfills/v8.ts b/ext/node/polyfills/v8.ts index f06227cd54..5849f3ccc9 100644 --- a/ext/node/polyfills/v8.ts +++ b/ext/node/polyfills/v8.ts @@ -6,15 +6,36 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { core } from "ext:core/mod.js"; +import { primordials } from "ext:core/mod.js"; +const { ObjectPrototypeToString } = primordials; import { op_v8_cached_data_version_tag, op_v8_get_heap_statistics, + op_v8_get_wire_format_version, + op_v8_new_deserializer, + op_v8_new_serializer, + op_v8_read_double, + op_v8_read_header, + op_v8_read_raw_bytes, + op_v8_read_uint32, + op_v8_read_uint64, + op_v8_read_value, + op_v8_release_buffer, + op_v8_set_treat_array_buffer_views_as_host_objects, + op_v8_transfer_array_buffer, + op_v8_transfer_array_buffer_de, + op_v8_write_double, + op_v8_write_header, + op_v8_write_raw_bytes, + op_v8_write_uint32, + op_v8_write_uint64, + op_v8_write_value, } from "ext:core/ops"; import { Buffer } from "node:buffer"; -import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; +import { notImplemented } from "ext:deno_node/_utils.ts"; +import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; export function cachedDataVersionTag() { return op_v8_cached_data_version_tag(); @@ -71,65 +92,225 @@ export function takeCoverage() { export function writeHeapSnapshot() { notImplemented("v8.writeHeapSnapshot"); } -export function serialize(value) { - return Buffer.from(core.serialize(value)); +// deno-lint-ignore no-explicit-any +export function serialize(value: any) { + const ser = new DefaultSerializer(); + ser.writeHeader(); + ser.writeValue(value); + return ser.releaseBuffer(); } -export function deserialize(data) { - return core.deserialize(data); +export function deserialize(buffer: Buffer | ArrayBufferView | DataView) { + if (!isArrayBufferView(buffer)) { + throw new TypeError( + "buffer must be a TypedArray or a DataView", + ); + } + const der = new DefaultDeserializer(buffer); + der.readHeader(); + return der.readValue(); } + +const kHandle = Symbol("kHandle"); + export class Serializer { + [kHandle]: object; constructor() { - warnNotImplemented("v8.Serializer.prototype.constructor"); + this[kHandle] = op_v8_new_serializer(this); + } + + _setTreatArrayBufferViewsAsHostObjects(value: boolean): void { + op_v8_set_treat_array_buffer_views_as_host_objects(this[kHandle], value); } releaseBuffer(): Buffer { - warnNotImplemented("v8.DefaultSerializer.prototype.releaseBuffer"); - return Buffer.from(""); + return Buffer.from(op_v8_release_buffer(this[kHandle])); } transferArrayBuffer(_id: number, _arrayBuffer: ArrayBuffer): void { - warnNotImplemented("v8.DefaultSerializer.prototype.transferArrayBuffer"); + op_v8_transfer_array_buffer(this[kHandle], _id, _arrayBuffer); } - writeDouble(_value: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeDouble"); + writeDouble(value: number): void { + op_v8_write_double(this[kHandle], value); } writeHeader(): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeHeader"); + op_v8_write_header(this[kHandle]); } - writeRawBytes(_value: ArrayBufferView): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeRawBytes"); + writeRawBytes(source: ArrayBufferView): void { + if (!isArrayBufferView(source)) { + throw new TypeError( + "source must be a TypedArray or a DataView", + ); + } + op_v8_write_raw_bytes(this[kHandle], source); } - writeUint32(_value: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeUint32"); + writeUint32(value: number): void { + op_v8_write_uint32(this[kHandle], value); } - writeUint64(_hi: number, _lo: number): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeUint64"); + writeUint64(hi: number, lo: number): void { + op_v8_write_uint64(this[kHandle], hi, lo); } // deno-lint-ignore no-explicit-any - writeValue(_value: any): void { - warnNotImplemented("v8.DefaultSerializer.prototype.writeValue"); + writeValue(value: any): void { + op_v8_write_value(this[kHandle], value); + } + + _getDataCloneError = Error; +} + +export class Deserializer { + buffer: ArrayBufferView; + [kHandle]: object; + constructor(buffer: ArrayBufferView) { + if (!isArrayBufferView(buffer)) { + throw new TypeError( + "buffer must be a TypedArray or a DataView", + ); + } + this.buffer = buffer; + this[kHandle] = op_v8_new_deserializer(this, buffer); + } + readRawBytes(length: number): Buffer { + const offset = this._readRawBytes(length); + return Buffer.from( + this.buffer.buffer, + this.buffer.byteOffset + offset, + length, + ); + } + _readRawBytes(length: number): number { + return op_v8_read_raw_bytes(this[kHandle], length); + } + getWireFormatVersion(): number { + return op_v8_get_wire_format_version(this[kHandle]); + } + readDouble(): number { + return op_v8_read_double(this[kHandle]); + } + readHeader(): boolean { + return op_v8_read_header(this[kHandle]); + } + + readUint32(): number { + return op_v8_read_uint32(this[kHandle]); + } + readUint64(): [hi: number, lo: number] { + return op_v8_read_uint64(this[kHandle]); + } + readValue(): unknown { + return op_v8_read_value(this[kHandle]); + } + transferArrayBuffer( + id: number, + arrayBuffer: ArrayBuffer | SharedArrayBuffer, + ): void { + return op_v8_transfer_array_buffer_de(this[kHandle], id, arrayBuffer); } } -export class Deserializer { - constructor() { - notImplemented("v8.Deserializer.prototype.constructor"); - } +function arrayBufferViewTypeToIndex(abView: ArrayBufferView) { + const type = ObjectPrototypeToString(abView); + if (type === "[object Int8Array]") return 0; + if (type === "[object Uint8Array]") return 1; + if (type === "[object Uint8ClampedArray]") return 2; + if (type === "[object Int16Array]") return 3; + if (type === "[object Uint16Array]") return 4; + if (type === "[object Int32Array]") return 5; + if (type === "[object Uint32Array]") return 6; + if (type === "[object Float32Array]") return 7; + if (type === "[object Float64Array]") return 8; + if (type === "[object DataView]") return 9; + // Index 10 is FastBuffer. + if (type === "[object BigInt64Array]") return 11; + if (type === "[object BigUint64Array]") return 12; + return -1; } export class DefaultSerializer extends Serializer { constructor() { - warnNotImplemented("v8.DefaultSerializer.prototype.constructor"); super(); + this._setTreatArrayBufferViewsAsHostObjects(true); + } + + // deno-lint-ignore no-explicit-any + _writeHostObject(abView: any) { + // Keep track of how to handle different ArrayBufferViews. The default + // Serializer for Node does not use the V8 methods for serializing those + // objects because Node's `Buffer` objects use pooled allocation in many + // cases, and their underlying `ArrayBuffer`s would show up in the + // serialization. Because a) those may contain sensitive data and the user + // may not be aware of that and b) they are often much larger than the + // `Buffer` itself, custom serialization is applied. + let i = 10; // FastBuffer + if (abView.constructor !== Buffer) { + i = arrayBufferViewTypeToIndex(abView); + if (i === -1) { + throw new this._getDataCloneError( + `Unserializable host object: ${abView}`, + ); + } + } + this.writeUint32(i); + this.writeUint32(abView.byteLength); + this.writeRawBytes( + new Uint8Array(abView.buffer, abView.byteOffset, abView.byteLength), + ); } } -export class DefaultDeserializer { - constructor() { - notImplemented("v8.DefaultDeserializer.prototype.constructor"); + +// deno-lint-ignore no-explicit-any +function arrayBufferViewIndexToType(index: number): any { + if (index === 0) return Int8Array; + if (index === 1) return Uint8Array; + if (index === 2) return Uint8ClampedArray; + if (index === 3) return Int16Array; + if (index === 4) return Uint16Array; + if (index === 5) return Int32Array; + if (index === 6) return Uint32Array; + if (index === 7) return Float32Array; + if (index === 8) return Float64Array; + if (index === 9) return DataView; + if (index === 10) return Buffer; + if (index === 11) return BigInt64Array; + if (index === 12) return BigUint64Array; + return undefined; +} + +export class DefaultDeserializer extends Deserializer { + constructor(buffer: ArrayBufferView) { + super(buffer); + } + + _readHostObject() { + const typeIndex = this.readUint32(); + const ctor = arrayBufferViewIndexToType(typeIndex); + const byteLength = this.readUint32(); + const byteOffset = this._readRawBytes(byteLength); + const BYTES_PER_ELEMENT = ctor?.BYTES_PER_ELEMENT ?? 1; + + const offset = this.buffer.byteOffset + byteOffset; + if (offset % BYTES_PER_ELEMENT === 0) { + return new ctor( + this.buffer.buffer, + offset, + byteLength / BYTES_PER_ELEMENT, + ); + } + // Copy to an aligned buffer first. + const bufferCopy = Buffer.allocUnsafe(byteLength); + Buffer.from( + this.buffer.buffer, + byteOffset, + byteLength, + ).copy(bufferCopy); + return new ctor( + bufferCopy.buffer, + bufferCopy.byteOffset, + byteLength / BYTES_PER_ELEMENT, + ); } } export const promiseHooks = { diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 72d8728122..ed63b5950b 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -104,6 +104,7 @@ "test-util-promisify.js", "test-util-types.js", "test-util.js", + "test-v8-serdes.js", "test-webcrypto-sign-verify.js", "test-whatwg-url-properties.js", // needs replace ".on" => ".addEventListener" in L29 diff --git a/tests/node_compat/test/parallel/test-v8-serdes.js b/tests/node_compat/test/parallel/test-v8-serdes.js new file mode 100644 index 0000000000..175f5546c3 --- /dev/null +++ b/tests/node_compat/test/parallel/test-v8-serdes.js @@ -0,0 +1,285 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually. + +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); +const { internalBinding } = require('internal/test/binding'); +const assert = require('assert'); +const v8 = require('v8'); +const os = require('os'); + +const circular = {}; +circular.circular = circular; + +const objects = [ + { foo: 'bar' }, + { bar: 'baz' }, + new Int8Array([1, 2, 3, 4]), + new Uint8Array([1, 2, 3, 4]), + new Int16Array([1, 2, 3, 4]), + new Uint16Array([1, 2, 3, 4]), + new Int32Array([1, 2, 3, 4]), + new Uint32Array([1, 2, 3, 4]), + new Float32Array([1, 2, 3, 4]), + new Float64Array([1, 2, 3, 4]), + new DataView(new ArrayBuffer(42)), + Buffer.from([1, 2, 3, 4]), + new BigInt64Array([42n]), + new BigUint64Array([42n]), + undefined, + null, + 42, + circular, +]; + +// TODO(nathanwhit): we don't have any exposed host objects, so these parts don't work +// const hostObject = new (internalBinding('js_stream').JSStream)(); + +{ + const ser = new v8.DefaultSerializer(); + ser.writeHeader(); + for (const obj of objects) { + ser.writeValue(obj); + } + + const des = new v8.DefaultDeserializer(ser.releaseBuffer()); + des.readHeader(); + + for (const obj of objects) { + assert.deepStrictEqual(des.readValue(), obj); + } +} + +{ + for (const obj of objects) { + assert.deepStrictEqual(v8.deserialize(v8.serialize(obj)), obj); + } +} + +{ + const ser = new v8.DefaultSerializer(); + ser._getDataCloneError = common.mustCall((message) => { + assert.strictEqual(message, '# could not be cloned.'); + return new Error('foobar'); + }); + + ser.writeHeader(); + + assert.throws(() => { + ser.writeValue(new Proxy({}, {})); + }, /foobar/); +} + +// TODO(nathanwhit): we don't have any exposed host objects, so these parts don't work +// { +// const ser = new v8.DefaultSerializer(); +// ser._writeHostObject = common.mustCall((object) => { +// assert.strictEqual(object, hostObject); +// const buf = Buffer.from('hostObjectTag'); + +// ser.writeUint32(buf.length); +// ser.writeRawBytes(buf); + +// ser.writeUint64(1, 2); +// ser.writeDouble(-0.25); +// }); + +// ser.writeHeader(); +// ser.writeValue({ val: hostObject }); + +// const des = new v8.DefaultDeserializer(ser.releaseBuffer()); +// des._readHostObject = common.mustCall(() => { +// const length = des.readUint32(); +// const buf = des.readRawBytes(length); + +// assert.strictEqual(buf.toString(), 'hostObjectTag'); + +// assert.deepStrictEqual(des.readUint64(), [1, 2]); +// assert.strictEqual(des.readDouble(), -0.25); +// return hostObject; +// }); + +// des.readHeader(); + +// assert.strictEqual(des.readValue().val, hostObject); +// } + +// This test ensures that `v8.Serializer.writeRawBytes()` support +// `TypedArray` and `DataView`. +// { +// const text = 'hostObjectTag'; +// const data = Buffer.from(text); +// const arrayBufferViews = common.getArrayBufferViews(data); + +// // `buf` is one of `TypedArray` or `DataView`. +// function testWriteRawBytes(buf) { +// let writeHostObjectCalled = false; +// const ser = new v8.DefaultSerializer(); + +// ser._writeHostObject = common.mustCall((object) => { +// writeHostObjectCalled = true; +// ser.writeUint32(buf.byteLength); +// ser.writeRawBytes(buf); +// }); + +// ser.writeHeader(); +// ser.writeValue({ val: hostObject }); + +// const des = new v8.DefaultDeserializer(ser.releaseBuffer()); +// des._readHostObject = common.mustCall(() => { +// assert.strictEqual(writeHostObjectCalled, true); +// const length = des.readUint32(); +// const buf = des.readRawBytes(length); +// assert.strictEqual(buf.toString(), text); + +// return hostObject; +// }); + +// des.readHeader(); + +// assert.strictEqual(des.readValue().val, hostObject); +// } + +// arrayBufferViews.forEach((buf) => { +// testWriteRawBytes(buf); +// }); +// } + +// { +// const ser = new v8.DefaultSerializer(); +// ser._writeHostObject = common.mustCall((object) => { +// throw new Error('foobar'); +// }); + +// ser.writeHeader(); +// assert.throws(() => { +// ser.writeValue({ val: hostObject }); +// }, /foobar/); +// } + +// { +// assert.throws(() => v8.serialize(hostObject), { +// constructor: Error, +// message: 'Unserializable host object: JSStream {}' +// }); +// } + +{ + // Test that an old serialized value can still be deserialized. + const buf = Buffer.from('ff0d6f2203666f6f5e007b01', 'hex'); + + const des = new v8.DefaultDeserializer(buf); + des.readHeader(); + assert.strictEqual(des.getWireFormatVersion(), 0x0d); + + const value = des.readValue(); + assert.strictEqual(value, value.foo); +} + +{ + const message = `New serialization format. + + This test is expected to fail when V8 changes its serialization format. + When that happens, the "desStr" variable must be updated to the new value + and the change should be mentioned in the release notes, as it is semver-major. + + Consider opening an issue as a heads up at https://github.com/nodejs/node/issues/new + `; + + const desStr = 'ff0f6f2203666f6f5e007b01'; + + const desBuf = Buffer.from(desStr, 'hex'); + const des = new v8.DefaultDeserializer(desBuf); + des.readHeader(); + const value = des.readValue(); + + const ser = new v8.DefaultSerializer(); + ser.writeHeader(); + ser.writeValue(value); + + const serBuf = ser.releaseBuffer(); + const serStr = serBuf.toString('hex'); + assert.deepStrictEqual(serStr, desStr, message); +} + +{ + // Unaligned Uint16Array read, with padding in the underlying array buffer. + let buf = Buffer.alloc(32 + 9); + buf.write('ff0d5c0404addeefbe', 32, 'hex'); + buf = buf.slice(32); + + const expectedResult = os.endianness() === 'LE' ? + new Uint16Array([0xdead, 0xbeef]) : new Uint16Array([0xadde, 0xefbe]); + + assert.deepStrictEqual(v8.deserialize(buf), expectedResult); +} + +{ + assert.throws(() => v8.Serializer(), { + constructor: TypeError, + message: "Class constructor Serializer cannot be invoked without 'new'", + // code: 'ERR_CONSTRUCT_CALL_REQUIRED' + }); + assert.throws(() => v8.Deserializer(), { + constructor: TypeError, + message: "Class constructor Deserializer cannot be invoked without 'new'", + // code: 'ERR_CONSTRUCT_CALL_REQUIRED' + }); +} + + +// `v8.deserialize()` and `new v8.Deserializer()` should support both +// `TypedArray` and `DataView`. +{ + for (const obj of objects) { + const buf = v8.serialize(obj); + + for (const arrayBufferView of common.getArrayBufferViews(buf)) { + assert.deepStrictEqual(v8.deserialize(arrayBufferView), obj); + } + + for (const arrayBufferView of common.getArrayBufferViews(buf)) { + const deserializer = new v8.DefaultDeserializer(arrayBufferView); + deserializer.readHeader(); + const value = deserializer.readValue(); + assert.deepStrictEqual(value, obj); + + const serializer = new v8.DefaultSerializer(); + serializer.writeHeader(); + serializer.writeValue(value); + assert.deepStrictEqual(buf, serializer.releaseBuffer()); + } + } +} + +{ + const INVALID_SOURCE = 'INVALID_SOURCE_TYPE'; + const serializer = new v8.Serializer(); + serializer.writeHeader(); + assert.throws( + () => serializer.writeRawBytes(INVALID_SOURCE), + /^TypeError: source must be a TypedArray or a DataView$/, + ); + assert.throws( + () => v8.deserialize(INVALID_SOURCE), + /^TypeError: buffer must be a TypedArray or a DataView$/, + ); + assert.throws( + () => new v8.Deserializer(INVALID_SOURCE), + /^TypeError: buffer must be a TypedArray or a DataView$/, + ); +} + +{ + // Regression test for https://github.com/nodejs/node/issues/37978 + assert.throws(() => { + new v8.Deserializer(new v8.Serializer().releaseBuffer()).readDouble(); + }, /ReadDouble\(\) failed/); +}