From 81bfd2dbcce450b5df1ac779ddfd007015d14895 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 8 May 2026 13:06:08 +0700 Subject: [PATCH] fix(export): harden post-merge file handling --- electron/ipc/register/export.test.ts | 26 ++++++++++++++++++++++++++ electron/ipc/register/export.ts | 11 ++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/electron/ipc/register/export.test.ts b/electron/ipc/register/export.test.ts index 29ddb00a..33941eb1 100644 --- a/electron/ipc/register/export.test.ts +++ b/electron/ipc/register/export.test.ts @@ -40,6 +40,7 @@ async function makeTempDir() { } afterEach(async () => { + vi.restoreAllMocks(); await Promise.allSettled( tempDirs.splice(0).map((dir) => fs.rm(dir, { force: true, recursive: true })), ); @@ -59,4 +60,29 @@ describe("moveExportedTempFile", () => { ); await expect(fs.access(tempPath)).rejects.toThrow(); }); + + it("falls back when Windows reports the destination already exists during initial rename", async () => { + const dir = await makeTempDir(); + const tempPath = path.join(dir, "export-temp.mp4"); + const destinationPath = path.join(dir, "export-final.mp4"); + await fs.writeFile(tempPath, "new-export"); + await fs.writeFile(destinationPath, "previous-export"); + + const originalRename = fs.rename.bind(fs); + const renameSpy = vi.spyOn(fs, "rename"); + renameSpy.mockImplementation(async (from, to) => { + if (from === tempPath && to === destinationPath) { + const error = new Error("destination exists") as NodeJS.ErrnoException; + error.code = "EEXIST"; + throw error; + } + + return originalRename(from, to); + }); + + await moveExportedTempFile(tempPath, destinationPath); + + await expect(fs.readFile(destinationPath, "utf8")).resolves.toBe("new-export"); + await expect(fs.access(tempPath)).rejects.toThrow(); + }); }); diff --git a/electron/ipc/register/export.ts b/electron/ipc/register/export.ts index f8aa29ee..c095cf39 100644 --- a/electron/ipc/register/export.ts +++ b/electron/ipc/register/export.ts @@ -58,7 +58,12 @@ export async function moveExportedTempFile(tempPath: string, destinationPath: st return; } catch (error) { const code = (error as NodeJS.ErrnoException).code; - if (code !== "EXDEV" && code !== "EPERM" && code !== "ENOTEMPTY") { + if ( + code !== "EXDEV" && + code !== "EPERM" && + code !== "ENOTEMPTY" && + code !== "EEXIST" + ) { throw error; } // Cross-device or Windows permission quirks — fall back to copy + unlink so @@ -143,6 +148,10 @@ async function resolveAllowedReadableFilePath( } const resolvedPath = path.resolve(filePath); + if (!isAllowedLocalReadPath(resolvedPath)) { + throw new Error(`${label} is not approved for local reads`); + } + const realPath = await fs.realpath(resolvedPath).catch(() => null); if (!realPath) { throw new Error(`${label} does not exist`);