mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
function safeDecode(value: string): string {
|
|
try {
|
|
return decodeURIComponent(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function normalizeFilePathSlashes(filePath: string): string {
|
|
if (/^[a-zA-Z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\")) {
|
|
return filePath.replace(/\\/g, "/");
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
function stripLineSuffix(filePath: string): string {
|
|
return filePath.replace(/:\d+(?::\d+)?$/, "");
|
|
}
|
|
|
|
function normalizeLocalPath(filePath: string): string {
|
|
const normalized = normalizeFilePathSlashes(filePath);
|
|
const isWindowsDrive = /^[a-zA-Z]:\//.test(normalized);
|
|
const isUnc = normalized.startsWith("//");
|
|
const leadingSlash = normalized.startsWith("/") && !isWindowsDrive && !isUnc;
|
|
const parts: string[] = [];
|
|
|
|
for (const part of normalized.split("/")) {
|
|
if (!part || part === ".") continue;
|
|
if (part === "..") {
|
|
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
parts.pop();
|
|
} else if (!leadingSlash && !isWindowsDrive && !isUnc) {
|
|
parts.push(part);
|
|
}
|
|
continue;
|
|
}
|
|
parts.push(part);
|
|
}
|
|
|
|
const joined = parts.join("/");
|
|
if (isWindowsDrive) return joined;
|
|
if (isUnc) return `//${joined}`;
|
|
return leadingSlash ? `/${joined}` : joined;
|
|
}
|
|
|
|
function isPathInside(candidate: string, root: string): boolean {
|
|
const normalizedCandidate = normalizeLocalPath(candidate).replace(/\/+$/, "");
|
|
const normalizedRoot = normalizeLocalPath(root).replace(/\/+$/, "");
|
|
const useCaseInsensitive = /^[a-zA-Z]:\//.test(normalizedCandidate) || /^[a-zA-Z]:\//.test(normalizedRoot);
|
|
const filePath = useCaseInsensitive ? normalizedCandidate.toLowerCase() : normalizedCandidate;
|
|
const rootPath = useCaseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot;
|
|
return filePath === rootPath || filePath.startsWith(`${rootPath}/`);
|
|
}
|
|
|
|
function looksLikeRelativeFileHref(href: string): boolean {
|
|
if (href.startsWith("#") || href.startsWith("?")) return false;
|
|
if (href.startsWith("./") || href.startsWith("../")) return true;
|
|
if (href.includes("/")) return true;
|
|
return /(^|\/)\.?[^/]+\.[^/.]+$/.test(href);
|
|
}
|
|
|
|
function fileUrlToPath(href: string): string | null {
|
|
try {
|
|
const url = new URL(href);
|
|
if (url.protocol !== "file:") return null;
|
|
const pathname = safeDecode(url.pathname);
|
|
if (url.hostname) {
|
|
return `//${url.hostname}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
|
|
}
|
|
if (/^\/[a-zA-Z]:\//.test(pathname)) return pathname.slice(1);
|
|
return pathname;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function resolveLocalFileHref(
|
|
href: string | undefined,
|
|
baseDir?: string,
|
|
relativeRoot = baseDir,
|
|
): string | null {
|
|
if (!href) return null;
|
|
|
|
const cleanHref = href.split("#", 1)[0].split("?", 1)[0].trim();
|
|
if (!cleanHref) return null;
|
|
|
|
let candidate: string | null = null;
|
|
let candidateKind: "absolute" | "relative" | null = null;
|
|
const decodedHref = safeDecode(cleanHref);
|
|
const isBackslashUncPath = decodedHref.startsWith("\\\\");
|
|
const normalizedHref = normalizeFilePathSlashes(decodedHref);
|
|
const lowerHref = normalizedHref.toLowerCase();
|
|
|
|
if (lowerHref.startsWith("/api/") || lowerHref.startsWith("/_next/")) return null;
|
|
if (!isBackslashUncPath && normalizedHref.startsWith("//")) return null;
|
|
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(normalizedHref) && !lowerHref.startsWith("file:") && !/^[a-zA-Z]:\//.test(normalizedHref)) {
|
|
return null;
|
|
}
|
|
|
|
if (lowerHref.startsWith("file:")) {
|
|
candidate = fileUrlToPath(normalizedHref);
|
|
candidateKind = candidate ? "absolute" : null;
|
|
} else if (/^[a-zA-Z]:\//.test(normalizedHref)) {
|
|
candidate = normalizedHref;
|
|
candidateKind = "absolute";
|
|
} else if (normalizedHref.startsWith("/")) {
|
|
candidate = normalizedHref;
|
|
candidateKind = "absolute";
|
|
} else if (baseDir && looksLikeRelativeFileHref(normalizedHref)) {
|
|
candidate = `${normalizeFilePathSlashes(baseDir).replace(/\/+$/, "")}/${normalizedHref}`;
|
|
candidateKind = "relative";
|
|
}
|
|
|
|
if (!candidate) return null;
|
|
|
|
const filePath = stripLineSuffix(normalizeLocalPath(candidate));
|
|
if (candidateKind === "relative" && relativeRoot && !isPathInside(filePath, relativeRoot)) return null;
|
|
return filePath;
|
|
}
|