From c15c1bca8acc92c0030e22a4644c179aaf013db5 Mon Sep 17 00:00:00 2001 From: agape1225 <49804691+agape1225@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:54:04 +0900 Subject: [PATCH] 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> --- doc/api/fs.md | 4 +- lib/internal/streams/fast-utf8-stream.js | 13 ++- ...st-fastutf8stream-write-arraybufferview.js | 98 +++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-fastutf8stream-write-arraybufferview.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 21eaa6af0e1c..4bb81b9ef0d4 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -8495,12 +8495,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 51d80bbe5e74..309c30baaf50 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'); @@ -801,9 +805,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 000000000000..cd65f4706f2e --- /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(); + })); + } +}