176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import {
|
|
TextReader,
|
|
Uint8ArrayReader,
|
|
Uint8ArrayWriter,
|
|
ZipWriter,
|
|
} from "@zip.js/zip.js";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
createNonApplicationZipInventory,
|
|
requireSuccessfulProjection,
|
|
} from "../../support/applicationSessionFixture.js";
|
|
|
|
import { projectAndroidApplicationEvidence } from "../../../src/application/android/AndroidApplicationService.js";
|
|
import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js";
|
|
import { androidApplicationProjectionResultSchema } from "../../../src/domain/android/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 createTestTempDirectory("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", {}),
|
|
);
|
|
const first = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
});
|
|
const second = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
});
|
|
const left = androidApplicationProjectionResultSchema.parse(
|
|
requireSuccessfulProjection(first).normalized_result,
|
|
);
|
|
const right = androidApplicationProjectionResultSchema.parse(
|
|
requireSuccessfulProjection(second).normalized_result,
|
|
);
|
|
expect(left).toEqual(right);
|
|
expect(left).toMatchObject({
|
|
root_format: "apk",
|
|
coverage: {
|
|
status: "complete-within-inventory",
|
|
inventory_complete: true,
|
|
},
|
|
});
|
|
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 inventory = await createNonApplicationZipInventory(
|
|
"rea-android-invalid-",
|
|
);
|
|
expect(
|
|
projectAndroidApplicationEvidence({ inventory_evidence: [inventory] }),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisInputError" },
|
|
});
|
|
});
|
|
|
|
it("keeps uppercase path-derived bytecode families distinct from byte validity", async () => {
|
|
const root = await createTestTempDirectory("rea-android-suffixes-");
|
|
const path = join(root, "Suffixes.apk");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
for (const entry of [
|
|
"classes.DEX",
|
|
"extra.dex",
|
|
"Main.CLASS",
|
|
"Other.class",
|
|
])
|
|
await writer.add(entry, new TextReader("unrecognized bytes"));
|
|
await writeFile(path, await writer.close());
|
|
|
|
const inventory = parseEvidence(
|
|
await runProviderAnalysis(path, "inventory_artifact", {}),
|
|
);
|
|
const result = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
});
|
|
const projection = androidApplicationProjectionResultSchema.parse(
|
|
requireSuccessfulProjection(result).normalized_result,
|
|
);
|
|
|
|
expect(projection.components.dex.map(({ path }) => path).sort()).toEqual([
|
|
"classes.DEX",
|
|
"extra.dex",
|
|
]);
|
|
expect(
|
|
projection.components.jvm_classes.map(({ path }) => path).sort(),
|
|
).toEqual(["Main.CLASS", "Other.class"]);
|
|
expect(
|
|
projection.components.dex.every(({ format }) => format === "file"),
|
|
).toBe(true);
|
|
expect(
|
|
projection.components.jvm_classes.every(
|
|
({ format }) => format === "file",
|
|
),
|
|
).toBe(true);
|
|
expect(projection.runtime_families).toEqual(["dalvik-art", "java-kotlin"]);
|
|
expect(projection.limitations).toContain(
|
|
"Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.",
|
|
);
|
|
});
|
|
|
|
it("returns every component and bridge candidate from the inventory", async () => {
|
|
const root = await createTestTempDirectory("rea-android-complete-");
|
|
const path = join(root, "Fixture.apk");
|
|
const writer = new ZipWriter(new Uint8ArrayWriter());
|
|
const dex = Uint8Array.from([0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0]);
|
|
for (let index = 0; index < 1_001; index += 1)
|
|
await writer.add(
|
|
`classes${String(index).padStart(4, "0")}.dex`,
|
|
new Uint8ArrayReader(dex),
|
|
);
|
|
await writer.add("lib/arm64-v8a/libnative.so", new TextReader("native"));
|
|
await writeFile(path, await writer.close());
|
|
|
|
const inventory = parseEvidence(
|
|
await runProviderAnalysis(path, "inventory_artifact", {}),
|
|
);
|
|
const result = projectAndroidApplicationEvidence({
|
|
inventory_evidence: [inventory],
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
const projection = androidApplicationProjectionResultSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
expect(projection.components.dex).toHaveLength(1_001);
|
|
expect(projection.bridge_candidates).toHaveLength(1_001);
|
|
expect(projection.coverage.status).toBe("complete-within-inventory");
|
|
});
|
|
});
|