mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
277 lines
7.9 KiB
TypeScript
277 lines
7.9 KiB
TypeScript
export interface UpdateManifestFile {
|
|
readonly url: string;
|
|
readonly sha512: string;
|
|
readonly size: number;
|
|
}
|
|
|
|
export type UpdateManifestScalar = string | number | boolean;
|
|
|
|
export interface UpdateManifest {
|
|
readonly version: string;
|
|
readonly releaseDate: string;
|
|
readonly files: ReadonlyArray<UpdateManifestFile>;
|
|
readonly extras: Readonly<Record<string, UpdateManifestScalar>>;
|
|
}
|
|
|
|
interface MutableUpdateManifestFile {
|
|
url?: string;
|
|
sha512?: string;
|
|
size?: number;
|
|
}
|
|
|
|
function stripSingleQuotes(value: string): string {
|
|
if (value.startsWith("'") && value.endsWith("'")) {
|
|
return value.slice(1, -1).replace(/''/g, "'");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function parseFileRecord(
|
|
currentFile: MutableUpdateManifestFile | null,
|
|
sourcePath: string,
|
|
lineNumber: number,
|
|
platformLabel: string,
|
|
): UpdateManifestFile | null {
|
|
if (currentFile === null) {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof currentFile.url !== "string" ||
|
|
typeof currentFile.sha512 !== "string" ||
|
|
typeof currentFile.size !== "number"
|
|
) {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: incomplete file entry.`,
|
|
);
|
|
}
|
|
return {
|
|
url: currentFile.url,
|
|
sha512: currentFile.sha512,
|
|
size: currentFile.size,
|
|
};
|
|
}
|
|
|
|
function parseScalarValue(rawValue: string): UpdateManifestScalar {
|
|
const trimmed = rawValue.trim();
|
|
const isQuoted = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2;
|
|
const value = isQuoted ? trimmed.slice(1, -1).replace(/''/g, "'") : trimmed;
|
|
if (isQuoted) return value;
|
|
if (value === "true") return true;
|
|
if (value === "false") return false;
|
|
if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
return Number(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function parseUpdateManifest(
|
|
raw: string,
|
|
sourcePath: string,
|
|
platformLabel: string,
|
|
): UpdateManifest {
|
|
const lines = raw.split(/\r?\n/);
|
|
const files: UpdateManifestFile[] = [];
|
|
const extras: Record<string, UpdateManifestScalar> = {};
|
|
let version: string | null = null;
|
|
let releaseDate: string | null = null;
|
|
let inFiles = false;
|
|
let currentFile: MutableUpdateManifestFile | null = null;
|
|
|
|
for (const [index, rawLine] of lines.entries()) {
|
|
const lineNumber = index + 1;
|
|
const line = rawLine.trimEnd();
|
|
if (line.length === 0) continue;
|
|
|
|
const fileUrlMatch = line.match(/^ - url:\s*(.+)$/);
|
|
if (fileUrlMatch?.[1]) {
|
|
const finalized = parseFileRecord(currentFile, sourcePath, lineNumber, platformLabel);
|
|
if (finalized) files.push(finalized);
|
|
currentFile = { url: stripSingleQuotes(fileUrlMatch[1].trim()) };
|
|
inFiles = true;
|
|
continue;
|
|
}
|
|
|
|
const fileShaMatch = line.match(/^ sha512:\s*(.+)$/);
|
|
if (fileShaMatch?.[1]) {
|
|
if (currentFile === null) {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: sha512 without a file entry.`,
|
|
);
|
|
}
|
|
currentFile.sha512 = stripSingleQuotes(fileShaMatch[1].trim());
|
|
continue;
|
|
}
|
|
|
|
const fileSizeMatch = line.match(/^ size:\s*(\d+)$/);
|
|
if (fileSizeMatch?.[1]) {
|
|
if (currentFile === null) {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: size without a file entry.`,
|
|
);
|
|
}
|
|
currentFile.size = Number(fileSizeMatch[1]);
|
|
continue;
|
|
}
|
|
|
|
if (line === "files:") {
|
|
inFiles = true;
|
|
continue;
|
|
}
|
|
|
|
if (inFiles && currentFile !== null) {
|
|
const finalized = parseFileRecord(currentFile, sourcePath, lineNumber, platformLabel);
|
|
if (finalized) files.push(finalized);
|
|
currentFile = null;
|
|
}
|
|
inFiles = false;
|
|
|
|
const topLevelMatch = line.match(/^([A-Za-z][A-Za-z0-9]*):\s*(.+)$/);
|
|
if (!topLevelMatch?.[1] || topLevelMatch[2] === undefined) {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: unsupported line '${line}'.`,
|
|
);
|
|
}
|
|
|
|
const [, key, rawValue] = topLevelMatch;
|
|
const value = parseScalarValue(rawValue);
|
|
|
|
if (key === "version") {
|
|
if (typeof value !== "string") {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: version must be a string.`,
|
|
);
|
|
}
|
|
version = value;
|
|
continue;
|
|
}
|
|
|
|
if (key === "releaseDate") {
|
|
if (typeof value !== "string") {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}:${lineNumber}: releaseDate must be a string.`,
|
|
);
|
|
}
|
|
releaseDate = value;
|
|
continue;
|
|
}
|
|
|
|
if (key === "path" || key === "sha512") {
|
|
continue;
|
|
}
|
|
|
|
extras[key] = value;
|
|
}
|
|
|
|
const finalized = parseFileRecord(currentFile, sourcePath, lines.length, platformLabel);
|
|
if (finalized) files.push(finalized);
|
|
|
|
if (!version) {
|
|
throw new Error(`Invalid ${platformLabel} update manifest at ${sourcePath}: missing version.`);
|
|
}
|
|
if (!releaseDate) {
|
|
throw new Error(
|
|
`Invalid ${platformLabel} update manifest at ${sourcePath}: missing releaseDate.`,
|
|
);
|
|
}
|
|
if (files.length === 0) {
|
|
throw new Error(`Invalid ${platformLabel} update manifest at ${sourcePath}: missing files.`);
|
|
}
|
|
|
|
return {
|
|
version,
|
|
releaseDate,
|
|
files,
|
|
extras,
|
|
};
|
|
}
|
|
|
|
function mergeExtras(
|
|
primary: Readonly<Record<string, UpdateManifestScalar>>,
|
|
secondary: Readonly<Record<string, UpdateManifestScalar>>,
|
|
platformLabel: string,
|
|
): Record<string, UpdateManifestScalar> {
|
|
const merged: Record<string, UpdateManifestScalar> = { ...primary };
|
|
|
|
for (const [key, value] of Object.entries(secondary)) {
|
|
const existing = merged[key];
|
|
if (existing !== undefined && existing !== value) {
|
|
throw new Error(
|
|
`Cannot merge ${platformLabel} update manifests: conflicting '${key}' values ('${existing}' vs '${value}').`,
|
|
);
|
|
}
|
|
merged[key] = value;
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
export function mergeUpdateManifests(
|
|
primary: UpdateManifest,
|
|
secondary: UpdateManifest,
|
|
platformLabel: string,
|
|
): UpdateManifest {
|
|
if (primary.version !== secondary.version) {
|
|
throw new Error(
|
|
`Cannot merge ${platformLabel} update manifests with different versions (${primary.version} vs ${secondary.version}).`,
|
|
);
|
|
}
|
|
|
|
const filesByUrl = new Map<string, UpdateManifestFile>();
|
|
for (const file of [...primary.files, ...secondary.files]) {
|
|
const existing = filesByUrl.get(file.url);
|
|
if (existing && (existing.sha512 !== file.sha512 || existing.size !== file.size)) {
|
|
throw new Error(
|
|
`Cannot merge ${platformLabel} update manifests: conflicting file entry for ${file.url}.`,
|
|
);
|
|
}
|
|
filesByUrl.set(file.url, file);
|
|
}
|
|
|
|
return {
|
|
version: primary.version,
|
|
releaseDate:
|
|
primary.releaseDate >= secondary.releaseDate ? primary.releaseDate : secondary.releaseDate,
|
|
files: [...filesByUrl.values()],
|
|
extras: mergeExtras(primary.extras, secondary.extras, platformLabel),
|
|
};
|
|
}
|
|
|
|
function quoteYamlString(value: string): string {
|
|
return `'${value.replace(/'/g, "''")}'`;
|
|
}
|
|
|
|
function serializeScalarValue(value: UpdateManifestScalar): string {
|
|
if (typeof value === "string") {
|
|
return quoteYamlString(value);
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
export function serializeUpdateManifest(
|
|
manifest: UpdateManifest,
|
|
options: {
|
|
readonly platformLabel: string;
|
|
},
|
|
): string {
|
|
const lines = [`version: ${quoteYamlString(manifest.version)}`, "files:"];
|
|
|
|
for (const file of manifest.files) {
|
|
lines.push(` - url: ${file.url}`);
|
|
lines.push(` sha512: ${file.sha512}`);
|
|
lines.push(` size: ${file.size}`);
|
|
}
|
|
|
|
for (const key of Object.keys(manifest.extras).toSorted()) {
|
|
const value = manifest.extras[key];
|
|
if (value === undefined) {
|
|
throw new Error(
|
|
`Cannot serialize ${options.platformLabel} update manifest: missing value for '${key}'.`,
|
|
);
|
|
}
|
|
lines.push(`${key}: ${serializeScalarValue(value)}`);
|
|
}
|
|
|
|
lines.push(`releaseDate: ${quoteYamlString(manifest.releaseDate)}`);
|
|
lines.push("");
|
|
return lines.join("\n");
|
|
}
|