"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { createHash } = require("node:crypto"); const { mkdtemp, readFile, rm } = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); const { inflateRawSync } = require("node:zlib"); const { CHROMIUM_ARCHIVE_FILES, FIREFOX_ARCHIVE_FILES, normalizeVersion, packageExtension } = require("../scripts/package-extension.js"); function readZipEntries(archive) { const entries = new Map(); let offset = 0; while (archive.readUInt32LE(offset) === 0x04034b50) { const method = archive.readUInt16LE(offset + 8); const compressedSize = archive.readUInt32LE(offset + 18); const nameLength = archive.readUInt16LE(offset + 26); const extraLength = archive.readUInt16LE(offset + 28); const nameStart = offset + 30; const dataStart = nameStart + nameLength + extraLength; const name = archive.subarray(nameStart, nameStart + nameLength).toString("utf8"); const compressed = archive.subarray(dataStart, dataStart + compressedSize); assert.equal(method, 8, `${name} should use DEFLATE compression`); entries.set(name, inflateRawSync(compressed)); offset = dataStart + compressedSize; } assert.equal(archive.readUInt32LE(offset), 0x02014b50, "central directory is present"); return entries; } test("normalizes release tags and rejects versions Firefox cannot publish", () => { assert.equal(normalizeVersion("1.2.3"), "1.2.3"); assert.equal(normalizeVersion("v2.0"), "2.0"); assert.equal(normalizeVersion("V3"), "3"); for (const version of ["", "v", "1.2.3.4.5", "1.02", "1.0-beta", "1000000000"]) { assert.throws(() => normalizeVersion(version), /Invalid Mozilla extension version|required/); } }); test("builds a deterministic unsigned XPI with the release version and runtime files only", async (context) => { const rootDirectory = path.resolve(__dirname, ".."); const firstOutput = await mkdtemp(path.join(os.tmpdir(), "glagolify-package-")); const secondOutput = await mkdtemp(path.join(os.tmpdir(), "glagolify-package-")); context.after(async () => { await Promise.all([ rm(firstOutput, { recursive: true, force: true }), rm(secondOutput, { recursive: true, force: true }) ]); }); const first = await packageExtension({ rootDirectory, outputDirectory: firstOutput, version: "v9.8.7" }); const second = await packageExtension({ rootDirectory, outputDirectory: secondOutput, version: "9.8.7" }); const firstArchive = await readFile(first.archivePath); const secondArchive = await readFile(second.archivePath); const entries = readZipEntries(firstArchive); assert.equal(first.archiveName, "glagolify-9.8.7.xpi"); assert.deepEqual([...entries.keys()], [...FIREFOX_ARCHIVE_FILES]); const manifest = JSON.parse(entries.get("manifest.json")); assert.equal(manifest.version, "9.8.7"); assert.equal(manifest.manifest_version, 2); assert.equal(manifest.background.persistent, true); assert.equal(manifest.browser_action.default_popup, "popup.html"); assert.equal(entries.has("package.json"), false); assert.equal(entries.has("tests/package-extension.test.js"), false); assert.deepEqual(firstArchive, secondArchive); const digest = createHash("sha256").update(firstArchive).digest("hex"); assert.equal( await readFile(first.checksumPath, "utf8"), `${digest} glagolify-9.8.7.xpi\n` ); }); test("builds a Chromium MV3 ZIP with Chromium manifest fields and raster icons", async (context) => { const rootDirectory = path.resolve(__dirname, ".."); const outputDirectory = await mkdtemp(path.join(os.tmpdir(), "glagolify-chromium-")); context.after(() => rm(outputDirectory, { recursive: true, force: true })); const result = await packageExtension({ rootDirectory, outputDirectory, target: "chromium", version: "v4.5.6" }); const archive = await readFile(result.archivePath); const entries = readZipEntries(archive); const manifest = JSON.parse(entries.get("manifest.json")); assert.equal(result.archiveName, "glagolify-4.5.6-chromium.zip"); assert.deepEqual([...entries.keys()], [...CHROMIUM_ARCHIVE_FILES]); assert.equal(manifest.manifest_version, 3); assert.equal(manifest.minimum_chrome_version, "102"); assert.equal(manifest.version, "4.5.6"); assert.deepEqual(manifest.permissions, ["activeTab", "storage"]); assert.deepEqual(manifest.host_permissions, [""]); assert.deepEqual(manifest.background, { service_worker: "background.js" }); assert.equal(manifest.action.default_popup, "popup.html"); assert.equal("browser_action" in manifest, false); assert.equal("browser_specific_settings" in manifest, false); assert.deepEqual(manifest.web_accessible_resources, [ { resources: [ "fonts/NotoSansGlagolitic-Regular.ttf", "fonts/Shafarik-Regular.ttf" ], matches: [""] } ]); assert.equal(manifest.icons["128"], "icons/icon-128.png"); for (const relativePath of CHROMIUM_ARCHIVE_FILES.filter((name) => name.endsWith(".png") )) { assert.deepEqual( entries.get(relativePath).subarray(0, 8), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) ); } const popup = entries.get("popup.html").toString("utf8"); for (const [, relativePath] of popup.matchAll(/\b(?:href|src)="([^"]+)"/g)) { assert.equal( entries.has(relativePath), true, `${relativePath} referenced by popup.html should be packaged` ); } const digest = createHash("sha256").update(archive).digest("hex"); assert.equal( await readFile(result.checksumPath, "utf8"), `${digest} glagolify-4.5.6-chromium.zip\n` ); }); test("rejects Chromium version components outside the MV3 manifest range", async () => { await assert.rejects( packageExtension({ rootDirectory: path.resolve(__dirname, ".."), outputDirectory: os.tmpdir(), target: "chromium", version: "65536" }), /cannot exceed 65535/ ); });