| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 0147ed7 commit 7261276
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -877,11 +877,15 @@ class Utf8Stream extends EventEmitter { | |||
| 877 | 877 | function releaseWritingBuf(writingBuf, len, n) { | |
| 878 | 878 | if (typeof writingBuf === 'string') { | |
| 879 | 879 | const byteLength = 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. | ||
| 885 | 889 | const buf = Buffer.from(writingBuf); | |
| 886 | 890 | // Back up from position n to find a valid UTF-8 character boundary. | |
| 887 | 891 | // UTF-8 continuation bytes have the pattern 10xxxxxx (0x80-0xBF). | |
| Original file line number | Diff line number | Diff 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 | + const common = require('../common'); | ||
| 13 | + const assert = 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 | + const CHAR = '€'; | ||
| 19 | + const COUNT = 3; | ||
| 20 | + | ||
| 21 | + const chunks = []; | ||
| 22 | + const fsOverride = { | ||
| 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() { throw new Error('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 | + const stream = new Utf8Stream({ | ||
| 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 (let i = 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 | + })); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments