From eea859dd6f0b77dd6ee7225a1b6931abefde0096 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 20 Jan 2026 09:44:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(cli):=20=E4=BF=AE=E5=A4=8D=20Windows=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=8F=8D=E6=96=9C=E6=9D=A0=E8=A2=AB=E5=90=9E?= =?UTF-8?q?=E6=8E=89=E7=9A=84=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B7=A8=E5=B9=B3=E5=8F=B0=E8=B7=AF=E5=BE=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写 escapeWindowsArg 函数,正确处理反斜杠和引号转义 - 添加 escapeUnixArg 函数支持 Linux/macOS shell 转义 - 添加 normalizePathSeparators 函数自动转换路径分隔符 - 修复 vscode-lsp.ts 中的 TypeScript 类型错误 --- ccw/src/tools/vscode-lsp.ts | 4 +- ccw/src/utils/path-resolver.ts | 43 ++++++++++-- ccw/src/utils/shell-escape.ts | 121 ++++++++++++++++++++++++++++----- 3 files changed, 143 insertions(+), 25 deletions(-) diff --git a/ccw/src/tools/vscode-lsp.ts b/ccw/src/tools/vscode-lsp.ts index 0edbcf88..c9f3b72d 100644 --- a/ccw/src/tools/vscode-lsp.ts +++ b/ccw/src/tools/vscode-lsp.ts @@ -93,7 +93,7 @@ async function callVSCodeBridge( clearTimeout(timeoutId); if (!response.ok) { - const errorBody = await response.json(); + const errorBody = await response.json() as any; return { success: false, error: errorBody.error || `HTTP ${response.status}: ${response.statusText}`, @@ -101,7 +101,7 @@ async function callVSCodeBridge( }; } - const data = await response.json(); + const data = await response.json() as any; return { success: data.success !== false, result: data.result, diff --git a/ccw/src/utils/path-resolver.ts b/ccw/src/utils/path-resolver.ts index 25f3c22b..1001c6b9 100644 --- a/ccw/src/utils/path-resolver.ts +++ b/ccw/src/utils/path-resolver.ts @@ -21,25 +21,56 @@ export interface ValidatePathOptions { allowHome?: boolean; } +/** + * Normalize path separators to the native format for the current platform + * - Windows: converts / to \ + * - Linux/macOS: converts \ to / + * @param inputPath - Path with potentially mixed separators + * @returns Path with native separators + */ +export function normalizePathSeparators(inputPath: string): string { + if (!inputPath) return inputPath; + + if (process.platform === 'win32') { + // Windows: convert forward slashes to backslashes + return inputPath.replace(/\//g, '\\'); + } else { + // Linux/macOS: convert backslashes to forward slashes + // This handles Windows-style paths being used on Unix systems + return inputPath.replace(/\\/g, '/'); + } +} + /** * Resolve a path, handling ~ for home directory * Also handles Windows drive-relative paths (e.g., "D:path" -> "D:\path") - * @param inputPath - Path to resolve - * @returns Absolute path + * and normalizes mixed slashes for cross-platform compatibility + * + * Cross-platform behavior: + * - Windows: D:/path/to/file -> D:\path\to\file + * - Linux/macOS: /path\to/file -> /path/to/file + * + * @param inputPath - Path to resolve (can use / or \ on any platform) + * @returns Absolute path with native separators */ export function resolvePath(inputPath: string): string { if (!inputPath) return process.cwd(); - // Handle ~ for home directory + // Handle ~ for home directory (before normalizing separators) if (inputPath.startsWith('~')) { - return join(homedir(), inputPath.slice(1)); + const remainder = inputPath.slice(1); + return join(homedir(), normalizePathSeparators(remainder)); } + // Normalize path separators to native format + inputPath = normalizePathSeparators(inputPath); + // Handle Windows drive-relative paths (e.g., "D:path" without backslash) // Pattern: single letter followed by colon, then immediately a non-slash character // This converts "D:path" to "D:\path" to make it absolute - if (process.platform === 'win32' || /^[a-zA-Z]:/.test(inputPath)) { - const driveRelativeMatch = inputPath.match(/^([a-zA-Z]:)([^/\\].*)$/); + // Only apply on Windows or when path looks like a Windows drive path + if (process.platform === 'win32') { + const driveRelativeMatch = inputPath.match(/^([a-zA-Z]:)([^\\].*)$/); if (driveRelativeMatch) { // Insert backslash after drive letter inputPath = driveRelativeMatch[1] + '\\' + driveRelativeMatch[2]; diff --git a/ccw/src/utils/shell-escape.ts b/ccw/src/utils/shell-escape.ts index e2f751f4..d45de441 100644 --- a/ccw/src/utils/shell-escape.ts +++ b/ccw/src/utils/shell-escape.ts @@ -1,30 +1,117 @@ /** - * Windows cmd.exe argument escaping for spawn({ shell: true }). + * Cross-platform shell argument escaping utilities. * - * This utility escapes cmd.exe metacharacters using caret (^) so that user - * controlled input cannot inject additional commands. + * Provides proper escaping for command-line arguments when using spawn({ shell: true }) + * on different platforms: + * - Windows: cmd.exe metacharacter handling with proper quote escaping + * - Unix (Linux/macOS): POSIX shell escaping with single quotes */ -const WINDOWS_METACHARS = /[&|<>()%!"]/g; - +/** + * Escape a command-line argument for Windows cmd.exe. + * Follows Microsoft C/C++ runtime argv parsing rules. + * + * Rules: + * 1. Arguments containing spaces, tabs, or quotes must be quoted + * 2. Backslashes are literal unless followed by a quote + * 3. To include a literal quote, use \" + * 4. Backslashes before a quote must be doubled: \\" produces \" + * 5. Trailing backslashes must be doubled when the arg is quoted + * + * @param arg - The argument to escape + * @returns Properly escaped argument safe for Windows command line + */ export function escapeWindowsArg(arg: string): string { if (arg === '') return '""'; - // Normalize newlines to spaces to prevent cmd.exe from - // misinterpreting multiline arguments (breaks argument parsing) - let sanitizedArg = arg.replace(/\r?\n/g, ' '); + // Normalize newlines to spaces to prevent cmd.exe issues + arg = arg.replace(/\r?\n/g, ' '); - // Escape caret first to avoid double-escaping when prefixing other metachars. - let escaped = sanitizedArg.replace(/\^/g, '^^'); + // Check if the argument needs quoting + // Characters that require quoting: space, tab, double quote, or backslash + const needsQuoting = /[ \t"\\]/.test(arg); - // Escape cmd.exe metacharacters with caret. - escaped = escaped.replace(WINDOWS_METACHARS, '^$&'); - - // Wrap whitespace-containing args in double quotes. - if (/\s/.test(escaped)) { - escaped = `"${escaped}"`; + if (!needsQuoting) { + // No special characters - return as is + return arg; } - return escaped; + // Build the escaped string with proper quoting + let result = '"'; + + for (let i = 0; i < arg.length; i++) { + // Count consecutive backslashes + let backslashes = 0; + while (i < arg.length && arg[i] === '\\') { + backslashes++; + i++; + } + + if (i === arg.length) { + // Trailing backslashes: double them all (they precede the closing quote) + result += '\\'.repeat(backslashes * 2); + break; + } + + if (arg[i] === '"') { + // Backslashes followed by a quote: double the backslashes and escape the quote + result += '\\'.repeat(backslashes * 2 + 1) + '"'; + } else { + // Backslashes not followed by a quote: they are literal + result += '\\'.repeat(backslashes) + arg[i]; + } + } + + result += '"'; + return result; } +/** + * Escape a command-line argument for Unix shells (bash, sh, zsh). + * Uses single quotes which treat all characters literally except single quote itself. + * + * Strategy: + * - Wrap argument in single quotes + * - Replace any single quotes with: '\'' (end quote, escaped quote, start quote) + * + * @param arg - The argument to escape + * @returns Properly escaped argument safe for Unix shell + */ +export function escapeUnixArg(arg: string): string { + if (arg === '') return "''"; + + // If no special characters, return as-is + // Safe characters: alphanumeric, hyphen, underscore, dot, forward slash, colon + if (/^[a-zA-Z0-9_.\/:@=-]+$/.test(arg)) { + return arg; + } + + // Use single quotes and escape any single quotes within + // 'arg' -> 'arg'\''s' for "arg's" + return "'" + arg.replace(/'/g, "'\\''") + "'"; +} + +/** + * Escape a command-line argument for the current platform. + * Automatically selects the appropriate escaping method based on OS. + * + * @param arg - The argument to escape + * @returns Properly escaped argument for the current platform's shell + */ +export function escapeShellArg(arg: string): string { + if (process.platform === 'win32') { + return escapeWindowsArg(arg); + } + return escapeUnixArg(arg); +} + +/** + * Check if we need shell escaping on the current platform. + * Windows always needs escaping when shell: true due to cmd.exe. + * Unix typically doesn't need escaping when shell: false. + * + * @returns true if shell escaping should be applied + */ +export function needsShellEscaping(): boolean { + return process.platform === 'win32'; +}