mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(mobile): bound framework conventions to path segments (#1506)
Co-authored-by: morluto <[email protected]>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>]}'
|
||||
```
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -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.",
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
Reference in New Issue
Block a user