diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index a61438646..fc188488d 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -532,6 +532,7 @@ export const LIB_FN_SYMS: Record = { "fs.openSync": "scr_fs_open", "fs.openNumericSync": "scr_fs_open_numeric", "fs.readSync": "scr_fs_read_sync", + "fs.readSyncDyn": "scr_fs_read_sync_dyn", "fs.writeSync": "scr_fs_write_sync", "fs.writeStrSync": "scr_fs_write_str_sync", "fs.closeSync": "scr_fs_close", diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 0bbc3b3fa..6b5e2d8ae 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -2,7 +2,7 @@ import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js" import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { lowerFsPath } from "./fs-paths.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { type BuiltinModuleFn, FS_WRITE_FILE_DOCUMENTED_OPTIONS, @@ -403,6 +403,28 @@ export function lowerFsReadSyncCall( } lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } + // JavaScript arguments outside the typed signature (an untyped fd or + // buffer, a bigint or untyped position) take Node's argument checks at + // run time, then the same read. + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { + const nodes = expr.arguments.nodes; + const typed = [F64, BYTES_U8, F64, F64, F64].every((want, i) => { + const node = nodes[i]; + if (node === undefined) return true; + let value: ts.Expression = node; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (i === 4 && value.kind === ts.SyntaxKind.NullKeyword) return true; + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + return t !== null && (want.kind === "bytes" ? t.kind === "bytes" : t.kind === "f64"); + }); + if (!typed) { + const args = [0, 1, 2, 3, 4].map((i): IrExpr => { + const node = nodes[i]; + return node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + }); + return { kind: "libCall", fn: "fs.readSyncDyn", args, type: F64, loc }; + } + } const args: IrExpr[] = [ lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 498f9b6d4..6c6f8fa79 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -4,7 +4,7 @@ * and everything inherited goes through the stream spoke. */ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { isNodeTypesPath } from "../../dts-paths.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { @@ -178,3 +178,32 @@ export function lowerFsStreamNew( locOf(expr), ); } + +/** `fs.ReadStream(path, options)` / `fs.WriteStream(...)` called without + * `new` in JavaScript: Node's stream constructors construct when called as + * functions, so the call is the `new` form. Null for other calls. */ +export function lowerFsStreamCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; + const callee = expr.expression; + const name = ts.isPropertyAccessExpression(callee) + ? callee.name + : ts.isIdentifier(callee) + ? callee + : null; + if (name === null || !ts.isIdentifier(name)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(name); + const resolved = + symbol && symbol.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(symbol) + : symbol; + const cls = fsStreamClassOf(lowerer, resolved); + if (cls === null) return null; + return fsStreamCreateExpr( + lowerer, + expr.arguments.nodes, + cls === "ReadStream", + `fs.${cls}`, + expr, + locOf(expr), + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 4ded700fe..98cc188f8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -239,7 +239,9 @@ export function builtinNamespaceDestructureModuleOf( if (ts.isCallExpression(init)) { const cr = createRequireSpecOf(lowerer, init); module = cr !== null && cr.spec !== null ? canonicalBuiltinModule(cr.spec) : null; - } else if (ts.isIdentifier(init)) { + } else if (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) { + // `const { open } = fs.promises` / `= require("fs").promises`: a member + // that is itself a supported module. module = lowerer.builtinNamespaceModuleOf(init); } if (module === null) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 3d2cebe2b..ad10bb02b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -27,7 +27,11 @@ import { import { lowerMockTimersElementCall } from "./lower-test.js"; import { objectEnumerationReceiver } from "./object-enumeration-receiver.js"; import { InternalCompilerError } from "../../errors.js"; -import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js"; +import { + isFsStreamMember, + lowerFsStreamCall, + lowerFsStreamMethodCall, +} from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { isZlibStreamMember, @@ -378,6 +382,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const buffer = lowerBufferConstruct(lowerer, expr); if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc }; } + { + const stream = lowerFsStreamCall(lowerer, expr); + if (stream) return stream; + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3adb856fc..f20341612 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13875,7 +13875,7 @@ export class Lowerer { if ( ts.isPropertyAccessExpression(expr) && !expr.questionDotToken && - ts.isIdentifier(expr.expression) + (ts.isIdentifier(expr.expression) || requireSpecOf(expr.expression) !== null) ) { const outer = this.builtinNamespaceModuleOf(expr.expression); if (outer === null) return null; @@ -14080,6 +14080,15 @@ export class Lowerer { `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`, ); } + // A member that is a supported module in its own right (`fs.promises` + // is node:fs/promises, as in Node) reads as that module's namespace. + { + const sub = this.builtinNamespaceModuleOf(expr); + if (sub !== null) { + const type: IrType = { kind: "moduleNs", moduleId: `builtin:${sub}` }; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; + } + } this.noLowering( `${bi.module}.${bi.member}`, expr, diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index fcc945faa..0303d34d7 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2136,6 +2136,8 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< ], // The utf8 string overload; the table row is the Buffer-window form. writeSync: ["fs.writeStrSync"], + // The form whose arguments Node checks at run time. + readSync: ["fs.readSyncDyn"], // The { recursive, mode } option lowerings. mkdirSync: ["fs.mkdirModeSync", "fs.mkdirRecursiveSync", "fs.mkdirRecursiveModeSync"], // The { recursive, force } and maxRetries/retryDelay option lowerings. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 33bb23e86..aa6ddbfa7 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1374,9 +1374,16 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } // The deprecated Buffer(...) call without `new`, which Node's Buffer - // function accepts (containers/bytes.ts lowers it). + // function accepts (containers/bytes.ts lowers it), and fs.ReadStream(...) + // and fs.WriteStream(...), which construct their stream + // (builtins/fs-streams.ts). if (d.code === 2348) { - return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'"); + const text = ts.flattenDiagnosticMessageText(d, "\n"); + return ( + text.includes("'BufferConstructor'") || + text.includes("'typeof ReadStream'") || + text.includes("'typeof WriteStream'") + ); } return false; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 621f9081a..0b21c5eaf 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1032,6 +1032,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.openSync", "fs.openNumericSync", "fs.readSync", + "fs.readSyncDyn", "fs.writeSync", "fs.writeStrSync", "fs.closeSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 2c3f1f08e..21cd75490 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1199,6 +1199,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fs.openSync": { argTypes: [STRING, STRING], result: F64 }, "fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 }, "fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, + "fs.readSyncDyn": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: F64 }, "fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 }, "fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 }, // fs.watch's callback func type is program-dependent (zero params, or diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json new file mode 100644 index 000000000..eb3938040 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8270-fs-promises-member.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8270-fs-promises-member.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json new file mode 100644 index 000000000..2fcecbe67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8271-cjs-top-level-return.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8271-cjs-top-level-return.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json new file mode 100644 index 000000000..7fdb114e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8272-fs-stream-call-without-new.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8272-fs-stream-call-without-new.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json new file mode 100644 index 000000000..60e64d369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8273-fs-read-write-argument-checks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8273-fs-read-write-argument-checks.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index c7ea68988..0710f3460 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -530,6 +530,8 @@ 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); +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position); void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence); /* The fs streams' option pieces (scr_fs_stream.c): the getOptions, * options.fd and path checks above without the fence; the checked path diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 253cad1da..daeb47c60 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -635,9 +635,12 @@ void scr_fs_cb_schedule(const ScrDyn *callback, ScrDyn *result, const ScrDyn *bu #endif } -static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *); +static bool scr_fs_read_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, const ScrDyn *, + double *); static bool scr_fs_encoding_chk(const ScrDyn *); static bool scr_fs_int_range_chk(const ScrDyn *, const char *, double, double, const char *); +static bool scr_fs_length_int32(const ScrDyn *, double *); +static bool scr_fs_write_validate(const ScrDyn *, const ScrDyn *, const ScrDyn *); static bool scr_fs_mode_chk(const ScrDyn *, const char *); /* Where Node's signature handling finds the callback: a fixed position, or @@ -856,7 +859,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc * (below); every other form checks it here, before its path. */ bool late = !strcmp(op, "access") || !strcmp(op, "copyFile") || !strcmp(op, "chmod") || !strcmp(op, "open") || !strcmp(op, "mkdir") || !strcmp(op, "fchmod") || !strcmp(op, "ftruncate") || !strcmp(op, "truncate") || - !strcmp(op, "read"); + !strcmp(op, "read") || !strcmp(op, "write"); if (!late && !scr_fs_cb_chk(callback, legacy ? "callback" : "cb")) { scr_arr_release(vectors); return NULL; } size_t count = timestamp ? argc : slot < argc ? slot : argc; #define ARG(index) scr_fs_cb_arg(args, count, index) @@ -889,7 +892,8 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc const ScrDyn *params = count == 3 ? ARG(2) : scr_dyn_bytes_is(ARG(1), SCR_BYTES_U8) ? NULL : ARG(1); bool object = !params || scr_fs_dyn_absent(params) || params->kind == SCR_DYN_OBJ || params->kind == SCR_DYN_BYTES || - (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)); + (params->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_array(params)) || + (params->kind == SCR_DYN_PROXY && scr_dyn_is_object(params)); if (!object) { scr_dyn_arg_type_fail("options", "of type object", params); return NULL; @@ -901,7 +905,28 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc } if (!scr_fs_cb_chk(callback, "cb")) return NULL; } - if (!strcmp(op, "read") && count >= 4 && !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4))) return NULL; + /* The read length after `length |= 0`, converted once (a valueOf call + * runs once, as in Node). */ + double readLength = 0; + if (!strcmp(op, "read") && count >= 4 && + !scr_fs_read_validate(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), &readLength)) + return NULL; + /* write checks its fd first, then the buffer form's callback, offset and + * length (validateOffsetLengthWrite), or the string form's data and + * callback. */ + if (!strcmp(op, "write")) { + if (!scr_fs_int_range_chk(ARG(0), "fd", 0, 2147483647.0, ">= 0 && <= 2147483647")) return NULL; + if (ARG(1)->kind == SCR_DYN_BYTES) { + if (!scr_fs_cb_chk(callback, "cb") || !scr_fs_write_validate(ARG(1), ARG(2), ARG(3))) return NULL; + } else { + if (ARG(1)->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("buffer", "of type string or an instance of Buffer, TypedArray, or DataView", + ARG(1)); + return NULL; + } + if (!scr_fs_cb_chk(callback, "cb")) return NULL; + } + } bool descriptor = timestamp_fd || vector || newControl || !strcmp(op, "close") || !strcmp(op, "fstat") || !strcmp(op, "fsync") || !strcmp(op, "read") || !strcmp(op, "write") || !strcmp(op, "fchown"); /* truncate checks len, then the callback, before the open its request * starts with. */ @@ -1100,7 +1125,10 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc return NULL; } else { double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); - double length = ownedBuffer ? scr_fs_cb_option_number(options, "length", bytes->len - offset) : scr_fs_cb_number(ARG(3), "length", bytes->len - offset); + double length = bytes->len - offset; + if (ownedBuffer) length = scr_fs_cb_option_number(options, "length", length); + else if (readOp && count >= 4) length = readLength; + else length = scr_fs_cb_number(ARG(3), "length", length); double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); if (!scr_exc_pending()) result = scr_dyn_new_num(readOp ? scr_fs_read_sync(fd, bytes, offset, length, position) : scr_fs_write_sync(fd, bytes, offset, length, position)); } @@ -1877,68 +1905,136 @@ static bool scr_fs_int_range_chk(const ScrDyn *v, const char *name, double min, return true; } -static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, - const ScrDyn *length, const ScrDyn *position) { - if (buffer->kind != SCR_DYN_BYTES) { - scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); +/* `length |= 0`: ToInt32 of the length argument (a non-numeric value is + * 0). False with the exception pending when ToNumber throws. */ +static bool scr_fs_length_int32(const ScrDyn *length, double *out) { + double n; + if (length->kind == SCR_DYN_NUM) { + n = length->v.num; + } else if (!scr_dyn_number_coerce_js(length, &n)) { return false; } + if (!isfinite(n)) { + *out = 0; + return true; + } + double m = fmod(trunc(n), 4294967296.0); + if (m < 0) m += 4294967296.0; + *out = m >= 2147483648.0 ? m - 4294967296.0 : m; + return true; +} + +/* A bigint read position in [-1n, 2n ** 63n - 1n - BigInt(length)] + * (validatePosition); false with ERR_OUT_OF_RANGE pending otherwise. The + * received value groups the digits past 2^32 in threes, as Node's + * ERR_OUT_OF_RANGE does for bigints. */ +static bool scr_fs_bigint_position_chk(ScrBigInt *position, double length) { + ScrBigInt *top = scr_bigint_from_i64(INT64_MAX); + ScrBigInt *span = scr_bigint_from_f64(length); + ScrBigInt *max = scr_bigint_sub(top, span); + scr_bigint_release(top); + scr_bigint_release(span); + ScrBigInt *low = scr_bigint_from_i64(-1); + bool ok = scr_bigint_cmp_f64(position, low) >= 0 && scr_bigint_cmp_f64(position, max) <= 0; + scr_bigint_release(low); + if (ok) { + scr_bigint_release(max); + return true; + } + ScrStr *limit = scr_bigint_to_string(max, 10); + scr_bigint_release(max); + ScrStr *decimal = scr_bigint_to_string(position, 10); + if (!limit || !decimal) { + if (limit) scr_str_release(limit); + if (decimal) scr_str_release(decimal); + return false; + } + ScrBigInt *bound = scr_bigint_from_i64((int64_t)1 << 32); + ScrBigInt *negBound = scr_bigint_from_i64(-((int64_t)1 << 32)); + bool group = scr_bigint_cmp_f64(position, bound) > 0 || scr_bigint_cmp_f64(position, negBound) < 0; + scr_bigint_release(bound); + scr_bigint_release(negBound); + /* addNumericalSeparator: underscores every three digits from the right, + * after an optional sign. */ + char digits[200]; + size_t len = decimal->len < 120 ? decimal->len : 120, out = 0; + size_t start = len > 0 && decimal->data[0] == '-' ? 1 : 0; + for (size_t i = 0; i < len; i++) { + if (group && i > start && (len - i) % 3 == 0) digits[out++] = '_'; + digits[out++] = (char)decimal->data[i]; + } + digits[out] = '\0'; + char msg[384]; + int n = snprintf(msg, sizeof msg, + "The value of \"position\" is out of range. It must be >= -1 && <= %.*s. Received %sn", + (int)limit->len, (const char *)limit->data, digits); + scr_str_release(limit); + scr_str_release(decimal); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + + +/* fs.read(fd, buffer, offset, length, position, cb)'s argument checks, in + * Node's order: the fd, the buffer, the offset (an integer >= 0), the + * length as `length |= 0`, the position (validatePosition, against that + * length), then for a non-empty read the empty-buffer and + * validateOffsetLengthRead checks. The converted length goes to + * *length_out when it is not NULL, so the read does not convert again. */ +static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position, double *length_out) { if (fd->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("fd", "of type number", fd); return false; } + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return false; + } double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; 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 false; } - 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 false; - } + off = offset->v.num; } + double count = 0; + if (!scr_fs_length_int32(length, &count)) return false; + if (length_out) *length_out = count; if (!scr_fs_dyn_absent(position)) { - if (position->kind != SCR_DYN_NUM) { + if (position->kind == SCR_DYN_BIGINT) { + if (!scr_fs_bigint_position_chk(position->v.bigint, count)) return false; + } else if (position->kind != SCR_DYN_NUM) { scr_dyn_arg_type_fail("position", "of type bigint or integer", position); return false; - } - if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, - ">= -1 && <= 9007199254740991")) { + } else if (!scr_fs_int_range_chk(position, "position", -1, 9007199254740991.0, + ">= -1 && <= 9007199254740991")) { return false; } } - 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 false; - } - scr_dyn_arg_type_fail("length", "of type number", length); - return false; - } - 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 false; + if (count == 0) return true; + if (buflen == 0) { + if (scr_bytes_is_buffer(buffer->v.bytes)) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + } else { + scr_dyn_arg_value_fail("buffer", "is empty and cannot be written", buffer); } + return false; + } + if (count < 0 || off + count > buflen) { + char recv[48], range[64], msg[192]; + scr_num_received(count, recv); + if (count < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", buflen - off); + int len = snprintf(msg, sizeof msg, + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; } return true; } @@ -1946,7 +2042,7 @@ static bool scr_fs_read_validate(const ScrDyn *fd, const ScrDyn *buffer, const S void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length, const ScrDyn *position, const ScrStr *fence) { - if (scr_fs_read_validate(fd, buffer, offset, length, position)) scr_throw_lowering_fence(fence); + if (scr_fs_read_validate(fd, buffer, offset, length, position, NULL)) scr_throw_lowering_fence(fence); } /* createReadStream/createWriteStream(path, options?): getOptions' @@ -1991,3 +2087,68 @@ bool scr_fs_stream_mode(const ScrDyn *value, double *mode) { } return scr_fs_cb_file_mode(value, "mode", mode); } + +/* fs.readSync(fd, buffer, offset, length[, position]) with arguments the + * compiler could not type (JavaScript): Node's checks (validateBuffer + * first, then the fd, the offset, `length |= 0` and the position, which + * may be a bigint), then the typed read. */ +double scr_fs_read_sync_dyn(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset, + const ScrDyn *length, const ScrDyn *position) { + if (buffer->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer); + return 0; + } + double count = 0; + if (!scr_fs_read_validate(fd, buffer, offset, length, position, &count)) return 0; + if (count == 0) return 0; + double at = scr_fs_dyn_absent(position) ? -1 + : position->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(position->v.bigint) + : position->v.num; + ScrBytes *bytes = scr_dyn_bytes_unbox(buffer); + double read = scr_fs_read_sync(fd->v.num, bytes, scr_fs_dyn_absent(offset) ? 0 : offset->v.num, + count, at); + scr_bytes_release(bytes); + return read; +} + +/* ERR_OUT_OF_RANGE for the number `n` of argument `name`. Always false. */ +static bool scr_fs_num_range_fail(const char *name, const char *range, double n) { + char recv[48], msg[192]; + 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; +} + +/* fs.write(fd, buffer, offset, length, position, cb)'s buffer checks: the + * offset (an integer >= 0 unless omitted, null or the callback), a + * non-number length defaulting to the rest of the buffer, then + * validateOffsetLengthWrite. An options object in the offset slot is read + * by the write itself. */ +static bool scr_fs_write_validate(const ScrDyn *buffer, const ScrDyn *offset, const ScrDyn *length) { + if (offset->kind == SCR_DYN_OBJ || offset->kind == SCR_DYN_TYPED_REF || offset->kind == SCR_DYN_PROXY || + offset->kind == SCR_DYN_NULL) + return true; + double buflen = scr_bytes_byte_len(buffer->v.bytes); + double off = 0; + if (!scr_fs_dyn_absent(offset) && offset->kind != SCR_DYN_FUNC) { + if (!scr_fs_int_range_chk(offset, "offset", 0, 9007199254740991.0, ">= 0 && <= 9007199254740991")) + return false; + off = offset->v.num; + } + double len = length->kind == SCR_DYN_NUM ? length->v.num : buflen - off; + char range[64]; + if (off > buflen) { + snprintf(range, sizeof range, "<= %.0f", buflen); + return scr_fs_num_range_fail("offset", range, off); + } + if (len > buflen - off) { + snprintf(range, sizeof range, "<= %.0f", buflen - off); + return scr_fs_num_range_fail("length", range, len); + } + if (len < 0) return scr_fs_num_range_fail("length", ">= 0", len); + if (!(isfinite(len) && trunc(len) == len)) return scr_fs_num_range_fail("length", "an integer", len); + if (len > 2147483647.0) return scr_fs_num_range_fail("length", ">= 0 && <= 2147483647", len); + return true; +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 69c19e1b7..0f11619bf 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4236,27 +4236,35 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length, scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } - /* Node returns after the offset and position validation when the - * requested length normalizes to zero: buffer-window bounds and fd are - * not consulted. Preserve the existing size_t coercion's [0, 1) case. */ - if (length >= 0 && length < 1) return 0; - if (offset > (double)bytelen) { - numbuf[scr_f64_to_str(offset, numbuf)] = 0; + /* `length |= 0`, then Node's order: a zero length reads nothing after + * the offset and position checks; otherwise the buffer must not be + * empty and the window must fit it (validateOffsetLengthRead). */ + if (!isfinite(length)) { + length = 0; + } else { + double wrapped = fmod(trunc(length), 4294967296.0); + if (wrapped < 0) wrapped += 4294967296.0; + length = wrapped >= 2147483648.0 ? wrapped - 4294967296.0 : wrapped; + } + if (length == 0) return 0; + if (bytelen == 0) { + static const char empty[] = + "The argument 'buffer' is empty and cannot be written. Received "; + scr_throw_error_msg_code(SCR_ERR_TYPE, empty, sizeof empty - 1, "ERR_INVALID_ARG_VALUE"); + return 0; + } + if (length < 0 || offset + length > (double)bytelen) { + char recv[48], range[48]; + scr_num_received(length, recv); + if (length < 0) snprintf(range, sizeof range, ">= 0"); + else snprintf(range, sizeof range, "<= %.0f", (double)bytelen - offset); mlen = snprintf(msg, sizeof msg, - "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s", - numbuf); + "The value of \"length\" is out of range. It must be %s. Received %s", range, + recv); scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); return 0; } size_t off = (size_t)offset; - if (length < 0 || (double)length > (double)(bytelen - off)) { - numbuf[scr_f64_to_str(length, numbuf)] = 0; - mlen = snprintf(msg, sizeof msg, - "The value of \"length\" is out of range. It must be <= %zu. Received %s", - bytelen - off, numbuf); - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); - return 0; - } size_t want = (size_t)length; uint8_t *bounce = buf->shared ? malloc(want ? want : 1) : NULL; if (buf->shared && !bounce) scr_trap("scriptc: out of memory\n"); diff --git a/tests/corpus/8270-fs-promises-member.cjs b/tests/corpus/8270-fs-promises-member.cjs new file mode 100644 index 000000000..86448f012 --- /dev/null +++ b/tests/corpus/8270-fs-promises-member.cjs @@ -0,0 +1,25 @@ +'use strict'; +// fs.promises is node:fs/promises, as in Node: through a binding, an +// inline require, destructuring and a direct member call. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fsp-')); +const file = path.join(dir, 'data.txt'); +const fsPromises = fs.promises; +const { readFile, stat } = require('fs').promises; +const { open } = fs.promises; + +(async () => { + await fsPromises.writeFile(file, 'abc'); + await fs.promises.appendFile(file, 'def'); + console.log(await readFile(file, 'utf8')); + console.log((await stat(file)).size); + const handle = await open(file, 'r'); + const { bytesRead, buffer } = await handle.read(Buffer.alloc(3), 0, 3, 1); + console.log(bytesRead, buffer.toString()); + await handle.close(); + await fsPromises.rm(dir, { recursive: true }); + console.log(fs.existsSync(dir)); +})(); diff --git a/tests/corpus/8271-cjs-top-level-return.cjs b/tests/corpus/8271-cjs-top-level-return.cjs new file mode 100644 index 000000000..9f5cc6d23 --- /dev/null +++ b/tests/corpus/8271-cjs-top-level-return.cjs @@ -0,0 +1,20 @@ +'use strict'; +// A return at the top level of the entry CommonJS module ends its +// evaluation, as in Node's module wrapper: from a block, a loop and a try +// statement, whose finally block still runs. +const fs = require('fs'); + +console.log('start'); +for (const name of ['a', 'b', 'c']) { + if (name === 'c') { + try { + console.log('returning at', name); + process.on('exit', (code) => console.log('exit', code)); + if (!fs.existsSync('/scriptc-missing-path')) return; + } finally { + console.log('finally runs'); + } + } + console.log('visited', name); +} +console.log('not reached'); diff --git a/tests/corpus/8272-fs-stream-call-without-new.cjs b/tests/corpus/8272-fs-stream-call-without-new.cjs new file mode 100644 index 000000000..91fe0a6c0 --- /dev/null +++ b/tests/corpus/8272-fs-stream-call-without-new.cjs @@ -0,0 +1,21 @@ +'use strict'; +// fs.ReadStream and fs.WriteStream called without new construct their +// stream, as in Node. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-streams-')); +const target = path.join(dir, 'out.txt'); +const writer = fs.WriteStream(target); +writer.on('finish', () => { + const reader = fs.ReadStream(target, { encoding: 'utf8' }); + let text = ''; + reader.on('data', (chunk) => (text += chunk)); + reader.on('end', () => { + console.log(text); + fs.rmSync(dir, { recursive: true }); + }); +}); +writer.write('hello '); +writer.end('streams'); diff --git a/tests/corpus/8273-fs-read-write-argument-checks.cjs b/tests/corpus/8273-fs-read-write-argument-checks.cjs new file mode 100644 index 000000000..b27564837 --- /dev/null +++ b/tests/corpus/8273-fs-read-write-argument-checks.cjs @@ -0,0 +1,80 @@ +'use strict'; +// fs.read, fs.readSync and fs.write check their arguments in Node's order +// and with Node's errors: the fd, the buffer, the offset, `length |= 0`, +// number and bigint positions, and the buffer window. A length with a +// valueOf method is converted once. +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptc-fs-args-')); +const file = path.join(dir, 'data.txt'); +fs.writeFileSync(file, 'abcdef'); +const fd = fs.openSync(file, 'r+'); +const noop = () => {}; + +function attempt(name, fn) { + try { + fn(); + console.log(name, 'ok'); + } catch (error) { + console.log(name, error.name, error.code, error.message); + } +} + +attempt('read offset -1', () => fs.read(fd, Buffer.alloc(3), -1, 3, 0, noop)); +attempt('read offset NaN', () => fs.read(fd, Buffer.alloc(3), NaN, 3, 0, noop)); +attempt('read length -1', () => fs.read(fd, Buffer.alloc(3), 0, -1, 0, noop)); +attempt('read length 4', () => fs.read(fd, Buffer.alloc(3), 0, 4, 0, noop)); +attempt('read length 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 1.5, 0, noop)); +attempt('read length "x"', () => fs.read(fd, Buffer.alloc(3), 0, 'x', 0, noop)); +attempt('read position 1.5', () => fs.read(fd, Buffer.alloc(3), 0, 3, 1.5, noop)); +attempt('read position -2', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2, noop)); +attempt('read position ""', () => fs.read(fd, Buffer.alloc(3), 0, 3, '', noop)); +attempt('read position 2n ** 63n', () => fs.read(fd, Buffer.alloc(3), 0, 3, 2n ** 63n, noop)); +attempt('read position -2n', () => fs.read(fd, Buffer.alloc(3), 0, 3, -2n, noop)); +attempt('read empty buffer', () => fs.read(fd, Buffer.alloc(0), 0, 1, 0, noop)); +attempt('read fd true', () => fs.read(true, Buffer.alloc(3), 0, 3, 0, noop)); +attempt('read buffer number', () => fs.read(fd, 3, 0, 3, 0, noop)); + +const read = (position) => { + const buffer = Buffer.alloc(3); + const count = fs.readSync(fd, buffer, 0, 3, position); + return `${count} ${buffer.toString()}`; +}; +attempt('readSync position 1n', () => console.log(read(1n))); +attempt('readSync position 0n', () => console.log(read(0n))); +attempt('readSync position 2n ** 63n', () => read(2n ** 63n)); +attempt('readSync offset 4', () => fs.readSync(fd, Buffer.alloc(3), 4, 1, 0)); +attempt('readSync buffer number', () => fs.readSync(fd, 3, 0, 1, 0)); +attempt('readSync fd null', () => fs.readSync(null, Buffer.alloc(3), 0, 1, 0)); +console.log(fs.readSync(fd, Buffer.alloc(3), 0, 0.5, 0), fs.readSync(fd, Buffer.alloc(3), 0, 2.5, 0)); +let syncCalls = 0; +const syncLength = { valueOf: () => (++syncCalls === 1 ? 2 : 3) }; +const syncTarget = Buffer.alloc(3); +const syncCount = fs.readSync(fd, syncTarget, 0, syncLength, 0); +console.log('readSync valueOf', syncCount, syncCalls, syncTarget.toString('utf8', 0, syncCount)); + +attempt('write fd null', () => fs.write(null, Buffer.alloc(1), 0, 1, noop)); +attempt('write fd "1"', () => fs.write('1', 'x', noop)); +attempt('write offset -1', () => fs.write(fd, Buffer.alloc(0), -1, noop)); +attempt('write offset NaN', () => fs.write(fd, Buffer.from('abcd'), NaN, 4, 0, noop)); +attempt('write offset 5', () => fs.write(fd, Buffer.from('abcd'), 5, noop)); +attempt('write length 5', () => fs.write(fd, Buffer.from('abcd'), 0, 5, 0, noop)); +attempt('write length -1', () => fs.write(fd, Buffer.alloc(0), 0, -1, noop)); +attempt('write data number', () => fs.write(fd, 4, noop)); +attempt('write no callback', () => fs.write(fd, Buffer.from('a'), 0, 1, 0)); + +fs.read(fd, Buffer.alloc(2), 0, 2, 2n, (error, count, buffer) => { + console.log('read 2n', error, count, buffer.toString()); + fs.write(fd, Buffer.from('XY'), 0, 2, 0, (error2, written) => { + console.log('wrote', error2, written, fs.readFileSync(file, 'utf8')); + let calls = 0; + const length = { valueOf: () => (++calls === 1 ? 1 : 3) }; + fs.read(fd, Buffer.alloc(3), 0, length, 0, (error3, count3, buffer3) => { + console.log('read valueOf', error3, count3, calls, buffer3.toString('utf8', 0, count3)); + fs.closeSync(fd); + fs.rmSync(dir, { recursive: true }); + }); + }); +}); diff --git a/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json b/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json new file mode 100644 index 000000000..04488e737 --- /dev/null +++ b/tests/harness/corpus-costs/8270-fs-promises-member.cjs.json @@ -0,0 +1 @@ +{ "plain": 4040, "san": 4008 } diff --git a/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json b/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json new file mode 100644 index 000000000..33a1c426c --- /dev/null +++ b/tests/harness/corpus-costs/8271-cjs-top-level-return.cjs.json @@ -0,0 +1 @@ +{ "plain": 1907, "san": 2109 } diff --git a/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json b/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json new file mode 100644 index 000000000..dab2504e6 --- /dev/null +++ b/tests/harness/corpus-costs/8272-fs-stream-call-without-new.cjs.json @@ -0,0 +1 @@ +{ "plain": 2099, "san": 2373 } diff --git a/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json b/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json new file mode 100644 index 000000000..1989bfb5f --- /dev/null +++ b/tests/harness/corpus-costs/8273-fs-read-write-argument-checks.cjs.json @@ -0,0 +1 @@ +{ "plain": 2379, "san": 2450 }