fix(cli): 修复 Windows 路径反斜杠被吞掉的问题并添加跨平台路径支持

- 重写 escapeWindowsArg 函数,正确处理反斜杠和引号转义
- 添加 escapeUnixArg 函数支持 Linux/macOS shell 转义
- 添加 normalizePathSeparators 函数自动转换路径分隔符
- 修复 vscode-lsp.ts 中的 TypeScript 类型错误
This commit is contained in:
catlog22
2026-01-20 09:44:49 +08:00
parent 3fe630f221
commit eea859dd6f
3 changed files with 143 additions and 25 deletions

View File

@@ -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,

View File

@@ -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];

View File

@@ -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';
}