112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import {
|
|
TextReader,
|
|
Uint8ArrayReader,
|
|
Uint8ArrayWriter,
|
|
ZipWriter,
|
|
} from "@zip.js/zip.js";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { projectAndroidApplicationEvidence } from "../src/application/AndroidApplicationService.js";
|
|
import { runProviderAnalysis } from "../src/application/DirectAnalysis.js";
|
|
import { androidApplicationProjectionResultSchema } from "../src/domain/androidApplication.js";
|
|
import { parseEvidence } from "../src/domain/evidence.js";
|
|
|
|
describe("Android application projection", () => {
|
|
it("projects deterministic APK components and explicit bridge hypotheses", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-android-"));
|
|
const path = join(root, "Fixture.apk");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
await writer.add(
|
|
"AndroidManifest.xml",
|
|
new Uint8ArrayReader(Uint8Array.from([3, 0, 8, 0])),
|
|
);
|
|
await writer.add(
|
|
"classes.dex",
|
|
new Uint8ArrayReader(
|
|
Uint8Array.from([0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0]),
|
|
),
|
|
);
|
|
await writer.add(
|
|
"lib/arm64-v8a/libreactnativejni.so",
|
|
new Uint8ArrayReader(
|
|
Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]),
|
|
),
|
|
);
|
|
await writer.add("assets/index.js", new TextReader("bridge();"));
|
|
await writer.add("META-INF/FIXTURE.RSA", new TextReader("opaque signing"));
|
|
await writeFile(path, await writer.close());
|
|
|
|
const inventory = parseEvidence(
|
|
await runProviderAnalysis(path, "inventory_artifact", {
|
|
node_limit: 500,
|
|
occurrence_limit: 500,
|
|
edge_limit: 500,
|
|
}),
|
|
);
|
|
const first = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
limits: { max_components: 100 },
|
|
});
|
|
const second = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
limits: { max_components: 100 },
|
|
});
|
|
expect(first.ok).toBe(true);
|
|
expect(second.ok).toBe(true);
|
|
if (!first.ok || !second.ok) return;
|
|
const left = androidApplicationProjectionResultSchema.parse(
|
|
first.value.normalized_result,
|
|
);
|
|
const right = androidApplicationProjectionResultSchema.parse(
|
|
second.value.normalized_result,
|
|
);
|
|
expect(left).toEqual(right);
|
|
expect(left).toMatchObject({
|
|
root_format: "apk",
|
|
coverage: {
|
|
status: "complete-within-inventory",
|
|
inventory_complete: true,
|
|
omitted_components: 0,
|
|
},
|
|
});
|
|
expect(left.components.manifests).toHaveLength(1);
|
|
expect(left.components.dex).toHaveLength(1);
|
|
expect(left.components.native_libraries).toHaveLength(1);
|
|
expect(left.components.javascript).toHaveLength(1);
|
|
expect(left.components.signing).toHaveLength(1);
|
|
expect(left.runtime_families).toEqual(
|
|
expect.arrayContaining([
|
|
"dalvik-art",
|
|
"javascript",
|
|
"native",
|
|
"react-native",
|
|
]),
|
|
);
|
|
expect(left.bridge_candidates).toEqual([
|
|
expect.objectContaining({ basis: "react-native-convention" }),
|
|
]);
|
|
expect(JSON.stringify(left)).not.toContain("opaque signing");
|
|
});
|
|
|
|
it("rejects non-APK inventory Evidence", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-android-invalid-"));
|
|
const path = join(root, "fixture.zip");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
await writer.add("one.js", new TextReader("one"));
|
|
await writeFile(path, await writer.close());
|
|
const inventory = parseEvidence(
|
|
await runProviderAnalysis(path, "inventory_artifact", {}),
|
|
);
|
|
expect(
|
|
projectAndroidApplicationEvidence({ inventory_evidence: [inventory] }),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisInputError" },
|
|
});
|
|
});
|
|
});
|