export type UploadEntry = { file: File; path: string }; export type PublishManifestRule = { line: number; raw: string; pattern: string; exclude: boolean; }; export const RELEASE_MANIFEST_NAMES = ["发布清单.txt", "release_manifest.txt"]; export function rawPathForFile(file: File) { const raw = ((file as any).webkitRelativePath || file.name) as string; return raw.replaceAll("\\", "/"); } export function relativePathForFile(file: File) { const raw = rawPathForFile(file); const parts = raw.replaceAll("\\", "/").split("/").filter(Boolean); if ((file as any).webkitRelativePath && parts.length > 1) parts.shift(); return parts.join("/"); } export function detectSelectedRoot(files: File[]) { const first = files[0]; if (!first || !(first as any).webkitRelativePath) return ""; const parts = rawPathForFile(first).split("/").filter(Boolean); return parts.length > 1 ? parts[0] : ""; } function normalizePublishPath(value: string) { let normalized = value.replaceAll("\\", "/").trim(); while (normalized.startsWith("./")) normalized = normalized.slice(2); normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, ""); return normalized; } function normalizeManifestPattern(value: string, rootName: string) { let pattern = normalizePublishPath(value); const root = normalizePublishPath(rootName); if (root) { if (pattern === root) return "*"; if (pattern.startsWith(`${root}/`)) { pattern = pattern.slice(root.length + 1); } } return pattern || "*"; } export function parseReleaseManifest(text: string, rootName: string): PublishManifestRule[] { return text .split(/\r?\n/) .map((raw, index) => ({ raw, line: index + 1, trimmed: raw.trim() })) .filter(item => item.trimmed && !item.trimmed.startsWith("#")) .map(item => { const exclude = item.trimmed.startsWith("!"); const body = exclude ? item.trimmed.slice(1).trim() : item.trimmed; return { line: item.line, raw: item.raw, pattern: normalizeManifestPattern(body, rootName), exclude }; }) .filter(rule => rule.pattern); } function escapeRegExp(value: string) { return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); } export function manifestRuleMatches(pattern: string, filePath: string) { const normalizedPattern = normalizePublishPath(pattern); const normalizedPath = normalizePublishPath(filePath); if (normalizedPattern === "*") return true; if (normalizedPattern.endsWith("/*")) { const prefix = normalizedPattern.slice(0, -2); return normalizedPath.startsWith(`${prefix}/`); } if (!normalizedPattern.includes("*")) { return normalizedPath === normalizedPattern; } const regex = new RegExp( `^${normalizedPattern.split("*").map(escapeRegExp).join(".*")}$` ); return regex.test(normalizedPath); } export function isReleaseManifestFile(path: string) { const normalized = normalizePublishPath(path).toLowerCase(); return RELEASE_MANIFEST_NAMES.some(name => normalized === name.toLowerCase()); } export function isSafePublishPath(itemPath: string) { const protectedFiles = new Set([ "client.ini", "bootstrap.exe", "config/app_config.json", "config/local_state.json", "config/client_identity.dat", "config/version_policy.dat" ]); const path = normalizePublishPath(itemPath).toLowerCase(); const parts = path.split("/"); const blockedDirectory = parts .slice(0, -1) .some( part => part === ".git" || part === ".vs" || part === "debug" || part === "update" || part === "update_temp" || part === "cmakefiles" || part.startsWith("build") || part.endsWith("_autogen") ); const runtimeFile = protectedFiles.has(path) || (path.startsWith("bin/") && protectedFiles.has(path.slice(4))); return ( !blockedDirectory && !runtimeFile && !path.endsWith(".pdb") && !path.endsWith(".ilk") && !path.endsWith(".obj") ); } export function normalizeExecutableName(value: string) { const normalized = normalizePublishPath(value || "SimCAE.exe"); if (!normalized) return "SimCAE.exe"; const parts = normalized.split("/"); const leaf = parts[parts.length - 1]; if (!leaf.includes(".")) { parts[parts.length - 1] = `${leaf}.exe`; } return parts.join("/"); } export function releaseMainExecutableForInstall( installRoot: string, mainExecutable: string ) { const root = (installRoot || "..").trim(); const main = normalizeExecutableName(mainExecutable); if (root === ".") return main; if (main.toLowerCase().startsWith("bin/")) return main; return `bin/${main}`; } export function executableNameCandidates(value: string, fallback: string) { const leaf = normalizePublishPath(value || fallback) .split("/") .filter(Boolean) .pop() || fallback; const lower = leaf.toLowerCase(); const candidates = new Set([lower]); if (lower.endsWith(".exe")) { candidates.add(lower.slice(0, -4)); } else { candidates.add(`${lower}.exe`); } return candidates; } export function executableLeafName(value: string, fallback: string) { return ( normalizePublishPath(value || fallback) .split("/") .filter(Boolean) .pop() || fallback ); } export function dirname(path: string) { const parts = normalizePublishPath(path).split("/").filter(Boolean); parts.pop(); return parts.join("/"); } export function findExecutablePath(entries: UploadEntry[], candidates: Set) { return entries .map(item => normalizePublishPath(item.path)) .find(path => candidates.has(path.split("/").pop()?.toLowerCase() || "")); }