diff --git a/src/cli/CodacyCli.ts b/src/cli/CodacyCli.ts index 8c1f8b6..43b5da9 100644 --- a/src/cli/CodacyCli.ts +++ b/src/cli/CodacyCli.ts @@ -48,7 +48,11 @@ export abstract class CodacyCli { } protected preparePathForExec(path: string): string { - return path + // Wrap path in double quotes and escape characters that are special inside double quotes + // Special characters inside double quotes that we escape: $, `, \, " + // This approach works both when the path is used directly and when wrapped in additional quotes + const escaped = path.replace(/([\\$"`])/g, '\\$1') + return `"${escaped}"` } protected getIdentificationParameters(): Record { diff --git a/src/cli/MacCodacyCli.ts b/src/cli/MacCodacyCli.ts index 3786323..17a4f23 100644 --- a/src/cli/MacCodacyCli.ts +++ b/src/cli/MacCodacyCli.ts @@ -91,10 +91,10 @@ export class MacCodacyCli extends CodacyCli { const execPath = this.preparePathForExec(codacyCliPath) await this.execAsync( - `curl -Ls -o "${execPath}" https://raw.githubusercontent.com/codacy/codacy-cli-v2/main/codacy-cli.sh` + `curl -Ls -o ${execPath} https://raw.githubusercontent.com/codacy/codacy-cli-v2/main/codacy-cli.sh` ) - await this.execAsync(`chmod +x "${execPath}"`) + await this.execAsync(`chmod +x ${execPath}`) this.setCliCommand( this._cliVersion ? `CODACY_CLI_V2_VERSION=${this._cliVersion} ${codacyCliPath}` : codacyCliPath diff --git a/src/cli/WinWSLCodacyCli.ts b/src/cli/WinWSLCodacyCli.ts index 82e3ca3..455f0af 100644 --- a/src/cli/WinWSLCodacyCli.ts +++ b/src/cli/WinWSLCodacyCli.ts @@ -11,12 +11,13 @@ export class WinWSLCodacyCli extends MacCodacyCli { private static toWSLPath(path: string): string { // Convert Windows path to WSL path // Example: 'C:\Users\user\project' -> '/mnt/c/Users/user/project' - // First, unescape any escaped spaces (backslash-space -> space) + // Remove quotes and unescape any escaped spaces let cleanPath = path.replace(/^'|'$/g, '') - // Unescape any escaped spaces (backslash-space -> space) cleanPath = cleanPath.replace(/\\ /g, ' ') - // Then convert backslashes to slashes and add /mnt/ prefix - const wslPath = cleanPath.replace(/\\/g, '/').replace(/^'?([a-zA-Z]):/, '/mnt/$1').replace(/ /g, '\\ ') + // Convert backslashes to slashes and add /mnt/ prefix + // Note: We don't escape spaces here - the parent preparePathForExec will handle all escaping + // The regex [a-zA-Z] matches both uppercase and lowercase drive letters (A-Z, a-z) + const wslPath = cleanPath.replace(/\\/g, '/').replace(/^'?([a-zA-Z]):/, (_, letter) => `/mnt/${letter.toLowerCase()}`) return wslPath } @@ -28,9 +29,10 @@ export class WinWSLCodacyCli extends MacCodacyCli { } protected preparePathForExec(path: string): string { - // Convert the path to WSL format and wrap in quotes to handle spaces + // Convert the path to WSL format first const wslPath = WinWSLCodacyCli.toWSLPath(path) - return wslPath.includes(' ') ? `"${wslPath}"` : wslPath; + // Then apply the base class escaping for shell special characters + return super.preparePathForExec(wslPath) } protected async execAsync( diff --git a/src/test/suite/cli/preparePathForExec.test.ts b/src/test/suite/cli/preparePathForExec.test.ts new file mode 100644 index 0000000..77b012b --- /dev/null +++ b/src/test/suite/cli/preparePathForExec.test.ts @@ -0,0 +1,212 @@ +import * as assert from 'assert' +import { CodacyCli } from '../../../cli/CodacyCli' +import { ProcessedSarifResult } from '../../../cli' + +// Test implementation of CodacyCli to access protected method +class TestCodacyCli extends CodacyCli { + constructor() { + super('/test/root') + } + + public testPreparePathForExec(path: string): string { + return this.preparePathForExec(path) + } + + public async preflightCodacyCli(): Promise { + // Not needed for testing + } + + public async install(): Promise { + // Not needed for testing + } + + public async installDependencies(): Promise { + // Not needed for testing + } + + public async update(): Promise { + // Not needed for testing + } + + public async initialize(): Promise { + // Not needed for testing + } + + public async analyze(): Promise { + // Not needed for testing + return null + } + + public async configDiscover(): Promise { + // Not needed for testing + } +} + +suite('CodacyCli - preparePathForExec', () => { + let cli: TestCodacyCli + + setup(() => { + cli = new TestCodacyCli() + }) + + test('handles paths with parentheses', () => { + const input = '/path/to/file(with)parentheses.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes to handle special characters + assert.strictEqual(output, '"/path/to/file(with)parentheses.ts"') + }) + + test('handles paths with spaces', () => { + const input = '/path/to/file with spaces.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file with spaces.ts"') + }) + + test('handles paths with single quotes', () => { + const input = "/path/to/file'with'quotes.ts" + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes (single quotes don't need escaping inside double quotes) + assert.strictEqual(output, '"/path/to/file\'with\'quotes.ts"') + }) + + test('handles paths with double quotes', () => { + const input = '/path/to/file"with"quotes.ts' + const output = cli.testPreparePathForExec(input) + // Should escape double quotes inside double quotes + assert.strictEqual(output, '"/path/to/file\\"with\\"quotes.ts"') + }) + + test('handles paths with square brackets', () => { + const input = '/path/to/file[with]brackets.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file[with]brackets.ts"') + }) + + test('handles paths with curly braces', () => { + const input = '/path/to/file{with}braces.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file{with}braces.ts"') + }) + + test('handles paths with asterisks', () => { + const input = '/path/to/file*with*asterisks.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file*with*asterisks.ts"') + }) + + test('handles paths with question marks', () => { + const input = '/path/to/file?with?questions.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file?with?questions.ts"') + }) + + test('handles paths with ampersands', () => { + const input = '/path/to/file&with&ersands.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file&with&ersands.ts"') + }) + + test('handles paths with dollar signs', () => { + const input = '/path/to/file$with$dollars.ts' + const output = cli.testPreparePathForExec(input) + // Should escape dollar signs inside double quotes + assert.strictEqual(output, '"/path/to/file\\$with\\$dollars.ts"') + }) + + test('handles paths with backticks', () => { + const input = '/path/to/file`with`backticks.ts' + const output = cli.testPreparePathForExec(input) + // Should escape backticks inside double quotes + assert.strictEqual(output, '"/path/to/file\\`with\\`backticks.ts"') + }) + + test('handles paths with exclamation marks', () => { + const input = '/path/to/file!with!exclamations.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file!with!exclamations.ts"') + }) + + test('handles paths with hash/pound signs', () => { + const input = '/path/to/file#with#hashes.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file#with#hashes.ts"') + }) + + test('handles paths with semicolons', () => { + const input = '/path/to/file;with;semicolons.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file;with;semicolons.ts"') + }) + + test('handles paths with pipes', () => { + const input = '/path/to/file|with|pipes.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file|with|pipes.ts"') + }) + + test('handles paths with less-than and greater-than signs', () => { + const input = '/path/to/fileangles.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/fileangles.ts"') + }) + + test('handles paths with backslashes', () => { + const input = '/path/to/file\\with\\backslashes.ts' + const output = cli.testPreparePathForExec(input) + // Should escape backslashes inside double quotes + assert.strictEqual(output, '"/path/to/file\\\\with\\\\backslashes.ts"') + }) + + test('handles paths with multiple special characters', () => { + const input = '/path/to/(file) with [special] chars & $vars.ts' + const output = cli.testPreparePathForExec(input) + // Should escape only $, `, \, " inside double quotes + assert.strictEqual(output, '"/path/to/(file) with [special] chars & \\$vars.ts"') + }) + + test('handles normal paths without special characters', () => { + const input = '/path/to/normalfile.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/normalfile.ts"') + }) + + test('handles paths with dashes and underscores (safe characters)', () => { + const input = '/path/to/file-with_safe-chars.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file-with_safe-chars.ts"') + }) + + test('handles paths with dots (safe characters)', () => { + const input = '/path/to/file.with.dots.ts' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/path/to/file.with.dots.ts"') + }) + + test('handles empty string', () => { + const input = '' + const output = cli.testPreparePathForExec(input) + // Should return empty string with quotes + assert.strictEqual(output, '""') + }) + + test('handles root path', () => { + const input = '/' + const output = cli.testPreparePathForExec(input) + // Should wrap in double quotes + assert.strictEqual(output, '"/"') + }) +})