feat(il2cpp): retain v108 header and string evidence (#9925)

* feat(il2cpp): retain v108 header and string evidence

* fix(il2cpp): preserve v108 BOMs and yield while scanning

* fix(il2cpp): preserve uncertainty in v108 layout refusals

---------

Co-authored-by: dhruvac29 <dhruvac29@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-11 08:42:55 -05:00
1 parent 4dedf3e865
commit be6ec3f164
4 files changed
+359

No files matched your search

@@ -9,9 +9,11 @@ import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { Il2cppMetadataProvider } from "../../../src/il2cpp/Il2cppMetadataProvider.js";
import { inspectIl2cppMetadataV108 } from "../../../src/il2cpp/Il2cppMetadataV108.js";
import { Il2cppGhidraSeedWriter } from "../../../src/il2cpp/Il2cppGhidraSeedWriter.js";
import { Il2cppMetadataService } from "../../../src/application/il2cpp/Il2cppMetadataService.js";
import { il2cppMetadataSchema } from "../../../src/domain/il2cpp/il2cppMetadata.js";
@@ -41,6 +43,149 @@ const inspectMetadata = async (path: string, limit?: number) => {
}
};
/** Synthetic v108 header: checks bounded observations, not record-layout compatibility. */
const version108Header = (stringBytes: Buffer) => {
const header = Buffer.alloc(500);
header.writeUInt32LE(0xfab11baf, 0);
header.writeInt32LE(108, 4);
let cursor = header.length;
for (let index = 0; index < 41; index++) {
const size = index === 2 ? stringBytes.length : 0;
header.writeInt32LE(cursor, 8 + index * 12);
header.writeInt32LE(size, 12 + index * 12);
header.writeInt32LE(size, 16 + index * 12);
cursor += size;
}
return Buffer.concat([header, stringBytes]);
};
it("retains a bounded v108 header and string observation without claiming decoded rows", async () => {
const root = await createTestTempDirectory("rea-il2cpp-v108-");
const path = join(root, "global-metadata.dat");
await writeFile(path, version108Header(Buffer.from("Alpha\0Beta\0")));
const result = await inspectMetadata(path);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.partialObservation).toMatchObject({
normalized_result: {
inventory_status: "not-returned-as-complete",
version_108_partial_inventory: {
metadata_version: 108,
header_bytes: 500,
coverage: "header-and-bounded-string-sample-only",
sections: expect.arrayContaining([
{ name: "stringLiterals", offset: 500, size: 0, count: 0 },
{ name: "stringLiteralData", offset: 500, size: 0, count: 0 },
{ name: "strings", offset: 500, size: 11, count: 11 },
]),
strings: {
terminated_count: 2,
unterminated_tail_bytes: 0,
sample: [
{ offset: 0, value: "Alpha", truncated: false },
{ offset: 6, value: "Beta", truncated: false },
],
},
},
},
});
expect(JSON.stringify(result.error.partialObservation)).not.toContain(
'"images":[]',
);
});
it("accepts padding after the assumed v108 header", async () => {
const root = await createTestTempDirectory("rea-il2cpp-v108-padding-");
const path = join(root, "global-metadata.dat");
const bytes = version108Header(Buffer.from("Name\0"));
const padded = Buffer.concat([
bytes.subarray(0, 500),
Buffer.alloc(16),
bytes.subarray(500),
]);
for (let index = 0; index < 41; index++) {
padded.writeInt32LE(bytes.readInt32LE(8 + index * 12) + 16, 8 + index * 12);
}
await writeFile(path, padded);
const result = await inspectMetadata(path);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisUnsupportedTargetError);
expect(result.error.partialObservation).toMatchObject({
normalized_result: {
version_108_partial_inventory: {
strings: { sample: [{ value: "Name" }] },
},
},
});
});
it("retains source evidence when a v108 section conflicts with the unverified layout", async () => {
const root = await createTestTempDirectory("rea-il2cpp-v108-invalid-");
const path = join(root, "global-metadata.dat");
const bytes = version108Header(Buffer.from("Name\0"));
bytes.writeInt32LE(bytes.length + 1, 12 + 2 * 12);
await writeFile(path, bytes);
const result = await inspectMetadata(path);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisUnsupportedTargetError);
expect(result.error.partialObservation).toMatchObject({
normalized_result: { artifact: { path, bytes: bytes.length } },
});
expect(JSON.stringify(result.error.partialObservation)).not.toContain(
"version_108_partial_inventory",
);
});
it("bounds v108 string samples and preserves undecodable bytes and leading BOMs", async () => {
const repeated = await inspectIl2cppMetadataV108(
version108Header(Buffer.from("x\0".repeat(20))),
);
expect(repeated?.strings.terminated_count).toBe(20);
expect(repeated?.strings.sample).toHaveLength(16);
const invalid = await inspectIl2cppMetadataV108(
version108Header(Buffer.from([0xff, 0])),
);
expect(invalid?.strings.sample).toEqual([
{ offset: 0, value: null, value_base64: "/w==", truncated: false },
]);
const bom = await inspectIl2cppMetadataV108(
version108Header(Buffer.from([0xef, 0xbb, 0xbf, 0x41, 0])),
);
expect(bom?.strings.sample).toEqual([
{ offset: 0, value: "\uFEFFA", value_base64: null, truncated: false },
]);
});
it("yields during a large v108 string scan and honors cancellation before evidence", async () => {
const root = await createTestTempDirectory("rea-il2cpp-v108-cancel-");
const path = join(root, "global-metadata.dat");
await writeFile(path, version108Header(Buffer.alloc(128 * 1024, 65)));
const controller = new AbortController();
let batches = 0;
const provider = new Il2cppMetadataProvider(undefined, async () => {
batches++;
controller.abort("cancel v108 scan");
await setImmediate();
});
try {
const result = await provider.inspect(
{ path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(batches).toBe(1);
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(JSON.stringify(result.error.partialObservation)).not.toContain(
"version_108_partial_inventory",
);
} finally {
await provider.close();
}
});
/** Version 29 file whose methods all share one long name. */
const sharedNameMetadata = (
nameBytes: number | Buffer,