Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { buildDaemonEnv, resolveShellEnv, type ShellRunner } from "./shared/shel
import { DEFAULT_POSTHOG_HOST, DEFAULT_POSTHOG_PROJECT_KEY } from "./shared/posthog-config";
import { buildTelemetryBootstrap } from "./shared/telemetry";
import { createBrowserViewHost, type BrowserViewHost } from "./main/browser-view-host";
import { pathInside, samePath } from "./shared/path-identity";

// Globals injected at compile time by @electron-forge/plugin-vite.
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
Expand Down Expand Up @@ -307,21 +308,6 @@ function daemonEnv(): NodeJS.ProcessEnv {
return buildDaemonEnv(process.env, cachedShellEnv, telemetryOverrides());
}

function pathKey(value: string): string {
const resolved = path.resolve(value);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}

function samePath(a: string, b: string): boolean {
return pathKey(a) === pathKey(b);
}

function pathInside(child: string, parent: string): boolean {
const childKey = pathKey(child);
const parentKey = pathKey(parent);
return childKey === parentKey || childKey.startsWith(parentKey + path.sep);
}

function processAlive(pid: number): boolean {
if (!pid) return false;
try {
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/shared/path-identity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { pathInside, samePath } from "./path-identity";

const tempDirs: string[] = [];

function tempDir() {
const dir = mkdtempSync(path.join(os.tmpdir(), "ao-path-identity-"));
tempDirs.push(dir);
return dir;
}

afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});

describe("path identity", () => {
it("matches paths that resolve to the same real directory", () => {
const dir = tempDir();
const real = realpathSync.native(dir);
expect(samePath(path.join(real, "."), real)).toBe(true);
});

it("treats Windows paths as case-insensitive", () => {
expect(samePath("C:\\Users\\me\\AO\\backend", "c:\\users\\me\\ao\\backend", "win32")).toBe(true);
});

it("uses realpath canonical casing on macOS case-insensitive volumes", () => {
if (process.platform !== "darwin") return;

const current = realpathSync.native(process.cwd());
const lowerCased = current.toLowerCase();

expect(samePath(lowerCased, current, "darwin")).toBe(true);
});

it("detects children after canonicalization", () => {
const dir = tempDir();
const child = path.join(dir, "nested");

expect(pathInside(child, dir)).toBe(true);
expect(pathInside(dir, child)).toBe(false);
});
});
40 changes: 40 additions & 0 deletions frontend/src/shared/path-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { existsSync, realpathSync } from "node:fs";
import path from "node:path";

type Platform = NodeJS.Platform;

function canonicalPath(value: string): string {
const resolved = path.resolve(value);
try {
return realpathSync.native(resolved);
} catch {
let current = resolved;
const missingParts: string[] = [];
while (!existsSync(current)) {
const parent = path.dirname(current);
if (parent === current) return resolved;
missingParts.unshift(path.basename(current));
current = parent;
}
try {
return path.join(realpathSync.native(current), ...missingParts);
} catch {
return resolved;
}
}
}

export function pathIdentityKey(value: string, platform: Platform = process.platform): string {
const canonical = canonicalPath(value);
return platform === "win32" ? canonical.toLowerCase() : canonical;
}

export function samePath(a: string, b: string, platform: Platform = process.platform): boolean {
return pathIdentityKey(a, platform) === pathIdentityKey(b, platform);
}

export function pathInside(child: string, parent: string, platform: Platform = process.platform): boolean {
const childKey = pathIdentityKey(child, platform);
const parentKey = pathIdentityKey(parent, platform);
return childKey === parentKey || childKey.startsWith(parentKey + path.sep);
}