fix: handle ENAMETOOLONG gracefully during path resolution

This fix was recovered from a timed-out bot run. It addresses issue #26979
where the CLI would crash if a user provided an extremely long path string
in an @ command (e.g. @/aaa...a).

Changes:
- Updated 'robustRealpath' in 'packages/core/src/utils/paths.ts' to catch
  and gracefully handle 'ENAMETOOLONG' and 'EINVAL' errors from fs.realpathSync
  and fs.lstatSync.
- Added a defensive try-catch block to 'checkPermissions' in
  'packages/cli/src/ui/hooks/atCommandProcessor.ts' to prevent long path
  strings from crashing the CLI during @ command parsing.
- Added regression unit tests to verify the fix.
This commit is contained in:
Christian Gunderman
2026-05-13 20:24:15 -07:00
parent 98781cd97d
commit 5dc5b4ed4a
4 changed files with 50 additions and 5 deletions
@@ -17,6 +17,7 @@ import {
handleAtCommand,
escapeAtSymbols,
unescapeLiteralAt,
checkPermissions,
} from './atCommandProcessor.js';
import {
FileDiscoveryService,
@@ -1539,4 +1540,23 @@ describe('unescapeLiteralAt', () => {
const input = 'user@example.com and @scope/pkg';
expect(unescapeLiteralAt(escapeAtSymbols(input))).toBe(input);
});
describe('checkPermissions', () => {
it('should handle ENAMETOOLONG gracefully in checkPermissions', async () => {
const longPath = 'a'.repeat(5000);
const query = `@${longPath}`;
const localMockConfig = {
getTargetDir: () => '.',
validatePathAccess: () => true,
getResourceRegistry: () => ({
findResourceByUri: () => undefined,
}),
} as unknown as Config;
// checkPermissions should not throw ENAMETOOLONG
const permissions = await checkPermissions(query, localMockConfig);
expect(permissions).toEqual([]);
});
});
});
@@ -188,9 +188,15 @@ export async function checkPermissions(
const pathName = part.content.substring(1);
if (!pathName) continue;
const resolvedPathName = resolveToRealPath(
path.resolve(config.getTargetDir(), pathName),
);
let resolvedPathName: string;
try {
resolvedPathName = resolveToRealPath(
path.resolve(config.getTargetDir(), pathName),
);
} catch {
// If path resolution fails (e.g. ENAMETOOLONG), skip this path
continue;
}
if (config.validatePathAccess(resolvedPathName, 'read')) {
if (await fileExists(resolvedPathName)) {
+13
View File
@@ -602,6 +602,19 @@ describe('resolveToRealPath', () => {
/Infinite recursion detected/,
);
});
it('should handle ENAMETOOLONG gracefully', () => {
const longPath = path.resolve('/' + 'a'.repeat(5000));
vi.spyOn(fs, 'realpathSync').mockImplementation(() => {
const err = new Error('ENAMETOOLONG') as NodeJS.ErrnoException;
err.code = 'ENAMETOOLONG';
throw err;
});
// Should return the path itself if realpathSync fails with ENAMETOOLONG
expect(resolveToRealPath(longPath)).toBe(longPath);
});
});
describe('makeRelative', () => {
+8 -2
View File
@@ -440,7 +440,10 @@ function robustRealpath(p: string, visited = new Set<string>()): string {
e &&
typeof e === 'object' &&
'code' in e &&
(e.code === 'ENOENT' || e.code === 'EISDIR')
(e.code === 'ENOENT' ||
e.code === 'EISDIR' ||
e.code === 'ENAMETOOLONG' ||
e.code === 'EINVAL')
) {
try {
const stat = fs.lstatSync(p);
@@ -457,7 +460,10 @@ function robustRealpath(p: string, visited = new Set<string>()): string {
lstatError &&
typeof lstatError === 'object' &&
'code' in lstatError &&
(lstatError.code === 'ENOENT' || lstatError.code === 'EISDIR')
(lstatError.code === 'ENOENT' ||
lstatError.code === 'EISDIR' ||
lstatError.code === 'ENAMETOOLONG' ||
lstatError.code === 'EINVAL')
)
) {
throw lstatError;