Skip to content

Commit

Permalink
Don't escape customOptions or single-string command options (#3729)
Browse files Browse the repository at this point in the history
  • Loading branch information
bwateratmsft authored Nov 29, 2022
1 parent 87957e9 commit a5b21d7
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 20 deletions.
6 changes: 3 additions & 3 deletions src/debugging/netcore/NetCoreDebugHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import * as fse from 'fs-extra';
import * as path from 'path';
import { composeArgs, ContainerOS, VoidCommandResponse, withArg, withQuotedArg } from '../../runtimes/docker';
import { CommandLineArgs, composeArgs, ContainerOS, VoidCommandResponse, withArg, withQuotedArg } from '../../runtimes/docker';
import { DialogResponses, IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils';
import { DebugConfiguration, MessageItem, ProgressLocation, ShellQuotedString, window } from 'vscode';
import { DebugConfiguration, MessageItem, ProgressLocation, window } from 'vscode';
import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { NetCoreTaskHelper, NetCoreTaskOptions } from '../../tasks/netcore/NetCoreTaskHelper';
Expand Down Expand Up @@ -307,7 +307,7 @@ export class NetCoreDebugHelper implements DebugHelper {

private async isDebuggerInstalled(containerName: string, debuggerPath: string, containerOS: ContainerOS): Promise<boolean> {
let containerCommand: string;
let containerCommandArgs: ShellQuotedString[];
let containerCommandArgs: CommandLineArgs;
if (containerOS === 'windows') {
containerCommand = 'cmd';
containerCommandArgs = composeArgs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
withFlagArg,
withNamedArg,
withQuotedArg,
withVerbatimArg,
} from "../../utils/commandLineBuilder";
import { CommandNotSupportedError } from '../../utils/CommandNotSupportedError';
import { byteStreamToGenerator, stringStreamToGenerator } from '../../utils/streamToGenerator';
Expand Down Expand Up @@ -390,7 +391,7 @@ export abstract class DockerClientBase extends ConfigurableClient implements ICo
withDockerLabelsArg(options.labels),
withNamedArg('--iidfile', options.imageIdFile),
withDockerBuildArg(options.args),
withArg(options.customOptions),
withVerbatimArg(options.customOptions),
withQuotedArg(options.path),
)();
}
Expand Down Expand Up @@ -829,9 +830,9 @@ export abstract class DockerClientBase extends ConfigurableClient implements ICo
withDockerEnvArg(options.environmentVariables),
withNamedArg('--env-file', options.environmentFiles),
withNamedArg('--entrypoint', options.entrypoint),
withArg(options.customOptions),
withVerbatimArg(options.customOptions),
withArg(options.imageRef),
withArg(...(toArray(options.command || []))),
typeof options.command === 'string' ? withVerbatimArg(options.command) : withArg(...(toArray(options.command || []))),
)();
}

Expand Down Expand Up @@ -875,7 +876,7 @@ export abstract class DockerClientBase extends ConfigurableClient implements ICo
withFlagArg('--tty', options.tty),
withDockerEnvArg(options.environmentVariables),
withArg(options.container),
withArg(...toArray(options.command)),
typeof options.command === 'string' ? withVerbatimArg(options.command) : withArg(...toArray(options.command)),
)();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ import {
RestartCommandOptions,
StartCommandOptions,
StopCommandOptions,
UpCommandOptions
UpCommandOptions,
} from '../../contracts/ContainerOrchestratorClient';
import {
CommandLineArgs,
CommandLineCurryFn,
composeArgs,
withArg,
withFlagArg,
withNamedArg
withNamedArg,
withVerbatimArg,
} from '../../utils/commandLineBuilder';
import { stringStreamToGenerator } from '../../utils/streamToGenerator';
import { ConfigurableClient } from '../ConfigurableClient';
Expand Down Expand Up @@ -96,7 +97,7 @@ export class DockerComposeClient extends ConfigurableClient implements IContaine
withNamedArg('--scale', Object.entries(options.scale || {}).map(([service, scale]) => `${service}=${scale}`)),
withNamedArg('--timeout', options.timeoutSeconds?.toString(10)),
withFlagArg('--wait', options.wait),
withArg(options.customOptions),
withVerbatimArg(options.customOptions),
withArg(...(options.services || [])),
)();
}
Expand Down Expand Up @@ -125,7 +126,7 @@ export class DockerComposeClient extends ConfigurableClient implements IContaine
withNamedArg('--rmi', options.removeImages),
withFlagArg('--volumes', options.removeVolumes),
withNamedArg('--timeout', options.timeoutSeconds?.toString(10)),
withArg(options.customOptions),
withVerbatimArg(options.customOptions),
)();
}

