fs, net, dgram, tls, and stream misuse throws Node's validation ladders

- Per-API validators in the checked-dynamic lane run Node's argument checks in Node's order, then the real operation or a rendered fence thrown after validation
- fs.exists is genuinely async including the synchronous-false wart; mkdtempSync and lchmod are real
- Destructured sub-namespace bindings resolve as their module; function-armed unions cross into the checked-dynamic world
- dgram state errors carry their ERR_SOCKET codes

# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
This commit is contained in:
Chris Tate
2026-07-24 08:07:01 -05:00
26 changed files with 2131 additions and 58 deletions
@@ -3143,6 +3143,17 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
case "net.connect":
E.usesTimers = true; // a connecting/open socket holds the loop open
return finish(`scr_net_connect(${arg(0)}, ${arg(1)}, NULL)`);
case "net.connectAttempt":
// The validated autoSelectFamilyAttemptTimeout form: Node's
// range ladder runs first, the dial follows.
E.usesTimers = true;
return finish(`scr_net_connect_attempt(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "net.connectOptsChk":
// The runtime option-bag ladder (always throws — a validation
// error or the trailing fence; the error.nodeThrow dummy).
return finish(
`(scr_net_connect_opts_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "net.connectCb": {
E.usesTimers = true;
const cb = args[2]!;
@@ -3245,6 +3256,11 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
case "dgram.sendBytes":
E.usesTimers = true;
return finish(`scr_dgram_send_bytes(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)})`);
case "dgram.sendChk":
E.usesTimers = true; // a validated send implicit-binds
return finish(
`scr_dgram_send_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
);
case "dgram.address": {
// The AddressInfo record, built here from runtime parts (the
// frontend pinned the {address, family, port} shape). The
@@ -3956,6 +3972,15 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
return finish(
`scr_tls_create_secure_context((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len)`,
);
case "tls.createSecureContextDyn":
// The runtime option-bag form: Node's typed validations, then
// the pem walk (throws catchably on both ladders).
return finish(`scr_tls_create_secure_context_dyn(${arg(0)})`);
case "tls.caCertsChk":
// Always throws (validation error or the trailing fence).
return finish(
`(scr_tls_ca_certs_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "http.serverOnRequest": {
const cbT = e.args[1]!.type;
if (cbT.kind !== "func") throw new Error("emitter bug: http.serverOnRequest handler not a func");
@@ -4392,12 +4417,59 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
return finish(`scr_buffer_new_string_fail(${arg(0)})`);
case "fs.toUnixTimestamp":
return finish(`scr_fs_to_unix_timestamp(${arg(0)})`);
// The fs argument-validation ladders: the always-throw Chk
// forms (validation error or the trailing fence) take the
// error.nodeThrow dummy pattern; mkdtempSyncChk and the lchmod
// pair answer real results on a validated pass.
case "fs.existsChk":
E.usesTimers = true; // the scheduled answer holds the loop open
return finish(`scr_fs_exists_async(${arg(0)}, ${arg(1)})`);
case "fs.mkdtempChk":
return finish(
`(scr_fs_mkdtemp_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.mkdtempSyncChk":
return finish(`scr_fs_mkdtemp_sync_chk(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "fs.readFileChk":
return finish(
`(scr_fs_read_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.opendirChk":
return finish(
`(scr_fs_opendir_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.watchFileChk":
return finish(
`(scr_fs_watch_file_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.lchmodChk":
return finish(
`(scr_fs_lchmod_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.lchmodSyncChk":
return finish(`scr_fs_lchmod_sync_chk(${arg(0)}, ${arg(1)})`);
case "fsp.lchmodChk":
return finish(`scr_fsp_lchmod_chk(${arg(0)}, ${arg(1)})`);
case "fs.readChk":
return finish(
`(scr_fs_read_chk(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "fs.streamOptsChk":
return finish(
`(scr_fs_stream_opts_chk(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "error.argTypeThrow":
// Always throws with the runtime-rendered Received tail (the
// error.nodeThrow dummy pattern). Borrows all three.
return finish(
`(scr_throw_arg_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
case "error.propTypeThrow":
// The property flavor ("The \"options.x\" property must be
// ...") — same always-throw dummy pattern.
return finish(
`(scr_throw_prop_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
);
// The fs Buffer forms (scr_bytes_io.c): the sync pair throws
// like the utf8 forms (may-throw seed set); the promise form
// rejects instead.
@@ -1481,6 +1481,10 @@ export function jsonWriteHelper(E: CEmitter, t: IrType): string {
d.push(` case ${i}: return scr_dyn_new_num(scr_union_get_f64(v));`);
} else if (arm.kind === "bool") {
d.push(` case ${i}: return scr_dyn_new_bool(scr_union_get_bool(v));`);
} else if (arm.kind === "func") {
// A boxable function arm crosses through the checked-dynamic
// function boundary (the dynFrom func special case, sans name).
d.push(` case ${i}: return ${dynFuncBoxHelper(E, arm)}((ScrClosure *)scr_union_peek(v), NULL);`);
} else {
d.push(` case ${i}: return ${E.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(v));`);
}
+10
View File
@@ -1273,6 +1273,16 @@ export class LlDyn {
B.line(`${x} = call zeroext i1 @scr_union_get_bool(ptr %v)`);
B.line(`${r} = call ptr @scr_dyn_new_bool(i1 ${x})`);
B.terminate(`ret ptr ${r}`);
} else if (arm.kind === "func") {
// A boxable function arm crosses through the checked-dynamic
// function boundary (the dynFrom func special case, sans name).
const pp = B.tmp();
const p = B.tmp();
B.line(`${pp} = getelementptr inbounds %ScrUnion, ptr %v, i64 0, i32 5`);
B.line(`${p} = load ptr, ptr ${pp}`);
const r = B.tmp();
B.line(`${r} = call ptr @${this.dynFuncBoxHelper(arm)}(ptr ${p}, ptr null)`);
B.terminate(`ret ptr ${r}`);
} else {
const pp = B.tmp();
const p = B.tmp();
+55 -1
View File
@@ -322,6 +322,11 @@ const LIB_FN_SYMS: Record<string, string> = {
"bytes.compareChk": "scr_bytes_compare_chk",
"buffer.newStringFail": "scr_buffer_new_string_fail",
"fs.toUnixTimestamp": "scr_fs_to_unix_timestamp",
"fs.existsChk": "scr_fs_exists_async",
"fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk",
"net.connectAttempt": "scr_net_connect_attempt",
"fs.lchmodSyncChk": "scr_fs_lchmod_sync_chk",
"fsp.lchmodChk": "scr_fsp_lchmod_chk",
"fs.readFileSyncBuf": "scr_fs_read_file_bytes",
"fs.readFileSyncBytes": "scr_fs_read_file_bytes",
"fs.writeFileSyncBytes": "scr_fs_write_file_bytes",
@@ -738,7 +743,8 @@ const USES_TIMERS_LIB_FNS = new Set<string>([
"stream.finished", "stream.finishedDyn", "stream.pipeline", "stream.pipelineDyn",
"sp.finished", "sp.pipeline",
"net.listen", "net.listenCb", "net.listenOpts", "net.listenOptsCb",
"net.connect", "net.connectCb", "net.connectLookup",
"net.connect", "net.connectCb", "net.connectLookup", "net.connectAttempt",
"fs.existsChk",
"http.createServer", "http.createServerEmpty",
"http.request", "http.requestCb", "http.requestUrl", "http.requestUrlCb",
"http.requestConn", "http.requestConnCb",
@@ -9488,6 +9494,54 @@ class LlEmitter {
this.emitPendingCheck();
return out;
}
if (e.fn === "error.propTypeThrow") {
// The property flavor of argTypeThrow — same always-throw shape.
const an = this.emitExpr(e.args[0]!);
const ex = this.emitExpr(e.args[1]!);
const got = this.emitExpr(e.args[2]!);
this.declare(`declare void @scr_throw_prop_type(ptr, ptr, ptr)`);
B.line(`call void @scr_throw_prop_type(ptr ${an.name}, ptr ${ex.name}, ptr ${got.name})`);
const ty = this.llType(e.type);
if (ty === "void") {
this.emitPendingCheck();
return { name: "", type: e.type };
}
const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null";
const out = this.own({ name: dummy, type: e.type });
this.emitPendingCheck();
return out;
}
{
// The fs validation-ladder Chk forms that ALWAYS throw (a
// validation error or the trailing compiler-rendered fence): every
// argument is a ptr (dyns + the fence string), and the typed dummy
// is abandoned by the pending check's unwind.
const FS_CHK_THROW_SYMS: Record<string, string | undefined> = {
"fs.mkdtempChk": "scr_fs_mkdtemp_chk",
"fs.readFileChk": "scr_fs_read_file_chk",
"fs.opendirChk": "scr_fs_opendir_chk",
"fs.watchFileChk": "scr_fs_watch_file_chk",
"fs.lchmodChk": "scr_fs_lchmod_chk",
"fs.readChk": "scr_fs_read_chk",
"fs.streamOptsChk": "scr_fs_stream_opts_chk",
"net.connectOptsChk": "scr_net_connect_opts_chk",
};
const sym = FS_CHK_THROW_SYMS[e.fn];
if (sym !== undefined) {
const args = e.args.map((a) => this.emitExpr(a));
this.declare(`declare void @${sym}(${args.map(() => "ptr").join(", ")})`);
B.line(`call void @${sym}(${args.map((a) => `ptr ${a.name}`).join(", ")})`);
const ty = this.llType(e.type);
if (ty === "void") {
this.emitPendingCheck();
return { name: "", type: e.type };
}
const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null";
const out = this.own({ name: dummy, type: e.type });
this.emitPendingCheck();
return out;
}
}
if (e.fn === "error.nodeThrow") {
// The compiler-resolved Node-parity throw (always throws — the
// typed dummy is abandoned by the pending check's unwind).
@@ -5,7 +5,7 @@
import { builtinModules } from "node:module";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { dynUndefinedExpr, nodeThrowExpr, own } from "./lowerer.js";
import { dynUndefinedExpr, ladderFenceExpr, nodeThrowExpr, own } from "./lowerer.js";
import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../program.js";
import {
BuiltinModuleFn,
@@ -396,6 +396,141 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
return null;
}
/** The fs validation-ladder spoke (checked-dynamic lane, JS sources
* only — TypeScript keeps its compile fences): implemented-namespace
* calls whose misuse Node rejects with typed errors lower to fs.*Chk
* libCalls that replicate the validation ladder over DOM values and
* throw Node's exact ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE /
* ERR_OUT_OF_RANGE — the honest tail (the real operation where one
* exists, the compiler-rendered SC2020 fence otherwise) runs only
* after every validation passes, exactly Node's order. Null when this
* is not a claimed member/shape (the table or fence path stands). */
export function lowerFsLadderCall(L: Lowerer, expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
if (bi.module !== "fs" && bi.module !== "fs/promises") return null;
if (!isJsSourceFile(expr.getSourceFile())) return null;
const args = expr.arguments;
if (args.some(ts.isSpreadElement)) return null;
// Every ladder argument crosses as a DOM value; an argument that
// cannot leaves the historical fence in place.
const dynArg = (node: ts.Expression | undefined): IrExpr | null => {
if (!node) return dynUndefinedExpr(loc);
const raw = L.lowerExpr(node);
if (raw.type.kind === "dyn") return raw;
if (raw.kind === "unitLit" || L.dynConvertible(raw.type)) {
return { kind: "dynFrom", value: raw, type: DYN, loc };
}
return null;
};
const dynArgs = (nodes: (ts.Expression | undefined)[]): IrExpr[] | null => {
const out: IrExpr[] = [];
for (const n of nodes) {
const v = dynArg(n);
if (v === null) return null;
out.push(v);
}
return out;
};
const resultT = L.mapTypeOf(L.typeOf(expr)) ?? DYN;
const chk = (fn: IrLibFn, chkArgs: IrExpr[], type: IrType): IrExpr =>
({ kind: "libCall", fn, args: chkArgs, type, loc });
if (bi.module === "fs/promises") {
if (bi.member !== "lchmod" || args.length !== 2) return null;
const a = dynArgs([args[0], args[1]]);
return a && chk("fsp.lchmodChk", a, { kind: "promise", inner: VOID });
}
switch (bi.member) {
case "exists": {
// The REAL deprecated-API shape: the callback validates
// synchronously (Node's one throwing arm), invalid paths answer
// false THROUGH it, and the answer is asynchronous.
// A provably-non-file `new URL('<literal>')` path (the suite's
// https://foo probe): Node answers false through the callback
// synchronously — the DOM has no URL kind, so the path slot
// carries the unvalidatable token instead (construction of a
// parseable literal is effect-free; file: URLs keep the fence —
// they would need the real path conversion).
let pathNode: ts.Expression | undefined = args[0];
let pathExpr: IrExpr | null = null;
if (pathNode && ts.isNewExpression(pathNode) && ts.isIdentifier(pathNode.expression) &&
pathNode.expression.text === "URL" && L.mapTypeOf(L.typeOf(pathNode))?.kind === "url" &&
pathNode.arguments?.length === 1 && ts.isStringLiteral(pathNode.arguments[0]!)) {
let parsed: URL | null = null;
try {
parsed = new URL(pathNode.arguments[0]!.text);
} catch {
parsed = null;
}
if (parsed === null || parsed.protocol === "file:") return null;
pathExpr = dynUndefinedExpr(loc);
pathNode = undefined;
}
pathExpr ??= dynArg(pathNode);
const cbExpr = dynArg(args[1]);
if (pathExpr === null || cbExpr === null) return null;
return chk("fs.existsChk", [pathExpr, cbExpr], DYN);
}
case "mkdtemp": {
// mkdtemp(prefix[, options], callback) — the callback is the
// LAST argument (makeCallback runs first, then the prefix).
const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
return a && chk("fs.mkdtempChk", [...a, ladderFenceExpr(L, "fs.mkdtemp", expr)], resultT);
}
case "mkdtempSync": {
// The table serves the plain string 1-arg form; the ladder takes
// every other shape — prefix/encoding validation, then the REAL
// mkdtemp when the options leave utf8 semantics.
if (args.length === 1 && L.mapTypeOf(L.typeOf(args[0]!))?.kind === "string") return null;
if (args.length > 2) return null;
const a = dynArgs([args[0], args[1]]);
return a && chk("fs.mkdtempSyncChk", [...a, ladderFenceExpr(L, "fs.mkdtempSync with these options", expr)], STRING);
}
case "readFile": {
// readFile(path[, options], callback): callback, assertEncoding,
// path — then the async read fences.
const a = dynArgs([args[0], args.length >= 3 ? args[1] : undefined, args.length >= 2 ? args[args.length - 1] : undefined]);
return a && chk("fs.readFileChk", [...a, ladderFenceExpr(L, "fs.readFile", expr)], resultT);
}
case "opendirSync": {
const a = dynArgs([args[0], args[1]]);
return a && chk("fs.opendirChk", [...a, ladderFenceExpr(L, "fs.opendirSync", expr)], resultT);
}
case "watchFile": {
// watchFile(filename[, options], listener): the path first, the
// listener's function contract second; real watching fences.
const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
return a && chk("fs.watchFileChk", [...a, ladderFenceExpr(L, "fs.watchFile", expr)], resultT);
}
case "lchmod": {
// lchmod(path, mode, callback): callback, path, mode — macOS
// shapes (non-APPLE answers Node's not-a-function TypeError).
const a = dynArgs([args[0], args[1], args[2]]);
return a && chk("fs.lchmodChk", [...a, ladderFenceExpr(L, "fs.lchmod", expr)], resultT);
}
case "lchmodSync": {
if (args.length > 2) return null;
const a = dynArgs([args[0], args[1]]);
return a && chk("fs.lchmodSyncChk", a, DYN);
}
case "read": {
// read(fd, buffer, offset, length, position, callback) — the
// positional form's full ladder; options-object forms keep the
// fence (their misuse arms are not in the target set).
if (args.length < 4) return null;
const a = dynArgs([args[0], args[1], args[2], args[3], args.length >= 6 ? args[4] : undefined]);
return a && chk("fs.readChk", [...a, ladderFenceExpr(L, "fs.read", expr)], resultT);
}
case "createReadStream":
case "createWriteStream": {
const a = dynArgs([args[0], args[1]]);
return a && chk("fs.streamOptsChk", [...a, ladderFenceExpr(L, `fs.${bi.member}`, expr)], resultT);
}
default:
return null;
}
}
export function lowerBuiltinModuleCall(L: Lowerer, expr: ts.CallExpression,
bi: { module: string; member: string },
fn: BuiltinModuleFn,
@@ -3130,6 +3130,11 @@ export function lowerCall(L: Lowerer, expr: ts.CallExpression): IrExpr {
if (streamServed) return streamServed;
const fsTs = L.lowerFsToUnixTimestampCall(expr, bi, loc);
if (fsTs) return fsTs;
// The fs validation-ladder spoke (checked-dynamic lane): misuse
// of implemented-namespace members throws Node's typed errors
// instead of meeting the table fence.
const fsLadder = L.lowerFsLadderCall(expr, bi, loc);
if (fsLadder) return fsLadder;
const builtinFn = builtinModuleFnOf(L, bi.module, bi.member);
if (!builtinFn) {
// Typed by @types/node (the fallback declarations only declare
@@ -9,8 +9,9 @@
* dgram multicast); its members fence with a named hint. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { locOf } from "../program.js";
import { BOOL, DGRAMSOCK_T, F64, IrExpr, IrLibFn, IrType, SrcLoc, STRING, VOID } from "../../ir/nodes.js";
import { ladderFenceExpr } from "./lowerer.js";
import { isJsSourceFile, locOf } from "../program.js";
import { BOOL, canBoxFuncIntoDyn, DGRAMSOCK_T, DYN, F64, funcOf, IrExpr, IrLibFn, IrType, SrcLoc, STRING, UNDEFINED_T, VOID } from "../../ir/nodes.js";
import { DNS_LOOKUP_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "./surfaces.js";
const DGRAM_SURFACE_HINT =
@@ -46,7 +47,13 @@ function lowerCallbackArg(
paramOk: (p: IrType, i: number) => boolean,
paramHint: string,
): { cb: IrExpr; nparams: number } {
const cb = L.lowerExpr(node);
let cb = L.lowerExpr(node);
// A checked-dynamic callback (test/common's mustCall wrapper — a dyn
// value): the zero-parameter slots adapt through the dynCheck function
// boundary, the lower-server listen-callback precedent.
if (cb.type.kind === "dyn" && maxParams === 0) {
cb = { kind: "dynCheck", value: cb, type: funcOf([], VOID), loc: locOf(node) };
}
if (cb.type.kind !== "func" || cb.type.params.length > maxParams) {
L.unsupported(
"SC1090",
@@ -162,6 +169,32 @@ export function lowerDgramDnsModuleCall(L: Lowerer, expr: ts.CallExpression,
sawType = true;
} else if (name === "reuseAddr") {
reuseAddr = L.lowerExprExpecting(prop.initializer, BOOL);
} else if (name === "signal" && isJsSourceFile(expr.getSourceFile())) {
// A provably-non-AbortSignal signal (the invalid-input probes:
// strings, numbers, plain records) throws Node's
// validateAbortSignal ladder; plausible signal values keep the
// fence — abort-driven close has no lowering yet.
const raw = L.lowerExpr(prop.initializer);
const provablyNot = raw.type.kind === "string" || raw.type.kind === "f64" ||
raw.type.kind === "bool" || raw.type.kind === "record" || raw.type.kind === "array";
if (provablyNot && L.dynConvertible(raw.type)) {
return {
kind: "libCall",
fn: "error.propTypeThrow",
args: [
{ kind: "strLit", value: "options.signal", type: STRING, loc },
{ kind: "strLit", value: "an instance of AbortSignal", type: STRING, loc },
{ kind: "dynFrom", value: raw, type: DYN, loc },
],
type: DGRAMSOCK_T,
loc,
};
}
L.noLowering(
`createSocket option 'signal'`,
prop,
"abort-driven close has no lowering yet — type and reuseAddr are the supported options",
);
} else {
L.noLowering(
`createSocket option '${name}'`,
@@ -328,29 +361,70 @@ export function lowerDgramMethodCall(L: Lowerer, call: ts.CallExpression,
}
if (name === "send") {
requireStatementPosition(L, call, "socket.send(...)");
// send(msg, port, address) — one datagram to an explicit destination.
// The connected send and callback forms have no lowering yet.
if (args.length !== 3) {
L.noLowering(
`send with ${args.length} arguments`,
call,
"the supported form is send(msg, port, address) — one string or Buffer datagram",
);
// send(msg, port, address) — one datagram to an explicit destination
// (the static fast path). Every OTHER shape in a JS source rides the
// checked-dynamic ladder (dgram.sendChk): Node's signature shuffle,
// slice bounds, list/type contracts, port/address validation, and
// the connected-state errors — with the compiler-rendered fence as
// the post-validation tail for the callback/list/connected forms.
const staticShape =
args.length === 3 && !args.some(ts.isSpreadElement) &&
(() => {
const dataT = L.mapTypeOf(L.typeOf(args[0]!));
const portT = L.mapTypeOf(L.typeOf(args[1]!));
const hostT = L.mapTypeOf(L.typeOf(args[2]!));
return (dataT?.kind === "string" || (dataT?.kind === "bytes" && dataT.elem === "u8")) &&
portT?.kind === "f64" && hostT?.kind === "string";
})();
if (staticShape) {
const receiver = L.lowerExpr(access.expression);
const data = L.lowerExpr(args[0]!);
const port = L.lowerExprExpecting(args[1]!, F64);
const host = L.lowerExprExpecting(args[2]!, STRING);
const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes";
return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc };
}
const receiver = L.lowerExpr(access.expression);
const data = L.lowerExpr(args[0]!);
const port = L.lowerExprExpecting(args[1]!, F64);
const host = L.lowerExprExpecting(args[2]!, STRING);
if (data.type.kind === "string") {
return { kind: "libCall", fn: "dgram.sendStr", args: [receiver, data, port, host], type: VOID, loc };
}
if (data.type.kind === "bytes" && data.type.elem === "u8") {
return { kind: "libCall", fn: "dgram.sendBytes", args: [receiver, data, port, host], type: VOID, loc };
if (isJsSourceFile(call.getSourceFile()) && args.length <= 5 && !args.some(ts.isSpreadElement)) {
const receiver = L.lowerExpr(access.expression);
const slots: IrExpr[] = [];
let ok = true;
for (let i = 0; i < 5; i++) {
const n = args[i];
if (!n) {
slots.push({
kind: "dynFrom",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: DYN,
loc,
});
continue;
}
const raw = L.lowerExpr(n);
if (raw.type.kind === "dyn") slots.push(raw);
else if (raw.kind === "unitLit" || L.dynConvertible(raw.type) ||
(raw.type.kind === "func" &&
canBoxFuncIntoDyn(raw.type, (id) => L.shapes.get(id), (id) => L.unions.get(id)))) {
slots.push({ kind: "dynFrom", value: raw, type: DYN, loc });
} else {
ok = false;
break;
}
}
if (ok) {
return {
kind: "libCall",
fn: "dgram.sendChk",
args: [receiver, ...slots, ladderFenceExpr(L, `send in this form`, call,
"send(msg, port, address) — one string or Buffer datagram — is the lowered form; callback, list, and connected sends have no lowering yet")],
type: VOID,
loc,
};
}
}
L.noLowering(
`send of '${L.fmt(data.type)}' data`,
args[0] ?? call,
"send takes one string or one Uint8Array/Buffer datagram (narrow unions first)",
`send with ${args.length} arguments`,
call,
"the supported form is send(msg, port, address) — one string or Buffer datagram",
);
}
if (name === "address") {
@@ -10,7 +10,8 @@
* rejection, never silence. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { locOf } from "../program.js";
import { ladderFenceExpr, nodeThrowExpr } from "./lowerer.js";
import { isJsSourceFile, locOf } from "../program.js";
import { arrayOf, BOOL, BYTES_U8, canBoxFuncIntoDyn, canConvertToDyn, DYN, DYN_HANDLE_KINDS, F64, funcOf, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, IrExpr, IrLibFn, IrStmt, IrType, NETSERVER_T, NETSOCKET_T, NULL_T, SECURECTX_T, STRING, UNDEFINED_T, SrcLoc, typeKey, VOID } from "../../ir/nodes.js";
import {
builtinFenceHintOf,
@@ -460,6 +461,28 @@ export function lowerNetModuleCall(L: Lowerer, expr: ts.CallExpression,
return { kind: "libCall", fn: "net.createServer", args: [], type: NETSERVER_T, loc };
}
if (args.length === 1) {
// A provably-non-object, non-function argument (the invalid-input
// probes: createServer('path'), createServer(0)): Node throws
// ERR_INVALID_ARG_TYPE on 'options' before any server exists. DYN
// result — the checked-dynamic lane's concise arrows box it, and
// the throw means it never materializes.
if (isJsSourceFile(expr.getSourceFile())) {
const t = L.mapTypeOf(L.typeOf(args[0]!));
if (t !== null && (t.kind === "string" || t.kind === "f64" || t.kind === "bool")) {
const raw = L.lowerExpr(args[0]!);
return {
kind: "libCall",
fn: "error.argTypeThrow",
args: [
{ kind: "strLit", value: "options", type: STRING, loc },
{ kind: "strLit", value: "of type object", type: STRING, loc },
{ kind: "dynFrom", value: raw, type: DYN, loc },
],
type: NETSERVER_T,
loc,
};
}
}
const { cb } = lowerCallbackArg(
L, args[0]!, "connection handlers", 1,
(p) => p.kind === "netSocket",
@@ -487,6 +510,27 @@ export function lowerNetModuleCall(L: Lowerer, expr: ts.CallExpression,
if (args.length >= 1 && ts.isObjectLiteralExpression(args[0]!)) {
return lowerNetConnectOptions(L, expr, bi.member, loc);
}
// A RUNTIME option bag (a record binding or dyn value — the
// invalid-input probes build theirs with computed keys): the
// checked-dynamic walk validates Node-order and the compiler-rendered
// fence is the post-validation tail.
if (args.length === 1 && isJsSourceFile(expr.getSourceFile())) {
const t = L.mapTypeOf(L.typeOf(args[0]!));
if (t !== null && (t.kind === "dyn" || t.kind === "record")) {
const raw = L.lowerExpr(args[0]!);
if (raw.type.kind === "dyn" || L.dynConvertible(raw.type)) {
const bag: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
return {
kind: "libCall",
fn: "net.connectOptsChk",
args: [bag, ladderFenceExpr(L, `${bi.member} with a runtime options record`, expr,
"pass the options as an object literal — port, host, autoSelectFamily, autoSelectFamilyAttemptTimeout, and lookup are the supported options")],
type: NETSOCKET_T,
loc,
};
}
}
}
if (args.length < 1 || args.length > 3) {
L.noLowering(
`${bi.member} with ${args.length} arguments`,
@@ -565,10 +609,54 @@ function lookupFnShapeOk(L: Lowerer, t: IrType): boolean {
function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: string, loc: SrcLoc): IrExpr {
const args = expr.arguments;
const optsNode = args[0] as ts.ObjectLiteralExpression;
const isJs = isJsSourceFile(expr.getSourceFile());
// The checked-dynamic option-bag route (JS sources): a literal with
// computed keys (the invalid-input probes' spelling) or an objectMode-
// trio member rides WHOLE to the runtime walk — Node's Socket-ctor
// validation order (the trio's ERR_INVALID_ARG_VALUE first, then
// port/host/autoSelectFamily), with the compiler-rendered fence as the
// post-validation tail.
if (isJs) {
const needsBag = optsNode.properties.some((p) =>
(!ts.isPropertyAssignment(p) && !ts.isShorthandPropertyAssignment(p)) ||
(ts.isPropertyAssignment(p) && ts.isComputedPropertyName(p.name)) ||
["objectMode", "readableObjectMode", "writableObjectMode"].includes(
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : "",
),
);
if (needsBag) {
const raw = L.lowerExpr(optsNode);
if (raw.type.kind === "dyn" || L.dynConvertible(raw.type)) {
const bag: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
return {
kind: "libCall",
fn: "net.connectOptsChk",
args: [bag, ladderFenceExpr(L, `${member} with these options`, optsNode,
"port, host, autoSelectFamily, autoSelectFamilyAttemptTimeout, and lookup are the supported options")],
type: NETSOCKET_T,
loc,
};
}
}
}
let port: IrExpr | null = null;
let host: IrExpr | null = null;
let lookup: IrExpr | null = null;
let autoSelect = false;
let attempt: IrExpr | null = null;
let optionThrow: IrExpr | null = null;
const propThrow = (name: string, expected: string, got: IrExpr): IrExpr => ({
kind: "libCall",
fn: "error.propTypeThrow",
args: [
{ kind: "strLit", value: name, type: STRING, loc },
{ kind: "strLit", value: expected, type: STRING, loc },
got.type.kind === "dyn" ? got : { kind: "dynFrom", value: got, type: DYN, loc },
],
type: NETSOCKET_T,
loc,
});
for (const prop of optsNode.properties) {
let initializer: ts.Expression | null;
if (ts.isPropertyAssignment(prop) &&
@@ -596,6 +684,13 @@ function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: str
} else if (key === "host") {
host = lowerVal();
if (host.type.kind !== "string") {
// A provably-non-string host (the invalid-input probes): Node's
// lookupAndConnect throws ERR_INVALID_ARG_TYPE at connect time.
if (isJs && (host.type.kind === "dyn" || L.dynConvertible(host.type))) {
optionThrow ??= propThrow("options.host", "of type string", host);
host = null;
continue;
}
L.noLowering(`a ${member} 'host' option of '${L.fmt(host.type)}' values`, prop, "the host is a string here");
}
} else if (key === "autoSelectFamily") {
@@ -603,6 +698,14 @@ function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: str
// dial below (false/dynamic would mean Node's single-address dial,
// which the lookup form does not implement).
if (initializer === null || initializer.kind !== ts.SyntaxKind.TrueKeyword) {
// A provably-non-boolean value throws Node's validateBoolean
// ladder instead of fencing.
const raw = initializer !== null && isJs ? L.lowerExpr(initializer) : null;
if (raw !== null && raw.type.kind !== "bool" &&
(raw.type.kind === "dyn" || L.dynConvertible(raw.type))) {
optionThrow ??= propThrow("options.autoSelectFamily", "of type boolean", raw);
continue;
}
L.noLowering(
`${member} with a non-literal autoSelectFamily option`,
prop,
@@ -610,6 +713,19 @@ function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: str
);
}
autoSelect = true;
} else if (key === "autoSelectFamilyAttemptTimeout") {
// The attempt budget validates at runtime (Node's validateInt32-
// from-1 ladder) and is then inert — the single dial has nothing
// to time, the autoSelectFamily simplification's sibling.
const raw = lowerVal();
if (!(raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type))) {
L.noLowering(
`a ${member} 'autoSelectFamilyAttemptTimeout' option of '${L.fmt(raw.type)}' values`,
prop,
"the budget is a number here",
);
}
attempt = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
} else if (key === "lookup") {
if (initializer === null) {
L.noLowering(`${member} with a shorthand lookup option`, prop, "spell it out: lookup: theResolver");
@@ -630,6 +746,10 @@ function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: str
);
}
}
// A collected option-contract violation replaces the whole call: Node
// throws it from Socket.connect before dialing (port validation held —
// the port arm above fenced non-number ports already).
if (optionThrow !== null) return optionThrow;
if (port === null) {
L.noLowering(
`${member} options without a port`,
@@ -638,6 +758,16 @@ function lowerNetConnectOptions(L: Lowerer, expr: ts.CallExpression, member: str
);
}
host ??= { kind: "strLit", value: "localhost", type: STRING, loc };
if (attempt !== null) {
if (lookup !== null || args.length !== 1) {
L.noLowering(
`${member} with an autoSelectFamilyAttemptTimeout beside a lookup or connect listener`,
expr,
"the validated-budget form is the bare options call — register listeners separately",
);
}
return { kind: "libCall", fn: "net.connectAttempt", args: [port, host, attempt], type: NETSOCKET_T, loc };
}
if (lookup !== null) {
if (!autoSelect) {
L.noLowering(
@@ -878,6 +1008,33 @@ function lowerNetServerMethodCall(L: Lowerer, call: ts.CallExpression,
}
v6only = v;
}
} else if (key === "signal" && ts.isPropertyAssignment(prop) &&
isJsSourceFile(call.getSourceFile())) {
// A provably-non-AbortSignal signal (the invalid-input probes:
// strings, numbers, plain records) throws Node's
// validateAbortSignal ladder; plausible signal values keep the
// fence — abort-driven close has no lowering yet.
const raw = L.lowerExpr(prop.initializer);
const provablyNot = raw.type.kind === "string" || raw.type.kind === "f64" ||
raw.type.kind === "bool" || raw.type.kind === "record" || raw.type.kind === "array";
if (provablyNot && L.dynConvertible(raw.type)) {
return {
kind: "libCall",
fn: "error.propTypeThrow",
args: [
{ kind: "strLit", value: "options.signal", type: STRING, loc },
{ kind: "strLit", value: "an instance of AbortSignal", type: STRING, loc },
{ kind: "dynFrom", value: raw, type: DYN, loc },
],
type: ts.isExpressionStatement(call.parent) ? VOID : NETSERVER_T,
loc,
};
}
L.noLowering(
`listen option 'signal'`,
prop,
"abort-driven close has no lowering yet — port, host, and ipv6Only are the supported listen options",
);
} else {
L.noLowering(
`listen option '${key}'`,
@@ -1207,6 +1364,32 @@ function lowerNetSocketMethodCall(L: Lowerer, call: ts.CallExpression,
const args = call.arguments;
if (name === "write" || name === "end") {
requireStatementPosition(L, call, `socket.${name}(...)`);
// write(chunk, encoding) — the two-argument encoding form: 'buffer'
// beside a string chunk is Node's stream_base typecheck ("Second
// argument must be a buffer", thrown synchronously on an established
// socket — the invalid-input probes' shape); utf8 spellings are the
// plain write (that IS the encoding written); a Buffer chunk ignores
// the encoding like Node does. Other encodings keep the fence.
if (name === "write" && args.length === 2) {
const encT = L.typeOf(args[1]!);
const chunkT = L.mapTypeOf(L.typeOf(args[0]!));
if (encT.isStringLiteralType() && chunkT !== null) {
if (encT.value === "buffer" && chunkT.kind === "string" &&
isJsSourceFile(call.getSourceFile())) {
L.lowerExpr(args[0]!); // evaluation order (effect-free in practice)
return nodeThrowExpr(1, "ERR_INVALID_ARG_TYPE", "Second argument must be a buffer", VOID, loc);
}
const passthrough =
(chunkT.kind === "string" && (encT.value === "utf8" || encT.value === "utf-8")) ||
(chunkT.kind === "bytes" && chunkT.elem === "u8");
if (passthrough) {
const receiver2 = handleReceiver(L, access.expression, NETSOCKET_T);
const data2 = L.lowerExpr(args[0]!);
const fn: IrLibFn = data2.type.kind === "string" ? "net.sockWrite" : "net.sockWriteBytes";
return { kind: "libCall", fn, args: [receiver2, data2], type: VOID, loc };
}
}
}
const maxArgs = name === "write" ? 1 : 1;
const minArgs = name === "write" ? 1 : 0;
if (args.length < minArgs || args.length > maxArgs) {
@@ -2267,10 +2450,34 @@ function lowerTlsServerOptions(L: Lowerer, node: ts.Expression, what: string): {
* checked-dynamic JS lane's record — passes whole to the runtime walk
* (scr_tls_srv_opts_walk: same member split, fences thrown at runtime,
* the divergence-66 stance); anything else keeps the compile fence. */
/** The option keys whose Node argument contracts the runtime walker
* validates (scr_tls_opts_validate) — a literal carrying any of them
* rides WHOLE to the walker so the typed ladders run in Node's order
* (the static walk would fence them before validating). */
const TLS_VALIDATED_OPTIONS: ReadonlySet<string> = new Set([
"ciphers", "passphrase", "ecdhCurve", "sessionIdContext",
"clientCertEngine", "privateKeyEngine", "privateKeyIdentifier",
"minVersion", "maxVersion", "handshakeTimeout", "keepAliveInitialDelay",
"sessionTimeout", "ticketKeys",
]);
/** True when a literal options bag carries a runtime-validated key (and
* the source is JS — TypeScript keeps its compile fences). */
function tlsLiteralNeedsRuntimeWalk(node: ts.ObjectLiteralExpression): boolean {
if (!isJsSourceFile(node.getSourceFile())) return false;
return node.properties.some((p) =>
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
TLS_VALIDATED_OPTIONS.has(p.name.text),
);
}
function lowerTlsServerOptionsOrDyn(
L: Lowerer, node: ts.Expression, what: string,
): { cert: IrExpr; key: IrExpr; dyn?: undefined } | { dyn: IrExpr } {
if (ts.isObjectLiteralExpression(node)) return lowerTlsServerOptions(L, node, what);
if (ts.isObjectLiteralExpression(node) && !tlsLiteralNeedsRuntimeWalk(node)) {
return lowerTlsServerOptions(L, node, what);
}
const v = L.lowerExpr(node);
if (v.type.kind === "dyn") return { dyn: v };
// A typed options RECORD binding (`const options = { key, cert, ... };
@@ -2424,6 +2631,23 @@ function lowerTlsModuleCall(L: Lowerer, expr: ts.CallExpression,
if (bi.member === "connect") {
return lowerTlsConnectCall(L, expr, loc);
}
if (bi.member === "getCACertificates" && args.length === 1 && !args.some(ts.isSpreadElement) &&
isJsSourceFile(expr.getSourceFile())) {
// The type-argument ladder (validateString + the documented name
// set); the real CA list has no lowering, so a valid name meets the
// compiler-rendered fence after the validation.
const raw = L.lowerExpr(args[0]!);
if (raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type)) {
const t: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
return {
kind: "libCall",
fn: "tls.caCertsChk",
args: [t, ladderFenceExpr(L, "tls.getCACertificates", expr)],
type: L.mapTypeOf(L.typeOf(expr)) ?? DYN,
loc,
};
}
}
if (bi.member === "createSecureContext") {
// createSecureContext({ cert, key }) → the opaque SecureContext handle
// an SNI callback answers with. The minimal honest form: exactly the
@@ -2436,8 +2660,11 @@ function lowerTlsModuleCall(L: Lowerer, expr: ts.CallExpression,
"the supported form is createSecureContext({ cert, key })",
);
}
const { cert, key } = lowerTlsServerOptions(L, args[0]!, "tls.createSecureContext");
return { kind: "libCall", fn: "tls.createSecureContext", args: [cert, key], type: SECURECTX_T, loc };
const opts = lowerTlsServerOptionsOrDyn(L, args[0]!, "tls.createSecureContext");
if (opts.dyn !== undefined) {
return { kind: "libCall", fn: "tls.createSecureContextDyn", args: [opts.dyn], type: SECURECTX_T, loc };
}
return { kind: "libCall", fn: "tls.createSecureContext", args: [opts.cert, opts.key], type: SECURECTX_T, loc };
}
L.noLowering(
`tls.${bi.member}`,
@@ -28,9 +28,9 @@
* its Node signature (the emitter listener rule). */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { dynFallbackType } from "./lowerer.js";
import { dynFallbackType, nodeThrowExpr } from "./lowerer.js";
import type { ClassInfo } from "./lower-classes.js";
import { locOf } from "../program.js";
import { isJsSourceFile, locOf } from "../program.js";
import { newFnCtx, own } from "./lowerer.js";
import { appendImplicitUndefinedReturn } from "./lower-calls.js";
import { bufEncoding } from "./lower-containers.js";
@@ -1002,6 +1002,27 @@ export function lowerStreamStaticCall(L: Lowerer, call: ts.CallExpression,
const member = access.name.text;
const loc = locOf(call);
const cls = info.def.name;
// Readable.toWeb's type-option ladder (JS sources): a provably-invalid
// `type` throws Node's ERR_INVALID_ARG_VALUE before any web stream
// exists; valid shapes keep the fence — the web bridge has no lowering.
if (member === "toWeb" && cls === "%Readable" && call.arguments.length === 2 &&
isJsSourceFile(call.getSourceFile()) && ts.isObjectLiteralExpression(call.arguments[1]!)) {
for (const p of call.arguments[1]!.properties) {
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type") {
const t = L.typeOf(p.initializer);
if (t.isStringLiteralType() && t.value !== "bytes") {
L.lowerExpr(call.arguments[0]!); // evaluation order (the stream argument)
return nodeThrowExpr(
1,
"ERR_INVALID_ARG_VALUE",
`The property 'options.type' must be one of: 'bytes', undefined. Received '${t.value}'`,
L.mapTypeOf(L.typeOf(call)) ?? DYN,
loc,
);
}
}
}
}
if (member !== "from") {
L.noLowering(`${cls.slice(1)}.${member}`, call);
}
@@ -1058,6 +1079,25 @@ function lowerStreamArg(L: Lowerer, node: ts.Expression, what: string): IrExpr {
const v = L.lowerExpr(node);
const info = v.type.kind === "object" ? L.classes.get(v.type.className) : undefined;
if (!streamSidesOf(L, info)) {
// A provably-non-stream value in a JS source (the invalid-input
// probes: finished({}, cb)): Node's isNodeStream gate throws
// ERR_INVALID_ARG_TYPE before any watcher exists.
if (isJsSourceFile(node.getSourceFile()) &&
(v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" ||
v.type.kind === "bool" || v.type.kind === "array") &&
L.dynConvertible(v.type)) {
throw new StreamArgTypeThrow({
kind: "libCall",
fn: "error.argTypeThrow",
args: [
{ kind: "strLit", value: "stream", type: STRING, loc: locOf(node) },
{ kind: "strLit", value: "an instance of ReadableStream, WritableStream, or Stream", type: STRING, loc: locOf(node) },
{ kind: "dynFrom", value: v, type: DYN, loc: locOf(node) },
],
type: VOID,
loc: locOf(node),
});
}
L.noLowering(
`${what} over a '${L.fmt(v.type)}'`,
node,
@@ -1067,6 +1107,15 @@ function lowerStreamArg(L: Lowerer, node: ts.Expression, what: string): IrExpr {
return v;
}
/** The always-throw replacement lowerStreamArg surfaces when the stream
* slot holds a provably-non-stream value — the CALL's lowering catches it
* and answers the throw as the whole call's value (Node throws before
* registering anything, so the other arguments never evaluate their
* effects; listener arguments are effect-free in practice). */
class StreamArgTypeThrow {
constructor(readonly expr: IrExpr) {}
}
/** The finished/pipeline completion callback: an inline function lowers
* through the option-callback machinery (leading `this`, an `Error |
* null` prefix — Node binds the stream and passes the error); any other
@@ -1145,7 +1194,13 @@ export function lowerStreamModuleCall(L: Lowerer, call: ts.CallExpression,
args.length === 2 ? "the options argument has no lowering yet — the one-argument form is supported" : "the supported form is finished(stream)",
);
}
const recv = lowerStreamArg(L, args[0]!, "finished");
let recv: IrExpr;
try {
recv = lowerStreamArg(L, args[0]!, "finished");
} catch (e) {
if (e instanceof StreamArgTypeThrow) return e.expr;
throw e;
}
return { kind: "libCall", fn: "sp.finished", args: [recv], type: { kind: "promise", inner: VOID }, loc };
}
// pipeline(...streams) → the callback pipeline's chaining/destroyer
@@ -1190,7 +1245,13 @@ export function lowerStreamModuleCall(L: Lowerer, call: ts.CallExpression,
args.length === 3 ? "the options argument has no lowering yet — the two-argument form is supported" : "the supported form is finished(stream, callback)",
);
}
const recv = lowerStreamArg(L, args[0]!, "finished");
let recv: IrExpr;
try {
recv = lowerStreamArg(L, args[0]!, "finished");
} catch (e) {
if (e instanceof StreamArgTypeThrow) return e.expr;
throw e;
}
const { cb, dyn } = lowerEosCallback(L, "finished", args[1]!, recv.type);
return {
kind: "libCall",
@@ -92,7 +92,7 @@ import { MixinFnShape, mixinCallClassInfoOf, mixinIntersectionInstanceType } fro
import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassigned, bodyReadsArguments, isThisParameter, paramShape, paramShapes, checkDefaultParamBodyType, completeArgs, wrappedUndefined, undefinedArgFor, requireExactArityValue, bodyReturnType, declaredReturnType, collectSignature, collectSignatureInner, collectGenericSignature, genericFnOf, lowerGenericCall, lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, lowerCall, lowerTimersMemberCall, lowerPromiseMethodCall, lowerFilterNarrowCall, isTopLevelFnSymbol, lowerNestedFunctionDecl, lambdaSignature, lowerLambda, lowerFunction } from "./lower-calls.js";
import { lowerArrayMethodCall, lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn, lowerRegexMethodCall, lowerStringMethodCall } from "./lower-containers.js";
import { lowerStreamModuleCall } from "./lower-stream.js";
import { builtinImportOf, lowerBuiltinModuleCall, lowerFsToUnixTimestampCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, promisifiedExecFileDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerFsConstantsProperty, lowerHttp2ConstantsProperty, http2ConstantBindingOf, http2ConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall } from "./lower-builtins.js";
import { builtinImportOf, lowerBuiltinModuleCall, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, promisifiedExecFileDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerFsConstantsProperty, lowerHttp2ConstantsProperty, http2ConstantBindingOf, http2ConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall } from "./lower-builtins.js";
import { isIslandExpr, islandFuncValueFence, islandRegexpOf, jsvalIn, requireDynamicApi, islandGlobalFnOf, lowerDynamicImportCall, lowerFetchCall, lowerIslandMethodCall, lowerMathProperty, npmPackageOf, npmMemberFence, npmPackageOfSymbol } from "./lower-island.js";
import { lowerHttpHeadersElement, lowerNetModuleCall, lowerServerMethodCall, lowerServerProperty } from "./lower-server.js";
import { lowerDgramDnsModuleCall, lowerDgramMethodCall } from "./lower-dgram.js";
@@ -536,6 +536,27 @@ export function nodeThrowExpr(kind: 0 | 1 | 2, code: string, message: string, ty
};
}
/** The post-validation fence STRING a validation-ladder Chk libCall
* throws after its Node-order checks pass: the same SC2020 text the
* per-statement runtime fence would have thrown (message + "[code at
* file:line]"), rendered eagerly so the runtime can throw it verbatim
* (scr_throw_lowering_fence). The diagnostic joins the runtime-fence
* ledger exactly like a deferred statement fence — nothing silently
* drops off the coverage report. */
export function ladderFenceExpr(L: Lowerer, surface: string, node: ts.Node, hint?: string): IrExpr {
const loc = locOf(node);
const d = noLoweringDiag(surface, loc, hint);
L.runtimeFences.push(d);
const sf = node.getSourceFile();
const pos = ts.getLineAndCharacterOfPosition(sf, loc.start);
return {
kind: "strLit",
value: `${d.message} [${d.code} at ${loc.file}:${pos.line + 1}]`,
type: STRING,
loc,
};
}
/** The checked-dynamic declaration fallback for unmappable binding types
* (see irTypeOf), two gates over one story:
*
@@ -7195,6 +7216,25 @@ export class Lowerer {
const spec = requireSpecOf(decl.initializer);
return spec !== null ? canonicalBuiltinModule(spec) : null;
}
// A DESTRUCTURED sub-namespace binding — `const { promises } =
// fs` / `= require('fs')`: the member is itself a supported module
// ("fs/promises"), so the binding carries that module's namespace
// surface. canonicalBuiltinModule gates the composition (an
// ordinary destructured FUNCTION binding composes to an unknown
// name and answers null — builtinImportOf owns those).
if (ts.isBindingElement(decl) && ts.isObjectBindingPattern(decl.parent) &&
ts.isVariableDeclaration(decl.parent.parent) && decl.parent.parent.initializer !== undefined &&
decl.propertyName === undefined && decl.initializer === undefined) {
const name = decl.name;
if (name === undefined || !ts.isIdentifier(name)) return null;
const init = decl.parent.parent.initializer;
const spec = requireSpecOf(init);
const outer = spec !== null
? canonicalBuiltinModule(spec)
: ts.isIdentifier(init) ? this.builtinNamespaceModuleOf(init) : null;
if (outer !== null) return canonicalBuiltinModule(`${outer}/${name.text}`);
return null;
}
}
// The INLINE CommonJS spelling: `require("cluster").isPrimary` — the
// call expression IS the module namespace (Node evaluates the member
@@ -7293,6 +7333,11 @@ export class Lowerer {
// internal), served by its own spoke before the table fence.
const fsTs = this.lowerFsToUnixTimestampCall(call, bi, locOf(access));
if (fsTs) return fsTs;
// The fs validation-ladder spoke (checked-dynamic lane): misuse of
// implemented-namespace members throws Node's typed errors instead
// of meeting the table fence.
const fsLadder = this.lowerFsLadderCall(call, bi, locOf(access));
if (fsLadder) return fsLadder;
const builtinFn = builtinModuleFnOf(this, bi.module, bi.member);
if (!builtinFn) {
this.noLowering(
@@ -7371,6 +7416,12 @@ export class Lowerer {
return lowerFsToUnixTimestampCall(this, expr, bi, loc);
}
lowerFsLadderCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerFsLadderCall(this, expr, bi, loc);
}
lowerChildArgsArg(node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
return lowerChildArgsArg(this, node, loc);
}
+77 -1
View File
@@ -2043,6 +2043,17 @@ export type IrLibFn =
* exactly Node's split. */
| "net.serverOnSecureConnection"
| "net.connect"
/** connect with a validated autoSelectFamilyAttemptTimeout option (the
* budget runs Node's validateInt32-from-1 ladder and is then inert —
* the single dial has nothing to time). May-throw. */
| "net.connectAttempt"
/** net.connect/createConnection over a RUNTIME option bag (computed
* keys — the invalid-input probes): Node-order validation (the
* objectMode trio's ERR_INVALID_ARG_VALUE, validatePort, host string,
* autoSelectFamily boolean, the attempt budget), then the trailing
* compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow
* polymorphic-result carve-out). May-throw seed. */
| "net.connectOptsChk"
| "net.connectCb"
/** connect({ port, host, autoSelectFamily: true, lookup }) — the
* caller-resolver dial (portless's createLoopbackConnection): args
@@ -2105,6 +2116,12 @@ export type IrLibFn =
| "dgram.connectCb"
| "dgram.sendStr"
| "dgram.sendBytes"
/** The send argument-validation ladder over DOM arguments (Node's
* signature shuffle: slice bounds, list/type contracts, port/address
* validation, connected-state errors) — a fully-validated unconnected
* single-payload send RUNS; callback/list/connected forms meet the
* trailing fence. May-throw. */
| "dgram.sendChk"
| "dgram.address"
| "dgram.close"
| "dgram.closeCb"
@@ -2361,6 +2378,16 @@ export type IrLibFn =
* opaque SecureContext handle (secureCtx kind) for SNI callbacks to
* answer with; cert/key are PEM strings or Buffers like createServer's. */
| "tls.createSecureContext"
/** createSecureContext over a RUNTIME options record (the checked-
* dynamic lane): Node's typed option validations first (the ciphers/
* passphrase/engine/version/timeout/ticketKeys ladders), then the pem
* walk — a validated { cert, key } bag builds the real context.
* May-throw. */
| "tls.createSecureContextDyn"
/** tls.getCACertificates(type): validateString + the documented name
* set, then the trailing compiler-rendered fence — ALWAYS THROWS (the
* error.nodeThrow polymorphic-result carve-out). May-throw seed. */
| "tls.caCertsChk"
| "https.createServer"
| "https.request"
| "https.requestCb"
@@ -2570,11 +2597,35 @@ export type IrLibFn =
* finite numbers coerce (negatives answer now/1000, Node's shape);
* everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */
| "fs.toUnixTimestamp"
/** The fs argument-validation ladders (checked-dynamic lane): each Chk
* replicates its API's Node-order validation over DOM values (Node's
* exact typed errors — ERR_INVALID_ARG_TYPE/VALUE, ERR_OUT_OF_RANGE),
* and a full pass meets the trailing compiler-rendered SC2020 fence
* string — so the ALWAYS-THROW forms take the error.nodeThrow
* polymorphic-result carve-out. mkdtempSyncChk and the lchmod sync/
* promise pair run the REAL operation on a validated pass instead
* (macOS lchmod(2); non-APPLE answers Node's not-a-function /
* ERR_METHOD_NOT_IMPLEMENTED shapes). May-throw seeds, all of them. */
| "fs.existsChk"
| "fs.mkdtempChk"
| "fs.mkdtempSyncChk"
| "fs.readFileChk"
| "fs.opendirChk"
| "fs.watchFileChk"
| "fs.lchmodChk"
| "fs.lchmodSyncChk"
| "fsp.lchmodChk"
| "fs.readChk"
| "fs.streamOptsChk"
/** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME-
* rendered Received tail: args [argname, "of type ..." clause, the
* offending DOM value]. ALWAYS THROWS; polymorphic result (the
* error.nodeThrow pattern). May-throw seed. */
| "error.argTypeThrow"
/** The property flavor of argTypeThrow ("The \"options.x\" property
* must be ..."): the option-bag ladders' provably-invalid arms. ALWAYS
* THROWS; polymorphic result. May-throw seed. */
| "error.propTypeThrow"
/** The checked-dynamic max-listeners ladders: setMaxChk is the
* instance form over a DOM n (non-numbers ERR_INVALID_ARG_TYPE,
* negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining);
@@ -5046,7 +5097,14 @@ export function canConvertToDyn(
}
if (t.kind === "union") {
const def = getUnion(t.unionId);
return !!def && def.arms.every((a) => a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion));
// JSON-safe arms box as before; BOXABLE FUNCTION arms join them (the
// invalid-input probes iterate `[1, null, () => {}, true]` — the
// union's func arm crosses through the checked-dynamic function
// boundary exactly like a bare func dynFrom).
return !!def && def.arms.every((a) =>
a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) ||
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion)),
);
}
return false;
}
@@ -6140,6 +6198,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// divergence-66 stance).
"tls.pemDyn",
"tls.createServerDyn",
"tls.createSecureContextDyn",
"tls.caCertsChk",
"tls.createServerDynCb",
"https.createServerDyn",
"https.createServerDynCb",
@@ -6351,7 +6411,22 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"bytes.compareChk",
"buffer.newStringFail",
"fs.toUnixTimestamp",
"fs.existsChk",
"fs.mkdtempChk",
"fs.mkdtempSyncChk",
"fs.readFileChk",
"fs.opendirChk",
"fs.watchFileChk",
"fs.lchmodChk",
"fs.lchmodSyncChk",
"fsp.lchmodChk",
"fs.readChk",
"fs.streamOptsChk",
"net.connectAttempt",
"net.connectOptsChk",
"net.setAutoSelTimeout",
"error.argTypeThrow",
"error.propTypeThrow",
"emitter.setMaxChk",
"emitter.setDefaultMaxChk",
"fs.readFileSyncBytes",
@@ -6374,6 +6449,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dgram.connectCb",
"dgram.sendStr",
"dgram.sendBytes",
"dgram.sendChk",
"dgram.address",
"dgram.close",
"dgram.closeCb",
+26 -2
View File
@@ -327,6 +327,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T },
"net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T },
"net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID },
"net.connectCb": { argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], result: NETSOCKET_T },
// The lookup's exact func shape is program data (its answer callback's
// union/record types) — checked specially in the libCall case.
@@ -358,6 +360,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dgram.connectCb": { argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], result: VOID },
"dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID },
"dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID },
"dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, STRING], result: VOID },
"dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.closeCb": { argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], result: VOID },
@@ -494,6 +497,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
// createSecureContext({ cert, key }) mints the opaque SNI-answer handle.
"tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T },
"tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T },
"tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID },
"https.createServer": { argTypes: [null, null, null], result: NETSERVER_T },
// http2's allowHTTP1 compatibility server (divergence 57): cert/key
// like tls.createServer; the 'request' handler arrives separately via
@@ -674,7 +679,19 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 },
"buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 },
"fs.toUnixTimestamp": { argTypes: [DYN], result: F64 },
"fs.existsChk": { argTypes: [DYN, DYN], result: DYN },
"fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING },
"fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID },
"fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID },
"fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN },
"fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } },
"fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID },
"fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID },
"error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID },
"emitter.setMaxChk": { argTypes: [null, DYN], result: VOID },
"emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID },
"fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 },
@@ -3885,9 +3902,16 @@ function validateFunction(
// of the undefined global mapped to (never materialized).
break;
}
if (e.fn === "error.nodeThrow" || e.fn === "error.argTypeThrow") {
if (e.fn === "error.nodeThrow" || e.fn === "error.argTypeThrow" || e.fn === "error.propTypeThrow" ||
e.fn === "fs.mkdtempChk" || e.fn === "fs.readFileChk" ||
e.fn === "fs.opendirChk" || e.fn === "fs.watchFileChk" || e.fn === "fs.lchmodChk" ||
e.fn === "fs.readChk" || e.fn === "fs.streamOptsChk" || e.fn === "net.connectOptsChk" ||
e.fn === "tls.caCertsChk") {
// Always throws — the result type is the replaced expression's
// own (never materialized; the global.undefRead pattern).
// own (never materialized; the global.undefRead pattern). The
// fs Chk ladders qualify: every validation failure throws
// Node's typed error, and a full pass throws the trailing
// compiler-rendered fence.
break;
}
if (e.fn === "error.new") {
@@ -4862,9 +4862,9 @@
],
"diags": []
},
"<repo>/tests/corpus/2591-ambient-generic-traps.ts": {
"<repo>/tests/corpus/2595-fs-arg-ladders.cjs": {
"order": [
"<repo>/tests/corpus/2591-ambient-generic-traps.ts"
"<repo>/tests/corpus/2595-fs-arg-ladders.cjs"
],
"diags": []
},
@@ -4874,27 +4874,33 @@
],
"diags": []
},
"<repo>/tests/corpus/2592-ambient-trap-uncaught.ts": {
"order": [
"<repo>/tests/corpus/2592-ambient-trap-uncaught.ts"
],
"diags": []
},
"<repo>/tests/corpus/2579-jsval-object-param-crossing.js": {
"order": [
"<repo>/tests/corpus/2579-jsval-object-param-crossing.js"
],
"diags": []
},
"<repo>/tests/corpus/2593-generic-inert-bindings.ts": {
"<repo>/tests/corpus/2596-net-arg-ladders.cjs": {
"order": [
"<repo>/tests/corpus/2593-generic-inert-bindings.ts"
"<repo>/tests/corpus/2596-net-arg-ladders.cjs"
],
"diags": []
},
"<repo>/tests/corpus/2594-nullish-generic-bindings.ts": {
"<repo>/tests/corpus/2597-dgram-send-ladders.cjs": {
"order": [
"<repo>/tests/corpus/2594-nullish-generic-bindings.ts"
"<repo>/tests/corpus/2597-dgram-send-ladders.cjs"
],
"diags": []
},
"<repo>/tests/corpus/2598-tls-arg-ladders.cjs": {
"order": [
"<repo>/tests/corpus/2598-tls-arg-ladders.cjs"
],
"diags": []
},
"<repo>/tests/corpus/2599-stream-arg-ladders.cjs": {
"order": [
"<repo>/tests/corpus/2599-stream-arg-ladders.cjs"
],
"diags": []
},
@@ -4928,6 +4934,30 @@
],
"diags": []
},
"<repo>/tests/corpus/2591-ambient-generic-traps.ts": {
"order": [
"<repo>/tests/corpus/2591-ambient-generic-traps.ts"
],
"diags": []
},
"<repo>/tests/corpus/2592-ambient-trap-uncaught.ts": {
"order": [
"<repo>/tests/corpus/2592-ambient-trap-uncaught.ts"
],
"diags": []
},
"<repo>/tests/corpus/2593-generic-inert-bindings.ts": {
"order": [
"<repo>/tests/corpus/2593-generic-inert-bindings.ts"
],
"diags": []
},
"<repo>/tests/corpus/2594-nullish-generic-bindings.ts": {
"order": [
"<repo>/tests/corpus/2594-nullish-generic-bindings.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
+7 -6
View File
@@ -927,7 +927,7 @@ ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) {
* the '>= 0 && <= max' render, ERR_OUT_OF_RANGE's Received rules — and
* copy's C++-side ladder; pinned by corpus 1663) ─────────────────────── */
static size_t scr_bytes_received(double v, char out[48]); /* the numeric section below */
size_t scr_num_received(double v, char out[48]); /* the numeric section below */
/* validateOffset(name, 0, max): non-integers are 'an integer' (Number.
* isInteger — ±Infinity render 'an integer' too), the rest '>= 0 && <=
@@ -938,7 +938,7 @@ static size_t scr_bytes_received(double v, char out[48]); /* the numeric section
bool scr_bytes_validate_off(const char *name, double value, double max) {
if (isfinite(value) && floor(value) == value && value >= 0 && (max < 0 || value <= max)) return true;
char recv[48];
scr_bytes_received(value, recv);
scr_num_received(value, recv);
char msg[160];
int mlen;
if (floor(value) != value || !isfinite(value)) {
@@ -1250,8 +1250,9 @@ ScrBytes *scr_bytes_concat(const ScrArr *list) {
/* ERR_OUT_OF_RANGE's "Received" rendering: integers with |v| > 2^32 get
* Node's addNumericalSeparator underscores applied to the plain String()
* form — INCLUDING its quirks on exponent renderings ("1e_+21",
* "1e+_300"); everything else is the shortest-roundtrip number. */
static size_t scr_bytes_received(double v, char out[48]) {
* "1e+_300"); everything else is the shortest-roundtrip number. Shared
* with the fs/net option-ladder validators (scr_num_received). */
size_t scr_num_received(double v, char out[48]) {
char plain[32];
size_t n = scr_f64_to_str(v, plain);
if (!(isfinite(v) && trunc(v) == v && fabs(v) > 4294967296.0)) {
@@ -1281,7 +1282,7 @@ static size_t scr_bytes_received(double v, char out[48]) {
* "byteLength" renders min 1. All catchable RangeErrors. */
static void scr_bytes_bounds_error(double value, double length, const char *type) {
char recv[48];
scr_bytes_received(value, recv);
scr_num_received(value, recv);
char msg[160];
int mlen;
if (floor(value) != value) {
@@ -1322,7 +1323,7 @@ static bool scr_bytes_check_int(const ScrBytes *b, double value, double offset,
double min = sign ? -exp2((double)(8 * width - 1)) : 0;
if (value > max || value < min) {
char recv[48];
scr_bytes_received(value, recv);
scr_num_received(value, recv);
char msg[160];
int mlen;
if (width > 4) {
+400
View File
@@ -12,6 +12,9 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef __APPLE__
#include <sys/stat.h> /* lchmod(2) — the fs.lchmodSync ladder's real tail */
#endif
static void scr_bytes_io_oom(void) {
scr_trap("scriptc: out of memory\n");
@@ -254,3 +257,400 @@ double scr_fs_to_unix_timestamp(const ScrDyn *t) {
scr_dyn_arg_type_fail("time", "an instance of Date or an Time in seconds", t);
return 0;
}
/* ── the fs argument-validation ladders (checked-dynamic lane) ─────────
* Each fs.*Chk libCall replicates its API's Node-order validation over
* DOM values and throws Node's exact typed errors; when every validation
* passes, the honest tail runs — the real operation where one exists
* (mkdtempSync, lchmodSync on macOS), the compiler-rendered SC2020 fence
* otherwise (scr_throw_lowering_fence). All arguments borrowed. */
static bool scr_fs_dyn_absent(const ScrDyn *v) {
return v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL;
}
static bool scr_fs_str_is(const ScrStr *s, const char *lit) {
size_t n = strlen(lit);
return s->len == n && memcmp(s->data, lit, n) == 0;
}
/* Node's maybeCallback/validateFunction over a callback slot. */
static bool scr_fs_cb_chk(const ScrDyn *cb, const char *name) {
if (cb->kind == SCR_DYN_FUNC) return true;
scr_dyn_arg_type_fail(name, "of type function", cb);
return false;
}
/* getValidatedPath: strings and Buffers pass (URL instances never reach
* these ladders — the DOM has no URL kind here, and Node would accept
* only file: URLs anyway). */
static bool scr_fs_path_chk(const ScrDyn *p, const char *name) {
if (p->kind == SCR_DYN_STR || p->kind == SCR_DYN_BYTES) return true;
scr_dyn_arg_type_fail(name, "of type string or an instance of Buffer or URL", p);
return false;
}
/* assertEncoding over an options slot (a bare encoding string or an
* options record's `encoding` member): Node throws ERR_INVALID_ARG_VALUE
* for any truthy value Buffer.isEncoding rejects. */
static bool scr_fs_encoding_chk(const ScrDyn *opts) {
const ScrDyn *enc = opts;
if (opts->kind == SCR_DYN_OBJ) {
enc = scr_dyn_obj_get(opts, "encoding", 8);
if (enc == NULL) return true;
}
if (scr_fs_dyn_absent(enc)) return true;
if (enc->kind == SCR_DYN_STR) {
if (enc->v.str->len == 0) return true; /* falsy: assertEncoding's `encoding &&` gate */
if (scr_bytes_is_encoding(enc->v.str)) return true;
}
if (enc->kind == SCR_DYN_BOOL && !enc->v.b) return true;
if (enc->kind == SCR_DYN_NUM && enc->v.num == 0) return true;
scr_dyn_arg_value_fail("encoding", "is invalid encoding", enc);
return false;
}
/* parseFileMode: integers 0..2^32-1 pass, octal strings parse, and the
* rest throw Node's exact ladder (validateUint32's wording). */
static bool scr_fs_mode_chk(const ScrDyn *m, const char *name) {
if (m->kind == SCR_DYN_STR) {
const ScrStr *s = m->v.str;
bool octal = s->len > 0;
for (size_t i = 0; octal && i < s->len; i++) {
if (s->data[i] < '0' || s->data[i] > '7') octal = false;
}
if (octal) return true;
scr_dyn_arg_value_fail(name, "must be a 32-bit unsigned integer or an octal string", m);
return false;
}
if (m->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail(name, "of type number", m);
return false;
}
double v = m->v.num;
if (!(isfinite(v) && trunc(v) == v)) {
char recv[48], msg[160];
scr_num_received(v, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be an integer. Received %s",
name, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
if (v < 0 || v > 4294967295.0) {
char recv[48], msg[160];
scr_num_received(v, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be >= 0 && <= 4294967295. Received %s",
name, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
return true;
}
/* fs.exists(path, cb) — the REAL deprecated-API shape: the callback
* validates synchronously (Node's one throwing arm), and the answer
* arrives asynchronously through it — string/Buffer paths run the
* existsSync probe at fire time, every other path kind answers `false`
* (Node swallows getValidatedPath failures there). The loop-held timer
* matches Node's I/O-completion timing closely enough for the
* sequential CLI corpus. */
static void scr_fs_exists_fire(ScrClosure *self) {
ScrDyn *path = scr_box_get_ref(self->caps[0]); /* +1 */
ScrDyn *cb = scr_box_get_ref(self->caps[1]); /* +1 */
bool ans = false;
if (path->kind == SCR_DYN_STR) {
ScrStr *p = scr_str_retain(path->v.str);
ans = scr_fs_exists(p);
scr_str_release(p);
} else if (path->kind == SCR_DYN_BYTES) {
ScrStr *p = scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len);
ans = scr_fs_exists(p);
scr_str_release(p);
}
ScrDyn *arg = scr_dyn_new_bool(ans);
ScrDyn *r = scr_dyn_call(cb, &arg, 1, "the fs.exists callback");
scr_dyn_release(arg);
scr_dyn_release(r);
scr_dyn_release(path);
scr_dyn_release(cb);
}
ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb) {
if (!scr_fs_cb_chk(cb, "cb")) return NULL;
if (path->kind != SCR_DYN_STR && path->kind != SCR_DYN_BYTES) {
/* Node's wart, kept exactly: a path getValidatedPath rejects answers
* false through the callback SYNCHRONOUSLY (`return callback(false)`
* in lib/fs.js exists). */
ScrDyn *arg = scr_dyn_new_bool(false);
ScrDyn *r = scr_dyn_call(cb, &arg, 1, "the fs.exists callback");
scr_dyn_release(arg);
scr_dyn_release(r);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(scr_dyn_undefined());
}
ScrClosure *clo = scr_closure_new((void *)scr_fs_exists_fire, 2);
clo->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
scr_box_set_ref(clo->caps[0], scr_dyn_retain((ScrDyn *)path));
clo->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
scr_box_set_ref(clo->caps[1], scr_dyn_retain((ScrDyn *)cb));
scr_set_timeout(clo, 0);
return scr_dyn_retain(scr_dyn_undefined());
}
/* fs.mkdtemp(prefix, options?, cb): callback first (makeCallback), then
* the prefix (getValidatedPath's 'prefix' slot); the async op fences. */
void scr_fs_mkdtemp_chk(const ScrDyn *prefix, const ScrDyn *cb, const ScrStr *fence) {
if (!scr_fs_cb_chk(cb, "cb")) return;
if (!scr_fs_path_chk(prefix, "prefix")) return;
scr_throw_lowering_fence(fence);
}
/* fs.mkdtempSync(prefix, options?): prefix validates (Node's 'prefix'
* slot), the options walk accepts only shapes that leave utf8 semantics
* (absent/empty records, utf8 spellings — invalid encodings throw the
* assertEncoding ladder, other effectful options fence), then the REAL
* mkdtemp runs. +1 result or NULL with the exception pending. */
ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const ScrStr *fence) {
if (!scr_fs_path_chk(prefix, "prefix")) return NULL;
if (!scr_fs_encoding_chk(opts)) return NULL;
bool utf8 = true;
if (opts->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < opts->v.obj.len; i++) {
const ScrDynEntry *e = &opts->v.obj.entries[i];
if (scr_fs_dyn_absent(e->value)) continue;
if (strcmp(e->key, "encoding") == 0) {
const ScrDyn *enc = e->value;
if (!(enc->kind == SCR_DYN_STR &&
(scr_fs_str_is(enc->v.str, "utf8") || scr_fs_str_is(enc->v.str, "utf-8")))) {
utf8 = false;
}
continue;
}
utf8 = false; /* an unmodeled effectful option */
}
} else if (opts->kind == SCR_DYN_STR) {
utf8 = scr_fs_str_is(opts->v.str, "utf8") || scr_fs_str_is(opts->v.str, "utf-8");
} else if (!scr_fs_dyn_absent(opts)) {
utf8 = false;
}
if (!utf8 || prefix->kind != SCR_DYN_STR) {
scr_throw_lowering_fence(fence);
return NULL;
}
ScrStr *p = scr_str_retain(prefix->v.str);
ScrStr *r = scr_fs_mkdtemp(p);
scr_str_release(p);
return r;
}
/* fs.readFile(path, options?, cb): Node's order — the callback
* (maybeCallback), the options walk's assertEncoding, then the path;
* the async read itself fences. */
void scr_fs_read_file_chk(const ScrDyn *path, const ScrDyn *opts, const ScrDyn *cb,
const ScrStr *fence) {
if (!scr_fs_cb_chk(cb, "cb")) return;
if (!scr_fs_encoding_chk(opts)) return;
if (!scr_fs_path_chk(path, "path")) return;
scr_throw_lowering_fence(fence);
}
/* fs.opendirSync(path, options?): getValidatedPath, then getOptions'
* assertEncoding; the Dir machinery fences. */
void scr_fs_opendir_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence) {
if (!scr_fs_path_chk(path, "path")) return;
if (!scr_fs_encoding_chk(opts)) return;
scr_throw_lowering_fence(fence);
}
/* fs.watchFile(path, options?, listener): the path first, the listener's
* function check second (Node's watchFile order); real watching fences. */
void scr_fs_watch_file_chk(const ScrDyn *path, const ScrDyn *listener, const ScrStr *fence) {
if (!scr_fs_path_chk(path, "path")) return;
if (listener->kind != SCR_DYN_FUNC) {
scr_dyn_arg_type_fail("listener", "of type function", listener);
return;
}
scr_throw_lowering_fence(fence);
}
/* fs.lchmod / lchmodSync / fs.promises.lchmod — macOS-only in Node (the
* callback/sync pair is not even exported elsewhere, so non-APPLE builds
* answer the not-a-function TypeError; the promise form rejects
* ERR_METHOD_NOT_IMPLEMENTED, Node's own linux shape). On macOS the
* validation ladder runs in Node's order and the real lchmod(2) applies
* where an operation survives it. */
static bool scr_fs_lchmod_defined(const char *api) {
#ifdef __APPLE__
(void)api;
return true;
#else
char msg[64];
int len = snprintf(msg, sizeof msg, "%s is not a function", api);
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
return false;
#endif
}
void scr_fs_lchmod_chk(const ScrDyn *path, const ScrDyn *mode, const ScrDyn *cb,
const ScrStr *fence) {
if (!scr_fs_lchmod_defined("fs.lchmod")) return;
if (!scr_fs_cb_chk(cb, "cb")) return;
if (!scr_fs_path_chk(path, "path")) return;
if (!scr_fs_mode_chk(mode, "mode")) return;
scr_throw_lowering_fence(fence); /* the async op + callback dispatch */
}
#ifdef __APPLE__
static void scr_fs_lchmod_apply(const ScrDyn *path, const ScrDyn *mode) {
ScrStr *p = path->kind == SCR_DYN_STR ? scr_str_retain(path->v.str)
: scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len);
double m = mode->kind == SCR_DYN_NUM ? mode->v.num : (double)strtol(mode->v.str->data, NULL, 8);
if (lchmod(p->data, (mode_t)m) != 0) scr_fs_throw(errno, "lchmod", p);
scr_str_release(p);
}
#endif
/* Answers the DOM undefined (+1) on success — lchmodSync's JS value, so
* return-position uses lower; NULL with the exception pending. */
ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode) {
if (!scr_fs_lchmod_defined("fs.lchmodSync")) return NULL;
if (!scr_fs_path_chk(path, "path")) return NULL;
if (!scr_fs_mode_chk(mode, "mode")) return NULL;
#ifdef __APPLE__
scr_fs_lchmod_apply(path, mode);
if (scr_exc_pending()) return NULL;
#endif
return scr_dyn_retain(scr_dyn_undefined());
}
ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode) {
#ifndef __APPLE__
(void)path;
(void)mode;
static const char ni[] = "The lchmod() method is not implemented";
scr_throw_error_msg_code(SCR_ERR_ERROR, ni, sizeof ni - 1, "ERR_METHOD_NOT_IMPLEMENTED");
return scr_promise_settled_void();
#else
if (scr_fs_path_chk(path, "path") && scr_fs_mode_chk(mode, "mode")) {
scr_fs_lchmod_apply(path, mode);
}
return scr_promise_settled_void();
#endif
}
/* fs.read(fd, buffer, offset, length, position, cb) — the full argument
* ladder in Node's order (buffer, fd, offset, length, position); the
* async read fences. Bounds follow lib/fs.js read(): offset within the
* buffer, length within buffer - offset. */
static bool scr_fs_int_range_chk(const ScrDyn *v, const char *name, double min, double max,
const char *range) {
if (v->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail(name, "of type number", v);
return false;
}
double n = v->v.num;
char recv[48], msg[192];
if (!(isfinite(n) && trunc(n) == n)) {
scr_num_received(n, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be an integer. Received %s",
name, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
if (n < min || n > max) {
scr_num_received(n, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be %s. Received %s",
name, range, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
return true;
}
void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset,
const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) {
if (buffer->kind != SCR_DYN_BYTES) {
scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer);
return;
}
if (fd->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("fd", "of type number", fd);
return;
}
double buflen = (double)buffer->v.bytes->len;
if (!scr_fs_dyn_absent(offset)) {
/* validateInteger's MAX_SAFE range first, the buffer bound second —
* Node renders each with its own max. */
if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0,
">= 0 && <= 9007199254740991")) {
return;
}
if (offset->v.num > buflen) {
char range[64];
snprintf(range, sizeof range, ">= 0 && <= %.0f", buflen);
if (!scr_fs_int_range_chk(offset, "offset", 0, buflen, range)) return;
}
}
double off = offset->kind == SCR_DYN_NUM ? offset->v.num : 0;
if (!scr_fs_dyn_absent(length)) {
if (length->kind != SCR_DYN_NUM || !(isfinite(length->v.num) && trunc(length->v.num) == length->v.num) ||
length->v.num < 0) {
/* Node renders the bare ">= 0" form here (checkPosition's cousin). */
if (length->kind == SCR_DYN_NUM) {
char recv[48], msg[160];
scr_num_received(length->v.num, recv);
const char *shape = (isfinite(length->v.num) && trunc(length->v.num) == length->v.num)
? "It must be >= 0."
: "It must be an integer.";
int len = snprintf(msg, sizeof msg,
"The value of \"length\" is out of range. %s Received %s", shape, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return;
}
scr_dyn_arg_type_fail("length", "of type number", length);
return;
}
if (length->v.num > buflen - off) {
char recv[48], msg[160];
scr_num_received(length->v.num, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"length\" is out of range. It must be <= %.0f. Received %s",
buflen - off, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return;
}
}
if (!scr_fs_dyn_absent(position)) {
if (position->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("position", "of type bigint or integer", position);
return;
}
if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0,
">= -1 && <= 9007199254740991")) {
return;
}
}
scr_throw_lowering_fence(fence);
}
/* createReadStream/createWriteStream(path, options?): getOptions'
* assertEncoding, the fd member's FileHandle-or-integer contract when
* present, the path contract otherwise; the stream machinery fences. */
void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence) {
if (!scr_fs_encoding_chk(opts)) return;
const ScrDyn *fd = opts->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(opts, "fd", 2) : NULL;
if (fd != NULL && !scr_fs_dyn_absent(fd)) {
if (fd->kind != SCR_DYN_NUM) {
scr_dyn_prop_type_fail("options.fd", "of type number or an instance of FileHandle", fd);
return;
}
if (!scr_fs_int_range_chk(fd, "fd", 0, 2147483647.0, ">= 0 && <= 2147483647")) return;
} else if (!scr_fs_path_chk(path, "path")) {
return;
}
scr_throw_lowering_fence(fence);
}
+176 -1
View File
@@ -64,6 +64,7 @@
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -466,7 +467,18 @@ ScrDgramSocket *scr_dgram_create(bool reuse_addr) {
}
static void scr_dgram_throw(const char *msg) {
scr_throw_error_msg(0 /* Error */, msg, strlen(msg));
/* Node's state errors carry their ERR_SOCKET_* codes; the message IS
* the discriminant (each arm throws exactly one text). */
const char *code =
strcmp(msg, "Already connected") == 0 ? "ERR_SOCKET_DGRAM_IS_CONNECTED"
: strcmp(msg, "Not running") == 0 ? "ERR_SOCKET_DGRAM_NOT_RUNNING"
: strcmp(msg, "Socket is already bound") == 0 ? "ERR_SOCKET_ALREADY_BOUND"
: NULL;
if (code != NULL) {
scr_throw_error_msg_code(0 /* Error */, msg, strlen(msg), code);
} else {
scr_throw_error_msg(0 /* Error */, msg, strlen(msg));
}
}
/* Resolve a numeric host into a sockaddr_in; "" means any (bind's
@@ -1005,3 +1017,166 @@ void scr_dgram_install(void) {
atexit(scr_dgram_cleanup_atexit);
scr_loop_set_dgram(&scr_dgram_pending, &scr_dgram_dispatch, &scr_dgram_pollfd);
}
/* ── the send argument-validation ladder (checked-dynamic lane) ─────────
* Node's Socket.prototype.send signature shuffle and validation order
* over DOM arguments, byte-for-byte: the connected/unconnected split
* decides whether (a1, a2) are an offset/length slice or the port/address
* pair; sliceBuffer validates the buffer's type and bounds
* (ERR_BUFFER_OUT_OF_BOUNDS); list payloads validate per element with the
* LIST as the Received tail; unconnected sends validate the port
* (ERR_SOCKET_BAD_PORT, > 0 and < 65536 with the specific-type tail and
* Node's trailing period) and the address's string contract; a send with
* a port or address on a connected socket answers ERR_SOCKET_DGRAM_IS_
* CONNECTED. A fully-validated unconnected single-payload send RUNS —
* the callback form and the connected sends keep the compiler-rendered
* fence. All dyn arguments borrowed. */
static bool scr_dgram_dyn_truthy(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL: return false;
case SCR_DYN_BOOL: return v->v.b;
case SCR_DYN_NUM: return v->v.num == v->v.num && v->v.num != 0;
case SCR_DYN_STR: return v->v.str->len > 0;
default: return true;
}
}
static uint32_t scr_dgram_to_u32(const ScrDyn *v) {
if (v->kind != SCR_DYN_NUM) return 0; /* >>> 0 over the ladder's shapes */
double t = v->v.num;
if (t != t || isinf(t)) return 0;
t = trunc(t);
t = fmod(t, 4294967296.0);
if (t < 0) t += 4294967296.0;
return (uint32_t)t;
}
static const char SCR_DGRAM_BUF_EXPECTED[] =
"of type string or an instance of Buffer, TypedArray, or DataView";
void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a1,
const ScrDyn *a2, const ScrDyn *a3, const ScrDyn *a4,
const ScrStr *fence) {
const ScrDyn *offset = a1, *length = a2, *port = a3, *address = a4;
const ScrDyn *callback = scr_dyn_undefined();
bool connected = s->connected;
bool sliced = false;
if (!connected) {
if (scr_dgram_dyn_truthy(address) ||
(scr_dgram_dyn_truthy(port) && port->kind != SCR_DYN_FUNC)) {
sliced = true;
} else {
callback = port;
port = offset;
address = length;
}
} else {
if (length->kind == SCR_DYN_NUM) {
sliced = true;
if (port->kind == SCR_DYN_FUNC) callback = port;
} else {
callback = offset;
port = a3;
address = a4;
}
}
size_t slice_off = 0, slice_len = 0;
if (sliced) {
double bytelen;
if (buffer->kind == SCR_DYN_STR) bytelen = (double)buffer->v.str->len;
else if (buffer->kind == SCR_DYN_BYTES) bytelen = scr_bytes_byte_len(buffer->v.bytes);
else {
scr_dyn_arg_type_fail("buffer", SCR_DGRAM_BUF_EXPECTED, buffer);
return;
}
uint32_t off = scr_dgram_to_u32(offset);
uint32_t len = scr_dgram_to_u32(length);
if ((double)off > bytelen) {
static const char msg[] = "\"offset\" is outside of buffer bounds";
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS");
return;
}
if ((double)off + (double)len > bytelen) {
static const char msg[] = "\"length\" is outside of buffer bounds";
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS");
return;
}
slice_off = off;
slice_len = len;
} else if (buffer->kind == SCR_DYN_ARR) {
for (size_t i = 0; i < buffer->v.arr.len; i++) {
const ScrDyn *e = buffer->v.arr.items[i];
if (e->kind != SCR_DYN_STR && e->kind != SCR_DYN_BYTES) {
scr_dyn_arg_type_fail("buffer list arguments", SCR_DGRAM_BUF_EXPECTED, buffer);
return;
}
}
} else if (buffer->kind != SCR_DYN_STR && buffer->kind != SCR_DYN_BYTES) {
scr_dyn_arg_type_fail("buffer", SCR_DGRAM_BUF_EXPECTED, buffer);
return;
}
if (connected) {
if (scr_dgram_dyn_truthy(port) || scr_dgram_dyn_truthy(address)) {
scr_dgram_throw("Already connected");
return;
}
scr_throw_lowering_fence(fence); /* connected sends have no lowering yet */
return;
}
/* validatePort(port, 'Port', false): integers (or numeric strings)
* strictly between 0 and 65536; everything else renders the specific
* type with Node's trailing period. */
double portnum = -1;
{
bool ok = false;
if (port->kind == SCR_DYN_NUM && trunc(port->v.num) == port->v.num &&
port->v.num > 0 && port->v.num < 65536) {
ok = true;
portnum = port->v.num;
} else if (port->kind == SCR_DYN_STR && port->v.str->len > 0) {
ScrStr *ps = scr_str_retain(port->v.str);
double n = scr_string_to_number(ps);
scr_str_release(ps);
if (n == n && trunc(n) == n && n > 0 && n < 65536) {
ok = true;
portnum = n;
}
}
if (!ok) {
char detail[64], msg[160];
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
int len = snprintf(msg, sizeof msg, "Port should be > 0 and < 65536. Received %s.", d);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
return;
}
}
if (address->kind == SCR_DYN_FUNC) {
callback = address;
address = scr_dyn_undefined();
} else if (address->kind != SCR_DYN_UNDEF && address->kind != SCR_DYN_NULL &&
address->kind != SCR_DYN_STR) {
/* Node's gate is null/undefined-only: a falsy 0 still throws. */
scr_dyn_arg_type_fail("address", "of type string", address);
return;
}
if (callback->kind == SCR_DYN_FUNC || buffer->kind == SCR_DYN_ARR) {
/* completion callbacks and list concatenation have no lowering yet —
* refuse loudly after the full validation ladder, never drop */
scr_throw_lowering_fence(fence);
return;
}
const char *data = buffer->kind == SCR_DYN_STR ? buffer->v.str->data
: (const char *)buffer->v.bytes->data;
size_t datalen = buffer->kind == SCR_DYN_STR ? buffer->v.str->len
: (size_t)scr_bytes_byte_len(buffer->v.bytes);
if (sliced) {
data += slice_off;
datalen = slice_len;
}
ScrStr *host = address->kind == SCR_DYN_STR ? scr_str_retain(address->v.str)
: scr_str_new("127.0.0.1", 9);
scr_dgram_send_raw(s, data, datalen, portnum, host);
scr_str_release(host);
}
+80 -1
View File
@@ -674,7 +674,11 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
case SCR_DYN_OBJ: d = "an instance of Object"; break;
case SCR_DYN_ARR: d = "an instance of Array"; break;
case SCR_DYN_BYTES: d = "an instance of Uint8Array"; break;
case SCR_DYN_FUNC: d = "function"; break; /* callers usually return before this */
case SCR_DYN_FUNC:
/* determineSpecificType: `function ${value.name}` — anonymous
* functions keep Node's trailing space. */
snprintf(detail, cap, "function %s", cb->v.fn.name != NULL ? cb->v.fn.name : "");
break;
case SCR_DYN_HANDLE:
snprintf(detail, cap, "an instance of %s", scr_dyn_handle_cls(cb));
break;
@@ -736,6 +740,81 @@ void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrD
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_TYPE");
}
/* The property flavor of the same ladder — Node renders option-bag
* members as "The \"options.x\" property must be ..." (errors.js keys the
* wording on the name, but every property-path caller here knows it is
* one). Same runtime-rendered Received tail; always throws catchably. */
void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got) {
char detail[64];
const char *d = scr_dyn_specific_type(got, detail, sizeof detail);
char msg[224];
int len = snprintf(msg, sizeof msg,
"The \"%s\" property must be %s. Received %s", name, expected, d);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_TYPE");
}
/* The compiler-resolved property-typed throw (error.propTypeThrow —
* argTypeThrow's option-bag sibling). Borrows all three; always throws. */
void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got) {
scr_dyn_prop_type_fail(name->data, expected->data, got);
}
/* ERR_INVALID_ARG_VALUE's "Received" tail — util.inspect where ARG_TYPE
* renders determineSpecificType: strings quote, scalars print plain.
* Deep shapes render their bracket sketch (enough for the validators'
* ladders; nothing observable pins the deep forms). */
const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap) {
switch (v->kind) {
case SCR_DYN_NULL: return "null";
case SCR_DYN_UNDEF: return "undefined";
case SCR_DYN_BOOL: return v->v.b ? "true" : "false";
case SCR_DYN_NUM: {
scr_f64_to_str(v->v.num, buf);
return buf;
}
case SCR_DYN_STR: {
const ScrStr *s = v->v.str;
size_t n = 0;
buf[n++] = '\'';
for (size_t i = 0; i < s->len && n + 5 < cap; i++) buf[n++] = s->data[i];
if (s->len + 2 + 5 > cap) {
memcpy(buf + n, "...", 3);
n += 3;
}
buf[n++] = '\'';
buf[n] = 0;
return buf;
}
case SCR_DYN_ARR: return "[ ... ]";
case SCR_DYN_OBJ: return "{ ... }";
case SCR_DYN_BYTES: return "<Buffer ...>";
default: return "[object]";
}
}
/* Node's ERR_INVALID_ARG_VALUE thrower: "The <argument|property> '<name>'
* <reason>. Received <inspected>" — the argument/property choice follows
* errors.js (a dotted name is a property path). `reason` defaults to
* "is invalid" when NULL. TypeError, like Node's default. */
void scr_dyn_arg_value_fail(const char *name, const char *reason, const ScrDyn *got) {
char insp[64];
const char *d = scr_dyn_inspect_lite(got, insp, sizeof insp);
char msg[256];
int len = snprintf(msg, sizeof msg, "The %s '%s' %s. Received %s",
strchr(name, '.') != NULL ? "property" : "argument", name,
reason != NULL ? reason : "is invalid", d);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE");
}
/* The deferred JS lowering fence, thrown from a ladder's post-validation
* tail: the compiler renders the message (the statement fence's own text,
* "[SC2020 at file:line]" included) and the ladder throws it verbatim
* AFTER its Node-order validations pass — Node's validation errors come
* first, the honest refuse second. Borrowed; always throws catchably. */
void scr_throw_lowering_fence(const ScrStr *msg) {
scr_throw_error_msg_code(SCR_ERR_ERROR, msg->data, msg->len, "SC2020");
}
static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag) {
scr_dyn_handle_ops(tag)->release(h);
}
+21 -1
View File
@@ -1347,7 +1347,27 @@ static double scr_net_autosel_timeout_ms = 250;
double scr_net_get_autosel_timeout(void) { return scr_net_autosel_timeout_ms; }
void scr_net_set_autosel_timeout(double ms) { scr_net_autosel_timeout_ms = ms; }
/* Node's setDefaultAutoSelectFamilyAttemptTimeout: validateInt32(value,
* 'value', 1), then the sub-10ms floor (Node clamps small budgets to
* 10ms). Throws ERR_OUT_OF_RANGE catchably. */
void scr_net_set_autosel_timeout(double ms) {
char recv[48], msg[160];
if (!(isfinite(ms) && trunc(ms) == ms)) {
scr_num_received(ms, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"value\" 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;
}
if (ms < 1 || ms > 2147483647.0) {
scr_num_received(ms, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"value\" is out of range. It must be >= 1 && <= 2147483647. Received %s", recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return;
}
scr_net_autosel_timeout_ms = ms < 10 ? 10 : ms;
}
/* ── fs error formatting ─────────────────────────────────────────────
* Node's fs errors read "<ERRNO>: <text>, <syscall> '<path>'"
+112
View File
@@ -92,6 +92,7 @@
#include "scr_runtime.h"
#include <errno.h>
#include <math.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
@@ -1601,6 +1602,117 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
return s;
}
/* ── the connect option-bag validation ladders (checked-dynamic lane) ──
* Node-order validation over DOM option values with Node's exact typed
* errors; the honest tail (connect for the validated forms, the
* compiler-rendered fence for bags with unmodeled keys) runs only after
* every validation passes. */
static bool scr_net_attempt_timeout_chk(const ScrDyn *t, const char *name) {
if (t->kind != SCR_DYN_NUM) {
scr_dyn_prop_type_fail(name, "of type number", t);
return false;
}
char recv[48], msg[192];
if (!(isfinite(t->v.num) && trunc(t->v.num) == t->v.num)) {
scr_num_received(t->v.num, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be an integer. Received %s",
name, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
if (t->v.num < 1 || t->v.num > 2147483647.0) {
scr_num_received(t->v.num, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"%s\" is out of range. It must be >= 1 && <= 2147483647. Received %s",
name, recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
return true;
}
/* connect({ ..., autoSelectFamilyAttemptTimeout }): the budget validates
* (validateInt32 from 1, Node's exact texts) and is then inert — this
* slice's single dial has nothing to time, the same simplification the
* autoSelectFamily flag already takes. NULL with the throw pending. */
ScrNetSocket *scr_net_connect_attempt(double port, ScrStr *host, const ScrDyn *t) {
if (!scr_net_attempt_timeout_chk(t, "options.autoSelectFamilyAttemptTimeout")) return NULL;
return scr_net_connect(port, host, NULL);
}
static bool scr_net_dyn_truthy(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL: return false;
case SCR_DYN_BOOL: return v->v.b;
case SCR_DYN_NUM: return v->v.num == v->v.num && v->v.num != 0;
case SCR_DYN_STR: return v->v.str->len > 0;
default: return true;
}
}
/* net.connect/createConnection over a RUNTIME option bag (computed keys
* — the invalid-input probes): Node's Socket-constructor order — the
* objectMode trio throws ERR_INVALID_ARG_VALUE first, then the port
* (validatePort), the host's string contract, autoSelectFamily's boolean
* contract, and the attempt budget. A bag that survives everything meets
* the compiler-rendered fence: an unmodeled key must refuse loudly, never
* silently drop. Always leaves an exception pending. */
void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) {
if (opts == NULL || opts->kind != SCR_DYN_OBJ) {
scr_dyn_arg_type_fail("options", "of type object",
opts ? opts : scr_dyn_undefined());
return;
}
static const char *const om[] = { "objectMode", "readableObjectMode", "writableObjectMode" };
for (size_t i = 0; i < 3; i++) {
const ScrDyn *v = scr_dyn_obj_get(opts, om[i], strlen(om[i]));
if (v != NULL && scr_net_dyn_truthy(v)) {
char name[48];
snprintf(name, sizeof name, "options.%s", om[i]);
scr_dyn_arg_value_fail(name, "is not supported", v);
return;
}
}
const ScrDyn *port = scr_dyn_obj_get(opts, "port", 4);
if (port != NULL && port->kind != SCR_DYN_UNDEF) {
bool ok = port->kind == SCR_DYN_NUM && trunc(port->v.num) == port->v.num &&
port->v.num >= 0 && port->v.num < 65536;
if (!ok && port->kind == SCR_DYN_STR) {
ScrStr *ps = scr_str_retain(port->v.str);
double n = scr_string_to_number(ps);
scr_str_release(ps);
ok = n == n && trunc(n) == n && n >= 0 && n < 65536 && port->v.str->len > 0;
}
if (!ok) {
char detail[64], msg[160];
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
int len = snprintf(msg, sizeof msg,
"options.port should be >= 0 and < 65536. Received %s", d);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
return;
}
}
const ScrDyn *host = scr_dyn_obj_get(opts, "host", 4);
if (host != NULL && host->kind != SCR_DYN_UNDEF && host->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.host", "of type string", host);
return;
}
const ScrDyn *asf = scr_dyn_obj_get(opts, "autoSelectFamily", 16);
if (asf != NULL && asf->kind != SCR_DYN_UNDEF && asf->kind != SCR_DYN_BOOL) {
scr_dyn_prop_type_fail("options.autoSelectFamily", "of type boolean", asf);
return;
}
const ScrDyn *att = scr_dyn_obj_get(opts, "autoSelectFamilyAttemptTimeout", 30);
if (att != NULL && att->kind != SCR_DYN_UNDEF &&
!scr_net_attempt_timeout_chk(att, "options.autoSelectFamilyAttemptTimeout")) {
return;
}
scr_throw_lowering_fence(fence);
}
/* ── the caller-lookup dial (net.connect with a lookup option) ─────────
*
* portless's createLoopbackConnection: connect({ host, port,
+59
View File
@@ -3136,6 +3136,24 @@ const char *scr_dyn_specific_type(const ScrDyn *v, char *buf, size_t cap);
* error.argTypeThrow libCall). Borrows all three; always throws. */
void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got);
void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got);
/* The property flavor ("The \"options.x\" property must be ...") — the
* option-bag validators' gate (error.propTypeThrow). Always throws. */
void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got);
void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got);
/* Node's ERR_INVALID_ARG_VALUE ("The argument 'encoding' is invalid
* encoding. Received 'no'") — reason NULL renders "is invalid".
* TypeError; always throws catchably. */
void scr_dyn_arg_value_fail(const char *name, const char *reason, const ScrDyn *got);
/* The ERR_INVALID_ARG_VALUE/%j "Received" renderer (inspect-lite:
* strings quote, scalars print plain, deep shapes sketch). */
const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap);
/* A ladder's post-validation refuse: throws the compiler-rendered SC2020
* statement-fence text verbatim (Node's validation errors run first). */
void scr_throw_lowering_fence(const ScrStr *msg);
/* ERR_OUT_OF_RANGE's "Received" number rendering (Node's
* addNumericalSeparator underscores past 2^32) — scr_bytes.c's renderer,
* shared by the fs/net/tls option-ladder validators. */
size_t scr_num_received(double v, char out[48]);
/* Listener-closure builders for the handle dispatchers' .on(...) paths:
* a runtime-built ScrClosure whose capture is the boxed dyn listener and
* whose invoke boxes the event tuple back into the DOM and calls through
@@ -4403,6 +4421,23 @@ ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got);
* numbers coerce (negatives answer now/1000), the rest throw Node's
* ERR_INVALID_ARG_TYPE. Borrowed. */
double scr_fs_to_unix_timestamp(const ScrDyn *t);
/* The fs argument-validation ladders (the fs.*Chk libCalls): Node-order
* validation over DOM values with Node's exact typed errors; a pass
* meets the real operation where one exists (mkdtempSync, macOS
* lchmodSync) or the compiler-rendered fence. All borrowed; the Chk
* forms without results always leave an exception pending. */
ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb);
void scr_fs_mkdtemp_chk(const ScrDyn *prefix, const ScrDyn *cb, const ScrStr *fence);
ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_read_file_chk(const ScrDyn *path, const ScrDyn *opts, const ScrDyn *cb, const ScrStr *fence);
void scr_fs_opendir_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_watch_file_chk(const ScrDyn *path, const ScrDyn *listener, const ScrStr *fence);
void scr_fs_lchmod_chk(const ScrDyn *path, const ScrDyn *mode, const ScrDyn *cb, const ScrStr *fence);
ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode);
ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode);
void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset,
const ScrDyn *length, const ScrDyn *position, const ScrStr *fence);
void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
/* The checked-dynamic max-listeners ladders (scr_events_emitter.c). */
ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n);
void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name);
@@ -4670,6 +4705,16 @@ void scr_net_server_on_connection(ScrNetServer *s, ScrClosure *cb /*moves*/, Scr
* server never fires it, like Node). */
void scr_net_server_on_secure_connection(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/); /* +1 */
/* connect with a validated autoSelectFamilyAttemptTimeout option: the
* budget runs Node's validateInt32-from-1 ladder (ERR_OUT_OF_RANGE /
* ERR_INVALID_ARG_TYPE) and is then inert — the single dial has nothing
* to time. +1, or NULL with the throw pending. */
ScrNetSocket *scr_net_connect_attempt(double port, ScrStr *host /*borrowed*/, const ScrDyn *t /*borrowed*/);
/* net.connect/createConnection over a RUNTIME option bag (computed
* keys): Node-order validation (objectMode trio, port, host,
* autoSelectFamily, attempt budget), then the compiler-rendered fence —
* always leaves an exception pending. Borrowed. */
void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence);
/* connect with a caller lookup (net.connect({ ..., lookup })): invokes
* lookup(hostname, options, answer-closure) synchronously; answer_fn is
* the emitted per-shape thunk that decodes the answer down to
@@ -4884,6 +4929,13 @@ void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool
* the default pair, exactly Node. */
typedef struct ScrSecureCtx ScrSecureCtx;
ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, const char *key, size_t key_len); /* +1 */
/* createSecureContext over a RUNTIME options record: Node's typed option
* validations first, then the pem walk (+1, or NULL with the exception
* pending). Borrowed. */
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts);
/* tls.getCACertificates(type): validateString + the documented name set,
* then the compiler-rendered fence — always leaves an exception pending. */
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence);
ScrSecureCtx *scr_secure_ctx_retain(ScrSecureCtx *c);
void scr_secure_ctx_release(ScrSecureCtx *c);
void *scr_secure_ctx_retain_v(void *p);
@@ -5307,6 +5359,13 @@ void scr_dgram_bind(ScrDgramSocket *s, double port, ScrStr *host /*borrowed*/, S
void scr_dgram_connect(ScrDgramSocket *s, double port, ScrStr *host /*borrowed*/, ScrClosure *cb /*moves, nullable*/);
void scr_dgram_send_str(ScrDgramSocket *s, ScrStr *data /*borrowed*/, double port, ScrStr *host /*borrowed*/);
void scr_dgram_send_bytes(ScrDgramSocket *s, ScrBytes *data /*borrowed*/, double port, ScrStr *host /*borrowed*/);
/* The send argument-validation ladder over DOM arguments (Node's
* signature shuffle, slice bounds, list/type contracts, port/address
* validation, and the connected-state errors); a fully-validated
* unconnected single-payload send RUNS, the rest meet the fence. */
void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a1,
const ScrDyn *a2, const ScrDyn *a3, const ScrDyn *a4,
const ScrStr *fence);
/* address() parts: ip THROWS "Not running" before bind/connect (+1
* otherwise); family/port are only called after ip succeeded. */
ScrStr *scr_dgram_addr_ip(ScrDgramSocket *s); /* +1, may throw */
+172
View File
@@ -73,6 +73,7 @@
#include "scr_runtime.h"
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -1102,6 +1103,121 @@ static const char *const SCR_TLS_SRV_FENCED_OPTIONS[] = {
"sessionTimeout", "sigalgs", "ticketKeys", NULL,
};
/* ── the typed option-validation ladders (checked-dynamic lane) ────────
* Node's configSecureContext / Server-constructor argument contracts over
* every PRESENT validated key, run BEFORE the pem walk and its fences so
* Node's typed errors always come first (the invalid-input probes isolate
* one bad option per call, so the fixed order below matches each).
* false = exception pending. */
static bool scr_tls_opts_validate(const ScrDyn *opts) {
if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true;
static const char *const STR_OPTS[] = { "ciphers", "passphrase", "ecdhCurve", "sessionIdContext", NULL };
for (size_t i = 0; STR_OPTS[i] != NULL; i++) {
const ScrDyn *v = scr_dyn_obj_get(opts, STR_OPTS[i], strlen(STR_OPTS[i]));
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_STR) {
char name[48];
snprintf(name, sizeof name, "options.%s", STR_OPTS[i]);
scr_dyn_prop_type_fail(name, "of type string", v);
return false;
}
}
static const char *const ENGINE_OPTS[] = { "clientCertEngine", "privateKeyEngine", "privateKeyIdentifier", NULL };
for (size_t i = 0; ENGINE_OPTS[i] != NULL; i++) {
const ScrDyn *v = scr_dyn_obj_get(opts, ENGINE_OPTS[i], strlen(ENGINE_OPTS[i]));
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_STR) {
char name[48];
snprintf(name, sizeof name, "options.%s", ENGINE_OPTS[i]);
scr_dyn_prop_type_fail(name, "of type string or one of null or undefined", v);
return false;
}
}
static const char *const VERSION_OPTS[] = { "minVersion", "maxVersion", NULL };
for (size_t i = 0; VERSION_OPTS[i] != NULL; i++) {
const ScrDyn *v = scr_dyn_obj_get(opts, VERSION_OPTS[i], strlen(VERSION_OPTS[i]));
if (v == NULL || v->kind == SCR_DYN_UNDEF) continue;
bool valid = false;
if (v->kind == SCR_DYN_STR) {
static const char *const KNOWN[] = { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3", NULL };
for (size_t k = 0; KNOWN[k] != NULL; k++) {
if (v->v.str->len == strlen(KNOWN[k]) && memcmp(v->v.str->data, KNOWN[k], v->v.str->len) == 0) {
valid = true;
break;
}
}
}
if (!valid) {
/* %j: strings render JSON-quoted, everything else inspect-lite. */
char rendered[96];
if (v->kind == SCR_DYN_STR) {
snprintf(rendered, sizeof rendered, "\"%.*s\"",
(int)(v->v.str->len < 80 ? v->v.str->len : 80), v->v.str->data);
} else {
/* %j over non-strings: JSON.stringify's scalar renderings. */
char lite[64];
snprintf(rendered, sizeof rendered, "%s", scr_dyn_inspect_lite(v, lite, sizeof lite));
}
char msg[192];
int len = snprintf(msg, sizeof msg, "%s is not a valid %s TLS protocol version",
rendered, VERSION_OPTS[i][2] == 'n' ? "minimum" : "maximum");
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_TLS_INVALID_PROTOCOL_VERSION");
return false;
}
}
static const char *const NUM_OPTS[] = { "handshakeTimeout", "keepAliveInitialDelay", NULL };
for (size_t i = 0; NUM_OPTS[i] != NULL; i++) {
const ScrDyn *v = scr_dyn_obj_get(opts, NUM_OPTS[i], strlen(NUM_OPTS[i]));
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_NUM) {
char name[48];
snprintf(name, sizeof name, "options.%s", NUM_OPTS[i]);
scr_dyn_prop_type_fail(name, "of type number", v);
return false;
}
}
{
const ScrDyn *v = scr_dyn_obj_get(opts, "sessionTimeout", 14);
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
if (v->kind != SCR_DYN_NUM) {
scr_dyn_prop_type_fail("options.sessionTimeout", "of type number", v);
return false;
}
double n = v->v.num;
char recv[48], msg[192];
if (!(isfinite(n) && trunc(n) == n)) {
scr_num_received(n, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"options.sessionTimeout\" 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 (n < 0 || n > 2147483647.0) {
scr_num_received(n, recv);
int len = snprintf(msg, sizeof msg,
"The value of \"options.sessionTimeout\" is out of range. It must be >= 0 && <= 2147483647. Received %s", recv);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
}
}
{
const ScrDyn *v = scr_dyn_obj_get(opts, "ticketKeys", 10);
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
if (v->kind != SCR_DYN_BYTES) {
scr_dyn_prop_type_fail("options.ticketKeys", "an instance of Buffer, TypedArray, or DataView", v);
return false;
}
double bytelen = scr_bytes_byte_len(v->v.bytes);
if (bytelen != 48) {
char msg[160];
int len = snprintf(msg, sizeof msg,
"The property 'options.ticketKeys' must be exactly 48 bytes. Received %.0f", bytelen);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE");
return false;
}
}
}
return true;
}
/* The server-options walk. Fills the cert/key out-params (+1 each) from a
* DOM options record; false = exception pending (partial results released). */
static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
@@ -1112,6 +1228,7 @@ static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes
scr_dyn_arg_type_fail("options", "of type object", opts ? opts : scr_dyn_undefined());
return false;
}
if (!scr_tls_opts_validate(opts)) return false; /* Node's typed errors first */
bool ok = true;
for (size_t i = 0; ok && i < opts->v.obj.len; i++) {
const ScrDynEntry *e = &opts->v.obj.entries[i];
@@ -1176,6 +1293,45 @@ static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes
return ok;
}
/* createSecureContext over a RUNTIME options record: Node's typed
* validations first (scr_tls_opts_validate), then the pem walk — a bag
* that validates AND carries both cert and key builds the real context;
* everything else met its ladder error or the walk's per-key fence.
* +1, or NULL with the exception pending. */
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/) {
ScrBytes *cert, *key;
if (!scr_tls_srv_opts_walk(opts, "tls.createSecureContext", &cert, &key)) return NULL;
ScrSecureCtx *c = scr_tls_create_secure_context((const char *)cert->data, cert->len,
(const char *)key->data, key->len);
scr_bytes_release(cert);
scr_bytes_release(key);
return c;
}
/* tls.getCACertificates(type): validateString, then the documented name
* set — an unknown name answers ERR_INVALID_ARG_VALUE; the real CA list
* has no lowering, so a valid name meets the fence. Always throws. */
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence) {
if (type->kind != SCR_DYN_STR) {
scr_dyn_arg_type_fail("type", "of type string", type);
return;
}
static const char *const KNOWN[] = { "default", "system", "bundled", "extra", NULL };
bool valid = false;
for (size_t i = 0; KNOWN[i] != NULL; i++) {
if (type->v.str->len == strlen(KNOWN[i]) &&
memcmp(type->v.str->data, KNOWN[i], type->v.str->len) == 0) {
valid = true;
break;
}
}
if (!valid) {
scr_dyn_arg_value_fail("type", NULL, type);
return;
}
scr_throw_lowering_fence(fence);
}
ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/,
ScrClosure *handler /*moves, nullable*/,
ScrNetConnFn fn) {
@@ -1241,6 +1397,22 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
for (size_t i = 0; ok && i < opts->v.obj.len; i++) {
const ScrDynEntry *e = &opts->v.obj.entries[i];
const ScrDyn *v = e->value;
if (strcmp(e->key, "checkServerIdentity") == 0) {
/* Node spreads user options over the defaults, so a PRESENT key
* replaces the builtin verifier even when its value is undefined
* — validateFunction then throws for anything non-callable. A
* real function keeps the fence (custom verification has no
* lowering). */
if (v->kind != SCR_DYN_FUNC) {
scr_dyn_prop_type_fail("options.checkServerIdentity", "of type function", v);
ok = false;
} else {
scr_tls_opt_fence("tls.connect", "checkServerIdentity",
"custom identity verification has no lowering — the runtime verifies against the servername/host");
ok = false;
}
continue;
}
if (v->kind == SCR_DYN_UNDEF) continue;
if (strcmp(e->key, "port") == 0) {
if (port >= 0) continue; /* the argument form wins, like Node */
+88
View File
@@ -0,0 +1,88 @@
// The fs argument-validation ladders: misuse of implemented-namespace fs
// APIs answers Node's exact typed errors (ERR_INVALID_ARG_TYPE /
// ERR_INVALID_ARG_VALUE / ERR_OUT_OF_RANGE) instead of fencing — exists'
// callback contract (and its REAL async answers, including the
// synchronous false for unvalidatable paths, Node's own wart), mkdtemp's
// prefix slot (the sync form running the real mkdtemp through the ladder),
// readFile/opendirSync's assertEncoding, watchFile's path/listener pair,
// createReadStream/createWriteStream's path and options.fd contracts, and
// fs.read's buffer/fd/offset/length/position ladder. The lchmod family is
// per-platform like Node itself: macOS validates (cb, path, then mode —
// octal strings, uint32 range) and non-macOS answers the not-a-function /
// ERR_METHOD_NOT_IMPLEMENTED shapes.
'use strict';
const fs = require('fs');
const os = require('os');
const { promises } = fs;
const show = (fn) => {
try {
const r = fn();
console.log('ret', typeof r === 'string' ? 'string' : r);
} catch (e) {
console.log(`${e.name}|${e.code}|${e.message}`);
}
};
// exists: the one throwing arm is the callback contract
show(() => fs.exists(__filename));
show(() => fs.exists());
show(() => fs.exists(__filename, {}));
// mkdtemp/mkdtempSync: prefix validation, then the real operation
show(() => fs.mkdtempSync(0, {}));
show(() => fs.mkdtempSync(null, {}));
show(() => fs.mkdtemp(true, () => {}));
const made = fs.mkdtempSync(os.tmpdir() + '/scrladder-', {});
console.log('made', typeof made, made.length > os.tmpdir().length, fs.existsSync(made));
fs.rmdirSync(made);
// readFile / opendirSync: assertEncoding before everything else
show(() => fs.readFile('bar.txt', { encoding: 'foo-8' }, () => {}));
show(() => fs.readFile('bar.txt'));
show(() => fs.opendirSync('.', { encoding: 'no' }));
// watchFile: path first, listener's function contract second
show(() => fs.watchFile('./some-file'));
show(() => fs.watchFile('./another-file', {}, 'bad listener'));
show(() => fs.watchFile(new Object(), () => {}));
// createReadStream/createWriteStream: options.fd, then the path contract
show(() => fs.createReadStream(46));
show(() => fs.createWriteStream(46));
show(() => fs.createReadStream(null, { fd: 'k' }));
show(() => fs.createWriteStream(null, { fd: 'k' }));
// fs.read: buffer, fd, offset, length, position — Node's order
show(() => fs.read(3, 4, 0, 'utf-8', () => {}));
show(() => fs.read(true, Buffer.allocUnsafe(4), 0, 4, 0, () => {}));
show(() => fs.read(3, Buffer.allocUnsafe(4), NaN, 4, 0, () => {}));
show(() => fs.read(3, Buffer.allocUnsafe(4), -1, 4, 0, () => {}));
show(() => fs.read(3, Buffer.allocUnsafe(4), 0, -1, 0, () => {}));
show(() => fs.read(3, Buffer.allocUnsafe(4), 0, 4, true, () => {}));
show(() => fs.read(3, Buffer.allocUnsafe(4), 0, 4, 0.5, () => {}));
// lchmod: per-platform, exactly like Node (macOS validates; the rest
// answer not-a-function / ERR_METHOD_NOT_IMPLEMENTED)
show(() => fs.lchmod(__filename));
show(() => fs.lchmod(__filename, {}));
show(() => fs.lchmod(false, 0o777, () => {}));
show(() => fs.lchmodSync(1));
show(() => fs.lchmodSync([]));
show(() => fs.lchmodSync(__filename, false));
show(() => fs.lchmodSync(__filename, '123x'));
show(() => fs.lchmodSync(__filename, -1));
show(() => fs.lchmodSync(__filename, 2 ** 32));
// exists' synchronous false for a path getValidatedPath rejects — Node
// calls back before returning
fs.exists({}, (y) => console.log('cb invalid', y));
(async () => {
try { await promises.lchmod(__filename, {}); } catch (e) { console.log('rejected', e.code); }
try { await promises.lchmod(__filename, -1); } catch (e) { console.log('rejected', e.code, e.message); }
// exists' real async answers, registered after both rejections so the
// completion order is deterministic under Node's threadpool AND the
// compiled runtime's settled promises (divergence 23).
fs.exists(__filename, (y) => console.log('cb file', y));
fs.exists(`${__filename}-NO`, (y) => console.log('cb missing', y));
})();
console.log('sync tail');
+36
View File
@@ -0,0 +1,36 @@
// The net argument-validation ladders: misuse of the implemented net
// surface answers Node's exact typed errors instead of fencing —
// createServer's options-type contract, the connect option bag's
// Socket-constructor order (the objectMode trio's ERR_INVALID_ARG_VALUE,
// host's string contract, autoSelectFamily's boolean contract, the
// autoSelectFamilyAttemptTimeout range ladder — validated and then inert,
// the single-dial simplification), listen's options.signal AbortSignal
// contract, and the setDefaultAutoSelectFamilyAttemptTimeout value ladder
// with Node's sub-10ms floor.
'use strict';
const net = require('net');
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
show(() => { net.createServer('path'); });
show(() => { net.createServer(0); });
show(() => { net.createConnection({ port: 8080, host: ['192.168.0.1'] }); });
show(() => { net.connect({ port: 8080, autoSelectFamily: 'INVALID' }); });
show(() => { net.connect({ port: 8080, autoSelectFamily: true, autoSelectFamilyAttemptTimeout: -10 }); });
show(() => { net.connect({ port: 8080, autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 0 }); });
for (const autoSelectFamilyAttemptTimeout of [-10, 0]) {
show(() => { net.connect({ port: 8080, autoSelectFamily: true, autoSelectFamilyAttemptTimeout }); });
show(() => { net.setDefaultAutoSelectFamilyAttemptTimeout(autoSelectFamilyAttemptTimeout); });
}
show(() => net.setDefaultAutoSelectFamilyAttemptTimeout(2.5));
for (const v of [1, 9, 25]) {
net.setDefaultAutoSelectFamilyAttemptTimeout(v);
console.log('budget', net.getDefaultAutoSelectFamilyAttemptTimeout());
}
{
const server = net.createServer();
show(() => { server.listen({ port: 0, signal: 'INVALID_SIGNAL' }); });
}
const invalidKeys = ['objectMode', 'readableObjectMode', 'writableObjectMode'];
for (const invalidKey of invalidKeys) {
const option = { port: 8080, [invalidKey]: true };
show(() => { net.createConnection(option); });
}
+37
View File
@@ -0,0 +1,37 @@
// dgram.Socket.send's argument-validation ladder: Node's signature
// shuffle over DOM arguments — the offset/length slice's type and bounds
// contracts (ERR_BUFFER_OUT_OF_BOUNDS, DataViews and subarrays included),
// the buffer-list per-element contract with the LIST as the Received
// tail, the unconnected port ladder (ERR_SOCKET_BAD_PORT with the
// specific-type tail and Node's trailing period), the address string
// contract (null/undefined-only gate — a falsy 0 still throws), and the
// connected-state ERR_SOCKET_DGRAM_IS_CONNECTED arms. createSocket's
// options.signal validates the AbortSignal contract the same way.
'use strict';
const dgram = require('dgram');
const sock = dgram.createSocket('udp4');
const buf = Buffer.from('test');
const host = '127.0.0.1';
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
show(() => { dgram.createSocket({ type: 'udp4', signal: {} }); });
show(() => sock.send());
show(() => sock.send(buf, 1, 1, -1, host));
show(() => sock.send(buf, 1, 1, 0, host));
show(() => sock.send(buf, 1, 1, 65536, host));
show(() => sock.send(23, 12345, host));
show(() => sock.send([buf, 23], 12345, host));
show(() => sock.send(buf, 6, 0));
show(() => sock.send('hello', 6, 0, 12345, host));
show(() => sock.send('hello', 0, 6, 12345, host));
show(() => sock.send('hello', 3, 4, 12345, host));
show(() => sock.send(new Uint8Array([1, 2, 3, 4, 5]).subarray(0, 5), 6, 0, 12345, host));
show(() => sock.send(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).subarray(2, 7), 0, 6, 12345, host));
show(() => sock.send(new DataView(new ArrayBuffer(7), 1, 5), 3, 4, 12345, host));
sock.connect(12345, () => {
show(() => sock.send(buf, 1, 1, -1, host));
show(() => sock.send(buf, 1234, '127.0.0.1', () => {}));
show(() => sock.send('hello', 6, 0));
show(() => sock.send('hello', 0, 6));
show(() => sock.send(23, 12345, host));
sock.close();
});
+39
View File
@@ -0,0 +1,39 @@
// The tls option-bag validation ladders: misuse of the implemented tls
// surface answers Node's exact typed errors instead of fencing — the
// createSecureContext/createServer string contracts (ciphers, passphrase,
// ecdhCurve), the engine trio's string-or-null-or-undefined clause, the
// number contracts (handshakeTimeout, sessionTimeout with validateInt32's
// range), ticketKeys' view contract and exact-48-bytes value ladder,
// minVersion/maxVersion's ERR_TLS_INVALID_PROTOCOL_VERSION with %j
// rendering, tls.connect's checkServerIdentity function contract (a
// PRESENT key replaces the builtin verifier even holding undefined —
// Node's defaults spread), and getCACertificates' type/value ladder.
'use strict';
const tls = require('tls');
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
show(() => { tls.createSecureContext({ ciphers: 1 }); });
show(() => { tls.createServer({ ciphers: 1 }); });
show(() => { tls.createSecureContext({ key: 'dummykey', passphrase: 1 }); });
show(() => { tls.createServer({ key: 'dummykey', passphrase: 1 }); });
show(() => { tls.createServer({ ecdhCurve: 1 }); });
show(() => { tls.createServer({ handshakeTimeout: 'abcd' }); });
show(() => { tls.createServer({ sessionTimeout: 'abcd' }); });
show(() => { tls.createServer({ ticketKeys: 'abcd' }); });
show(() => { tls.createServer({ ticketKeys: Buffer.alloc(0) }); });
show(() => { tls.createServer({ ticketKeys: Buffer.alloc(51) }); });
show(() => { tls.createSecureContext({ clientCertEngine: 0 }); });
show(() => { tls.createSecureContext({ privateKeyEngine: 0, privateKeyIdentifier: 'key' }); });
show(() => { tls.createSecureContext({ privateKeyEngine: 'engine', privateKeyIdentifier: 0 }); });
show(() => { tls.createSecureContext({ minVersion: 'fhqwhgads' }); });
show(() => { tls.createSecureContext({ maxVersion: 'fhqwhgads' }); });
show(() => { tls.createSecureContext({ minVersion: 42 }); });
show(() => { tls.createSecureContext({ sessionTimeout: -1 }); });
show(() => { tls.createSecureContext({ sessionTimeout: 2 ** 31 }); });
show(() => { tls.createSecureContext({ sessionTimeout: 1.5 }); });
for (const checkServerIdentity of [undefined, null, 1, true]) {
show(() => { tls.connect({ checkServerIdentity }); });
}
for (const invalid of [1, null, () => {}, true]) {
show(() => tls.getCACertificates(invalid));
}
show(() => tls.getCACertificates('test'));
+32
View File
@@ -0,0 +1,32 @@
// The stream-surface validation ladders: finished() over a provably-non-
// stream value answers Node's isNodeStream ERR_INVALID_ARG_TYPE (the
// watcher never registers), socket write's two-argument encoding form
// implements Node's stream_base typecheck — write(string, 'buffer') is
// the synchronous "Second argument must be a buffer" TypeError on an
// established socket, utf8 spellings are the plain write — and
// Readable.toWeb's `type` option answers Node's one-of ladder before any
// web-stream machinery.
'use strict';
const net = require('net');
const { Duplex, Readable, finished } = require('stream');
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
show(() => { finished({}, () => {}); });
show(() => { finished('nope', () => {}); });
const nodeStream = new Readable({ read() {} });
show(() => { Readable.toWeb(nodeStream, { type: 'wrong type' }); });
nodeStream.destroy();
const streamObj = new Duplex();
streamObj.end();
finished(streamObj, () => console.log('finished fired'));
const server = net.createServer().listen(0, () => {
const client = net.connect(server.address().port, () => {
show(() => { client.write('broken', 'buffer'); });
client.write('fine', 'utf8');
client.destroy();
server.close();
});
});