Skip to content

Commit 56ef7a4

Browse files
committed
vfs: make recursive readdir iterative
MemoryProvider recursive readdir walked the directory tree with a recursive helper. Rewrite it to traverse iteratively with an explicit stack so a deeply nested tree can no longer exhaust the call stack. The set of directories on the active traversal path is still tracked, so a circular symlink stops descending while its entry remains listed; the output and observable behavior are unchanged. Refs: #64168 Signed-off-by: AkshatOP <hunterdevil0987@gmail.com>
1 parent b59def5 commit 56ef7a4

1 file changed

Lines changed: 60 additions & 45 deletions

File tree

lib/internal/vfs/providers/memory.js

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayFrom,
5+
ArrayPrototypePop,
56
ArrayPrototypePush,
67
DateNow,
78
SafeMap,
@@ -612,59 +613,73 @@ class MemoryProvider extends VirtualProvider {
612613
*/
613614
#readdirRecursive(dirEntry, dirPath, withFileTypes) {
614615
const results = [];
616+
// Directories on the current traversal path. A directory reached again
617+
// through a symlink cycle is not descended into (but is still listed).
615618
const active = new SafeSet();
616619

617-
const walk = (entry, currentPath, relativePath) => {
618-
if (active.has(entry)) {
619-
return;
620+
// Traverse depth-first with an explicit stack instead of recursion, so a
621+
// deeply nested tree cannot exhaust the call stack. Each frame is a
622+
// directory being walked together with a snapshot of its children and the
623+
// index of the next child to visit.
624+
const enter = (entry, currentPath, relativePath) => {
625+
this.#ensurePopulated(entry, currentPath);
626+
active.add(entry);
627+
ArrayPrototypePush(stack, {
628+
entry,
629+
currentPath,
630+
relativePath,
631+
children: ArrayFrom(entry.children),
632+
index: 0,
633+
});
634+
};
635+
636+
const stack = [];
637+
enter(dirEntry, dirPath, '');
638+
639+
while (stack.length > 0) {
640+
const frame = stack[stack.length - 1];
641+
if (frame.index >= frame.children.length) {
642+
active.delete(frame.entry);
643+
ArrayPrototypePop(stack);
644+
continue;
620645
}
621646

622-
active.add(entry);
623-
try {
624-
this.#ensurePopulated(entry, currentPath);
625-
626-
for (const { 0: name, 1: childEntry } of entry.children) {
627-
const childRelative = relativePath ?
628-
relativePath + '/' + name : name;
629-
630-
if (withFileTypes) {
631-
let type;
632-
if (childEntry.isSymbolicLink()) {
633-
type = UV_DIRENT_LINK;
634-
} else if (childEntry.isDirectory()) {
635-
type = UV_DIRENT_DIR;
636-
} else {
637-
type = UV_DIRENT_FILE;
638-
}
639-
ArrayPrototypePush(results,
640-
new Dirent(childRelative, type, dirPath));
641-
} else {
642-
ArrayPrototypePush(results, childRelative);
643-
}
647+
const { 0: name, 1: childEntry } = frame.children[frame.index++];
648+
const childRelative = frame.relativePath ?
649+
frame.relativePath + '/' + name : name;
644650

645-
// Follow symlinks to directories for recursive traversal.
646-
// Track the active traversal path to avoid symlink cycles.
647-
let resolvedChild = childEntry;
648-
if (childEntry.isSymbolicLink()) {
649-
const targetPath = this.#resolveSymlinkTarget(
650-
pathPosix.join(currentPath, name), childEntry.target,
651-
);
652-
const result = this.#lookupEntry(targetPath, true, 0);
653-
if (result.entry) {
654-
resolvedChild = result.entry;
655-
}
656-
}
657-
if (resolvedChild.isDirectory()) {
658-
const childPath = pathPosix.join(currentPath, name);
659-
walk(resolvedChild, childPath, childRelative);
660-
}
651+
if (withFileTypes) {
652+
let type;
653+
if (childEntry.isSymbolicLink()) {
654+
type = UV_DIRENT_LINK;
655+
} else if (childEntry.isDirectory()) {
656+
type = UV_DIRENT_DIR;
657+
} else {
658+
type = UV_DIRENT_FILE;
661659
}
662-
} finally {
663-
active.delete(entry);
660+
ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath));
661+
} else {
662+
ArrayPrototypePush(results, childRelative);
664663
}
665-
};
666664

667-
walk(dirEntry, dirPath, '');
665+
// Follow symlinks to directories for recursive traversal, skipping any
666+
// directory already on the active path to avoid symlink cycles.
667+
let resolvedChild = childEntry;
668+
if (childEntry.isSymbolicLink()) {
669+
const targetPath = this.#resolveSymlinkTarget(
670+
pathPosix.join(frame.currentPath, name), childEntry.target,
671+
);
672+
const result = this.#lookupEntry(targetPath, true, 0);
673+
if (result.entry) {
674+
resolvedChild = result.entry;
675+
}
676+
}
677+
if (resolvedChild.isDirectory() && !active.has(resolvedChild)) {
678+
enter(resolvedChild, pathPosix.join(frame.currentPath, name),
679+
childRelative);
680+
}
681+
}
682+
668683
return results;
669684
}
670685

0 commit comments

Comments
 (0)