Skip to content

Commit 1627c02

Browse files
authored
test(ci): cover the extension build scripts' hand-written zip writer (#7481)
scripts/build-extension.mjs and scripts/build-miner-extension.mjs each hand-write a STORED (uncompressed) ZIP archive byte-for-byte -- their own CRC-32 table, manual local-file-header/central-directory/end-of-central- directory construction, and a running offset accumulator -- to produce the extension downloads a real user unzips and loads. The two writers were byte-identical copies, and the only test mentioning either script asserted wiring strings in package.json/ci.yml, never exercising the zip format, so a regression in any of the ~15 manually-computed offsets would only surface in a user's own unzip tool. Extract the shared crc32/listFiles/createStoredZip into scripts/ extension-zip-core.mjs (following the existing scripts/*-core.mjs pattern) so both build scripts import one implementation, and add a test that: - asserts the canonical crc32("123456789") === 0xCBF43926 vector, - round-trips a fixture directory (including a nested path and binary and empty entries) through createStoredZip, parsing the archive via the end-of-central-directory + central directory and verifying every file extracts byte-identically with a matching stored CRC. Closes #7464 Co-authored-by: bitfathers94 <237535319+bitfathers94@users.noreply.github.com>
1 parent dfcc2f9 commit 1627c02

5 files changed

Lines changed: 252 additions & 177 deletions

File tree

scripts/build-extension.mjs

