diff --git a/src/shared/quick-open-filter.test.ts b/src/shared/quick-open-filter.test.ts index ed6ea79a69f..06767dc9b37 100644 --- a/src/shared/quick-open-filter.test.ts +++ b/src/shared/quick-open-filter.test.ts @@ -75,6 +75,12 @@ describe('buildExcludePathPrefixes', () => { it('strips trailing slashes', () => { expect(buildExcludePathPrefixes('/r', ['/r/a/', '/r/b///'])).toEqual(['a', 'b']) }) + + it('keeps valid child prefixes whose segment starts with dotdot characters', () => { + expect(buildExcludePathPrefixes('/home/u/repo', ['/home/u/repo/..fixtures'])).toEqual([ + '..fixtures' + ]) + }) }) describe('shouldExcludeQuickOpenRelPath', () => { @@ -179,6 +185,12 @@ describe('normalizeQuickOpenRgLine', () => { expect(normalizeQuickOpenRgLine('./src/a.ts', { kind: 'cwd-relative' })).toBe('src/a.ts') }) + it('keeps cwd-relative files under dotdot-prefixed child directories', () => { + expect(normalizeQuickOpenRgLine('./..fixtures/a.ts', { kind: 'cwd-relative' })).toBe( + '..fixtures/a.ts' + ) + }) + it('strips CRLF', () => { expect(normalizeQuickOpenRgLine('/root/a.ts\r', { kind: 'absolute', rootPath: '/root' })).toBe( 'a.ts' @@ -195,6 +207,11 @@ describe('normalizeQuickOpenRgLine', () => { expect(normalizeQuickOpenRgLine('', { kind: 'cwd-relative' })).toBeNull() expect(normalizeQuickOpenRgLine('.', { kind: 'cwd-relative' })).toBeNull() }) + + it('returns null for cwd-relative parent-directory escapes', () => { + expect(normalizeQuickOpenRgLine('../outside/a.ts', { kind: 'cwd-relative' })).toBeNull() + expect(normalizeQuickOpenRgLine('./../outside/a.ts', { kind: 'cwd-relative' })).toBeNull() + }) }) describe('buildGitLsFilesArgsForQuickOpen', () => { diff --git a/src/shared/quick-open-filter.ts b/src/shared/quick-open-filter.ts index d5d927c2447..d1d5e1aa4d3 100644 --- a/src/shared/quick-open-filter.ts +++ b/src/shared/quick-open-filter.ts @@ -132,7 +132,7 @@ export function buildExcludePathPrefixes(rootPath: string, excludePaths?: unknow : // Fall back to path-flavor relative so we do not accidentally use the // local OS's semantics on remote paths. flavor.relative(trimmedRoot, raw).replace(/\\/g, '/') - if (!rel || rel.startsWith('..') || rel.startsWith('/')) { + if (!rel || isParentRelativePath(rel) || rel.startsWith('/')) { continue } // Strip any trailing slash so boundary checks are unambiguous. @@ -189,6 +189,11 @@ function escapeGlobPath(relPath: string): string { return relPath.split('/').map(escapeGlob).join('/') } +function isParentRelativePath(relPath: string): boolean { + // Why: `..name` is a valid child path; only `..` and `../...` escape. + return relPath === '..' || relPath.startsWith('../') +} + // ─── rg traversal-pruning globs ────────────────────────────────────── /** @@ -305,7 +310,7 @@ export function normalizeQuickOpenRgLine(rawLine: string, outputMode: RgOutputMo } else if (rel === '.') { return null } - if (!rel || rel.startsWith('/') || rel.startsWith('..')) { + if (!rel || rel.startsWith('/') || isParentRelativePath(rel)) { return null } return rel