1import type { ReleaseFile } from "../bucket/repo.ts";2import type { Release } from "./releases.ts";3456789const ESCAPES: Record<string, string> = {10 '"': '\\"',11 "\\": "\\\\",12 "/": "\\/",13 "<": "\\u003c",14 ">": "\\u003e",15 "\n": "\\n",16 "\r": "\\r",17 "\t": "\\t",18};192021function jsonEscape(text: string): string {22 return text.replace(/["\\/<>\p{Cc}]/gu, (ch) => {23 const code = ch.charCodeAt(0);24 if (code >= 0x80) return ch;25 return ESCAPES[ch] ?? `\\u${code.toString(16).padStart(4, "0")}`;26 });27}2829function str(text: string): string {30 return `"${jsonEscape(text)}"`;31}3233function optional(text: string | null): string {34 return text === null ? "null" : str(text);35}3637function provenance(version: string, file: ReleaseFile): string {38 return `{"version":${str(version)},"platform":${str(file.platform)},"checksum":${str(`blake3:${file.blake3}`)},"source":"koh-offer"}`;39}404142function head(release: Release): string {43 const notes = release.notes === "" ? "" : `,"notes":${str(release.notes)}`;44 return `{"version":${str(release.version)}${notes}`;45}464748export function releaseJson(domain: string, repo: string, release: Release): string {49 const platforms = release.files.map(50 (file) =>51 `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/${repo}/releases/${release.version}/${encodeURIComponent(file.name)}","blake3":${str(file.blake3)},"sha256":${str(file.sha256)},"provenance":${provenance(release.version, file)},"signature":""}`,52 );53 return `${head(release)},"platforms":{${platforms.join(",")}}}`;54}555657export function legacyReleaseJson(domain: string, repo: string, release: Release): string {58 const platforms = release.files.map(59 (file) =>60 `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/api/${repo}/releases/${release.version}/${file.platform}","checksum":${str(`blake3:${file.blake3}`)},"provenance":${provenance(release.version, file)},"signature":""}`,61 );62 return `${head(release)},"platforms":{${platforms.join(",")}}}`;63}646566export function historyJson(releases: Release[]): string {67 const rows = releases.map((r) => `{"version":${str(r.version)},"notes":${str(r.notes)}}`);68 return `[${rows.join(",")}]`;69}7071export type UpdateHead = { id: string; version: string; createdAt: number };727374export function updateInfoJson(75 domain: string,76 repo: string,77 saveHead: UpdateHead | null,78 newest: Release | undefined,79): string {80 const headPart = saveHead81 ? `{"save_id":${str(saveHead.id)},"version":${str(saveHead.version)},"timestamp":${saveHead.createdAt}}`82 : "null";83 const platforms = newest?.files.map(84 (file) =>85 `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/${repo}/releases/${newest.version}/${repo}-${file.platform}","blake3":${str(file.blake3)}}`,86 );87 const build = newest88 ? `{"version":${str(newest.version)},"platforms":{${platforms?.join(",")}}}`89 : "null";90 return `{"head":${headPart},"latest_build":${build}}`;91}9293export type RepoMeta = {94 repo: string;95 saveCount: number;96 lineCount: number;97 lastSaveAt: number;98 latestRelease: string | null;99 description: string | null;100 language: string | null;101 category: string | null;102};103104105106107108export function metaJson(meta: RepoMeta): string {109 return `{"repo":${str(meta.repo)},"save_count":${meta.saveCount},"line_count":${meta.lineCount},"last_save_at":${meta.lastSaveAt},"latest_release":${optional(meta.latestRelease)},"latest_version":${optional(meta.latestRelease)},"description":${optional(meta.description)},"languages":${optional(meta.language)},"categories":${optional(meta.category)}}`;110}111112113export function checksumsTxt(repo: string, release: Release): string {114 const lines = (algorithm: "sha256" | "blake3") =>115 release.files.map((file) => `${algorithm}:${file[algorithm]} ${repo}-${file.platform}\n`);116 return `# ${repo} ${release.version}\n# built by kepr\n\n${[...lines("sha256"), ...lines("blake3")].join("")}`;117}