Lines changed: 4 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2-
import { dirname, relative, resolve } from "node:path";
1+
import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2+
import { dirname, resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
44

5+
import { createStoredZip } from "./extension-zip-core.mjs";
6+
57
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
68
const source = resolve(root, "apps/loopover-extension");
79
const outDir = resolve(root, "apps/loopover-extension/dist/package");
810
const zipPath = resolve(root, "apps/loopover-ui/public/downloads/loopover-extension.zip");
9-
const crcTable = Array.from({ length: 256 }, (_, index) => {
10-
let value = index;
11-
for (let bit = 0; bit < 8; bit += 1) {
12-
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
13-
}
14-
return value >>> 0;
15-
});
1611

1712
rmSync(outDir, { recursive: true, force: true });
1813
mkdirSync(outDir, { recursive: true });
@@ -26,83 +21,3 @@ rmSync(zipPath, { force: true });
2621
writeFileSync(zipPath, createStoredZip(outDir));
2722

2823
console.log(`wrote ${zipPath.replace(`${root}/`, "")}`);
29-
30-
function createStoredZip(directory) {
31-
const files = listFiles(directory);
32-
const localParts = [];
33-
const centralParts = [];
34-
let offset = 0;
35-
36-
for (const file of files) {
37-
const name = relative(directory, file).replaceAll("\\", "/");
38-
const nameBuffer = Buffer.from(name);
39-
const data = readFileSync(file);
40-
const crc = crc32(data);
41-
const localHeader = Buffer.alloc(30);
42-
localHeader.writeUInt32LE(0x04034b50, 0);
43-
localHeader.writeUInt16LE(10, 4);
44-
localHeader.writeUInt16LE(0, 6);
45-
localHeader.writeUInt16LE(0, 8);
46-
localHeader.writeUInt16LE(0, 10);
47-
localHeader.writeUInt16LE(33, 12);
48-
localHeader.writeUInt32LE(crc, 14);
49-
localHeader.writeUInt32LE(data.length, 18);
50-
localHeader.writeUInt32LE(data.length, 22);
51-
localHeader.writeUInt16LE(nameBuffer.length, 26);
52-
localHeader.writeUInt16LE(0, 28);
53-
localParts.push(localHeader, nameBuffer, data);
54-
55-
const centralHeader = Buffer.alloc(46);
56-
centralHeader.writeUInt32LE(0x02014b50, 0);
57-
centralHeader.writeUInt16LE(20, 4);
58-
centralHeader.writeUInt16LE(10, 6);
59-
centralHeader.writeUInt16LE(0, 8);
60-
centralHeader.writeUInt16LE(0, 10);
61-
centralHeader.writeUInt16LE(0, 12);
62-
centralHeader.writeUInt16LE(33, 14);
63-
centralHeader.writeUInt32LE(crc, 16);
64-
centralHeader.writeUInt32LE(data.length, 20);
65-
centralHeader.writeUInt32LE(data.length, 24);
66-
centralHeader.writeUInt16LE(nameBuffer.length, 28);
67-
centralHeader.writeUInt16LE(0, 30);
68-
centralHeader.writeUInt16LE(0, 32);
69-
centralHeader.writeUInt16LE(0, 34);
70-
centralHeader.writeUInt16LE(0, 36);
71-
centralHeader.writeUInt32LE(0, 38);
72-
centralHeader.writeUInt32LE(offset, 42);
73-
centralParts.push(centralHeader, nameBuffer);
74-
75-
offset += localHeader.length + nameBuffer.length + data.length;
76-
}
77-
78-
const centralDirectory = Buffer.concat(centralParts);
79-
const end = Buffer.alloc(22);
80-
end.writeUInt32LE(0x06054b50, 0);
81-
end.writeUInt16LE(0, 4);
82-
end.writeUInt16LE(0, 6);
83-
end.writeUInt16LE(files.length, 8);
84-
end.writeUInt16LE(files.length, 10);
85-
end.writeUInt32LE(centralDirectory.length, 12);
86-
end.writeUInt32LE(offset, 16);
87-
end.writeUInt16LE(0, 20);
88-
89-
return Buffer.concat([...localParts, centralDirectory, end]);
90-
}
91-
92-
function listFiles(directory) {
93-
return readdirSync(directory, { withFileTypes: true })
94-
.flatMap((entry) => {
95-
const path = resolve(directory, entry.name);
96-
return entry.isDirectory() ? listFiles(path) : [path];
97-
})
98-
.filter((path) => statSync(path).isFile())
99-
.sort();
100-
}
101-
102-
function crc32(buffer) {
103-
let value = 0xffffffff;
104-
for (const byte of buffer) {
105-
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
106-
}
107-
return (value ^ 0xffffffff) >>> 0;
108-
}

scripts/build-miner-extension.mjs

Lines changed: 3 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
1+
import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
22
import { dirname, relative, resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
44

5+
import { createStoredZip } from "./extension-zip-core.mjs";
6+
57
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
68
const source = resolve(root, "apps/loopover-miner-extension");
79
const outDir = resolve(source, "dist/package");
8-
const crcTable = Array.from({ length: 256 }, (_, index) => {
9-
let value = index;
10-
for (let bit = 0; bit < 8; bit += 1) {
11-
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
12-
}
13-
return value >>> 0;
14-
});
1510

1611
const PACKAGE_FILES = [
1712
"manifest.json",
@@ -42,83 +37,3 @@ rmSync(zipPath, { force: true });
4237
writeFileSync(zipPath, createStoredZip(outDir));
4338

4439
console.log(`wrote ${relative(root, zipPath)}`);
45-
46-
function createStoredZip(directory) {
47-
const files = listFiles(directory);
48-
const localParts = [];
49-
const centralParts = [];
50-
let offset = 0;
51-
52-
for (const file of files) {
53-
const name = relative(directory, file).replaceAll("\\", "/");
54-
const nameBuffer = Buffer.from(name);
55-
const data = readFileSync(file);
56-
const crc = crc32(data);
57-
const localHeader = Buffer.alloc(30);
58-
localHeader.writeUInt32LE(0x04034b50, 0);
59-
localHeader.writeUInt16LE(10, 4);
60-
localHeader.writeUInt16LE(0, 6);
61-
localHeader.writeUInt16LE(0, 8);
62-
localHeader.writeUInt16LE(0, 10);
63-
localHeader.writeUInt16LE(33, 12);
64-
localHeader.writeUInt32LE(crc, 14);
65-
localHeader.writeUInt32LE(data.length, 18);
66-
localHeader.writeUInt32LE(data.length, 22);
67-
localHeader.writeUInt16LE(nameBuffer.length, 26);
68-
localHeader.writeUInt16LE(0, 28);
69-
localParts.push(localHeader, nameBuffer, data);
70-
71-
const centralHeader = Buffer.alloc(46);
72-
centralHeader.writeUInt32LE(0x02014b50, 0);
73-
centralHeader.writeUInt16LE(20, 4);
74-
centralHeader.writeUInt16LE(10, 6);
75-
centralHeader.writeUInt16LE(0, 8);
76-
centralHeader.writeUInt16LE(0, 10);
77-
centralHeader.writeUInt16LE(0, 12);
78-
centralHeader.writeUInt16LE(33, 14);
79-
centralHeader.writeUInt32LE(crc, 16);
80-
centralHeader.writeUInt32LE(data.length, 20);
81-
centralHeader.writeUInt32LE(data.length, 24);
82-
centralHeader.writeUInt16LE(nameBuffer.length, 28);
83-
centralHeader.writeUInt16LE(0, 30);
84-
centralHeader.writeUInt16LE(0, 32);
85-
centralHeader.writeUInt16LE(0, 34);
86-
centralHeader.writeUInt16LE(0, 36);
87-
centralHeader.writeUInt32LE(0, 38);
88-
centralHeader.writeUInt32LE(offset, 42);
89-
centralParts.push(centralHeader, nameBuffer);
90-
91-
offset += localHeader.length + nameBuffer.length + data.length;
92-
}
93-
94-
const centralDirectory = Buffer.concat(centralParts);
95-
const end = Buffer.alloc(22);
96-
end.writeUInt32LE(0x06054b50, 0);
97-
end.writeUInt16LE(0, 4);
98-
end.writeUInt16LE(0, 6);
99-
end.writeUInt16LE(files.length, 8);
100-
end.writeUInt16LE(files.length, 10);
101-
end.writeUInt32LE(centralDirectory.length, 12);
102-
end.writeUInt32LE(offset, 16);
103-
end.writeUInt16LE(0, 20);
104-
105-
return Buffer.concat([...localParts, centralDirectory, end]);
106-
}
107-
108-
function listFiles(directory) {
109-
return readdirSync(directory, { withFileTypes: true })
110-
.flatMap((entry) => {
111-
const path = resolve(directory, entry.name);
112-
return entry.isDirectory() ? listFiles(path) : [path];
113-
})
114-
.filter((path) => statSync(path).isFile())
115-
.sort();
116-
}
117-
118-
function crc32(buffer) {
119-
let value = 0xffffffff;
120-
for (const byte of buffer) {
121-
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
122-
}
123-
return (value ^ 0xffffffff) >>> 0;
124-
}

scripts/extension-zip-core.d.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function crc32(buffer: Buffer | Uint8Array): number;
2+
export function listFiles(directory: string): string[];
3+
export function createStoredZip(directory: string): Buffer;

scripts/extension-zip-core.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { readdirSync, readFileSync, statSync } from "node:fs";
2+
import { relative, resolve } from "node:path";
3+
4+
// Shared byte-level ZIP writer for the browser-extension download bundles produced by
5+
// build-extension.mjs and build-miner-extension.mjs. Both scripts hand-write a STORED
6+
// (uncompressed) archive so the shipped .zip needs no runtime deflate dependency; keeping
7+
// the writer here means a regression in any of the manual byte offsets is caught once, by
8+
// test/unit/build-extension-zip-script.test.ts, instead of by a user's own unzip tool.
9+
10+
const crcTable = Array.from({ length: 256 }, (_, index) => {
11+
let value = index;
12+
for (let bit = 0; bit < 8; bit += 1) {
13+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
14+
}
15+
return value >>> 0;
16+
});
17+
18+
export function crc32(buffer) {
19+
let value = 0xffffffff;
20+
for (const byte of buffer) {
21+
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
22+
}
23+
return (value ^ 0xffffffff) >>> 0;
24+
}
25+
26+
export function listFiles(directory) {
27+
return readdirSync(directory, { withFileTypes: true })
28+
.flatMap((entry) => {
29+
const path = resolve(directory, entry.name);
30+
return entry.isDirectory() ? listFiles(path) : [path];
31+
})
32+
.filter((path) => statSync(path).isFile())
33+
.sort();
34+
}
35+
36+
export function createStoredZip(directory) {
37+
const files = listFiles(directory);
38+
const localParts = [];
39+
const centralParts = [];
40+
let offset = 0;
41+
42+
for (const file of files) {
43+
const name = relative(directory, file).replaceAll("\\", "/");
44+
const nameBuffer = Buffer.from(name);
45+
const data = readFileSync(file);
46+
const crc = crc32(data);
47+
const localHeader = Buffer.alloc(30);
48+
localHeader.writeUInt32LE(0x04034b50, 0);
49+
localHeader.writeUInt16LE(10, 4);
50+
localHeader.writeUInt16LE(0, 6);
51+
localHeader.writeUInt16LE(0, 8);
52+
localHeader.writeUInt16LE(0, 10);
53+
localHeader.writeUInt16LE(33, 12);
54+
localHeader.writeUInt32LE(crc, 14);
55+
localHeader.writeUInt32LE(data.length, 18);
56+
localHeader.writeUInt32LE(data.length, 22);
57+
localHeader.writeUInt16LE(nameBuffer.length, 26);
58+
localHeader.writeUInt16LE(0, 28);
59+
localParts.push(localHeader, nameBuffer, data);
60+
61+
const centralHeader = Buffer.alloc(46);
62+
centralHeader.writeUInt32LE(0x02014b50, 0);
63+
centralHeader.writeUInt16LE(20, 4);
64+
centralHeader.writeUInt16LE(10, 6);
65+
centralHeader.writeUInt16LE(0, 8);
66+
centralHeader.writeUInt16LE(0, 10);
67+
centralHeader.writeUInt16LE(0, 12);
68+
centralHeader.writeUInt16LE(33, 14);
69+
centralHeader.writeUInt32LE(crc, 16);
70+
centralHeader.writeUInt32LE(data.length, 20);
71+
centralHeader.writeUInt32LE(data.length, 24);
72+
centralHeader.writeUInt16LE(nameBuffer.length, 28);
73+
centralHeader.writeUInt16LE(0, 30);
74+
centralHeader.writeUInt16LE(0, 32);
75+
centralHeader.writeUInt16LE(0, 34);
76+
centralHeader.writeUInt16LE(0, 36);
77+
centralHeader.writeUInt32LE(0, 38);
78+
centralHeader.writeUInt32LE(offset, 42);
79+
centralParts.push(centralHeader, nameBuffer);
80+
81+
offset += localHeader.length + nameBuffer.length + data.length;
82+
}
83+
84+
const centralDirectory = Buffer.concat(centralParts);
85+
const end = Buffer.alloc(22);
86+
end.writeUInt32LE(0x06054b50, 0);
87+
end.writeUInt16LE(0, 4);
88+
end.writeUInt16LE(0, 6);
89+
end.writeUInt16LE(files.length, 8);
90+
end.writeUInt16LE(files.length, 10);
91+
end.writeUInt32LE(centralDirectory.length, 12);
92+
end.writeUInt32LE(offset, 16);
93+
end.writeUInt16LE(0, 20);
94+
95+
return Buffer.concat([...localParts, centralDirectory, end]);
96+
}

0 commit comments

Comments
 (0)