@@ -92,6 +92,15 @@ test("compares two local package inventories", async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole("row", { name: /changed word\/document\.xml/u }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByRole("button", { name: "Verify comparable contents" })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByText(/Verified \d+ comparable entries/u),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("row", { name: /changed word\/document\.xml/u }),
|
||||
).toContainText(/[0-9a-f]{12} ↔ [0-9a-f]{12}/u);
|
||||
});
|
||||
|
||||
test("integrates help, theme, PWA identity and hardened headers", async ({
|
||||
@@ -123,7 +132,7 @@ test("integrates help, theme, PWA identity and hardened headers", async ({
|
||||
const manifest = await request.get("/deep/nested/package/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.package-tools",
|
||||
version: "0.1.0",
|
||||
version: "0.2.0",
|
||||
privacy: { processing: "local", telemetry: false },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/package/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -104,7 +104,19 @@ describe("compound package analysis", () => {
|
||||
const jar = await analyzePackage(
|
||||
archive("tool.jar", {
|
||||
"META-INF/MANIFEST.MF": strToU8(
|
||||
"Manifest-Version: 1.0\r\nMain-Class: example.Main\r\n",
|
||||
"Manifest-Version: 1.0\r\nMain-Class: example.Main\r\nClass-Path: lib/a.jar lib/b.jar\r\n",
|
||||
),
|
||||
"META-INF/LOCAL.SF": strToU8(
|
||||
"Signature-Version: 1.0\r\nSHA-256-Digest-Manifest: abc\r\n",
|
||||
),
|
||||
"META-INF/LOCAL.RSA": new Uint8Array([1, 2, 3]),
|
||||
"META-INF/LICENSE.txt": strToU8("Example licence evidence"),
|
||||
"package.json": strToU8(
|
||||
JSON.stringify({
|
||||
dependencies: { alpha: "^1.2.0" },
|
||||
devDependencies: { beta: "2.0.0" },
|
||||
license: "MIT",
|
||||
}),
|
||||
),
|
||||
"example/Main.class": new Uint8Array([0xca, 0xfe, 0xba, 0xbe]),
|
||||
}),
|
||||
@@ -115,6 +127,30 @@ describe("compound package analysis", () => {
|
||||
name: "Main-Class",
|
||||
value: "example.Main",
|
||||
});
|
||||
expect(jar.dependencies).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: "alpha", scope: "runtime" }),
|
||||
expect.objectContaining({
|
||||
name: "lib/a.jar",
|
||||
scope: "JAR Class-Path declaration",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(jar.licenses).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: "MIT", source: "package.json" }),
|
||||
expect.objectContaining({ source: "META-INF/LICENSE.txt" }),
|
||||
]),
|
||||
);
|
||||
expect(jar.signatures).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "META-INF/LOCAL.SF",
|
||||
state: "complete-pair",
|
||||
declaredDigests: ["SHA-256"],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("adapts APK and both WebExtension identities with bounded inventories", async () => {
|
||||
|
||||
@@ -45,7 +45,23 @@ describe("inventory comparison", () => {
|
||||
changed: "changed",
|
||||
left: "only-left",
|
||||
right: "only-right",
|
||||
same: "same",
|
||||
same: "unverified",
|
||||
});
|
||||
});
|
||||
|
||||
it("only calls file contents equal after matching SHA-256 evidence", () => {
|
||||
const left = document([entry("same-size", 4, "declared")]);
|
||||
const right = document([entry("same-size", 4, "declared")]);
|
||||
expect(compareInventories(left, right)[0]?.status).toBe("unverified");
|
||||
expect(
|
||||
compareInventories(left, right, {
|
||||
"same-size": { leftSha256: "abc", rightSha256: "abc" },
|
||||
})[0]?.status,
|
||||
).toBe("same");
|
||||
expect(
|
||||
compareInventories(left, right, {
|
||||
"same-size": { leftSha256: "abc", rightSha256: "def" },
|
||||
})[0]?.status,
|
||||
).toBe("changed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { strToU8, zipSync } from "fflate";
|
||||
import { analyzePackage } from "../../src/package/analyze";
|
||||
import { compareSemanticEntry } from "../../src/package/content-compare";
|
||||
import { verifyPackageContents } from "../../src/package/content-compare";
|
||||
|
||||
const bytes = (value: string) => new TextEncoder().encode(value);
|
||||
|
||||
describe("bounded semantic package comparison", () => {
|
||||
it("hashes verified decompressed bytes instead of ZIP representation", async () => {
|
||||
const contents = { "same.txt": strToU8("same decompressed value") };
|
||||
const left = await analyzePackage(
|
||||
new File([zipSync(contents, { level: 0 })], "left.zip"),
|
||||
);
|
||||
const right = await analyzePackage(
|
||||
new File([zipSync(contents, { level: 9 })], "right.zip"),
|
||||
);
|
||||
const evidence = await verifyPackageContents(left, right);
|
||||
expect(evidence["same.txt"]?.leftSha256).toMatch(/^[0-9a-f]{64}$/u);
|
||||
expect(evidence["same.txt"]?.leftSha256).toBe(
|
||||
evidence["same.txt"]?.rightSha256,
|
||||
);
|
||||
});
|
||||
|
||||
it("recognizes equivalent JSON serialization and reports value paths", () => {
|
||||
expect(
|
||||
compareSemanticEntry(
|
||||
"manifest.json",
|
||||
bytes('{"name":"demo","items":[1,2]}'),
|
||||
bytes('{\n "items": [1, 2], "name": "demo"\n}'),
|
||||
),
|
||||
).toMatchObject({ kind: "json", equivalent: true });
|
||||
expect(
|
||||
compareSemanticEntry(
|
||||
"manifest.json",
|
||||
bytes('{"name":"before"}'),
|
||||
bytes('{"name":"after"}'),
|
||||
)?.differences,
|
||||
).toContain("$.name: value changed");
|
||||
});
|
||||
|
||||
it("normalizes safe XML attributes but rejects active declarations", () => {
|
||||
expect(
|
||||
compareSemanticEntry(
|
||||
"content.xml",
|
||||
bytes('<root a="1" b="2"><item>value</item></root>'),
|
||||
bytes('<root b="2" a="1">\n<item>value</item>\n</root>'),
|
||||
),
|
||||
).toMatchObject({ kind: "xml", equivalent: true });
|
||||
expect(
|
||||
compareSemanticEntry(
|
||||
"content.xml",
|
||||
bytes("<!DOCTYPE root><root/>"),
|
||||
bytes("<root/>"),
|
||||
)?.summary,
|
||||
).toMatch(/could not/u);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user