Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

82 lines · 4.6 KBRaw History
1// Reads the plugin zip in apps/jetbrains/dist for Diffinery's server build (apps/diffinery/scripts/build-docs.mjs),
2// which serves it at /-/jetbrains/. The zip is checked in because the server's build has no JDK, so this refuses one
3// that doesn't match the sources: its version must be gradle.properties' pluginVersion, and the fingerprint of
4// src/main built into it (see sourcesFingerprint in build.gradle.kts) must match the sources as they are now.
5// Rebuild it with `./gradlew dist`.
6import { createHash } from "node:crypto";
7import { readdir, readFile } from "node:fs/promises";
8import path from "node:path";
9import { fileURLToPath } from "node:url";
10import { inflateRawSync } from "node:zlib";
11
12const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13const sha256 = (b) => createHash("sha256").update(b).digest("hex");
14
15/** The entries of a zip file, by name. Enough of the format for what Gradle writes: stored or deflated, no zip64. */
16export function unzip(buf) {
17 let eocd = -1;
18 for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 65535); i--)
19 if (buf.readUInt32LE(i) === 0x06054b50) {
20 eocd = i;
21 break;
22 }
23 if (eocd < 0) throw new Error("not a zip file");
24 const count = buf.readUInt16LE(eocd + 10);
25 let p = buf.readUInt32LE(eocd + 16);
26 const entries = new Map();
27 for (let n = 0; n < count; n++) {
28 if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error("bad zip central directory");
29 const method = buf.readUInt16LE(p + 10);
30 const size = buf.readUInt32LE(p + 20);
31 const nameLen = buf.readUInt16LE(p + 28);
32 const local = buf.readUInt32LE(p + 42);
33 const name = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
34 p += 46 + nameLen + buf.readUInt16LE(p + 30) + buf.readUInt16LE(p + 32);
35 if (name.endsWith("/")) continue;
36 const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
37 const raw = buf.subarray(start, start + size);
38 if (method === 0) entries.set(name, raw);
39 else if (method === 8) entries.set(name, inflateRawSync(raw));
40 else throw new Error(`${name}: zip method ${method} isn't supported`);
41 }
42 return entries;
43}
44
45/** The same fingerprint of src/main as build.gradle.kts computes. */
46export async function sourcesFingerprint() {
47 const src = path.join(root, "src/main");
48 const files = (await readdir(src, { recursive: true, withFileTypes: true })).filter((d) => d.isFile()).map((d) => path.relative(src, path.join(d.parentPath, d.name)).split(path.sep).join("/"));
49 files.sort();
50 let listing = "";
51 for (const f of files) listing += `${f} ${sha256(await readFile(path.join(src, f)))}\n`;
52 return sha256(listing);
53}
54
55export async function readPlugin() {
56 const props = Object.fromEntries(
57 (await readFile(path.join(root, "gradle.properties"), "utf8"))
58 .split("\n")
59 .filter((l) => /^\w+=/.test(l))
60 .map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1).trim()]),
61 );
62 const version = props.pluginVersion;
63 const fileName = `diffinery-jetbrains-${version}.zip`;
64 const rebuild = "rebuild it with `./gradlew dist` in apps/jetbrains (see apps/jetbrains/README.md)";
65 const inDist = (await readdir(path.join(root, "dist")).catch(() => [])).filter((f) => f.endsWith(".zip"));
66 if (!inDist.includes(fileName)) throw new Error(`apps/jetbrains/dist/${fileName} is missing: ${rebuild}`);
67 if (inDist.length > 1) throw new Error(`apps/jetbrains/dist has more than one zip (${inDist.join(", ")}): keep only ${fileName}`);
68 const zip = await readFile(path.join(root, "dist", fileName));
69 const jarName = `diffinery-jetbrains/lib/diffinery-jetbrains-${version}.jar`;
70 const jarBytes = unzip(zip).get(jarName);
71 if (!jarBytes) throw new Error(`apps/jetbrains/dist/${fileName} has no ${jarName}: ${rebuild}`);
72 const jar = unzip(jarBytes);
73 const xml = jar.get("META-INF/plugin.xml")?.toString("utf8") ?? "";
74 const built = jar.get("META-INF/diffinery-sources.sha256")?.toString("utf8").trim();
75 if (built !== (await sourcesFingerprint())) throw new Error(`apps/jetbrains/dist/${fileName} was built from other sources than apps/jetbrains/src/main: ${rebuild}`);
76 const attr = (re) => xml.match(re)?.[1];
77 const id = attr(/<id>([^<]+)<\/id>/);
78 const sinceBuild = attr(/since-build="([^"]+)"/);
79 if (id !== "com.diffinery" || attr(/<version>([^<]+)<\/version>/) !== version || sinceBuild !== props.sinceBuild)
80 throw new Error(`apps/jetbrains/dist/${fileName}: its plugin.xml doesn't match gradle.properties: ${rebuild}`);
81 return { id, version, sinceBuild, fileName, zip, description: attr(/<description><!\[CDATA\[([\s\S]*?)\]\]><\/description>/)?.trim() ?? "" };
82}