Skip to content
Closed
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
30 changes: 30 additions & 0 deletions packages/core/src/utils/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,36 @@ describe('resolveToRealPath', () => {
expect(resolveToRealPath(input)).toBe(expected);
});

it('should return input path even if fs.realpathSync fails with ENAMETOOLONG', () => {
// Regression test for #26010: pasting long strings that the at-mention
// regex misinterprets as a path must not throw an unhandled ENAMETOOLONG
// up to the caller.
vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => {
const err = new Error('name too long') as NodeJS.ErrnoException;
err.code = 'ENAMETOOLONG';
throw err;
});

const longInput = path.resolve('/tmp', 'a'.repeat(4096));

expect(resolveToRealPath(longInput)).toBe(longInput);
});

it('should return input path even if fs.realpathSync fails with ENOTDIR', () => {
// Regression test for #26010: an intermediate path component being a
// regular file (e.g. @path/to/file.json/extra) surfaces ENOTDIR from
// realpathSync — handle it the same way as a non-existent path.
vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => {
const err = new Error('not a directory') as NodeJS.ErrnoException;
err.code = 'ENOTDIR';
throw err;
});

const input = path.resolve('/tmp', 'file.json', 'extra');

expect(resolveToRealPath(input)).toBe(input);
});

it('should recursively resolve symlinks for non-existent child paths', () => {
const parentPath = path.resolve('/some/parent/path');
const resolvedParentPath = path.resolve('/resolved/parent/path');
Expand Down
32 changes: 18 additions & 14 deletions packages/core/src/utils/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ export function resolveToRealPath(pathStr: string): string {
return robustRealpath(path.resolve(resolvedPath));
}

function hasErrorCode(e: unknown, codes: readonly string[]): boolean {
if (!e || typeof e !== 'object' || !('code' in e)) {
return false;
}
const code = e.code;
return typeof code === 'string' && codes.includes(code);
}

function robustRealpath(p: string, visited = new Set<string>()): string {
const key = process.platform === 'win32' ? p.toLowerCase() : p;
if (visited.has(key)) {
Expand All @@ -421,12 +429,7 @@ function robustRealpath(p: string, visited = new Set<string>()): string {
try {
return fs.realpathSync(p);
} catch (e: unknown) {
if (
e &&
typeof e === 'object' &&
'code' in e &&
(e.code === 'ENOENT' || e.code === 'EISDIR')
) {
if (hasErrorCode(e, ['ENOENT', 'EISDIR'])) {
try {
const stat = fs.lstatSync(p);
if (stat.isSymbolicLink()) {
Expand All @@ -437,21 +440,22 @@ function robustRealpath(p: string, visited = new Set<string>()): string {
} catch (lstatError: unknown) {
// Not a symlink, or lstat failed. Re-throw if it's not an expected
// ENOENT (e.g., a permissions error), otherwise resolve parent.
if (
!(
lstatError &&
typeof lstatError === 'object' &&
'code' in lstatError &&
(lstatError.code === 'ENOENT' || lstatError.code === 'EISDIR')
)
) {
if (!hasErrorCode(lstatError, ['ENOENT', 'EISDIR'])) {
throw lstatError;
}
}
const parent = path.dirname(p);
if (parent === p) return p;
return path.join(robustRealpath(parent, visited), path.basename(p));
}
if (hasErrorCode(e, ['ENAMETOOLONG', 'ENOTDIR'])) {
// The input is not a resolvable real path — for example a caller
// is probing whether a pasted string is a file reference (see the
// @-mention path parsing in atCommandProcessor). Returning the
// input unchanged lets downstream fileExists / fs.stat calls
// reject it normally instead of surfacing an unhandled rejection.
return p;
}
throw e;
}
}
Expand Down