|
2 | 2 |
|
3 | 3 | const { |
4 | 4 | ArrayFrom, |
| 5 | + ArrayPrototypePop, |
5 | 6 | ArrayPrototypePush, |
6 | 7 | DateNow, |
7 | 8 | SafeMap, |
@@ -613,59 +614,73 @@ class MemoryProvider extends VirtualProvider { |
613 | 614 | */ |
614 | 615 | #readdirRecursive(dirEntry, dirPath, withFileTypes) { |
615 | 616 | const results = []; |
| 617 | + // Directories on the current traversal path. A directory reached again |
| 618 | + // through a symlink cycle is not descended into (but is still listed). |
616 | 619 | const active = new SafeSet(); |
617 | 620 |
|
618 | | - const walk = (entry, currentPath, relativePath) => { |
619 | | - if (active.has(entry)) { |
620 | | - return; |
| 621 | + // Traverse depth-first with an explicit stack instead of recursion, so a |
| 622 | + // deeply nested tree cannot exhaust the call stack. Each frame is a |
| 623 | + // directory being walked together with a snapshot of its children and the |
| 624 | + // index of the next child to visit. |
| 625 | + const enter = (entry, currentPath, relativePath) => { |
| 626 | + this.#ensurePopulated(entry, currentPath); |
| 627 | + active.add(entry); |
| 628 | + ArrayPrototypePush(stack, { |
| 629 | + entry, |
| 630 | + currentPath, |
| 631 | + relativePath, |
| 632 | + children: ArrayFrom(entry.children), |
| 633 | + index: 0, |
| 634 | + }); |
| 635 | + }; |
| 636 | + |
| 637 | + const stack = []; |
| 638 | + enter(dirEntry, dirPath, ''); |
| 639 | + |
| 640 | + while (stack.length > 0) { |
| 641 | + const frame = stack[stack.length - 1]; |
| 642 | + if (frame.index >= frame.children.length) { |
| 643 | + active.delete(frame.entry); |
| 644 | + ArrayPrototypePop(stack); |
| 645 | + continue; |
621 | 646 | } |
622 | 647 |
|
623 | | - active.add(entry); |
624 | | - try { |
625 | | - this.#ensurePopulated(entry, currentPath); |
626 | | - |
627 | | - for (const { 0: name, 1: childEntry } of entry.children) { |
628 | | - const childRelative = relativePath ? |
629 | | - relativePath + '/' + name : name; |
630 | | - |
631 | | - if (withFileTypes) { |
632 | | - let type; |
633 | | - if (childEntry.isSymbolicLink()) { |
634 | | - type = UV_DIRENT_LINK; |
635 | | - } else if (childEntry.isDirectory()) { |
636 | | - type = UV_DIRENT_DIR; |
637 | | - } else { |
638 | | - type = UV_DIRENT_FILE; |
639 | | - } |
640 | | - ArrayPrototypePush(results, |
641 | | - new Dirent(childRelative, type, dirPath)); |
642 | | - } else { |
643 | | - ArrayPrototypePush(results, childRelative); |
644 | | - } |
| 648 | + const { 0: name, 1: childEntry } = frame.children[frame.index++]; |
| 649 | + const childRelative = frame.relativePath ? |
| 650 | + frame.relativePath + '/' + name : name; |
645 | 651 |
|
646 | | - // Follow symlinks to directories for recursive traversal. |
647 | | - // Track the active traversal path to avoid symlink cycles. |
648 | | - let resolvedChild = childEntry; |
649 | | - if (childEntry.isSymbolicLink()) { |
650 | | - const targetPath = this.#resolveSymlinkTarget( |
651 | | - pathPosix.join(currentPath, name), childEntry.target, |
652 | | - ); |
653 | | - const result = this.#lookupEntry(targetPath, true, 0); |
654 | | - if (result.entry) { |
655 | | - resolvedChild = result.entry; |
656 | | - } |
657 | | - } |
658 | | - if (resolvedChild.isDirectory()) { |
659 | | - const childPath = pathPosix.join(currentPath, name); |
660 | | - walk(resolvedChild, childPath, childRelative); |
661 | | - } |
| 652 | + if (withFileTypes) { |
| 653 | + let type; |
| 654 | + if (childEntry.isSymbolicLink()) { |
| 655 | + type = UV_DIRENT_LINK; |
| 656 | + } else if (childEntry.isDirectory()) { |
| 657 | + type = UV_DIRENT_DIR; |
| 658 | + } else { |
| 659 | + type = UV_DIRENT_FILE; |
662 | 660 | } |
663 | | - } finally { |
664 | | - active.delete(entry); |
| 661 | + ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath)); |
| 662 | + } else { |
| 663 | + ArrayPrototypePush(results, childRelative); |
665 | 664 | } |
666 | | - }; |
667 | 665 |
|
668 | | - walk(dirEntry, dirPath, ''); |
| 666 | + // Follow symlinks to directories for recursive traversal, skipping any |
| 667 | + // directory already on the active path to avoid symlink cycles. |
| 668 | + let resolvedChild = childEntry; |
| 669 | + if (childEntry.isSymbolicLink()) { |
| 670 | + const targetPath = this.#resolveSymlinkTarget( |
| 671 | + pathPosix.join(frame.currentPath, name), childEntry.target, |
| 672 | + ); |
| 673 | + const result = this.#lookupEntry(targetPath, true, 0); |
| 674 | + if (result.entry) { |
| 675 | + resolvedChild = result.entry; |
| 676 | + } |
| 677 | + } |
| 678 | + if (resolvedChild.isDirectory() && !active.has(resolvedChild)) { |
| 679 | + enter(resolvedChild, pathPosix.join(frame.currentPath, name), |
| 680 | + childRelative); |
| 681 | + } |
| 682 | + } |
| 683 | + |
669 | 684 | return results; |
670 | 685 | } |
671 | 686 |
|
|
0 commit comments