Files
rea/tests/boundary/providers/native/objcClassMetadataFallback.test.ts
T
morluto 4d7de00d4c fix(native): coalesce Objective-C class imports by binding identity
Join provider class observations only when selected-image identity and exact import bindings establish equivalence. Preserve distinct observations and unsupported coverage.

Refs #5607
2026-10-21 06:12:21 +08:00

180 lines
6.0 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import type {
AnalysisOperation,
AnalysisOperationPort,
} from "../../../../src/application/AnalysisProvider.js";
import { createAnalysisExecution } from "../../../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../../../src/application/EnhancedTools.js";
import {
UnsupportedMachoMetadataFormatError,
parseMachoLayout,
} from "../../../../src/native/AppleMachoSelection.js";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import { NativeFixtureRunner } from "../../../fixtures/nativeCommands.js";
import { ok } from "../../../../src/domain/result.js";
const thin32MachOHeader = (cpuType: number): Buffer => {
const bytes = Buffer.alloc(84);
bytes.writeUInt32LE(0xfeedface, 0);
bytes.writeUInt32LE(cpuType, 4);
bytes.writeUInt32LE(cpuType === 7 ? 3 : 0, 8);
bytes.writeUInt32LE(2, 12);
bytes.writeUInt32LE(1, 16);
bytes.writeUInt32LE(56, 20);
bytes.writeUInt32LE(1, 28); // LC_SEGMENT
bytes.writeUInt32LE(56, 32);
bytes.write("__TEXT", 36);
bytes.writeUInt32LE(0x1000, 52); // vmaddr
bytes.writeUInt32LE(0x1000, 56); // vmsize
bytes.writeUInt32LE(0, 60); // fileoff
bytes.writeUInt32LE(bytes.length, 64); // filesize
bytes.writeUInt32LE(7, 68); // maxprot
bytes.writeUInt32LE(5, 72); // initprot
return bytes;
};
const inspect = async (
bytes: Buffer,
architecture: "x86" | "arm" | "x86_64",
) => {
const directory = await mkdtemp(join(tmpdir(), "rea-objc-macho-fallback-"));
const path = join(directory, "fixture");
await writeFile(path, bytes);
const sha256 = createHash("sha256").update(bytes).digest("hex");
const target = {
path,
sha256,
kind: "executable" as const,
format: "mach-o" as const,
architecture,
availableArchitectures: [architecture],
};
const nativeClient = new NativeMacOSProvider(
{},
new NativeFixtureRunner(),
"darwin",
).createClient(target);
try {
const hopper = { id: "hopper", name: "Hopper", version: "1" };
const analysis: AnalysisOperationPort = {
execute: async (operation: AnalysisOperation, parameters, options) => {
if (operation === "list_names")
return ok(
createAnalysisExecution(
[{ address: "0x1000", value: "OBJC_CLASS_$_Known" }],
hopper,
{ subject: target },
),
);
return nativeClient.execute(operation, parameters, options);
},
};
return await new EnhancedTools(analysis).execute("get_objc_classes", {});
} finally {
let closeResult: Awaited<ReturnType<typeof nativeClient.close>> | undefined;
let closeError: unknown;
try {
closeResult = await nativeClient.close();
} catch (cause: unknown) {
closeError = cause;
}
await rm(directory, { recursive: true, force: true });
if (closeError !== undefined) throw closeError;
expect(closeResult).toMatchObject({ ok: true });
}
};
describe("native Objective-C class metadata capability fallback", () => {
it.each([
{ architecture: "x86" as const, cpuType: 7 },
{ architecture: "arm" as const, cpuType: 12 },
])(
"keeps $architecture names when a valid 32-bit executable is unsupported",
async ({ architecture, cpuType }) => {
const result = await inspect(thin32MachOHeader(cpuType), architecture);
expect(result).toMatchObject({
ok: true,
value: {
count: 1,
classes: [{ address: "0x1000", name: "OBJC_CLASS_$_Known" }],
identity_coverage: { status: "unknown" },
},
});
},
);
it("classifies exactly the complete 28-byte 32-bit header as unsupported", () => {
const bytes = Buffer.alloc(28);
bytes.writeUInt32LE(0xfeedface, 0);
bytes.writeUInt32LE(7, 4);
bytes.writeUInt32LE(3, 8);
bytes.writeUInt32LE(2, 12);
let failure: unknown;
try {
parseMachoLayout(bytes, "x86");
} catch (cause: unknown) {
failure = cause;
}
expect(failure).toBeInstanceOf(UnsupportedMachoMetadataFormatError);
});
it("keeps truncated 32-bit and 64-bit headers as errors", async () => {
const truncated32 = Buffer.alloc(27);
truncated32.writeUInt32LE(0xfeedface, 0);
const truncated32Result = await inspect(truncated32, "x86");
expect(truncated32Result).toMatchObject({
ok: false,
error: { _tag: "ProviderAdapterError" },
});
const truncated64 = Buffer.alloc(31);
truncated64.writeUInt32LE(0xfeedfacf, 0);
truncated64.writeUInt32LE(0x01000007, 4);
const truncated64Result = await inspect(truncated64, "x86_64");
expect(truncated64Result).toMatchObject({
ok: false,
error: { _tag: "ProviderAdapterError" },
});
});
it("does not treat an unknown header as a supported-format capability limit", async () => {
const result = await inspect(Buffer.alloc(32), "x86_64");
expect(result).toMatchObject({
ok: false,
error: { _tag: "ProviderAdapterError" },
});
});
it("classifies a complete big-endian PowerPC64 Mach-O header as unsupported", () => {
const bytes = Buffer.alloc(32);
bytes.writeUInt32BE(0xfeedfacf, 0);
bytes.writeUInt32BE(0x01000012, 4); // CPU_TYPE_POWERPC64
bytes.writeUInt32BE(0, 8);
bytes.writeUInt32BE(2, 12);
let failure: unknown;
try {
parseMachoLayout(bytes, "powerpc");
} catch (cause: unknown) {
failure = cause;
}
expect(failure).toBeInstanceOf(UnsupportedMachoMetadataFormatError);
bytes.writeUInt32BE(0x01000007, 4);
let mismatchedCpuFailure: unknown;
try {
parseMachoLayout(bytes, "powerpc");
} catch (cause: unknown) {
mismatchedCpuFailure = cause;
}
expect(mismatchedCpuFailure).toBeInstanceOf(TypeError);
expect(mismatchedCpuFailure).not.toBeInstanceOf(
UnsupportedMachoMetadataFormatError,
);
});
});