fix: declare toolbar icons and ship real PNGs in both archives (closes #371)
Neither manifest declared an `icons` block, so Chrome and Firefox both drew the generic puzzle piece for this extension. That is the first thing the owner sees on every launch, and an unbranded placeholder is also how a user fails to tell a real extension from a look-alike. Both manifests now declare 16/32/48/128 as `icons/icon<size>.png`, and the four PNGs live at `icons/` in the tree. build.js copies them into each browser directory relative to it, so nothing points up and out the way `dist/styles.css` does, and the receipt records them like every other emitted file. The sizes copied come from the manifest that will ship next to them, not from a second list in build.js: a size a manifest declares and `icons/` does not hold fails the build with the path that is missing, rather than emitting a directory whose manifest references nothing. script/lib/package.js already resolved `.png` strings, so an icon that reached a manifest but not the archive fails self-containment; tests/packaging.test.js now pins that case, since it was covered only incidentally before. tests/manifest.test.js asserts the declaration in both manifests, that both declare the same set, and that each referenced file is a PNG whose IHDR states the size the entry claims — a declaration alone would still permit a reference to a file that is not there or is not an image. The artwork is original, drawn from geometry rather than traced or downloaded: a flat dark-navy rounded square (#101A2E) with a teal (#35E0C2) triangular "A" — one outer triangle minus a triangular counter — rasterised with 8x8 supersampling and encoded as RGBA PNG. One shape, two flat colours, which is what a 16px toolbar slot can carry. Verified: make check 56 suites / 1023 tests, make build and make package green, both archives unpacked and the four icons confirmed inside each with bytes identical to the tree, make test-e2e 55/55 and 5/5, make test-e2e-firefox 8/8 and 7/7 (the latter installs the packaged XPI). Removing icons/icon48.png fails make build; making build.js skip one copy fails make package with "the chrome archive would not be self-contained: it is told to load icons/icon48.png, which is not in it".
This commit is contained in:
@@ -47,6 +47,12 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const MANIFEST_DIR = path.join(__dirname, "..", "manifest");
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
// The sizes both stores and both toolbars ask for.
|
||||
const EXPECTED_ICON_SIZES = ["16", "32", "48", "128"];
|
||||
|
||||
const PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
|
||||
|
||||
const EXPECTED_DIRECTIVES = {
|
||||
"default-src": ["'self'"],
|
||||
@@ -115,6 +121,51 @@ function assertPolicy(policy) {
|
||||
}
|
||||
}
|
||||
|
||||
// The declared icons, in both manifests.
|
||||
//
|
||||
// Without an "icons" block a browser draws a generic puzzle piece in the
|
||||
// toolbar for this extension, which is both the first thing the user sees and
|
||||
// how a real extension is told apart from a look-alike. Declaring one is not
|
||||
// enough on its own: an entry naming a file that is not in the tree ships a
|
||||
// reference to nothing, so the referenced bytes are read here and required to
|
||||
// be a PNG of the size the entry claims. build.js copies these into each
|
||||
// browser directory, relative to it, and script/lib/package.js then refuses to
|
||||
// build an archive that does not contain everything the manifest names.
|
||||
function assertIcons(target) {
|
||||
const icons = readManifest(target).icons;
|
||||
expect(Object.keys(icons).sort()).toEqual(EXPECTED_ICON_SIZES.sort());
|
||||
for (const size of EXPECTED_ICON_SIZES) {
|
||||
const ref = icons[size];
|
||||
expect([size, ref]).toEqual([size, `icons/icon${size}.png`]);
|
||||
|
||||
const bytes = fs.readFileSync(path.join(ROOT, ref));
|
||||
expect(bytes.subarray(0, 8)).toEqual(PNG_SIGNATURE);
|
||||
// IHDR width and height, at fixed offsets right after the signature
|
||||
// and the chunk header.
|
||||
expect([ref, bytes.readUInt32BE(16), bytes.readUInt32BE(20)]).toEqual([
|
||||
ref,
|
||||
Number(size),
|
||||
Number(size),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
describe("declared icons", () => {
|
||||
test("chrome declares real icons at every size", () => {
|
||||
assertIcons("chrome");
|
||||
});
|
||||
|
||||
test("firefox declares real icons at every size", () => {
|
||||
assertIcons("firefox");
|
||||
});
|
||||
|
||||
test("both targets declare the same icons", () => {
|
||||
expect(readManifest("firefox").icons).toEqual(
|
||||
readManifest("chrome").icons,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shipped Content Security Policy", () => {
|
||||
// MV3 takes an object and applies extension_pages to the popup and the
|
||||
// background service worker, which is where libsodium runs.
|
||||
|
||||
@@ -90,6 +90,26 @@ describe("archive self-containment", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Toolbar icons are the other file the manifest names and no bundler
|
||||
// emits, so an archive built without them would carry a manifest whose
|
||||
// "icons" resolve to nothing and a browser would fall back to a generic
|
||||
// placeholder without saying so.
|
||||
test("a manifest naming an icon that is not in the archive fails", () => {
|
||||
const { members, read } = archiveOf({
|
||||
"manifest.json": JSON.stringify({
|
||||
...MINIMAL_MANIFEST,
|
||||
icons: { 16: "icons/icon16.png", 128: "icons/icon128.png" },
|
||||
}),
|
||||
"src/popup/index.html": "<html></html>",
|
||||
"src/popup/index.js": "//",
|
||||
"src/background/index.js": "//",
|
||||
"icons/icon16.png": "PNG",
|
||||
});
|
||||
expect(() => checkSelfContained("chrome", members, read)).toThrow(
|
||||
/would not be self-contained.*icons\/icon128\.png/s,
|
||||
);
|
||||
});
|
||||
|
||||
test("an archive with no manifest.json at its root fails", () => {
|
||||
const { members, read } = archiveOf({ "src/popup/index.js": "//" });
|
||||
expect(() => checkSelfContained("chrome", members, read)).toThrow(
|
||||
|
||||
Reference in New Issue
Block a user