fix(mobile): bound framework conventions to path segments (#1506)

Co-authored-by: morluto <[email protected]>
This commit is contained in:
Thang Luu
2026-10-10 21:42:03 +08:00
committed by GitHub
co-authored by morluto
parent 8c47a9ce4b
commit a929c9f714
7 changed files with 163 additions and 7 deletions
+6 -1
View File
@@ -153,7 +153,12 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
The projection reports exact component paths and hashes, runtime-family hints,
and path-based bridge hypotheses. It does not decode DEX or claim observed
runtime calls. Bridge hypotheses form a DEX/JVM-class by native-library
runtime calls. Framework hints match complete native library names, such as
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
The bridge basis is inferred from the native path and repeated for each managed
component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library
Cartesian product. The projection computes the product and exact serialized
candidate-array size before creating candidate objects, then retains a
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
+10
View File
@@ -7,6 +7,16 @@ hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
runtime calls.
Framework hints match complete path segments such as `React.framework`,
`ReactNative.framework`, `hermes.framework`, `Flutter.framework`, and
`UnityFramework.framework`. Substrings in `Preact.framework`,
`Community.framework`, or `MyApp.framework` do not identify those families.
A generic `App.framework` also does not identify Flutter by itself. Runtime
families remain inventory-derived hints, not observed loading. Generic framework
and dylib paths do not establish Swift or Objective-C; this projection does not
decode language metadata. A bridge basis is inferred from the native path and
repeated for each paired script, rather than proving a particular binding.
```sh
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
+1 -1
View File
@@ -222,7 +222,7 @@ const bridgeBasis = (
if (convention === "react-native") return "react-native-convention";
if (convention === "flutter") return "flutter-convention";
if (convention === "unity") return "unity-convention";
if (/lib\/[^/]+\/lib[^/]+\.so$/u.test(path.toLowerCase()))
if (/(?:^|\/)lib\/[^/]+\/lib[^/]+\.so$/iu.test(path))
return "jni-library-convention";
return "managed-and-native-content";
};
+1 -1
View File
@@ -624,7 +624,7 @@ describe("Apple path conventions", () => {
).toBe(true);
expect(result.limitations).toEqual(
expect.arrayContaining([
"Runtime families are inferred from inventory formats and paths.",
"Runtime families are inferred from inventory formats and bounded framework path conventions, not observed runtime loading.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
]),
);
+2 -1
View File
@@ -431,7 +431,8 @@ const projectionLimitations = (facts: {
: []),
"Bundle roles follow path conventions. Read each info_plist_path with inspect_plist for CFBundleExecutable, identifiers, and declared services.",
"Bundle identifiers and signing claims require dedicated plist and CMS parsing; this projection reports only exact paths and hashes.",
"Runtime families are inferred from inventory formats and paths.",
"Runtime families are inferred from inventory formats and bounded framework path conventions, not observed runtime loading.",
"Generic framework and dylib paths do not establish Swift or Objective-C; this inventory projection does not decode language metadata.",
"Bridge candidates are path-based hypotheses, not observed runtime calls.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
];
+6 -3
View File
@@ -7,6 +7,10 @@ const foldSegment = (segment: string): string =>
const REACT_NATIVE_SEGMENTS = new Set([
"react.framework",
"reactnative.framework",
"libreactnative.so",
"libreactnativejni.so",
"libhermes_executor.so",
"libhermes.so",
"hermes.framework",
]);
@@ -15,15 +19,14 @@ const UNITY_SEGMENTS = new Set(["libunity.so", "unityframework.framework"]);
/**
* A runtime family named by one filename or directory segment.
* `reactnative` may occur inside a segment (`libreactnativejni.so`).
* The other names match the whole segment.
* Only recognized complete library and framework names establish a convention.
*/
export const nativeRuntimeConvention = (
path: string,
): NativeRuntimeConvention | null => {
for (const segment of path.split("/")) {
const folded = foldSegment(segment);
if (folded.includes("reactnative") || REACT_NATIVE_SEGMENTS.has(folded))
if (REACT_NATIVE_SEGMENTS.has(folded))
return "react-native";
if (FLUTTER_SEGMENTS.has(folded)) return "flutter";
if (UNITY_SEGMENTS.has(folded)) return "unity";
@@ -0,0 +1,137 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
TextReader,
Uint8ArrayReader,
Uint8ArrayWriter,
ZipWriter,
} from "@zip.js/zip.js";
import { expect } from "vitest";
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const android = {
extension: "apk",
operation: "project_android_application_graph",
command: "project-android-application-graph",
scripts: ["classes.dex", "extra.dex"],
nativeBytes: Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]),
libraries: {
"lib/arm64-v8a/libcommunity.so": "jni-library-convention",
"lib/arm64-v8a/libpreact.so": "jni-library-convention",
"lib/arm64-v8a/libreactivity.so": "jni-library-convention",
"lib/arm64-v8a/libhermesfake.so": "jni-library-convention",
"lib/arm64-v8a/libflutter.so.backup.so": "jni-library-convention",
},
positives: {
"lib/arm64-v8a/libreactnativejni.so": "react-native-convention",
"lib/arm64-v8a/libhermes.so": "react-native-convention",
"lib/arm64-v8a/libflutter.so": "flutter-convention",
"lib/arm64-v8a/libunity.so": "unity-convention",
},
};
const apple = {
extension: "ipa",
operation: "project_apple_application_graph",
command: "project-apple-application-graph",
scripts: ["Payload/Fixture.app/main.js", "Payload/Fixture.app/extra.js"],
nativeBytes: Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0, 0, 1]),
libraries: {
"Payload/Fixture.app/Frameworks/App.framework/App":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/MyApp.framework/MyApp":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Community.framework/Community":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Preact.framework/Preact":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Reactivity.framework/Reactivity":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/HermesExtra.framework/HermesExtra":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Flutter.framework.backup/Flutter":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/plain.dylib":
"javascript-and-native-content",
},
positives: {
"Payload/Fixture.app/Frameworks/React.framework/React":
"react-native-convention",
"Payload/Fixture.app/Frameworks/hermes.framework/hermes":
"react-native-convention",
"Payload/Fixture.app/Frameworks/Flutter.framework/Flutter":
"flutter-convention",
"Payload/Fixture.app/Frameworks/UnityFramework.framework/UnityFramework":
"unity-convention",
},
};
cliTest(
"bounds mobile conventions and preserves inference provenance through compiled CLI and real MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-mobile-conventions-");
const { call } = await connectLocalToolsMcp();
const { runProviderAnalysis } = createDirectAnalysis({});
for (const fixture of [android, apple]) {
for (const positive of [false, true]) {
const libraries = positive ? fixture.positives : fixture.libraries;
const path = join(
root,
`${positive ? "positive" : "negative"}.${fixture.extension}`,
);
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const script of fixture.scripts)
await writer.add(script, new TextReader("export const value = 1;"));
for (const name of Object.keys(libraries))
await writer.add(name, new Uint8ArrayReader(fixture.nativeBytes));
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
const input = { inventory_evidence: [inventory] };
const result = await cli.run({
arguments: [fixture.command, JSON.stringify(input), "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = parseEvidence(result.json);
const response = await call(fixture.operation, input);
expect(response.isError).not.toBe(true);
const mcp = parseEvidence(response.structuredContent);
expect(mcp).toEqual(evidence);
expect(evidence.normalized_result).toMatchObject({
runtime_families: positive
? fixture.extension === "apk"
? ["dalvik-art", "flutter", "native", "react-native", "unity"]
: ["flutter", "javascript", "native", "react-native", "unity"]
: fixture.extension === "apk"
? ["dalvik-art", "native"]
: ["javascript", "native"],
coverage: {
status: "complete-within-inventory",
inventory_complete: true,
},
limitations: expect.arrayContaining([
"A bridge basis is inferred from the native path and is repeated for every managed component.",
]),
bridge_candidates: expect.arrayContaining(
Object.entries(libraries).flatMap(([native_path, basis]) =>
fixture.scripts.map((script) =>
expect.objectContaining({
native_path,
basis,
...(fixture.extension === "apk"
? { managed_path: script }
: { source_path: script }),
}),
),
),
),
});
}
}
},
30_000,
);