diff --git a/lib/util/escape.test.js b/lib/util/escape.test.js new file mode 100644 index 0000000..4fa8337 --- /dev/null +++ b/lib/util/escape.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const escape = require('./escape'); + +// Expected values follow the cmd.exe escaping rules documented at +// https://qntm.org/cmd and the meta-char list in +// http://www.robvanderwoude.com/escapechars.php + +describe('escape.command', () => { + it('should escape meta chars with a caret', () => { + expect(escape.command('foo bar')).toBe('foo^ bar'); + expect(escape.command('a&b|ce')).toBe('a^&b^|c^e'); + expect(escape.command('(x)')).toBe('^(x^)'); + expect(escape.command('100%')).toBe('100^%'); + }); + + it('should not add surrounding quotes', () => { + expect(escape.command('foo')).toBe('foo'); + }); +}); + +describe('escape.argument', () => { + it('should wrap the argument in double quotes and escape meta chars', () => { + expect(escape.argument('foo')).toBe('^"foo^"'); + expect(escape.argument('foo bar')).toBe('^"foo^ bar^"'); + expect(escape.argument('%PATH%')).toBe('^"^%PATH^%^"'); + expect(escape.argument('a&b|ce')).toBe('^"a^&b^|c^e^"'); + expect(escape.argument('(x)')).toBe('^"^(x^)^"'); + expect(escape.argument('a,b;c')).toBe('^"a^,b^;c^"'); + }); + + it('should escape a double quote preceded by no backslashes', () => { + expect(escape.argument('a"b')).toBe('^"a\\^"b^"'); + }); + + it('should double backslashes that precede a double quote', () => { + // Two literal backslashes before the quote become four + expect(escape.argument('a\\\\"b')).toBe('^"a\\\\\\\\^"b^"'); + }); + + it('should extend a run of trailing backslashes (current behavior: n become n + 1)', () => { + // Regression coverage for the existing implementation, which was + // deliberately altered in https://github.com/moxystudio/node-cross-spawn/pull/160 + const bs = '\\'; + + expect(escape.argument(`abc${bs.repeat(2)}`)).toBe(`^"abc${bs.repeat(3)}^"`); + expect(escape.argument(`abc${bs}`)).toBe(`^"abc${bs.repeat(2)}^"`); + }); + + it('should convert non-string input to string', () => { + expect(escape.argument(42)).toBe('^"42^"'); + }); + + it('should double escape meta chars when doubleEscapeMetaChars is set', () => { + expect(escape.argument('a&b', true)).toBe('^^^"a^^^&b^^^"'); + }); +}); diff --git a/lib/util/resolveCommand.test.js b/lib/util/resolveCommand.test.js new file mode 100644 index 0000000..dd1a44b --- /dev/null +++ b/lib/util/resolveCommand.test.js @@ -0,0 +1,77 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const resolveCommand = require('./resolveCommand'); + +describe('resolveCommand', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cross-spawn-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + const makeParsed = (command, options) => ({ command, options: options || {} }); + + it('should resolve a command on the PATH to an absolute path', () => { + const resolved = resolveCommand(makeParsed('node')); + + expect(typeof resolved).toBe('string'); + expect(path.isAbsolute(resolved)).toBe(true); + }); + + it('should return undefined for a command that does not exist', () => { + const resolved = resolveCommand(makeParsed('somecommandthatwillneverexist')); + + expect(resolved).toBe(undefined); + }); + + it('should resolve a command from a custom env PATH', () => { + const binDir = path.join(tmpDir, 'bin'); + + fs.mkdirSync(binDir); + const exe = path.join(binDir, 'mytool'); + + fs.writeFileSync(exe, '#!/bin/sh\n', { mode: 0o755 }); + + const env = { PATH: binDir }; + const resolved = resolveCommand(makeParsed('mytool', { env })); + + expect(resolved).toBe(exe); + }); + + it('should switch to a custom cwd while resolving and restore it afterwards', () => { + const cwdBefore = process.cwd(); + const customCwd = path.join(tmpDir, 'workdir'); + + fs.mkdirSync(customCwd); + + const resolved = resolveCommand(makeParsed('node', { cwd: customCwd })); + + expect(process.cwd()).toBe(cwdBefore); + expect(typeof resolved).toBe('string'); + }); + + it('should resolve relative to the custom cwd', () => { + const customCwd = path.join(tmpDir, 'workdir'); + + fs.mkdirSync(customCwd); + const binDir = path.join(tmpDir, 'bin'); + + fs.mkdirSync(binDir); + const exe = path.join(binDir, 'mytool'); + + fs.writeFileSync(exe, '#!/bin/sh\n', { mode: 0o755 }); + + const env = { PATH: binDir }; + const resolved = resolveCommand(makeParsed('mytool', { env, cwd: customCwd })); + + // Resolved absolute path is based on the custom cwd + expect(resolved).toBe(path.resolve(customCwd, exe)); + }); +});