Release Package Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 11:29:24 +02:00
parent e42943e689
commit 01c7a8c372
32 changed files with 1316 additions and 85 deletions
+37 -1
View File
@@ -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 () => {
+17 -1
View File
@@ -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");
});
});
+58
View File
@@ -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);
});
});