From fb3749aff692a7a8225dff00a4516b48a76866b6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:00:22 +0800 Subject: [PATCH] fix(android): align runtime suffix inference (#977) --- src/domain/android/androidApplication.ts | 11 ++++- tests/composition/android/application.test.ts | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/domain/android/androidApplication.ts b/src/domain/android/androidApplication.ts index 0a48a8a98..55718a664 100644 --- a/src/domain/android/androidApplication.ts +++ b/src/domain/android/androidApplication.ts @@ -118,6 +118,7 @@ export const projectAndroidApplication = ( ? ["Source inventory pages are incomplete; absence is unknown."] : []), "Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.", + "Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.", "Bridge candidates are path-based hypotheses, not decoded JNI declarations or observed runtime calls.", ]; const withoutId = { @@ -176,11 +177,17 @@ const runtimeFamilies = (all: readonly Component[]) => { const families = new Set< AndroidApplicationProjectionResult["runtime_families"][number] >(); - if (all.some(({ path, format }) => format === "dex" || path.endsWith(".dex"))) + if ( + all.some( + ({ path, format }) => + format === "dex" || path.toLowerCase().endsWith(".dex"), + ) + ) families.add("dalvik-art"); if ( all.some( - ({ path, format }) => format === "jvm-class" || path.endsWith(".class"), + ({ path, format }) => + format === "jvm-class" || path.toLowerCase().endsWith(".class"), ) ) families.add("java-kotlin"); diff --git a/tests/composition/android/application.test.ts b/tests/composition/android/application.test.ts index fe8def8ad..ad7ff2b94 100644 --- a/tests/composition/android/application.test.ts +++ b/tests/composition/android/application.test.ts @@ -99,6 +99,50 @@ describe("Android application projection", () => { }); }); + 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");