Skip to content

Commit 8faf918

Browse files
RafaelGSStargos
authored andcommitted
lib: fix fs.readdir recursive async
Fixes: #56006 PR-URL: #56041 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com>
1 parent b6005b3 commit 8faf918

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
@@ -1369,6 +1369,102 @@ function mkdirSync(path, options) {
13691369
}
13701370
}
13711371

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

13871485
function read(path) {
13881486
const readdirResult = binding.readdir(
13891487
path,
1390-
encoding,
1391-
withFileTypes,
1488+
context.encoding,
1489+
context.withFileTypes,
13921490
);
13931491

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

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

1428-
for (let i = 0; i < pathsQueue.length; i++) {
1429-
read(pathsQueue[i]);
1503+
for (let i = 0; i < context.pathsQueue.length; i++) {
1504+
read(context.pathsQueue[i]);
14301505
}
14311506

1432-
return readdirResults;
1507+
return context.readdirResults;
14331508
}
14341509

14351510
/**
@@ -1455,7 +1530,7 @@ function readdir(path, options, callback) {
14551530
}
14561531

14571532
if (options.recursive) {
1458-
callback(null, readdirSyncRecursive(path, options));
1533+
readdirRecursive(path, options, callback);
14591534
return;
14601535
}
14611536

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)