From 06e15fe3eafb329c25a95c1b35d3978bedd7d3d6 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 19 Aug 2024 12:01:39 -0700 Subject: [PATCH] feat(ses): shim ArrayBuffer.p.transfer --- packages/ses/src/commons.js | 4 ++ packages/ses/src/lockdown.js | 2 + packages/ses/src/shim-arraybuffer-transfer.js | 61 +++++++++++++++++++ .../test/shim-arraybuffer-transfer.test.js | 42 +++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 packages/ses/src/shim-arraybuffer-transfer.js create mode 100644 packages/ses/test/shim-arraybuffer-transfer.test.js diff --git a/packages/ses/src/commons.js b/packages/ses/src/commons.js index 3d696f3a77..0e82f9856c 100644 --- a/packages/ses/src/commons.js +++ b/packages/ses/src/commons.js @@ -19,6 +19,7 @@ export { universalThis as globalThis }; export const { Array, + ArrayBuffer, Date, FinalizationRegistry, Float32Array, @@ -124,6 +125,7 @@ export const { } = Reflect; export const { isArray, prototype: arrayPrototype } = Array; +export const { prototype: arrayBufferPrototype } = ArrayBuffer; export const { prototype: mapPrototype } = Map; export const { revocable: proxyRevocable } = Proxy; export const { prototype: regexpPrototype } = RegExp; @@ -178,6 +180,8 @@ export const arraySome = uncurryThis(arrayPrototype.some); export const arraySort = uncurryThis(arrayPrototype.sort); export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]); // +export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice); +// export const mapSet = uncurryThis(mapPrototype.set); export const mapGet = uncurryThis(mapPrototype.get); export const mapHas = uncurryThis(mapPrototype.has); diff --git a/packages/ses/src/lockdown.js b/packages/ses/src/lockdown.js index 1a2c6b2727..9a0aaf0348 100644 --- a/packages/ses/src/lockdown.js +++ b/packages/ses/src/lockdown.js @@ -56,6 +56,7 @@ import { tameHarden } from './tame-harden.js'; import { tameSymbolConstructor } from './tame-symbol-constructor.js'; import { tameFauxDataProperties } from './tame-faux-data-properties.js'; import { tameRegeneratorRuntime } from './tame-regenerator-runtime.js'; +import { shimArrayBufferTransfer } from './shim-arraybuffer-transfer.js'; /** @import {LockdownOptions} from '../types.js' */ @@ -284,6 +285,7 @@ export const repairIntrinsics = (options = {}) => { addIntrinsics(tameMathObject(mathTaming)); addIntrinsics(tameRegExpConstructor(regExpTaming)); addIntrinsics(tameSymbolConstructor()); + shimArrayBufferTransfer(); addIntrinsics(getAnonymousIntrinsics()); diff --git a/packages/ses/src/shim-arraybuffer-transfer.js b/packages/ses/src/shim-arraybuffer-transfer.js new file mode 100644 index 0000000000..2c2a52ee73 --- /dev/null +++ b/packages/ses/src/shim-arraybuffer-transfer.js @@ -0,0 +1,61 @@ +import { + arrayBufferPrototype, + arrayBufferSlice, + globalThis, + TypeError, + defineProperty, +} from './commons.js'; + +export const shimArrayBufferTransfer = () => { + // @ts-expect-error TODO extend ArrayBuffer type to include transfer, etc. + if (typeof arrayBufferPrototype.transfer === 'function') { + // Assume already exists so does not need to be shimmed. + // Such conditional shimming is ok in this case since ArrayBuffer.p.transfer + // is already officially part of JS. + return; + } + const clone = globalThis.structuredClone; + if (typeof clone !== 'function') { + // Indeed, Node <= 16 has neither. + throw TypeError( + `Can only shim missing ArrayBuffer.prototype.transfer on a platform with "structuredClone"`, + ); + } + + /** + * @type {ThisType} + */ + const methods = { + /** + * @param {number} [newLength] + */ + transfer(newLength = undefined) { + // Hopefully, a zero-length slice is cheap, but still enforces that + // `this` is a genuine `ArrayBuffer` exotic object. + arrayBufferSlice(this, 0, 0); + const oldLength = this.byteLength; + if (newLength === undefined || newLength === oldLength) { + return clone(this, { transfer: [this] }); + } + if (typeof newLength !== 'number') { + throw new TypeError(`transfer newLength if provided must be a number`); + } + if (newLength > oldLength) { + // TODO support this case somehow + throw new TypeError( + `Cannot yet emulate transfer to larger ArrayBuffer ${newLength}`, + ); + } + const tmp = clone(this, { transfer: [this] }); + return arrayBufferSlice(tmp, 0, newLength); + }, + }; + + defineProperty(arrayBufferPrototype, 'transfer', { + // @ts-expect-error + value: methods.transfer, + writable: true, + enumerable: false, + configurable: true, + }); +}; diff --git a/packages/ses/test/shim-arraybuffer-transfer.test.js b/packages/ses/test/shim-arraybuffer-transfer.test.js new file mode 100644 index 0000000000..e166cf9905 --- /dev/null +++ b/packages/ses/test/shim-arraybuffer-transfer.test.js @@ -0,0 +1,42 @@ +import test from 'ava'; +import '../index.js'; + +lockdown(); + +// The purpose of this test is to see if Array.prototype.transfer works +// correctly enough on platforms like Node 18 or Node 20 that don't yet have +// it natively, and so are testing the shim on those. On platforms where +// Array.prototype.transfer is present, like Node 22, +// we also run the same tests.Thus, +// this test only tests the intersection behavior of the standard and +// the shim. The shim does not yet support a `newLength` argument +// larger than the original. +// +// TODO once the shim supports transfering to a larger length, we must +// test that as well. + +test('ArrayBuffer.p.transfer', t => { + const abX = new ArrayBuffer(3); + t.is(abX.byteLength, 3); + const taX = new Uint8Array(abX); + t.is(taX[2], 0); + t.is(taX[3], undefined); + + // because this test must run on platforms prior to + // ArrayBuffer.prototype.detached, we test detachment by other means. + + const abY = abX.transfer(); + t.is(abY.byteLength, 3); + t.is(abX.byteLength, 0); + const taY = new Uint8Array(abY); + t.is(taX[2], undefined); + t.is(taY[2], 0); + + const abZ = abY.transfer(2); + t.is(abY.byteLength, 0); + t.is(abZ.byteLength, 2); + const taZ = new Uint8Array(abZ); + t.is(taY[2], undefined); + t.is(taZ[2], undefined); + t.is(taZ[1], 0); +});