From 6330e26eb80d0bb23bb7650a65ec34ffcd8e54a7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 16 Oct 2026 00:46:22 -0500 Subject: [PATCH] Open a WriteStream's file when its encoding throws Node's WriteStream constructor calls setDefaultEncoding after the Writable constructor has scheduled _construct, so with encoding: 'buffer' it throws ERR_UNKNOWN_ENCODING but the unreachable stream still opens its file on a later tick, creating it, and with the default 'w' flags truncating it. The runtime threw before scheduling the open; it now constructs the stream first, as Node does. Corpus 5547 ran that check on the file its other blocks write, so the late truncation raced with the fd block's write and Node's own output changed with the timing (it printed an empty file on macOS). The check now uses its own file, and the program prints that the file was created. --- packages/runtime/src/scr_fs_stream.c | 21 +++++++++++++-------- tests/corpus/5547-fs-write-stream.cjs | 5 ++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 2ac88b706..dde56ddb0 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -557,23 +557,19 @@ static ScrStream *scr_fss_new(const ScrDyn *path, const ScrDyn *opts_in, bool re scr_dyn_release(eo); } if (!s) goto fail; + bool unknown_encoding = false; if (!readable) { /* setDefaultEncoding(options.encoding) after the Writable constructor: * 'buffer' is not an encoding there; utf8 is the default */ ScrDyn *enc = scr_fss_get(o, "encoding"); - bool buffer = enc && enc->kind == SCR_DYN_STR && enc->v.str->len == 6 && + unknown_encoding = enc && enc->kind == SCR_DYN_STR && enc->v.str->len == 6 && memcmp(enc->v.str->data, "buffer", 6) == 0; - bool other = enc && enc->kind == SCR_DYN_STR && enc->v.str->len > 0 && !buffer && + bool other = enc && enc->kind == SCR_DYN_STR && enc->v.str->len > 0 && !unknown_encoding && !(enc->v.str->len == 4 && memcmp(enc->v.str->data, "utf8", 4) == 0) && !(enc->v.str->len == 5 && memcmp(enc->v.str->data, "utf-8", 5) == 0); if (enc) scr_dyn_release(enc); - if (buffer) { - static const char m[] = "Unknown encoding: buffer"; - scr_throw_error_msg_code(SCR_ERR_TYPE, m, sizeof m - 1, "ERR_UNKNOWN_ENCODING"); - } else if (other) { + if (other) { scr_fss_unsupported("the WriteStream option 'encoding' is not supported yet (utf8, the default, is the write-side string encoding)"); - } - if (buffer || other) { scr_stream_release(s); s = NULL; goto fail; @@ -592,6 +588,15 @@ static ScrStream *scr_fss_new(const ScrDyn *path, const ScrDyn *opts_in, bool re } scr_stream_set_destroy(s, scr_fss_hook((void *)&scr_fss_destroy), &scr_fss_destroy); scr_stream_construct(s, scr_fss_hook((void *)&scr_fss_construct), &scr_fss_construct); + if (unknown_encoding) { + /* Node's constructor has already scheduled _construct when + * setDefaultEncoding throws, so the unreachable stream still opens + * (and with the default 'w' flags truncates) its file. */ + static const char m[] = "Unknown encoding: buffer"; + scr_throw_error_msg_code(SCR_ERR_TYPE, m, sizeof m - 1, "ERR_UNKNOWN_ENCODING"); + scr_stream_release(s); + s = NULL; + } fail: if (f) scr_fss_free(f); if (owner) owner_ops->release(owner); diff --git a/tests/corpus/5547-fs-write-stream.cjs b/tests/corpus/5547-fs-write-stream.cjs index 6bacc19c1..9198ed221 100644 --- a/tests/corpus/5547-fs-write-stream.cjs +++ b/tests/corpus/5547-fs-write-stream.cjs @@ -114,7 +114,9 @@ function check(label, fn) { check("start negative", () => fs.createWriteStream(file, { start: -1 })); check("path null", () => fs.createWriteStream(null)); check("fd string", () => fs.createWriteStream(file, { fd: "3" })); - check("encoding buffer", () => fs.createWriteStream(file, { encoding: "buffer" })); + // Node's constructor throws here after scheduling the open, so the + // stream still opens (and truncates) its file later: keep it off `file`. + check("encoding buffer", () => fs.createWriteStream(path.join(dir, "unknown-encoding.txt"), { encoding: "buffer" })); check("hwm string", () => fs.createWriteStream(file, { highWaterMark: "4" })); { const log = []; @@ -134,5 +136,6 @@ function check(label, fn) { await new Promise((resolve) => ws.on("close", () => resolve(report("pipe", [], ws.bytesWritten, ws.pending)))); console.log("piped", fs.statSync(file).size); } + console.log("unknown-encoding stream opened its file", fs.existsSync(path.join(dir, "unknown-encoding.txt"))); fs.rmSync(dir, { recursive: true }); })();