Skip to content

Commit d2007ae

Browse files
RafaelGSSruyadorno
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 28a11ad commit d2007ae

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
@@ -1372,6 +1372,102 @@ function mkdirSync(path, options) {
13721372
}
13731373
}
13741374

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

13901488
function read(path) {
13911489
const readdirResult = binding.readdir(
13921490
path,
1393-
encoding,
1394-
withFileTypes,
1491+
context.encoding,
1492+
context.withFileTypes,
13951493
);
13961494

13971495
if (readdirResult === undefined) {
13981496
return;
13991497
}
14001498

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

1431-
for (let i = 0; i < pathsQueue.length; i++) {
1432-
read(pathsQueue[i]);
1506+
for (let i = 0; i < context.pathsQueue.length; i++) {
1507+
read(context.pathsQueue[i]);
14331508
}
14341509

1435-
return readdirResults;
1510+
return context.readdirResults;
14361511
}
14371512

14381513
/**
@@ -1458,7 +1533,7 @@ function readdir(path, options, callback) {
14581533
}
14591534

14601535
if (options.recursive) {
1461-
callback(null, readdirSyncRecursive(path, options));
1536+
readdirRecursive(path, options, callback);
14621537
return;
14631538
}
14641539

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)