Skip to content

Commit 728239f

Browse files
authored
fix(ext/node): drop extra positional args in promisified fs.promises.* (#34347)
`Array#map` invokes `fn(elem, index, array)`, so the idiomatic `paths.map(fs.promises.unlink)` ends up calling the promisify wrapper with three positionals. The wrapper appends its own callback, producing `fs.unlink(path, index, array, cb)`; callback-style `fs.unlink` then validates the second positional as the callback and rejects with `TypeError: The "callback" argument must be of type function. Received type number (0)`. The same shape affected every `fs.promises.*` method built via `util.promisify` (`rename`, `rm`, `chmod`, `stat`, `readFile`, ...). `lazyPromisifyFs` now takes the underlying method's positional arity and slices incoming args to that length before delegating, matching Node, whose own `fs.promises.*` wrappers don't go through `util.promisify` and so don't have this issue. Fixes #34335
1 parent 54179d8 commit 728239f

2 files changed

Lines changed: 52 additions & 21 deletions

File tree

ext/node/polyfills/internal/fs/promises.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,22 @@ const {
4545
// promises` on fs.ts, whose lazyInternalPromises() then hits a TDZ on the
4646
// in-flight `default` binding). Build wrappers lazily on first call.
4747
const _promisifyCache: Record<string, any> = { __proto__: null } as any;
48-
function lazyPromisifyFs(name: string): any {
48+
// `arity` is the max positional arg count the underlying fs callback method
49+
// accepts (excluding the callback). Extra args are dropped so idiomatic
50+
// patterns like `paths.map(fs.promises.unlink)` -- which Array#map invokes as
51+
// `unlink(elem, index, array)` -- don't trip the promisify wrapper's appended
52+
// callback (the wrapper would otherwise call `fs.unlink(path, index, array,
53+
// cb)` and `fs.unlink` reads the second positional as the callback). Node's
54+
// own `fs.promises.*` wrappers don't go through `util.promisify` and so don't
55+
// have this issue.
56+
function lazyPromisifyFs(name: string, arity: number): any {
4957
return (...args: any[]) => {
5058
let fn = _promisifyCache[name];
5159
if (fn === undefined) {
5260
fn = promisify((lazyFs() as any)[name]);
5361
_promisifyCache[name] = fn;
5462
}
63+
if (args.length > arity) args.length = arity;
5564
return fn(...new SafeArrayIterator(args));
5665
};
5766
}
@@ -94,7 +103,7 @@ async function handleFdClose<T>(
94103

95104
// -- access --
96105

97-
const accessPromise = lazyPromisifyFs("access") as (
106+
const accessPromise = lazyPromisifyFs("access", 2) as (
98107
path: string | Buffer | URL,
99108
mode?: number,
100109
) => Promise<void>;
@@ -123,14 +132,14 @@ function appendFilePromise(
123132

124133
// -- chmod --
125134

126-
const chmodPromise = lazyPromisifyFs("chmod") as (
135+
const chmodPromise = lazyPromisifyFs("chmod", 2) as (
127136
path: string | Buffer | URL,
128137
mode: string | number,
129138
) => Promise<void>;
130139

131140
// -- chown --
132141

133-
const chownPromise = lazyPromisifyFs("chown") as (
142+
const chownPromise = lazyPromisifyFs("chown", 3) as (
134143
path: string | Buffer | URL,
135144
uid: number,
136145
gid: number,
@@ -147,22 +156,22 @@ const lchmodPromise: (
147156
return await op_node_lchmod(path, mode);
148157
};
149158

150-
const lchownPromise = lazyPromisifyFs("lchown") as (
159+
const lchownPromise = lazyPromisifyFs("lchown", 3) as (
151160
path: string | Buffer | URL,
152161
uid: number,
153162
gid: number,
154163
) => Promise<void>;
155164

156-
const linkPromise = lazyPromisifyFs("link") as (
165+
const linkPromise = lazyPromisifyFs("link", 2) as (
157166
existingPath: string | Buffer | URL,
158167
newPath: string | Buffer | URL,
159168
) => Promise<void>;
160169

161-
const unlinkPromise = lazyPromisifyFs("unlink") as (
170+
const unlinkPromise = lazyPromisifyFs("unlink", 1) as (
162171
path: string | Buffer | URL,
163172
) => Promise<void>;
164173

165-
const renamePromise = lazyPromisifyFs("rename") as (
174+
const renamePromise = lazyPromisifyFs("rename", 2) as (
166175
oldPath: string | Buffer | URL,
167176
newPath: string | Buffer | URL,
168177
) => Promise<void>;
@@ -176,7 +185,7 @@ type rmOptions = {
176185
retryDelay?: number;
177186
};
178187

179-
const rmPromise = lazyPromisifyFs("rm") as (
188+
const rmPromise = lazyPromisifyFs("rm", 2) as (
180189
path: string | URL,
181190
options?: rmOptions,
182191
) => Promise<void>;
@@ -189,7 +198,7 @@ type rmdirOptions = {
189198
retryDelay?: number;
190199
};
191200

192-
const rmdirPromise = lazyPromisifyFs("rmdir") as (
201+
const rmdirPromise = lazyPromisifyFs("rmdir", 2) as (
193202
path: string | Buffer | URL,
194203
options?: rmdirOptions,
195204
) => Promise<void>;
@@ -199,12 +208,12 @@ type MkdirOptions =
199208
| number
200209
| boolean;
201210

202-
const mkdirPromise = lazyPromisifyFs("mkdir") as (
211+
const mkdirPromise = lazyPromisifyFs("mkdir", 2) as (
203212
path: string | URL,
204213
options?: MkdirOptions,
205214
) => Promise<string | undefined>;
206215

207-
const mkdtempPromise = lazyPromisifyFs("mkdtemp") as (
216+
const mkdtempPromise = lazyPromisifyFs("mkdtemp", 2) as (
208217
prefix: string | Buffer | Uint8Array | URL,
209218
options?: { encoding: string } | string,
210219
) => Promise<string>;
@@ -277,14 +286,14 @@ type OpendirOptions = {
277286
bufferSize?: number;
278287
};
279288

280-
const opendirPromise = lazyPromisifyFs("opendir") as (
289+
const opendirPromise = lazyPromisifyFs("opendir", 2) as (
281290
path: string | Buffer | URL,
282291
options?: OpendirOptions,
283292
) => Promise<Dir>;
284293

285294
// -- symlink --
286295

287-
const symlinkPromise = lazyPromisifyFs("symlink") as (
296+
const symlinkPromise = lazyPromisifyFs("symlink", 3) as (
288297
target: string | Buffer | URL,
289298
path: string | Buffer | URL,
290299
type?: string,
@@ -306,7 +315,7 @@ async function truncatePromise(
306315

307316
// -- utimes --
308317

309-
const utimesPromise = lazyPromisifyFs("utimes") as (
318+
const utimesPromise = lazyPromisifyFs("utimes", 3) as (
310319
path: string | URL,
311320
atime: number | string | Date,
312321
mtime: number | string | Date,
@@ -316,7 +325,7 @@ const utimesPromise = lazyPromisifyFs("utimes") as (
316325

317326
// Low-level callback writeFile, used when we already have an fd/FileHandle
318327
// (i.e. avoid recursing back through writeFilePromise via FileHandle.writeFile).
319-
const rawWriteFilePromise = lazyPromisifyFs("writeFile") as (
328+
const rawWriteFilePromise = lazyPromisifyFs("writeFile", 3) as (
320329
pathOrRid: string | number | URL | FileHandle,
321330
data:
322331
| string
@@ -366,21 +375,21 @@ function writeFilePromise(
366375

367376
// -- realpath --
368377

369-
const realpathPromise = lazyPromisifyFs("realpath") as (
378+
const realpathPromise = lazyPromisifyFs("realpath", 2) as (
370379
path: string | Buffer,
371380
options?: string | { encoding?: string },
372381
) => Promise<string | Buffer>;
373382

374383
// -- stat --
375384

376-
const statPromise = lazyPromisifyFs("stat") as (
385+
const statPromise = lazyPromisifyFs("stat", 2) as (
377386
path: string | Buffer | URL,
378387
options?: { bigint?: boolean },
379388
) => Promise<unknown>;
380389

381390
// -- statfs --
382391

383-
const statfsPromise = lazyPromisifyFs("statfs") as (
392+
const statfsPromise = lazyPromisifyFs("statfs", 2) as (
384393
path: string | Buffer | URL,
385394
options?: { bigint?: boolean },
386395
) => Promise<unknown>;
@@ -389,7 +398,7 @@ const statfsPromise = lazyPromisifyFs("statfs") as (
389398

390399
// Low-level callback readFile, used when we already have an fd/FileHandle
391400
// (i.e. avoid recursing back through readFilePromise via FileHandle.readFile).
392-
const rawReadFilePromise = lazyPromisifyFs("readFile");
401+
const rawReadFilePromise = lazyPromisifyFs("readFile", 2);
393402

394403
// Mirrors Node's lib/internal/fs/promises.js readFile(): when given a path,
395404
// open a FileHandle and delegate via handleFdClose so the close error
@@ -424,7 +433,7 @@ function readFilePromise(
424433
})();
425434
}
426435

427-
const readlinkPromise = lazyPromisifyFs("readlink") as (
436+
const readlinkPromise = lazyPromisifyFs("readlink", 2) as (
428437
path: string | Buffer | URL,
429438
opt?: { encoding?: string | null },
430439
) => Promise<string | Uint8Array>;

tests/unit_node/fs_test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,3 +2012,25 @@ Deno.test(
20122012
);
20132013
},
20142014
);
2015+
2016+
// Regression test for #34335: `paths.map(fs.promises.unlink)` passes
2017+
// `(elem, index, array)` to unlink. The promisify wrapper used to forward all
2018+
// three to callback-style `fs.unlink`, which then read `index` as the callback
2019+
// and rejected. Promisified `fs.promises.*` methods must slice extra args.
2020+
Deno.test(
2021+
"[node/fs] promises methods drop extra positional args (Array#map use)",
2022+
async () => {
2023+
const tmp = mkdtempSync(join(tmpdir(), "fs-arity-"));
2024+
try {
2025+
const a = join(tmp, "a");
2026+
const b = join(tmp, "b");
2027+
writeFileSync(a, "");
2028+
writeFileSync(b, "");
2029+
await Promise.all([a, b].map(promises.unlink));
2030+
assertEquals(existsSync(a), false);
2031+
assertEquals(existsSync(b), false);
2032+
} finally {
2033+
rmSync(tmp, { recursive: true, force: true });
2034+
}
2035+
},
2036+
);

0 commit comments

Comments
 (0)