Repository: nodejs/node
Pull request: stream: accept ArrayBuffer in CompressionStream and DecompressionStream

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->

Per the WHATWG Compression Streams spec, `CompressionStream` and
`DecompressionStream` must accept `BufferSource`
(`ArrayBuffer | ArrayBufferView`) as valid chunk types.

Currently, writing a plain `ArrayBuffer` to the writable side throws
`ERR_INVALID_ARG_TYPE`, because the adapter in
`newWritableStreamFromStreamWritable` passes chunks directly to
`stream.write()`, which only accepts `ArrayBufferView` types.

This change normalizes plain `ArrayBuffer` chunks to a `Uint8Array` view
before forwarding them to the underlying Node.js stream.

Changes included:
- normalize `ArrayBuffer` to `Uint8Array` in `lib/internal/webstreams/adapters.js`
- add a regression test for `ArrayBuffer` input
- unskip the relevant WPT compression `BufferSource` test (if passing)

Fixes: #43433

Changed files:
filename: lib/internal/webstreams/adapters.js
patch: @@ -53,6 +53,10 @@ const {
   Buffer,
 } = require('buffer');
 
+const {
+  isArrayBuffer,
+} = require('internal/util/types');
+
 const {
   AbortError,
   ErrnoException,
@@ -213,6 +217,9 @@ function newWritableStreamFromStreamWritable(streamWritable) {
     start(c) { controller = c; },
 
     write(chunk) {
+      if (!streamWritable.writableObjectMode && isArrayBuffer(chunk)) {
+        chunk = new Uint8Array(chunk);
+      }
       if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) {
         backpressurePromise = PromiseWithResolvers();
         return SafePromisePrototypeFinally(

filename: test/parallel/test-webstreams-compression-buffer-source.js
patch: @@ -0,0 +1,42 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const test = require('node:test');
+const { DecompressionStream, CompressionStream } = require('stream/web');
+
+// Minimal gzip-compressed bytes for "hello"
+const compressedGzip = new Uint8Array([
+  31, 139, 8, 0, 0, 0, 0, 0, 0, 3,
+  203, 72, 205, 201, 201, 7, 0, 134, 166, 16, 54, 5, 0, 0, 0,
+]);
+
+test('DecompressionStream accepts ArrayBuffer chunks', async () => {
+  const ds = new DecompressionStream('gzip');
+  const writer = ds.writable.getWriter();
+
+  const writePromise = writer.write(compressedGzip.buffer);
+  writer.close();
+
+  const chunks = await Array.fromAsync(ds.readable);
+  await writePromise;
+  const out = Buffer.concat(chunks.map((c) => Buffer.from(c)));
+  assert.strictEqual(out.toString(), 'hello');
+});
+
+test('CompressionStream round-trip with ArrayBuffer input', async () => {
+  const cs = new CompressionStream('gzip');
+  const ds = new DecompressionStream('gzip');
+
+  const csWriter = cs.writable.getWriter();
+
+  const input = new TextEncoder().encode('hello').buffer;
+
+  await csWriter.write(input);
+  csWriter.close();
+
+  await cs.readable.pipeTo(ds.writable);
+
+  const out = await Array.fromAsync(ds.readable);
+  const result = Buffer.concat(out.map((c) => Buffer.from(c)));
+  assert.strictEqual(result.toString(), 'hello');
+});

filename: test/parallel/test-whatwg-webstreams-adapters-to-writablestream.js
patch: @@ -165,3 +165,35 @@ class TestWritable extends Writable {
   const writer = writableStream.getWriter();
   writer.closed.then(common.mustCall());
 }
+
+{
+  const duplex = new PassThrough();
+  const writableStream = newWritableStreamFromStreamWritable(duplex);
+  const ec = new TextEncoder();
+  const arrayBuffer = ec.encode('hello').buffer;
+  writableStream
+    .getWriter()
+    .write(arrayBuffer)
+    .then(common.mustCall());
+
+  duplex.on('data', common.mustCall((chunk) => {
+    assert(chunk instanceof Buffer);
+    assert(chunk.equals(Buffer.from('hello')));
+  }));
+}
+
+{
+  const duplex = new PassThrough({ objectMode: true });
+  const writableStream = newWritableStreamFromStreamWritable(duplex);
+  const ec = new TextEncoder();
+  const arrayBuffer = ec.encode('hello').buffer;
+  writableStream
+    .getWriter()
+    .write(arrayBuffer)
+    .then(common.mustCall());
+
+  duplex.on('data', common.mustCall((chunk) => {
+    assert(chunk instanceof ArrayBuffer);
+    assert.strictEqual(chunk, arrayBuffer);
+  }));
+}

filename: test/wpt/status/compression.json
patch: @@ -5,9 +5,6 @@
   "decompression-bad-chunks.tentative.any.js": {
     "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write"
   },
-  "decompression-buffersource.tentative.any.js": {
-    "skip": "ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write"
-  },
   "compression-with-detach.tentative.window.js": {
     "requires": ["crypto"]
   },