Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 60 additions & 45 deletions lib/internal/vfs/providers/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ArrayFrom,
ArrayPrototypePop,
ArrayPrototypePush,
DateNow,
SafeMap,
Expand Down Expand Up @@ -612,59 +613,73 @@ class MemoryProvider extends VirtualProvider {
*/
#readdirRecursive(dirEntry, dirPath, withFileTypes) {
const results = [];
// Directories on the current traversal path. A directory reached again
// through a symlink cycle is not descended into (but is still listed).
const active = new SafeSet();

const walk = (entry, currentPath, relativePath) => {
if (active.has(entry)) {
return;
// Traverse depth-first with an explicit stack instead of recursion, so a
// deeply nested tree cannot exhaust the call stack. Each frame is a
// directory being walked together with a snapshot of its children and the
// index of the next child to visit.
const enter = (entry, currentPath, relativePath) => {
this.#ensurePopulated(entry, currentPath);
active.add(entry);
ArrayPrototypePush(stack, {
entry,
currentPath,
relativePath,
children: ArrayFrom(entry.children),
index: 0,
});
};

const stack = [];
enter(dirEntry, dirPath, '');

while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.index >= frame.children.length) {
active.delete(frame.entry);
ArrayPrototypePop(stack);
continue;
}

active.add(entry);
try {
this.#ensurePopulated(entry, currentPath);

for (const { 0: name, 1: childEntry } of entry.children) {
const childRelative = relativePath ?
relativePath + '/' + name : name;

if (withFileTypes) {
let type;
if (childEntry.isSymbolicLink()) {
type = UV_DIRENT_LINK;
} else if (childEntry.isDirectory()) {
type = UV_DIRENT_DIR;
} else {
type = UV_DIRENT_FILE;
}
ArrayPrototypePush(results,
new Dirent(childRelative, type, dirPath));
} else {
ArrayPrototypePush(results, childRelative);
}
const { 0: name, 1: childEntry } = frame.children[frame.index++];
const childRelative = frame.relativePath ?
frame.relativePath + '/' + name : name;

// Follow symlinks to directories for recursive traversal.
// Track the active traversal path to avoid symlink cycles.
let resolvedChild = childEntry;
if (childEntry.isSymbolicLink()) {
const targetPath = this.#resolveSymlinkTarget(
pathPosix.join(currentPath, name), childEntry.target,
);
const result = this.#lookupEntry(targetPath, true, 0);
if (result.entry) {
resolvedChild = result.entry;
}
}
if (resolvedChild.isDirectory()) {
const childPath = pathPosix.join(currentPath, name);
walk(resolvedChild, childPath, childRelative);
}
if (withFileTypes) {
let type;
if (childEntry.isSymbolicLink()) {
type = UV_DIRENT_LINK;
} else if (childEntry.isDirectory()) {
type = UV_DIRENT_DIR;
} else {
type = UV_DIRENT_FILE;
}
} finally {
active.delete(entry);
ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath));
} else {
ArrayPrototypePush(results, childRelative);
}
};

walk(dirEntry, dirPath, '');
// Follow symlinks to directories for recursive traversal, skipping any
// directory already on the active path to avoid symlink cycles.
let resolvedChild = childEntry;
if (childEntry.isSymbolicLink()) {
const targetPath = this.#resolveSymlinkTarget(
pathPosix.join(frame.currentPath, name), childEntry.target,
);
const result = this.#lookupEntry(targetPath, true, 0);
if (result.entry) {
resolvedChild = result.entry;
}
}
if (resolvedChild.isDirectory() && !active.has(resolvedChild)) {
enter(resolvedChild, pathPosix.join(frame.currentPath, name),
childRelative);
}
}

return results;
}

Expand Down