From ec99a9ef841fdd1f237519ca4f4180f8dcabed85 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 27 Sep 2026 15:55:08 +0200 Subject: [PATCH] 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 Assisted-by: Codex --- test/js-native-api/js-native-api.status | 5 + .../test-immutable-arraybuffer.js | 28 ++++ .../test-immutable-arraybuffer-buffer.js | 79 ++++++++++ .../test-immutable-arraybuffer-copy.js | 100 +++++++++++++ .../test-immutable-arraybuffer-crypto.js | 52 +++++++ .../test-immutable-arraybuffer-encoding.js | 24 +++ .../test-immutable-arraybuffer-fs-copy.js | 138 ++++++++++++++++++ .../test-immutable-arraybuffer-fs.js | 76 ++++++++++ .../test-immutable-arraybuffer-inspector.js | 91 ++++++++++++ .../test-immutable-arraybuffer-net.js | 78 ++++++++++ ...est-immutable-arraybuffer-serialization.js | 52 +++++++ .../test-immutable-arraybuffer-vfs.js | 83 +++++++++++ .../test-immutable-arraybuffer-zip.js | 63 ++++++++ 13 files changed, 869 insertions(+) create mode 100644 test/js-native-api/js-native-api.status create mode 100644 test/js-native-api/test_typedarray/test-immutable-arraybuffer.js create mode 100644 test/known_issues/test-immutable-arraybuffer-buffer.js create mode 100644 test/known_issues/test-immutable-arraybuffer-copy.js create mode 100644 test/known_issues/test-immutable-arraybuffer-crypto.js create mode 100644 test/known_issues/test-immutable-arraybuffer-encoding.js create mode 100644 test/known_issues/test-immutable-arraybuffer-fs-copy.js create mode 100644 test/known_issues/test-immutable-arraybuffer-fs.js create mode 100644 test/known_issues/test-immutable-arraybuffer-inspector.js create mode 100644 test/known_issues/test-immutable-arraybuffer-net.js create mode 100644 test/known_issues/test-immutable-arraybuffer-serialization.js create mode 100644 test/known_issues/test-immutable-arraybuffer-vfs.js create mode 100644 test/known_issues/test-immutable-arraybuffer-zip.js diff --git a/test/js-native-api/js-native-api.status b/test/js-native-api/js-native-api.status new file mode 100644 index 000000000000..b8dcd80de6ab --- /dev/null +++ b/test/js-native-api/js-native-api.status @@ -0,0 +1,5 @@ +prefix js-native-api + +[true] +# napi_detach_arraybuffer() aborts when given an immutable ArrayBuffer. +test_typedarray/test-immutable-arraybuffer: FAIL diff --git a/test/js-native-api/test_typedarray/test-immutable-arraybuffer.js b/test/js-native-api/test_typedarray/test-immutable-arraybuffer.js new file mode 100644 index 000000000000..cbe311b3c1a5 --- /dev/null +++ b/test/js-native-api/test_typedarray/test-immutable-arraybuffer.js @@ -0,0 +1,28 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +const common = require('../../common'); +const assert = require('node:assert'); + +if (process.argv[2] === 'child') { + const binding = require(`./build/${common.buildType}/test_typedarray`); + const mutable = Uint8Array.from([65, 66, 67, 68]); + binding.Detach(mutable); + assert.strictEqual(binding.IsDetached(mutable.buffer), true); + + const buffer = Uint8Array.from([65, 66, 67, 68]).buffer.transferToImmutable(); + const view = new Uint8Array(buffer); + + // napi_detach_arraybuffer() must return an error or propagate an exception + // when detaching an immutable ArrayBuffer, without aborting the process. + assert.throws(() => binding.Detach(view), Error); + assert.strictEqual(binding.IsDetached(buffer), false); + assert.strictEqual(buffer.immutable, true); + assert.deepStrictEqual([...view], [65, 66, 67, 68]); +} else { + const { spawnSyncAndExitWithoutError } = require('../../common/child_process'); + const [command, options] = common.escapePOSIXShell`"${process.execPath}" --js-immutable-arraybuffer "${__filename}" child`; + // Avoid core files while this known issue still aborts the child. + const childCommand = common.isWindows ? command : `ulimit -c 0 && exec ${command}`; + spawnSyncAndExitWithoutError(childCommand, { ...options, shell: true }); +} diff --git a/test/known_issues/test-immutable-arraybuffer-buffer.js b/test/known_issues/test-immutable-arraybuffer-buffer.js new file mode 100644 index 000000000000..15442efac12d --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-buffer.js @@ -0,0 +1,79 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +require('../common'); +const assert = require('assert'); +const { test } = require('node:test'); + +// Native Buffer mutators must preserve immutable backing storage, whether they +// reject the operation or return without modifying the bytes. +function checkImmutable(mutate) { + const expected = Array.from({ length: 64 }, (_, i) => i); + const ab = Uint8Array.from(expected).buffer.transferToImmutable(); + const buffer = Buffer.from(ab); + + try { + mutate(buffer); + } catch (err) { + if (!(err instanceof TypeError)) throw err; + } + + assert.deepStrictEqual(Array.from(new Uint8Array(ab)), expected); +} + +const encodings = [ + ['utf8', 'hello'], + ['utf-8', 'hello'], + ['ascii', 'hello'], + ['latin1', 'hello'], + ['binary', 'hello'], + ['hex', '68656c6c6f'], + ['base64', 'aGVsbG8='], + ['base64url', 'aGVsbG8'], + ['ucs2', 'hello'], + ['ucs-2', 'hello'], + ['utf16le', 'hello'], + ['utf-16le', 'hello'], +]; + +for (const [encoding, value] of encodings) { + test(`Buffer.write with ${encoding} preserves immutable bytes`, () => { + checkImmutable((buffer) => buffer.write(value, encoding)); + }); +} + +const writers = [ + ['utf8Write', 'hello'], + ['asciiWrite', 'hello'], + ['latin1Write', 'hello'], + ['hexWrite', '68656c6c6f'], + ['base64Write', 'aGVsbG8='], + ['base64urlWrite', 'aGVsbG8'], + ['ucs2Write', 'hello'], +]; + +for (const [method, value] of writers) { + test(`Buffer.${method} preserves immutable bytes`, () => { + checkImmutable((buffer) => buffer[method](value, 0, buffer.length)); + }); +} + +const fills = [ + ['string', 'AB'], + ['non-ASCII character', 'é'], + ['Buffer', Buffer.from([0xfe, 0xff])], + ['coerced value', true], +]; + +for (const [name, value] of fills) { + test(`Buffer.fill with ${name} preserves immutable bytes`, () => { + checkImmutable((buffer) => buffer.fill(value)); + }); +} + +for (const method of ['swap16', 'swap32', 'swap64']) { + test(`Buffer.${method} preserves immutable bytes`, () => { + // A 64-byte buffer reaches the native path for all three swap methods. + checkImmutable((buffer) => buffer[method]()); + }); +} diff --git a/test/known_issues/test-immutable-arraybuffer-copy.js b/test/known_issues/test-immutable-arraybuffer-copy.js new file mode 100644 index 000000000000..6cafae243f90 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-copy.js @@ -0,0 +1,100 @@ +// Flags: --js-immutable-arraybuffer --experimental-stream-iter +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { test } = require('node:test'); +const { Readable } = require('node:stream'); +const iter = require('node:stream/iter'); +const { serialize, deserialize } = require('node:v8'); + +// V8 currently checks the source of TypedArray.prototype.set() for write +// access. Copying immutable bytes into a mutable destination must be allowed. +function immutable(bytes = [65, 66, 67, 68]) { + return new Uint8Array(Uint8Array.from(bytes).buffer.transferToImmutable()); +} + +for (const [name, copy] of [ + ['Buffer.from', (view) => Buffer.from(view)], + ['Buffer.copyBytesFrom', (view) => Buffer.copyBytesFrom(view)], + ['Buffer.concat', (view) => Buffer.concat([view])], + ['Buffer.concat with length', (view) => Buffer.concat([view], view.length)], +]) { + test(name, () => { + const source = immutable(); + assert.deepStrictEqual([...copy(source)], [...source]); + }); +} + +test('TextDecoder single-byte non-ASCII input', () => { + assert.strictEqual(new TextDecoder('windows-1252').decode(immutable([0xe9])), '\u00e9'); +}); + +test('TextDecoder split UTF-8 sequence', () => { + const decoder = new TextDecoder(); + assert.strictEqual(decoder.decode(immutable([0xc3]), { stream: true }), ''); + assert.strictEqual(decoder.decode(immutable([0xa9])), '\u00e9'); +}); + +test('TextDecoderStream split UTF-8 sequence', async () => { + const source = new ReadableStream({ + start(controller) { + controller.enqueue(immutable([0xc3])); + controller.enqueue(immutable([0xa9])); + controller.close(); + }, + }); + let result = ''; + for await (const chunk of source.pipeThrough(new TextDecoderStream())) { + result += chunk; + } + assert.strictEqual(result, '\u00e9'); +}); + +test('Readable.read spanning chunks', (t) => { + const stream = new Readable({ read() {} }); + t.after(() => stream.destroy()); + stream.push(Buffer.from(immutable().buffer)); + stream.push(Buffer.from(immutable().buffer)); + stream.push(null); + assert.deepStrictEqual([...stream.read(6)], [65, 66, 67, 68, 65, 66]); +}); + +for (const name of ['bytes', 'bytesSync', 'arrayBuffer', 'arrayBufferSync', 'text', 'textSync']) { + test(`stream/iter.${name}`, async () => { + // A partial view cannot use the full-buffer fast path. + const result = await iter[name](immutable().subarray(1)); + if (typeof result === 'string') { + assert.strictEqual(result, 'BCD'); + } else { + assert.deepStrictEqual([...new Uint8Array(result)], [66, 67, 68]); + } + }); +} + +for (const [name, consume] of [ + ['Response', (view) => new Response(view).text()], + ['Request', (view) => new Request('http://localhost/', { + method: 'POST', body: view, + }).text()], +]) { + test(`${name} body`, async () => { + assert.strictEqual(await consume(immutable()), 'ABCD'); + }); +} + +test('Response consumes immutable stream chunks', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(immutable()); + controller.close(); + }, + }); + assert.strictEqual(await new Response(stream).text(), 'ABCD'); +}); + +test('v8.deserialize copies an unaligned payload from immutable storage', () => { + const wire = serialize(new Uint32Array([1, 2])); + const source = wire.buffer.sliceToImmutable(wire.byteOffset, wire.byteOffset + wire.length); + assert.deepStrictEqual([...deserialize(new Uint8Array(source))], [1, 2]); +}); diff --git a/test/known_issues/test-immutable-arraybuffer-crypto.js b/test/known_issues/test-immutable-arraybuffer-crypto.js new file mode 100644 index 000000000000..d200bd33b290 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-crypto.js @@ -0,0 +1,52 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { test } = require('node:test'); + +const cases = [ + ['randomFillSync with an ArrayBuffer', (buffer) => { + crypto.randomFillSync(buffer); + }], + ['randomFill with a DataView', (buffer) => { + return new Promise((resolve, reject) => { + crypto.randomFill(new DataView(buffer), (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + }], + ['getRandomValues with a Uint8Array', (buffer) => { + crypto.getRandomValues(new Uint8Array(buffer)); + }], + ['global crypto.getRandomValues with a Buffer', (buffer) => { + globalThis.crypto.getRandomValues(Buffer.from(buffer)); + }], +]; + +for (const [name, fill] of cases) { + test(name, async () => { + const buffer = new ArrayBuffer(32).transferToImmutable(); + const bytes = new Uint8Array(buffer); + const original = Array.from(bytes); + + try { + await fill(buffer); + } catch (err) { + if (!(err instanceof TypeError)) + throw err; + } + + assert.strictEqual(buffer.immutable, true); + assert.deepStrictEqual(Array.from(bytes), original); + }); +} diff --git a/test/known_issues/test-immutable-arraybuffer-encoding.js b/test/known_issues/test-immutable-arraybuffer-encoding.js new file mode 100644 index 000000000000..cfcdb9070c8c --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-encoding.js @@ -0,0 +1,24 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +require('../common'); +const assert = require('assert'); +const { test } = require('node:test'); + +for (const [name, input] of [['short', 'hello'], ['long', 'hello'.repeat(16)]]) { + test(`TextEncoder.encodeInto with ${name} input preserves immutable bytes`, () => { + const expected = Array(128).fill(0xaa); + const ab = Uint8Array.from(expected).buffer.transferToImmutable(); + const destination = new Uint8Array(ab); + + // The short and long inputs exercise different native encoding paths. + // Do not require a particular rejection behavior, only unchanged bytes. + try { + new TextEncoder().encodeInto(input, destination); + } catch (err) { + if (!(err instanceof TypeError)) throw err; + } + + assert.deepStrictEqual(Array.from(new Uint8Array(ab)), expected); + }); +} diff --git a/test/known_issues/test-immutable-arraybuffer-fs-copy.js b/test/known_issues/test-immutable-arraybuffer-fs-copy.js new file mode 100644 index 000000000000..564068fa1362 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-fs-copy.js @@ -0,0 +1,138 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const { once } = require('node:events'); +const { join } = require('node:path'); +const { test } = require('node:test'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const tmpdir = require('../common/tmpdir'); + +// These APIs only read the supplied bytes. Internal copies must accept an +// immutable source just as they accept a mutable one. +function immutable(value) { + return Buffer.from(Uint8Array.from(Buffer.from(value)).buffer.transferToImmutable()); +} + +const cases = new Map(); +const options = { recursive: true, withFileTypes: true }; + +for (const method of ['readdirSync', 'readdir', 'promises.readdir']) { + cases.set(`fs.${method} with a recursive Buffer path`, (directory) => checkReaddir(directory, method)); +} + +async function checkReaddir(directory, method) { + fs.mkdirSync(join(directory, 'nested')); + fs.writeFileSync(join(directory, 'nested', 'file'), 'contents'); + const path = immutable(directory); + let entries; + if (method === 'readdirSync') { + entries = fs.readdirSync(path, options); + } else if (method === 'readdir') { + entries = await new Promise((resolve) => { + fs.readdir(path, options, common.mustSucceed(resolve)); + }); + } else { + entries = await fs.promises.readdir(path, options); + } + assert.deepStrictEqual(entries.map((entry) => entry.name).sort(), ['file', 'nested']); + const file = entries.find((entry) => entry.name === 'file'); + assert(file.isFile()); + assert.strictEqual(file.parentPath.toString(), join(directory, 'nested')); +} + +for (const method of ['rm', 'promises.rm']) { + cases.set(`fs.${method} with a recursive Buffer path`, (directory) => checkRm(directory, method)); +} + +async function checkRm(directory, method) { + fs.writeFileSync(join(directory, 'file'), 'contents'); + const path = immutable(directory); + if (method === 'rm') { + await new Promise((resolve) => { + fs.rm(path, { recursive: true }, common.mustSucceed(resolve)); + }); + } else { + await fs.promises.rm(path, { recursive: true }); + } + assert.strictEqual(fs.existsSync(directory), false); +} + +for (const sync of [false, true]) { + cases.set(`fs.Utf8Stream with ${sync ? 'merged synchronous' : 'asynchronous'} buffers`, + (directory) => checkUtf8Stream(directory, sync)); +} + +async function checkUtf8Stream(directory, sync) { + const path = join(directory, 'output'); + const stream = new fs.Utf8Stream({ + fd: fs.openSync(path, 'w'), + contentMode: 'buffer', + sync, + // Buffer two chunks before the synchronous write so they must be merged. + minLength: sync ? 8 : 0, + }); + const closed = once(stream, 'close'); + try { + stream.write(immutable('ABCD')); + if (sync) stream.write(immutable('EFGH')); + stream.end(); + await closed; + assert.strictEqual(fs.readFileSync(path, 'utf8'), sync ? 'ABCDEFGH' : 'ABCD'); + } finally { + stream.destroy(); + await closed; + } +} + +cases.set('fs.WriteStream retries a partial writev', checkWriteStream); + +async function checkWriteStream() { + const output = []; + let calls = 0; + const stream = fs.createWriteStream(null, { + // All descriptor operations are provided by the custom fs implementation. + fd: 123, + fs: { + writev: common.mustCall((fd, buffers, position, callback) => { + const bytes = buffers.flatMap((buffer) => [...buffer]); + const written = calls++ === 0 ? 1 : bytes.length; + output.push(...bytes.slice(0, written)); + process.nextTick(callback, null, written, buffers); + }, 2), + close: common.mustCall((fd, callback) => process.nextTick(callback, null)), + }, + }); + const closed = once(stream, 'close'); + stream.cork(); + stream.write(immutable('ABCD')); + stream.write(immutable('EFGH')); + stream.end(); + await closed; + assert.deepStrictEqual(output, [...Buffer.from('ABCDEFGH')]); + assert.strictEqual(stream.bytesWritten, 8); +} + +if (process.argv[2] === 'child') { + // Some current failures escape an asynchronous callback or leave a promise + // pending. Require completion even when the child otherwise exits normally. + cases.get(process.argv[3])(process.argv[4]).then(common.mustCall()); +} else { + tmpdir.refresh(); + let index = 0; + for (const name of cases.keys()) { + test(name, () => { + const directory = tmpdir.resolve(`case-${index++}`); + fs.mkdirSync(directory); + try { + spawnSyncAndAssert(process.execPath, [ + ...process.execArgv, __filename, 'child', name, directory, + ], {}); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + } +} diff --git a/test/known_issues/test-immutable-arraybuffer-fs.js b/test/known_issues/test-immutable-arraybuffer-fs.js new file mode 100644 index 000000000000..34139b19f516 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-fs.js @@ -0,0 +1,76 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { test } = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +// Reading into an immutable backing store must not change its bytes. +tmpdir.refresh(); +const path = tmpdir.resolve('immutable-arraybuffer'); +fs.writeFileSync(path, 'test'); + +async function checkRead(read) { + const buffer = new Uint8Array(new ArrayBuffer(4).transferToImmutable()); + try { + await read(buffer); + } catch (err) { + if (!(err instanceof TypeError)) throw err; + } + assert.deepStrictEqual([...buffer], [0, 0, 0, 0]); +} + +const fdReads = [ + ['fs.readSync', (fd, buffer) => fs.readSync(fd, buffer, 0, 4, 0)], + ['fs.read', (fd, buffer) => new Promise((resolve, reject) => { + fs.read(fd, buffer, 0, 4, 0, (err) => (err ? reject(err) : resolve())); + })], + ['fs.readvSync', (fd, buffer) => fs.readvSync(fd, [buffer], 0)], + ['fs.readv', (fd, buffer) => new Promise((resolve, reject) => { + fs.readv(fd, [buffer], 0, (err) => (err ? reject(err) : resolve())); + })], +]; + +for (const [name, read] of fdReads) { + test(name, async (t) => { + const fd = fs.openSync(path, 'r'); + t.after(() => fs.closeSync(fd)); + await checkRead((buffer) => read(fd, buffer)); + }); +} + +for (const method of ['read', 'readv']) { + test(`FileHandle.${method}`, async (t) => { + const handle = await fs.promises.open(path, 'r'); + t.after(() => handle.close()); + await checkRead((buffer) => (method === 'read' ? + handle.read(buffer, 0, 4, 0) : handle.readv([buffer], 0))); + }); +} + +const fileReads = [ + ['fs.readFileSync', (options) => fs.readFileSync(path, options)], + ['fs.readFile', (options) => new Promise((resolve, reject) => { + fs.readFile(path, options, (err) => (err ? reject(err) : resolve())); + })], + ['fs.promises.readFile', (options) => fs.promises.readFile(path, options)], +]; + +for (const factory of [false, true]) { + const name = factory ? 'buffer factory' : 'buffer'; + const options = (buffer) => ({ buffer: factory ? () => buffer : buffer }); + + for (const [method, read] of fileReads) { + test(`${method} with ${name}`, () => { + return checkRead((buffer) => read(options(buffer))); + }); + } + + test(`FileHandle.readFile with ${name}`, async (t) => { + const handle = await fs.promises.open(path, 'r'); + t.after(() => handle.close()); + await checkRead((buffer) => handle.readFile(options(buffer))); + }); +} diff --git a/test/known_issues/test-immutable-arraybuffer-inspector.js b/test/known_issues/test-immutable-arraybuffer-inspector.js new file mode 100644 index 000000000000..908e3aedd734 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-inspector.js @@ -0,0 +1,91 @@ +// Flags: --js-immutable-arraybuffer --experimental-network-inspection +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +common.skipIfInspectorDisabled(); + +const assert = require('node:assert'); + +// Network inspection copies request body chunks, which must also work when +// the body is immutable. Isolate uncaught diagnostic-channel errors so both +// request paths can be tested. +if (process.argv[2]) { + const { once } = require('node:events'); + const { Session } = require('node:inspector/promises'); + const kind = process.argv[2]; + const enabled = process.argv[3] === 'on'; + + async function run() { + const session = new Session(); + session.connect(); + const http = require(kind === 'http2' ? 'node:http2' : 'node:http'); + const expected = kind === 'http2' ? 'ABCDABCD' : 'ABCD'; + const server = http.createServer(common.mustCall((req, res) => { + let body = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => { body += chunk; }); + req.on('end', common.mustCall(() => { + assert.strictEqual(body, expected); + res.end(body); + })); + })); + let client; + try { + if (enabled) { + session.on('Network.requestWillBeSent', common.mustCall()); + await session.post('Network.enable'); + } + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const url = `http://127.0.0.1:${server.address().port}`; + const chunk = Buffer.from(Uint8Array.from([65, 66, 67, 68]).buffer.transferToImmutable()); + let body = ''; + if (kind === 'http2') { + client = http.connect(url); + const req = client.request({ ':method': 'POST' }); + req.cork(); + req.write(chunk); + req.end(chunk); + for await (const data of req) body += data; + } else { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(chunk); + controller.close(); + }, + }); + const response = await fetch(url, { + method: 'POST', body: stream, duplex: 'half', + }); + body = await response.text(); + } + assert.strictEqual(body, expected); + assert.strictEqual(chunk.toString(), 'ABCD'); + assert.strictEqual(chunk.buffer.immutable, true); + } finally { + client?.destroy(); + server.closeAllConnections?.(); + if (server.listening) await new Promise((resolve) => server.close(resolve)); + session.disconnect(); + } + } + + run().then(common.mustCall()); +} else { + const { test } = require('node:test'); + const { spawnSyncAndAssert } = require('../common/child_process'); + + for (const kind of ['http2', 'fetch']) { + test(`${kind} sends immutable chunks with network inspection`, () => { + // The same request succeeds with inspection disabled. + for (const inspection of ['off', 'on']) { + spawnSyncAndAssert(process.execPath, [ + '--js-immutable-arraybuffer', '--experimental-network-inspection', + __filename, kind, inspection, + ], { timeout: common.platformTimeout(10000) }, { status: 0 }); + } + }); + } +} diff --git a/test/known_issues/test-immutable-arraybuffer-net.js b/test/known_issues/test-immutable-arraybuffer-net.js new file mode 100644 index 000000000000..15df19f66bf3 --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-net.js @@ -0,0 +1,78 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) { common.skip('missing crypto'); }; +const assert = require('assert'); +const { once } = require('events'); +const net = require('net'); +const { test } = require('node:test'); + +// Socket reads must not write into an immutable onread buffer. +for (const factory of [false, true]) { + const name = factory ? 'buffer factory' : 'buffer'; + test(`net.Socket onread with ${name}`, { + timeout: common.platformTimeout(5000), + }, async (t) => { + const buffer = new Uint8Array(new ArrayBuffer(4).transferToImmutable()); + const sockets = []; + const server = net.createServer((socket) => { + sockets.push(socket); + socket.end('test'); + }); + t.after(async () => { + for (const socket of sockets) socket.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => { + server.close((err) => (err ? reject(err) : resolve())); + }); + } + }); + + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + + try { + await new Promise((resolve, reject) => { + const socket = net.connect({ + host: '127.0.0.1', + port: server.address().port, + onread: { + buffer: factory ? () => buffer : buffer, + callback: resolve, + }, + }); + sockets.push(socket); + socket.on('error', reject); + socket.on('end', () => reject(new Error('No onread callback'))); + }); + } catch (err) { + if (!(err instanceof TypeError)) throw err; + } + assert.deepStrictEqual([...buffer], [0, 0, 0, 0]); + }); +} + +// ALPN normalization only needs to read the supplied protocol bytes. +for (const type of ['Buffer', 'Uint8Array']) { + for (const api of ['createServer', 'TLSSocket']) { + test(`tls.${api} with immutable ${type} ALPNProtocols`, { + skip: !common.hasCrypto, + }, (t) => { + const tls = require('tls'); + const bytes = [2, 104, 50]; // Length-prefixed "h2". + const buffer = Uint8Array.from(bytes).buffer.transferToImmutable(); + const protocols = type === 'Buffer' ? Buffer.from(buffer) : new Uint8Array(buffer); + const options = { ALPNProtocols: protocols }; + if (api === 'createServer') { + // No listening socket is needed to exercise option normalization. + tls.createServer(options); + } else { + const socket = new tls.TLSSocket(undefined, options); + t.after(() => socket.destroy()); + } + assert.deepStrictEqual([...protocols], bytes); + assert.strictEqual(buffer.immutable, true); + }); + } +} diff --git a/test/known_issues/test-immutable-arraybuffer-serialization.js b/test/known_issues/test-immutable-arraybuffer-serialization.js new file mode 100644 index 000000000000..a7f5ee536cfb --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-serialization.js @@ -0,0 +1,52 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { fork } = require('node:child_process'); +const { once } = require('node:events'); +const { serialize, deserialize } = require('node:v8'); + +// Node's custom view serialization records the type and bytes, but omits the +// backing store's immutability. Bare ArrayBuffers already preserve it. +if (process.argv[2] === 'child') { + process.on('message', common.mustCall((view) => { + const buffer = view.buffer ?? view; + process.send({ + immutable: buffer.immutable, + bytes: [...new Uint8Array(buffer, view.byteOffset ?? 0, view.byteLength)], + }); + process.disconnect(); + })); +} else { + const { test } = require('node:test'); + const bytes = [65, 66, 67, 68]; + + for (const [name, create] of [ + ['ArrayBuffer', (buffer) => buffer], + ['Uint8Array', (buffer) => new Uint8Array(buffer)], + ['DataView', (buffer) => new DataView(buffer)], + ['Buffer', (buffer) => Buffer.from(buffer)], + ]) { + test(`v8 serialization preserves ${name} immutability`, () => { + const input = create(Uint8Array.from(bytes).buffer.transferToImmutable()); + const output = deserialize(serialize(input)); + const buffer = output.buffer ?? output; + assert.deepStrictEqual( + [...new Uint8Array(buffer, output.byteOffset ?? 0, output.byteLength)], bytes); + assert.strictEqual(buffer.immutable, true); + }); + + test(`advanced IPC preserves ${name} immutability`, async (t) => { + const child = fork(__filename, ['child'], { serialization: 'advanced' }); + t.after(() => child.kill()); + const message = once(child, 'message'); + const exit = once(child, 'exit'); + child.send(create(Uint8Array.from(bytes).buffer.transferToImmutable())); + const [output] = await message; + assert.deepStrictEqual(await exit, [0, null]); + assert.deepStrictEqual(output.bytes, bytes); + assert.strictEqual(output.immutable, true); + }); + } +} diff --git a/test/known_issues/test-immutable-arraybuffer-vfs.js b/test/known_issues/test-immutable-arraybuffer-vfs.js new file mode 100644 index 000000000000..3865c5eb481c --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-vfs.js @@ -0,0 +1,83 @@ +// Flags: --js-immutable-arraybuffer --experimental-vfs +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { test } = require('node:test'); +const vfs = require('node:vfs'); +const { ZipEntry, ZipBuffer, createZipArchiveSync } = require('node:zlib'); + +const data = Buffer.from('test'); + +function immutable(buffer) { + return Buffer.from(buffer.buffer.sliceToImmutable( + buffer.byteOffset, buffer.byteOffset + buffer.length)); +} + +function createZip() { + const entry = ZipEntry.createSync('file', data, { method: 'store' }); + const bytes = Buffer.concat([...createZipArchiveSync([entry])]); + return vfs.create(new vfs.ZipProvider(new ZipBuffer(bytes))); +} + +for (const [name, write] of [ + ['writeFileSync', (store, path, source) => store.writeFileSync(path, source)], + ['promises.writeFile', (store, path, source) => store.promises.writeFile(path, source)], +]) { + test(`ZipProvider ${name} accepts immutable source bytes`, async () => { + const store = createZip(); + await write(store, '/control', data); + assert.deepStrictEqual(store.readFileSync('/control'), data); + + const source = immutable(data); + await write(store, '/immutable', source); + assert.deepStrictEqual(store.readFileSync('/immutable'), data); + assert.deepStrictEqual([...source], [...data]); + }); +} + +const reads = [ + ['readSync', (store, fd, buffer) => store.readSync(fd, buffer, 0, buffer.length, null)], + ['read', (store, fd, buffer) => new Promise((resolve, reject) => { + store.read(fd, buffer, 0, buffer.length, null, (err, bytesRead) => { + if (err) reject(err); + else resolve(bytesRead); + }); + })], +]; + +for (const [provider, create] of [ + ['MemoryProvider', () => { + const store = vfs.create(); + store.writeFileSync('/file', data); + return store; + }], + ['ZipProvider', createZip], +]) { + for (const [method, read] of reads) { + test(`${provider} ${method} with an immutable destination`, async (t) => { + const store = create(); + const fd = store.openSync('/file', 'r'); + t.after(() => store.closeSync(fd)); + + const control = Buffer.alloc(data.length); + assert.strictEqual(store.readSync(fd, control, 0, control.length, 0), data.length); + assert.deepStrictEqual(control, data); + + const target = immutable(Buffer.alloc(data.length)); + let bytesRead = 0; + try { + bytesRead = await read(store, fd, target); + } catch (err) { + if (!(err instanceof TypeError)) throw err; + } + assert.deepStrictEqual([...target], [0, 0, 0, 0]); + // Buffer.copy() returns zero for an immutable destination. A read must + // reject or report no bytes, and must not advance the file position. + assert.strictEqual(bytesRead, 0); + control.fill(0); + assert.strictEqual(store.readSync(fd, control, 0, control.length, null), data.length); + assert.deepStrictEqual(control, data); + }); + } +} diff --git a/test/known_issues/test-immutable-arraybuffer-zip.js b/test/known_issues/test-immutable-arraybuffer-zip.js new file mode 100644 index 000000000000..32982127220c --- /dev/null +++ b/test/known_issues/test-immutable-arraybuffer-zip.js @@ -0,0 +1,63 @@ +// Flags: --js-immutable-arraybuffer +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { test } = require('node:test'); +const { ZipEntry, ZipBuffer, createZipArchiveSync } = require('node:zlib'); + +// ZIP copies must accept immutable source bytes, including payloads retained +// from an immutable archive. +// Keep the input compressible so DEFLATE does not fall back to STORE. +const data = Buffer.from('test'.repeat(256)); + +function immutable(buffer) { + return Buffer.from(buffer.buffer.sliceToImmutable( + buffer.byteOffset, buffer.byteOffset + buffer.length)); +} + +function archive(method) { + const entry = ZipEntry.createSync('file', data, { method }); + assert.strictEqual(entry.method, method === 'store' ? 0 : 8); + return Buffer.concat([...createZipArchiveSync([entry])]); +} + +for (const method of ['content', 'contentSync']) { + test(`ZipEntry.${method} with immutable STORE input`, async () => { + const entry = ZipEntry.createSync('file', data, { method: 'store' }); + assert.deepStrictEqual(await entry[method](), data); + + const source = immutable(data); + const immutableEntry = ZipEntry.createSync('file', source, { method: 'store' }); + assert.deepStrictEqual(await immutableEntry[method](), data); + assert.deepStrictEqual([...source], [...data]); + }); + + test(`ZipEntry.${method} with an immutable STORE archive`, async () => { + const bytes = archive('store'); + using mutableZip = new ZipBuffer(bytes); + assert.deepStrictEqual(await mutableZip.get('file')[method](), data); + + const source = immutable(bytes); + using zip = new ZipBuffer(source); + assert.deepStrictEqual(await zip.get('file')[method](), data); + assert.deepStrictEqual([...source], [...bytes]); + }); +} + +for (const compression of ['store', 'deflate']) { + for (const method of ['toBuffer', 'toBufferSync']) { + test(`ZipBuffer.${method} with an immutable ${compression} archive`, async () => { + const bytes = archive(compression); + using mutableZip = new ZipBuffer(bytes); + using control = new ZipBuffer(await mutableZip[method]()); + assert.deepStrictEqual(control.get('file').contentSync(), data); + + const source = immutable(bytes); + using zip = new ZipBuffer(source); + using result = new ZipBuffer(await zip[method]()); + assert.deepStrictEqual(result.get('file').contentSync(), data); + assert.deepStrictEqual([...source], [...bytes]); + }); + } +}