diff --git a/src/domain/appleApplication.ts b/src/domain/appleApplication.ts index 5a22e8fc6..72908cd94 100644 --- a/src/domain/appleApplication.ts +++ b/src/domain/appleApplication.ts @@ -129,7 +129,10 @@ export const projectAppleApplication = ( const runtimeFamilies = identifyRuntimeFamilies(all); const bridgeProjection = identifyBridgeCandidates( retained.javascript, - [...retained.frameworks, ...retained.native_libraries], + deduplicateComponents([ + ...retained.executables, + ...retained.native_libraries, + ]), parsed.limits.max_components, ); const limitations = projectionLimitations( @@ -212,6 +215,11 @@ const retainComponents = >( const componentCount = (groups: Record): number => Object.values(groups).reduce((total, values) => total + values.length, 0); +const deduplicateComponents = (values: readonly Component[]): Component[] => + [...new Map(values.map((value) => [value.path, value])).values()].sort( + (left, right) => compare(left.path, right.path), + ); + const identifyRuntimeFamilies = (all: readonly Component[]) => { const paths = all.map(({ path }) => path.toLowerCase()); const families = new Set< diff --git a/src/domain/runtimeIdentification.ts b/src/domain/runtimeIdentification.ts index 981f4af79..32755db8c 100644 --- a/src/domain/runtimeIdentification.ts +++ b/src/domain/runtimeIdentification.ts @@ -161,7 +161,7 @@ export const identifyRuntimes = ( remaining -= observations.length; const omittedObservations = unique.length - observations.length; omitted += omittedObservations; - const provider = PROVIDERS[family]; + const provider = providerFor(family, inventory.manifest.root_format); return { family, inspection: provider.inspection, @@ -208,11 +208,25 @@ export const identifyRuntimes = ( }); }; +const providerFor = ( + family: RuntimeFamily, + rootFormat: string, +): RuntimeProvider => + family === "android" && rootFormat !== "apk" + ? { + inspection: "provider-selection-required", + id: null, + reason: + "Extract or select an APK root before using the Android application provider.", + } + : PROVIDERS[family]; + const familiesFor = (format: string, path: string): RuntimeFamily[] => { const families = new Set(); if ( format === "apk" || format === "dex" || + /(?:^|\/)[^/]+\.dex$/iu.test(path) || /(?:^|\/)AndroidManifest\.xml$/u.test(path) ) families.add("android"); diff --git a/tests/appleApplication.test.ts b/tests/appleApplication.test.ts index 34b3a7ed2..6b098e0e3 100644 --- a/tests/appleApplication.test.ts +++ b/tests/appleApplication.test.ts @@ -90,7 +90,14 @@ describe("Apple application projection", () => { expect.arrayContaining(["javascript", "native", "react-native"]), ); expect(left.bridge_candidates).toEqual([ - expect.objectContaining({ basis: "react-native-convention" }), + expect.objectContaining({ + basis: "javascript-and-native-content", + native_path: "Payload/Fixture.app/Fixture", + }), + expect.objectContaining({ + basis: "react-native-convention", + native_path: "Payload/Fixture.app/Frameworks/React.framework/React", + }), ]); expect(JSON.stringify(left)).not.toContain("opaque signing bytes"); }); diff --git a/tests/runtimeIdentification.test.ts b/tests/runtimeIdentification.test.ts index 2a7111dc7..2d4423177 100644 --- a/tests/runtimeIdentification.test.ts +++ b/tests/runtimeIdentification.test.ts @@ -127,4 +127,30 @@ describe("runtime identification", () => { }); expect(result.coverage.omitted_observations).toBeGreaterThan(0); }); + + it("does not route nested DEX content to the APK-only provider", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-runtime-dex-")); + const path = join(root, "Fixture.zip"); + const writer = new ZipWriter(new Uint8ArrayWriter()); + await writer.add( + "classes.dex", + new Uint8ArrayReader( + Uint8Array.from([...Buffer.from("dex\n035\0"), 0, 0, 0, 0]), + ), + ); + await writeFile(path, await writer.close()); + + const inventory = parseEvidence( + await runProviderAnalysis(path, "inventory_artifact", {}), + ); + const result = identifyRuntimes({ inventory_evidence: [inventory] }); + + expect(result.runtimes).toContainEqual( + expect.objectContaining({ + family: "android", + inspection: "provider-selection-required", + provider_id: null, + }), + ); + }); });