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 historyJson(releases: Release[]): string {58 const rows = releases.map((r) => `{"version":${str(r.version)},"notes":${str(r.notes)}}`);59 return `[${rows.join(",")}]`;60}6162export type UpdateHead = { id: string; version: string; createdAt: number };636465export function updateInfoJson(66 domain: string,67 repo: string,68 saveHead: UpdateHead | null,69 newest: Release | undefined,70): string {71 const headPart = saveHead72 ? `{"save_id":${str(saveHead.id)},"version":${str(saveHead.version)},"timestamp":${saveHead.createdAt}}`73 : "null";74 const platforms = newest?.files.map(75 (file) =>76 `${str(file.platform)}:{"url":"https://${jsonEscape(domain)}/${repo}/releases/${newest.version}/${repo}-${file.platform}","blake3":${str(file.blake3)}}`,77 );78 const build = newest79 ? `{"version":${str(newest.version)},"platforms":{${platforms?.join(",")}}}`80 : "null";81 return `{"head":${headPart},"latest_build":${build}}`;82}8384export type RepoMeta = {85 repo: string;86 saveCount: number;87 lineCount: number;88 lastSaveAt: number;89 latestRelease: string | null;90 description: string | null;91 language: string | null;92 category: string | null;93};949596979899export function metaJson(meta: RepoMeta): string {100 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)}}`;101}102103104export function checksumsTxt(repo: string, release: Release): string {105 const lines = (algorithm: "sha256" | "blake3") =>106 release.files.map((file) => `${algorithm}:${file[algorithm]} ${repo}-${file.platform}\n`);107 return `# ${repo} ${release.version}\n# built by kepr\n\n${[...lines("sha256"), ...lines("blake3")].join("")}`;108}