Expand Down
48 changes: 48 additions & 0 deletions src/runtimes/docker/test/DockerClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { ShellQuoting } from 'vscode';
import {
DockerClient,
} from '../clients/DockerClient/DockerClient';
import { BuildImageCommandOptions, RunContainerCommandOptions } from '../contracts/ContainerClient';
import { escaped } from '../utils/commandLineBuilder';
import { Bash, Powershell } from '../utils/spawnStreamAsync';

dayjs.extend(customParseFormat);
dayjs.extend(utc);
Expand Down Expand Up @@ -91,3 +93,49 @@ describe('DockerClient', () => {
});
});
});

describe('DockerClient (unit)', () => {
const client = new DockerClient();

it('Should produce the expected lack of quoting/escaping customOptions', async () => {
const options: BuildImageCommandOptions = {
path: '.',
customOptions: '--no-cache --progress plain'
};

const commandResponse = await client.buildImage(options);
const pwshQuoted = new Powershell().quote(commandResponse.args);
const bashQuoted = new Bash().quote(commandResponse.args);

expect(pwshQuoted).to.deep.equal(['image', 'build', '--no-cache --progress plain', '\'.\'']);
expect(bashQuoted).to.deep.equal(['image', 'build', '--no-cache --progress plain', '\'.\'']);
});

it('Should produce the expected lack of quoting/escaping a single string command', async () => {
const options: RunContainerCommandOptions = {
imageRef: 'someimage',
command: 'sh -c "echo hello world"',
};

const commandResponse = await client.runContainer(options);
const pwshQuoted = new Powershell().quote(commandResponse.args);
const bashQuoted = new Bash().quote(commandResponse.args);

expect(pwshQuoted).to.deep.equal(['container', 'run', 'someimage', 'sh -c "echo hello world"']);
expect(bashQuoted).to.deep.equal(['container', 'run', 'someimage', 'sh -c "echo hello world"']);
});

it('Should produce the expected quoting/escaping of an array command', async () => {
const options: RunContainerCommandOptions = {
imageRef: 'someimage',
command: ['sh', '-c', 'echo hello world'],
};

const commandResponse = await client.runContainer(options);
const pwshQuoted = new Powershell().quote(commandResponse.args);
const bashQuoted = new Bash().quote(commandResponse.args);

expect(pwshQuoted).to.deep.equal(['container', 'run', 'someimage', 'sh', '-c', 'echo` hello` world']);
expect(bashQuoted).to.deep.equal(['container', 'run', 'someimage', 'sh', '-c', 'echo\\ hello\\ world']);
});
});
22 changes: 22 additions & 0 deletions src/runtimes/docker/test/DockerComposeClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
UpCommandOptions
} from '../contracts/ContainerOrchestratorClient';
import { AccumulatorStream } from '../utils/AccumulatorStream';
import { Bash, Powershell } from '../utils/spawnStreamAsync';

const commonOptions: CommonOrchestratorCommandOptions = {
files: ['docker-compose.yml'],
Expand Down Expand Up @@ -102,3 +103,24 @@ xdescribe('DockerComposeClient', () => {
expect(result).to.contain('registry');
});
});

describe('DockerComposeClient (unit)', () => {
const client = new DockerComposeClient();
client.composeV2 = false;

it('Should produce the expected lack of quoting/escaping customOptions', async () => {
const options: UpCommandOptions = {
...commonOptions,
detached: true,
build: true,
customOptions: '--timeout 10 --wait'
};

const commandResponse = await client.up(options);
const pwshQuoted = new Powershell().quote(commandResponse.args);
const bashQuoted = new Bash().quote(commandResponse.args);

expect(pwshQuoted).to.deep.equal(['--file', '\'docker-compose.yml\'', 'up', '--detach', '--build', '--timeout 10 --wait']);
expect(bashQuoted).to.deep.equal(['--file', '\'docker-compose.yml\'', 'up', '--detach', '--build', '--timeout 10 --wait']);
});
});
33 changes: 26 additions & 7 deletions src/runtimes/docker/utils/commandLineBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { ShellQuotedString, ShellQuoting } from 'vscode';
import { toArray } from './toArray';

