summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Whitaker <17734409+nathanwhit@users.noreply.github.com>2024-09-10 14:50:21 -0700
committerGitHub <noreply@github.com>2024-09-10 21:50:21 +0000
commitbe0ba6d84f190f4fc1b4517e62d9d8ad30c8cfb1 (patch)
tree7e088e66f70d69e7074ff20fb192e73a661facfa
parente522f4b65a3439030506733b104498f21422ede0 (diff)
fix(ext/node): Rewrite `node:v8` serialize/deserialize (#25439)
Closes #20613. Reimplements the serialization on top of the v8 APIs instead of deno_core. Implements `v8.Serializer`, `v8.DefaultSerializer`, `v8.Deserializer`, and `v8.DefaultSerializer`.
-rw-r--r--ext/node/lib.rs19
-rw-r--r--ext/node/ops/v8.rs361
-rw-r--r--ext/node/polyfills/v8.ts235
-rw-r--r--tests/node_compat/config.jsonc1
-rw-r--r--tests/node_compat/test/parallel/test-v8-serdes.js285
5 files changed, 874 insertions, 27 deletions
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index f569f5b2a..a589a99be 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 ebcf6b080..8813d2e18 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<v8::Object>,
+}
+
+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<u32> {
+ 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::<v8::Function>() {
+ 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::<v8::Function>()
+ .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<bool> {
+ 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::<v8::Function>() {
+ 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<bool> {
+ // 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<v8::Object>,
+) -> 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<v8::ArrayBuffer>,
+) {
+ 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<v8::Value>,
+) -> Result<(), AnyError> {
+ let context = scope.get_current_context();
+ ser.inner.write_value(context, value);
+ Ok(())
+}
+
+struct DeserBuffer {
+ ptr: Option<NonNull<u8>>,
+ // Hold onto backing store to keep the underlying buffer
+ // alive while we hold a reference to it.
+ _backing_store: v8::SharedRef<v8::BackingStore>,
+}
+
+pub struct Deserializer<'a> {
+ buf: DeserBuffer,
+ inner: v8::ValueDeserializer<'a>,
+}
+
+impl<'a> deno_core::GarbageCollected for Deserializer<'a> {}
+
+pub struct DeserializerDelegate {
+ obj: v8::Global<v8::Object>,
+}
+
+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<v8::Local<'s, v8::Object>> {
+ 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::<v8::Function>() {
+ 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<v8::Object>,
+ buffer: v8::Local<v8::ArrayBufferView>,
+) -> Result<Deserializer<'static>, 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::<u8>().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::<NonNull<u8>>)
+ };
+ 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<v8::ArrayBuffer>,
+) {
+ // 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<f64, AnyError> {
+ 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<u32, AnyError> {
+ 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 f06227cd5..5849f3ccc 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 {
- constructor() {
- notImplemented("v8.Deserializer.prototype.constructor");
+ 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);
+ }
+}
+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 72d872812..ed63b5950 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 000000000..175f5546c
--- /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, '#<Object> 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/);
+}