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

const fsWithSymlinksToPrefixedSiblings = {
"/prefixed/root": {
src: mock.symlink({
path: resolveSymlinkRoot("/prefixed/shared"),
}),
},
"/prefixed/shared": {
alias: mock.symlink({
path: resolveSymlinkRoot("/prefixed/shared-other"),
}),
},
"/prefixed/shared-other": {
"file-1": "file contents",
},
};

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

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

test(`resolve a symlink to a sibling whose name starts the same (real paths: false)`, async (t) => {
const api = new fdir()
.withErrors()
.withSymlinks({ resolvePaths: false })
.crawl("/prefixed/root");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize(["/prefixed/root/src/alias/file-1"])
);
});

test(`do not resolve symlinks`, async (t) => {
const api = new fdir().withErrors().crawl("/some/dir");
const files = await api[type]();
Expand Down
16 changes: 12 additions & 4 deletions src/api/functions/resolve-symlink.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Stats } from "fs";
import { WalkerState, Options } from "../../types";
import { dirname } from "path";
import { dirname, normalize, sep } from "path";

export type ResolveSymlinkFunction = (
path: string,
Expand Down Expand Up @@ -72,22 +72,30 @@ function isRecursive(path: string, resolved: string, state: WalkerState) {
if (state.options.useRealPaths)
return isRecursiveUsingRealPaths(resolved, state);

// A custom fs can return `/` on Windows, and isInside compares with the native separator
resolved = normalize(resolved);
let parent = dirname(path);
let depth = 1;
while (parent !== state.root && depth < 2) {
const resolvedPath = state.symlinks.get(parent);
const isSameRoot =
!!resolvedPath &&
(resolvedPath === resolved ||
resolvedPath.startsWith(resolved) ||
resolved.startsWith(resolvedPath));
(isInside(resolvedPath, resolved) || isInside(resolved, resolvedPath));
if (isSameRoot) depth++;
else parent = dirname(parent);
}
state.symlinks.set(path, resolved);
return depth > 1;
}

// Compares whole path segments, so `/a/b-c` isn't inside `/a/b`
function isInside(path: string, directory: string) {
return (
path === directory ||
path.startsWith(directory.endsWith(sep) ? directory : directory + sep)
);
}

function isRecursiveUsingRealPaths(resolved: string, state: WalkerState) {
return state.visited.includes(resolved + state.options.pathSeparator);
}
Loading