fix
This commit is contained in:
parent
69adf3488e
commit
228e8868ed
2 changed files with 42 additions and 7 deletions
|
|
@ -68,14 +68,21 @@ function needsShell(binaryPath: string): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Minimal quoting for command‑line arguments when building a shell
|
||||
* invocation. We only escape spaces and double quotes since our
|
||||
* callers only ever use simple strings (paths, flags, literals) and
|
||||
* the shell itself will handle most quoting rules.
|
||||
* Quote an argument for cmd.exe shell invocation on Windows.
|
||||
*
|
||||
* cmd.exe rules:
|
||||
* - Double-quote args containing spaces or special characters
|
||||
* - Inside double quotes, escape literal `"` as `""`
|
||||
* - `%` is expanded as env var even inside double quotes — escape as `%%`
|
||||
* - `^`, `&`, `|`, `<`, `>` are safe inside double quotes
|
||||
*
|
||||
* Our callers only pass controlled strings (binary paths, CLI flags),
|
||||
* NOT arbitrary user input.
|
||||
*/
|
||||
function quoteArg(arg: string): string {
|
||||
if (/[^A-Za-z0-9_\-/.]/.test(arg)) {
|
||||
return `"${arg.replace(/"/g, '\\"')}"`;
|
||||
const escaped = arg.replace(/%/g, '%%').replace(/"/g, '""');
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
|
@ -133,7 +140,7 @@ export function spawnCli(
|
|||
if (process.platform === 'win32' && needsShell(binaryPath)) {
|
||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
||||
return spawn(cmd, { shell: true, ...options });
|
||||
return spawn(cmd, { ...options, shell: true });
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -144,7 +151,7 @@ export function spawnCli(
|
|||
if (process.platform === 'win32' && code === 'EINVAL') {
|
||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
||||
return spawn(cmd, { shell: true, ...options });
|
||||
return spawn(cmd, { ...options, shell: true });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,34 @@ describe('cli child process helpers', () => {
|
|||
expect(result.stdout).toBe('1.2.3');
|
||||
});
|
||||
|
||||
it('escapes percent signs and quotes for cmd.exe in shell fallback', async () => {
|
||||
setPlatform('win32');
|
||||
const execMock = child.exec as unknown as vi.Mock;
|
||||
execMock.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
|
||||
cb(null, 'ok', '');
|
||||
return {} as any;
|
||||
});
|
||||
|
||||
await execCli('C:\\Users\\Алексей\\bin\\claude.cmd', ['--model', 'test%PATH%"arg']);
|
||||
const shellCmd = execMock.mock.calls[0][0] as string;
|
||||
// %PATH% must become %%PATH%% to prevent cmd.exe env var expansion
|
||||
expect(shellCmd).toContain('%%PATH%%');
|
||||
// double quote inside arg must become "" (cmd.exe escaping)
|
||||
expect(shellCmd).toContain('""arg');
|
||||
// should NOT contain \" (Unix-style escaping)
|
||||
expect(shellCmd).not.toContain('\\"');
|
||||
});
|
||||
|
||||
it('shell: true cannot be overridden by caller options', () => {
|
||||
setPlatform('win32');
|
||||
const spawnMock = child.spawn as unknown as vi.Mock;
|
||||
spawnMock.mockReturnValue({} as any);
|
||||
|
||||
spawnCli('C:\\Users\\Алексей\\bin\\claude.cmd', ['--version'], { shell: false } as any);
|
||||
// shell: true must win over caller's shell: false
|
||||
expect(spawnMock.mock.calls[0][1]).toMatchObject({ shell: true });
|
||||
});
|
||||
|
||||
it('falls back to shell when execFile throws EINVAL on windows', async () => {
|
||||
setPlatform('win32');
|
||||
const execFileMock = child.execFile as unknown as vi.Mock;
|
||||
|
|
|
|||
Loading…
Reference in a new issue