Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update escaping of double quotes for PowerShell #1023

Merged
merged 10 commits into from Jul 6, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,8 @@ Versioning].
## [Unreleased]

- Fix incorrect escaping of `"` when escaping for CMD. ([#1022])
- Fix incorrect escaping of `"` when escaping for PowerShell. ([#1023])
- Fix incorrect escaping of `"` when quoting for PowerShell. ([#1023])
- Fix incorrect escaping of `%` when quoting for CMD. ([#986], [#998])

## [1.7.1] - 2023-06-21
Expand Down Expand Up @@ -275,6 +277,7 @@ Versioning].
[#986]: https://github.com/ericcornelissen/shescape/pull/986
[#998]: https://github.com/ericcornelissen/shescape/pull/998
[#1022]: https://github.com/ericcornelissen/shescape/pull/1022
[#1023]: https://github.com/ericcornelissen/shescape/pull/1023
[552e8ea]: https://github.com/ericcornelissen/shescape/commit/552e8eab56861720b1d4e5474fb65741643358f9
[keep a changelog]: https://keepachangelog.com/en/1.0.0/
[semantic versioning]: https://semver.org/spec/v2.0.0.html
29 changes: 25 additions & 4 deletions src/win/powershell.js
Expand Up @@ -10,15 +10,26 @@
* @returns {string} The escaped argument.
*/
function escapeArgForInterpolation(arg) {
return arg
arg = arg
.replace(/[\0\u0008\u001B\u009B]/gu, "")
.replace(/`/gu, "``")
.replace(/\r(?!\n)/gu, "")
.replace(/\r?\n/gu, " ")
.replace(/(?<=^|[\s\u0085])([*1-6]?)(>)/gu, "$1`$2")
.replace(/(?<=^|[\s\u0085])([#\-:<@\]])/gu, "`$1")
.replace(/(["$&'(),;{|}‘’‚‛“”„])/gu, "`$1")
.replace(/([\s\u0085])/gu, "`$1");
.replace(/([$&'(),;{|}‘’‚‛“”„])/gu, "`$1");

if (/[\s\u0085]/u.test(arg.replace(/^[\s\u0085]+/gu, ""))) {
arg = arg
.replace(/(?<!\\)(\\*)"/gu, '$1$1`"`"')
.replace(/(?<!\\)(\\+)$/gu, "$1$1");
} else {
arg = arg.replace(/(?<!\\)(\\*)"/gu, '$1$1\\`"');
}

arg = arg.replace(/([\s\u0085])/gu, "`$1");

return arg;
}

/**
Expand Down Expand Up @@ -55,10 +66,20 @@ export function getEscapeFunction(options) {
* @returns {string} The escaped argument.
*/
function escapeArgForQuoted(arg) {
return arg
arg = arg
.replace(/[\0\u0008\u001B\u009B]/gu, "")
.replace(/\r(?!\n)/gu, "")
.replace(/(['‘’‚‛])/gu, "$1$1");

if (/[\s\u0085]/u.test(arg)) {
arg = arg
.replace(/(?<!\\)(\\*)"/gu, '$1$1""')
.replace(/(?<!\\)(\\+)$/gu, "$1$1");
} else {
arg = arg.replace(/(?<!\\)(\\*)"/gu, '$1$1\\"');
}

return arg;
}

/**
Expand Down
150 changes: 142 additions & 8 deletions test/fixtures/win.js
Expand Up @@ -2278,32 +2278,78 @@ export const escape = {
{
input: 'a"b',
expected: {
interpolation: 'a`"b',
interpolation: 'a\\`"b',
noInterpolation: 'a"b',
},
},
{
input: 'a"b"c',
expected: {
interpolation: 'a`"b`"c',
interpolation: 'a\\`"b\\`"c',
noInterpolation: 'a"b"c',
},
},
{
input: 'a"',
expected: {
interpolation: 'a`"',
interpolation: 'a\\`"',
noInterpolation: 'a"',
},
},
{
input: '"a',
expected: {
interpolation: '`"a',
interpolation: '\\`"a',
noInterpolation: '"a',
},
},
],
"double quotes ('\"') + backslashes ('\\')": [
{
input: 'a\\"b',
expected: {
interpolation: 'a\\\\\\`"b',
noInterpolation: 'a\\"b',
},
},
{
input: 'a\\\\"b',
expected: {
interpolation: 'a\\\\\\\\\\`"b',
noInterpolation: 'a\\\\"b',
},
},
],
"double quotes ('\"') + whitespace": [
{
input: 'a "b',
expected: {
interpolation: 'a` `"`"b',
noInterpolation: 'a "b',
},
},
{
input: 'a "b "c',
expected: {
interpolation: 'a` `"`"b` `"`"c',
noInterpolation: 'a "b "c',
},
},
{
input: 'a "',
expected: {
interpolation: 'a` `"`"',
noInterpolation: 'a "',
},
},
{
input: ' "a',
expected: {
interpolation: '` \\`"a',
noInterpolation: ' "a',
},
},
],
"backticks ('`')": [
{
input: "a`b",
Expand Down Expand Up @@ -2529,6 +2575,44 @@ export const escape = {
expected: { interpolation: "a\\", noInterpolation: "a\\" },
},
],
"backslashes ('\\') + whitespace": [
{
input: "a b\\c",
expected: { interpolation: "a` b\\c", noInterpolation: "a b\\c" },
},
{
input: "a\\b c",
expected: { interpolation: "a\\b` c", noInterpolation: "a\\b c" },
},
{
input: "a b\\",
expected: { interpolation: "a` b\\\\", noInterpolation: "a b\\" },
},
{
input: "a b\\\\",
expected: { interpolation: "a` b\\\\\\\\", noInterpolation: "a b\\\\" },
},
{
input: "\\a b",
expected: { interpolation: "\\a` b", noInterpolation: "\\a b" },
},
{
input: " a\\",
expected: { interpolation: "` a\\", noInterpolation: " a\\" },
},
{
input: " a\\",
expected: { interpolation: "` ` a\\", noInterpolation: " a\\" },
},
{
input: " a b\\",
expected: { interpolation: "` a` b\\\\", noInterpolation: " a b\\" },
},
{
input: " a b\\",
expected: { interpolation: "` ` a` b\\\\", noInterpolation: " a b\\" },
},
],
"colons (':')": [
{
input: "a:b",
Expand Down Expand Up @@ -4349,19 +4433,47 @@ export const quote = {
"double quotes ('\"')": [
{
input: 'a"b',
expected: "'a\"b'",
expected: "'a\\\"b'",
},
{
input: 'a"b"c',
expected: "'a\"b\"c'",
expected: "'a\\\"b\\\"c'",
},
{
input: 'a"',
expected: "'a\"'",
expected: "'a\\\"'",
},
{
input: '"a',
expected: "'\"a'",
expected: "'\\\"a'",
},
],
"double quotes ('\"') + backslashes ('\\')": [
{
input: 'a\\"b',
expected: "'a\\\\\\\"b'",
},
{
input: 'a\\\\"b',
expected: "'a\\\\\\\\\\\"b'",
},
],
"double quotes ('\"') + whitespace": [
{
input: 'a "b',
expected: "'a \"\"b'",
},
{
input: 'a "b "c',
expected: '\'a ""b ""c\'',
},
{
input: 'a "',
expected: "'a \"\"'",
},
{
input: ' "a',
expected: "' \"\"a'",
},
],
"backticks ('`')": [
Expand Down Expand Up @@ -4490,6 +4602,28 @@ export const quote = {
expected: "'\\a'",
},
],
"backslashes ('\\') + whitespace": [
{
input: "a b\\c",
expected: "'a b\\c'",
},
{
input: "a\\b c",
expected: "'a\\b c'",
},
{
input: "a b\\",
expected: "'a b\\\\'",
},
{
input: "a b\\\\",
expected: "'a b\\\\\\\\'",
},
{
input: "\\a b",
expected: "'\\a b'",
},
],
"pipes ('|')": [
{
input: "a|b",
Expand Down
53 changes: 0 additions & 53 deletions test/fuzz/_common.cjs
Expand Up @@ -96,62 +96,9 @@ function getFuzzShell() {
return process.env.FUZZ_SHELL || undefined;
}

/**
* Prepares an argument for echoing to accommodate shell-specific behaviour.
*
* @param {object} args The function arguments.
* @param {string} args.arg The input argument that will be echoed.
* @param {boolean} args.quoted Will `arg` be quoted prior to echoing.
* @param {string} args.shell The shell to be used for echoing.
* @param {boolean} disableExtraWindowsPreparations Disable Windows prep.
* @returns {string} The prepared `arg`.
*/
function prepareArg({ arg, quoted, shell }, disableExtraWindowsPreparations) {
if (constants.isWindows && !disableExtraWindowsPreparations) {
// Node on Windows ...
if (isShellPowerShell(shell)) {
// ... in PowerShell, depending on if there's whitespace in the
// argument ...
if (
(quoted &&
/[\t\n\v\f \u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/u.test(
arg,
)) ||
(!quoted &&
/(?<!^)[\t\n\v\f \u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/u.test(
arg.replace(/^[\s\0\u0008\u001B\u0085\u009B]+/gu, ""),
))
) {
// ... interprets arguments with `"` as nothing so we escape it with
// extra double quotes as `""` ...
arg = arg.replace(/"/gu, '""');

// ... and interprets arguments with `\"` as `"` so we escape the `\`.
arg = arg.replace(
/(?<!\\)((?:\\[\0\u0008\r\u001B\u009B]*)+)(?="|$)/gu,
"$1$1",
);
} else {
// ... interprets arguments with `\"` as `"` so we escape the `\` ...
arg = arg.replace(
/(?<!\\)((?:\\[\0\u0008\r\u001B\u009B]*)+)(?=")/gu,
"$1$1",
);

// ... and interprets arguments with `"` as nothing so we escape it
// with `\"`.
arg = arg.replace(/"/gu, '\\"');
}
}
}

return arg;
}

module.exports = {
ECHO_SCRIPT,
isShellPowerShell,
getExpectedOutput,
getFuzzShell,
prepareArg,
};
Binary file not shown.
Binary file not shown.
16 changes: 6 additions & 10 deletions test/fuzz/exec-file.test.cjs
Expand Up @@ -12,13 +12,11 @@ const common = require("./_common.cjs");
const shescape = require("../../index.cjs");

function check({ arg, shell }) {
const argInfo = { arg, shell, quoted: Boolean(shell) };
const execFileOptions = { encoding: "utf8", shell };

const preparedArg = common.prepareArg(argInfo, !Boolean(shell));
const safeArg = execFileOptions.shell
? shescape.quote(preparedArg, execFileOptions)
: shescape.escape(preparedArg, execFileOptions);
? shescape.quote(arg, execFileOptions)
: shescape.escape(arg, execFileOptions);

return new Promise((resolve, reject) => {
execFile(
Expand All @@ -30,7 +28,7 @@ function check({ arg, shell }) {
reject(`an unexpected error occurred: ${error}`);
} else {
const result = stdout;
const expected = common.getExpectedOutput(argInfo);
const expected = common.getExpectedOutput({ arg, shell });
try {
assert.strictEqual(result, expected);
resolve();
Expand All @@ -44,13 +42,11 @@ function check({ arg, shell }) {
}

function checkSync({ arg, shell }) {
const argInfo = { arg, shell, quoted: Boolean(shell) };
const execFileOptions = { encoding: "utf8", shell };

const preparedArg = common.prepareArg(argInfo, !Boolean(shell));
const safeArg = execFileOptions.shell
? shescape.quote(preparedArg, execFileOptions)
: shescape.escape(preparedArg, execFileOptions);
? shescape.quote(arg, execFileOptions)
: shescape.escape(arg, execFileOptions);

let stdout;
try {
Expand All @@ -64,7 +60,7 @@ function checkSync({ arg, shell }) {
}

const result = stdout;
const expected = common.getExpectedOutput(argInfo);
const expected = common.getExpectedOutput({ arg, shell });
assert.strictEqual(result, expected);
}

Expand Down