normalize paths on all OS

This commit is contained in:
Siddharth
2026-03-14 12:43:12 -07:00
parent 16dea49fa8
commit 5f6576768c
5 changed files with 119 additions and 31 deletions
+30 -6
View File
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { app, BrowserWindow, desktopCapturer, dialog, ipcMain, screen, shell } from "electron";
import { RECORDINGS_DIR } from "../main";
@@ -18,6 +19,27 @@ function normalizePath(filePath: string) {
return path.resolve(filePath);
}
function normalizeVideoSourcePath(videoPath?: string | null): string | null {
if (typeof videoPath !== "string") {
return null;
}
const trimmed = videoPath.trim();
if (!trimmed) {
return null;
}
if (/^file:\/\//i.test(trimmed)) {
try {
return fileURLToPath(trimmed);
} catch {
// Fall through and keep best-effort string path below.
}
}
return trimmed;
}
function isTrustedProjectPath(filePath?: string | null) {
if (!filePath || !currentProjectPath) {
return false;
@@ -199,7 +221,7 @@ export function registerIpcHandlers(
});
ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
const targetVideoPath = videoPath ?? currentVideoPath;
const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath);
if (!targetVideoPath) {
return { success: true, samples: [] };
}
@@ -265,9 +287,11 @@ export function registerIpcHandlers(
ipcMain.handle("get-asset-base-path", () => {
try {
if (app.isPackaged) {
return path.join(process.resourcesPath, "assets");
const assetPath = path.join(process.resourcesPath, "assets");
return pathToFileURL(`${assetPath}${path.sep}`).toString();
}
return path.join(app.getAppPath(), "public", "assets");
const assetPath = path.join(app.getAppPath(), "public", "assets");
return pathToFileURL(`${assetPath}${path.sep}`).toString();
} catch (err) {
console.error("Failed to resolve asset base path:", err);
return null;
@@ -456,7 +480,7 @@ export function registerIpcHandlers(
const project = JSON.parse(content);
currentProjectPath = filePath;
if (project && typeof project === "object" && typeof project.videoPath === "string") {
currentVideoPath = project.videoPath;
currentVideoPath = normalizeVideoSourcePath(project.videoPath) ?? project.videoPath;
}
return {
@@ -483,7 +507,7 @@ export function registerIpcHandlers(
const content = await fs.readFile(currentProjectPath, "utf-8");
const project = JSON.parse(content);
if (project && typeof project === "object" && typeof project.videoPath === "string") {
currentVideoPath = project.videoPath;
currentVideoPath = normalizeVideoSourcePath(project.videoPath) ?? project.videoPath;
}
return {
success: true,
@@ -500,7 +524,7 @@ export function registerIpcHandlers(
}
});
ipcMain.handle("set-current-video-path", (_, path: string) => {
currentVideoPath = path;
currentVideoPath = normalizeVideoSourcePath(path) ?? path;
currentProjectPath = null;
return { success: true };
});
+4 -3
View File
@@ -118,7 +118,7 @@ export default function VideoEditor() {
}
const project = candidate;
const sourcePath = project.videoPath;
const sourcePath = fromFileUrl(project.videoPath);
const normalizedEditor = normalizeProjectEditor(project.editor);
try {
@@ -259,8 +259,9 @@ export default function VideoEditor() {
const result = await window.electronAPI.getCurrentVideoPath();
if (result.success && result.path) {
setVideoSourcePath(result.path);
setVideoPath(toFileUrl(result.path));
const sourcePath = fromFileUrl(result.path);
setVideoSourcePath(sourcePath);
setVideoPath(toFileUrl(sourcePath));
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
} else {
@@ -58,21 +58,50 @@ function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function isFileUrl(value: string): boolean {
return /^file:\/\//i.test(value);
}
function encodePathSegments(pathname: string, keepWindowsDrive = false): string {
return pathname
.split("/")
.map((segment, index) => {
if (!segment) return "";
if (keepWindowsDrive && index === 1 && /^[a-zA-Z]:$/.test(segment)) {
return segment;
}
return encodeURIComponent(segment);
})
.join("/");
}
export function toFileUrl(filePath: string): string {
const normalized = filePath.replace(/\\/g, "/");
if (normalized.match(/^[a-zA-Z]:/)) {
return `file:///${encodeURI(normalized)}`;
// Windows drive path: C:/Users/...
if (/^[a-zA-Z]:\//.test(normalized)) {
return `file://${encodePathSegments(`/${normalized}`, true)}`;
}
return `file://${encodeURI(normalized)}`;
// UNC path: //server/share/...
if (normalized.startsWith("//")) {
const [host, ...pathParts] = normalized.replace(/^\/+/, "").split("/");
const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/");
return encodedPath ? `file://${host}/${encodedPath}` : `file://${host}/`;
}
const absolutePath = normalized.startsWith("/") ? normalized : `/${normalized}`;
return `file://${encodePathSegments(absolutePath)}`;
}
export function fromFileUrl(fileUrl: string): string {
if (!fileUrl.startsWith("file://")) {
const value = fileUrl.trim();
if (!isFileUrl(value)) {
return fileUrl;
}
try {
const url = new URL(fileUrl);
const url = new URL(value);
const pathname = decodeURIComponent(url.pathname);
if (url.host && url.host !== "localhost") {
@@ -85,7 +114,13 @@ export function fromFileUrl(fileUrl: string): string {
return pathname;
} catch {
const fallbackPath = decodeURIComponent(fileUrl.replace(/^file:\/\//, ""));
const rawFallbackPath = value.replace(/^file:\/\//i, "");
let fallbackPath = rawFallbackPath;
try {
fallbackPath = decodeURIComponent(rawFallbackPath);
} catch {
// Keep raw best-effort path if percent decoding fails.
}
return fallbackPath.replace(/^\/([a-zA-Z]:)/, "$1");
}
}
+18 -4
View File
@@ -1,4 +1,19 @@
function encodeRelativeAssetPath(relativePath: string): string {
return relativePath
.replace(/^\/+/, "")
.split("/")
.filter(Boolean)
.map((part) => encodeURIComponent(part))
.join("/");
}
function ensureTrailingSlash(value: string): string {
return value.endsWith("/") ? value : `${value}/`;
}
export async function getAssetPath(relativePath: string): Promise<string> {
const encodedRelativePath = encodeRelativeAssetPath(relativePath);
try {
if (typeof window !== "undefined") {
// If running in a dev server (http/https), prefer the web-served path
@@ -7,14 +22,13 @@ export async function getAssetPath(relativePath: string): Promise<string> {
window.location.protocol &&
window.location.protocol.startsWith("http")
) {
return `/${relativePath.replace(/^\//, "")}`;
return `/${encodedRelativePath}`;
}
if (window.electronAPI && typeof window.electronAPI.getAssetBasePath === "function") {
const base = await window.electronAPI.getAssetBasePath();
if (base) {
const normalized = base.replace(/\\/g, "/");
return `file://${normalized}/${relativePath}`;
return new URL(encodedRelativePath, ensureTrailingSlash(base)).toString();
}
}
}
@@ -23,7 +37,7 @@ export async function getAssetPath(relativePath: string): Promise<string> {
}
// Fallback for web/dev server: public/wallpapers are served at '/wallpapers/...'
return `/${relativePath.replace(/^\//, "")}`;
return `/${encodedRelativePath}`;
}
export default getAssetPath;
+26 -12
View File
@@ -23,6 +23,7 @@ import {
import { clampFocusToStage as clampFocusToStageUtil } from "@/components/video-editor/videoPlayback/focusUtils";
import { findDominantRegion } from "@/components/video-editor/videoPlayback/zoomRegionUtils";
import { applyZoomTransform } from "@/components/video-editor/videoPlayback/zoomTransform";
import { getAssetPath } from "@/lib/assetPath";
import { renderAnnotations } from "./annotationRenderer";
interface FrameRenderConfig {
@@ -178,18 +179,14 @@ export class FrameRenderer {
) {
// Image background
const img = new Image();
// Don't set crossOrigin for same-origin images to avoid CORS taint
// Only set it for cross-origin URLs
let imageUrl: string;
if (wallpaper.startsWith("http")) {
imageUrl = wallpaper;
if (!imageUrl.startsWith(window.location.origin)) {
img.crossOrigin = "anonymous";
}
} else if (wallpaper.startsWith("file://") || wallpaper.startsWith("data:")) {
imageUrl = wallpaper;
} else {
imageUrl = window.location.origin + wallpaper;
const imageUrl = await this.resolveWallpaperImageUrl(wallpaper);
// Don't set crossOrigin for same-origin images to avoid CORS taint.
if (
imageUrl.startsWith("http") &&
window.location.origin &&
!imageUrl.startsWith(window.location.origin)
) {
img.crossOrigin = "anonymous";
}
await new Promise<void>((resolve, reject) => {
@@ -283,6 +280,23 @@ export class FrameRenderer {
this.backgroundSprite = bgCanvas;
}
private async resolveWallpaperImageUrl(wallpaper: string): Promise<string> {
if (
wallpaper.startsWith("file://") ||
wallpaper.startsWith("data:") ||
wallpaper.startsWith("http")
) {
return wallpaper;
}
const resolved = await getAssetPath(wallpaper.replace(/^\/+/, ""));
if (resolved.startsWith("/") && window.location.protocol.startsWith("http")) {
return `${window.location.origin}${resolved}`;
}
return resolved;
}
async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise<void> {
if (!this.app || !this.videoContainer || !this.cameraContainer) {
throw new Error("Renderer not initialized");