From b9317338987acfd52be6c24462a9ec3c1816524c Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sat, 10 Jun 2023 17:09:07 +0200 Subject: [PATCH 01/12] test: add `escapePOSIXShell` util --- test/abort/test-abort-fatal-error.js | 11 +- test/async-hooks/test-callback-error.js | 5 +- test/common/escapePOSIXShell.js | 26 +++ test/common/index.js | 10 +- test/common/index.mjs | 2 + test/fixtures/snapshot/child-process-sync.js | 4 +- test/parallel/test-child-process-bad-stdio.js | 3 +- .../test-child-process-exec-encoding.js | 3 +- .../test-child-process-exec-maxbuf.js | 5 +- .../test-child-process-exec-std-encoding.js | 3 +- .../test-child-process-exec-timeout-expire.js | 3 +- .../test-child-process-exec-timeout-kill.js | 3 +- ...-child-process-exec-timeout-not-expired.js | 5 +- .../test-child-process-execsync-maxbuf.js | 21 +-- .../test-child-process-promisified.js | 12 +- test/parallel/test-cli-eval.js | 175 +++++++++--------- test/parallel/test-cli-node-print-help.js | 12 +- test/parallel/test-cli-syntax-eval.js | 13 +- test/parallel/test-crypto-sign-verify.js | 10 +- .../parallel/test-domain-abort-on-uncaught.js | 9 +- ...n-throw-from-uncaught-exception-handler.js | 21 +-- ...domain-with-abort-on-uncaught-exception.js | 18 +- test/parallel/test-env-var-no-warnings.js | 7 +- test/parallel/test-error-reporting.js | 3 +- test/parallel/test-fs-read-stream.js | 2 +- test/parallel/test-fs-readfile-eof.js | 24 ++- test/parallel/test-fs-readfile-error.js | 4 +- test/parallel/test-fs-readfile-pipe-large.js | 6 +- test/parallel/test-fs-readfile-pipe.js | 5 +- .../test-fs-readfilesync-pipe-large.js | 6 +- test/parallel/test-fs-write-sigxfsz.js | 4 +- ...test-permission-allow-child-process-cli.js | 10 +- .../test-permission-child-process-cli.js | 16 +- .../parallel/test-preload-self-referential.js | 8 +- test/parallel/test-preload-worker.js | 2 +- test/parallel/test-stdin-from-file.js | 6 +- test/parallel/test-stdio-closed.js | 4 +- test/parallel/test-stdout-close-catch.js | 16 +- test/parallel/test-stdout-to-file.js | 7 +- test/parallel/test-stream-pipeline-process.js | 9 +- test/parallel/test-tls-ecdh.js | 4 +- test/parallel/test-worker-init-failure.js | 4 +- test/sequential/test-child-process-emfile.js | 4 +- test/sequential/test-cli-syntax-bad.js | 21 +-- .../test-cli-syntax-file-not-found.js | 11 +- test/sequential/test-cli-syntax-good.js | 11 +- test/sequential/test-init.js | 3 +- 47 files changed, 237 insertions(+), 334 deletions(-) create mode 100644 test/common/escapePOSIXShell.js diff --git a/test/abort/test-abort-fatal-error.js b/test/abort/test-abort-fatal-error.js index 6c197ceb028540..be7d3c6f65c3b5 100644 --- a/test/abort/test-abort-fatal-error.js +++ b/test/abort/test-abort-fatal-error.js @@ -27,11 +27,12 @@ if (common.isWindows) const assert = require('assert'); const exec = require('child_process').exec; -let cmdline = `ulimit -c 0; ${process.execPath}`; -cmdline += ' --max-old-space-size=16 --max-semi-space-size=4'; -cmdline += ' -e "a = []; for (i = 0; i < 1e9; i++) { a.push({}) }"'; +const cmdline = + common.escapePOSIXShell`ulimit -c 0; "${ + process.execPath + }" --max-old-space-size=16 --max-semi-space-size=4 -e "a = []; for (i = 0; i < 1e9; i++) { a.push({}) }"`; -exec(cmdline, function(err, stdout, stderr) { +exec(...cmdline, common.mustCall((err, stdout, stderr) => { if (!err) { console.log(stdout); console.log(stderr); @@ -39,4 +40,4 @@ exec(cmdline, function(err, stdout, stderr) { } assert(common.nodeProcessAborted(err.code, err.signal)); -}); +})); diff --git a/test/async-hooks/test-callback-error.js b/test/async-hooks/test-callback-error.js index f6af8e0018d534..67eb7bab81e9c8 100644 --- a/test/async-hooks/test-callback-error.js +++ b/test/async-hooks/test-callback-error.js @@ -62,11 +62,12 @@ assert.ok(!arg); let program = process.execPath; let args = [ '--abort-on-uncaught-exception', __filename, 'test_callback_abort' ]; - const options = { encoding: 'utf8' }; + let options = { encoding: 'utf8' }; if (!common.isWindows) { - program = `ulimit -c 0 && exec ${program} ${args.join(' ')}`; + [program, options] = common.escapePOSIXShell`ulimit -c 0 && exec "${program}" ${args[0]} "${args[1]}" ${args[2]}`; args = []; options.shell = true; + options.encoding = 'utf8'; } const child = spawnSync(program, args, options); if (common.isWindows) { diff --git a/test/common/escapePOSIXShell.js b/test/common/escapePOSIXShell.js new file mode 100644 index 00000000000000..442f663ecb7b03 --- /dev/null +++ b/test/common/escapePOSIXShell.js @@ -0,0 +1,26 @@ +'use strict'; + +/** +* Escape quoted values in a string template literal. On Windows, this function +* does not escape anything (which is fine for paths, as `"` is not a valid char +* in a path on Windows), so you should use it only to escape paths – or other +* values on tests which are skipped on Windows. +* @returns {[string, object | undefined]} An array that can be passed as +* arguments to `exec` or `execSync`. +*/ +module.exports = function escapePOSIXShell(cmdParts, ...args) { + if (common.isWindows) { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +}; diff --git a/test/common/index.js b/test/common/index.js index dd5f43c8664eb8..5bcfb4a5de6468 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -34,6 +34,7 @@ const { inspect, getCallSite } = require('util'); const { isMainThread } = require('worker_threads'); const { isModuleNamespaceObject } = require('util/types'); +const escapePOSIXShell = require('./escapePOSIXShell'); const tmpdir = require('./tmpdir'); const bits = ['arm64', 'loong64', 'mips', 'mipsel', 'ppc64', 'riscv64', 's390x', 'x64'] .includes(process.arch) ? 64 : 32; @@ -249,15 +250,13 @@ const PIPE = (() => { // `$node --abort-on-uncaught-exception $file child` // the process aborts. function childShouldThrowAndAbort() { - let testCmd = ''; + const escapedArgs = escapePOSIXShell`"${process.argv[0]}" --abort-on-uncaught-exception "${process.argv[1]}" child`; if (!isWindows) { // Do not create core files, as it can take a lot of disk space on // continuous testing and developers' machines - testCmd += 'ulimit -c 0 && '; + escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0]; } - testCmd += `"${process.argv[0]}" --abort-on-uncaught-exception `; - testCmd += `"${process.argv[1]}" child`; - const child = exec(testCmd); + const child = exec(...escapedArgs); child.on('exit', function onExit(exitCode, signal) { const errMsg = 'Test should have aborted ' + `but instead exited with exit code ${exitCode}` + @@ -951,6 +950,7 @@ const common = { childShouldThrowAndAbort, createZeroFilledFile, defaultAutoSelectFamilyAttemptTimeout, + escapePOSIXShell, expectsError, expectRequiredModule, expectWarning, diff --git a/test/common/index.mjs b/test/common/index.mjs index 42c5d5750965a1..748977b85f8012 100644 --- a/test/common/index.mjs +++ b/test/common/index.mjs @@ -11,6 +11,7 @@ const { childShouldThrowAndAbort, createZeroFilledFile, enoughTestMem, + escapePOSIXShell, expectsError, expectWarning, getArrayBufferViews, @@ -64,6 +65,7 @@ export { createRequire, createZeroFilledFile, enoughTestMem, + escapePOSIXShell, expectsError, expectWarning, getArrayBufferViews, diff --git a/test/fixtures/snapshot/child-process-sync.js b/test/fixtures/snapshot/child-process-sync.js index 5ffacb05357df6..52c3e935c59767 100644 --- a/test/fixtures/snapshot/child-process-sync.js +++ b/test/fixtures/snapshot/child-process-sync.js @@ -4,11 +4,13 @@ const { setDeserializeMainFunction, isBuildingSnapshot } = require('v8').startupSnapshot; +const escapePOSIXShell = require('../../common/escapePOSIXShell'); function spawn() { const { spawnSync, execFileSync, execSync } = require('child_process'); spawnSync(process.execPath, [ __filename, 'spawnSync' ], { stdio: 'inherit' }); - execSync(`"${process.execPath}" "${__filename}" "execSync"`, { stdio: 'inherit' }); + const [cmd, opts] = escapePOSIXShell`"${process.execPath}" "${__filename}" "execSync"`; + execSync(cmd, { ...opts, stdio: 'inherit'}); execFileSync(process.execPath, [ __filename, 'execFileSync' ], { stdio: 'inherit' }); } diff --git a/test/parallel/test-child-process-bad-stdio.js b/test/parallel/test-child-process-bad-stdio.js index 1f382e2966043d..6ede126fca7bea 100644 --- a/test/parallel/test-child-process-bad-stdio.js +++ b/test/parallel/test-child-process-bad-stdio.js @@ -27,7 +27,8 @@ ChildProcess.prototype.spawn = function() { }; function createChild(options, callback) { - const cmd = `"${process.execPath}" "${__filename}" child`; + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; + options = { ...options, env: { ...opts.env, ...options.env } }; return cp.exec(cmd, options, common.mustCall(callback)); } diff --git a/test/parallel/test-child-process-exec-encoding.js b/test/parallel/test-child-process-exec-encoding.js index 0c3178e3f20607..5e59ff1fd56df0 100644 --- a/test/parallel/test-child-process-exec-encoding.js +++ b/test/parallel/test-child-process-exec-encoding.js @@ -13,7 +13,8 @@ if (process.argv[2] === 'child') { const expectedStdout = `${stdoutData}\n`; const expectedStderr = `${stderrData}\n`; function run(options, callback) { - const cmd = `"${process.execPath}" "${__filename}" child`; + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; + options = { ...options, env: { ...opts.env, ...options.env } }; cp.exec(cmd, options, common.mustSucceed((stdout, stderr) => { callback(stdout, stderr); diff --git a/test/parallel/test-child-process-exec-maxbuf.js b/test/parallel/test-child-process-exec-maxbuf.js index f2e29b8a7d0745..d13454d25b0c75 100644 --- a/test/parallel/test-child-process-exec-maxbuf.js +++ b/test/parallel/test-child-process-exec-maxbuf.js @@ -14,14 +14,15 @@ function runChecks(err, stdio, streamName, expected) { // On non-Windows, we can pass the path via the env; `"` is not a valid char on // Windows, so we can simply pass the path. const execNode = (args, optionsOrCallback, callback) => { + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" `; let options = optionsOrCallback; if (typeof optionsOrCallback === 'function') { options = undefined; callback = optionsOrCallback; } return cp.exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? options : { ...options, env: { ...process.env, NODE: process.execPath } }, + cmd + args, + { ...opts, ...options }, callback, ); }; diff --git a/test/parallel/test-child-process-exec-std-encoding.js b/test/parallel/test-child-process-exec-std-encoding.js index 08187316726e96..ed5050e14ddb3a 100644 --- a/test/parallel/test-child-process-exec-std-encoding.js +++ b/test/parallel/test-child-process-exec-std-encoding.js @@ -12,8 +12,7 @@ if (process.argv[2] === 'child') { console.log(stdoutData); console.error(stderrData); } else { - const cmd = `"${process.execPath}" "${__filename}" child`; - const child = cp.exec(cmd, common.mustSucceed((stdout, stderr) => { + const child = cp.exec(...common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, expectedStdout); assert.strictEqual(stderr, expectedStderr); })); diff --git a/test/parallel/test-child-process-exec-timeout-expire.js b/test/parallel/test-child-process-exec-timeout-expire.js index 6b62d131cbede5..0fd4f85cbc9e3e 100644 --- a/test/parallel/test-child-process-exec-timeout-expire.js +++ b/test/parallel/test-child-process-exec-timeout-expire.js @@ -18,9 +18,10 @@ if (process.argv[2] === 'child') { return; } -const cmd = `"${process.execPath}" "${__filename}" child`; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; cp.exec(cmd, { + ...opts, timeout: kExpiringParentTimer, }, common.mustCall((err, stdout, stderr) => { console.log('[stdout]', stdout.trim()); diff --git a/test/parallel/test-child-process-exec-timeout-kill.js b/test/parallel/test-child-process-exec-timeout-kill.js index 845fd1eaece24d..4938cea8a22384 100644 --- a/test/parallel/test-child-process-exec-timeout-kill.js +++ b/test/parallel/test-child-process-exec-timeout-kill.js @@ -18,10 +18,11 @@ if (process.argv[2] === 'child') { return; } -const cmd = `"${process.execPath}" "${__filename}" child`; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; // Test with a different kill signal. cp.exec(cmd, { + ...opts, timeout: kExpiringParentTimer, killSignal: 'SIGKILL' }, common.mustCall((err, stdout, stderr) => { diff --git a/test/parallel/test-child-process-exec-timeout-not-expired.js b/test/parallel/test-child-process-exec-timeout-not-expired.js index fb0af5fa8f59d5..0d0c0f47f9dac0 100644 --- a/test/parallel/test-child-process-exec-timeout-not-expired.js +++ b/test/parallel/test-child-process-exec-timeout-not-expired.js @@ -22,10 +22,11 @@ if (process.argv[2] === 'child') { return; } -const cmd = `"${process.execPath}" "${__filename}" child`; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; cp.exec(cmd, { - timeout: kTimeoutNotSupposedToExpire + ...opts, + timeout: kTimeoutNotSupposedToExpire, }, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout.trim(), 'child stdout'); assert.strictEqual(stderr.trim(), 'child stderr'); diff --git a/test/parallel/test-child-process-execsync-maxbuf.js b/test/parallel/test-child-process-execsync-maxbuf.js index 62b211cc3a3de1..5700d02ab677d8 100644 --- a/test/parallel/test-child-process-execsync-maxbuf.js +++ b/test/parallel/test-child-process-execsync-maxbuf.js @@ -1,5 +1,5 @@ 'use strict'; -require('../common'); +const { escapePOSIXShell } = require('../common'); // This test checks that the maxBuffer option for child_process.spawnSync() // works as expected. @@ -10,15 +10,12 @@ const { execSync } = require('child_process'); const msgOut = 'this is stdout'; const msgOutBuf = Buffer.from(`${msgOut}\n`); -const args = [ - '-e', - `"console.log('${msgOut}')";`, -]; +const [cmd, opts] = escapePOSIXShell`"${process.execPath}" -e "${`console.log('${msgOut}')`}"`; // Verify that an error is returned if maxBuffer is surpassed. { assert.throws(() => { - execSync(`"${process.execPath}" ${args.join(' ')}`, { maxBuffer: 1 }); + execSync(cmd, { ...opts, maxBuffer: 1 }); }, (e) => { assert.ok(e, 'maxBuffer should error'); assert.strictEqual(e.code, 'ENOBUFS'); @@ -33,8 +30,8 @@ const args = [ // Verify that a maxBuffer size of Infinity works. { const ret = execSync( - `"${process.execPath}" ${args.join(' ')}`, - { maxBuffer: Infinity } + cmd, + { ...opts, maxBuffer: Infinity }, ); assert.deepStrictEqual(ret, msgOutBuf); @@ -43,9 +40,7 @@ const args = [ // Default maxBuffer size is 1024 * 1024. { assert.throws(() => { - execSync( - `"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024))"` - ); + execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024))"`); }, (e) => { assert.ok(e, 'maxBuffer should error'); assert.strictEqual(e.code, 'ENOBUFS'); @@ -56,9 +51,7 @@ const args = [ // Default maxBuffer size is 1024 * 1024. { - const ret = execSync( - `"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024 - 1))"` - ); + const ret = execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024 - 1))"`); assert.deepStrictEqual( ret.toString().trim(), diff --git a/test/parallel/test-child-process-promisified.js b/test/parallel/test-child-process-promisified.js index de707ce703f4e2..208ee50816fb98 100644 --- a/test/parallel/test-child-process-promisified.js +++ b/test/parallel/test-child-process-promisified.js @@ -7,16 +7,8 @@ const { promisify } = require('util'); const exec = promisify(child_process.exec); const execFile = promisify(child_process.execFile); -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (args) => exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath } }, -); - { - const promise = execNode('-p 42'); + const promise = exec(...common.escapePOSIXShell`"${process.execPath}" -p 42`); assert(promise.child instanceof child_process.ChildProcess); promise.then(common.mustCall((obj) => { @@ -53,7 +45,7 @@ const execNode = (args) => exec( const failingCodeWithStdoutErr = 'console.log(42);console.error(43);process.exit(1)'; { - execNode(`-e "${failingCodeWithStdoutErr}"`) + exec(...common.escapePOSIXShell`"${process.execPath}" -e "${failingCodeWithStdoutErr}"`) .catch(common.mustCall((err) => { assert.strictEqual(err.code, 1); assert.strictEqual(err.stdout, '42\n'); diff --git a/test/parallel/test-cli-eval.js b/test/parallel/test-cli-eval.js index 9fa1bbce35a15b..dc93287f800d77 100644 --- a/test/parallel/test-cli-eval.js +++ b/test/parallel/test-cli-eval.js @@ -32,7 +32,6 @@ const assert = require('assert'); const child = require('child_process'); const path = require('path'); const fixtures = require('../common/fixtures'); -const nodejs = `"${process.execPath}"`; if (process.argv.length > 2) { console.log(process.argv.slice(2).join(' ')); @@ -40,13 +39,13 @@ if (process.argv.length > 2) { } // Assert that nothing is written to stdout. -child.exec(`${nodejs} --eval 42`, common.mustSucceed((stdout, stderr) => { +child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval 42`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, ''); assert.strictEqual(stderr, ''); })); // Assert that "42\n" is written to stderr. -child.exec(`${nodejs} --eval "console.error(42)"`, +child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval "console.error(42)"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, ''); assert.strictEqual(stderr, '42\n'); @@ -54,14 +53,14 @@ child.exec(`${nodejs} --eval "console.error(42)"`, // Assert that the expected output is written to stdout. ['--print', '-p -e', '-pe', '-p'].forEach((s) => { - const cmd = `${nodejs} ${s} `; + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" ${s}`; - child.exec(`${cmd}42`, common.mustSucceed((stdout, stderr) => { + child.exec(`${cmd} 42`, opts, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, '42\n'); assert.strictEqual(stderr, ''); })); - child.exec(`${cmd} '[]'`, common.mustSucceed((stdout, stderr) => { + child.exec(`${cmd} '[]'`, opts, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, '[]\n'); assert.strictEqual(stderr, ''); })); @@ -69,11 +68,7 @@ child.exec(`${nodejs} --eval "console.error(42)"`, // Assert that module loading works. { - // Replace \ by / because Windows uses backslashes in paths, but they're still - // interpreted as the escape character when put between quotes. - const filename = __filename.replace(/\\/g, '/'); - - child.exec(`${nodejs} --eval "require('${filename}')"`, + child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval "${`require(${JSON.stringify(__filename)})`}"`, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err.code, 42); assert.strictEqual( @@ -83,15 +78,16 @@ child.exec(`${nodejs} --eval "console.error(42)"`, } // Check that builtin modules are pre-defined. -child.exec(`${nodejs} --print "os.platform()"`, +child.exec(...common.escapePOSIXShell`"${process.execPath}" --print "os.platform()"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stderr, ''); assert.strictEqual(stdout.trim(), require('os').platform()); })); // Module path resolve bug regression test. -child.exec(`${nodejs} --eval "require('./test/parallel/test-cli-eval.js')"`, - { cwd: path.resolve(__dirname, '../../') }, +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" --eval "require('./test/parallel/test-cli-eval.js')"`; +child.exec(cmd, + { ...opts, cwd: path.resolve(__dirname, '../../') }, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err.code, 42); assert.strictEqual( @@ -100,7 +96,7 @@ child.exec(`${nodejs} --eval "require('./test/parallel/test-cli-eval.js')"`, })); // Missing argument should not crash. -child.exec(`${nodejs} -e`, common.mustCall((err, stdout, stderr) => { +child.exec(...common.escapePOSIXShell`"${process.execPath}" -e`, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err.code, 9); assert.strictEqual(stdout, ''); assert.strictEqual(stderr.trim(), @@ -108,18 +104,18 @@ child.exec(`${nodejs} -e`, common.mustCall((err, stdout, stderr) => { })); // Empty program should do nothing. -child.exec(`${nodejs} -e ""`, common.mustSucceed((stdout, stderr) => { +child.exec(...common.escapePOSIXShell`"${process.execPath}" -e ""`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, ''); assert.strictEqual(stderr, ''); })); // "\\-42" should be interpreted as an escaped expression, not a switch. -child.exec(`${nodejs} -p "\\-42"`, common.mustSucceed((stdout, stderr) => { +child.exec(...common.escapePOSIXShell`"${process.execPath}" -p "\\-42"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, '-42\n'); assert.strictEqual(stderr, ''); })); -child.exec(`${nodejs} --use-strict -p process.execArgv`, +child.exec(...common.escapePOSIXShell`"${process.execPath}" --use-strict -p process.execArgv`, common.mustSucceed((stdout, stderr) => { assert.strictEqual( stdout, "[ '--use-strict', '-p', 'process.execArgv' ]\n" @@ -129,12 +125,9 @@ child.exec(`${nodejs} --use-strict -p process.execArgv`, // Regression test for https://github.com/nodejs/node/issues/3574. { - let emptyFile = fixtures.path('empty.js'); - if (common.isWindows) { - emptyFile = emptyFile.replace(/\\/g, '\\\\'); - } + const emptyFile = fixtures.path('empty.js'); - child.exec(`${nodejs} -e 'require("child_process").fork("${emptyFile}")'`, + child.exec(...common.escapePOSIXShell`"${process.execPath}" -e "${`require("child_process").fork(${JSON.stringify(emptyFile)})`}"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, ''); assert.strictEqual(stderr, ''); @@ -142,13 +135,14 @@ child.exec(`${nodejs} --use-strict -p process.execArgv`, // Make sure that monkey-patching process.execArgv doesn't cause child_process // to incorrectly munge execArgv. - child.exec( - `${nodejs} -e "process.execArgv = ['-e', 'console.log(42)', 'thirdArg'];` + - `require('child_process').fork('${emptyFile}')"`, - common.mustSucceed((stdout, stderr) => { - assert.strictEqual(stdout, '42\n'); - assert.strictEqual(stderr, ''); - })); + child.exec(...common.escapePOSIXShell`"${process.execPath}" -e "${ + 'process.execArgv = [\'-e\', \'console.log(42)\', \'thirdArg\'];' + + `require('child_process').fork(${JSON.stringify(emptyFile)})` + }"`, + common.mustSucceed((stdout, stderr) => { + assert.strictEqual(stdout, '42\n'); + assert.strictEqual(stderr, ''); + })); } // Regression test for https://github.com/nodejs/node/issues/8534. @@ -192,32 +186,34 @@ child.exec(`${nodejs} --use-strict -p process.execArgv`, ].forEach(function(args) { // Ensure that arguments are successfully passed to eval. - const opt = ' --eval "console.log(process.argv.slice(1).join(\' \'))"'; - const cmd = `${nodejs}${opt} -- ${args}`; - child.exec(cmd, common.mustCall(function(err, stdout, stderr) { - assert.strictEqual(stdout, `${args}\n`); - assert.strictEqual(stderr, ''); - assert.strictEqual(err, null); - })); + child.exec( + ...common.escapePOSIXShell`"${process.execPath}" --eval "console.log(process.argv.slice(1).join(' '))" -- ${args}`, + common.mustCall(function(err, stdout, stderr) { + assert.strictEqual(stdout, `${args}\n`); + assert.strictEqual(stderr, ''); + assert.strictEqual(err, null); + }) + ); // Ensure that arguments are successfully passed to print. - const popt = ' --print "process.argv.slice(1).join(\' \')"'; - const pcmd = `${nodejs}${popt} -- ${args}`; - child.exec(pcmd, common.mustCall(function(err, stdout, stderr) { - assert.strictEqual(stdout, `${args}\n`); - assert.strictEqual(stderr, ''); - assert.strictEqual(err, null); - })); + child.exec( + ...common.escapePOSIXShell`"${process.execPath}" --print "process.argv.slice(1).join(' ')" -- ${args}`, + common.mustCall(function(err, stdout, stderr) { + assert.strictEqual(stdout, `${args}\n`); + assert.strictEqual(stderr, ''); + assert.strictEqual(err, null); + }) + ); // Ensure that arguments are successfully passed to a script. // The first argument after '--' should be interpreted as a script // filename. - const filecmd = `${nodejs} -- "${__filename}" ${args}`; - child.exec(filecmd, common.mustCall(function(err, stdout, stderr) { - assert.strictEqual(stdout, `${args}\n`); - assert.strictEqual(stderr, ''); - assert.strictEqual(err, null); - })); + child.exec(...common.escapePOSIXShell`"${process.execPath}" -- "${__filename}" ${args}`, + common.mustCall(function(err, stdout, stderr) { + assert.strictEqual(stdout, `${args}\n`); + assert.strictEqual(stderr, ''); + assert.strictEqual(err, null); + })); }); // ESModule eval tests @@ -226,14 +222,14 @@ child.exec(`${nodejs} --use-strict -p process.execArgv`, // Assert that "42\n" is written to stdout on module eval. const execOptions = '--input-type module'; child.exec( - `${nodejs} ${execOptions} --eval "console.log(42)"`, + ...common.escapePOSIXShell`"${process.execPath}" ${execOptions} --eval "console.log(42)"`, common.mustSucceed((stdout) => { assert.strictEqual(stdout, '42\n'); })); // Assert that "42\n" is written to stdout with print option. child.exec( - `${nodejs} ${execOptions} --print --eval "42"`, + ...common.escapePOSIXShell`"${process.execPath}" ${execOptions} --print --eval "42"`, common.mustCall((err, stdout, stderr) => { assert.ok(err); assert.strictEqual(stdout, ''); @@ -242,7 +238,7 @@ child.exec( // Assert that error is written to stderr on invalid input. child.exec( - `${nodejs} ${execOptions} --eval "!!!!"`, + ...common.escapePOSIXShell`"${process.execPath}" ${execOptions} --eval "!!!!"`, common.mustCall((err, stdout, stderr) => { assert.ok(err); assert.strictEqual(stdout, ''); @@ -251,100 +247,95 @@ child.exec( // Assert that require is undefined in ESM support child.exec( - `${nodejs} ${execOptions} --eval "console.log(typeof require);"`, + ...common.escapePOSIXShell`"${process.execPath}" ${execOptions} --eval "console.log(typeof require);"`, common.mustSucceed((stdout) => { assert.strictEqual(stdout, 'undefined\n'); })); // Assert that import.meta is defined in ESM child.exec( - `${nodejs} ${execOptions} --eval "console.log(typeof import.meta);"`, + ...common.escapePOSIXShell`"${process.execPath}" ${execOptions} --eval "console.log(typeof import.meta);"`, common.mustSucceed((stdout) => { assert.strictEqual(stdout, 'object\n'); })); +{ // Assert that packages can be imported cwd-relative with --eval -child.exec( - `${nodejs} ${execOptions} ` + - '--eval "import \'./test/fixtures/es-modules/mjs-file.mjs\'"', - common.mustSucceed((stdout) => { - assert.strictEqual(stdout, '.mjs file\n'); - })); + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" ${execOptions} --eval`; + const options = { ...opts, cwd: path.join(__dirname, '../..') }; + child.exec( + `${cmd} "import './test/fixtures/es-modules/mjs-file.mjs'"`, + options, + common.mustSucceed((stdout) => { + assert.strictEqual(stdout, '.mjs file\n'); + })); -// Assert that packages can be dynamic imported initial cwd-relative with --eval -child.exec( - `${nodejs} ${execOptions} ` + - '--eval "process.chdir(\'..\');' + + // Assert that packages can be dynamic imported initial cwd-relative with --eval + child.exec( + cmd + ' "process.chdir(\'..\');' + 'import(\'./test/fixtures/es-modules/mjs-file.mjs\')"', - common.mustSucceed((stdout) => { - assert.strictEqual(stdout, '.mjs file\n'); - })); + options, + common.mustSucceed((stdout) => { + assert.strictEqual(stdout, '.mjs file\n'); + })); -child.exec( - `${nodejs} ` + - '--eval "process.chdir(\'..\');' + + child.exec( + cmd + ' "process.chdir(\'..\');' + 'import(\'./test/fixtures/es-modules/mjs-file.mjs\')"', - common.mustSucceed((stdout) => { - assert.strictEqual(stdout, '.mjs file\n'); - })); + options, + common.mustSucceed((stdout) => { + assert.strictEqual(stdout, '.mjs file\n'); + })); +} if (common.hasCrypto) { // Assert that calls to crypto utils work without require. child.exec( - `${nodejs} ` + - '-e "console.log(crypto.randomBytes(16).toString(\'hex\'))"', + ...common.escapePOSIXShell`"${process.execPath}" -e "console.log(crypto.randomBytes(16).toString('hex'))"`, common.mustSucceed((stdout) => { assert.match(stdout, /[0-9a-f]{32}/i); })); child.exec( - `${nodejs} ` + - '-p "crypto.randomBytes(16).toString(\'hex\')"', + ...common.escapePOSIXShell`"${process.execPath}" -p "crypto.randomBytes(16).toString('hex')"`, common.mustSucceed((stdout) => { assert.match(stdout, /[0-9a-f]{32}/i); })); } // Assert that overriding crypto works. child.exec( - `${nodejs} ` + - '-p "crypto=Symbol(\'test\')"', + ...common.escapePOSIXShell`"${process.execPath}" -p "crypto=Symbol('test')"`, common.mustSucceed((stdout) => { assert.match(stdout, /Symbol\(test\)/i); })); child.exec( - `${nodejs} ` + - '-e "crypto = {};console.log(\'randomBytes\', typeof crypto.randomBytes)"', + ...common.escapePOSIXShell`"${process.execPath}" -e "crypto = {};console.log('randomBytes', typeof crypto.randomBytes)"`, common.mustSucceed((stdout) => { assert.match(stdout, /randomBytes\sundefined/); })); // Assert that overriding crypto with a local variable works. child.exec( - `${nodejs} ` + - '-e "const crypto = {};console.log(\'randomBytes\', typeof crypto.randomBytes)"', + ...common.escapePOSIXShell`"${process.execPath}" -e "const crypto = {};console.log('randomBytes', typeof crypto.randomBytes)"`, common.mustSucceed((stdout) => { assert.match(stdout, /randomBytes\sundefined/); })); child.exec( - `${nodejs} ` + - '-e "let crypto = {};console.log(\'randomBytes\', typeof crypto.randomBytes)"', + ...common.escapePOSIXShell`"${process.execPath}" -e "let crypto = {};console.log('randomBytes', typeof crypto.randomBytes)"`, common.mustSucceed((stdout) => { assert.match(stdout, /randomBytes\sundefined/); })); child.exec( - `${nodejs} ` + - '-e "var crypto = {};console.log(\'randomBytes\', typeof crypto.randomBytes)"', + ...common.escapePOSIXShell`"${process.execPath}" -e "var crypto = {};console.log('randomBytes', typeof crypto.randomBytes)"`, common.mustSucceed((stdout) => { assert.match(stdout, /randomBytes\sundefined/); })); child.exec( - `${nodejs} ` + - '-p "const crypto = {randomBytes:1};typeof crypto.randomBytes"', + ...common.escapePOSIXShell`"${process.execPath}" -p "const crypto = {randomBytes:1};typeof crypto.randomBytes"`, common.mustSucceed((stdout) => { assert.match(stdout, /^number/); })); child.exec( - `${nodejs} ` + - '-p "let crypto = {randomBytes:1};typeof crypto.randomBytes"', + ...common.escapePOSIXShell`"${process.execPath}" -p "let crypto = {randomBytes:1};typeof crypto.randomBytes"`, common.mustSucceed((stdout) => { assert.match(stdout, /^number/); })); diff --git a/test/parallel/test-cli-node-print-help.js b/test/parallel/test-cli-node-print-help.js index eea05dc1f00885..92abe816ffe46f 100644 --- a/test/parallel/test-cli-node-print-help.js +++ b/test/parallel/test-cli-node-print-help.js @@ -11,18 +11,8 @@ const { exec, spawn } = require('child_process'); const { once } = require('events'); let stdOut; -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (args, callback) => exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath } }, - callback, -); - - function startPrintHelpTest() { - execNode('--help', common.mustSucceed((stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" --help`, common.mustSucceed((stdout, stderr) => { stdOut = stdout; validateNodePrintHelp(); })); diff --git a/test/parallel/test-cli-syntax-eval.js b/test/parallel/test-cli-syntax-eval.js index b5b8db4547a864..ad107405b20804 100644 --- a/test/parallel/test-cli-syntax-eval.js +++ b/test/parallel/test-cli-syntax-eval.js @@ -4,21 +4,10 @@ const common = require('../common'); const assert = require('assert'); const { exec } = require('child_process'); -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (args, callback) => exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath } }, - callback, -); - - // Should throw if -c and -e flags are both passed ['-c', '--check'].forEach(function(checkFlag) { ['-e', '--eval'].forEach(function(evalFlag) { - const args = [checkFlag, evalFlag, 'foo']; - execNode(args.join(' '), common.mustCall((err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" ${checkFlag} ${evalFlag} foo`, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err instanceof Error, true); assert.strictEqual(err.code, 9); assert( diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index 9dd586a1a1f9a0..8a263ec3350f55 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -621,12 +621,10 @@ assert.throws( const msgfile = tmpdir.resolve('s5.msg'); fs.writeFileSync(msgfile, msg); - const cmd = - `"${common.opensslCli}" dgst -sha256 -verify "${pubfile}" -signature "${ - sigfile}" -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-2 "${ - msgfile}"`; - - exec(cmd, common.mustCall((err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${ + common.opensslCli}" dgst -sha256 -verify "${pubfile}" -signature "${ + sigfile}" -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-2 "${msgfile + }"`, common.mustCall((err, stdout, stderr) => { assert(stdout.includes('Verified OK')); })); } diff --git a/test/parallel/test-domain-abort-on-uncaught.js b/test/parallel/test-domain-abort-on-uncaught.js index 08551721c1d39b..284f6b908fe404 100644 --- a/test/parallel/test-domain-abort-on-uncaught.js +++ b/test/parallel/test-domain-abort-on-uncaught.js @@ -202,18 +202,15 @@ if (process.argv[2] === 'child') { } else { tests.forEach(function(test, testIndex) { - let testCmd = ''; + const escapedArgs = common.escapePOSIXShell`"${process.execPath}" --abort-on-uncaught-exception "${__filename}" child ${testIndex}`; if (!common.isWindows) { // Do not create core files, as it can take a lot of disk space on // continuous testing and developers' machines - testCmd += 'ulimit -c 0 && '; + escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0]; } - testCmd += `"${process.argv[0]}" --abort-on-uncaught-exception ` + - `"${process.argv[1]}" child ${testIndex}`; - try { - child_process.execSync(testCmd); + child_process.execSync(...escapedArgs); } catch (e) { assert.fail(`Test index ${testIndex} failed: ${e}`); } diff --git a/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js b/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js index 31889c17f742cf..6307d9f838c693 100644 --- a/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js +++ b/test/parallel/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js @@ -50,7 +50,7 @@ if (process.argv[2] === 'child') { function runTestWithoutAbortOnUncaughtException() { child_process.exec( - createTestCmdLine(), + ...createTestCmdLine(), function onTestDone(err, stdout, stderr) { // When _not_ passing --abort-on-uncaught-exception, the process' // uncaughtException handler _must_ be called, and thus the error @@ -70,7 +70,7 @@ function runTestWithoutAbortOnUncaughtException() { } function runTestWithAbortOnUncaughtException() { - child_process.exec(createTestCmdLine({ + child_process.exec(...createTestCmdLine({ withAbortOnUncaughtException: true }), function onTestDone(err, stdout, stderr) { assert.notStrictEqual(err.code, RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE, @@ -82,21 +82,14 @@ function runTestWithAbortOnUncaughtException() { } function createTestCmdLine(options) { - let testCmd = ''; + const escapedArgs = common.escapePOSIXShell`"${process.execPath}" ${ + options?.withAbortOnUncaughtException ? '--abort-on-uncaught-exception' : '' + } "${__filename}" child`; if (!common.isWindows) { // Do not create core files, as it can take a lot of disk space on // continuous testing and developers' machines - testCmd += 'ulimit -c 0 && '; + escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0]; } - - testCmd += `"${process.argv[0]}"`; - - if (options?.withAbortOnUncaughtException) { - testCmd += ' --abort-on-uncaught-exception'; - } - - testCmd += ` "${process.argv[1]}" child`; - - return testCmd; + return escapedArgs; } diff --git a/test/parallel/test-domain-with-abort-on-uncaught-exception.js b/test/parallel/test-domain-with-abort-on-uncaught-exception.js index 5f10b19926b955..1a3dfb8a8ab907 100644 --- a/test/parallel/test-domain-with-abort-on-uncaught-exception.js +++ b/test/parallel/test-domain-with-abort-on-uncaught-exception.js @@ -91,21 +91,17 @@ if (process.argv[2] === 'child') { if (options.throwInDomainErrHandler) throwInDomainErrHandlerOpt = 'throwInDomainErrHandler'; - let cmdToExec = ''; - if (!common.isWindows) { - // Do not create core files, as it can take a lot of disk space on - // continuous testing and developers' machines - cmdToExec += 'ulimit -c 0 && '; - } - let useTryCatchOpt; if (options.useTryCatch) useTryCatchOpt = 'useTryCatch'; - cmdToExec += `"${process.argv[0]}" ${cmdLineOption ? cmdLineOption : ''} "${ - process.argv[1]}" child ${throwInDomainErrHandlerOpt} ${useTryCatchOpt}`; - - const child = exec(cmdToExec); + const escapedArgs = common.escapePOSIXShell`"${process.execPath}" ${cmdLineOption || ''} "${__filename}" child ${throwInDomainErrHandlerOpt || ''} ${useTryCatchOpt || ''}`; + if (!common.isWindows) { + // Do not create core files, as it can take a lot of disk space on + // continuous testing and developers' machines + escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0]; + } + const child = exec(...escapedArgs); if (child) { child.on('exit', function onChildExited(exitCode, signal) { diff --git a/test/parallel/test-env-var-no-warnings.js b/test/parallel/test-env-var-no-warnings.js index b6164da1039a55..f186edf72865aa 100644 --- a/test/parallel/test-env-var-no-warnings.js +++ b/test/parallel/test-env-var-no-warnings.js @@ -7,14 +7,13 @@ if (process.argv[2] === 'child') { process.emitWarning('foo'); } else { function test(newEnv) { - const env = { ...process.env, ...newEnv }; - const cmd = `"${process.execPath}" "${__filename}" child`; + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; - cp.exec(cmd, { env }, common.mustCall((err, stdout, stderr) => { + cp.exec(cmd, { ...opts, env: { ...opts.env, ...newEnv } }, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err, null); assert.strictEqual(stdout, ''); - if (env.NODE_NO_WARNINGS === '1') + if (newEnv.NODE_NO_WARNINGS === '1') assert.strictEqual(stderr, ''); else assert.match(stderr.trim(), /Warning: foo\n/); diff --git a/test/parallel/test-error-reporting.js b/test/parallel/test-error-reporting.js index 98abf949fb0c0f..e8a8f14223d6e9 100644 --- a/test/parallel/test-error-reporting.js +++ b/test/parallel/test-error-reporting.js @@ -28,8 +28,7 @@ const fixtures = require('../common/fixtures'); function errExec(script, option, callback) { callback = typeof option === 'function' ? option : callback; option = typeof option === 'string' ? option : ''; - const cmd = `"${process.argv[0]}" ${option} "${fixtures.path(script)}"`; - return exec(cmd, (err, stdout, stderr) => { + return exec(...common.escapePOSIXShell`"${process.execPath}" ${option} "${fixtures.path(script)}"`, (err, stdout, stderr) => { // There was some error assert.ok(err); diff --git a/test/parallel/test-fs-read-stream.js b/test/parallel/test-fs-read-stream.js index a038eac1efdfa1..80bd7b01c860fc 100644 --- a/test/parallel/test-fs-read-stream.js +++ b/test/parallel/test-fs-read-stream.js @@ -198,7 +198,7 @@ if (!common.isWindows) { const filename = `${tmpdir.path}/foo.pipe`; const mkfifoResult = child_process.spawnSync('mkfifo', [filename]); if (!mkfifoResult.error) { - child_process.exec(`echo "xyz foobar" > '${filename}'`); + child_process.exec(...common.escapePOSIXShell`echo "xyz foobar" > "${filename}"`); const stream = new fs.createReadStream(filename, common.mustNotMutateObjectDeep({ end: 1 })); stream.data = ''; diff --git a/test/parallel/test-fs-readfile-eof.js b/test/parallel/test-fs-readfile-eof.js index 94354b915b8dd5..d7f9e21c5bf158 100644 --- a/test/parallel/test-fs-readfile-eof.js +++ b/test/parallel/test-fs-readfile-eof.js @@ -25,21 +25,19 @@ const data2 = 'World'; const expected = `${data1}\n${data2}\n`; const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); function test(child) { - const cmd = `(echo ${data1}; sleep 0.5; echo ${data2}) | ${node} ${f} ${child}`; - exec(cmd, common.mustSucceed((stdout, stderr) => { - assert.strictEqual( - stdout, - expected, - `expected to read(${child === childType[0] ? 'with' : 'without'} encoding): '${expected}' but got: '${stdout}'`); - assert.strictEqual( - stderr, - '', - `expected not to read anything from stderr but got: '${stderr}'`); - })); + exec(...common.escapePOSIXShell`(echo "${data1}"; sleep 0.5; echo "${data2}") | "${process.execPath}" "${__filename}" "${child}"`, + common.mustSucceed((stdout, stderr) => { + assert.strictEqual( + stdout, + expected, + `expected to read(${child === childType[0] ? 'with' : 'without'} encoding): '${expected}' but got: '${stdout}'`); + assert.strictEqual( + stderr, + '', + `expected not to read anything from stderr but got: '${stderr}'`); + })); } test(childType[0]); diff --git a/test/parallel/test-fs-readfile-error.js b/test/parallel/test-fs-readfile-error.js index da4638d22d59f1..ae2b7dcda865de 100644 --- a/test/parallel/test-fs-readfile-error.js +++ b/test/parallel/test-fs-readfile-error.js @@ -36,9 +36,7 @@ const fixtures = require('../common/fixtures'); function test(env, cb) { const filename = fixtures.path('test-fs-readfile-error.js'); - const execPath = `"${process.execPath}" "${filename}"`; - const options = { env: { ...process.env, ...env } }; - exec(execPath, options, (err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" "${filename}"`, (err, stdout, stderr) => { assert(err); assert.strictEqual(stdout, ''); assert.notStrictEqual(stderr, ''); diff --git a/test/parallel/test-fs-readfile-pipe-large.js b/test/parallel/test-fs-readfile-pipe-large.js index 4376774bb411d6..fa5fea3ca38826 100644 --- a/test/parallel/test-fs-readfile-pipe-large.js +++ b/test/parallel/test-fs-readfile-pipe-large.js @@ -25,10 +25,8 @@ tmpdir.refresh(); fs.writeFileSync(filename, dataExpected); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; -exec(cmd, { maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; +exec(cmd, { ...opts, maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { assert.strictEqual( stdout, dataExpected, diff --git a/test/parallel/test-fs-readfile-pipe.js b/test/parallel/test-fs-readfile-pipe.js index 79d5699fef64a4..782265e8ce47b5 100644 --- a/test/parallel/test-fs-readfile-pipe.js +++ b/test/parallel/test-fs-readfile-pipe.js @@ -43,10 +43,7 @@ const filename = fixtures.path('readfile_pipe_test.txt'); const dataExpected = fs.readFileSync(filename).toString(); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; -exec(cmd, common.mustSucceed((stdout, stderr) => { +exec(...common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`, common.mustSucceed((stdout, stderr) => { assert.strictEqual( stdout, dataExpected, diff --git a/test/parallel/test-fs-readfilesync-pipe-large.js b/test/parallel/test-fs-readfilesync-pipe-large.js index 5450337c4f680a..3a8909797edd2b 100644 --- a/test/parallel/test-fs-readfilesync-pipe-large.js +++ b/test/parallel/test-fs-readfilesync-pipe-large.js @@ -22,12 +22,10 @@ tmpdir.refresh(); fs.writeFileSync(filename, dataExpected); const exec = require('child_process').exec; -const f = JSON.stringify(__filename); -const node = JSON.stringify(process.execPath); -const cmd = `cat ${filename} | ${node} ${f} child`; +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; exec( cmd, - { maxBuffer: 1000000 }, + { ...opts, maxBuffer: 1000000 }, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, dataExpected); assert.strictEqual(stderr, ''); diff --git a/test/parallel/test-fs-write-sigxfsz.js b/test/parallel/test-fs-write-sigxfsz.js index b746faeeb47214..d5290d7d84216d 100644 --- a/test/parallel/test-fs-write-sigxfsz.js +++ b/test/parallel/test-fs-write-sigxfsz.js @@ -19,8 +19,8 @@ if (process.argv[2] === 'child') { tmpdir.refresh(); fs.writeFileSync(filename, '.'.repeat(1 << 16)); // Exceeds RLIMIT_FSIZE. } else { - const cmd = `ulimit -f 1 && '${process.execPath}' '${__filename}' child`; - const result = child_process.spawnSync('/bin/sh', ['-c', cmd]); + const [cmd, opts] = common.escapePOSIXShell`ulimit -f 1 && "${process.execPath}" "${__filename}" child`; + const result = child_process.spawnSync('/bin/sh', ['-c', cmd], opts); const haystack = result.stderr.toString(); const needle = 'Error: EFBIG: file too large, write'; const ok = haystack.includes(needle); diff --git a/test/parallel/test-permission-allow-child-process-cli.js b/test/parallel/test-permission-allow-child-process-cli.js index 1216e29886a443..f28aae9cdd1ce2 100644 --- a/test/parallel/test-permission-allow-child-process-cli.js +++ b/test/parallel/test-permission-allow-child-process-cli.js @@ -15,20 +15,12 @@ if (process.argv[2] === 'child') { assert.ok(process.permission.has('child')); } -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (args) => childProcess.execSync( - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath } }, -); - // When a permission is set by cli, the process shouldn't be able // to spawn unless --allow-child-process is sent { // doesNotThrow childProcess.spawnSync(process.execPath, ['--version']); - execNode('--version'); + childProcess.exec(...common.escapePOSIXShell`"${process.execPath}" --version`); childProcess.fork(__filename, ['child']); childProcess.execFileSync(process.execPath, ['--version']); } diff --git a/test/parallel/test-permission-child-process-cli.js b/test/parallel/test-permission-child-process-cli.js index 5a9c6345aaf946..76586a1c538bed 100644 --- a/test/parallel/test-permission-child-process-cli.js +++ b/test/parallel/test-permission-child-process-cli.js @@ -15,14 +15,6 @@ if (process.argv[2] === 'child') { assert.ok(!process.permission.has('child')); } -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (args) => [ - `"${common.isWindows ? process.execPath : '$NODE'}" ${args}`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath } }, -]; - // When a permission is set by cli, the process shouldn't be able // to spawn { @@ -39,13 +31,13 @@ const execNode = (args) => [ permission: 'ChildProcess', })); assert.throws(() => { - childProcess.exec(...execNode('--version')); + childProcess.exec(...common.escapePOSIXShell`"${process.execPath}" --version`); }, common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'ChildProcess', })); assert.throws(() => { - childProcess.execSync(...execNode('--version')); + childProcess.execSync(...common.escapePOSIXShell`"${process.execPath}" --version`); }, common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'ChildProcess', @@ -57,13 +49,13 @@ const execNode = (args) => [ permission: 'ChildProcess', })); assert.throws(() => { - childProcess.execFile(...execNode('--version')); + childProcess.execFile(...common.escapePOSIXShell`"${process.execPath}" --version`); }, common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'ChildProcess', })); assert.throws(() => { - childProcess.execFileSync(...execNode('--version')); + childProcess.execFileSync(...common.escapePOSIXShell`"${process.execPath}" --version`); }, common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'ChildProcess', diff --git a/test/parallel/test-preload-self-referential.js b/test/parallel/test-preload-self-referential.js index 2624527deb3984..867e1c67983c83 100644 --- a/test/parallel/test-preload-self-referential.js +++ b/test/parallel/test-preload-self-referential.js @@ -13,8 +13,8 @@ if (!common.isMainThread) const selfRefModule = fixtures.path('self_ref_module'); const fixtureA = fixtures.path('printA.js'); -exec(`"${nodeBinary}" -r self_ref "${fixtureA}"`, { cwd: selfRefModule }, - (err, stdout, stderr) => { - assert.ifError(err); +const [cmd, opts] = common.escapePOSIXShell`"${nodeBinary}" -r self_ref "${fixtureA}"`; +exec(cmd, { ...opts, cwd: selfRefModule }, + common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, 'A\n'); - }); + })); diff --git a/test/parallel/test-preload-worker.js b/test/parallel/test-preload-worker.js index 3e9134b470cf37..552698c2fce49c 100644 --- a/test/parallel/test-preload-worker.js +++ b/test/parallel/test-preload-worker.js @@ -7,4 +7,4 @@ const { exec } = require('child_process'); const kNodeBinary = process.argv[0]; -exec(`"${kNodeBinary}" -r "${worker}" -pe "1+1"`, common.mustSucceed()); +exec(...common.escapePOSIXShell`"${kNodeBinary}" -r "${worker}" -pe "1+1"`, common.mustSucceed()); diff --git a/test/parallel/test-stdin-from-file.js b/test/parallel/test-stdin-from-file.js index 67404c84c9830b..c8301ff4598040 100644 --- a/test/parallel/test-stdin-from-file.js +++ b/test/parallel/test-stdin-from-file.js @@ -10,15 +10,11 @@ const stdoutScript = fixtures.path('echo-close-check.js'); const tmpFile = tmpdir.resolve('stdin.txt'); const string = fixtures.utf8TestText; -const cmd = `"${process.argv[0]}" "${stdoutScript}" < "${tmpFile}"`; - tmpdir.refresh(); -console.log(`${cmd}\n\n`); - fs.writeFileSync(tmpFile, string); -childProcess.exec(cmd, common.mustCall(function(err, stdout, stderr) { +childProcess.exec(...common.escapePOSIXShell`"${process.argv0}" "${stdoutScript}" < "${tmpFile}"`, common.mustCall(function(err, stdout, stderr) { fs.unlinkSync(tmpFile); assert.ifError(err); diff --git a/test/parallel/test-stdio-closed.js b/test/parallel/test-stdio-closed.js index cc9f1e86ccbf6c..3d2cb88a476f29 100644 --- a/test/parallel/test-stdio-closed.js +++ b/test/parallel/test-stdio-closed.js @@ -29,8 +29,8 @@ if (process.argv[2] === 'child') { } // Run the script in a shell but close stdout and stderr. -const cmd = `"${process.execPath}" "${__filename}" child 1>&- 2>&-`; -const proc = spawn('/bin/sh', ['-c', cmd], { stdio: 'inherit' }); +const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child 1>&- 2>&-`; +const proc = spawn('/bin/sh', ['-c', cmd], { ...opts, stdio: 'inherit' }); proc.on('exit', common.mustCall(function(exitCode) { assert.strictEqual(exitCode, 0); diff --git a/test/parallel/test-stdout-close-catch.js b/test/parallel/test-stdout-close-catch.js index 924b52715a54f3..8a06acfda0568a 100644 --- a/test/parallel/test-stdout-close-catch.js +++ b/test/parallel/test-stdout-close-catch.js @@ -7,12 +7,9 @@ const { getSystemErrorName } = require('util'); const testScript = fixtures.path('catch-stdout-error.js'); -const cmd = `${JSON.stringify(process.execPath)} ` + - `${JSON.stringify(testScript)} | ` + - `${JSON.stringify(process.execPath)} ` + - '-pe "process.stdin.on(\'data\' , () => process.exit(1))"'; - -const child = child_process.exec(cmd); +const child = child_process.exec( + ...common.escapePOSIXShell`"${process.execPath}" "${testScript}" | "${process.execPath}" -pe "process.stdin.on('data' , () => process.exit(1))"` +); let output = ''; child.stderr.on('data', function(c) { @@ -21,12 +18,7 @@ child.stderr.on('data', function(c) { child.on('close', common.mustCall(function(code) { - try { - output = JSON.parse(output); - } catch { - console.error(output); - process.exit(1); - } + output = JSON.parse(output); assert.strictEqual(output.code, 'EPIPE'); assert.strictEqual(getSystemErrorName(output.errno), 'EPIPE'); diff --git a/test/parallel/test-stdout-to-file.js b/test/parallel/test-stdout-to-file.js index 9114f22443139c..761c26f820dc4d 100644 --- a/test/parallel/test-stdout-to-file.js +++ b/test/parallel/test-stdout-to-file.js @@ -13,9 +13,6 @@ const tmpFile = tmpdir.resolve('stdout.txt'); tmpdir.refresh(); function test(size, useBuffer, cb) { - const cmd = `"${process.argv[0]}" "${ - useBuffer ? scriptBuffer : scriptString}" ${size} > "${tmpFile}"`; - try { fs.unlinkSync(tmpFile); } catch { @@ -24,7 +21,9 @@ function test(size, useBuffer, cb) { console.log(`${size} chars to ${tmpFile}...`); - childProcess.exec(cmd, common.mustSucceed(() => { + childProcess.exec(...common.escapePOSIXShell`"${ + process.execPath}" "${useBuffer ? scriptBuffer : scriptString}" ${size} > "${tmpFile + }"`, common.mustSucceed(() => { console.log('done!'); const stat = fs.statSync(tmpFile); diff --git a/test/parallel/test-stream-pipeline-process.js b/test/parallel/test-stream-pipeline-process.js index a535e7263ebf64..2212c702ff9b71 100644 --- a/test/parallel/test-stream-pipeline-process.js +++ b/test/parallel/test-stream-pipeline-process.js @@ -13,14 +13,7 @@ if (process.argv[2] === 'child') { ); } else { const cp = require('child_process'); - cp.exec([ - 'echo', - 'hello', - '|', - `"${process.execPath}"`, - `"${__filename}"`, - 'child', - ].join(' '), common.mustSucceed((stdout) => { + cp.exec(...common.escapePOSIXShell`echo hello | "${process.execPath}" "${__filename}" child`, common.mustSucceed((stdout) => { assert.strictEqual(stdout.split(os.EOL).shift().trim(), 'hello'); })); } diff --git a/test/parallel/test-tls-ecdh.js b/test/parallel/test-tls-ecdh.js index 8c879f850c9b8d..276b713f5ecf70 100644 --- a/test/parallel/test-tls-ecdh.js +++ b/test/parallel/test-tls-ecdh.js @@ -49,10 +49,10 @@ const server = tls.createServer(options, common.mustCall(function(conn) { })); server.listen(0, '127.0.0.1', common.mustCall(function() { - const cmd = `"${common.opensslCli}" s_client -cipher ${ + const cmd = common.escapePOSIXShell`"${common.opensslCli}" s_client -cipher ${ options.ciphers} -connect 127.0.0.1:${this.address().port}`; - exec(cmd, common.mustSucceed((stdout, stderr) => { + exec(...cmd, common.mustSucceed((stdout, stderr) => { assert(stdout.includes(reply)); server.close(); })); diff --git a/test/parallel/test-worker-init-failure.js b/test/parallel/test-worker-init-failure.js index 078329ee68874f..8233116e1e05ba 100644 --- a/test/parallel/test-worker-init-failure.js +++ b/test/parallel/test-worker-init-failure.js @@ -47,9 +47,7 @@ if (process.argv[2] === 'child') { } else { // Limit the number of open files, to force workers to fail. - let testCmd = `ulimit -n ${OPENFILES} && `; - testCmd += `${process.execPath} ${__filename} child`; - const cp = child_process.exec(testCmd); + const cp = child_process.exec(...common.escapePOSIXShell`ulimit -n ${OPENFILES} && "${process.execPath}" "${__filename}" child`); // Turn on the child streams for debugging purposes. let stdout = ''; diff --git a/test/sequential/test-child-process-emfile.js b/test/sequential/test-child-process-emfile.js index 8091e54a5c5bde..8ee6dd52e30aa7 100644 --- a/test/sequential/test-child-process-emfile.js +++ b/test/sequential/test-child-process-emfile.js @@ -30,12 +30,14 @@ const fs = require('fs'); const ulimit = Number(child_process.execSync('ulimit -Hn')); if (ulimit > 64 || Number.isNaN(ulimit)) { + const [cmd, opts] = common.escapePOSIXShell`ulimit -n 64 && "${process.execPath}" "${__filename}"`; // Sorry about this nonsense. It can be replaced if // https://github.com/nodejs/node-v0.x-archive/pull/2143#issuecomment-2847886 // ever happens. const result = child_process.spawnSync( '/bin/sh', - ['-c', `ulimit -n 64 && '${process.execPath}' '${__filename}'`] + ['-c', cmd], + opts, ); assert.strictEqual(result.stdout.toString(), ''); assert.strictEqual(result.stderr.toString(), ''); diff --git a/test/sequential/test-cli-syntax-bad.js b/test/sequential/test-cli-syntax-bad.js index c16de3b50133ec..e967ff36ac28d8 100644 --- a/test/sequential/test-cli-syntax-bad.js +++ b/test/sequential/test-cli-syntax-bad.js @@ -1,16 +1,14 @@ 'use strict'; -require('../common'); +const common = require('../common'); const { exec } = require('child_process'); const { test } = require('node:test'); const fixtures = require('../common/fixtures'); -const node = process.execPath; - // Test both sets of arguments that check syntax const syntaxArgs = [ - ['-c'], - ['--check'], + '-c', + '--check', ]; // Match on the name of the `Error` but not the message as it is different @@ -27,13 +25,10 @@ const syntaxErrorRE = /^SyntaxError: \b/m; const path = fixtures.path(file); // Loop each possible option, `-c` or `--check` - syntaxArgs.forEach((args) => { - test(`Checking syntax for ${file} with ${args.join(' ')}`, async (t) => { - const _args = args.concat(path); - const cmd = [node, ..._args].join(' '); - + syntaxArgs.forEach((flag) => { + test(`Checking syntax for ${file} with ${flag}`, async (t) => { try { - const { stdout, stderr } = await execPromise(cmd); + const { stdout, stderr } = await execNode(flag, path); // No stdout should be produced t.assert.strictEqual(stdout, ''); @@ -51,9 +46,9 @@ const syntaxErrorRE = /^SyntaxError: \b/m; }); // Helper function to promisify exec -function execPromise(cmd) { +function execNode(flag, path) { const { promise, resolve, reject } = Promise.withResolvers(); - exec(cmd, (err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" ${flag} "${path}"`, (err, stdout, stderr) => { if (err) return reject({ ...err, stdout, stderr }); resolve({ stdout, stderr }); }); diff --git a/test/sequential/test-cli-syntax-file-not-found.js b/test/sequential/test-cli-syntax-file-not-found.js index 189d6b8d58f349..203074a0e7aa4f 100644 --- a/test/sequential/test-cli-syntax-file-not-found.js +++ b/test/sequential/test-cli-syntax-file-not-found.js @@ -5,15 +5,6 @@ const assert = require('assert'); const { exec } = require('child_process'); const fixtures = require('../common/fixtures'); -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (flag, file, callback) => exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${flag} "${common.isWindows ? file : '$FILE'}"`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath, FILE: file } }, - callback, -); - // Test both sets of arguments that check syntax const syntaxArgs = [ '-c', @@ -31,7 +22,7 @@ const notFoundRE = /^Error: Cannot find module/m; // Loop each possible option, `-c` or `--check` syntaxArgs.forEach(function(flag) { - execNode(flag, file, common.mustCall((err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" ${flag} "${file}"`, common.mustCall((err, stdout, stderr) => { // No stdout should be produced assert.strictEqual(stdout, ''); diff --git a/test/sequential/test-cli-syntax-good.js b/test/sequential/test-cli-syntax-good.js index 24c68781876556..00d4e05e246cb7 100644 --- a/test/sequential/test-cli-syntax-good.js +++ b/test/sequential/test-cli-syntax-good.js @@ -5,15 +5,6 @@ const assert = require('assert'); const { exec } = require('child_process'); const fixtures = require('../common/fixtures'); -// The execPath might contain chars that should be escaped in a shell context. -// On non-Windows, we can pass the path via the env; `"` is not a valid char on -// Windows, so we can simply pass the path. -const execNode = (flag, file, callback) => exec( - `"${common.isWindows ? process.execPath : '$NODE'}" ${flag} "${common.isWindows ? file : '$FILE'}"`, - common.isWindows ? undefined : { env: { ...process.env, NODE: process.execPath, FILE: file } }, - callback, -); - // Test both sets of arguments that check syntax const syntaxArgs = [ '-c', @@ -33,7 +24,7 @@ const syntaxArgs = [ // Loop each possible option, `-c` or `--check` syntaxArgs.forEach(function(flag) { - execNode(flag, file, common.mustCall((err, stdout, stderr) => { + exec(...common.escapePOSIXShell`"${process.execPath}" ${flag} "${file}"`, common.mustCall((err, stdout, stderr) => { if (err) { console.log('-- stdout --'); console.log(stdout); diff --git a/test/sequential/test-init.js b/test/sequential/test-init.js index b64fb23daeabc7..7195369e0e4f8e 100644 --- a/test/sequential/test-init.js +++ b/test/sequential/test-init.js @@ -35,8 +35,7 @@ if (process.env.TEST_INIT) { process.env.TEST_INIT = 1; function test(file, expected) { - const path = `"${process.execPath}" ${file}`; - child.exec(path, { env: process.env }, common.mustSucceed((out) => { + child.exec(...common.escapePOSIXShell`"${process.execPath}" "${file}"`, common.mustSucceed((out) => { assert.strictEqual(out, expected, `'node ${file}' failed!`); })); } From 7c9f0060f7515677de6743cac9c6636256473d70 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 26 Sep 2024 00:24:43 +0200 Subject: [PATCH 02/12] fixup! test: add `escapePOSIXShell` util --- test/common/escapePOSIXShell.js | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/common/escapePOSIXShell.js b/test/common/escapePOSIXShell.js index 442f663ecb7b03..a15d39421583df 100644 --- a/test/common/escapePOSIXShell.js +++ b/test/common/escapePOSIXShell.js @@ -1,26 +1,26 @@ 'use strict'; /** -* Escape quoted values in a string template literal. On Windows, this function -* does not escape anything (which is fine for paths, as `"` is not a valid char -* in a path on Windows), so you should use it only to escape paths – or other -* values on tests which are skipped on Windows. -* @returns {[string, object | undefined]} An array that can be passed as -* arguments to `exec` or `execSync`. -*/ + * Escape quoted values in a string template literal. On Windows, this function + * does not escape anything (which is fine for paths, as `"` is not a valid char + * in a path on Windows), so you should use it only to escape paths – or other + * values on tests which are skipped on Windows. + * @returns {[string, object | undefined]} An array that can be passed as + * arguments to `exec` or `execSync`. + */ module.exports = function escapePOSIXShell(cmdParts, ...args) { - if (common.isWindows) { - // On Windows, paths cannot contain `"`, so we can return the string unchanged. - return [String.raw({ raw: cmdParts }, ...args)]; - } - // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. - const env = { ...process.env }; - let cmd = cmdParts[0]; - for (let i = 0; i < args.length; i++) { - const envVarName = `ESCAPED_${i}`; - env[envVarName] = args[i]; - cmd += '${' + envVarName + '}' + cmdParts[i + 1]; - } + if (process.platform === 'win32') { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } - return [cmd, { env }]; + return [cmd, { env }]; }; From 354271c444007960f733a381121f0c05ad448d3c Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 26 Sep 2024 11:43:19 +0200 Subject: [PATCH 03/12] fixup! test: add `escapePOSIXShell` util --- test/common/escapePOSIXShell.js | 26 ------------------- test/common/index.js | 27 +++++++++++++++++++- test/fixtures/snapshot/child-process-sync.js | 21 ++++++++++++++- 3 files changed, 46 insertions(+), 28 deletions(-) delete mode 100644 test/common/escapePOSIXShell.js diff --git a/test/common/escapePOSIXShell.js b/test/common/escapePOSIXShell.js deleted file mode 100644 index a15d39421583df..00000000000000 --- a/test/common/escapePOSIXShell.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -/** - * Escape quoted values in a string template literal. On Windows, this function - * does not escape anything (which is fine for paths, as `"` is not a valid char - * in a path on Windows), so you should use it only to escape paths – or other - * values on tests which are skipped on Windows. - * @returns {[string, object | undefined]} An array that can be passed as - * arguments to `exec` or `execSync`. - */ -module.exports = function escapePOSIXShell(cmdParts, ...args) { - if (process.platform === 'win32') { - // On Windows, paths cannot contain `"`, so we can return the string unchanged. - return [String.raw({ raw: cmdParts }, ...args)]; - } - // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. - const env = { ...process.env }; - let cmd = cmdParts[0]; - for (let i = 0; i < args.length; i++) { - const envVarName = `ESCAPED_${i}`; - env[envVarName] = args[i]; - cmd += '${' + envVarName + '}' + cmdParts[i + 1]; - } - - return [cmd, { env }]; -}; diff --git a/test/common/index.js b/test/common/index.js index 5bcfb4a5de6468..14714adb9d49c3 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -34,7 +34,6 @@ const { inspect, getCallSite } = require('util'); const { isMainThread } = require('worker_threads'); const { isModuleNamespaceObject } = require('util/types'); -const escapePOSIXShell = require('./escapePOSIXShell'); const tmpdir = require('./tmpdir'); const bits = ['arm64', 'loong64', 'mips', 'mipsel', 'ppc64', 'riscv64', 's390x', 'x64'] .includes(process.arch) ? 64 : 32; @@ -887,6 +886,32 @@ function spawnPromisified(...args) { }); } +/** + * Escape quoted values in a string template literal. On Windows, this function + * does not escape anything (which is fine for paths, as `"` is not a valid char + * in a path on Windows), so you should use it only to escape paths – or other + * values on tests which are skipped on Windows. + * This function is meant to be used for tagged template strings. + * @returns {[string, object | undefined]} An array that can be passed as + * arguments to `exec` or `execSync`. + */ +function escapePOSIXShell(cmdParts, ...args) { + if (common.isWindows) { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +}; + function getPrintedStackTrace(stderr) { const lines = stderr.split('\n'); diff --git a/test/fixtures/snapshot/child-process-sync.js b/test/fixtures/snapshot/child-process-sync.js index 52c3e935c59767..d90201277f2b91 100644 --- a/test/fixtures/snapshot/child-process-sync.js +++ b/test/fixtures/snapshot/child-process-sync.js @@ -4,7 +4,26 @@ const { setDeserializeMainFunction, isBuildingSnapshot } = require('v8').startupSnapshot; -const escapePOSIXShell = require('../../common/escapePOSIXShell'); + +/** + * @see {import('../../common').escapePOSIXShell} + */ +function escapePOSIXShell(cmdParts, ...args) { + if (process.platform === 'win32') { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +} function spawn() { const { spawnSync, execFileSync, execSync } = require('child_process'); From aaf5b0a013f74bed4cc989d2a6c708276cbdc173 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 27 Sep 2024 15:53:07 +0200 Subject: [PATCH 04/12] fixup! test: add `escapePOSIXShell` util --- test/parallel/test-child-process-bad-stdio.js | 2 +- .../test-child-process-exec-encoding.js | 2 +- test/parallel/test-cli-eval.js | 26 ++++++++++--------- test/parallel/test-env-var-no-warnings.js | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/test/parallel/test-child-process-bad-stdio.js b/test/parallel/test-child-process-bad-stdio.js index 6ede126fca7bea..90e8ddd0215a2b 100644 --- a/test/parallel/test-child-process-bad-stdio.js +++ b/test/parallel/test-child-process-bad-stdio.js @@ -28,7 +28,7 @@ ChildProcess.prototype.spawn = function() { function createChild(options, callback) { const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; - options = { ...options, env: { ...opts.env, ...options.env } }; + options = { ...options, env: { ...opts?.env, ...options.env } }; return cp.exec(cmd, options, common.mustCall(callback)); } diff --git a/test/parallel/test-child-process-exec-encoding.js b/test/parallel/test-child-process-exec-encoding.js index 5e59ff1fd56df0..059e300e4e760e 100644 --- a/test/parallel/test-child-process-exec-encoding.js +++ b/test/parallel/test-child-process-exec-encoding.js @@ -14,7 +14,7 @@ if (process.argv[2] === 'child') { const expectedStderr = `${stderrData}\n`; function run(options, callback) { const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; - options = { ...options, env: { ...opts.env, ...options.env } }; + options = { ...options, env: { ...opts?.env, ...options.env } }; cp.exec(cmd, options, common.mustSucceed((stdout, stderr) => { callback(stdout, stderr); diff --git a/test/parallel/test-cli-eval.js b/test/parallel/test-cli-eval.js index dc93287f800d77..1bca12df43dcab 100644 --- a/test/parallel/test-cli-eval.js +++ b/test/parallel/test-cli-eval.js @@ -127,22 +127,24 @@ child.exec(...common.escapePOSIXShell`"${process.execPath}" --use-strict -p proc { const emptyFile = fixtures.path('empty.js'); - child.exec(...common.escapePOSIXShell`"${process.execPath}" -e "${`require("child_process").fork(${JSON.stringify(emptyFile)})`}"`, - common.mustSucceed((stdout, stderr) => { - assert.strictEqual(stdout, ''); - assert.strictEqual(stderr, ''); - })); + common.spawnPromisified(process.execPath, ['-e', `require("child_process").fork(${JSON.stringify(emptyFile)})`]) + .then(common.mustCall(({ stdout, stderr, code }) => { + assert.strictEqual(stdout, ''); + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); + })); // Make sure that monkey-patching process.execArgv doesn't cause child_process // to incorrectly munge execArgv. - child.exec(...common.escapePOSIXShell`"${process.execPath}" -e "${ + common.spawnPromisified(process.execPath, [ + '-e', 'process.execArgv = [\'-e\', \'console.log(42)\', \'thirdArg\'];' + - `require('child_process').fork(${JSON.stringify(emptyFile)})` - }"`, - common.mustSucceed((stdout, stderr) => { - assert.strictEqual(stdout, '42\n'); - assert.strictEqual(stderr, ''); - })); + `require('child_process').fork(${JSON.stringify(emptyFile)})`, + ]).then(common.mustCall(({ stdout, stderr, code }) => { + assert.strictEqual(stdout, '42\n'); + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); + })); } // Regression test for https://github.com/nodejs/node/issues/8534. diff --git a/test/parallel/test-env-var-no-warnings.js b/test/parallel/test-env-var-no-warnings.js index f186edf72865aa..cee06c8596d3ac 100644 --- a/test/parallel/test-env-var-no-warnings.js +++ b/test/parallel/test-env-var-no-warnings.js @@ -9,7 +9,7 @@ if (process.argv[2] === 'child') { function test(newEnv) { const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`; - cp.exec(cmd, { ...opts, env: { ...opts.env, ...newEnv } }, common.mustCall((err, stdout, stderr) => { + cp.exec(cmd, { ...opts, env: { ...opts?.env, ...newEnv } }, common.mustCall((err, stdout, stderr) => { assert.strictEqual(err, null); assert.strictEqual(stdout, ''); From 779d566c9e32d900edfbf3f69c182b2a0d0b7a97 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 27 Sep 2024 15:58:59 +0200 Subject: [PATCH 05/12] fixup! test: add `escapePOSIXShell` util --- test/parallel/test-cli-eval.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/parallel/test-cli-eval.js b/test/parallel/test-cli-eval.js index 1bca12df43dcab..de12b783dd4de3 100644 --- a/test/parallel/test-cli-eval.js +++ b/test/parallel/test-cli-eval.js @@ -68,13 +68,13 @@ child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval "console.erro // Assert that module loading works. { - child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval "${`require(${JSON.stringify(__filename)})`}"`, - common.mustCall((err, stdout, stderr) => { - assert.strictEqual(err.code, 42); - assert.strictEqual( - stdout, 'Loaded as a module, exiting with status code 42.\n'); - assert.strictEqual(stderr, ''); - })); + common.spawnPromisified(process.execPath, ['--eval', `require(${JSON.stringify(__filename)})`]) + .then(common.mustCall(({ stdout, stderr, code }) => { + assert.strictEqual(stderr, ''); + assert.strictEqual( + stdout, 'Loaded as a module, exiting with status code 42.\n'); + assert.strictEqual(code, 42); + })) } // Check that builtin modules are pre-defined. From 38868c221a4eb27df642dcbdf3b3070495e683f2 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Fri, 27 Sep 2024 16:02:04 +0200 Subject: [PATCH 06/12] fixup! test: add `escapePOSIXShell` util --- test/parallel/test-cli-eval.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-cli-eval.js b/test/parallel/test-cli-eval.js index de12b783dd4de3..24031581fd737e 100644 --- a/test/parallel/test-cli-eval.js +++ b/test/parallel/test-cli-eval.js @@ -74,7 +74,7 @@ child.exec(...common.escapePOSIXShell`"${process.execPath}" --eval "console.erro assert.strictEqual( stdout, 'Loaded as a module, exiting with status code 42.\n'); assert.strictEqual(code, 42); - })) + })); } // Check that builtin modules are pre-defined. From ad045ab4ba05e006262f1127d57fdbce3541370f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sat, 28 Sep 2024 14:41:24 +0200 Subject: [PATCH 07/12] fixup! test: add `escapePOSIXShell` util --- test/fixtures/snapshot/child-process-sync.js | 23 +------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/test/fixtures/snapshot/child-process-sync.js b/test/fixtures/snapshot/child-process-sync.js index d90201277f2b91..5ffacb05357df6 100644 --- a/test/fixtures/snapshot/child-process-sync.js +++ b/test/fixtures/snapshot/child-process-sync.js @@ -5,31 +5,10 @@ const { isBuildingSnapshot } = require('v8').startupSnapshot; -/** - * @see {import('../../common').escapePOSIXShell} - */ -function escapePOSIXShell(cmdParts, ...args) { - if (process.platform === 'win32') { - // On Windows, paths cannot contain `"`, so we can return the string unchanged. - return [String.raw({ raw: cmdParts }, ...args)]; - } - // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. - const env = { ...process.env }; - let cmd = cmdParts[0]; - for (let i = 0; i < args.length; i++) { - const envVarName = `ESCAPED_${i}`; - env[envVarName] = args[i]; - cmd += '${' + envVarName + '}' + cmdParts[i + 1]; - } - - return [cmd, { env }]; -} - function spawn() { const { spawnSync, execFileSync, execSync } = require('child_process'); spawnSync(process.execPath, [ __filename, 'spawnSync' ], { stdio: 'inherit' }); - const [cmd, opts] = escapePOSIXShell`"${process.execPath}" "${__filename}" "execSync"`; - execSync(cmd, { ...opts, stdio: 'inherit'}); + execSync(`"${process.execPath}" "${__filename}" "execSync"`, { stdio: 'inherit' }); execFileSync(process.execPath, [ __filename, 'execFileSync' ], { stdio: 'inherit' }); } From 4e0ede911e8023ade01bf9a30cf5eac77d5da633 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 29 Sep 2024 17:43:41 +0200 Subject: [PATCH 08/12] Apply suggestions from code review Co-authored-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- test/async-hooks/test-callback-error.js | 5 +++-- test/parallel/test-fs-readfilesync-pipe-large.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/async-hooks/test-callback-error.js b/test/async-hooks/test-callback-error.js index 67eb7bab81e9c8..ad70feb70c9571 100644 --- a/test/async-hooks/test-callback-error.js +++ b/test/async-hooks/test-callback-error.js @@ -62,13 +62,14 @@ assert.ok(!arg); let program = process.execPath; let args = [ '--abort-on-uncaught-exception', __filename, 'test_callback_abort' ]; - let options = { encoding: 'utf8' }; + let options = {}; if (!common.isWindows) { [program, options] = common.escapePOSIXShell`ulimit -c 0 && exec "${program}" ${args[0]} "${args[1]}" ${args[2]}`; args = []; options.shell = true; - options.encoding = 'utf8'; } + + options.encoding = 'utf8'; const child = spawnSync(program, args, options); if (common.isWindows) { assert.strictEqual(child.status, 134); diff --git a/test/parallel/test-fs-readfilesync-pipe-large.js b/test/parallel/test-fs-readfilesync-pipe-large.js index 3a8909797edd2b..60c7dccd1acaf2 100644 --- a/test/parallel/test-fs-readfilesync-pipe-large.js +++ b/test/parallel/test-fs-readfilesync-pipe-large.js @@ -25,7 +25,7 @@ const exec = require('child_process').exec; const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child < "${filename}"`; exec( cmd, - { ...opts, maxBuffer: 1000000 }, + { ...opts, maxBuffer: 1_000_000 }, common.mustSucceed((stdout, stderr) => { assert.strictEqual(stdout, dataExpected); assert.strictEqual(stderr, ''); From d07a1ac3ff53aa18137c978c3fd9bf2f02692024 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 29 Sep 2024 19:42:27 +0200 Subject: [PATCH 09/12] Update test/parallel/test-permission-allow-child-process-cli.js Co-authored-by: Livia Medeiros --- test/parallel/test-permission-allow-child-process-cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-permission-allow-child-process-cli.js b/test/parallel/test-permission-allow-child-process-cli.js index f28aae9cdd1ce2..d805c6fb973c3c 100644 --- a/test/parallel/test-permission-allow-child-process-cli.js +++ b/test/parallel/test-permission-allow-child-process-cli.js @@ -20,7 +20,7 @@ if (process.argv[2] === 'child') { { // doesNotThrow childProcess.spawnSync(process.execPath, ['--version']); - childProcess.exec(...common.escapePOSIXShell`"${process.execPath}" --version`); + childProcess.execSync(...common.escapePOSIXShell`"${process.execPath}" --version`); childProcess.fork(__filename, ['child']); childProcess.execFileSync(process.execPath, ['--version']); } From b3663acca2598cc4fa304c4fc16014a2ad74c120 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 29 Sep 2024 19:43:03 +0200 Subject: [PATCH 10/12] docs --- test/common/README.md | 21 +++++++++++++++++++++ test/common/index.js | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/test/common/README.md b/test/common/README.md index 8fe8fea2c1d8f5..2a29c306459ce5 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -112,6 +112,27 @@ Creates a 10 MiB file of all null characters. Indicates if there is more than 1gb of total memory. +### ``escapePOSIXShell`shell command` `` + +Escapes values in a string template literal to pass them as env variable. On Windows, this function +does not escape anything (which is fine for most paths, as `"` is not a valid +char in a path on Windows), so for tests that must pass on Windows, you should +use it only to escape paths, inside double quotes. +This function is meant to be used for tagged template strings. + +```js +childProcess.execSync(...common.escapePOSIXShell`cp "${origin}" "${destination}"`); + +// When you need to specify specific options, and/or additional env variables: +const [cmd, opts] = common.escapePOSIXShell`cp "${origin}" "${destination}"`; +typeof cmd === 'string'; // true +opts === undefined || typeof opts.env === 'object'; // true +childProcess.execSync(cmd, { ...opts, stdio: 'ignore' }); +childProcess.execSync(cmd, { stdio: 'ignore', env: { ...opts?.env, KEY: 'value' } }); +``` + +When possible, avoid using a shell; that way, there's no need to escape values. + ### `expectsError(validator[, exact])` * `validator` [\][] | [\][] | diff --git a/test/common/index.js b/test/common/index.js index 14714adb9d49c3..b95e812c5a9fab 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -887,7 +887,7 @@ function spawnPromisified(...args) { } /** - * Escape quoted values in a string template literal. On Windows, this function + * Escape values in a string template literal. On Windows, this function * does not escape anything (which is fine for paths, as `"` is not a valid char * in a path on Windows), so you should use it only to escape paths – or other * values on tests which are skipped on Windows. From 7215a860d5a294083b67a955b3ac4da15f8fe35f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 29 Sep 2024 19:49:22 +0200 Subject: [PATCH 11/12] fixup! docs --- test/common/README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/common/README.md b/test/common/README.md index 2a29c306459ce5..687d60cc948c40 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -121,14 +121,20 @@ use it only to escape paths, inside double quotes. This function is meant to be used for tagged template strings. ```js -childProcess.execSync(...common.escapePOSIXShell`cp "${origin}" "${destination}"`); +const { escapePOSIXShell } = require('../common'); +const fixtures = require('../common/fixtures'); +const { execSync } = require('node:child_process'); +const origin = fixtures.path('origin'); +const origin = fixtures.path('destination'); + +execSync(...escapePOSIXShell`cp "${origin}" "${destination}"`); // When you need to specify specific options, and/or additional env variables: -const [cmd, opts] = common.escapePOSIXShell`cp "${origin}" "${destination}"`; -typeof cmd === 'string'; // true -opts === undefined || typeof opts.env === 'object'; // true -childProcess.execSync(cmd, { ...opts, stdio: 'ignore' }); -childProcess.execSync(cmd, { stdio: 'ignore', env: { ...opts?.env, KEY: 'value' } }); +const [cmd, opts] = escapePOSIXShell`cp "${origin}" "${destination}"`; +console.log(typeof cmd === 'string'); // true +console.log(opts === undefined || typeof opts.env === 'object'); // true +execSync(cmd, { ...opts, stdio: 'ignore' }); +execSync(cmd, { stdio: 'ignore', env: { ...opts?.env, KEY: 'value' } }); ``` When possible, avoid using a shell; that way, there's no need to escape values. From e0a22b50af2f39cd91d0ec2b19e68aa0c9e2e684 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 29 Sep 2024 19:52:07 +0200 Subject: [PATCH 12/12] fixup! docs --- test/common/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/common/README.md b/test/common/README.md index 687d60cc948c40..5f5ff75fca2431 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -125,7 +125,7 @@ const { escapePOSIXShell } = require('../common'); const fixtures = require('../common/fixtures'); const { execSync } = require('node:child_process'); const origin = fixtures.path('origin'); -const origin = fixtures.path('destination'); +const destination = fixtures.path('destination'); execSync(...escapePOSIXShell`cp "${origin}" "${destination}"`);