From 5dc5b4ed4a24e07c1554eb50917b72082bd4b03f Mon Sep 17 00:00:00 2001 From: Christian Gunderman Date: Wed, 13 May 2026 20:24:15 -0700 Subject: [PATCH] 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. --- .../src/ui/hooks/atCommandProcessor.test.ts | 20 +++++++++++++++++++ .../cli/src/ui/hooks/atCommandProcessor.ts | 12 ++++++++--- packages/core/src/utils/paths.test.ts | 13 ++++++++++++ packages/core/src/utils/paths.ts | 10 ++++++++-- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index ca2ecf7bc1..8076d8a2be 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -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([]); + }); + }); }); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 512fe952ba..38cb541f1b 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -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)) { diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index bb2801a9ad..4d08c85548 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -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', () => { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index 70afe289fa..064db93a74 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -440,7 +440,10 @@ function robustRealpath(p: string, visited = new Set()): 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 { 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;