runtime: link the wave's new entries only into the programs that use them

Every new runtime entry this wave added landed in a unit that baseline
programs link (scr_lib.c's write/copyFile flags, scr_bytes.c's
string_decoder dyn forms, scr_events.c's prepend registries and
stdin.isRaw, scr_async_dyn.c's uncaught prepend, scr_net.c's server.off).
A changed unit changes its object's build-id and symbol metadata even when
every retained function is byte-identical, so 1517 corpus programs
reported a runtime change for code they never call.

Each new entry now lives where only a program that uses it can reach it:

- writeFile's `flag` option and the fs/promises twins move to
  scr_fs_flags.c (feature fsFlags); copyFile's mode form reuses
  scr_fs_cp.c's reflink walk, so it is that unit's SCR_FS_FLAGS build.
- string_decoder's checked-dynamic chunk forms move to
  scr_bytes_strdec_dyn.c (feature strdecDyn).
- the prepend registrations, stdin.isRaw and server.off are the
  SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
  SCR_NET_SERVER_OFF build of scr_net.c (feature processExtras).

The prepend entry points reuse the append registration and move the new
entry to the front, so no existing function changes and every default
object stays byte-identical. The Windows copyFile arm stays beside the
Win32 path helpers in scr_lib.c, inside its _WIN32 block.

