mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 19:35:56 +00:00
feat(il2cpp): link native methods and export Ghidra seeds
Link IL2CPP metadata names to native addresses and types through shared CLI/MCP workflows, with bounded queries and atomic Ghidra seed export. Preserve the original implementation by 8rulerstar and follow-up correctness, resource, provenance, and CI dependency repairs. Verified against real Windows/Android artifacts, independent references, actual Ghidra, and focused CLI/MCP tests. Fixes #5112.
This commit is contained in:
1 parent
e50a271c72
commit
52d1a72af0
43 files changed
+5060
-368
No files matched your search
@@ -8,6 +8,10 @@ import {
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../../src/domain/analysisErrorCore.js";
|
||||
import { Il2cppMetadataProvider } from "../../../src/il2cpp/Il2cppMetadataProvider.js";
|
||||
import {
|
||||
createElfFixture,
|
||||
createIl2cppBinaryFixture,
|
||||
} from "../../fixtures/il2cpp/binary.js";
|
||||
import { createIl2cppMetadataFixture } from "../../fixtures/il2cpp/metadata.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
@@ -15,6 +19,16 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
/** One-shot inspections keep the real reader's cleanup at one test boundary. */
|
||||
const inspectMetadata = async (path: string, limit?: number) => {
|
||||
const provider = new Il2cppMetadataProvider(limit);
|
||||
try {
|
||||
return await provider.inspect({ path });
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
};
|
||||
|
||||
/** Version 29 file whose methods all share one long name. */
|
||||
const sharedNameMetadata = (
|
||||
nameBytes: number | Buffer,
|
||||
@@ -66,23 +80,18 @@ it("bounds repeated literal output during decoding and reuses one span instead o
|
||||
const path = join(root, "global-metadata.dat");
|
||||
await writeFile(path, sharedLiteralMetadata(Buffer.alloc(4096, 65), 20));
|
||||
const decode = vi.spyOn(TextDecoder.prototype, "decode");
|
||||
const provider = new Il2cppMetadataProvider(12_000);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expanded literals accepted");
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
validated_section_row_counts: { string_literals: 20 },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
decode.mock.calls.filter(([bytes]) => bytes?.byteLength === 4096),
|
||||
).toHaveLength(1);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
const result = await inspectMetadata(path, 12_000);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expanded literals accepted");
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
validated_section_row_counts: { string_literals: 20 },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
decode.mock.calls.filter(([bytes]) => bytes?.byteLength === 4096),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("refuses raw literal expansion before decoding and admits invalid UTF-8 only when base64 fits", async () => {
|
||||
@@ -95,20 +104,13 @@ it("refuses raw literal expansion before decoding and admits invalid UTF-8 only
|
||||
] as const) {
|
||||
decode.mockClear();
|
||||
await writeFile(path, sharedLiteralMetadata(bytes, 1));
|
||||
const provider = new Il2cppMetadataProvider(limit);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(
|
||||
decode.mock.calls.filter(
|
||||
([value]) => value?.byteLength === bytes.length,
|
||||
),
|
||||
).toHaveLength(expectedDecodes);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
const result = await inspectMetadata(path, limit);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(
|
||||
decode.mock.calls.filter(([value]) => value?.byteLength === bytes.length),
|
||||
).toHaveLength(expectedDecodes);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,61 +123,48 @@ it("counts an unclaimed parameter table before allocating any member rows", asyn
|
||||
const bytes = Buffer.from(fixture.bytes);
|
||||
bytes.writeInt32LE(0x7fffffff, fixture.section("parameters").offset);
|
||||
await writeFile(path, bytes);
|
||||
const provider = new Il2cppMetadataProvider(4096);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
const result = await inspectMetadata(path, 4096);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
});
|
||||
|
||||
it("refuses an oversized report from its row lower bound before serializing it", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-provider-budget-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
// Shared names make about 100 KiB of input expand beyond 64 MiB of JSON.
|
||||
await writeFile(path, sharedNameMetadata(65_536, 1_100));
|
||||
const stringify = vi.spyOn(JSON, "stringify");
|
||||
const provider = new Il2cppMetadataProvider();
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
it.each([
|
||||
{
|
||||
name: "shared-name row lower bound",
|
||||
bytes: () => sharedNameMetadata(65_536, 1_100),
|
||||
limit: undefined,
|
||||
},
|
||||
{
|
||||
name: "JSON escaping",
|
||||
bytes: () => sharedNameMetadata(4096, 1, 0x01),
|
||||
limit: 8192,
|
||||
},
|
||||
{
|
||||
name: "UTF-8 expansion",
|
||||
bytes: () => sharedNameMetadata(Buffer.from("雪".repeat(4096)), 1),
|
||||
limit: 8192,
|
||||
},
|
||||
])(
|
||||
"refuses $name before allocating serialized text",
|
||||
async ({ bytes, limit }) => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-text-budget-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
await writeFile(path, bytes());
|
||||
const stringify = vi.spyOn(JSON, "stringify");
|
||||
const result = await inspectMetadata(path, limit);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(
|
||||
stringify.mock.calls.some(
|
||||
([value]) =>
|
||||
typeof value === "object" && value !== null && "methods" in value,
|
||||
(typeof value === "string" && value.length >= 4096) ||
|
||||
(typeof value === "object" && value !== null && "methods" in value),
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("counts escaped name characters toward the budget without escaping the name", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-provider-escape-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
// 4 KiB of U+0001 is 4 KiB raw but 24 KiB once escaped as .
|
||||
await writeFile(path, sharedNameMetadata(4096, 1, 0x01));
|
||||
const stringify = vi.spyOn(JSON, "stringify");
|
||||
const provider = new Il2cppMetadataProvider(8192);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(
|
||||
stringify.mock.calls.some(
|
||||
([value]) => typeof value === "string" && value.length >= 4096,
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("admits a report whose escaped names fit the budget exactly", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-provider-fit-");
|
||||
@@ -207,15 +196,10 @@ it("refuses from row counts before decoding any row", async () => {
|
||||
const bytes = Buffer.from(fixture.bytes);
|
||||
bytes.writeInt32LE(0x7fff, fixture.record("methods", 0));
|
||||
await writeFile(path, bytes);
|
||||
const provider = new Il2cppMetadataProvider(256);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
const result = await inspectMetadata(path, 256);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -317,28 +301,6 @@ it("bounds concurrent snapshots and retains active decoding until close complete
|
||||
expect((await provider.inspect({ path })).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses UTF-8 expansion before serializing the complete report", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-provider-unicode-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
await writeFile(path, sharedNameMetadata(Buffer.from("雪".repeat(4096)), 1));
|
||||
const stringify = vi.spyOn(JSON, "stringify");
|
||||
const provider = new Il2cppMetadataProvider(8192);
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
expect(
|
||||
stringify.mock.calls.some(
|
||||
([value]) =>
|
||||
typeof value === "object" && value !== null && "methods" in value,
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps an independent failure with shared cancellation reason: %s",
|
||||
async (sharedReason) => {
|
||||
@@ -373,19 +335,42 @@ it("retains the exact empty source observation without claiming a decoded invent
|
||||
const root = await createTestTempDirectory("rea-il2cpp-empty-");
|
||||
const path = join(root, "empty.dat");
|
||||
await writeFile(path, Buffer.alloc(0));
|
||||
const result = await inspectMetadata(path);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unsupported empty metadata");
|
||||
expect(result.error.message).toContain("unrecognized-magic");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
operation: "observe_il2cpp_metadata_source",
|
||||
normalized_result: {
|
||||
artifact: {
|
||||
path,
|
||||
bytes: 0,
|
||||
sha256: createHash("sha256").digest("hex"),
|
||||
},
|
||||
inventory_status: "not-returned-as-complete",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the metadata source observation when the selected binary does not link", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-binary-failure-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
const binaryPath = join(root, "GameAssembly.dll");
|
||||
const fixture = createIl2cppMetadataFixture();
|
||||
await writeFile(path, fixture.bytes);
|
||||
await writeFile(binaryPath, Buffer.from("not a native image"));
|
||||
const provider = new Il2cppMetadataProvider();
|
||||
try {
|
||||
const result = await provider.inspect({ path });
|
||||
const result = await provider.inspect({ path, binary_path: binaryPath });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unsupported empty metadata");
|
||||
expect(result.error.message).toContain("unrecognized-magic");
|
||||
if (result.ok) throw new Error("Expected the binary to be refused");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
operation: "observe_il2cpp_metadata_source",
|
||||
normalized_result: {
|
||||
artifact: {
|
||||
path,
|
||||
bytes: 0,
|
||||
sha256: createHash("sha256").digest("hex"),
|
||||
sha256: createHash("sha256").update(fixture.bytes).digest("hex"),
|
||||
bytes: fixture.bytes.length,
|
||||
},
|
||||
inventory_status: "not-returned-as-complete",
|
||||
},
|
||||
@@ -394,3 +379,72 @@ it("retains the exact empty source observation without claiming a decoded invent
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts cancellation between binary linking batches and keeps the metadata source", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-link-cancel-");
|
||||
const path = join(root, "global-metadata.dat");
|
||||
const binaryPath = join(root, "libil2cpp.so");
|
||||
const fixture = createIl2cppMetadataFixture();
|
||||
await writeFile(path, fixture.bytes);
|
||||
await writeFile(binaryPath, createElfFixture(createIl2cppBinaryFixture()));
|
||||
// Yields taken by the metadata decode alone; later ones belong to linking.
|
||||
let decodeYields = 0;
|
||||
const counting = new Il2cppMetadataProvider(undefined, async () => {
|
||||
decodeYields++;
|
||||
});
|
||||
try {
|
||||
expect((await counting.inspect({ path })).ok).toBe(true);
|
||||
} finally {
|
||||
await counting.close();
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const reason = new Error("stop linking");
|
||||
let yields = 0;
|
||||
const provider = new Il2cppMetadataProvider(undefined, async () => {
|
||||
if (++yields === decodeYields + 2) controller.abort(reason);
|
||||
});
|
||||
try {
|
||||
const result = await provider.inspect(
|
||||
{ path, binary_path: binaryPath },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(yields).toBe(decodeYields + 2);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
operation: "observe_il2cpp_metadata_source",
|
||||
normalized_result: { artifact: { path } },
|
||||
});
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("retains both observed file identities and the selected architecture when native linking fails", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-failed-pair-");
|
||||
const path = join(root, "global-metadata.dat"),
|
||||
binary_path = join(root, "GameAssembly.dylib");
|
||||
const bytes = createIl2cppBinaryFixture({ codeRegistration: false });
|
||||
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
||||
await writeFile(binary_path, bytes);
|
||||
const provider = new Il2cppMetadataProvider();
|
||||
try {
|
||||
const result = await provider.inspect({ path, binary_path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Unresolved registration accepted");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
artifact: { path },
|
||||
selected_binary: {
|
||||
path: binary_path,
|
||||
identity_status: "observed",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user