Skip to content

Commit 2800733

Browse files
RafaelGSSpull[bot]
authored andcommitted
lib: fix fs.readdir recursive async
Fixes: #56006 PR-URL: #56041 Reviewed-By: Ethan Arrowood <[email protected]> Reviewed-By: Juan José Arboleda <[email protected]>
1 parent b15c90f commit 2800733

File tree

2 files changed

+119
-39
lines changed

2 files changed

+119
-39
lines changed

lib/fs.js

+114-39
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,102 @@ function mkdirSync(path, options) {
13701370
}
13711371
}
13721372

1373+
/*
1374+
* An recursive algorithm for reading the entire contents of the `basePath` directory.
1375+
* This function does not validate `basePath` as a directory. It is passed directly to
1376+
* `binding.readdir`.
1377+
* @param {string} basePath
1378+
* @param {{ encoding: string, withFileTypes: boolean }} options
1379+
* @param {(
1380+
* err?: Error,
1381+
* files?: string[] | Buffer[] | Dirent[]
1382+
* ) => any} callback
1383+
* @returns {void}
1384+
*/
1385+
function readdirRecursive(basePath, options, callback) {
1386+
const context = {
1387+
withFileTypes: Boolean(options.withFileTypes),
1388+
encoding: options.encoding,
1389+
basePath,
1390+
readdirResults: [],
1391+
pathsQueue: [basePath],
1392+
};
1393+
1394+
let i = 0;
1395+
1396+
function read(path) {
1397+
const req = new FSReqCallback();
1398+
req.oncomplete = (err, result) => {
1399+
if (err) {
1400+
callback(err);
1401+
return;
1402+
}
1403+
1404+
if (result === undefined) {
1405+
callback(null, context.readdirResults);
1406+
return;
1407+
}
1408+
1409+
processReaddirResult({
1410+
result,
1411+
currentPath: path,
1412+
context,
1413+
});
1414+
1415+
if (i < context.pathsQueue.length) {
1416+
read(context.pathsQueue[i++]);
1417+
} else {
1418+
callback(null, context.readdirResults);
1419+
}
1420+
};
1421+
1422+
binding.readdir(
1423+
path,
1424+
context.encoding,
1425+
context.withFileTypes,
1426+
req,
1427+
);
1428+
}
1429+
1430+
read(context.pathsQueue[i++]);
1431+
}
1432+
1433+
// Calling `readdir` with `withFileTypes=true`, the result is an array of arrays.
1434+
// The first array is the names, and the second array is the types.
1435+
// They are guaranteed to be the same length; hence, setting `length` to the length
1436+
// of the first array within the result.
1437+
const processReaddirResult = (args) => (args.context.withFileTypes ? handleDirents(args) : handleFilePaths(args));
1438+
1439+
function handleDirents({ result, currentPath, context }) {
1440+
const { 0: names, 1: types } = result;
1441+
const { length } = names;
1442+
1443+
for (let i = 0; i < length; i++) {
1444+
// Avoid excluding symlinks, as they are not directories.
1445+
// Refs: https://github.com/nodejs/node/issues/52663
1446+
const fullPath = pathModule.join(currentPath, names[i]);
1447+
const dirent = getDirent(currentPath, names[i], types[i]);
1448+
ArrayPrototypePush(context.readdirResults, dirent);
1449+
1450+
if (dirent.isDirectory() || binding.internalModuleStat(binding, fullPath) === 1) {
1451+
ArrayPrototypePush(context.pathsQueue, fullPath);
1452+
}
1453+
}
1454+
}
1455+
1456+
function handleFilePaths({ result, currentPath, context }) {
1457+
for (let i = 0; i < result.length; i++) {
1458+
const resultPath = pathModule.join(currentPath, result[i]);
1459+
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
1460+
const stat = binding.internalModuleStat(binding, resultPath);
1461+
ArrayPrototypePush(context.readdirResults, relativeResultPath);
1462+
1463+
if (stat === 1) {
1464+
ArrayPrototypePush(context.pathsQueue, resultPath);
1465+
}
1466+
}
1467+
}
1468+
13731469
/**
13741470
* An iterative algorithm for reading the entire contents of the `basePath` directory.
13751471
* This function does not validate `basePath` as a directory. It is passed directly to
@@ -1379,58 +1475,37 @@ function mkdirSync(path, options) {
13791475
* @returns {string[] | Dirent[]}
13801476
*/
13811477
function readdirSyncRecursive(basePath, options) {
1382-
const withFileTypes = Boolean(options.withFileTypes);
1383-
const encoding = options.encoding;
1384-
1385-
const readdirResults = [];
1386-
const pathsQueue = [basePath];
1478+
const context = {
1479+
withFileTypes: Boolean(options.withFileTypes),
1480+
encoding: options.encoding,
1481+
basePath,
1482+
readdirResults: [],
1483+
pathsQueue: [basePath],
1484+
};
13871485

13881486
function read(path) {
13891487
const readdirResult = binding.readdir(
13901488
path,
1391-
encoding,
1392-
withFileTypes,
1489+
context.encoding,
1490+
context.withFileTypes,
13931491
);
13941492

13951493
if (readdirResult === undefined) {
13961494
return;
13971495
}
13981496

1399-
if (withFileTypes) {
1400-
// Calling `readdir` with `withFileTypes=true`, the result is an array of arrays.
1401-
// The first array is the names, and the second array is the types.
1402-
// They are guaranteed to be the same length; hence, setting `length` to the length
1403-
// of the first array within the result.
1404-
const length = readdirResult[0].length;
1405-
for (let i = 0; i < length; i++) {
1406-
// Avoid excluding symlinks, as they are not directories.
1407-
// Refs: https://github.com/nodejs/node/issues/52663
1408-
const stat = binding.internalModuleStat(binding, pathModule.join(path, readdirResult[0][i]));
1409-
const dirent = getDirent(path, readdirResult[0][i], readdirResult[1][i]);
1410-
ArrayPrototypePush(readdirResults, dirent);
1411-
if (dirent.isDirectory() || stat === 1) {
1412-
ArrayPrototypePush(pathsQueue, pathModule.join(dirent.parentPath, dirent.name));
1413-
}
1414-
}
1415-
} else {
1416-
for (let i = 0; i < readdirResult.length; i++) {
1417-
const resultPath = pathModule.join(path, readdirResult[i]);
1418-
const relativeResultPath = pathModule.relative(basePath, resultPath);
1419-
const stat = binding.internalModuleStat(binding, resultPath);
1420-
ArrayPrototypePush(readdirResults, relativeResultPath);
1421-
// 1 indicates directory
1422-
if (stat === 1) {
1423-
ArrayPrototypePush(pathsQueue, resultPath);
1424-
}
1425-
}
1426-
}
1497+
processReaddirResult({
1498+
result: readdirResult,
1499+
currentPath: path,
1500+
context,
1501+
});
14271502
}
14281503

1429-
for (let i = 0; i < pathsQueue.length; i++) {
1430-
read(pathsQueue[i]);
1504+
for (let i = 0; i < context.pathsQueue.length; i++) {
1505+
read(context.pathsQueue[i]);
14311506
}
14321507

1433-
return readdirResults;
1508+
return context.readdirResults;
14341509
}
14351510

14361511
/**
@@ -1456,7 +1531,7 @@ function readdir(path, options, callback) {
14561531
}
14571532

14581533
if (options.recursive) {
1459-
callback(null, readdirSyncRecursive(path, options));
1534+
readdirRecursive(path, options, callback);
14601535
return;
14611536
}
14621537

test/fixtures/permission/fs-read.js

+5
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ const regularFile = __filename;
289289
permission: 'FileSystemRead',
290290
resource: path.toNamespacedPath(blockedFolder),
291291
}));
292+
fs.readdir(blockedFolder, { recursive: true }, common.expectsError({
293+
code: 'ERR_ACCESS_DENIED',
294+
permission: 'FileSystemRead',
295+
resource: path.toNamespacedPath(blockedFolder),
296+
}));
292297
assert.throws(() => {
293298
fs.readdirSync(blockedFolder);
294299
}, common.expectsError({

0 commit comments

Comments
 (0)