The source build (the -O1 -fsanitize=address lane) compiles these units
from their sources rather than from the prebuilt pack, so RUNTIME_DEFINES
names the same three builds the pack matrix selects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:38:30 -07:00
co-authored by Claude Opus 5.5
parent 26f15896eb
commit 9172fecf19
17 changed files with 380 additions and 166 deletions
@@ -20,6 +20,9 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.fsFlags ? { fsFlags: true } : {}),
...(features.strdecDyn ? { strdecDyn: true } : {}),
...(features.processExtras ? { processExtras: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
@@ -34,6 +34,15 @@ export interface NativeLinkFeatures {
/** The program uses process.stdin.setEncoding, string 'data' listeners
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
stdioExtended?: boolean;
/** writeFile's `flag` disposition or copyFile's mode argument:
* scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c. */
fsFlags?: boolean;
/** string_decoder's checked-dynamic chunk forms: scr_bytes_strdec_dyn.c. */
strdecDyn?: boolean;
/** process.prependListener, stdin.isRaw or server.off: the
* SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
* SCR_NET_SERVER_OFF build of scr_net.c. */
processExtras?: boolean;
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
* SCR_NODE_ISLANDS variant of the event loop unit. */
nodeIsland?: boolean;
@@ -161,6 +161,15 @@ export interface CcOptions {
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
stdioExtended?: boolean;
/** SCR_FS_FLAGS: writeFile's `flag` option and copyFile's mode argument
* (scr_fs_flags.c and that build of scr_fs_cp.c). */
fsFlags?: boolean;
/** string_decoder's checked-dynamic chunk forms: compiles
* scr_bytes_strdec_dyn.c. */
strdecDyn?: boolean;
/** SCR_PROCESS_EXTRAS / SCR_NET_SERVER_OFF: process.prependListener,
* stdin.isRaw and server.off. */
processExtras?: boolean;
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
nodeIsland?: boolean;
@@ -153,6 +153,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
),
unit((u) => u.opts.copying, "scr_copying.c"),
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
// writeFile's `flag` option and the fs/promises twins; the copyFile mode
// argument is the SCR_FS_FLAGS build of scr_fs_cp.c.
unit((u) => u.opts.fsFlags === true, "scr_fs_flags.c"),
// string_decoder's checked-dynamic chunk forms, beside their typed twins.
unit((u) => u.opts.strdecDyn === true, "scr_bytes_strdec_dyn.c"),
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
@@ -412,6 +417,13 @@ const RUNTIME_DEFINES: readonly {
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
// The wave's prepend registrations and stdin.isRaw (scr_events.c,
// scr_async_dyn.c), server.off (scr_net.c) and copyFile's mode form
// (scr_fs_cp.c). The source build compiles these units from source, so the
// defines must name the same builds runtime-pack-matrix.mjs selects.
{ define: "SCR_PROCESS_EXTRAS", when: (opts) => opts.processExtras },
{ define: "SCR_NET_SERVER_OFF", when: (opts) => opts.processExtras },
{ define: "SCR_FS_FLAGS", when: (opts) => opts.fsFlags },
// scr_async.c's native promise reactions, which the web streams unit uses,
// scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes,
// and scr_fetch.c's bodies of its streams.
@@ -65,6 +65,9 @@ export interface EarlyExecutableNativeFeatures {
wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
fsFlags?: boolean;
strdecDyn?: boolean;
processExtras?: boolean;
nodeIsland?: boolean;
moduleNamespace?: boolean;
moduleNamespaceDyn?: boolean;
+3
View File
@@ -700,6 +700,9 @@ async function compileExecutableNative(
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.fsFlags ? { fsFlags: true } : {}),
...(features.strdecDyn ? { strdecDyn: true } : {}),
...(features.processExtras ? { processExtras: true } : {}),
...(features.nodeIsland ? { nodeIsland: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
@@ -1248,6 +1248,33 @@ const RUNTIME_FEATURE_RULES = {
"tls.sockGetCertificate",
],
}),
/** writeFile's `flag` disposition ("w" / "wx") and copyFile's mode
* argument: scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c.
* Programs that never pass either keep both units' default objects. */
fsFlags: rule({
fns: [
"fs.copyFileFlagsSync",
"fs.writeFileFlagsSync",
"fsp.copyFileFlags",
"fsp.writeFileFlags",
],
}),
/** string_decoder's checked-dynamic chunk forms (a stream "data" payload):
* scr_bytes_strdec_dyn.c. */
strdecDyn: rule({ fns: ["strdec.nextDyn", "strdec.writeDyn"] }),
/** The process and stream event registries' extras — prependListener for
* signals, 'exit' and uncaught exceptions, stdin.isRaw, and server.off:
* the SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and
* the SCR_NET_SERVER_OFF build of scr_net.c. */
processExtras: rule({
fns: [
"net.serverOff",
"process.prependExit",
"process.prependSignal",
"process.prependUncaughtException",
"process.stdinIsRaw",
],
}),
zlib: rule({ prefixes: ["zlib."] }),
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
* the program uses Brotli (scr_brotli.c). */
@@ -1289,6 +1316,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
eventsOnceSignal: false,
fetch: false,
fileHandle: false,
fsFlags: false,
fsWatch: false,
http2: false,
http: false,
@@ -1308,6 +1336,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
pki: false,
ed25519: false,
processEvents: false,
processExtras: false,
qs: false,
regex: false,
searchParams: false,
@@ -1316,6 +1345,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
moduleNamespace: false,
moduleNamespaceDyn: false,
stream: false,
strdecDyn: false,
streamDyn: false,
handleProps: false,
childLifecycle: false,
@@ -62,6 +62,12 @@ export const BASE_RUNTIME_SOURCES = [
export const OPTIONAL_RUNTIME_SOURCES = [
["scr_copying.c", "copying"],
["scr_file_handle.c", "fileHandle"],
// writeFile's `flag` disposition and the fs/promises twins; the copyFile
// mode form is the SCR_FS_FLAGS build of scr_fs_cp.c.
["scr_fs_flags.c", "fsFlags"],
// string_decoder's checked-dynamic chunk forms, apart from scr_bytes.c's
// typed twins so a typed program keeps that object byte for byte.
["scr_bytes_strdec_dyn.c", "strdecDyn"],
["scr_regex.c", "regex"],
["scr_glob.c", "regex"],
["scr_idna.c", "regex"],
@@ -278,6 +284,37 @@ export function variantsFor(source) {
when: { warningReplace: true },
defines: ["SCR_WARNING_REPLACE"],
});
// The process-prepend registries (processExtras) add
// scr_process_prepend_uncaught_exception, in a build of its own so the
// default object stays put for every program.
variants.push({
id: "process-extras",
when: { processExtras: true },
defines: ["SCR_PROCESS_EXTRAS"],
});
variants.push({
id: "warning-replace-process-extras",
when: { warningReplace: true, processExtras: true },
defines: ["SCR_WARNING_REPLACE", "SCR_PROCESS_EXTRAS"],
});
}
// copyFileSync's mode argument reuses scr_fs_cp.c's reflink walk, so the
// entry lives in a build of this unit rather than a unit of its own.
if (source === "scr_fs_cp.c") {
variants.push({
id: "flags",
when: { fsFlags: true },
defines: ["SCR_FS_FLAGS"],
});
}
// server.off / removeListener: the listener-removal entry, in a build of
// its own so a program that never removes a listener keeps the default.
if (source === "scr_net.c") {
variants.push({
id: "server-off",
when: { processExtras: true },
defines: ["SCR_NET_SERVER_OFF"],
});
}
// Programs with web streams link the promise unit with native promise
// reactions, the builtin-class unit whose instanceof knows their classes
@@ -331,6 +368,29 @@ export function variantsFor(source) {
when: { stdioExtended: true, nodeIsland: true },
defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
});
// processExtras adds the prepend registrations and stdin's raw-mode
// flag; the combinations above then need their own builds, since the
// variant with the most conditions is the one selected.
variants.push({
id: "process-extras",
when: { processExtras: true },
defines: ["SCR_PROCESS_EXTRAS"],
});
variants.push({
id: "process-extras-stdio-extended",
when: { processExtras: true, stdioExtended: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED"],
});
variants.push({
id: "process-extras-node-island",
when: { processExtras: true, nodeIsland: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_NODE_ISLANDS"],
});
variants.push({
id: "process-extras-stdio-extended-node-island",
when: { processExtras: true, stdioExtended: true, nodeIsland: true },
defines: ["SCR_PROCESS_EXTRAS", "SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"],
});
}
// Programs with Node islands link the event loop with the Node island slot;
// every other program keeps the default object, byte for byte.
-10
View File
@@ -3234,11 +3234,6 @@ ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
scr_fs_write_file_flags(path, data, mode, exclusive);
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_mkdir(ScrStr *path) {
scr_fs_mkdir(path);
return scr_promise_settled_void();
@@ -3299,11 +3294,6 @@ ScrPromise *scr_fsp_copyfile(ScrStr *src, ScrStr *dest) {
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
scr_fs_copyfile_flags(src, dest, mode);
return scr_promise_settled_void();
}
/* readFile(path, encoding | { encoding }): the encoding is checked first,
* so an unknown name rejects before the read. */
ScrPromise *scr_fsp_read_file_encoded(ScrStr *path, ScrStr *encoding) {
+17 -32
View File
@@ -341,11 +341,7 @@ static bool scr_rej_check_listener(ScrDyn *fn) {
return false;
}
/* The registration body: `prepend` puts the listener in FRONT of the
* registry's existing entries (process.prependListener — Node's
* EventEmitter order), the plain form appends. */
static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once,
bool prepend) {
static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) {
if (*n == *cap) {
*cap = *cap ? *cap * 2 : 4;
*list = realloc(*list, *cap * sizeof **list);
@@ -354,18 +350,7 @@ static void scr_rej_push_at(ScrRejListener **list, size_t *n, size_t *cap, ScrDy
ScrRejState *state = calloc(1, sizeof *state);
if (!state) scr_ad_oom();
state->refs = 1;
ScrRejListener entry = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state};
if (prepend) {
memmove(*list + 1, *list, *n * sizeof **list);
(*list)[0] = entry;
(*n)++;
} else {
(*list)[(*n)++] = entry;
}
}
static void scr_rej_push(ScrRejListener **list, size_t *n, size_t *cap, ScrDyn *fn, bool once) {
scr_rej_push_at(list, n, cap, fn, once, false);
(*list)[(*n)++] = (ScrRejListener){scr_dyn_retain(fn), once, ++scr_rej_id, state};
}
static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) {
@@ -482,32 +467,32 @@ static void scr_uncaught_teardown(void) {
scr_uncaught_sync_hook();
}
/* Arm the registry's exit-time teardown once, whichever entry point
* registers first. */
static void scr_uncaught_arm(void) {
static SCR_TL bool armed = false;
if (armed) return;
armed = true;
scr_atexit(scr_uncaught_teardown);
}
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
scr_uncaught_arm();
static SCR_TL bool armed = false;
if (!armed) { armed = true; scr_atexit(scr_uncaught_teardown); }
size_t k = monitor ? 1 : 0;
scr_rej_push(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, once);
scr_uncaught_sync_hook();
}
#ifdef SCR_PROCESS_EXTRAS
/* process.prependListener("uncaughtException"[Monitor], fn): the same
* listener in front of the registry's existing entries. */
* listener in FRONT of the registry's existing entries (Node's
* EventEmitter order). The append path above registers it and the new entry
* then moves to the front, so the two share one registration; a refused
* listener (a non-function) left the list alone and nothing moves. */
void scr_process_prepend_uncaught_exception(ScrDyn *fn, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
scr_uncaught_arm();
size_t k = monitor ? 1 : 0;
scr_rej_push_at(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, false, true);
scr_uncaught_sync_hook();
size_t before = scr_uncaught_n[k];
scr_process_on_uncaught_exception(fn, false, monitor);
if (scr_uncaught_n[k] != before + 1) return;
ScrRejListener entry = scr_uncaught_ls[k][scr_uncaught_n[k] - 1];
memmove(scr_uncaught_ls[k] + 1, scr_uncaught_ls[k],
(scr_uncaught_n[k] - 1) * sizeof *scr_uncaught_ls[k]);
scr_uncaught_ls[k][0] = entry;
}
#endif
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
-39
View File
@@ -1509,45 +1509,6 @@ double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk)
return hold;
}
/* The bytes view of a checked-dynamic write chunk, or NULL for a string
* chunk (Node's write returns it unchanged, never touching the decoder).
* Anything that is not a string or an ArrayBufferView throws Node's
* ERR_INVALID_ARG_TYPE for "buf". */
static ScrBytes *scr_strdec_dyn_view(const ScrDyn *chunk, bool *is_string) {
*is_string = chunk->kind == SCR_DYN_STR;
if (*is_string) return NULL;
if (
chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES ||
scr_buffer_storage_is(chunk)) {
return scr_bytes_buffer_source(chunk);
}
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", chunk);
return NULL;
}
/* decoder.write(chunk) where the chunk crossed the checked-dynamic
* boundary (`stream.on("data", (data) => decoder.write(data))`). */
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return scr_str_retain(chunk->v.str);
if (!view) return NULL;
ScrStr *out = scr_strdec_write(enc, pending, view);
scr_bytes_release(view);
return out;
}
/* The decoder state after that write: a string chunk leaves it alone. */
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return pending;
if (!view) return 0;
double hold = scr_strdec_next(enc, pending, view);
scr_bytes_release(view);
return hold;
}
/* decoder.end(): the buffered partial flushes — utf8's truncated sequence
* becomes its replacement chars (the maximal-subpart rule), base64's
* remainder encodes (padded; unpadded for base64url), utf16le's held
@@ -0,0 +1,50 @@
/* string_decoder's checked-dynamic chunk forms.
*
* A stream's "data" payload crosses the checked-dynamic boundary, so
* decoder.write and decoder.end receive it as an ScrDyn: a string passes
* through unchanged without touching the buffered state, an ArrayBufferView
* (Buffer, TypedArray, DataView) decodes through scr_bytes.c's per-encoding
* helpers with the packed-f64 pending state, and anything else throws Node's
* ERR_INVALID_ARG_TYPE for "buf". They live in their own unit because the
* static twins in scr_bytes.c belong only to programs whose chunks are
* already typed; those programs keep that object byte for byte. */
#include "scr_runtime.h"
/* The bytes view of a checked-dynamic write chunk, or NULL for a string
* chunk (Node's write returns it unchanged, never touching the decoder).
* Anything that is not a string or an ArrayBufferView throws Node's
* ERR_INVALID_ARG_TYPE for "buf". */
static ScrBytes *scr_strdec_dyn_view(const ScrDyn *chunk, bool *is_string) {
*is_string = chunk->kind == SCR_DYN_STR;
if (*is_string) return NULL;
if (
chunk->kind == SCR_DYN_TYPED_REF || chunk->kind == SCR_DYN_BYTES ||
scr_buffer_storage_is(chunk)) {
return scr_bytes_buffer_source(chunk);
}
scr_dyn_arg_type_fail("buf", "an instance of Buffer, TypedArray, or DataView", chunk);
return NULL;
}
/* decoder.write(chunk) where the chunk crossed the checked-dynamic
* boundary (`stream.on("data", (data) => decoder.write(data))`). */
ScrStr *scr_strdec_write_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return scr_str_retain(chunk->v.str);
if (!view) return NULL;
ScrStr *out = scr_strdec_write(enc, pending, view);
scr_bytes_release(view);
return out;
}
/* The decoder state after that write: a string chunk leaves it alone. */
double scr_strdec_next_dyn(const ScrStr *enc, double pending, const ScrDyn *chunk) {
bool is_string = false;
ScrBytes *view = scr_strdec_dyn_view(chunk, &is_string);
if (is_string) return pending;
if (!view) return 0;
double hold = scr_strdec_next(enc, pending, view);
scr_bytes_release(view);
return hold;
}
+53 -50
View File
@@ -192,10 +192,7 @@ static bool scr_signal_check(ScrStr *name, ScrDyn *cb) {
return true;
}
/* The registration body both entry points share: `prepend` puts the
* listener in FRONT of the signal's existing ones (process.prependListener
* — Node's EventEmitter order), the plain form appends. */
static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) {
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
if (!scr_signal_check(name, cb)) return;
if (!scr_context_is_main()) return;
#ifdef __wasi__
@@ -245,25 +242,37 @@ static void scr_signal_add(ScrStr *name, ScrDyn *cb, bool once, bool prepend) {
reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls);
if (!reg->ls) scr_events_oom();
}
ScrSigListener entry =
(ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id};
if (prepend) {
memmove(reg->ls + 1, reg->ls, reg->n * sizeof *reg->ls);
reg->ls[0] = entry;
reg->n++;
} else {
reg->ls[reg->n++] = entry;
}
reg->ls[reg->n++] = (ScrSigListener){scr_dyn_retain(cb), scr_str_retain(name), once, ++scr_sig_id};
#endif
}
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
scr_signal_add(name, cb, once, false);
#ifdef SCR_PROCESS_EXTRAS
/* process.prependListener(<signal>, cb): the same listener in FRONT of the
* signal's existing registrations (Node's EventEmitter order). The append
* path above registers it and the new entry then moves to the front, so the
* two share one registration; a refused or inert registration (a throw, a
* non-main context) left the list alone and nothing moves. */
void scr_signal_prepend(ScrStr *name, ScrDyn *cb) {
#ifdef __wasi__
scr_signal_on(name, cb, false); /* traps, exactly as the append form does */
#else
int sig = scr_signal_from_name(name);
if (sig < 0) sig = 0;
ScrSigReg *reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
size_t before = reg ? reg->n : 0;
scr_signal_on(name, cb, false);
if (!reg) {
reg = scr_sig_regs;
while (reg && reg->sig != sig) reg = reg->next;
}
if (!reg || reg->n != before + 1) return;
ScrSigListener entry = reg->ls[reg->n - 1];
memmove(reg->ls + 1, reg->ls, (reg->n - 1) * sizeof *reg->ls);
reg->ls[0] = entry;
#endif
}
/* process.prependListener(<signal>, cb): the same listener in front of the
* signal's existing registrations. */
void scr_signal_prepend(ScrStr *name, ScrDyn *cb) { scr_signal_add(name, cb, false, true); }
#endif
static void scr_sig_reg_drop(ScrSigReg *reg) {
if (reg->sig > 0) {
@@ -1708,6 +1717,31 @@ void scr_process_on_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, doub
scr_exit_n++;
}
#ifdef SCR_PROCESS_EXTRAS
/* process.stdin.isRaw: the raw-mode flag of a TTY stdin (1/0); a
* pipe-backed stdin has no such flag and answers Node's undefined, which
* the emitter's union construction spells -1. */
double scr_process_stdin_is_raw(void) {
if (!scr_context_is_main()) return -1;
if (!scr_process_is_tty(0)) return -1;
return scr_stdio_raw ? 1.0 : 0.0;
}
/* process.prependListener("exit", cb): the same listener in FRONT of the
* registry's existing entries (Node's EventEmitter order). The append path
* above registers it and the new entry then moves to the front, so the two
* share one registration; a registry already swept at exit released the
* closure and left the list empty, and nothing moves. */
void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) {
size_t before = scr_exit_n;
scr_process_on_exit(cb, fn, false);
if (scr_exit_n != before + 1) return;
ScrExitListener entry = scr_exit_ls[scr_exit_n - 1];
memmove(scr_exit_ls + 1, scr_exit_ls, (scr_exit_n - 1) * sizeof *scr_exit_ls);
scr_exit_ls[0] = entry;
}
#endif
#ifdef SCR_NODE_ISLANDS
/* Island code's 'exit' listeners, at their registration position among
* the program's own. */
@@ -1734,37 +1768,6 @@ void scr_process_off_exit_island(uint32_t island) {
}
#endif
/* process.stdin.isRaw: the raw-mode flag of a TTY stdin (1/0); a
* pipe-backed stdin has no such flag and answers Node's undefined, which
* the emitter's union construction spells -1. */
double scr_process_stdin_is_raw(void) {
if (!scr_context_is_main()) return -1;
if (!scr_process_is_tty(0)) return -1;
return scr_stdio_raw ? 1.0 : 0.0;
}
/* process.prependListener("exit", cb): the same listener in front of the
* registry's existing entries. */
void scr_process_prepend_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, double)) {
if (scr_exit_ran) {
scr_closure_release(cb);
return;
}
if (scr_exit_n == scr_exit_cap) {
scr_exit_cap = scr_exit_cap ? scr_exit_cap * 2 : 2;
scr_exit_ls = realloc(scr_exit_ls, scr_exit_cap * sizeof *scr_exit_ls);
if (!scr_exit_ls) scr_events_oom();
}
memmove(scr_exit_ls + 1, scr_exit_ls, scr_exit_n * sizeof *scr_exit_ls);
scr_exit_ls[0].cb = cb;
scr_exit_ls[0].fn = fn;
scr_exit_ls[0].once = false;
#ifdef SCR_NODE_ISLANDS
scr_exit_ls[0].island = 0;
#endif
scr_exit_n++;
}
void scr_process_off_exit(ScrClosure *cb /*borrowed*/) {
for (size_t i = 0; i < scr_exit_n; i++) {
#ifdef SCR_NODE_ISLANDS
+9 -3
View File
@@ -589,14 +589,19 @@ static void cp_copyfile(const ScrStr *src, const ScrStr *dest, int mode, const c
if (e) scr_fs_throw2(e, syscall, src, dest);
}
#if defined(SCR_FS_FLAGS) && !defined(_WIN32)
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
* uv_fs_copyfile with Node's flag bits (the C++ binding's range check and
* message: any value in [0, 7], truncated to an integer — 1.5 is EXCL).
* COPYFILE_EXCL (1) fails EEXIST on an existing destination,
* COPYFILE_FICLONE (2) requests a reflink and falls back to a copy, and
* COPYFILE_FICLONE_FORCE (4) fails where no reflink is possible. This file
* excludes _WIN32, whose arm (CopyFileW and its ENOSYS for FICLONE_FORCE)
* lives in scr_lib.c beside the Win32 fs helpers. */
* COPYFILE_FICLONE_FORCE (4) fails where no reflink is possible.
*
* This is the SCR_FS_FLAGS build of the unit: cp_copyfile is the reflink
* walk, and keeping the entry behind a define leaves every program that
* never passes a mode with the default object, byte for byte. The Windows
* arm (CopyFileW and its ENOSYS for FICLONE_FORCE) sits beside the Win32 fs
* helpers in scr_lib.c. */
void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
if (!(mode >= 0 && mode <= 7)) {
static const char text[] = "mode is out of range: >= 0 && <= 7";
@@ -607,6 +612,7 @@ void scr_fs_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
if (!scr_fs_path_ok(dest, "dest")) return;
cp_copyfile(src, dest, (int)mode, "copyfile");
}
#endif
/* utimes(dest, atime, mtime) in seconds, through the utimesSync path. */
static bool cp_utime(const ScrStr *dest, double atime, double mtime) {
+102
View File
@@ -0,0 +1,102 @@
/* fs.writeFile's `flag` option ("w", "wx") and the fs/promises twins of the
* flag-aware write and copy forms.
*
* The write is scr_lib.c's scr_fs_write_file_mode body with the disposition
* flag added (the same portable open(2) calls, so it builds for every
* target). It lives in this unit rather than beside that function so a
* program that never passes `flag` keeps scr_lib.c's object byte for byte;
* the fs.copyFile mode form rides scr_fs_cp.c's SCR_FS_FLAGS build, whose
* reflink helper it reuses, and that form's Windows arm sits beside the
* Win32 path helpers in scr_lib.c. */
#include "scr_runtime.h"
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <stdio.h>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
/* O_BINARY: Windows-only (CRT text mode would translate \n on fd writes);
* zero everywhere else. scr_lib.c carries the same guard for its writes. */
#ifndef O_BINARY
#define O_BINARY 0
#endif
/* Node's mode validation for the options-record write: an integer in
* open(2)'s mode_t range. Throws ERR_OUT_OF_RANGE and answers false. */
static bool scr_fs_write_mode_ok(double mode) {
char recv[48], msg[176];
scr_num_received(mode, recv);
if (!(isfinite(mode) && trunc(mode) == mode)) {
int len = snprintf(msg, sizeof msg,
"The value of \"mode\" is out of range. It must be an integer. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
if (mode < 0 || mode > 4294967295.0) {
int len = snprintf(msg, sizeof msg,
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
return true;
}
/* The open(2) write behind the flag form: O_TRUNC plus `extra` (only O_EXCL
* today), creation mode applying at CREATION only (umask applying) — an
* existing file keeps its permissions, exactly Node (which never chmods
* here). Same error shapes as the plain form. */
static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int extra) {
if (!scr_fs_path_ok(path, "path")) return;
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
* \n in these byte-exact writes (fopen's "wb" path already does). */
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | extra, mode);
if (fd < 0) {
scr_fs_throw(errno, "open", path);
return;
}
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
size_t at = 0;
while (at < data->len) {
ssize_t wrote = write(fd, bytes + at, data->len - at);
if (wrote < 0) {
if (errno == EINTR) continue;
int e = errno;
close(fd);
scr_str_utf8_end(owned);
scr_fs_throw(e, "write", path);
return;
}
at += (size_t)wrote;
}
scr_str_utf8_end(owned);
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
}
/* writeFileSync(p, data, { mode, flag }): Node's open(2) with the
* disposition flag. `exclusive` is "wx" (O_EXCL: EEXIST when the path
* exists); 0 is "w", the default truncate-or-create. */
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
if (!scr_fs_write_mode_ok(mode)) return;
scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0);
}
/* fs.promises.writeFile(path, data, { mode, flag }) and
* fs.promises.copyFile(src, dest, mode): the settled-promise twins of
* fs.writeFileFlagsSync and fs.copyFileFlagsSync. */
ScrPromise *scr_fsp_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
scr_fs_write_file_flags(path, data, mode, exclusive);
return scr_promise_settled_void();
}
ScrPromise *scr_fsp_copyfile_flags(ScrStr *src, ScrStr *dest, double mode) {
scr_fs_copyfile_flags(src, dest, mode);
return scr_promise_settled_void();
}
+15 -31
View File
@@ -3531,9 +3531,12 @@ void scr_fs_write_file(ScrStr *path, ScrStr *data) {
scr_fs_write_common(path, data, "wb");
}
/* Node's mode validation for the options-record write: an integer in
* open(2)'s mode_t range. Throws ERR_OUT_OF_RANGE and answers false. */
static bool scr_fs_write_mode_ok(double mode) {
/* writeFileSync(p, data, { mode }): the mode is open(2)'s O_CREAT
* argument — it applies at CREATION only (umask applying), and an
* existing file keeps its permissions, exactly Node (which never chmods
* here). Same error shapes as the plain form. */
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
if (!scr_fs_path_ok(path, "path")) return;
char recv[48], msg[176];
scr_num_received(mode, recv);
if (!(isfinite(mode) && trunc(mode) == mode)) {
@@ -3541,27 +3544,18 @@ static bool scr_fs_write_mode_ok(double mode) {
"The value of \"mode\" is out of range. It must be an integer. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
return;
}
if (mode < 0 || mode > 4294967295.0) {
int len = snprintf(msg, sizeof msg,
"The value of \"mode\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
return;
}
return true;
}
/* The open(2) write behind the mode/flag forms: O_TRUNC plus `extra` (only
* O_EXCL today), creation mode applying at CREATION only (umask applying) —
* an existing file keeps its permissions, exactly Node (which never chmods
* here). Same error shapes as the plain form. */
static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int extra) {
if (!scr_fs_path_ok(path, "path")) return;
/* O_BINARY: zero on POSIX; on Windows it keeps the CRT from translating
* \n in these byte-exact writes (fopen's "wb" path already does). */
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | extra, mode);
int fd = open(path->data, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, (mode_t)mode);
if (fd < 0) {
scr_fs_throw(errno, "open", path);
return;
@@ -3585,22 +3579,6 @@ static void scr_fs_write_file_open(ScrStr *path, ScrStr *data, mode_t mode, int
if (close(fd) != 0) scr_fs_throw(errno, "close", path);
}
/* writeFileSync(p, data, { mode }). */
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
if (!scr_fs_path_ok(path, "path")) return;
if (!scr_fs_write_mode_ok(mode)) return;
scr_fs_write_file_open(path, data, (mode_t)mode, 0);
}
/* writeFileSync(p, data, { mode, flag }): Node's open(2) with the
* disposition flag. `exclusive` is "wx" (O_EXCL: EEXIST when the path
* exists); 0 is "w", the default truncate-or-create. */
void scr_fs_write_file_flags(ScrStr *path, ScrStr *data, double mode, double exclusive) {
if (!scr_fs_path_ok(path, "path")) return;
if (!scr_fs_write_mode_ok(mode)) return;
scr_fs_write_file_open(path, data, (mode_t)mode, exclusive != 0 ? O_EXCL : 0);
}
void scr_fs_append_file(ScrStr *path, ScrStr *data) {
scr_fs_write_common(path, data, "ab");
}
@@ -4694,6 +4672,12 @@ static bool scr_fs_win_same_file(const WCHAR *a, const WCHAR *b) {
first.nFileIndexLow == second.nFileIndexLow;
}
/* The Win32 arm of copyFile's mode form (scr_fs_cp.c's SCR_FS_FLAGS build
* has the POSIX one; writeFile's flag form is portable and lives in
* scr_fs_flags.c). It sits here because the wide-path and errno helpers
* above are this file's; _WIN32 excludes it from the POSIX builds, so every
* other platform's scr_lib.c object is unchanged. */
/* copyFileSync(src, dest, mode) and copyFile(src, dest, mode): libuv's
* fs__copyfile in src/win/fs.c. CopyFileW carries only COPYFILE_EXCL (its
* bFailIfExists flag); COPYFILE_FICLONE (2) falls back to a plain copy and
+5 -1
View File
@@ -2438,9 +2438,12 @@ void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool onc
scr_net_ls_add(&s->close_ls, cb, NULL, once);
}
#ifdef SCR_NET_SERVER_OFF
/* server.off(event, cb) / removeListener: the LAST listener standing for
* the same function leaves the event's list — Node's EventEmitter removes
* the most recently added match, so the scan runs back to front. */
* the most recently added match, so the scan runs back to front. The
* SCR_NET_SERVER_OFF build of the unit carries it, so a program that never
* removes a listener keeps the default object byte for byte. */
void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kind) {
ScrNetLs *l = kind == SCR_NET_OFF_ERROR ? &s->err_ls
: kind == SCR_NET_OFF_CLOSE ? &s->close_ls
@@ -2461,6 +2464,7 @@ void scr_net_server_off(ScrNetServer *s, ScrClosure *cb /*borrowed*/, double kin
return;
}
}
#endif
/* server.on('listening', cb): joins the deferred 'listening' list the
* listen(port, cb) callback rides (the sweep fires it once after a