1/**
2 * Deterministically builds minimal, structurally valid zip archives —
3 * enough for CreateApplication's ZIPFILE code validation (which requires a
4 * `.jar` entry inside the uploaded zip) without a real Flink build. The
5 * application is never started in ungated tests; a start with this
6 * placeholder would fail job submission.
7 */
8
9const crc32 = (data: Uint8Array): number => {
10 let crc = 0xffffffff;
11 for (let i = 0; i < data.length; i++) {
12 crc ^= data[i]!;
13 for (let j = 0; j < 8; j++) {
14 crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
15 }
16 }
17 return (crc ^ 0xffffffff) >>> 0;
18};
19
20interface ZipEntry {
21 readonly name: string;
22 readonly data: Uint8Array | string;
23}
24
25export const makeZip = (entries: ReadonlyArray<ZipEntry>): Uint8Array => {
26 const encoder = new TextEncoder();
27 const chunks: Uint8Array[] = [];
28 const centralChunks: Uint8Array[] = [];
29 let offset = 0;
30
31 for (const entry of entries) {
32 const name = encoder.encode(entry.name);
33 const body =
34 typeof entry.data === "string" ? encoder.encode(entry.data) : entry.data;
35 const crc = crc32(body);
36
37 const localHeader = new Uint8Array(30 + name.length);
38 const localView = new DataView(localHeader.buffer);
39 localView.setUint32(0, 0x04034b50, true); // local file header signature
40 localView.setUint16(4, 20, true); // version needed to extract
41 localView.setUint16(8, 0, true); // compression method: STORED
42 localView.setUint32(14, crc, true);
43 localView.setUint32(18, body.length, true); // compressed size
44 localView.setUint32(22, body.length, true); // uncompressed size
45 localView.setUint16(26, name.length, true);
46 localHeader.set(name, 30);
47
48 const centralHeader = new Uint8Array(46 + name.length);
49 const centralView = new DataView(centralHeader.buffer);
50 centralView.setUint32(0, 0x02014b50, true); // central directory signature
51 centralView.setUint16(4, 20, true); // version made by
52 centralView.setUint16(6, 20, true); // version needed to extract
53 centralView.setUint16(10, 0, true); // compression method: STORED
54 centralView.setUint32(16, crc, true);
55 centralView.setUint32(20, body.length, true);
56 centralView.setUint32(24, body.length, true);
57 centralView.setUint16(28, name.length, true);
58 centralView.setUint32(42, offset, true); // local header offset
59 centralHeader.set(name, 46);
60
61 chunks.push(localHeader, body);
62 centralChunks.push(centralHeader);
63 offset += localHeader.length + body.length;
64 }
65
66 const centralSize = centralChunks.reduce((n, c) => n + c.length, 0);
67 const eocd = new Uint8Array(22);
68 const eocdView = new DataView(eocd.buffer);
69 eocdView.setUint32(0, 0x06054b50, true); // end of central directory
70 eocdView.setUint16(8, entries.length, true); // entries on this disk
71 eocdView.setUint16(10, entries.length, true); // total entries
72 eocdView.setUint32(12, centralSize, true);
73 eocdView.setUint32(16, offset, true);
74
75 const zip = new Uint8Array(offset + centralSize + eocd.length);
76 let cursor = 0;
77 for (const chunk of [...chunks, ...centralChunks, eocd]) {
78 zip.set(chunk, cursor);
79 cursor += chunk.length;
80 }
81 return zip;
82};
83
84/**
85 * A zip whose single entry is `app.jar` — itself a structurally valid
86 * (empty) jar with a manifest.
87 */
88export const makeDummyFlinkCodeZip = (): Uint8Array => {
89 const jar = makeZip([
90 { name: "META-INF/MANIFEST.MF", data: "Manifest-Version: 1.0\r\n\r\n" },
91 ]);
92 return makeZip([{ name: "app.jar", data: jar }]);
93};