win32.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /** Host-independent binding tests for the Win32 DACL and replacement helpers. */
  2. import { toNamespacedPath } from 'node:path'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. type GetFileSecurityW = (
  5. path: string,
  6. requestedInformation: number,
  7. descriptor: Buffer | null,
  8. length: number,
  9. needed: [number],
  10. ) => number
  11. type SetFileSecurityW = (path: string, securityInformation: number, descriptor: Buffer) => number
  12. type ReplaceFileW = (
  13. replaced: string,
  14. replacement: string,
  15. backup: null,
  16. flags: number,
  17. exclude: null,
  18. reserved: null,
  19. ) => number
  20. interface NativeMock {
  21. getFileSecurityW: GetFileSecurityW
  22. setFileSecurityW: SetFileSecurityW
  23. replaceFileW: ReplaceFileW
  24. getLastError: () => number
  25. }
  26. async function importWithNative(native: NativeMock): Promise<typeof import('../src/win32.ts')> {
  27. vi.resetModules()
  28. vi.doMock('koffi', () => ({
  29. default: {
  30. load: () => ({
  31. func: (definition: string) => {
  32. if (definition.includes('GetFileSecurityW')) return native.getFileSecurityW
  33. if (definition.includes('SetFileSecurityW')) return native.setFileSecurityW
  34. if (definition.includes('ReplaceFileW')) return native.replaceFileW
  35. if (definition.includes('GetLastError')) return native.getLastError
  36. throw new Error(`unexpected native function: ${definition}`)
  37. },
  38. }),
  39. },
  40. }))
  41. return import('../src/win32.ts')
  42. }
  43. function successfulNative(descriptor: Buffer): NativeMock & { installed: Buffer[]; replacements: string[][] } {
  44. let lastError = 0
  45. const installed: Buffer[] = []
  46. const replacements: string[][] = []
  47. return {
  48. installed,
  49. replacements,
  50. getLastError: () => lastError,
  51. getFileSecurityW: (_path, _requested, output, _length, needed) => {
  52. needed[0] = descriptor.length
  53. if (output === null) {
  54. lastError = 122
  55. return 0
  56. }
  57. descriptor.copy(output)
  58. lastError = 0
  59. return 1
  60. },
  61. setFileSecurityW: (_path, information, value) => {
  62. expect(information).toBe(0x80000004)
  63. installed.push(Buffer.from(value))
  64. lastError = 0
  65. return 1
  66. },
  67. replaceFileW: (replaced, replacement, backup, flags, exclude, reserved) => {
  68. expect([backup, flags, exclude, reserved]).toEqual([null, 0, null, null])
  69. replacements.push([replaced, replacement])
  70. lastError = 0
  71. return 1
  72. },
  73. }
  74. }
  75. afterEach(() => {
  76. vi.doUnmock('koffi')
  77. vi.resetModules()
  78. })
  79. describe('Windows file-security helpers', () => {
  80. it('reads and installs a protected DACL before replacing the destination', async () => {
  81. const descriptor = Buffer.from([1, 2, 3, 4])
  82. const native = successfulNative(descriptor)
  83. const { copyFileDaclWin32, readFileDaclWin32, replaceFileWin32 } = await importWithNative(native)
  84. expect(await readFileDaclWin32('source')).toEqual(descriptor)
  85. await copyFileDaclWin32('source', 'temp')
  86. expect(native.installed).toEqual([descriptor])
  87. await replaceFileWin32('target', 'temp')
  88. expect(native.replacements).toEqual([[toNamespacedPath('target'), toNamespacedPath('temp')]])
  89. })
  90. it('maps descriptor-size probe failures to Node-style codes', async () => {
  91. const cases = [[2, 'ENOENT'], [3, 'ENOENT'], [5, 'EACCES'], [9999, 'EIO']] as const
  92. for (const [win32Code, code] of cases) {
  93. const native = successfulNative(Buffer.from([1]))
  94. native.getFileSecurityW = (_path, _requested, _output, _length, needed) => {
  95. needed[0] = 0
  96. return 0
  97. }
  98. native.getLastError = () => win32Code
  99. const { readFileDaclWin32 } = await importWithNative(native)
  100. await expect(readFileDaclWin32('source')).rejects.toMatchObject({ code, win32Code, path: 'source' })
  101. }
  102. })
  103. it('surfaces a descriptor read failure after the size probe', async () => {
  104. const native = successfulNative(Buffer.from([1, 2]))
  105. native.getFileSecurityW = (_path, _requested, _output, _length, needed) => {
  106. needed[0] = 2
  107. return 0
  108. }
  109. native.getLastError = () => 5
  110. const { readFileDaclWin32 } = await importWithNative(native)
  111. await expect(readFileDaclWin32('source')).rejects.toMatchObject({ code: 'EACCES', syscall: 'GetFileSecurityW' })
  112. })
  113. it('surfaces DACL installation and replacement failures', async () => {
  114. const setFailure = successfulNative(Buffer.from([1]))
  115. setFailure.setFileSecurityW = () => 0
  116. setFailure.getLastError = () => 5
  117. const setModule = await importWithNative(setFailure)
  118. await expect(setModule.copyFileDaclWin32('source', 'temp')).rejects.toMatchObject({
  119. code: 'EACCES',
  120. syscall: 'SetFileSecurityW',
  121. path: 'temp',
  122. })
  123. const replaceFailure = successfulNative(Buffer.from([1]))
  124. replaceFailure.replaceFileW = () => 0
  125. replaceFailure.getLastError = () => 2
  126. const replaceModule = await importWithNative(replaceFailure)
  127. await expect(replaceModule.replaceFileWin32('target', 'temp')).rejects.toMatchObject({
  128. code: 'ENOENT',
  129. syscall: 'ReplaceFileW',
  130. path: 'target',
  131. })
  132. })
  133. })