fix(core): handle URI-encoded workspace paths in IdeClient (#17476)

Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
This commit is contained in:
Dongjun Shin
2026-01-27 02:09:43 +09:00
committed by GitHub
parent 93c62a2bdc
commit 4827333c48
4 changed files with 186 additions and 19 deletions
+88
View File
@@ -1131,4 +1131,92 @@ describe('getIdeServerHost', () => {
'/run/.containerenv',
); // Short-circuiting
});
describe('validateWorkspacePath', () => {
describe('with special characters and encoding', () => {
it('should return true for a URI-encoded path with spaces', () => {
const workspacePath = 'file:///test/my%20workspace';
const cwd = '/test/my workspace/sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
});
it('should return true for a URI-encoded path with Korean characters', () => {
const workspacePath = 'file:///test/%ED%85%8C%EC%8A%A4%ED%8A%B8'; // "테스트"
const cwd = '/test/테스트/sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
});
it('should return true for a plain decoded path with Korean characters', () => {
const workspacePath = '/test/테스트';
const cwd = '/test/테스트/sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
});
it('should return true when one of multi-root paths is a valid URI-encoded path', () => {
const workspacePath = [
'/another/workspace',
'file:///test/%ED%85%8C%EC%8A%A4%ED%8A%B8', // "테스트"
].join(path.delimiter);
const cwd = '/test/테스트/sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
});
it('should return true for paths containing a literal % sign', () => {
const workspacePath = '/test/a%path';
const cwd = '/test/a%path/sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
});
it.skipIf(process.platform !== 'win32')(
'should correctly convert a Windows file URI',
() => {
const workspacePath = 'file:///C:\\Users\\test';
const cwd = 'C:\\Users\\test\\sub-dir';
const result = IdeClient.validateWorkspacePath(workspacePath, cwd);
expect(result.isValid).toBe(true);
},
);
});
});
describe('validateWorkspacePath (sanitization)', () => {
it.each([
{
description: 'should return true for identical paths',
workspacePath: '/test/ws',
cwd: '/test/ws',
expectedValid: true,
},
{
description: 'should return true when workspace has file:// protocol',
workspacePath: 'file:///test/ws',
cwd: '/test/ws',
expectedValid: true,
},
{
description: 'should return true when workspace has encoded spaces',
workspacePath: '/test/my%20ws',
cwd: '/test/my ws',
expectedValid: true,
},
{
description:
'should return true when cwd needs normalization matching workspace',
workspacePath: '/test/my ws',
cwd: '/test/my%20ws',
expectedValid: true,
},
])('$description', ({ workspacePath, cwd, expectedValid }) => {
expect(IdeClient.validateWorkspacePath(workspacePath, cwd)).toMatchObject(
{ isValid: expectedValid },
);
});
});
});
+9 -17
View File
@@ -5,7 +5,7 @@
*/
import * as fs from 'node:fs';
import { isSubpath } from '../utils/paths.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { detectIde, type IdeInfo } from '../ide/detect-ide.js';
import { ideContextStore } from './ideContext.js';
import {
@@ -65,16 +65,6 @@ type ConnectionConfig = {
stdio?: StdioConfig;
};
function getRealPath(path: string): string {
try {
return fs.realpathSync(path);
} catch (_e) {
// If realpathSync fails, it might be because the path doesn't exist.
// In that case, we can fall back to the original path.
return path;
}
}
/**
* Manages the connection to and interaction with the IDE server.
*/
@@ -521,12 +511,14 @@ export class IdeClient {
};
}
const ideWorkspacePaths = ideWorkspacePath.split(path.delimiter);
const realCwd = getRealPath(cwd);
const isWithinWorkspace = ideWorkspacePaths.some((workspacePath) => {
const idePath = getRealPath(workspacePath);
return isSubpath(idePath, realCwd);
});
const ideWorkspacePaths = ideWorkspacePath
.split(path.delimiter)
.map((p) => resolveToRealPath(p))
.filter((e) => !!e);
const realCwd = resolveToRealPath(cwd);
const isWithinWorkspace = ideWorkspacePaths.some((workspacePath) =>
isSubpath(workspacePath, realCwd),
);
if (!isWithinWorkspace) {
return {