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.
This commit is contained in:
Chris Tate
2026-10-16 00:46:22 -05:00
parent 524af28870
commit 6330e26eb8
2 changed files with 17 additions and 9 deletions
+13 -8
View File
@@ -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);
+4 -1
View File
@@ -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 });
})();