Skip to content

Commit e61344e

Browse files
committed
fix(frontend): canonicalize daemon identity paths
1 parent a31cf1b commit e61344e

3 files changed

Lines changed: 99 additions & 15 deletions

File tree

frontend/src/main.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { createBrowserViewHost, type BrowserViewHost } from "./main/browser-view
4949
import { connectSupervisor, type SupervisorLinkHandle } from "./main/supervisor-link";
5050
import { shouldLinkOnAttach } from "./main/daemon-owner";
5151
import { readMigrationState, updateMigration, writeAppStateMarker, type MigrationState } from "./main/app-state";
52+
import { pathInside, samePath } from "./shared/path-identity";
5253

5354
// Globals injected at compile time by @electron-forge/plugin-vite.
5455
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
@@ -329,21 +330,6 @@ function daemonEnv(): NodeJS.ProcessEnv {
329330
return buildDaemonEnv(process.env, cachedShellEnv, { ...telemetryOverrides(), ...ownerTag });
330331
}
331332

332-
function pathKey(value: string): string {
333-
const resolved = path.resolve(value);
334-
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
335-
}
336-
337-
function samePath(a: string, b: string): boolean {
338-
return pathKey(a) === pathKey(b);
339-
}
340-
341-
function pathInside(child: string, parent: string): boolean {
342-
const childKey = pathKey(child);
343-
const parentKey = pathKey(parent);
344-
return childKey === parentKey || childKey.startsWith(parentKey + path.sep);
345-
}
346-
347333
function processAlive(pid: number): boolean {
348334
if (!pid) return false;
349335
try {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { pathInside, samePath } from "./path-identity";
6+
7+
const tempDirs: string[] = [];
8+
9+
function tempDir() {
10+
const dir = mkdtempSync(path.join(os.tmpdir(), "ao-path-identity-"));
11+
tempDirs.push(dir);
12+
return dir;
13+
}
14+
15+
afterEach(() => {
16+
for (const dir of tempDirs.splice(0)) {
17+
rmSync(dir, { recursive: true, force: true });
18+
}
19+
});
20+
21+
describe("path identity", () => {
22+
it("matches paths that resolve to the same real directory", () => {
23+
const dir = tempDir();
24+
const real = realpathSync.native(dir);
25+
expect(samePath(path.join(real, "."), real)).toBe(true);
26+
});
27+
28+
it("treats Windows paths as case-insensitive", () => {
29+
expect(samePath("C:\\Users\\me\\AO\\backend", "c:\\users\\me\\ao\\backend", "win32")).toBe(true);
30+
});
31+
32+
it("uses realpath canonical casing on macOS case-insensitive volumes", () => {
33+
if (process.platform !== "darwin") return;
34+
35+
const current = realpathSync.native(process.cwd());
36+
const lowerCased = current.toLowerCase();
37+
try {
38+
if (realpathSync.native(lowerCased) !== current) return;
39+
} catch {
40+
return;
41+
}
42+
43+
expect(samePath(lowerCased, current, "darwin")).toBe(true);
44+
});
45+
46+
it("detects non-existent children after canonicalization", () => {
47+
const dir = tempDir();
48+
const child = path.join(dir, "nested", "future");
49+
50+
expect(pathInside(child, dir)).toBe(true);
51+
expect(pathInside(dir, child)).toBe(false);
52+
});
53+
54+
it("does not confuse sibling path prefixes with children", () => {
55+
const dir = tempDir();
56+
expect(pathInside(`${dir}-other`, dir)).toBe(false);
57+
});
58+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { existsSync, realpathSync } from "node:fs";
2+
import path from "node:path";
3+
4+
type Platform = NodeJS.Platform;
5+
6+
function canonicalPath(value: string): string {
7+
const resolved = path.resolve(value);
8+
try {
9+
return realpathSync.native(resolved);
10+
} catch {
11+
let current = resolved;
12+
const missingParts: string[] = [];
13+
while (!existsSync(current)) {
14+
const parent = path.dirname(current);
15+
if (parent === current) return resolved;
16+
missingParts.unshift(path.basename(current));
17+
current = parent;
18+
}
19+
try {
20+
return path.join(realpathSync.native(current), ...missingParts);
21+
} catch {
22+
return resolved;
23+
}
24+
}
25+
}
26+
27+
export function pathIdentityKey(value: string, platform: Platform = process.platform): string {
28+
const canonical = canonicalPath(value);
29+
return platform === "win32" ? canonical.toLowerCase() : canonical;
30+
}
31+
32+
export function samePath(a: string, b: string, platform: Platform = process.platform): boolean {
33+
return pathIdentityKey(a, platform) === pathIdentityKey(b, platform);
34+
}
35+
36+
export function pathInside(child: string, parent: string, platform: Platform = process.platform): boolean {
37+
const childKey = pathIdentityKey(child, platform);
38+
const parentKey = pathIdentityKey(parent, platform);
39+
return childKey === parentKey || childKey.startsWith(parentKey + path.sep);
40+
}

0 commit comments

Comments
 (0)