mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Read fs.promises as a module and check fs read and write arguments as Node does (#2098)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8270-fs-promises-member.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8271-cjs-top-level-return.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8272-fs-stream-call-without-new.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8273-fs-read-write-argument-checks.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user