Skip to content

Commit f2adf5f

Browse files
committed
feat(ses): ArrayBuffer.transferToImmutable
1 parent f845665 commit f2adf5f

6 files changed

Lines changed: 217 additions & 0 deletions

File tree

packages/ses/NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
User-visible changes in SES:
22

3+
# Next release
4+
5+
- 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
6+
- Unlike `ArrayBuffer` and `SharedArrayBuffer` this ArrayBuffer-like object cannot be transfered or cloned between JS threads.
7+
- Unlike `ArrayBuffer` and `SharedArrayBuffer`, this ArrayBuffer-like object cannot be used as the backing store of TypeArrays or DataViews.
8+
- 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.
9+
310
# v1.5.0 (2024-05-06)
411

512
- 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`.

packages/ses/src/commons.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export { universalThis as globalThis };
1919

2020
export const {
2121
Array,
22+
ArrayBuffer,
2223
Date,
2324
FinalizationRegistry,
2425
Float32Array,
@@ -124,6 +125,7 @@ export const {
124125
} = Reflect;
125126

126127
export const { isArray, prototype: arrayPrototype } = Array;
128+
export const { isView, prototype: arrayBufferPrototype } = ArrayBuffer;
127129
export const { prototype: mapPrototype } = Map;
128130
export const { revocable: proxyRevocable } = Proxy;
129131
export const { prototype: regexpPrototype } = RegExp;
@@ -174,6 +176,19 @@ export const arraySome = uncurryThis(arrayPrototype.some);
174176
export const arraySort = uncurryThis(arrayPrototype.sort);
175177
export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]);
176178
//
179+
export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice);
180+
export const arrayBufferTransferToFixedLength =
181+
// @ts-expect-error absent from Node 20, which we still support
182+
arrayBufferPrototype.transferToFixedLength
183+
? // @ts-expect-error absent from Node 20, which we still support
184+
uncurryThis(arrayBufferPrototype.transferToFixedLength)
185+
: (arrayBuffer, newLength = arrayBuffer.byteLength) =>
186+
// There is no `transferToFixedLength` on Node 20, which we still support.
187+
// In that case, there is no way just within the language to detach an
188+
// ArrayBuffer, so the best we can do is emulate it using `slice`.
189+
// Unfortunately, this leaves the original non-detached.
190+
arrayBufferSlice(arrayBuffer, 0, newLength);
191+
//
177192
export const mapSet = uncurryThis(mapPrototype.set);
178193
export const mapGet = uncurryThis(mapPrototype.get);
179194
export const mapHas = uncurryThis(mapPrototype.has);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* eslint-disable class-methods-use-this */
2+
import {
3+
setPrototypeOf,
4+
defineProperties,
5+
arrayBufferSlice,
6+
arrayBufferTransferToFixedLength,
7+
arrayBufferPrototype,
8+
getOwnPropertyDescriptors,
9+
TypeError,
10+
} from './commons.js';
11+
12+
/**
13+
* This class only exists within the shim, as a convience for imperfectly
14+
* emulating the proposal, which would not have this class. In the proposal,
15+
* `transferToImmutable` makes a new `ArrayBuffer` that inherits from
16+
* `ArrayBuffer.prototype` as you'd expect. In the shim, `transferToImmutable`
17+
* makes a normal object that inherits from
18+
* `ImmutableArrayBufferInternal.prototype`, which has been surgically
19+
* altered to inherit from `ArrayBuffer.prototype`. The constructor is
20+
* captured for use internal to this module, and is made otherwise inaccessible.
21+
* Therefore, `ImmutableArrayBufferInternal.prototype` and all its methods
22+
* and accessor functions effectively become hidden intrinsics.
23+
*
24+
* TODO handle them as hidden intrinsics, so they get hardened when they should.
25+
*/
26+
class ImmutableArrayBufferInternal {
27+
/** @type {ArrayBuffer} */
28+
#buffer;
29+
30+
constructor(buffer) {
31+
// This also enforces that `buffer` is a genuine `ArrayBuffer`
32+
this.#buffer = arrayBufferSlice(buffer, 0);
33+
}
34+
35+
get byteLength() {
36+
return this.#buffer.byteLength;
37+
}
38+
39+
get detached() {
40+
return false;
41+
}
42+
43+
get maxByteLength() {
44+
// Not underlying maxByteLength, which is irrelevant
45+
return this.#buffer.byteLength;
46+
}
47+
48+
get resizable() {
49+
return false;
50+
}
51+
52+
get immutable() {
53+
return true;
54+
}
55+
56+
slice(begin = 0, end = undefined) {
57+
return arrayBufferSlice(this.#buffer, begin, end);
58+
}
59+
60+
resize(_newByteLength = undefined) {
61+
throw TypeError('Cannot resize an immutable ArrayBuffer');
62+
}
63+
64+
transfer(_newLength = undefined) {
65+
throw TypeError('Cannot detach an immutable ArrayBuffer');
66+
}
67+
68+
transferToFixedLength(_newLength = undefined) {
69+
throw TypeError('Cannot detach an immutable ArrayBuffer');
70+
}
71+
72+
transferToImmutable(_newLength = undefined) {
73+
throw TypeError('Cannot detach an immutable ArrayBuffer');
74+
}
75+
}
76+
77+
const ImmutableArrayBufferInternalPrototype =
78+
ImmutableArrayBufferInternal.prototype;
79+
// @ts-expect-error can only delete optionals
80+
delete ImmutableArrayBufferInternalPrototype.constructor;
81+
82+
setPrototypeOf(ImmutableArrayBufferInternalPrototype, arrayBufferPrototype);
83+
84+
defineProperties(
85+
arrayBufferPrototype,
86+
getOwnPropertyDescriptors({
87+
get immutable() {
88+
return false;
89+
},
90+
transferToImmutable(newLength = undefined) {
91+
return new ImmutableArrayBufferInternal(
92+
arrayBufferTransferToFixedLength(this, newLength),
93+
);
94+
},
95+
}),
96+
);

packages/ses/src/lockdown.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { makeCompartmentConstructor } from './compartment.js';
5555
import { tameHarden } from './tame-harden.js';
5656
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
5757
import { tameFauxDataProperties } from './tame-faux-data-properties.js';
58+
import './immutable-array-buffer-shim.js';
5859

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

packages/ses/src/permits.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,10 @@ export const permitted = {
12671267
// https://github.com/tc39/proposal-arraybuffer-transfer
12681268
transferToFixedLength: fn,
12691269
detached: getter,
1270+
// https://github.com/endojs/endo/pull/2309#issuecomment-2155513240
1271+
// to be proposed
1272+
transferToImmutable: fn,
1273+
immutable: getter,
12701274
},
12711275

12721276
// SharedArrayBuffer Objects
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import test from 'ava';
2+
import '../index.js';
3+
4+
const { isFrozen, getPrototypeOf } = Object;
5+
6+
lockdown();
7+
8+
// TODO Need to treat the hidden prototype as a hidden instrinsic so it
9+
// gets hardened when the rest do.
10+
test.failing('Immutable ArrayBuffer installed and hardened', t => {
11+
const ab1 = new ArrayBuffer(2);
12+
const iab = ab1.transferToImmutable();
13+
const iabProto = getPrototypeOf(iab);
14+
t.true(isFrozen(iabProto));
15+
t.true(isFrozen(iabProto.slice));
16+
});
17+
18+
test('Immutable ArrayBuffer ops', t => {
19+
// Absent on Node <= 18
20+
const canResize = 'maxByteLength' in ArrayBuffer.prototype;
21+
const canDetach = 'detached' in ArrayBuffer.prototype;
22+
23+
const ab1 = new ArrayBuffer(2, { maxByteLength: 7 });
24+
const ta1 = new Uint8Array(ab1);
25+
ta1[0] = 3;
26+
ta1[1] = 4;
27+
const iab = ab1.transferToImmutable();
28+
t.true(iab instanceof ArrayBuffer);
29+
ta1[1] = 5;
30+
const ab2 = iab.slice(0);
31+
const ta2 = new Uint8Array(ab2);
32+
t.is(ta1[1], canDetach ? undefined : 5);
33+
t.is(ta2[1], 4);
34+
ta2[1] = 6;
35+
36+
const ab3 = iab.slice(0);
37+
t.true(ab3 instanceof ArrayBuffer);
38+
39+
const ta3 = new Uint8Array(ab3);
40+
t.is(ta1[1], canDetach ? undefined : 5);
41+
t.is(ta2[1], 6);
42+
t.is(ta3[1], 4);
43+
44+
t.is(ab1.byteLength, canDetach ? 0 : 2);
45+
t.is(iab.byteLength, 2);
46+
t.is(ab2.byteLength, 2);
47+
48+
t.is(iab.maxByteLength, 2);
49+
if (canResize) {
50+
t.is(ab1.maxByteLength, canDetach? 0 : 7);
51+
t.is(ab2.maxByteLength, 2);
52+
}
53+
54+
if (canDetach) {
55+
t.true(ab1.detached);
56+
t.false(ab2.detached);
57+
t.false(ab3.detached);
58+
}
59+
t.false(iab.detached);
60+
t.false(iab.resizable);
61+
});
62+
63+
// This could have been written as a test.failing as compared to
64+
// the immutable ArrayBuffer we'll propose. However, I'd rather test what
65+
// the shim purposely does instead.
66+
test('Immutable ArrayBuffer shim limitations', t => {
67+
const ab1 = new ArrayBuffer(2);
68+
const dv1 = new DataView(ab1);
69+
t.is(dv1.buffer, ab1);
70+
t.is(dv1.byteLength, 2);
71+
const ta1 = new Uint8Array(ab1);
72+
ta1[0] = 3;
73+
ta1[1] = 4;
74+
t.is(ta1.byteLength, 2);
75+
76+
t.throws(() => new DataView({}), { instanceOf: TypeError });
77+
// Unfortutanely, calling a TypeArray constructor with an object that
78+
// is not a TypeArray, ArrayBuffer, or Iterable just creates a useless
79+
// empty TypedArray, rather than throwing.
80+
const ta2 = new Uint8Array({});
81+
t.is(ta2.byteLength, 0);
82+
83+
const iab = ab1.transferToImmutable();
84+
t.throws(() => new DataView(iab), {
85+
instanceOf: TypeError,
86+
});
87+
// Unfortunately, unlike the immutable ArrayBuffer to be proposed,
88+
// calling a TypedArray constructor with the shim implementation of
89+
// an immutable ArrayBuffer as argument treats it as an unrecognized object,
90+
// rather than throwing an error.
91+
t.is(iab.byteLength, 2);
92+
const ta3 = new Uint8Array(iab);
93+
t.is(ta3.byteLength, 0);
94+
});

0 commit comments

Comments
 (0)