Read fs.promises as a module and check fs read and write arguments as Node does (#2098)

This commit is contained in:
Chris Tate
2026-10-25 00:23:19 -05:00
committed by GitHub
25 changed files with 498 additions and 71 deletions
@@ -532,6 +532,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
@@ -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),
@@ -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),
);
}
@@ -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;
@@ -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)) {
@@ -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,
@@ -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.
+9 -2
View File
@@ -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;
}
@@ -1032,6 +1032,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fs.openSync",
"fs.openNumericSync",
"fs.readSync",
"fs.readSyncDyn",
"fs.writeSync",
"fs.writeStrSync",
"fs.closeSync",
@@ -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
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8270-fs-promises-member.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8271-cjs-top-level-return.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8272-fs-stream-call-without-new.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8273-fs-read-write-argument-checks.cjs"
],
"diags": []
}
+2
View File
@@ -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
+209 -48
View File
@@ -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 <Buffer >";
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;
}
+24 -16
View File
@@ -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 <Buffer >";
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");
+25
View File
@@ -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));
})();
@@ -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');
@@ -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');
@@ -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 });
});
});
});
@@ -0,0 +1 @@
{ "plain": 4040, "san": 4008 }
@@ -0,0 +1 @@
{ "plain": 1907, "san": 2109 }
@@ -0,0 +1 @@
{ "plain": 2099, "san": 2373 }
@@ -0,0 +1 @@
{ "plain": 2379, "san": 2450 }