Skip to content
Open
Show file tree
Hide file tree
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
32 changes: 32 additions & 0 deletions __tests__/symlinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,25 @@ const fsWithSymlinkInRootDir = {
}),
};

const fsWithManySymlinksToSameDir = {
"/shared/target": {
"file-1": "file contents",
nested: { "file-2": "file contents" },
},
"/many/links": Object.fromEntries(
Array.from({ length: 50 }, (_, i) => [
`link-${i}`,
mock.symlink({ path: resolveSymlinkRoot("/shared/target") }),
])
),
};

const mockFs = {
...fsWithRelativeSymlinks,
...fsWithRecursiveSymlinks,
...fsWithRecursiveRelativeSymlinks,
...fsWithSymlinkInRootDir,
...fsWithManySymlinksToSameDir,

"/sym/linked": {
"file-1": "file contents",
Expand Down Expand Up @@ -301,6 +315,24 @@ for (const type of apiTypes) {
);
});

test(`crawl a directory reached via many symlinks only once (real paths: true)`, async (t) => {
const api = new fdir().withErrors().withSymlinks().crawl("/many/links");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize(["/shared/target/file-1", "/shared/target/nested/file-2"])
);
});

test(`count a directory reached via many symlinks only once (real paths: true)`, async (t) => {
const api = new fdir()
.withErrors()
.withSymlinks()
.onlyCounts()
.crawl("/many/links");
const counts = await api[type]();
t.expect(counts.files).toBe(2);
});

test(`resolve symlinks (real paths: false)`, async (t) => {
const api = new fdir()
.withErrors()
Expand Down
3 changes: 2 additions & 1 deletion src/api/functions/resolve-symlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,6 @@ function isRecursive(path: string, resolved: string, state: WalkerState) {
}

function isRecursiveUsingRealPaths(resolved: string, state: WalkerState) {
return state.visited.includes(resolved + state.options.pathSeparator);
const { visited } = state;
return !!visited && visited.has(resolved + state.options.pathSeparator);
}
4 changes: 2 additions & 2 deletions src/api/functions/walk-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const walkAsync: WalkDirectoryFunction = (

const { fs } = state;

state.visited.push(crawlPath);
if (state.visited) state.visited.add(crawlPath);
state.counts.directories++;

// Perf: Node >= 10 introduced withFileTypes that helps us
Expand All @@ -45,7 +45,7 @@ const walkSync: WalkDirectoryFunction = (
) => {
const { fs } = state;
if (currentDepth < 0) return;
state.visited.push(crawlPath);
if (state.visited) state.visited.add(crawlPath);
state.counts.directories++;

let entries: Dirent[] = [];
Expand Down
4 changes: 3 additions & 1 deletion src/api/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export class Walker<TOutput extends Output> {
this.callbackInvoker(state, error, callback)
),
symlinks: new Map(),
visited: [""].slice(0, 0),
// Perf: only needed for recursion detection when resolving symlinks to
// real paths. A Set keeps that check O(1) instead of O(directories).
visited: options.useRealPaths ? new Set<string>() : undefined,
controller: new Aborter(),
fs: options.fs || nativeFs,
};
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type WalkerState = {
fs: FSLike;

symlinks: Map<string, string>;
visited: string[];
visited?: Set<string>;
};

export type ResultCallback<TOutput extends Output> = (
Expand Down