Skip to content

Commit 7261276

Browse files
watildeaduh95
authored andcommitted
stream: fix Utf8Stream stall after full write of multi-byte data
Signed-off-by: Daijiro Wachi <daijiro.wachi@gmail.com> PR-URL: #63964 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0147ed7 commit 7261276

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -877,11 +877,15 @@ class Utf8Stream extends EventEmitter {
877877
functionreleaseWritingBuf(writingBuf,len,n){
878878
if(typeofwritingBuf==='string'){
879879
constbyteLength=Buffer.byteLength(writingBuf);
880-
if(byteLength!==n){
881-
// Since fs.write returns the number of bytes written, we need to find
882-
// how many complete characters fit within those n bytes.
883-
// If a partial write splits a multi-byte UTF-8 character, we must back up
884-
// to the start of that character to avoid data corruption.
880+
// `fs.write` returns the number of bytes written, but `len` is tracked in
881+
// characters and `writingBuf` is sliced by character index below, so `n`
882+
// must be converted from bytes to characters in both cases.
883+
if(byteLength===n){
884+
// The whole string was written: advance past every character.
885+
n=writingBuf.length;
886+
}else{
887+
// A partial write may split a multi-byte UTF-8 character, so we must back
888+
// up to the start of that character to avoid data corruption.
885889
constbuf=Buffer.from(writingBuf);
886890
// Back up from position n to find a valid UTF-8 character boundary.
887891
// UTF-8 continuation bytes have the pattern 10xxxxxx (0x80-0xBF).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
// Regression test: after a multi-byte UTF-8 chunk is *fully* written, the
4+
// stream must keep flushing the remaining buffered chunks instead of stalling.
5+
//
6+
// `releaseWritingBuf()` tracks the buffered length in characters, but on a full
7+
// write it used to subtract the number of *bytes* reported by fs.write instead
8+
// of the number of *characters*. For multi-byte data this drove the internal
9+
// length to zero, so the stream emitted 'drain' and went idle while queued
10+
// chunks were left unwritten.
11+
12+
constcommon=require('../common');
13+
constassert=require('node:assert');
14+
const{ Utf8Stream }=require('node:fs');
15+
16+
// "€" is a single JS character that encodes to three UTF-8 bytes, so the byte
17+
// count and character count differ - which is exactly what triggered the bug.
18+
constCHAR='€';
19+
constCOUNT=3;
20+
21+
constchunks=[];
22+
constfsOverride={
23+
// Always report a full (successful) write.
24+
write: common.mustCallAtLeast((fd,data,enc,cb)=>{
25+
chunks.push(data);
26+
process.nextTick(cb,null,Buffer.byteLength(data));
27+
},COUNT),
28+
writeSync(){thrownewError('writeSync should not be used in async mode');},
29+
fsync(fd,cb){cb();},
30+
fsyncSync(){},
31+
close(fd,cb){cb();},
32+
open(path,flags,mode,cb){cb(null,42);},
33+
mkdir(path,opts,cb){cb();},
34+
mkdirSync(){},
35+
};
36+
37+
conststream=newUtf8Stream({
38+
fd: 42,
39+
sync: false,
40+
minLength: 0,
41+
// Force each character into its own buffered chunk so that, while the first
42+
// write is in flight, the remaining characters stay queued.
43+
maxWrite: 1,
44+
fs: fsOverride,
45+
});
46+
47+
stream.on('ready',common.mustCall(()=>{
48+
for(leti=0;i<COUNT;i++){
49+
stream.write(CHAR);
50+
}
51+
52+
// Without calling end(): the stream must flush everything on its own.
53+
setTimeout(common.mustCall(()=>{
54+
assert.strictEqual(chunks.length,COUNT,
55+
`expected ${COUNT} writes, got ${chunks.length}`);
56+
assert.strictEqual(chunks.join(''),CHAR.repeat(COUNT));
57+
stream.destroy();
58+
}),common.platformTimeout(100));
59+
}));

0 commit comments

Comments
 (0)