quote.spec.ts 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. import { describe, expect, it } from 'vitest'
  2. import { buildCommandLine, quoteArg } from '../src/process.ts'
  3. const isWin32 = process.platform === 'win32'
  4. const cases: Array<[string, string]> = [
  5. ['', '""'],
  6. ['a', 'a'],
  7. ['a b', '"a b"'],
  8. ['a"b', '"a\\"b"'],
  9. ['a\\b', 'a\\b'],
  10. ['a b\\', '"a b\\\\"'],
  11. ['a b\\\\', '"a b\\\\\\\\"'],
  12. ['a\\\\"b', '"a\\\\\\\\\\"b"'],
  13. ]
  14. describe('quoteArg', () => {
  15. it.each(cases)('quotes %j as %j', (input, expected) => {
  16. expect(quoteArg(input)).toBe(expected)
  17. })
  18. it('builds one CreateProcess command line without shell interpretation', () => {
  19. expect(buildCommandLine('C:\\Program Files\\tool.exe', ['a b', 'c'])).toBe(
  20. '"C:\\Program Files\\tool.exe" "a b" c',
  21. )
  22. })
  23. })
  24. describe.skipIf(!isWin32)('CommandLineToArgvW round-trip', () => {
  25. it('parses the shared command line back to the original argv', async () => {
  26. const { default: koffi } = await import('koffi')
  27. const PVOID = koffi.pointer('void')
  28. const shell32 = koffi.load('shell32.dll')
  29. const kernel32 = koffi.load('kernel32.dll')
  30. const commandLineToArgvW = shell32.func(
  31. '__stdcall',
  32. 'CommandLineToArgvW',
  33. PVOID,
  34. ['str16', koffi.pointer('int')],
  35. )
  36. const lstrcpynW = kernel32.func('__stdcall', 'lstrcpynW', PVOID, [PVOID, PVOID, 'int'])
  37. const lstrlenW = kernel32.func('__stdcall', 'lstrlenW', 'int', [PVOID])
  38. const localFree = kernel32.func('__stdcall', 'LocalFree', PVOID, [PVOID])
  39. const parse = (commandLine: string): string[] => {
  40. const countSlot = koffi.alloc('int', 1) as unknown
  41. const argvBlock = commandLineToArgvW(commandLine, countSlot) as unknown
  42. try {
  43. if (argvBlock === null) throw new Error('CommandLineToArgvW returned NULL')
  44. const count = koffi.decode(countSlot, 0, 'int') as number
  45. const table = Buffer.from(koffi.view(argvBlock, count * 8))
  46. return Array.from({ length: count }, (_, index) => {
  47. const stringAddress = table.readBigUInt64LE(index * 8)
  48. const copied = Buffer.alloc(2048)
  49. lstrcpynW(copied, stringAddress, copied.length / 2)
  50. const length = lstrlenW(copied) as number
  51. return copied.subarray(0, length * 2).toString('utf16le')
  52. })
  53. } finally {
  54. localFree(argvBlock)
  55. }
  56. }
  57. const argv = ['', 'a', 'a b', 'a"b', 'a\\b', 'a b\\', 'a b\\\\', 'a\\\\"b']
  58. expect(parse(buildCommandLine('prog.exe', argv))).toEqual(['prog.exe', ...argv])
  59. })
  60. })