From 9624c44e4aa80a53f23173131ee671197d354b67 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 30 Jan 2026 23:33:47 +1300 Subject: [PATCH] fix: handle forward-slash drive roots and UNC paths in validation Broaden the trailing-slash exemption to recognize both C:\ and C:/ as drive roots. Tighten the client-side absolute path check to require a separator after the drive letter and accept UNC paths (\\server\share). Co-Authored-By: Claude Opus 4.5 --- src/app/api/browse-directory/__tests__/route.test.ts | 7 ++++++- src/app/api/browse-directory/route.ts | 2 +- src/components/ProjectSetupModal.tsx | 4 ++-- src/components/__tests__/ProjectSetupModal.test.tsx | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/api/browse-directory/__tests__/route.test.ts b/src/app/api/browse-directory/__tests__/route.test.ts index 5eff72ad..c325da55 100644 --- a/src/app/api/browse-directory/__tests__/route.test.ts +++ b/src/app/api/browse-directory/__tests__/route.test.ts @@ -32,11 +32,16 @@ describe("normalizeSelectedPath", () => { .toBe("C:\\Users\\guy"); }); - it("should preserve Windows drive root", () => { + it("should preserve Windows drive root with backslash", () => { expect(normalizeSelectedPath("C:\\", "win32")) .toBe("C:\\"); }); + it("should preserve Windows drive root with forward slash", () => { + expect(normalizeSelectedPath("C:/", "win32")) + .toBe("C:/"); + }); + it("should preserve Unix root /", () => { expect(normalizeSelectedPath("/", "darwin")) .toBe("/"); diff --git a/src/app/api/browse-directory/route.ts b/src/app/api/browse-directory/route.ts index a6cd2304..1f3df3b2 100644 --- a/src/app/api/browse-directory/route.ts +++ b/src/app/api/browse-directory/route.ts @@ -26,7 +26,7 @@ export function normalizeSelectedPath(selectedPath: string, platform: string): s // Remove trailing slash/backslash (except root paths like "/" or "C:\") if (selectedPath.length > 1 && (selectedPath.endsWith("/") || selectedPath.endsWith("\\"))) { - if (!(platform === "win32" && /^[A-Za-z]:\\$/.test(selectedPath))) { + if (!(platform === "win32" && /^[A-Za-z]:[\\\/]$/.test(selectedPath))) { selectedPath = selectedPath.slice(0, -1); } } diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index c69b0fe0..7476907a 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -200,8 +200,8 @@ export function ProjectSetupModal({ } const trimmedPath = directoryPath.trim(); - if (!(trimmedPath.startsWith("/") || /^[A-Za-z]:/.test(trimmedPath))) { - setError("Project directory must be an absolute path (starting with / or a drive letter)"); + if (!(trimmedPath.startsWith("/") || /^[A-Za-z]:[\\\/]/.test(trimmedPath) || trimmedPath.startsWith("\\\\"))) { + setError("Project directory must be an absolute path (starting with /, a drive letter, or a UNC path)"); return; } diff --git a/src/components/__tests__/ProjectSetupModal.test.tsx b/src/components/__tests__/ProjectSetupModal.test.tsx index 1e6d1173..ed1a0efb 100644 --- a/src/components/__tests__/ProjectSetupModal.test.tsx +++ b/src/components/__tests__/ProjectSetupModal.test.tsx @@ -407,7 +407,7 @@ describe("ProjectSetupModal", () => { await waitFor(() => { expect( - screen.getByText("Project directory must be an absolute path (starting with / or a drive letter)") + screen.getByText("Project directory must be an absolute path (starting with /, a drive letter, or a UNC path)") ).toBeInTheDocument(); }); expect(onSave).not.toHaveBeenCalled();