const DRIVE_PATH = /^[A-Za-z]:[\\/]/; const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/; const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; function invalidPath(message: "ASSET_PATH_INVALID" | "ASSET_PATH_OUTSIDE_PROJECT"): never { throw new Error(message); } function decodePath(sourcePath: string): string { let decoded: string; try { decoded = decodeURIComponent(sourcePath); } catch { return invalidPath("ASSET_PATH_INVALID"); } // A canonical path must be safe to normalize again. Residual percent octets could otherwise // become separators or dot segments in a second decoder. if (decoded.includes("%")) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); try { encodeURIComponent(decoded); } catch { return invalidPath("ASSET_PATH_INVALID"); } return decoded.normalize("NFC"); } export function normalizeProjectAssetPath(sourcePath: string): string { if (typeof sourcePath !== "string" || sourcePath.length === 0 || sourcePath.length > 2048) { return invalidPath("ASSET_PATH_INVALID"); } const blenderRelative = sourcePath.startsWith("//"); if (sourcePath.startsWith("\\") || sourcePath.startsWith("/") && !blenderRelative) { return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); } let relative = decodePath(sourcePath); if (relative.startsWith("//")) { if (!blenderRelative) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); relative = relative.slice(2); } relative = relative.replaceAll("\\", "/"); if (relative.startsWith("/") || DRIVE_PATH.test(relative) || URI_SCHEME.test(relative)) { return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); } const canonicalSegments: string[] = []; for (const segment of relative.split("/")) { if (segment.length === 0 || segment === ".") continue; if (segment === "..") { if (canonicalSegments.length === 0) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); canonicalSegments.pop(); continue; } if (CONTROL_CHARACTER.test(segment)) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); canonicalSegments.push(segment); } const canonical = canonicalSegments.join("/"); if (!canonical || canonical.length > 2048) return invalidPath("ASSET_PATH_INVALID"); return canonical; }