-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: correctly double escape when script runs a known .cmd file (#80)
* fix: correctly double escape when script runs a known .cmd file * chore(tests): add some integration tests
- Loading branch information
Showing
5 changed files
with
308 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,63 +1,129 @@ | ||
'use strict' | ||
|
||
const { writeFileSync: writeFile, unlinkSync: unlink, chmodSync: chmod } = require('fs') | ||
const { join } = require('path') | ||
const t = require('tap') | ||
const promiseSpawn = require('@npmcli/promise-spawn') | ||
|
||
const escape = require('../lib/escape.js') | ||
const isWindows = process.platform === 'win32' | ||
|
||
t.test('sh', (t) => { | ||
t.test('returns empty quotes when input is empty', async (t) => { | ||
const input = '' | ||
const output = escape.sh(input) | ||
t.equal(output, `''`, 'returned empty single quotes') | ||
}) | ||
const expectations = [ | ||
['', `''`], | ||
['test', 'test'], | ||
['test words', `'test words'`], | ||
['$1', `'$1'`], | ||
['"$1"', `'"$1"'`], | ||
[`'$1'`, `\\''$1'\\'`], | ||
['\\$1', `'\\$1'`], | ||
['--arg="$1"', `'--arg="$1"'`], | ||
['--arg=npm exec -c "$1"', `'--arg=npm exec -c "$1"'`], | ||
[`--arg=npm exec -c '$1'`, `'--arg=npm exec -c '\\''$1'\\'`], | ||
[`'--arg=npm exec -c "$1"'`, `\\''--arg=npm exec -c "$1"'\\'`], | ||
] | ||
|
||
t.test('returns plain string if quotes are not necessary', async (t) => { | ||
const input = 'test' | ||
const output = escape.sh(input) | ||
t.equal(output, input, 'returned plain string') | ||
}) | ||
for (const [input, expectation] of expectations) { | ||
t.equal(escape.sh(input), expectation, | ||
`expected to escape \`${input}\` to \`${expectation}\``) | ||
} | ||
|
||
t.test('wraps in single quotes if special character is present', async (t) => { | ||
const input = 'test words' | ||
const output = escape.sh(input) | ||
t.equal(output, `'test words'`, 'wrapped in single quotes') | ||
t.test('integration', { skip: isWindows && 'posix only' }, async (t) => { | ||
const dir = t.testdir() | ||
|
||
for (const [input] of expectations) { | ||
const filename = join(dir, 'posix.sh') | ||
const script = `#!/usr/bin/env sh\nnode -p process.argv[1] -- ${escape.sh(input)}` | ||
writeFile(filename, script) | ||
chmod(filename, '0755') | ||
const p = await promiseSpawn('sh', ['-c', filename], { stdioString: true }) | ||
const stdout = p.stdout.trim() | ||
t.equal(input, stdout, 'actual output matches input') | ||
unlink(filename) | ||
} | ||
|
||
t.end() | ||
}) | ||
|
||
t.end() | ||
}) | ||
|
||
t.test('cmd', (t) => { | ||
t.test('returns empty quotes when input is empty', async (t) => { | ||
const input = '' | ||
const output = escape.cmd(input) | ||
t.equal(output, '""', 'returned empty double quotes') | ||
}) | ||
const expectations = [ | ||
['', '""'], | ||
['test', 'test'], | ||
['%PATH%', '%%PATH%%'], | ||
['%PATH%', '%%PATH%%', true], | ||
['"%PATH%"', '^"\\^"%%PATH%%\\^"^"'], | ||
['"%PATH%"', '^^^"\\^^^"%%PATH%%\\^^^"^^^"', true], | ||
[`'%PATH%'`, `'%%PATH%%'`], | ||
[`'%PATH%'`, `'%%PATH%%'`, true], | ||
['\\%PATH%', '\\%%PATH%%'], | ||
['\\%PATH%', '\\%%PATH%%', true], | ||
['--arg="%PATH%"', '^"--arg=\\^"%%PATH%%\\^"^"'], | ||
['--arg="%PATH%"', '^^^"--arg=\\^^^"%%PATH%%\\^^^"^^^"', true], | ||
['--arg=npm exec -c "%PATH%"', '^"--arg=npm exec -c \\^"%%PATH%%\\^"^"'], | ||
['--arg=npm exec -c "%PATH%"', '^^^"--arg=npm exec -c \\^^^"%%PATH%%\\^^^"^^^"', true], | ||
[`--arg=npm exec -c '%PATH%'`, `^"--arg=npm exec -c '%%PATH%%'^"`], | ||
[`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm exec -c '%%PATH%%'^^^"`, true], | ||
[`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm exec -c \\^"%%PATH%%\\^"'^"`], | ||
[`'--arg=npm exec -c "%PATH%"'`, `^^^"'--arg=npm exec -c \\^^^"%%PATH%%\\^^^"'^^^"`, true], | ||
['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program Files\\test.bat\\^"^"'], | ||
['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program Files\\test.bat\\^^^"^^^"', true], | ||
['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program Files\\test%%.bat\\^"^"'], | ||
['"C:\\Program Files\\test%.bat"', '^^^"\\^^^"C:\\Program Files\\test%%.bat\\^^^"^^^"', true], | ||
['% % %', '^"%% %% %%^"'], | ||
['% % %', '^^^"%% %% %%^^^"', true], | ||
['hello^^^^^^', 'hello^^^^^^^^^^^^'], | ||
['hello^^^^^^', 'hello^^^^^^^^^^^^^^^^^^^^^^^^', true], | ||
['hello world', '^"hello world^"'], | ||
['hello world', '^^^"hello world^^^"', true], | ||
['hello"world', '^"hello\\^"world^"'], | ||
['hello"world', '^^^"hello\\^^^"world^^^"', true], | ||
['hello""world', '^"hello\\^"\\^"world^"'], | ||
['hello""world', '^^^"hello\\^^^"\\^^^"world^^^"', true], | ||
['hello\\world', 'hello\\world'], | ||
['hello\\world', 'hello\\world', true], | ||
['hello\\\\world', 'hello\\\\world'], | ||
['hello\\\\world', 'hello\\\\world', true], | ||
['hello\\"world', '^"hello\\\\\\^"world^"'], | ||
['hello\\"world', '^^^"hello\\\\\\^^^"world^^^"', true], | ||
['hello\\\\"world', '^"hello\\\\\\\\\\^"world^"'], | ||
['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', true], | ||
['hello world\\', '^"hello world\\\\^"'], | ||
['hello world\\', '^^^"hello world\\\\^^^"', true], | ||
['hello %PATH%', '^"hello %%PATH%%^"'], | ||
['hello %PATH%', '^^^"hello %%PATH%%^^^"', true], | ||
] | ||
|
||
t.test('returns plain string if quotes are not necessary', async (t) => { | ||
const input = 'test' | ||
const output = escape.cmd(input) | ||
t.equal(output, input, 'returned plain string') | ||
}) | ||
for (const [input, expectation, double] of expectations) { | ||
const msg = `expected to${double ? ' double' : ''} escape \`${input}\` to \`${expectation}\`` | ||
t.equal(escape.cmd(input, double), expectation, msg) | ||
} | ||
|
||
t.test('wraps in double quotes when necessary', async (t) => { | ||
const input = 'test words' | ||
const output = escape.cmd(input) | ||
t.equal(output, '^"test words^"', 'wrapped in double quotes') | ||
}) | ||
t.test('integration', { skip: !isWindows && 'Windows only' }, async (t) => { | ||
const dir = t.testdir() | ||
|
||
t.test('doubles up backslashes at end of input', async (t) => { | ||
const input = 'one \\ two \\' | ||
const output = escape.cmd(input) | ||
t.equal(output, '^"one \\ two \\\\^"', 'doubles backslash at end of string') | ||
}) | ||
for (const [input,, double] of expectations) { | ||
const filename = join(dir, 'win.cmd') | ||
if (double) { | ||
const shimFile = join(dir, 'shim.cmd') | ||
const shim = `@echo off\nnode -p process.argv[1] -- %*` | ||
writeFile(shimFile, shim) | ||
const script = `@echo off\n"${shimFile}" ${escape.cmd(input, double)}` | ||
writeFile(filename, script) | ||
} else { | ||
const script = `@echo off\nnode -p process.argv[1] -- ${escape.cmd(input)}` | ||
writeFile(filename, script) | ||
} | ||
const p = await promiseSpawn('cmd', ['/d', '/s', '/c', filename], { stdioString: true }) | ||
const stdout = p.stdout.trim() | ||
t.equal(input, stdout, 'actual output matches input') | ||
unlink(filename) | ||
} | ||
|
||
t.test('doubles up backslashes immediately before a double quote', async (t) => { | ||
const input = 'one \\"' | ||
const output = escape.cmd(input) | ||
t.equal(output, '^"one \\\\\\^"^"', 'doubles backslash before double quote') | ||
t.end() | ||
}) | ||
|
||
t.test('backslash escapes double quotes', async (t) => { | ||
const input = '"test"' | ||
const output = escape.cmd(input) | ||
t.equal(output, '^"\\^"test\\^"^"', 'escaped double quotes') | ||
}) | ||
t.end() | ||
}) |
Oops, something went wrong.