mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Write run-time-typed chunks to the process streams (W2)
`process.stdout.write(data)`/`process.stderr.write(data)` with a chunk whose type is only known at run time (`any`, or a `string | Uint8Array` union) was refused. Lower the one-argument form to the new `process.stdioWriteDyn` libCall, and factor the existing stdio write method's dispatch into `scr_stdio_write_call` so both share Node's Writable validation: a string or a Uint8Array/Buffer writes its bytes, null throws ERR_STREAM_NULL_VALUES, and any other value throws ERR_INVALID_ARG_TYPE, all before anything is written. `scr_stdio_write_call` keeps its exact statements, so it is only the call sites that change; programs that contain a JS stdio value and link the events unit may differ (their write method's bytes). Corpus: `tests/corpus/process-write-dynamic-chunks.ts` covers strings, Buffer, Uint8Array, a string | Uint8Array union, a JSON field, and the four invalid chunks with their codes and messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bd06b809f1
commit
7be1a990c7
@@ -79679,6 +79679,7 @@
|
||||
"tests/corpus/process-builtin-values.js",
|
||||
"tests/corpus/process-hrtime.js",
|
||||
"tests/corpus/process-named-signals.cjs",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-uncaught-handler-throws.cjs",
|
||||
"tests/corpus/process-uncaught-loop.cjs",
|
||||
"tests/corpus/process-uncaught-module.mjs",
|
||||
@@ -79686,6 +79687,7 @@
|
||||
"tests/corpus/process-uncaught-rejection.cjs",
|
||||
"tests/corpus/process-uncaught-sync.cjs",
|
||||
"tests/corpus/process-versions-storage.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/corpus/stdio-values.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
@@ -82049,6 +82051,8 @@
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -82092,6 +82096,8 @@
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -82135,6 +82141,8 @@
|
||||
"tests/corpus/stdio-write-overrides.mjs",
|
||||
"tests/corpus/stdio-write-errors.mjs",
|
||||
"tests/corpus/stdout-write-patch.ts",
|
||||
"tests/corpus/process-stream-drain.ts",
|
||||
"tests/corpus/process-write-dynamic-chunks.ts",
|
||||
"tests/harness/event-loop.test.ts"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
{ "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] },
|
||||
{ "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "errors", "signatures": ["`stack` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/corpus/stdout-write-patch.ts", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/corpus/stdout-write-patch.ts", "tests/corpus/process-stream-drain.ts", "tests/corpus/process-write-dynamic-chunks.ts", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["Signal events"], "status": "partial", "evidence": ["tests/corpus/1443-signal-handlers.ts", "tests/corpus/process-named-signals.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["Event: `'uncaughtException'`", "Event: `'uncaughtExceptionMonitor'`"], "status": "partial", "evidence": ["tests/corpus/process-uncaught-sync.cjs", "tests/corpus/process-uncaught-loop.cjs", "tests/corpus/process-uncaught-module.mjs", "tests/corpus/process-uncaught-rejection.cjs", "tests/corpus/process-uncaught-monitor.cjs", "tests/corpus/process-uncaught-handler-throws.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["`process.getBuiltinModule(id)`"], "status": "partial", "evidence": ["tests/corpus/process-builtin-values.js", "packages/compiler/test/process-builtin-values.test.ts"] },
|
||||
|
||||
@@ -379,6 +379,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.envPairs": "scr_env_pairs",
|
||||
"process.stdoutWrite": "scr_process_stdout_write",
|
||||
"process.stdio": "scr_process_stdio",
|
||||
"process.stdioWriteDyn": "scr_process_stdio_write_dyn",
|
||||
"process.stderrWrite": "scr_process_stderr_write",
|
||||
"process.isTTY": "scr_process_is_tty",
|
||||
// A promise that never settles (events.once of an event a synchronous
|
||||
|
||||
@@ -102,6 +102,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"json.parse",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.envGet",
|
||||
"process.envPairs",
|
||||
"regex.groupEquals",
|
||||
|
||||
@@ -1357,6 +1357,24 @@ export function lowerProcessMethodCall(
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
data = { kind: "dynCheck", value: data, type: STRING, loc };
|
||||
}
|
||||
// A chunk known only at run time (`any`, or a string | Uint8Array
|
||||
// union): the runtime dispatches on the value — a string or a
|
||||
// Uint8Array/Buffer writes, null throws ERR_STREAM_NULL_VALUES, any
|
||||
// other value throws ERR_INVALID_ARG_TYPE, as Node's Writable.write
|
||||
// does before writing anything. Only the one-argument form.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
(data.type.kind === "dyn" || data.type.kind === "union") &&
|
||||
(data.type.kind === "dyn" || lowerer.dynConvertible(data.type))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "process.stdioWriteDyn",
|
||||
args: [numLit(stream === "stdout" ? 1 : 2, loc), lowerer.coerceToExpected(data, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
if (!isBytes && data.type.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -107,6 +107,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"ffi.argument",
|
||||
"ffi.memoryModule",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWriteBytes",
|
||||
"process.stderrWriteBytes",
|
||||
|
||||
@@ -3668,6 +3668,9 @@ function buildLibFnSignatures() {
|
||||
* never throws. */
|
||||
"process.stdoutWrite": { argTypes: [STRING], result: BOOL },
|
||||
"process.stdio": { argTypes: [F64], result: DYN },
|
||||
/** process.stdout/stderr.write(chunk) with a run-time chunk: Node's
|
||||
* Writable validation, then a synchronous write. */
|
||||
"process.stdioWriteDyn": { argTypes: [F64, DYN], result: BOOL },
|
||||
"process.stderrWrite": { argTypes: [STRING], result: BOOL },
|
||||
// error.new's result and the receiver slots are builtin-error classes —
|
||||
// program-dependent object types, checked in the libCall case.
|
||||
@@ -5341,6 +5344,10 @@ function buildLibFnSignatures() {
|
||||
/** Validated isatty(3) probe. Stream property lowering maps false to
|
||||
* undefined; tty.isatty exposes the boolean result directly. */
|
||||
"process.isTTY": { argTypes: [F64], result: BOOL },
|
||||
/** A promise that never settles (events.once of an event that cannot
|
||||
* occur, e.g. 'drain' on a synchronous process stream). The result is
|
||||
* events.once's own Promise<any[]> type. */
|
||||
"events.pendingForever": { argTypes: [], result: { kind: "promise", inner: arrayOf(DYN) } },
|
||||
// Like process.envGet: the result is the module's interned
|
||||
// `number | undefined` union — checked by arms in the libCall case.
|
||||
/** Terminal width over an fd literal (1/2 — process.stdout/stderr
|
||||
|
||||
@@ -647,6 +647,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.onExit",
|
||||
"process.onSignal",
|
||||
"process.stdio",
|
||||
"process.stdioWriteDyn",
|
||||
"stdin.nextChunk",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/process-write-dynamic-chunks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -68,6 +68,10 @@ ScrDyn *scr_console_new(double out_fd, double err_fd, bool ignore_errors);
|
||||
void scr_console_bind(ScrDyn *console, double which, ScrStream *stream);
|
||||
void scr_console_inst_write(ScrDyn *console, ScrStr *text, bool to_stderr);
|
||||
ScrDyn *scr_process_stdio(double fd);
|
||||
/* process.stdout/stderr.write(chunk) with a run-time chunk: Node's Writable
|
||||
* validation, then a synchronous write. false with an exception pending when
|
||||
* the chunk is invalid. */
|
||||
bool scr_process_stdio_write_dyn(double fd, ScrDyn *chunk);
|
||||
bool scr_dyn_process_stdio_is(const ScrDyn *value);
|
||||
double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path);
|
||||
|
||||
|
||||
@@ -1311,6 +1311,52 @@ static void scr_stdio_resize_listener(ScrStdio *stream, ScrDyn *listener, bool r
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/* stream.write(chunk[, encoding][, callback]) on a process output stream:
|
||||
* Node's Writable.write validation, then a synchronous descriptor write.
|
||||
* Returns +1 (true/false) or NULL with an exception pending. */
|
||||
static ScrDyn *scr_stdio_write_call(ScrStdio *stream, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *arg = argc ? args[0] : scr_dyn_undefined();
|
||||
ScrDyn *result = NULL;
|
||||
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined();
|
||||
if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); }
|
||||
if (callback->kind != SCR_DYN_UNDEF && callback->kind != SCR_DYN_NULL && callback->kind != SCR_DYN_FUNC) {
|
||||
scr_dyn_arg_type_fail("cb", "of type function", callback);
|
||||
return NULL;
|
||||
}
|
||||
if (arg->kind == SCR_DYN_NULL) {
|
||||
const char *m = "May not write null values to stream";
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, m, strlen(m), "ERR_STREAM_NULL_VALUES");
|
||||
return NULL;
|
||||
}
|
||||
if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem != SCR_BYTES_U8)
|
||||
return scr_stdio_refusal("write of a typed array other than Uint8Array");
|
||||
ScrBytes *bytes = NULL;
|
||||
if (arg->kind == SCR_DYN_STR) {
|
||||
if (encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && encoding->kind != SCR_DYN_STR) {
|
||||
return scr_stdio_refusal("write encoding");
|
||||
}
|
||||
ScrStr *enc = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4);
|
||||
bytes = scr_bytes_from_str(arg->v.str, enc);
|
||||
scr_str_release(enc);
|
||||
} else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) {
|
||||
bytes = arg->v.bytes->shared ? scr_bytes_copy(arg->v.bytes) : scr_bytes_retain(arg->v.bytes);
|
||||
} else {
|
||||
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg);
|
||||
return NULL;
|
||||
}
|
||||
if (!bytes) return NULL;
|
||||
int error = scr_stdio_write_raw(stream->fd, bytes->data, bytes->len);
|
||||
scr_bytes_release(bytes);
|
||||
if (error) {
|
||||
scr_stdio_queue_write_error(stream, callback, error);
|
||||
return scr_dyn_new_bool(false);
|
||||
}
|
||||
if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done));
|
||||
return scr_dyn_new_bool(true);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) {
|
||||
int method = (int)scr_box_get_f64(cb->caps[0]);
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
@@ -1328,38 +1374,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
scr_dyn_arr_push(result, columns);
|
||||
scr_dyn_arr_push(result, rows);
|
||||
} else if (method == STDIO_WRITE && stream->fd != 0) {
|
||||
ScrDyn *encoding = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
ScrDyn *callback = argc > 2 ? args[2] : scr_dyn_undefined();
|
||||
if (encoding->kind == SCR_DYN_FUNC) { callback = encoding; encoding = scr_dyn_undefined(); }
|
||||
if (callback->kind != SCR_DYN_UNDEF && callback->kind != SCR_DYN_NULL && callback->kind != SCR_DYN_FUNC) {
|
||||
scr_dyn_arg_type_fail("cb", "of type function", callback);
|
||||
goto done;
|
||||
}
|
||||
ScrBytes *bytes = NULL;
|
||||
if (arg->kind == SCR_DYN_STR) {
|
||||
if (encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && encoding->kind != SCR_DYN_STR) {
|
||||
result = scr_stdio_refusal("write encoding");
|
||||
goto done;
|
||||
}
|
||||
ScrStr *enc = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : scr_str_new("utf8", 4);
|
||||
bytes = scr_bytes_from_str(arg->v.str, enc);
|
||||
scr_str_release(enc);
|
||||
} else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) {
|
||||
bytes = arg->v.bytes->shared ? scr_bytes_copy(arg->v.bytes) : scr_bytes_retain(arg->v.bytes);
|
||||
} else {
|
||||
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg);
|
||||
goto done;
|
||||
}
|
||||
if (!bytes) goto done;
|
||||
int error = scr_stdio_write_raw(stream->fd, bytes->data, bytes->len);
|
||||
scr_bytes_release(bytes);
|
||||
if (error) {
|
||||
scr_stdio_queue_write_error(stream, callback, error);
|
||||
result = scr_dyn_new_bool(false);
|
||||
goto done;
|
||||
}
|
||||
if (callback->kind == SCR_DYN_FUNC) scr_next_tick(scr_stdio_callback(callback, (void *)scr_stdio_write_done));
|
||||
result = scr_dyn_new_bool(true);
|
||||
result = scr_stdio_write_call(stream, args, argc);
|
||||
} else if (method == STDIO_ON || method == STDIO_ONCE || method == STDIO_OFF) {
|
||||
ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
if (listener->kind != SCR_DYN_FUNC) {
|
||||
@@ -1597,6 +1612,19 @@ double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path) {
|
||||
return stream ? stream->fd : 0;
|
||||
}
|
||||
|
||||
/* process.stdout/stderr.write(chunk) with a run-time chunk: the stream
|
||||
* method's own validation and write, without creating the stream value
|
||||
* or installing the write hook. False with an exception pending when the
|
||||
* chunk is invalid. */
|
||||
bool scr_process_stdio_write_dyn(double fd, ScrDyn *chunk) {
|
||||
ScrStdio *stream = &scr_stdio_streams[fd == 2 ? 2 : 1];
|
||||
ScrDyn *result = scr_stdio_write_call(stream, &chunk, 1);
|
||||
if (!result) return false;
|
||||
bool accepted = scr_dyn_truthy(result);
|
||||
scr_dyn_release(result);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
/* ── the process 'exit' event ─────────────────────────────────────────
|
||||
* Listeners run SYNCHRONOUSLY when the process exits: at the end of a
|
||||
* normal run (the unit's atexit, registered by scr_events_install after
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// process.stdout/stderr.write of chunks whose type is only known at run
|
||||
// time: `any` values (a child's 'data' chunk, a JSON field) and
|
||||
// string | Uint8Array unions. Strings and Uint8Array/Buffer values write
|
||||
// their bytes; null throws ERR_STREAM_NULL_VALUES and any other value
|
||||
// throws ERR_INVALID_ARG_TYPE, both TypeErrors raised synchronously before
|
||||
// anything is written.
|
||||
|
||||
function forward(data: any): boolean {
|
||||
return process.stdout.write(data);
|
||||
}
|
||||
|
||||
function pick(useBytes: boolean): string | Uint8Array {
|
||||
return useBytes ? new Uint8Array([98, 121, 116, 101, 115, 10]) : "text\n";
|
||||
}
|
||||
|
||||
console.log("string", forward("any string\n"));
|
||||
console.log("buffer", forward(Buffer.from("any buffer\n")));
|
||||
console.log("u8", forward(new TextEncoder().encode("any u8\n")));
|
||||
process.stdout.write(pick(false));
|
||||
process.stdout.write(pick(true));
|
||||
process.stderr.write(pick(false));
|
||||
const parsed: any = JSON.parse('{"chunk":"from json\\n","n":42}');
|
||||
forward(parsed.chunk);
|
||||
|
||||
for (const bad of [parsed.n, null, undefined, { a: 1 }]) {
|
||||
try {
|
||||
forward(bad);
|
||||
console.log("no throw");
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) {
|
||||
console.log((error as NodeJS.ErrnoException).code, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4567, "san": 86527 }
|
||||
Reference in New Issue
Block a user