Skip to content

Commit 118f2a1

Browse files
agape1225panva
authored andcommitted
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: #65301 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 5ebf690 commit 118f2a1

3 files changed

Lines changed: 111 additions & 4 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8614,12 +8614,12 @@ Reopen the file in place, useful for log rotation.
86148614
86158615
#### `utf8Stream.write(data)`
86168616
8617-
* `data` {string|Buffer} The data to write.
8617+
* `data` {string|Buffer|TypedArray|DataView} The data to write.
86188618
* Returns {boolean}
86198619
86208620
When the `options.contentMode` is set to `'utf8'` when the stream is created,
86218621
the `data` argument must be a string. If the `contentMode` is set to `'buffer'`,
8622-
the `data` argument must be a {Buffer}.
8622+
the `data` argument must be a {Buffer}, {TypedArray}, or {DataView}.
86238623
86248624
#### `utf8Stream.writing`
86258625

‎lib/internal/streams/fast-utf8-stream.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const {
1919
Buffer,
2020
} = require('buffer');
2121

22+
const {
23+
isArrayBufferView,
24+
} = require('internal/util/types');
25+
2226
const fs = require('fs');
2327
const EventEmitter = require('events');
2428
const path = require('path');
@@ -803,9 +807,14 @@ class Utf8Stream extends EventEmitter {
803807
throw new ERR_INVALID_STATE('Utf8Stream is destroyed');
804808
}
805809

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

811820
const len = this.#len + data.length;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict';
2+
3+
// In 'buffer' content mode, Utf8Stream#write() used to only accept Buffer
4+
// instances. This verifies it also accepts other ArrayBufferView types
5+
// (TypedArrays, DataView), that byte length (not element count) is used
6+
// when the view has multiple bytes per element, that byteOffset-based
7+
// subviews only write the bytes they cover, and that non-ArrayBufferView
8+
// input is still rejected.
9+
10+
const common = require('../common');
11+
const tmpdir = require('../common/tmpdir');
12+
const assert = require('node:assert');
13+
const {
14+
readFile,
15+
Utf8Stream,
16+
} = require('node:fs');
17+
const { join } = require('node:path');
18+
19+
tmpdir.refresh();
20+
let fileCounter = 0;
21+
22+
function getTempFile() {
23+
return join(tmpdir.path, `fastutf8stream-abv-${process.pid}-${Date.now()}-${fileCounter++}.log`);
24+
}
25+
26+
function writeAndVerify(sync, data, expected) {
27+
const dest = getTempFile();
28+
const stream = new Utf8Stream({ dest, sync, contentMode: 'buffer' });
29+
30+
stream.on('ready', common.mustCall(() => {
31+
assert.ok(stream.write(data));
32+
stream.end();
33+
34+
stream.on('finish', common.mustCall(() => {
35+
readFile(dest, common.mustSucceed((buf) => {
36+
assert.deepStrictEqual(buf, expected);
37+
}));
38+
}));
39+
}));
40+
}
41+
42+
for (const sync of [false, true]) {
43+
{
44+
// A plain Uint8Array (not a Buffer instance) must be accepted, and
45+
// written byte-for-byte.
46+
const view = new Uint8Array([0x68, 0x69, 0x0a]); // "hi\n"
47+
writeAndVerify(sync, view, Buffer.from(view));
48+
}
49+
50+
{
51+
// A DataView must be accepted.
52+
const ab = new ArrayBuffer(4);
53+
new DataView(ab).setUint32(0, 0x61626364); // "abcd"
54+
const view = new DataView(ab);
55+
writeAndVerify(sync, view, Buffer.from(ab));
56+
}
57+
58+
{
59+
// Float64Array: each element is 8 bytes, so `.length` (element count)
60+
// must not be confused with `.byteLength` (actual byte count). If the
61+
// implementation used `.length` when accumulating/merging, the output
62+
// would be truncated to a fraction of the real byte size.
63+
const view = new Float64Array([1.5, -2.25, 3]);
64+
writeAndVerify(sync, view, Buffer.from(view.buffer, view.byteOffset, view.byteLength));
65+
}
66+
67+
{
68+
// A view with a non-zero byteOffset over a shared, larger ArrayBuffer
69+
// must only write the bytes it covers, not the whole backing buffer.
70+
const ab = new ArrayBuffer(8);
71+
const full = new Uint8Array(ab);
72+
full.set([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22]);
73+
const view = new Uint8Array(ab, 2, 3); // [0xcc, 0xdd, 0xee]
74+
writeAndVerify(sync, view, Buffer.from([0xcc, 0xdd, 0xee]));
75+
}
76+
77+
{
78+
// Non-ArrayBufferView input must still be rejected in 'buffer' mode.
79+
const dest = getTempFile();
80+
const stream = new Utf8Stream({ dest, sync, contentMode: 'buffer' });
81+
82+
stream.on('ready', common.mustCall(() => {
83+
assert.throws(() => {
84+
stream.write('not a buffer');
85+
}, {
86+
code: 'ERR_INVALID_ARG_TYPE',
87+
name: 'TypeError',
88+
});
89+
assert.throws(() => {
90+
stream.write([1, 2, 3]);
91+
}, {
92+
code: 'ERR_INVALID_ARG_TYPE',
93+
name: 'TypeError',
94+
});
95+
stream.end();
96+
}));
97+
}
98+
}

0 commit comments

Comments
 (0)