Skip to content

Commit

Permalink
feat(ses): ArrayBuffer.transferToImmutable
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jun 8, 2024
1 parent f845665 commit f2adf5f
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/ses/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
User-visible changes in SES:

# Next release

- Adds `ArrayBuffer.immutable` and `ArrayBuffer.transferToImmutable` as a shim for a future proposal. It makes an ArrayBuffer-like object whose contents cannot be mutated. However, due to limitations of the shim
- Unlike `ArrayBuffer` and `SharedArrayBuffer` this ArrayBuffer-like object cannot be transfered or cloned between JS threads.
- Unlike `ArrayBuffer` and `SharedArrayBuffer`, this ArrayBuffer-like object cannot be used as the backing store of TypeArrays or DataViews.
- On Node 20, which lacks `transfer`, `transferToFixedLength`, or any other way within the language to detach an ArrayBuffer, `transferToImmutable` will copy the contents of the original, but leave the original undetached. Node 21 does not have this limitation.

# v1.5.0 (2024-05-06)

- Adds `importNowHook` to the `Compartment` options. The compartment will invoke the hook whenever it encounters a missing dependency while running `compartmentInstance.importNow(specifier)`, which cannot use an asynchronous `importHook`.
Expand Down
15 changes: 15 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 { isView, prototype: arrayBufferPrototype } = ArrayBuffer;
export const { prototype: mapPrototype } = Map;
export const { revocable: proxyRevocable } = Proxy;
export const { prototype: regexpPrototype } = RegExp;
Expand Down Expand Up @@ -174,6 +176,19 @@ 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 arrayBufferTransferToFixedLength =
// @ts-expect-error absent from Node 20, which we still support
arrayBufferPrototype.transferToFixedLength
? // @ts-expect-error absent from Node 20, which we still support
uncurryThis(arrayBufferPrototype.transferToFixedLength)
: (arrayBuffer, newLength = arrayBuffer.byteLength) =>
// There is no `transferToFixedLength` on Node 20, which we still support.
// In that case, there is no way just within the language to detach an
// ArrayBuffer, so the best we can do is emulate it using `slice`.
// Unfortunately, this leaves the original non-detached.
arrayBufferSlice(arrayBuffer, 0, newLength);
//
export const mapSet = uncurryThis(mapPrototype.set);
export const mapGet = uncurryThis(mapPrototype.get);
export const mapHas = uncurryThis(mapPrototype.has);
Expand Down
96 changes: 96 additions & 0 deletions packages/ses/src/immutable-array-buffer-shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable class-methods-use-this */
import {
setPrototypeOf,
defineProperties,
arrayBufferSlice,
arrayBufferTransferToFixedLength,
arrayBufferPrototype,
getOwnPropertyDescriptors,
TypeError,
} from './commons.js';

/**
* This class only exists within the shim, as a convience for imperfectly
* emulating the proposal, which would not have this class. In the proposal,
* `transferToImmutable` makes a new `ArrayBuffer` that inherits from
* `ArrayBuffer.prototype` as you'd expect. In the shim, `transferToImmutable`
* makes a normal object that inherits from
* `ImmutableArrayBufferInternal.prototype`, which has been surgically
* altered to inherit from `ArrayBuffer.prototype`. The constructor is
* captured for use internal to this module, and is made otherwise inaccessible.
* Therefore, `ImmutableArrayBufferInternal.prototype` and all its methods
* and accessor functions effectively become hidden intrinsics.
*
* TODO handle them as hidden intrinsics, so they get hardened when they should.
*/
class ImmutableArrayBufferInternal {
/** @type {ArrayBuffer} */
#buffer;

constructor(buffer) {
// This also enforces that `buffer` is a genuine `ArrayBuffer`
this.#buffer = arrayBufferSlice(buffer, 0);
}

get byteLength() {
return this.#buffer.byteLength;
}

get detached() {
return false;
}

get maxByteLength() {
// Not underlying maxByteLength, which is irrelevant
return this.#buffer.byteLength;
}

get resizable() {
return false;
}

get immutable() {
return true;
}

slice(begin = 0, end = undefined) {
return arrayBufferSlice(this.#buffer, begin, end);
}

resize(_newByteLength = undefined) {
throw TypeError('Cannot resize an immutable ArrayBuffer');
}

transfer(_newLength = undefined) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}

transferToFixedLength(_newLength = undefined) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}

transferToImmutable(_newLength = undefined) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}
}

const ImmutableArrayBufferInternalPrototype =
ImmutableArrayBufferInternal.prototype;
// @ts-expect-error can only delete optionals
delete ImmutableArrayBufferInternalPrototype.constructor;

