High severityNVD Advisory· Published Aug 23, 2023· Updated Sep 30, 2024
Shescape on Windows escaping may be bypassed in threaded context
CVE-2023-40185
Description
shescape is simple shell escape library for JavaScript. This may impact users that use Shescape on Windows in a threaded context. The vulnerability can result in Shescape escaping (or quoting) for the wrong shell, thus allowing attackers to bypass protections depending on the combination of expected and used shell. This bug has been patched in version 1.7.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
shescapenpm | < 1.7.4 | 1.7.4 |
Affected products
1- Range: < 1.7.4
Patches
10b976dab645aProvide explicit `$PATH` value to which (#1142)
68 files changed · +1174 −302
CHANGELOG.md+2 −0 modified@@ -7,6 +7,7 @@ Versioning]. ## [Unreleased] +- Fix potential silent executable lookup failure for Windows. ([#1142]) - Support more valid `shell` values for Windows. ([#1137]) ## [1.7.3] - 2023-08-07 @@ -292,6 +293,7 @@ Versioning]. [#1083]: https://github.com/ericcornelissen/shescape/pull/1083 [#1094]: https://github.com/ericcornelissen/shescape/pull/1094 [#1137]: https://github.com/ericcornelissen/shescape/pull/1137 +[#1142]: https://github.com/ericcornelissen/shescape/pull/1142 [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
index.js+2 −2 modified@@ -53,7 +53,7 @@ function getPlatformHelpers() { export function escape(arg, options = {}) { const helpers = getPlatformHelpers(); const { flagProtection, interpolation, shellName } = parseOptions( - { options, process }, + { env: process.env, options }, helpers, ); const argAsString = checkedToString(arg); @@ -129,7 +129,7 @@ export function escapeAll(args, options = {}) { export function quote(arg, options = {}) { const helpers = getPlatformHelpers(); const { flagProtection, shellName } = parseOptions( - { options, process }, + { env: process.env, options }, helpers, ); const argAsString = checkedToString(arg);
src/executables.js+6 −2 modified@@ -12,16 +12,20 @@ * - Follows symbolic links. * * @param {object} args The arguments for this function. + * @param {object} args.env The environment variables. * @param {string} args.executable A string representation of the executable. * @param {object} deps The dependencies for this function. * @param {Function} deps.exists A function to check if a file exists. * @param {Function} deps.readlink A function to resolve (sym)links. * @param {Function} deps.which A function to perform a `which(1)`-like lookup. * @returns {string} The full path to the binary of the executable. */ -export function resolveExecutable({ executable }, { exists, readlink, which }) { +export function resolveExecutable( + { env, executable }, + { exists, readlink, which }, +) { try { - executable = which(executable); + executable = which(executable, { path: env.PATH || env.Path }); } catch (_) { // For backwards compatibility return the executable even if its location // cannot be obtained
src/options.js+3 −4 modified@@ -10,25 +10,24 @@ import { isString } from "./reflection.js"; * Parses options provided to shescape. * * @param {object} args The arguments for this function. + * @param {object} args.env The environment variables. * @param {object} args.options The options for escaping. * @param {boolean} [args.options.flagProtection] Is flag protection enabled. * @param {boolean} [args.options.interpolation] Is interpolation enabled. * @param {boolean | string} [args.options.shell] The shell to escape for. - * @param {object} args.process The `process` values. - * @param {object} args.process.env The environment variables. * @param {object} deps The dependencies for this function. * @param {Function} deps.getDefaultShell Function to get the default shell. * @param {Function} deps.getShellName Function to get the name of a shell. * @returns {object} The parsed arguments. */ export function parseOptions( - { options: { flagProtection, interpolation, shell }, process: { env } }, + { env, options: { flagProtection, interpolation, shell } }, { getDefaultShell, getShellName }, ) { flagProtection = flagProtection ? true : false; interpolation = interpolation ? true : false; shell = isString(shell) ? shell : getDefaultShell({ env }); - const shellName = getShellName({ shell }, { resolveExecutable }); + const shellName = getShellName({ env, shell }, { resolveExecutable }); return { flagProtection, interpolation, shellName }; }
src/unix.js+3 −2 modified@@ -121,14 +121,15 @@ export function getFlagProtectionFunction(shellName) { * Determines the name of the shell identified by a file path or file name. * * @param {object} args The arguments for this function. + * @param {object} args.env The environment variables. * @param {string} args.shell The name or path of the shell. * @param {object} deps The dependencies for this function. * @param {Function} deps.resolveExecutable Resolve the path to an executable. * @returns {string} The shell name. */ -export function getShellName({ shell }, { resolveExecutable }) { +export function getShellName({ env, shell }, { resolveExecutable }) { shell = resolveExecutable( - { executable: shell }, + { env, executable: shell }, { exists: fs.existsSync, readlink: fs.readlinkSync, which: which.sync }, );
src/win.js+3 −2 modified@@ -98,14 +98,15 @@ export function getFlagProtectionFunction(shellName) { * Determines the name of the shell identified by a file path or file name. * * @param {object} args The arguments for this function. + * @param {object} args.env The environment variables. * @param {string} args.shell The name or path of the shell. * @param {object} deps The dependencies for this function. * @param {Function} deps.resolveExecutable Resolve the path to an executable. * @returns {string} The shell name. */ -export function getShellName({ shell }, { resolveExecutable }) { +export function getShellName({ env, shell }, { resolveExecutable }) { shell = resolveExecutable( - { executable: shell }, + { env, executable: shell }, { exists: fs.existsSync, readlink: fs.readlinkSync, which: which.sync }, );
test/_constants.cjs+4 −2 modified@@ -60,11 +60,13 @@ module.exports.shellsUnix = [ /* Windows related constants */ module.exports.binCmd = "cmd.exe"; +module.exports.binCmdNoExt = "cmd"; module.exports.binPowerShell = "powershell.exe"; +module.exports.binPowerShellNoExt = "powershell"; module.exports.shellsWindows = [ module.exports.binCmd, - "cmd.EXE", + module.exports.binCmdNoExt, module.exports.binPowerShell, - "powershell.EXE", + module.exports.binPowerShellNoExt, ];
test/e2e/child_process.test.js+0 −40 removed@@ -1,40 +0,0 @@ -/** - * @overview Contains end-to-end tests of using Shescape with the child_process - * package. - * @license MIT - */ - -import test from "ava"; -import isCI from "is-ci"; -import which from "which"; - -import { constants, macros, injectionStrings } from "./_.js"; - -const systemShells = constants.isWindows - ? constants.shellsWindows - : constants.shellsUnix; - -const testArgs = ["harmless", ...injectionStrings]; -const testShells = [false, true, ...systemShells]; - -for (const arg of testArgs) { - test(macros.fork, { arg }); - - for (const shell of testShells) { - let runTest = test; - try { - if (!isCI && typeof shell === "string") { - which.sync(shell); - } - } catch (_) { - runTest = test.skip; - } - - runTest(macros.exec, { arg, shell }); - runTest(macros.execSync, { arg, shell }); - runTest(macros.execFile, { arg, shell }); - runTest(macros.execFileSync, { arg, shell }); - runTest(macros.spawn, { arg, shell }); - runTest(macros.spawnSync, { arg, shell }); - } -}
test/e2e/_common.js+53 −0 added@@ -0,0 +1,53 @@ +/** + * @overview Provides common utilities for end-to-end tests. + * @license MIT + */ + +import process from "node:process"; + +import test from "ava"; +import isCI from "is-ci"; +import which from "which"; + +import { injectionStrings } from "../../testing.js"; +import * as constants from "../_constants.cjs"; + +/** + * Get a list of strings to use as arguments in end-to-end tests. + * + * @returns {string[]} A list of test arguments. + */ +export function getTestArgs() { + return ["harmless", ...injectionStrings]; +} + +/** + * Get the AVA test function to use for the given shell. + * + * @param {string} shell The shell to run a test for. + * @returns {Function} An AVA `test` function. + */ +export function getTestFn(shell) { + try { + if (!isCI && typeof shell === "string") { + which.sync(shell, { path: process.env.PATH || process.env.Path }); + } + + return test; + } catch (_) { + return test.skip; + } +} + +/** + * Get a list of `shell` option values to use in end-to-end tests. + * + * @returns {(boolean | string)[]} A list of `shell` option values. + */ +export function getTestShells() { + const systemShells = constants.isWindows + ? constants.shellsWindows + : constants.shellsUnix; + + return [false, true, ...systemShells]; +}
test/e2e/exec-file.test.js+15 −0 added@@ -0,0 +1,15 @@ +/** + * @overview Contains end-to-end tests of using Shescape with the child_process + * package's `execFile` (and `execFileSync`) functions. + * @license MIT + */ + +import { common, macros } from "./_.js"; + +for (const shell of common.getTestShells()) { + const test = common.getTestFn(shell); + for (const arg of common.getTestArgs()) { + test(macros.execFile, { arg, shell }); + test(macros.execFileSync, { arg, shell }); + } +}
test/e2e/exec.test.js+15 −0 added@@ -0,0 +1,15 @@ +/** + * @overview Contains end-to-end tests of using Shescape with the child_process + * package's `exec` (and `execSync`) functions. + * @license MIT + */ + +import { common, macros } from "./_.js"; + +for (const shell of common.getTestShells()) { + const test = common.getTestFn(shell); + for (const arg of common.getTestArgs()) { + test(macros.exec, { arg, shell }); + test(macros.execSync, { arg, shell }); + } +}
test/e2e/fork.test.js+12 −0 added@@ -0,0 +1,12 @@ +/** + * @overview Contains end-to-end tests of using Shescape with the child_process + * package's `fork` functions. + * @license MIT + */ + +import { common, macros } from "./_.js"; + +for (const arg of common.getTestArgs()) { + const test = common.getTestFn(null); + test(macros.fork, { arg }); +}
test/e2e/_.js+2 −2 modified@@ -3,8 +3,8 @@ * @license MIT */ -import { injectionStrings } from "../../testing.js"; import * as constants from "../_constants.cjs"; +import * as common from "./_common.js"; import * as macros from "./_macros.js"; -export { constants, macros, injectionStrings }; +export { common, constants, macros };
test/e2e/spawn.test.js+15 −0 added@@ -0,0 +1,15 @@ +/** + * @overview Contains end-to-end tests of using Shescape with the child_process + * package's `spawn` (and `spawnSync`) functions. + * @license MIT + */ + +import { common, macros } from "./_.js"; + +for (const shell of common.getTestShells()) { + const test = common.getTestFn(shell); + for (const arg of common.getTestArgs()) { + test(macros.spawn, { arg, shell }); + test(macros.spawnSync, { arg, shell }); + } +}
test/integration/escape-all/bash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the + * Bourne-again shell (Bash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binBash}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binBash)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/cmd-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the Windows + * Command Prompt (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmd}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCmd)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/cmd.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the Windows + * Command Prompt (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmdNoExt}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCmdNoExt)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/commonjs.test.js+23 −0 added@@ -0,0 +1,23 @@ +/** + * @overview Contains integration test for the CommonJS version of + * `shescape.escapeAll`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import { arbitrary } from "../_.js"; + +import { escapeAll } from "shescape"; +import { escapeAll as escapeAllCjs } from "../../../index.cjs"; + +testProp( + "esm === cjs", + [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], + (t, args, options) => { + const resultEsm = escapeAll(args, options); + const resultCjs = escapeAllCjs(args, options); + t.deepEqual(resultEsm, resultCjs); + }, +);
test/integration/escape-all/csh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the C shell + * (csh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binCsh}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCsh)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/dash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the Debian + * Almquist shell (Dash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binDash}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binDash)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/invalid.test.js+22 −0 added@@ -0,0 +1,22 @@ +/** + * @overview Contains integration tests for invalid use of `shescape.escapeAll`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import test from "ava"; + +import { arbitrary, constants, macros } from "../_.js"; + +import { escapeAll } from "shescape"; + +testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { + for (const { value } of constants.illegalArguments) { + t.throws(() => escapeAll([value], options), { instanceOf: TypeError }); + t.throws(() => escapeAll(value, options), { instanceOf: TypeError }); + } +}); + +test(macros.prototypePollution, (_, payload) => { + escapeAll(["a"], payload); +});
test/integration/escape-all/powershell-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for Windows + * PowerShell (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShell}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binPowerShell)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/powershell.test.js+23 −0 added@@ -0,0 +1,23 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for Windows + * PowerShell (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShellNoExt}`, (t) => { + for (const scenario of generate.escapeExamples( + constants.binPowerShellNoExt, + )) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape-all/valid.test.js+2 −34 renamed@@ -1,25 +1,14 @@ /** - * @overview Contains integration tests for `shescape.escapeAll`. + * @overview Contains integration tests for valid use of `shescape.escapeAll`. * @license MIT */ import { testProp } from "@fast-check/ava"; -import test from "ava"; import * as fc from "fast-check"; -import { arbitrary, constants, generate, macros } from "./_.js"; +import { arbitrary } from "../_.js"; import { escape, escapeAll } from "shescape"; -import { escapeAll as escapeAllCjs } from "../../index.cjs"; - -for (const shell of generate.platformShells()) { - test(`inputs are escaped for ${shell}`, (t) => { - for (const { expected, input, options } of generate.escapeExamples(shell)) { - const result = escapeAll([input], options); - t.deepEqual(result, [expected]); - } - }); -} testProp( "return values", @@ -71,24 +60,3 @@ testProp( t.is(entry, escape(arg, options)); }, ); - -testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { - for (const { value } of constants.illegalArguments) { - t.throws(() => escapeAll([value], options), { instanceOf: TypeError }); - t.throws(() => escapeAll(value, options), { instanceOf: TypeError }); - } -}); - -test(macros.prototypePollution, (_, payload) => { - escapeAll(["a"], payload); -}); - -testProp( - "esm === cjs", - [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], - (t, args, options) => { - const resultEsm = escapeAll(args, options); - const resultCjs = escapeAllCjs(args, options); - t.deepEqual(resultEsm, resultCjs); - }, -);
test/integration/escape-all/zsh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escapeAll` for the Z shell + * (Zsh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escapeAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binZsh}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binZsh)) { + const { expected, input, options } = scenario; + const result = escapeAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/escape/bash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the + * Bourne-again shell (Bash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binBash}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binBash)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/escape/cmd-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the Windows + * Command Prompt (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmd}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCmd)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape/cmd.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the Windows + * Command Prompt (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmdNoExt}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCmdNoExt)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape/commonjs.test.js+22 −0 added@@ -0,0 +1,22 @@ +/** + * @overview Contains integration tests for the CommonJS version of + * `shescape.escape`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; + +import { arbitrary } from "../_.js"; + +import { escape } from "shescape"; +import { escape as escapeCjs } from "../../../index.cjs"; + +testProp( + "esm === cjs", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const resultEsm = escape(arg, options); + const resultCjs = escapeCjs(arg, options); + t.is(resultEsm, resultCjs); + }, +);
test/integration/escape/csh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the C shell + * (csh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binCsh}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binCsh)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape/dash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the Debian + * Almquist shell (Dash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binDash}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binDash)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape/invalid.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for invalid use of `shescape.escape`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import test from "ava"; + +import { arbitrary, constants, macros } from "../_.js"; + +import { escape } from "shescape"; + +testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { + for (const { value } of constants.illegalArguments) { + t.throws(() => escape(value, options), { instanceOf: TypeError }); + } +}); + +test(macros.prototypePollution, (_, payload) => { + escape("a", payload); +});
test/integration/escape/powershell-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for Windows + * PowerShell (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShell}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binPowerShell)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape/powershell.test.js+23 −0 added@@ -0,0 +1,23 @@ +/** + * @overview Contains integration tests for `shescape.escape` for Windows + * PowerShell (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShellNoExt}`, (t) => { + for (const scenario of generate.escapeExamples( + constants.binPowerShellNoExt, + )) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/escape.test.js+0 −50 removed@@ -1,50 +0,0 @@ -/** - * @overview Contains integration tests for `shescape.escape`. - * @license MIT - */ - -import { testProp } from "@fast-check/ava"; -import test from "ava"; - -import { arbitrary, constants, generate, macros } from "./_.js"; - -import { escape as escape } from "shescape"; -import { escape as escapeCjs } from "../../index.cjs"; - -for (const shell of generate.platformShells()) { - test(`input is escaped for ${shell}`, (t) => { - for (const { expected, input, options } of generate.escapeExamples(shell)) { - const result = escape(input, options); - t.is(result, expected); - } - }); -} - -testProp( - "return values", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const result = escape(arg, options); - t.is(typeof result, "string"); - }, -); - -testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { - for (const { value } of constants.illegalArguments) { - t.throws(() => escape(value, options), { instanceOf: TypeError }); - } -}); - -test(macros.prototypePollution, (_, payload) => { - escape("a", payload); -}); - -testProp( - "esm === cjs", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const resultEsm = escape(arg, options); - const resultCjs = escapeCjs(arg, options); - t.is(resultEsm, resultCjs); - }, -);
test/integration/escape/valid.test.js+19 −0 added@@ -0,0 +1,19 @@ +/** + * @overview Contains integration tests for valid use of `shescape.escape`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; + +import { arbitrary } from "../_.js"; + +import { escape } from "shescape"; + +testProp( + "return values", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const result = escape(arg, options); + t.is(typeof result, "string"); + }, +);
test/integration/escape/zsh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the Z shell + * (Zsh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binZsh}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binZsh)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/_generators.js+9 −18 modified@@ -5,28 +5,16 @@ import * as fixturesUnix from "../fixtures/unix.js"; import * as fixturesWindows from "../fixtures/win.js"; -import common from "../_constants.cjs"; -/** - * Returns the shells officially supported by Shescape for the current platform. - * - * @yields {string} Supported shells for the current platform. - */ -export function* platformShells() { - if (common.isWindows) { - yield* common.shellsWindows; - } else { - yield* common.shellsUnix; - } -} +import { constants } from "./_.js"; /** * Returns the test fixtures for the current platform. * * @returns {object} All test fixtures for the current platform. */ function getPlatformFixtures() { - if (common.isWindows) { + if (constants.isWindows) { return fixturesWindows; } else { return fixturesUnix; @@ -40,13 +28,16 @@ function getPlatformFixtures() { * @returns {object} All test fixtures for `shell`. */ function getShellFixtures(shell) { - shell = shell.toLowerCase(); + let shellName = shell.toLowerCase(); + if (constants.isWindows) { + shellName = shellName.endsWith(".exe") ? shellName : `${shellName}.exe`; + } const fixtures = getPlatformFixtures(); return { - escape: Object.values(fixtures.escape[shell]).flat(), - flag: Object.values(fixtures.flag[shell]).flat(), - quote: Object.values(fixtures.quote[shell]).flat(), + escape: Object.values(fixtures.escape[shellName]).flat(), + flag: Object.values(fixtures.flag[shellName]).flat(), + quote: Object.values(fixtures.quote[shellName]).flat(), }; }
test/integration/quote-all/bash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the + * Bourne-again shell (Bash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binBash}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binBash)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/cmd-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the Windows + * Command Prompt (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmd}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCmd)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/cmd.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the Windows + * Command Prompt (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmdNoExt}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCmdNoExt)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/commonjs.test.js+23 −0 added@@ -0,0 +1,23 @@ +/** + * @overview Contains integration test for the CommonJS version of + * `shescape.quoteAll`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import { arbitrary } from "../_.js"; + +import { quoteAll } from "shescape"; +import { quoteAll as quoteAllCjs } from "../../../index.cjs"; + +testProp( + "esm === cjs", + [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], + (t, args, options) => { + const resultEsm = quoteAll(args, options); + const resultCjs = quoteAllCjs(args, options); + t.deepEqual(resultEsm, resultCjs); + }, +);
test/integration/quote-all/csh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the C shell + * (csh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binCsh}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCsh)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/dash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the Debian + * Almquist shell (Dash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binDash}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binDash)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/invalid.test.js+22 −0 added@@ -0,0 +1,22 @@ +/** + * @overview Contains integration tests for invalid use of `shescape.quote`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import test from "ava"; + +import { arbitrary, constants, macros } from "../_.js"; + +import { quoteAll } from "shescape"; + +testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { + for (const { value } of constants.illegalArguments) { + t.throws(() => quoteAll([value], options), { instanceOf: TypeError }); + t.throws(() => quoteAll(value, options), { instanceOf: TypeError }); + } +}); + +test(macros.prototypePollution, (_, payload) => { + quoteAll(["a"], payload); +});
test/integration/quote-all/powershell-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for Windows + * PowerShell (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShell}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binPowerShell)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/powershell.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for Windows + * PowerShell (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShellNoExt}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binPowerShellNoExt)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote-all/valid.test.js+2 −34 renamed@@ -1,25 +1,14 @@ /** - * @overview Contains integration tests for `shescape.quoteAll`. + * @overview Contains integration tests for valid use of `shescape.quoteAll`. * @license MIT */ import { testProp } from "@fast-check/ava"; -import test from "ava"; import * as fc from "fast-check"; -import { arbitrary, constants, generate, macros } from "./_.js"; +import { arbitrary } from "../_.js"; import { quote, quoteAll as quoteAll } from "shescape"; -import { quoteAll as quoteAllCjs } from "../../index.cjs"; - -for (const shell of generate.platformShells()) { - test(`inputs are quoted for ${shell}`, (t) => { - for (const { expected, input, options } of generate.quoteExamples(shell)) { - const result = quoteAll([input], options); - t.deepEqual(result, [expected]); - } - }); -} testProp( "return values", @@ -71,24 +60,3 @@ testProp( t.is(entry, quote(arg, options)); }, ); - -testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { - for (const { value } of constants.illegalArguments) { - t.throws(() => quoteAll([value], options), { instanceOf: TypeError }); - t.throws(() => quoteAll(value, options), { instanceOf: TypeError }); - } -}); - -test(macros.prototypePollution, (_, payload) => { - quoteAll(["a"], payload); -}); - -testProp( - "esm === cjs", - [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], - (t, args, options) => { - const resultEsm = quoteAll(args, options); - const resultCjs = quoteAllCjs(args, options); - t.deepEqual(resultEsm, resultCjs); - }, -);
test/integration/quote-all/zsh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quoteAll` for the Z shell + * (Zsh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quoteAll } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binZsh}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binZsh)) { + const { expected, input, options } = scenario; + const result = quoteAll([input], options); + t.deepEqual(result, [expected]); + } +});
test/integration/quote/bash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.escape` for the + * Bourne-again shell (Bash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { escape } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binBash}`, (t) => { + for (const scenario of generate.escapeExamples(constants.binBash)) { + const { expected, input, options } = scenario; + const result = escape(input, options); + t.is(result, expected); + } +});
test/integration/quote/cmd-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the Windows + * Command Prompt (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmd}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCmd)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote/cmd.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the Windows + * Command Prompt (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binCmdNoExt}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCmdNoExt)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote/commonjs.test.js+22 −0 added@@ -0,0 +1,22 @@ +/** + * @overview Contains integration tests for the CommonJS version of + * `shescape.quote`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; + +import { arbitrary } from "../_.js"; + +import { quote } from "shescape"; +import { quote as quoteCjs } from "../../../index.cjs"; + +testProp( + "esm === cjs", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const resultEsm = quote(arg, options); + const resultCjs = quoteCjs(arg, options); + t.is(resultEsm, resultCjs); + }, +);
test/integration/quote/csh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the C shell + * (csh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binCsh}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binCsh)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote/dash.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the Debian + * Almquist shell (Dash). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binDash}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binDash)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote/invalid.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for invalid use of `shescape.quote`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import test from "ava"; + +import { arbitrary, constants, macros } from "../_.js"; + +import { quote } from "shescape"; + +testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { + for (const { value } of constants.illegalArguments) { + t.throws(() => quote(value, options), { instanceOf: TypeError }); + } +}); + +test(macros.prototypePollution, (_, payload) => { + quote("a", payload); +});
test/integration/quote/powershell-exe.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for Windows + * PowerShell (with extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShell}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binPowerShell)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote/powershell.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for Windows + * PowerShell (without extension). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test : test.skip; + +runTest(`input is escaped for ${constants.binPowerShellNoExt}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binPowerShellNoExt)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/quote.test.js+0 −50 removed@@ -1,50 +0,0 @@ -/** - * @overview Contains integration tests for `shescape.quote`. - * @license MIT - */ - -import { testProp } from "@fast-check/ava"; -import test from "ava"; - -import { arbitrary, constants, generate, macros } from "./_.js"; - -import { quote as quote } from "shescape"; -import { quote as quoteCjs } from "../../index.cjs"; - -for (const shell of generate.platformShells()) { - test(`input is quoted for ${shell}`, (t) => { - for (const { expected, input, options } of generate.quoteExamples(shell)) { - const result = quote(input, options); - t.is(result, expected); - } - }); -} - -testProp( - "return value", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const result = quote(arg, options); - t.is(typeof result, "string"); - }, -); - -testProp("invalid arguments", [arbitrary.shescapeOptions()], (t, options) => { - for (const { value } of constants.illegalArguments) { - t.throws(() => quote(value, options), { instanceOf: TypeError }); - } -}); - -test(macros.prototypePollution, (_, payload) => { - quote("a", payload); -}); - -testProp( - "esm === cjs", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const resultEsm = quote(arg, options); - const resultCjs = quoteCjs(arg, options); - t.is(resultEsm, resultCjs); - }, -);
test/integration/quote/valid.test.js+19 −0 added@@ -0,0 +1,19 @@ +/** + * @overview Contains integration tests for valid use of `shescape.quote`. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; + +import { arbitrary } from "../_.js"; + +import { quote } from "shescape"; + +testProp( + "return value", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const result = quote(arg, options); + t.is(typeof result, "string"); + }, +);
test/integration/quote/zsh.test.js+21 −0 added@@ -0,0 +1,21 @@ +/** + * @overview Contains integration tests for `shescape.quote` for the Z shell + * (Zsh). + * @license MIT + */ + +import test from "ava"; + +import { constants, generate } from "../_.js"; + +import { quote } from "shescape"; + +const runTest = constants.isWindows ? test.skip : test; + +runTest(`input is escaped for ${constants.binZsh}`, (t) => { + for (const scenario of generate.quoteExamples(constants.binZsh)) { + const { expected, input, options } = scenario; + const result = quote(input, options); + t.is(result, expected); + } +});
test/integration/testing/commonjs.test.js+53 −0 added@@ -0,0 +1,53 @@ +/** + * @overview Contains integration tests for the CommonJS version of the testing + * implementation of shescape. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; +import * as fc from "fast-check"; + +import { arbitrary } from "../_.js"; + +import { shescape as stubscape } from "shescape/testing"; +import { shescape as stubscapeCjs } from "../../../testing.cjs"; + +testProp( + "escape (esm === cjs)", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const resultEsm = stubscape.escape(arg, options); + const resultCjs = stubscapeCjs.escape(arg, options); + t.is(resultEsm, resultCjs); + }, +); + +testProp( + "escapeAll (esm === cjs)", + [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], + (t, args, options) => { + const resultEsm = stubscape.escapeAll(args, options); + const resultCjs = stubscapeCjs.escapeAll(args, options); + t.deepEqual(resultEsm, resultCjs); + }, +); + +testProp( + "quote (esm === cjs)", + [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + (t, arg, options) => { + const resultEsm = stubscape.quote(arg, options); + const resultCjs = stubscapeCjs.quote(arg, options); + t.is(resultEsm, resultCjs); + }, +); + +testProp( + "quoteAll (esm === cjs)", + [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], + (t, args, options) => { + const resultEsm = stubscape.quoteAll(args, options); + const resultCjs = stubscapeCjs.quoteAll(args, options); + t.deepEqual(resultEsm, resultCjs); + }, +);
test/integration/testing/functional.test.js+1 −42 renamed@@ -7,11 +7,10 @@ import { testProp } from "@fast-check/ava"; import * as fc from "fast-check"; -import { arbitrary } from "./_.js"; +import { arbitrary } from "../_.js"; import * as shescape from "shescape"; import { shescape as stubscape } from "shescape/testing"; -import { shescape as stubscapeCjs } from "../../testing.cjs"; testProp( "escape (stubscape ~ shescape)", @@ -104,43 +103,3 @@ testProp( t.is(typeof result, typeof stubResult); }, ); - -testProp( - "escape (esm === cjs)", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const resultEsm = stubscape.escape(arg, options); - const resultCjs = stubscapeCjs.escape(arg, options); - t.is(resultEsm, resultCjs); - }, -); - -testProp( - "escapeAll (esm === cjs)", - [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], - (t, args, options) => { - const resultEsm = stubscape.escapeAll(args, options); - const resultCjs = stubscapeCjs.escapeAll(args, options); - t.deepEqual(resultEsm, resultCjs); - }, -); - -testProp( - "quote (esm === cjs)", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], - (t, arg, options) => { - const resultEsm = stubscape.quote(arg, options); - const resultCjs = stubscapeCjs.quote(arg, options); - t.is(resultEsm, resultCjs); - }, -); - -testProp( - "quoteAll (esm === cjs)", - [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], - (t, args, options) => { - const resultEsm = stubscape.quoteAll(args, options); - const resultCjs = stubscapeCjs.quoteAll(args, options); - t.deepEqual(resultEsm, resultCjs); - }, -);
test/unit/executables/_.js+8 −0 added@@ -0,0 +1,8 @@ +/** + * @overview Provides testing utilities. + * @license MIT + */ + +import * as arbitrary from "../../_arbitraries.js"; + +export { arbitrary };
test/unit/executables/resolve.test.js+61 −10 modified@@ -3,9 +3,13 @@ * @license MIT */ +import { testProp } from "@fast-check/ava"; import test from "ava"; +import * as fc from "fast-check"; import sinon from "sinon"; +import { arbitrary } from "./_.js"; + import { resolveExecutable } from "../../../src/executables.js"; test.before((t) => { @@ -16,7 +20,11 @@ test.before((t) => { t.not(executable, linkedExecutable); t.not(resolvedExecutable, linkedExecutable); - t.context = { executable, linkedExecutable, resolvedExecutable }; + const env = { + PATH: "/bin:/usr/bin", + }; + + t.context = { env, executable, linkedExecutable, resolvedExecutable }; }); test.beforeEach((t) => { @@ -27,25 +35,68 @@ test.beforeEach((t) => { t.context.deps = { exists, readlink, which }; }); +testProp( + "env.PATH is defined", + [arbitrary.env({ keys: ["PATH", "Path"] }), fc.string({ minLength: 1 })], + (t, env, envPath) => { + t.context.deps.which.resetHistory(); + + env.PATH = envPath; + + const { executable } = t.context; + const args = { env, executable }; + + resolveExecutable(args, t.context.deps); + t.is(t.context.deps.which.callCount, 1); + t.true( + t.context.deps.which.calledWithExactly(sinon.match.any, { + path: env.PATH, + }), + ); + }, +); + +testProp( + "env.PATH is not defined", + [arbitrary.env({ keys: ["PATH", "Path"] }), fc.string({ minLength: 1 })], + (t, env, envPath) => { + t.context.deps.which.resetHistory(); + + delete env.PATH; + env.Path = envPath; + + const { executable } = t.context; + const args = { env, executable }; + + resolveExecutable(args, t.context.deps); + t.is(t.context.deps.which.callCount, 1); + t.true( + t.context.deps.which.calledWithExactly(sinon.match.any, { + path: env.Path, + }), + ); + }, +); + test("the executable cannot be resolved", (t) => { - const { executable } = t.context; - const args = { executable }; + const { env, executable } = t.context; + const args = { env, executable }; t.context.deps.which.throws(); const result = resolveExecutable(args, t.context.deps); t.is(result, executable); t.is(t.context.deps.which.callCount, 1); - t.true(t.context.deps.which.calledWithExactly(executable)); + t.true(t.context.deps.which.calledWithExactly(executable, sinon.match.any)); t.is(t.context.deps.exists.callCount, 0); t.is(t.context.deps.readlink.callCount, 0); }); test("the executable doesn't exist", (t) => { - const { executable, resolvedExecutable } = t.context; - const args = { executable }; + const { env, executable, resolvedExecutable } = t.context; + const args = { env, executable }; t.context.deps.exists.returns(false); t.context.deps.which.returns(resolvedExecutable); @@ -61,8 +112,8 @@ test("the executable doesn't exist", (t) => { }); test("the executable exists and is not a (sym)link", (t) => { - const { executable, resolvedExecutable } = t.context; - const args = { executable }; + const { env, executable, resolvedExecutable } = t.context; + const args = { env, executable }; t.context.deps.exists.returns(true); t.context.deps.readlink.throws(); @@ -79,8 +130,8 @@ test("the executable exists and is not a (sym)link", (t) => { }); test("the executable exists and is a (sym)link", (t) => { - const { executable, linkedExecutable, resolvedExecutable } = t.context; - const args = { executable }; + const { env, executable, linkedExecutable, resolvedExecutable } = t.context; + const args = { env, executable }; t.context.deps.exists.returns(true); t.context.deps.readlink.returns(linkedExecutable);
test/unit/options/parse-options.test.js+5 −4 modified@@ -18,7 +18,7 @@ const arbitraryInput = () => .tuple(arbitrary.shescapeOptions(), arbitrary.env()) .map(([options, env]) => { options = options || {}; - return { options, process: { env } }; + return { env, options }; }); test.beforeEach((t) => { @@ -84,7 +84,7 @@ testProp( t.context.deps.getShellName.resetHistory(); t.context.deps.getShellName.returns(shellName); - const env = args.process.env; + const env = args.env; args.options.shell = providedShell; const result = parseOptions(args, t.context.deps); @@ -93,7 +93,7 @@ testProp( t.is(t.context.deps.getShellName.callCount, 1); t.true( t.context.deps.getShellName.calledWithExactly( - { shell: defaultShell }, + { env, shell: defaultShell }, { resolveExecutable }, ), ); @@ -108,14 +108,15 @@ testProp( t.context.deps.getShellName.resetHistory(); t.context.deps.getShellName.returns(shellName); + const env = args.env; args.options.shell = providedShell; const result = parseOptions(args, t.context.deps); t.is(t.context.deps.getDefaultShell.callCount, 0); t.is(t.context.deps.getShellName.callCount, 1); t.true( t.context.deps.getShellName.calledWithExactly( - { shell: providedShell }, + { env, shell: providedShell }, { resolveExecutable }, ), );
test/unit/unix/index.test.js+1 −1 modified@@ -106,7 +106,7 @@ testProp( unix.getShellName({ env, shell }, { resolveExecutable }); t.true( resolveExecutable.calledWithExactly( - { executable: shell }, + { env, executable: shell }, { exists: sinon.match.func, readlink: sinon.match.func,
test/unit/win/index.test.js+7 −3 modified@@ -99,11 +99,15 @@ testProp( "get shell name for supported shell", [arbitrary.env(), arbitrary.windowsPath(), arbitrary.windowsShell()], (t, env, basePath, shell) => { + const executable = shell.toLowerCase().endsWith(".exe") + ? shell + : `${shell}.exe`; + const resolveExecutable = sinon.stub(); - resolveExecutable.returns(path.join(basePath, shell)); + resolveExecutable.returns(path.join(basePath, executable)); const result = win.getShellName({ env, shell }, { resolveExecutable }); - t.is(result, shell); + t.is(result, executable); }, ); @@ -133,7 +137,7 @@ testProp( win.getShellName({ env, shell }, { resolveExecutable }); t.true( resolveExecutable.calledWithExactly( - { executable: shell }, + { env, executable: shell }, { exists: sinon.match.func, readlink: sinon.match.func,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-j55r-787p-m549ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-40185ghsaADVISORY
- github.com/ericcornelissen/shescape/commit/0b976dab645abf45ffd85e74a8c6e51ee2f42d63ghsax_refsource_MISCWEB
- github.com/ericcornelissen/shescape/pull/1142ghsax_refsource_MISCWEB
- github.com/ericcornelissen/shescape/releases/tag/v1.7.4ghsax_refsource_MISCWEB
- github.com/ericcornelissen/shescape/security/advisories/GHSA-j55r-787p-m549ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.