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 3 commits
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
12 changes: 12 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 All @@ -34,6 +35,7 @@ export const {
Set,
String,
Symbol,
Uint8Array,
WeakMap,
WeakSet,
} = globalThis;
Expand Down Expand Up @@ -124,6 +126,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 +181,15 @@ export const arraySome = uncurryThis(arrayPrototype.some);
export const arraySort = uncurryThis(arrayPrototype.sort);
export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]);
//
export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice);
/** @type {(b: ArrayBuffer) => number} */
export const arrayBufferGetByteLength = uncurryThis(
// @ts-expect-error we know it is there on all conforming platforms
getOwnPropertyDescriptor(arrayBufferPrototype, 'byteLength').get,
);
//
export const typedArraySet = uncurryThis(typedArrayPrototype.set);
//
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());
addIntrinsics(shimArrayBufferTransfer());

addIntrinsics(getAnonymousIntrinsics());

Expand Down
75 changes: 75 additions & 0 deletions packages/ses/src/shim-arraybuffer-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
ArrayBuffer,
arrayBufferPrototype,
arrayBufferSlice,
arrayBufferGetByteLength,
Uint8Array,
typedArraySet,
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.
//
// Empty object because this shim has nothing for `addIntrinsics` to add.
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) {
// Using this builtin getter also ensures that `this` is a genuine
// ArrayBuffer.
const oldLength = arrayBufferGetByteLength(this);
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) {
const result = new ArrayBuffer(newLength);
const taOld = new Uint8Array(this);
const taNew = new Uint8Array(result);
typedArraySet(taNew, taOld);
// Using clone only to detach, and only after the copy succeeds
clone(this, { transfer: [this] });
return result;
} else {
const result = arrayBufferSlice(this, 0, newLength);
// Using clone only to detach, and only after the slice succeeds
clone(this, { transfer: [this] });
return result;
}
},
};

defineProperty(arrayBufferPrototype, 'transfer', {
// @ts-expect-error
value: methods.transfer,
writable: true,
enumerable: false,
configurable: true,
});

// Empty object because this shim has nothing for `addIntrinsics` to add.
return {};
};
57 changes: 57 additions & 0 deletions packages/ses/test/shim-arraybuffer-transfer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.

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);
taX[0] = 10;
taX[1] = 11;
taX[2] = 12;
t.is(taX[0], 10);
t.is(taX[1], 11);
t.is(taX[2], 12);
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], 12);

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[0], 10);
t.is(taZ[1], 11);
t.is(taZ[2], undefined);

const abW = abZ.transfer(4);
t.is(abZ.byteLength, 0);
t.is(abW.byteLength, 4);
const taW = new Uint8Array(abW);
t.is(taZ[2], undefined);
t.is(taW[0], 10);
t.is(taW[1], 11);
t.is(taW[2], 0);
t.is(taW[3], 0);
t.is(taW[4], undefined);
});
Loading