diff options
author | Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> | 2024-09-10 14:50:21 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-10 21:50:21 +0000 |
commit | be0ba6d84f190f4fc1b4517e62d9d8ad30c8cfb1 (patch) | |
tree | 7e088e66f70d69e7074ff20fb192e73a661facfa /tests/node_compat | |
parent | e522f4b65a3439030506733b104498f21422ede0 (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`.
Diffstat (limited to 'tests/node_compat')
-rw-r--r-- | tests/node_compat/config.jsonc | 1 | ||||
-rw-r--r-- | tests/node_compat/test/parallel/test-v8-serdes.js | 285 |
2 files changed, 286 insertions, 0 deletions
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/); +} |