Commit 118f2a1d78e for nodejs

commit 118f2a1d78e8337397e97a6a8734430c5a29f740
Author: agape1225 <49804691+agape1225@users.noreply.github.com>
Date:   Fri Aug 14 16:54:04 2026 +0900

    stream: support ArrayBufferView in Utf8Stream write() buffer mode

    Utf8Stream#write() in 'buffer' content mode only accepted Buffer
    instances, even though the underlying implementation only needs
    byte-addressable data. This accepts any ArrayBufferView (TypedArray,
    DataView) and reinterprets it as a Buffer over the same bytes
    (without copying), so callers no longer need to wrap other typed
    arrays in Buffer.from() themselves.

    Views are normalized to a Buffer at the single entry point
    (#writeBuffer), using byteOffset/byteLength rather than the view's
    element-count length, so that internal length bookkeeping used by
    mergeBuf()/Buffer.concat() and the write-release logic keeps
    operating on real byte counts. This mirrors the existing pattern in
    zlibBuffer() (lib/zlib.js).

    Signed-off-by: agape1225 <49804691+agape1225@users.noreply.github.com>
    PR-URL: https://github.com/nodejs/node/pull/65301
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>

diff --git a/doc/api/fs.md b/doc/api/fs.md
index 5cbdc2709ff..d6eb4208ef0 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -8614,12 +8614,12 @@ Reopen the file in place, useful for log rotation.

 #### `utf8Stream.write(data)`

-* `data` {string|Buffer} The data to write.
+* `data` {string|Buffer|TypedArray|DataView} The data to write.
 * Returns {boolean}

 When the `options.contentMode` is set to `'utf8'` when the stream is created,
 the `data` argument must be a string. If the `contentMode` is set to `'buffer'`,
-the `data` argument must be a {Buffer}.
+the `data` argument must be a {Buffer}, {TypedArray}, or {DataView}.

 #### `utf8Stream.writing`

diff --git a/lib/internal/streams/fast-utf8-stream.js b/lib/internal/streams/fast-utf8-stream.js
index 8a1b6185e82..a11fbf3bbc0 100644
--- a/lib/internal/streams/fast-utf8-stream.js
+++ b/lib/internal/streams/fast-utf8-stream.js
@@ -19,6 +19,10 @@ const {
   Buffer,
 } = require('buffer');

+const {
+  isArrayBufferView,
+} = require('internal/util/types');
+
 const fs = require('fs');
 const EventEmitter = require('events');
 const path = require('path');
@@ -803,9 +807,14 @@ class Utf8Stream extends EventEmitter {
       throw new ERR_INVALID_STATE('Utf8Stream is destroyed');
     }

-    // TODO(@jasnell): Support any ArrayBufferView type here, not just Buffer.
+    if (!isArrayBufferView(data)) {
+      throw new ERR_INVALID_ARG_TYPE('data', ['Buffer', 'TypedArray', 'DataView'], data);
+    }
     if (!Buffer.isBuffer(data)) {
-      throw new ERR_INVALID_ARG_TYPE('data', 'Buffer', data);
+      // Reinterpret the view as a byte-oriented Buffer without copying, so that
+      // `data.length` below (and everywhere else `bufs`/`lens` are consumed)
+      // reflects the byte length rather than the element count.
+      data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
     }

     const len = this.#len + data.length;
diff --git a/test/parallel/test-fastutf8stream-write-arraybufferview.js b/test/parallel/test-fastutf8stream-write-arraybufferview.js
new file mode 100644
index 00000000000..cd65f4706f2
--- /dev/null
+++ b/test/parallel/test-fastutf8stream-write-arraybufferview.js
@@ -0,0 +1,98 @@
+'use strict';
+
+// In 'buffer' content mode, Utf8Stream#write() used to only accept Buffer
+// instances. This verifies it also accepts other ArrayBufferView types
+// (TypedArrays, DataView), that byte length (not element count) is used
+// when the view has multiple bytes per element, that byteOffset-based
+// subviews only write the bytes they cover, and that non-ArrayBufferView
+// input is still rejected.
+
+const common = require('../common');
+const tmpdir = require('../common/tmpdir');
+const assert = require('node:assert');
+const {
+  readFile,
+  Utf8Stream,
+} = require('node:fs');
+const { join } = require('node:path');
+
+tmpdir.refresh();
+let fileCounter = 0;
+
+function getTempFile() {
+  return join(tmpdir.path, `fastutf8stream-abv-${process.pid}-${Date.now()}-${fileCounter++}.log`);
+}
+
+function writeAndVerify(sync, data, expected) {
+  const dest = getTempFile();
+  const stream = new Utf8Stream({ dest, sync, contentMode: 'buffer' });
+
+  stream.on('ready', common.mustCall(() => {
+    assert.ok(stream.write(data));
+    stream.end();
+
+    stream.on('finish', common.mustCall(() => {
+      readFile(dest, common.mustSucceed((buf) => {
+        assert.deepStrictEqual(buf, expected);
+      }));
+    }));
+  }));
+}
+
+for (const sync of [false, true]) {
+  {
+    // A plain Uint8Array (not a Buffer instance) must be accepted, and
+    // written byte-for-byte.
+    const view = new Uint8Array([0x68, 0x69, 0x0a]); // "hi\n"
+    writeAndVerify(sync, view, Buffer.from(view));
+  }
+
+  {
+    // A DataView must be accepted.
+    const ab = new ArrayBuffer(4);
+    new DataView(ab).setUint32(0, 0x61626364); // "abcd"
+    const view = new DataView(ab);
+    writeAndVerify(sync, view, Buffer.from(ab));
+  }
+
+  {
+    // Float64Array: each element is 8 bytes, so `.length` (element count)
+    // must not be confused with `.byteLength` (actual byte count). If the
+    // implementation used `.length` when accumulating/merging, the output
+    // would be truncated to a fraction of the real byte size.
+    const view = new Float64Array([1.5, -2.25, 3]);
+    writeAndVerify(sync, view, Buffer.from(view.buffer, view.byteOffset, view.byteLength));
+  }
+
+  {
+    // A view with a non-zero byteOffset over a shared, larger ArrayBuffer
+    // must only write the bytes it covers, not the whole backing buffer.
+    const ab = new ArrayBuffer(8);
+    const full = new Uint8Array(ab);
+    full.set([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22]);
+    const view = new Uint8Array(ab, 2, 3); // [0xcc, 0xdd, 0xee]
+    writeAndVerify(sync, view, Buffer.from([0xcc, 0xdd, 0xee]));
+  }
+
+  {
+    // Non-ArrayBufferView input must still be rejected in 'buffer' mode.
+    const dest = getTempFile();
+    const stream = new Utf8Stream({ dest, sync, contentMode: 'buffer' });
+
+    stream.on('ready', common.mustCall(() => {
+      assert.throws(() => {
+        stream.write('not a buffer');
+      }, {
+        code: 'ERR_INVALID_ARG_TYPE',
+        name: 'TypeError',
+      });
+      assert.throws(() => {
+        stream.write([1, 2, 3]);
+      }, {
+        code: 'ERR_INVALID_ARG_TYPE',
+        name: 'TypeError',
+      });
+      stream.end();
+    }));
+  }
+}