export type CommandLineArgs = Array<ShellQuotedString>;
export type CommandLineArgs = Array<ShellQuotedString | string>;

export type CommandLineCurryFn = (cmdLineArgs?: CommandLineArgs) => CommandLineArgs;

Expand Down Expand Up @@ -95,14 +95,33 @@ export function withNamedArg(
): CommandLineCurryFn {
return (cmdLineArgs: CommandLineArgs = []) => {
return toArray(args)
.reduce((allArgs, arg) => {
if (arg) {
const normalizedArg = shouldQuote ? quoted(arg) : escaped(arg);
if (assignValue) {
return withArg(`${name}=${normalizedArg}`)(allArgs);
.reduce((allArgs, arg) => {
if (arg) {
const normalizedArg = shouldQuote ? quoted(arg) : escaped(arg);
if (assignValue) {
return withArg(`${name}=${normalizedArg}`)(allArgs);
}

return withArg(name, normalizedArg)(allArgs);
}

return withArg(name, normalizedArg)(allArgs);
return allArgs;
}, cmdLineArgs);
};
}

/**
* Functional method for adding additional verbatim arguments to an existing list of
* arguments. They will not be quoted nor escaped.
* @param args Raw arguments to add to the CommandLineArguments records
* @returns A function that takes an optional array of CommandLineArguments and appends the provided arguments
* @deprecated {@link withArg} should be used instead in almost all cases
*/
export function withVerbatimArg(...args: Array<string | null | undefined>): CommandLineCurryFn {
return (cmdLineArgs: CommandLineArgs = []) => {
return args.reduce<CommandLineArgs>((allArgs, arg) => {
if (arg) {
return [...allArgs, arg];
}

return allArgs;
Expand Down
14 changes: 14 additions & 0 deletions src/runtimes/docker/utils/spawnStreamAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export class Powershell extends Shell {
const escape = (value: string) => `\`${value}`;

return args.map((quotedArg) => {
// If it's a verbatim argument, return it as-is.
// The overwhelming majority of arguments are `ShellQuotedString`, so
// verbatim arguments will only show up if `withVerbatimArg` is used.
if (typeof quotedArg === 'string') {
return quotedArg;
}

switch (quotedArg.quoting) {
case ShellQuoting.Escape:
return quotedArg.value.replace(/[ "'()]/g, escape);
Expand Down Expand Up @@ -103,6 +110,13 @@ export class Bash extends Shell {
const escape = (value: string) => `\\${value}`;

return args.map((quotedArg) => {
// If it's a verbatim argument, return it as-is.
// The overwhelming majority of arguments are `ShellQuotedString`, so
// verbatim arguments will only show up if `withVerbatimArg` is used.
if (typeof quotedArg === 'string') {
return quotedArg;
}

switch (quotedArg.quoting) {
case ShellQuoting.Escape:
return quotedArg.value.replace(/[ "']/g, escape);
Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/runners/TaskCommandRunnerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as os from 'os';
import * as vscode from 'vscode';
import { CommandNotSupportedError, CommandRunner, ICommandRunnerFactory, Like, normalizeCommandResponseLike, PromiseCommandResponse, StreamingCommandRunner, VoidCommandResponse } from '../docker';
import { CommandLineArgs, CommandNotSupportedError, CommandRunner, ICommandRunnerFactory, Like, normalizeCommandResponseLike, PromiseCommandResponse, StreamingCommandRunner, VoidCommandResponse } from '../docker';

interface TaskCommandRunnerOptions {
taskName: string;
Expand Down Expand Up @@ -35,7 +35,7 @@ export class TaskCommandRunnerFactory implements ICommandRunnerFactory {
}
}

async function executeAsTask(options: TaskCommandRunnerOptions, command: string, args?: vscode.ShellQuotedString[]): Promise<void> {
async function executeAsTask(options: TaskCommandRunnerOptions, command: string, args?: CommandLineArgs): Promise<void> {
const shellExecutionOptions = { cwd: options.cwd || options.workspaceFolder?.uri?.fsPath || os.homedir() };

const shellExecution = args ?
Expand Down

0 comments on commit a5b21d7

Please sign in to comment.