Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ses): shim ArrayBuffer.prototype.transfer #2417

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/ses/src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { universalThis as globalThis };

export const {
Array,
ArrayBuffer,
Date,
FinalizationRegistry,
Float32Array,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' */

Expand Down Expand Up @@ -284,6 +285,7 @@ export const repairIntrinsics = (options = {}) => {
addIntrinsics(tameMathObject(mathTaming));
addIntrinsics(tameRegExpConstructor(regExpTaming));
addIntrinsics(tameSymbolConstructor());
shimArrayBufferTransfer();
erights marked this conversation as resolved.
Show resolved Hide resolved
erights marked this conversation as resolved.
Show resolved Hide resolved

addIntrinsics(getAnonymousIntrinsics());

Expand Down
61 changes: 61 additions & 0 deletions packages/ses/src/shim-arraybuffer-transfer.js
Original file line number Diff line number Diff line change
@@ -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"`,
);
erights marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @type {ThisType<ArrayBuffer>}
*/
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;
erights marked this conversation as resolved.
Show resolved Hide resolved
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`);
erights marked this conversation as resolved.
Show resolved Hide resolved
}
if (newLength > oldLength) {
// TODO support this case somehow
throw new TypeError(
`Cannot yet emulate transfer to larger ArrayBuffer ${newLength}`,
);
erights marked this conversation as resolved.
Show resolved Hide resolved
}
const tmp = clone(this, { transfer: [this] });
return arrayBufferSlice(tmp, 0, newLength);
erights marked this conversation as resolved.
Show resolved Hide resolved
},
};

defineProperty(arrayBufferPrototype, 'transfer', {
// @ts-expect-error
value: methods.transfer,
writable: true,
enumerable: false,
configurable: true,
});
};
42 changes: 42 additions & 0 deletions packages/ses/test/shim-arraybuffer-transfer.test.js
Original file line number Diff line number Diff line change
@@ -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.
erights marked this conversation as resolved.
Show resolved Hide resolved

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);
});
Loading