Skip to content

Commit ec99a9e

Browse files
committed
test: cover immutable ArrayBuffer known issues
Cover native writes, immutable source copies, and view serialization across the affected API families. Exercise the Node-API detach abort using the existing typed-array addon and mark it as an expected failure. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex
1 parent 5137638 commit ec99a9e

13 files changed

Lines changed: 869 additions & 0 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
prefix js-native-api
2+
3+
[true]
4+
# napi_detach_arraybuffer() aborts when given an immutable ArrayBuffer.
5+
test_typedarray/test-immutable-arraybuffer: FAIL
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Flags: --js-immutable-arraybuffer
2+
'use strict';
3+
4+
const common = require('../../common');
5+
const assert = require('node:assert');
6+
7+
if (process.argv[2] === 'child') {
8+
const binding = require(`./build/${common.buildType}/test_typedarray`);
9+
const mutable = Uint8Array.from([65, 66, 67, 68]);
10+
binding.Detach(mutable);
11+
assert.strictEqual(binding.IsDetached(mutable.buffer), true);
12+
13+
const buffer = Uint8Array.from([65, 66, 67, 68]).buffer.transferToImmutable();
14+
const view = new Uint8Array(buffer);
15+
16+
// napi_detach_arraybuffer() must return an error or propagate an exception
17+
// when detaching an immutable ArrayBuffer, without aborting the process.
18+
assert.throws(() => binding.Detach(view), Error);
19+
assert.strictEqual(binding.IsDetached(buffer), false);
20+
assert.strictEqual(buffer.immutable, true);
21+
assert.deepStrictEqual([...view], [65, 66, 67, 68]);
22+
} else {
23+
const { spawnSyncAndExitWithoutError } = require('../../common/child_process');
24+
const [command, options] = common.escapePOSIXShell`"${process.execPath}" --js-immutable-arraybuffer "${__filename}" child`;
25+
// Avoid core files while this known issue still aborts the child.
26+
const childCommand = common.isWindows ? command : `ulimit -c 0 && exec ${command}`;
27+
spawnSyncAndExitWithoutError(childCommand, { ...options, shell: true });
28+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Flags: --js-immutable-arraybuffer
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const { test } = require('node:test');
7+
8+
// Native Buffer mutators must preserve immutable backing storage, whether they
9+
// reject the operation or return without modifying the bytes.
10+
function checkImmutable(mutate) {
11+
const expected = Array.from({ length: 64 }, (_, i) => i);
12+
const ab = Uint8Array.from(expected).buffer.transferToImmutable();
13+
const buffer = Buffer.from(ab);
14+
15+
try {
16+
mutate(buffer);
17+
} catch (err) {
18+
if (!(err instanceof TypeError)) throw err;
19+
}
20+
21+
assert.deepStrictEqual(Array.from(new Uint8Array(ab)), expected);
22+
}
23+
24+
const encodings = [
25+
['utf8', 'hello'],
26+
['utf-8', 'hello'],
27+
['ascii', 'hello'],
28+
['latin1', 'hello'],
29+
['binary', 'hello'],
30+
['hex', '68656c6c6f'],
31+
['base64', 'aGVsbG8='],
32+
['base64url', 'aGVsbG8'],
33+
['ucs2', 'hello'],
34+
['ucs-2', 'hello'],
35+
['utf16le', 'hello'],
36+
['utf-16le', 'hello'],
37+
];
38+
39+
for (const [encoding, value] of encodings) {
40+
test(`Buffer.write with ${encoding} preserves immutable bytes`, () => {
41+
checkImmutable((buffer) => buffer.write(value, encoding));
42+
});
43+
}
44+
45+
const writers = [
46+
['utf8Write', 'hello'],
47+
['asciiWrite', 'hello'],
48+
['latin1Write', 'hello'],
49+
['hexWrite', '68656c6c6f'],
50+
['base64Write', 'aGVsbG8='],
51+
['base64urlWrite', 'aGVsbG8'],
52+
['ucs2Write', 'hello'],
53+
];
54+
55+
for (const [method, value] of writers) {
56+
test(`Buffer.${method} preserves immutable bytes`, () => {
57+
checkImmutable((buffer) => buffer[method](value, 0, buffer.length));
58+
});
59+
}
60+
61+
const fills = [
62+
['string', 'AB'],
63+
['non-ASCII character', 'é'],
64+
['Buffer', Buffer.from([0xfe, 0xff])],
65+
['coerced value', true],
66+
];
67+
68+
for (const [name, value] of fills) {
69+
test(`Buffer.fill with ${name} preserves immutable bytes`, () => {
70+
checkImmutable((buffer) => buffer.fill(value));
71+
});
72+
}
73+
74+
for (const method of ['swap16', 'swap32', 'swap64']) {
75+
test(`Buffer.${method} preserves immutable bytes`, () => {
76+
// A 64-byte buffer reaches the native path for all three swap methods.
77+
checkImmutable((buffer) => buffer[method]());
78+
});
79+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Flags: --js-immutable-arraybuffer --experimental-stream-iter
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('node:assert');
6+
const { test } = require('node:test');
7+
const { Readable } = require('node:stream');
8+
const iter = require('node:stream/iter');
9+
const { serialize, deserialize } = require('node:v8');
10+
11+
// V8 currently checks the source of TypedArray.prototype.set() for write
12+
// access. Copying immutable bytes into a mutable destination must be allowed.
13+
function immutable(bytes = [65, 66, 67, 68]) {
14+
return new Uint8Array(Uint8Array.from(bytes).buffer.transferToImmutable());
15+
}
16+
17+
for (const [name, copy] of [
18+
['Buffer.from', (view) => Buffer.from(view)],
19+
['Buffer.copyBytesFrom', (view) => Buffer.copyBytesFrom(view)],
20+
['Buffer.concat', (view) => Buffer.concat([view])],
21+
['Buffer.concat with length', (view) => Buffer.concat([view], view.length)],
22+
]) {
23+
test(name, () => {
24+
const source = immutable();
25+
assert.deepStrictEqual([...copy(source)], [...source]);
26+
});
27+
}
28+
29+
test('TextDecoder single-byte non-ASCII input', () => {
30+
assert.strictEqual(new TextDecoder('windows-1252').decode(immutable([0xe9])), '\u00e9');
31+
});
32+
33+
test('TextDecoder split UTF-8 sequence', () => {
34+
const decoder = new TextDecoder();
35+
assert.strictEqual(decoder.decode(immutable([0xc3]), { stream: true }), '');
36+
assert.strictEqual(decoder.decode(immutable([0xa9])), '\u00e9');
37+
});
38+
39+
test('TextDecoderStream split UTF-8 sequence', async () => {
40+
const source = new ReadableStream({
41+
start(controller) {
42+
controller.enqueue(immutable([0xc3]));
43+
controller.enqueue(immutable([0xa9]));
44+
controller.close();
45+
},
46+
});
47+
let result = '';
48+
for await (const chunk of source.pipeThrough(new TextDecoderStream())) {
49+
result += chunk;
50+
}
51+
assert.strictEqual(result, '\u00e9');
52+
});
53+
54+
test('Readable.read spanning chunks', (t) => {
55+
const stream = new Readable({ read() {} });
56+
t.after(() => stream.destroy());
57+
stream.push(Buffer.from(immutable().buffer));
58+
stream.push(Buffer.from(immutable().buffer));
59+
stream.push(null);
60+
assert.deepStrictEqual([...stream.read(6)], [65, 66, 67, 68, 65, 66]);
61+
});
62+
63+
for (const name of ['bytes', 'bytesSync', 'arrayBuffer', 'arrayBufferSync', 'text', 'textSync']) {
64+
test(`stream/iter.${name}`, async () => {
65+
// A partial view cannot use the full-buffer fast path.
66+
const result = await iter[name](immutable().subarray(1));
67+
if (typeof result === 'string') {
68+
assert.strictEqual(result, 'BCD');
69+
} else {
70+
assert.deepStrictEqual([...new Uint8Array(result)], [66, 67, 68]);
71+
}
72+
});
73+
}
74+
75+
for (const [name, consume] of [
76+
['Response', (view) => new Response(view).text()],
77+
['Request', (view) => new Request('http://localhost/', {
78+
method: 'POST', body: view,
79+
}).text()],
80+
]) {
81+
test(`${name} body`, async () => {
82+
assert.strictEqual(await consume(immutable()), 'ABCD');
83+
});
84+
}
85+
86+
test('Response consumes immutable stream chunks', async () => {
87+
const stream = new ReadableStream({
88+
start(controller) {
89+
controller.enqueue(immutable());
90+
controller.close();
91+
},
92+
});
93+
assert.strictEqual(await new Response(stream).text(), 'ABCD');
94+
});
95+
96+
test('v8.deserialize copies an unaligned payload from immutable storage', () => {
97+
const wire = serialize(new Uint32Array([1, 2]));
98+
const source = wire.buffer.sliceToImmutable(wire.byteOffset, wire.byteOffset + wire.length);
99+
assert.deepStrictEqual([...deserialize(new Uint8Array(source))], [1, 2]);
100+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Flags: --js-immutable-arraybuffer
2+
'use strict';
3+
4+
const common = require('../common');
5+
6+
if (!common.hasCrypto)
7+
common.skip('missing crypto');
8+
9+
const assert = require('assert');
10+
const crypto = require('crypto');
11+
const { test } = require('node:test');
12+
13+
const cases = [
14+
['randomFillSync with an ArrayBuffer', (buffer) => {
15+
crypto.randomFillSync(buffer);
16+
}],
17+
['randomFill with a DataView', (buffer) => {
18+
return new Promise((resolve, reject) => {
19+
crypto.randomFill(new DataView(buffer), (err) => {
20+
if (err) {
21+
reject(err);
22+
return;
23+
}
24+
resolve();
25+
});
26+
});
27+
}],
28+
['getRandomValues with a Uint8Array', (buffer) => {
29+
crypto.getRandomValues(new Uint8Array(buffer));
30+
}],
31+
['global crypto.getRandomValues with a Buffer', (buffer) => {
32+
globalThis.crypto.getRandomValues(Buffer.from(buffer));
33+
}],
34+
];
35+
36+
for (const [name, fill] of cases) {
37+
test(name, async () => {
38+
const buffer = new ArrayBuffer(32).transferToImmutable();
39+
const bytes = new Uint8Array(buffer);
40+
const original = Array.from(bytes);
41+
42+
try {
43+
await fill(buffer);
44+
} catch (err) {
45+
if (!(err instanceof TypeError))
46+
throw err;
47+
}
48+
49+
assert.strictEqual(buffer.immutable, true);
50+
assert.deepStrictEqual(Array.from(bytes), original);
51+
});
52+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Flags: --js-immutable-arraybuffer
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const { test } = require('node:test');
7+
8+
for (const [name, input] of [['short', 'hello'], ['long', 'hello'.repeat(16)]]) {
9+
test(`TextEncoder.encodeInto with ${name} input preserves immutable bytes`, () => {
10+
const expected = Array(128).fill(0xaa);
11+
const ab = Uint8Array.from(expected).buffer.transferToImmutable();
12+
const destination = new Uint8Array(ab);
13+
14+
// The short and long inputs exercise different native encoding paths.
15+
// Do not require a particular rejection behavior, only unchanged bytes.
16+
try {
17+
new TextEncoder().encodeInto(input, destination);
18+
} catch (err) {
19+
if (!(err instanceof TypeError)) throw err;
20+
}
21+
22+
assert.deepStrictEqual(Array.from(new Uint8Array(ab)), expected);
23+
});
24+
}

0 commit comments

Comments
 (0)