setPrototypeOf(ImmutableArrayBufferInternalPrototype, arrayBufferPrototype);

defineProperties(
arrayBufferPrototype,
getOwnPropertyDescriptors({
get immutable() {
return false;
},
transferToImmutable(newLength = undefined) {
return new ImmutableArrayBufferInternal(
arrayBufferTransferToFixedLength(this, newLength),
);
},
}),
);
1 change: 1 addition & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { makeCompartmentConstructor } from './compartment.js';
import { tameHarden } from './tame-harden.js';
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
import { tameFauxDataProperties } from './tame-faux-data-properties.js';
import './immutable-array-buffer-shim.js';

/** @import {LockdownOptions} from '../types.js' */

Expand Down
4 changes: 4 additions & 0 deletions packages/ses/src/permits.js
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,10 @@ export const permitted = {
// https://github.com/tc39/proposal-arraybuffer-transfer
transferToFixedLength: fn,
detached: getter,
// https://github.com/endojs/endo/pull/2309#issuecomment-2155513240
// to be proposed
transferToImmutable: fn,
immutable: getter,
},

// SharedArrayBuffer Objects
Expand Down
94 changes: 94 additions & 0 deletions packages/ses/test/immutable-array-buffer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import test from 'ava';
import '../index.js';

const { isFrozen, getPrototypeOf } = Object;

lockdown();

// TODO Need to treat the hidden prototype as a hidden instrinsic so it
// gets hardened when the rest do.
test.failing('Immutable ArrayBuffer installed and hardened', t => {
const ab1 = new ArrayBuffer(2);
const iab = ab1.transferToImmutable();
const iabProto = getPrototypeOf(iab);
t.true(isFrozen(iabProto));
t.true(isFrozen(iabProto.slice));
});

test('Immutable ArrayBuffer ops', t => {
// Absent on Node <= 18
const canResize = 'maxByteLength' in ArrayBuffer.prototype;
const canDetach = 'detached' in ArrayBuffer.prototype;

const ab1 = new ArrayBuffer(2, { maxByteLength: 7 });
const ta1 = new Uint8Array(ab1);
ta1[0] = 3;
ta1[1] = 4;
const iab = ab1.transferToImmutable();
t.true(iab instanceof ArrayBuffer);
ta1[1] = 5;
const ab2 = iab.slice(0);
const ta2 = new Uint8Array(ab2);
t.is(ta1[1], canDetach ? undefined : 5);
t.is(ta2[1], 4);
ta2[1] = 6;

const ab3 = iab.slice(0);
t.true(ab3 instanceof ArrayBuffer);

const ta3 = new Uint8Array(ab3);
t.is(ta1[1], canDetach ? undefined : 5);
t.is(ta2[1], 6);
t.is(ta3[1], 4);

t.is(ab1.byteLength, canDetach ? 0 : 2);
t.is(iab.byteLength, 2);
t.is(ab2.byteLength, 2);

t.is(iab.maxByteLength, 2);
if (canResize) {
t.is(ab1.maxByteLength, canDetach? 0 : 7);
t.is(ab2.maxByteLength, 2);
}

if (canDetach) {
t.true(ab1.detached);
t.false(ab2.detached);
t.false(ab3.detached);
}
t.false(iab.detached);
t.false(iab.resizable);
});

// This could have been written as a test.failing as compared to
// the immutable ArrayBuffer we'll propose. However, I'd rather test what
// the shim purposely does instead.
test('Immutable ArrayBuffer shim limitations', t => {
const ab1 = new ArrayBuffer(2);
const dv1 = new DataView(ab1);
t.is(dv1.buffer, ab1);
t.is(dv1.byteLength, 2);
const ta1 = new Uint8Array(ab1);
ta1[0] = 3;
ta1[1] = 4;
t.is(ta1.byteLength, 2);

t.throws(() => new DataView({}), { instanceOf: TypeError });
// Unfortutanely, calling a TypeArray constructor with an object that
// is not a TypeArray, ArrayBuffer, or Iterable just creates a useless
// empty TypedArray, rather than throwing.
const ta2 = new Uint8Array({});
t.is(ta2.byteLength, 0);

const iab = ab1.transferToImmutable();
t.throws(() => new DataView(iab), {
instanceOf: TypeError,
});
// Unfortunately, unlike the immutable ArrayBuffer to be proposed,
// calling a TypedArray constructor with the shim implementation of
// an immutable ArrayBuffer as argument treats it as an unrecognized object,
// rather than throwing an error.
t.is(iab.byteLength, 2);
const ta3 = new Uint8Array(iab);
t.is(ta3.byteLength, 0);
});

0 comments on commit f2adf5f

Please sign in to comment.