Pass results from fs.mkdir, readFile and write callbacks (#1020)

This commit is contained in:
Chris Tate
2026-10-15 04:29:45 -05:00
committed by GitHub
10 changed files with 262 additions and 46 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "88ef6fabee3e41b50d74",
"artifactVersion": "35b26942ca49ff006818",
"rowCount": 3880
}
+11 -11
View File
@@ -314,9 +314,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 98,
"partial": 101,
"refused": 0,
"not-implemented": 175,
"not-implemented": 172,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -30840,9 +30840,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -31047,9 +31047,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -31484,9 +31484,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 37,
"verify-gap": 2472,
"audit-partial": 876,
"verify-gap": 2469,
"audit-partial": 879,
"classify": 2,
"implement": 85
}
@@ -27353,12 +27353,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.mkdir",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
}
}
},
@@ -27578,12 +27580,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.readFile",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
}
}
},
@@ -28084,12 +28088,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:fs.fs.write",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
}
}
},
@@ -326,9 +326,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 98,
"partial": 101,
"refused": 0,
"not-implemented": 175,
"not-implemented": 172,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -43746,6 +43746,7 @@
"tests/corpus/3135-fs-file-handle-controls.ts",
"tests/corpus/3136-fs-file-handle-options.ts",
"tests/corpus/3137-fs-vector-callbacks.cjs",
"tests/corpus/5540-fs-callback-forms.cjs",
"tests/corpus/fs-native-realpath.ts",
"tests/corpus/fs-read-encodings.ts",
"tests/corpus/node24-fs-links-callbacks.cjs",
@@ -46098,8 +46099,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.mkdir",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
},
"anchorSource": "exact"
},
@@ -46305,8 +46309,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.readFile",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
},
"anchorSource": "exact"
},
@@ -46918,8 +46925,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "compiler-feature:fs.fs.write",
"tests": [
"tests/corpus/5540-fs-callback-forms.cjs"
]
},
"anchorSource": "exact"
},
@@ -209,6 +209,7 @@
{ "chapter": "fs", "symbols": ["fs.statfs"], "status": "partial", "evidence": ["tests/corpus/node24-fs-statfs-callbacks.cjs", "tests/corpus/node24-fs-statfs-options.cjs", "tests/corpus/node24-fs-statfs-errors.cjs", "tests/fixtures/node-types/fs-statfs.ts"] },
{ "chapter": "fs", "symbols": ["fs.StatFs"], "status": "partial", "evidence": ["tests/corpus/node24-fs-statfs.ts", "tests/corpus/node24-fs-statfs-options.cjs", "tests/fixtures/node-types/fs-statfs.ts"] },
{ "chapter": "fs", "parent": "Class: `fs.StatFs`", "symbols": ["type", "bsize", "blocks", "bfree", "bavail", "files", "ffree"], "status": "supported", "evidence": ["tests/corpus/node24-fs-statfs.ts", "tests/corpus/node24-fs-statfs-options.cjs", "tests/fixtures/node-types/fs-statfs.ts"] },
{ "chapter": "fs", "signatures": ["`fs.mkdir(path[, options], callback)`", "`fs.readFile(path[, options], callback)`", "`fs.write(fd, string[, position[, encoding]], callback)`"], "status": "partial", "evidence": ["tests/corpus/5540-fs-callback-forms.cjs"] },
{ "chapter": "fs", "symbols": ["fs.utimes", "fs.futimes", "fs.lutimes"], "status": "partial", "evidence": ["tests/corpus/node24-fs-timestamps-callbacks.cjs", "tests/corpus/node24-fs-timestamps-symlinks.ts", "tests/corpus/node24-fs-timestamps-errors.cjs"] },
{ "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] },
{ "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] },
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5540-fs-callback-forms.cjs"
],
"diags": []
}
+74 -6
View File
@@ -794,9 +794,37 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
const ScrDyn *buffer = NULL;
if (!strcmp(op, "readFile")) {
const ScrDyn *options = ARG(1);
if (options->kind != SCR_DYN_STR && (!scr_fs_cb_signal(options) || scr_fs_cb_refuse_option(options, "flag"))) goto done;
if (options->kind != SCR_DYN_STR && !scr_fs_cb_signal(options)) goto done;
ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding");
if (encoding) result = scr_fs_read_file_sync_dyn(path, encoding);
ScrDyn *flag = options->kind == SCR_DYN_STR ? NULL : scr_fs_cb_option(options, "flag");
bool reading_flag = !flag || flag->kind == SCR_DYN_UNDEF || flag->kind == SCR_DYN_NULL ||
(flag->kind == SCR_DYN_STR && flag->v.str->len == 1 && flag->v.str->data[0] == 'r');
if (encoding && reading_flag) result = scr_fs_read_file_sync_dyn(path, encoding);
else if (encoding) {
/* Node opens the file with options.flag ('w+' truncates first), then
* reads the whole descriptor. */
double opened = scr_fs_cb_open(path, flag, 0666, "r");
if (opened >= 0 && !scr_exc_pending()) {
bool text = encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0;
if (text) {
ScrStr *content = scr_fs_read_fd_encoded(opened, encoding->v.str);
if (content) {
result = scr_dyn_new_str(content);
scr_str_release(content);
}
} else {
ScrBytes *content = scr_fs_read_fd_bytes(opened);
if (content) {
result = scr_dyn_new_buffer(content);
scr_bytes_release(content);
}
}
int saved = errno;
close((int)opened);
errno = saved;
}
}
scr_dyn_release(flag);
scr_dyn_release(encoding);
} else if (!strcmp(op, "writeFile") || !strcmp(op, "appendFile")) {
const ScrDyn *options = ARG(2);
@@ -828,8 +856,13 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
else if (!strcmp(op, "mkdir")) {
const ScrDyn *options = ARG(1);
double mode = options->kind == SCR_DYN_NUM ? options->v.num : scr_fs_cb_option_number(options, "mode", 0777);
if (scr_fs_cb_option_bool(options->kind == SCR_DYN_NUM ? scr_dyn_undefined() : options, "recursive")) scr_fs_mkdir_recursive_mode(path, mode);
else scr_fs_mkdir_mode(path, mode);
if (scr_fs_cb_option_bool(options->kind == SCR_DYN_NUM ? scr_dyn_undefined() : options, "recursive")) {
/* The callback receives the first directory created (undefined when
* the path already existed). */
ScrStr *first = scr_fs_mkdir_recursive_first(path, mode);
if (first && !scr_exc_pending()) result = scr_fs_cb_string(first);
else if (first) scr_str_release(first);
} else scr_fs_mkdir_mode(path, mode);
} else if (!strcmp(op, "mkdtemp")) result = scr_fs_cb_string(scr_fs_mkdtemp(path));
else if (!strcmp(op, "rm")) scr_fs_rm_opts_retry(path, scr_fs_cb_option_bool(ARG(1), "recursive"), scr_fs_cb_option_bool(ARG(1), "force"), scr_fs_cb_option_number(ARG(1), "maxRetries", 0), scr_fs_cb_option_number(ARG(1), "retryDelay", 100));
else if (!strcmp(op, "rmdir")) scr_fs_rmdir(path);
@@ -892,6 +925,33 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
#else
scr_fs_throw(ENOSYS, "glob", path);
#endif
} else if (!strcmp(op, "write") && ARG(1)->kind == SCR_DYN_STR) {
/* write(fd, string[, position[, encoding]], callback): a non-number
* position writes at the current offset; the encoding defaults to
* utf8 (unknown names write utf8, as the binding does). */
const ScrDyn *string = ARG(1);
const ScrDyn *position = count >= 3 ? ARG(2) : scr_dyn_undefined();
const ScrDyn *encoding = count >= 4 ? ARG(3) : scr_dyn_undefined();
ScrStr *codec = encoding->kind == SCR_DYN_STR && scr_bytes_is_encoding(encoding->v.str)
? scr_str_retain(encoding->v.str)
: scr_str_new("utf8", 4);
if (codec->len == 3 && memcmp(codec->data, "hex", 3) == 0 && string->v.str->len % 2 != 0) {
/* validateEncoding throws at the call, before any write. */
char reason[64];
snprintf(reason, sizeof reason, "is invalid for data of length %zu", (size_t)string->v.str->len);
scr_dyn_arg_value_fail("encoding", reason, encoding);
scr_str_release(codec);
return NULL;
} else {
ScrBytes *bytes = scr_buffer_from_dyn(string, codec);
if (bytes) {
double at = position->kind == SCR_DYN_NUM ? position->v.num : -1;
result = scr_dyn_new_num(scr_fs_write_sync(fd, bytes, 0, (double)bytes->len, at));
scr_bytes_release(bytes);
}
}
scr_str_release(codec);
buffer = string;
} else if (!strcmp(op, "read") || !strcmp(op, "write")) {
bool readOp = !strcmp(op, "read");
const ScrDyn *options = ARG(1);
@@ -899,8 +959,16 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc
if (readOp && !scr_dyn_bytes_is(options, SCR_BYTES_U8)) { ownedBuffer = scr_fs_cb_option(options, "buffer"); buffer = ownedBuffer; }
else buffer = options;
ScrBytes *bytes = buffer && buffer->kind == SCR_DYN_BYTES ? scr_dyn_bytes_unbox(buffer) : NULL;
if (!bytes) scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer ? buffer : scr_dyn_undefined());
else {
if (!bytes) {
/* The argument check throws at the call (Node validates before the
* request is made). */
scr_dyn_arg_type_fail("buffer",
readOp ? "an instance of Buffer, TypedArray, or DataView"
: "of type string or an instance of Buffer, TypedArray, or DataView",
buffer ? buffer : scr_dyn_undefined());
scr_dyn_release(ownedBuffer);
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 position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1);
+45
View File
@@ -4423,6 +4423,51 @@ void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode) {
free(buf);
}
/* mkdir(p, { recursive: true })'s answer, Node's MKDirpSync first path:
* the first directory its walk creates, or NULL when the path already
* existed. Node tries mkdir on the path and, on ENOENT, on the path cut
* before its last separator, repeatedly; the first mkdir that succeeds
* (its kernel parent exists) is the answer, spelled as cut. Creates the
* path like scr_fs_mkdir_recursive_mode (the exception pending on
* failure). */
static bool scr_fs_mkdirp_exists(char *buf, size_t len) {
char saved = buf[len];
buf[len] = 0;
struct stat st;
int rc = len == 0 ? stat(".", &st) : stat(buf, &st);
int e = errno;
buf[len] = saved;
return rc == 0 || e != ENOENT;
}
ScrStr *scr_fs_mkdir_recursive_first(ScrStr *path, double mode) {
char *buf = malloc(path->len + 1);
if (!buf) {
scr_trap("scriptc: out of memory\n");
}
memcpy(buf, path->data, path->len + 1);
size_t cur = path->len, first = 0;
while (cur > 0 && !scr_fs_mkdirp_exists(buf, cur)) {
first = cur;
/* The kernel's parent: trailing separators dropped, then the last
* component and the separators before it ("/" stays). */
size_t k = cur;
while (k > 1 && scr_fs_sep(buf[k - 1])) k--;
while (k > 0 && !scr_fs_sep(buf[k - 1])) k--;
while (k > 1 && scr_fs_sep(buf[k - 1])) k--;
if (scr_fs_mkdirp_exists(buf, k)) break;
/* ENOENT: Node retries with the path cut before its last separator. */
size_t cut = cur;
while (cut > 0 && !scr_fs_sep(buf[cut - 1])) cut--;
if (cut == 0) break;
cur = cut - 1;
}
scr_mkdir_rec(buf, path->len, (mode_t)mode);
free(buf);
if (scr_exc_pending() || first == 0) return NULL;
return scr_str_new(path->data, first);
}
/* First failure of an rm walk, recorded instead of thrown so the retry
* form can decide (retryable errno + attempts left → sleep and go again)
* before anything reaches the exception cell. `path` is +1 when err != 0;
+3
View File
@@ -287,6 +287,9 @@ void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode);
void scr_fs_mkdir_mode(ScrStr *path, double mode);
void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode);
/* The same walk, answering the first directory created (+1) or NULL
* when the path already existed (or on failure, the exception pending). */
ScrStr *scr_fs_mkdir_recursive_first(ScrStr *path, double mode);
/* Sequentially consistent integer operations and shared Int32 wait queues. */
typedef struct ScrBytes ScrBytes;
+77
View File
@@ -0,0 +1,77 @@
// fs callback forms that pass results: mkdir with { recursive: true }
// gives the first directory it creates (spelled as Node cuts the path, or
// undefined when everything exists), readFile honors options.flag
// ('w+' truncates, 'a+' and 'r+' read), and write takes a string with an
// optional position and encoding, reporting the bytes written and the
// string. Invalid data, buffers and encodings throw at the call.
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fs-cb-"));
process.chdir(dir);
function step(i) {
const steps = [
() => fs.mkdir("a/b/c", { recursive: true }, (e, p) => (console.log("mkdir", e, p), step(i + 1))),
() => fs.mkdir("a/b/c", { recursive: true }, (e, p) => (console.log("mkdir again", e, p), step(i + 1))),
() => fs.mkdir("a/b/d/e/", { recursive: true }, (e, p) => (console.log("trailing", e, p), step(i + 1))),
() => fs.mkdir("./x//y", { recursive: true }, (e, p) => (console.log("doubled", e, p), step(i + 1))),
() => fs.mkdir(path.join(dir, "abs/q"), { recursive: true }, (e, p) => (console.log("absolute", e, p === path.join(dir, "abs")), step(i + 1))),
() => fs.mkdir("plain", (e, p) => (console.log("non-recursive", e, p), step(i + 1))),
() => {
fs.writeFileSync("f.txt", "file text");
fs.mkdir("f.txt/sub", { recursive: true }, (e, p) => (console.log("under a file", e.code, p), step(i + 1)));
},
() => fs.readFile("f.txt", { encoding: "utf8", flag: "r" }, (e, t) => (console.log("flag r", e, t), step(i + 1))),
() => fs.readFile("f.txt", { encoding: "utf8", flag: "a+" }, (e, t) => (console.log("flag a+", e, t), step(i + 1))),
() => fs.readFile("f.txt", { flag: "r+" }, (e, b) => (console.log("flag r+", e, b.toString()), step(i + 1))),
() => fs.readFile("new.txt", { encoding: "utf8", flag: "a+" }, (e, t) => (console.log("flag a+ creates", e, JSON.stringify(t), fs.existsSync("new.txt")), step(i + 1))),
() => fs.readFile("f.txt", { encoding: "utf8", flag: "w+" }, (e, t) => (console.log("flag w+", e, JSON.stringify(t), fs.statSync("f.txt").size), step(i + 1))),
() => fs.readFile("missing.txt", { encoding: "utf8", flag: "r+" }, (e) => (console.log("flag r+ missing", e.code), step(i + 1))),
() => {
const fd = fs.openSync("w.txt", "w+");
fs.write(fd, "hello", (e, n, s) => {
console.log("write string", e, n, s);
fs.write(fd, "WORLD", 0, (e2, n2) => {
console.log("write at 0", e2, n2, fs.readFileSync("w.txt", "utf8"));
fs.write(fd, "686921", 5, "hex", (e3, n3, s3) => {
console.log("write hex", e3, n3, s3, fs.readFileSync("w.txt", "latin1"));
fs.write(fd, "é", null, "latin1", (e4, n4) => {
console.log("write latin1", e4, n4, fs.readFileSync("w.txt").length);
fs.closeSync(fd);
step(i + 1);
});
});
});
});
},
() => {
const fd = fs.openSync("w.txt", "r+");
try {
fs.write(fd, "abc", 0, "hex", () => {});
} catch (error) {
console.log("odd hex", error.code, error.message);
}
try {
fs.write(fd, 42, () => {});
} catch (error) {
console.log("bad data", error.code, error.message);
}
try {
fs.read(fd, { buffer: 5 }, () => {});
} catch (error) {
console.log("bad read buffer", error.code, error.message);
}
fs.closeSync(fd);
step(i + 1);
},
];
if (i < steps.length) steps[i]();
else {
process.chdir(os.tmpdir());
fs.rmSync(dir, { recursive: true });
console.log("removed", fs.existsSync(dir));
}
}
step(0);