diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 50eea7fc6d2..89e9e2e6123 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -424,8 +424,12 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Test Windows PowerShell CLI boundary - run: pnpm exec vitest run --config config/vitest.config.ts src/main/cli/wsl-cli-powershell-boundary.test.ts + - name: Test Windows-specific boundaries + run: >- + pnpm exec vitest run --config config/vitest.config.ts + src/main/cli/wsl-cli-powershell-boundary.test.ts + src/main/orca-profiles/profile-index-store.test.ts + src/shared/secure-file-fsync-flags.test.ts - name: Build package inputs run: pnpm run build:release diff --git a/src/shared/secure-file-fsync-flags.test.ts b/src/shared/secure-file-fsync-flags.test.ts new file mode 100644 index 00000000000..1c167bffdad --- /dev/null +++ b/src/shared/secure-file-fsync-flags.test.ts @@ -0,0 +1,52 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import type * as NodeFs from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const openedPaths = vi.hoisted(() => [] as { path: string; flags: string | number }[]) + +vi.mock('node:fs', async () => { + const actual = await vi.importActual('node:fs') + return { + ...actual, + openSync: (path: NodeFs.PathLike, flags: string | number, mode?: NodeFs.Mode) => { + openedPaths.push({ path: String(path), flags }) + return actual.openSync(path, flags, mode) + } + } +}) + +import { bestEffortFsyncDirectorySync, fsyncFileSync } from './secure-file' + +const createdPaths: string[] = [] + +afterEach(() => { + openedPaths.length = 0 + for (const path of createdPaths.splice(0)) { + rmSync(path, { recursive: true, force: true }) + } +}) + +describe('secure file fsync flags', () => { + it('opens files read/write before fsync', () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-file-fsync-')) + createdPaths.push(directory) + const path = join(directory, 'record.json') + writeFileSync(path, '{}') + + fsyncFileSync(path) + + expect(openedPaths).toEqual([{ path, flags: 'r+' }]) + }) + + const posixIt = process.platform === 'win32' ? it.skip : it + posixIt('opens directories read-only before fsync', () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-directory-fsync-')) + createdPaths.push(directory) + + bestEffortFsyncDirectorySync(directory) + + expect(openedPaths).toEqual([{ path: directory, flags: 'r' }]) + }) +}) diff --git a/src/shared/secure-file.ts b/src/shared/secure-file.ts index e3c38c70e09..7b2f646c540 100644 --- a/src/shared/secure-file.ts +++ b/src/shared/secure-file.ts @@ -133,8 +133,8 @@ export function writeSecureFile( } } -export function fsyncFileSync(path: string): void { - const descriptor = openSync(path, 'r') +function fsyncPathSync(path: string, flags: 'r' | 'r+'): void { + const descriptor = openSync(path, flags) try { fsyncSync(descriptor) } finally { @@ -142,12 +142,17 @@ export function fsyncFileSync(path: string): void { } } +export function fsyncFileSync(path: string): void { + // FlushFileBuffers requires a write-capable handle on Windows. + fsyncPathSync(path, 'r+') +} + export function bestEffortFsyncDirectorySync(directory: string): void { if (process.platform === 'win32') { return } try { - fsyncFileSync(directory) + fsyncPathSync(directory, 'r') } catch (error) { if ( error instanceof Error &&