mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
@@ -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));`);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>'"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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');
|
||||
@@ -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); });
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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'));
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user