65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
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;
|
|
}
|