Skip to content

Commit

Permalink
fix: handle recursive symlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
thecodrr committed Oct 14, 2024
1 parent 2c7df00 commit 189b0ec
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 77 deletions.
122 changes: 72 additions & 50 deletions src/api/functions/resolve-symlink.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import fs from "fs";
import { WalkerState, Options } from "../../types";
import { dirname } from "path";
import { readlink } from "fs/promises";

export type ResolveSymlinkFunction = (
path: string,
Expand All @@ -14,70 +16,50 @@ const resolveSymlinksAsync: ResolveSymlinkFunction = function (
) {
const {
queue,
options: { suppressErrors },
options: { suppressErrors, useRealPaths },
} = state;
queue.enqueue();

fs.stat(path, (error, stat) => {
if (error) {
queue.dequeue(suppressErrors ? null : error, state);
return;
}
fs.realpath(path, (error, resolvedPath) => {
if (error) return queue.dequeue(suppressErrors ? null : error, state);

fs.stat(resolvedPath, (error, stat) => {
if (error) return queue.dequeue(suppressErrors ? null : error, state);

callback(stat, path);
queue.dequeue(null, state);
if (stat.isDirectory()) {
isRecursiveAsync(path, resolvedPath, state).then((isRecursive) => {
if (isRecursive) return queue.dequeue(null, state);
callback(stat, useRealPaths ? resolvedPath : path);
queue.dequeue(null, state);
});
} else {
callback(stat, useRealPaths ? resolvedPath : path);
queue.dequeue(null, state);
}
});
});
};

const resolveSymlinksWithRealPathsAsync: ResolveSymlinkFunction = function (
const resolveSymlinks: ResolveSymlinkFunction = function (
path,
state,
callback
) {
const {
queue,
options: { suppressErrors },
options: { suppressErrors, useRealPaths },
} = state;
queue.enqueue();

fs.realpath(path, (error, resolvedPath) => {
if (error) {
queue.dequeue(suppressErrors ? null : error, state);
return;
}

fs.lstat(resolvedPath, (_error, stat) => {
callback(stat, resolvedPath);

queue.dequeue(null, state);
});
});
};

const resolveSymlinksSync: ResolveSymlinkFunction = function (
path,
state,
callback
) {
try {
const stat = fs.statSync(path);
callback(stat, path);
} catch (e) {
if (!state.options.suppressErrors) throw e;
}
};

const resolveSymlinksWithRealPathsSync: ResolveSymlinkFunction = function (
path,
state,
callback
) {
try {
const resolvedPath = fs.realpathSync(path);
const stat = fs.lstatSync(resolvedPath);
callback(stat, resolvedPath);
const stat = fs.statSync(resolvedPath);

if (stat.isDirectory() && isRecursive(path, resolvedPath, state)) return;

callback(stat, useRealPaths ? resolvedPath : path);
} catch (e) {
if (!state.options.suppressErrors) throw e;
if (!suppressErrors) throw e;
}
};

Expand All @@ -87,9 +69,49 @@ export function build(
): ResolveSymlinkFunction | null {
if (!options.resolveSymlinks || options.excludeSymlinks) return null;

if (options.useRealPaths)
return isSynchronous
? resolveSymlinksWithRealPathsSync
: resolveSymlinksWithRealPathsAsync;
return isSynchronous ? resolveSymlinksSync : resolveSymlinksAsync;
return isSynchronous ? resolveSymlinks : resolveSymlinksAsync;
}

async function isRecursiveAsync(
path: string,
resolved: string,
state: WalkerState
) {
if (state.options.useRealPaths)
return isRecursiveUsingRealPaths(resolved, state);

let parent = dirname(path);
if (parent + state.options.pathSeparator === state.root || parent === path)
return false;
try {
const resolvedParent =
state.symlinks.get(parent) || (await readlink(parent));
if (resolvedParent !== resolved) return false;
state.symlinks.set(path, resolved);
return true;
} catch (e) {
return isRecursiveAsync(parent, resolved, state);
}
}

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

function isRecursive(path: string, resolved: string, state: WalkerState) {
if (state.options.useRealPaths)
return isRecursiveUsingRealPaths(resolved, state);

let parent = dirname(path);
if (parent + state.options.pathSeparator === state.root || parent === path)
return false;
try {
const resolvedParent =
state.symlinks.get(parent) || fs.readlinkSync(parent);
if (resolvedParent !== resolved) return false;
state.symlinks.set(path, resolved);
return true;
} catch (e) {
return isRecursive(parent, resolved, state);
}
}
28 changes: 10 additions & 18 deletions src/api/functions/walk-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,19 @@ const walkAsync: WalkDirectoryFunction = (
currentDepth,
callback
) => {
state.queue.enqueue();

if (currentDepth < 0) {
state.queue.dequeue(null, state);
return;
}
if (currentDepth < 0) return;

state.visited.push(directoryPath);
state.counts.directories++;
state.queue.enqueue();

// Perf: Node >= 10 introduced withFileTypes that helps us
// skip an extra fs.stat call.
fs.readdir(
directoryPath || ".",
readdirOpts,
function process(error, entries = []) {
callback(entries, directoryPath, currentDepth);

state.queue.dequeue(state.options.suppressErrors ? null : error, state);
}
);
fs.readdir(directoryPath || ".", readdirOpts, (error, entries = []) => {
callback(entries, directoryPath, currentDepth);

state.queue.dequeue(state.options.suppressErrors ? null : error, state);
});
};

const walkSync: WalkDirectoryFunction = (
Expand All @@ -44,9 +37,8 @@ const walkSync: WalkDirectoryFunction = (
currentDepth,
callback
) => {
if (currentDepth < 0) {
return;
}
if (currentDepth < 0) return;
state.visited.push(directoryPath);
state.counts.directories++;

let entries: fs.Dirent[] = [];
Expand Down
6 changes: 4 additions & 2 deletions src/api/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export class Walker<TOutput extends Output> {
this.isSynchronous = !callback;
this.callbackInvoker = invokeCallback.build(options, this.isSynchronous);

this.root = normalizePath(root, options);
this.state = {
root: this.root,
// Perf: we explicitly tell the compiler to optimize for String arrays
paths: [""].slice(0, 0),
groups: [],
Expand All @@ -44,10 +46,10 @@ export class Walker<TOutput extends Output> {
queue: new Queue((error, state) =>
this.callbackInvoker(state, error, callback)
),
symlinks: new Map(),
visited: [""].slice(0, 0),
};

this.root = normalizePath(root, this.state.options);

/*
* Perf: We conditionally change functions according to options. This gives a slight
* performance boost. Since these functions are so small, they are automatically inlined
Expand Down
22 changes: 15 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ export type PathsOutput = string[];
export type Output = OnlyCountsOutput | PathsOutput | GroupOutput;

export type WalkerState = {
root: string;
paths: string[];
groups: Group[];
counts: Counts;
options: Options;
queue: Queue;

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

export type ResultCallback<TOutput extends Output> = (
Expand Down Expand Up @@ -60,13 +64,17 @@ export type Options<TGlobFunction = unknown> = {
relativePaths?: boolean;
pathSeparator: PathSeparator;
signal?: AbortSignal;
globFunction?: TGlobFunction
globFunction?: TGlobFunction;
};

export type GlobMatcher = (test: string) => boolean;
export type GlobFunction =
((glob: string | string[], ...params: unknown[]) => GlobMatcher);
export type GlobParams<T> =
T extends (globs: string|string[], ...params: infer TParams extends unknown[]) => GlobMatcher
? TParams
: [];
export type GlobFunction = (
glob: string | string[],
...params: unknown[]
) => GlobMatcher;
export type GlobParams<T> = T extends (
globs: string | string[],
...params: infer TParams extends unknown[]
) => GlobMatcher
? TParams
: [];

0 comments on commit 189b0ec

Please sign in to comment.