|
2 | 2 |
|
3 | 3 | const { |
4 | 4 | ArrayFrom, |
| 5 | + ArrayPrototypePop, |
5 | 6 | ArrayPrototypePush, |
6 | 7 | DateNow, |
7 | 8 | SafeMap, |
@@ -612,59 +613,73 @@ class MemoryProvider extends VirtualProvider { |
612 | 613 | */ |
613 | 614 | #readdirRecursive(dirEntry, dirPath, withFileTypes) { |
614 | 615 | 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). |
615 | 618 | const active = new SafeSet(); |
616 | 619 |
|
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; |
620 | 645 | } |
621 | 646 |
|
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; |
644 | 650 |
|
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; |
661 | 659 | } |
662 | | - } finally { |
663 | | - active.delete(entry); |
| 660 | + ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath)); |
| 661 | + } else { |
| 662 | + ArrayPrototypePush(results, childRelative); |
664 | 663 | } |
665 | | - }; |
666 | 664 |
|
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 | + |
668 | 683 | return results; |
669 | 684 | } |
670 | 685 |
|
|
0 commit comments