(() => { "use strict"; const mirrorIndexUrl = "https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/latest.json"; const loongArchPreviewIndexUrl = "https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json"; const fallbackUrl = "https://github.com/mesalogo/goodbuddy/releases/latest"; const maximumIndexBytes = 512 * 1024; const maximumPreviewIndexBytes = 16 * 1024; const semVerPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+((?:[0-9a-zA-Z-]+)(?:\.[0-9a-zA-Z-]+)*))?$/u; const sha256Pattern = /^[a-f0-9]{64}$/u; const safeFileNamePattern = /^(?!\.{1,2}$)[^/\\\0]+$/u; const targetDefinitions = Object.freeze({ "windows-x64": Object.freeze({ platform: "windows", arch: "x64", formats: Object.freeze(["nsis", "portable"]), }), "windows-arm64": Object.freeze({ platform: "windows", arch: "arm64", formats: Object.freeze(["nsis", "portable"]), }), "macos-x64": Object.freeze({ platform: "macos", arch: "x64", formats: Object.freeze(["dmg"]), }), "macos-arm64": Object.freeze({ platform: "macos", arch: "arm64", formats: Object.freeze(["dmg"]), }), "linux-x64": Object.freeze({ platform: "linux", arch: "x64", formats: Object.freeze(["AppImage", "deb", "rpm"]), }), "linux-arm64": Object.freeze({ platform: "linux", arch: "arm64", formats: Object.freeze(["AppImage", "deb", "rpm"]), }), }); const targetKeys = Object.freeze(Object.keys(targetDefinitions)); const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value); const hasExactKeys = (value, keys) => { if (!isRecord(value)) { return false; } const actualKeys = Object.keys(value); return ( actualKeys.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key)) ); }; const assert = (condition, message) => { if (!condition) { throw new Error(message); } }; const canonicalFileName = (version, platform, arch, format) => { if (platform === "windows" && format === "nsis") { return `GoodBuddy-${version}-windows-${arch}-setup.exe`; } if (platform === "windows" && format === "portable") { return `GoodBuddy-${version}-windows-${arch}-portable.zip`; } if (platform === "macos") { return `GoodBuddy-${version}-mac-${arch}.${format}`; } const artifactArch = arch === "x64" ? format === "deb" ? "amd64" : "x86_64" : format === "rpm" ? "aarch64" : "arm64"; return `GoodBuddy-${version}-linux-${artifactArch}.${format}`; }; const assertExactUrl = (value, expected, label) => { assert(typeof value === "string" && value.length <= 2_048, `${label} 无效`); let url; try { url = new URL(value); } catch { throw new Error(`${label} 无效`); } assert( value === expected && url.href === expected && url.protocol === "https:" && !url.username && !url.password && !url.port && !url.search && !url.hash, `${label} 不是受信任的正式地址`, ); }; const validateReleaseIndex = (index) => { assert( hasExactKeys(index, [ "formatVersion", "productName", "version", "targets", "checksumUrl", "fallbackUrl", ]), "发布索引结构无效", ); assert(index.formatVersion === 1, "发布索引版本无效"); assert(index.productName === "GoodBuddy", "发布索引产品名称无效"); assert( typeof index.version === "string" && index.version.length <= 256, "发布版本无效", ); const parsedVersion = semVerPattern.exec(index.version); assert(parsedVersion && !parsedVersion[4], "发布索引必须指向稳定 SemVer 版本"); assert( hasExactKeys(index.targets, targetKeys), "发布索引必须包含且仅包含六个平台目标", ); const releaseBase = new URL(`v${index.version}/`, mirrorIndexUrl); const seenNames = new Set(); const seenUrls = new Set(); for (const key of targetKeys) { const definition = targetDefinitions[key]; const target = index.targets[key]; const formats = definition.platform === "macos" && target?.files?.zip ? ["dmg", "zip"] : definition.formats; assert( hasExactKeys(target, ["platform", "arch", "files"]) && target.platform === definition.platform && target.arch === definition.arch, `发布目标与键不匹配:${key}`, ); assert( hasExactKeys(target.files, formats), `发布目标文件格式无效:${key}`, ); for (const format of formats) { const file = target.files[format]; assert( hasExactKeys(file, ["name", "size", "sha256", "url"]), `发布文件结构无效:${key}/${format}`, ); assert( typeof file.name === "string" && file.name.length >= 1 && file.name.length <= 255 && safeFileNamePattern.test(file.name) && file.name === canonicalFileName( index.version, target.platform, target.arch, format, ), `发布文件名或扩展名无效:${key}/${format}`, ); assert( Number.isSafeInteger(file.size) && file.size > 0, `发布文件大小无效:${key}/${format}`, ); assert( typeof file.sha256 === "string" && sha256Pattern.test(file.sha256), `发布文件校验值无效:${key}/${format}`, ); assert(!seenNames.has(file.name), `发布文件名重复:${file.name}`); seenNames.add(file.name); const expectedUrl = new URL( encodeURIComponent(file.name), releaseBase, ).href; assertExactUrl(file.url, expectedUrl, "发布文件地址"); assert(!seenUrls.has(file.url), `发布文件地址重复:${file.url}`); seenUrls.add(file.url); } } assertExactUrl( index.checksumUrl, new URL("SHA256SUMS", releaseBase).href, "校验清单地址", ); assertExactUrl(index.fallbackUrl, fallbackUrl, "GitHub 回退地址"); return index; }; const validateLoongArchPreviewIndex = (index) => { assert( hasExactKeys(index, [ "formatVersion", "product", "goodBuddyVersion", "previewVersion", "architecture", "format", "artifact", "manifestUrl", "checksumUrl", ]), "龙芯预览索引结构无效", ); assert(index.formatVersion === 1, "龙芯预览索引版本无效"); assert( index.product === "GoodBuddy LoongArch Preview", "龙芯预览产品名称无效", ); assert( typeof index.goodBuddyVersion === "string" && index.goodBuddyVersion.length <= 256, "龙芯预览 GoodBuddy 版本无效", ); const parsedVersion = semVerPattern.exec(index.goodBuddyVersion); assert( parsedVersion && !parsedVersion[4], "龙芯预览必须基于稳定 GoodBuddy 版本", ); const previewPrefix = `${index.goodBuddyVersion}-loong64-preview.`; assert( typeof index.previewVersion === "string" && index.previewVersion.startsWith(previewPrefix) && /^[1-9]\d*$/u.test(index.previewVersion.slice(previewPrefix.length)), "龙芯预览版本无效", ); assert(index.architecture === "loong64", "龙芯预览架构无效"); assert(index.format === "deb", "龙芯预览格式无效"); assert( hasExactKeys(index.artifact, ["name", "size", "sha256", "url"]), "龙芯预览文件结构无效", ); const expectedName = `GoodBuddy-${index.goodBuddyVersion}-linux-loong64-preview.deb`; assert( index.artifact.name === expectedName && safeFileNamePattern.test(index.artifact.name), "龙芯预览文件名无效", ); assert( Number.isSafeInteger(index.artifact.size) && index.artifact.size > 0, "龙芯预览文件大小无效", ); assert( typeof index.artifact.sha256 === "string" && sha256Pattern.test(index.artifact.sha256), "龙芯预览 SHA-256 无效", ); const previewBase = new URL( `v${index.goodBuddyVersion}/`, loongArchPreviewIndexUrl, ); assertExactUrl( index.artifact.url, new URL(encodeURIComponent(expectedName), previewBase).href, "龙芯预览文件地址", ); assertExactUrl( index.manifestUrl, new URL("preview-manifest.json", previewBase).href, "龙芯预览 manifest 地址", ); assertExactUrl( index.checksumUrl, new URL("SHA256SUMS", previewBase).href, "龙芯预览校验清单地址", ); return index; }; window.GoodBuddyReleaseIndex = Object.freeze({ maximumIndexBytes, maximumPreviewIndexBytes, validateReleaseIndex, validateLoongArchPreviewIndex, }); })();