@@ -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