Files
rea/src/android/AndroidNativeInventory.test.ts
T
gensokyo-dev 570abe95f2 feat(android): link native declarations to static JNI candidates (#7472)
Link original DEX native declarations to per-ABI function exports and relocated static JNI table candidates through shared CLI/MCP workflows. Preserve global attribution ambiguity, exact provenance, partial coverage and retained Evidence; default MCP delivery to bounded summaries with filtered/batch pages.

Use one immutable ELF relocation decoder with streaming IL2CPP materialization, cooperative bounded export/version parsing and bounded JNI state. Reuse validated session inventories without loading target libraries or executing JNI code.

Validation: relevant type/lint/build and parser/domain/public CLI/MCP regressions; genuine four-ABI NDK fixtures and independent library/slot oracles; actual JDK/JADX Conscrypt workflows with retained-source removal. Full measurements and limitations are recorded in docs/verification/android-jni-bindings-2026-10-27.md.

Closes #6403.
2026-10-27 18:53:22 +08:00

100 lines
2.9 KiB
TypeScript

import { expect, it } from "vitest";
import { readAndroidNativeInventory } from "./AndroidNativeInventory.js";
import type { JadxToolPort } from "./JadxAnalysis.js";
import type { JsonValue } from "../domain/jsonValue.js";
const SNAPSHOT = "/admitted/immutable target.apk";
const row = {
source_index: 0,
class_name: "fixture.Native",
method_name: "read_原",
descriptor: "([B)J",
dex_entry: "classes.dex",
dex_entry_index: 0,
class_index: 2,
method_index: 5,
};
const inventory = () => ({
scope: "apk-root-dex-declarations",
methods: [row],
coverage: {
status: "complete",
dex_entries: 1,
examined_dex_entries: 1,
examined_classes: 3,
examined_methods: 9,
},
unknowns: [] as string[],
});
const tools = (value: unknown): JadxToolPort => ({
json: async (name, input) => {
expect(name).toBe("rea_native_declarations");
expect(input).toEqual({ path: SNAPSHOT });
return value as JsonValue;
},
text: async () => {
throw new Error("Native inventory must not request source");
},
listClasses: async () => {
throw new Error("Native inventory must not page display classes");
},
});
it("preserves duplicate native identities from distinct original DEX members", async () => {
const source = inventory();
source.methods.push({
...row,
source_index: 1,
dex_entry: "classes2.dex",
dex_entry_index: 2,
});
source.coverage.dex_entries = 2;
source.coverage.examined_dex_entries = 2;
const actual = await readAndroidNativeInventory(tools(source), SNAPSHOT);
expect(actual.methods).toEqual(source.methods);
expect(actual.coverage.status).toBe("complete");
});
it("keeps usable facts and explicit incomplete DEX coverage", async () => {
const source = inventory();
source.coverage.status = "partial";
source.coverage.dex_entries = 2;
source.unknowns.push("classes2.dex cannot be read");
expect(await readAndroidNativeInventory(tools(source), SNAPSHOT)).toEqual(
source,
);
});
it.each([
(source: ReturnType<typeof inventory>) => {
source.methods[0] = { ...row, descriptor: "not a prototype" };
},
(source: ReturnType<typeof inventory>) => {
source.methods[0] = { ...row, source_index: 7 };
},
(source: ReturnType<typeof inventory>) => {
source.coverage.examined_dex_entries = 2;
},
(source: ReturnType<typeof inventory>) => {
source.coverage.dex_entries = 2;
},
(source: ReturnType<typeof inventory>) => {
source.coverage.examined_methods = 0;
},
(source: ReturnType<typeof inventory>) => {
source.unknowns.push("hidden incomplete input");
},
(source: ReturnType<typeof inventory>) => {
source.methods.push({ ...row, source_index: 1, dex_entry: "other.dex" });
},
])("rejects malformed or inconsistent producer facts (%#)", async (mutate) => {
const source = inventory();
mutate(source);
await expect(
readAndroidNativeInventory(tools(source), SNAPSHOT),
).rejects.toMatchObject({
_tag: "AnalysisOutputError",
});
});