From b7bcbc68d8d8662cb9b118664d79955be238527a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 02:58:50 -0500 Subject: [PATCH] Pass a Buffer to a patched stdio write from an encoded data listener A byte write to a replaced process.stdout.write or process.stderr.write was boxed with the 'data' chunk helper, which answers a string while a listener of a socket or HTTP response with setEncoding("utf8") runs. A `process.stdout.write(buffer)` from such a listener reached the replacement as a string. Node passes the Buffer it was given. The stdio hook now boxes a byte write as a Buffer whatever listener is running. --- .../main.ts.json | 7 ++++ packages/runtime/src/scr_events.c | 5 ++- .../main.ts | 32 +++++++++++++++++++ .../patch.mjs | 8 +++++ .../main.ts.json | 1 + 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json create mode 100644 tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts create mode 100644 tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs create mode 100644 tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json new file mode 100644 index 000000000..c26adc078 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs", + "/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index de1d12105..9b918061d 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1534,9 +1534,12 @@ static bool scr_stdio_intercept(int fd, const void *data, size_t length, int fla } ScrDyn *chunk; if (flags & SCR_STDIO_HOOK_BYTES) { + /* Always a Buffer: a write from a 'data' listener of a stream with an + * encoding set is still the bytes it was given. */ ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)length); memcpy(b->data, data, length); - chunk = scr_dyn_new_chunk(b); + b->is_buffer = true; + chunk = scr_dyn_new_bytes(b); scr_bytes_release(b); } else { ScrStr *text = scr_str_new(data, length); diff --git a/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts b/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts new file mode 100644 index 000000000..95e626f36 --- /dev/null +++ b/tests/corpus/stdout-patch-buffer-from-encoded-data/main.ts @@ -0,0 +1,32 @@ +// A Buffer written to a replaced process.stdout.write from a 'data' listener +// of a socket or an HTTP response with setEncoding("utf8") arrives as a +// Buffer; the listener's own chunks are strings. patch.mjs installs the +// replacement and prints what it received at exit. +import * as http from "node:http"; +import * as net from "node:net"; +import "./patch.mjs"; + +const tcp = net.createServer((socket) => socket.end("tcp-data")); +tcp.listen(0, "127.0.0.1", () => { + const port = (tcp.address() as net.AddressInfo).port; + const client = net.connect(port, "127.0.0.1"); + client.setEncoding("utf8"); + client.on("data", (text: string) => { + process.stdout.write(Buffer.from(`socket ${typeof text} ${text}`)); + process.stdout.write(`socket text ${text}`); + }); + client.on("end", () => { + tcp.close(); + const server = http.createServer((req, res) => res.end("http-data")); + server.listen(0, "127.0.0.1", () => { + const httpPort = (server.address() as net.AddressInfo).port; + http.get(`http://127.0.0.1:${httpPort}/`, (res) => { + res.setEncoding("utf8"); + res.on("data", (text: string) => { + process.stdout.write(Buffer.from(`response ${typeof text} ${text}`)); + }); + res.on("end", () => server.close()); + }); + }); + }); +}); diff --git a/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs b/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs new file mode 100644 index 000000000..2432a176b --- /dev/null +++ b/tests/corpus/stdout-patch-buffer-from-encoded-data/patch.mjs @@ -0,0 +1,8 @@ +// Replaces process.stdout.write with a recorder; an exit handler prints what it saw. +const seen = []; +process.stdout.write = function (chunk) { + const kind = typeof chunk === "string" ? "string" : Buffer.isBuffer(chunk) ? "Buffer" : typeof chunk; + seen.push(`${kind}:${String(chunk)}`); + return true; +}; +process.on("exit", () => process.stderr.write(`${seen.join("\n")}\n`)); diff --git a/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json b/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json new file mode 100644 index 000000000..9e947229f --- /dev/null +++ b/tests/harness/corpus-costs/stdout-patch-buffer-from-encoded-data/main.ts.json @@ -0,0 +1 @@ +{ "plain": 5044, "san": 13845 }