mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(macho): centralize FAT declaration facts
This commit is contained in:
1 parent
80b7bbc8bd
commit
cd10ee1f8a
7 files changed
+381
-61
No files matched your search
@@ -12,7 +12,9 @@ import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js";
|
||||
import { parseExecutableHeader } from "../../domain/binaryTarget.js";
|
||||
import {
|
||||
classifyFatContainerIdentity,
|
||||
findOverlappingFatSlices,
|
||||
readFatContainerHeader,
|
||||
readFatSliceDeclarations,
|
||||
readMachoHeaderPrefixFacts,
|
||||
} from "../../domain/apple/machoContainer.js";
|
||||
|
||||
@@ -29,6 +31,94 @@ const classify = (bytes: Uint8Array) => {
|
||||
});
|
||||
};
|
||||
|
||||
describe("FAT declaration facts", () => {
|
||||
it.each([
|
||||
{ wide: false, littleEndian: false },
|
||||
{ wide: false, littleEndian: true },
|
||||
{ wide: true, littleEndian: false },
|
||||
{ wide: true, littleEndian: true },
|
||||
])(
|
||||
"retains raw fields and reserved-field absence for wide=$wide littleEndian=$littleEndian",
|
||||
async ({ wide, littleEndian }) => {
|
||||
const arm64e = machoImage({ cpu: CPU.arm64e });
|
||||
const x86 = machoImage({ cpu: CPU.x86_64 });
|
||||
const bytes = fatImage(
|
||||
[
|
||||
{ cpu: CPU.arm64e, bytes: arm64e },
|
||||
{ cpu: CPU.x86_64, bytes: x86 },
|
||||
],
|
||||
wide,
|
||||
littleEndian,
|
||||
);
|
||||
if (wide)
|
||||
new DataView(
|
||||
bytes.buffer,
|
||||
bytes.byteOffset,
|
||||
bytes.byteLength,
|
||||
).setUint32(8 + 32 + 28, 0x89abcdef, littleEndian);
|
||||
const declarations = readFatSliceDeclarations(
|
||||
bytes.subarray(8, 8 + 2 * (wide ? 32 : 20)),
|
||||
{ count: 2, wide, littleEndian },
|
||||
);
|
||||
expect(declarations).toEqual({
|
||||
status: "parsed",
|
||||
slices: [
|
||||
{
|
||||
index: 0,
|
||||
offset: 4096,
|
||||
size: arm64e.byteLength,
|
||||
alignmentExponent: 12,
|
||||
cpuType: CPU.arm64e.type,
|
||||
cpuSubtype: CPU.arm64e.subtype,
|
||||
reserved: wide ? 0 : null,
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
offset: 8192,
|
||||
size: x86.byteLength,
|
||||
alignmentExponent: 12,
|
||||
cpuType: CPU.x86_64.type,
|
||||
cpuSubtype: CPU.x86_64.subtype,
|
||||
reserved: wide ? 0x89abcdef : null,
|
||||
},
|
||||
],
|
||||
});
|
||||
// Physical facts do not impose Hopper's reserved-field admission policy.
|
||||
expect(classify(bytes)).toEqual({ state: "mach-o" });
|
||||
expect(
|
||||
await readMachoImage(readerOf(bytes), bytes.byteLength),
|
||||
).toMatchObject({
|
||||
status: "parsed",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("finds overlap in physical order while retaining declarations in table order", () => {
|
||||
const bytes = fatImage([
|
||||
{ cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) },
|
||||
{ cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
|
||||
]);
|
||||
const header = readFatContainerHeader(bytes, bytes.byteLength);
|
||||
if (header.state !== "parsed") throw new Error("expected parsed FAT table");
|
||||
const first = { ...header.slices[0]!, offset: 128, size: 64 };
|
||||
const second = { ...header.slices[1]!, offset: 96, size: 64 };
|
||||
const tableOrder = [first, second];
|
||||
const overlap = findOverlappingFatSlices(tableOrder);
|
||||
expect(overlap).toEqual([second, first]);
|
||||
expect(overlap?.[0]).toBe(second);
|
||||
expect(overlap?.[1]).toBe(first);
|
||||
expect(tableOrder).toEqual([first, second]);
|
||||
expect(
|
||||
findOverlappingFatSlices([first, { ...second, size: 32 }]),
|
||||
).toBeNull();
|
||||
expect(
|
||||
findOverlappingFatSlices([first, { ...second, size: 16 }]),
|
||||
).toBeNull();
|
||||
expect(findOverlappingFatSlices([first])).toBeNull();
|
||||
expect(findOverlappingFatSlices([])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FAT Mach-O identity", () => {
|
||||
it("does not treat known CPU declarations as proof when slices are ar archives", async () => {
|
||||
const archive = arArchive();
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface FatSliceDeclaration {
|
||||
readonly alignmentExponent: number;
|
||||
readonly cpuType: number;
|
||||
readonly cpuSubtype: number;
|
||||
/** FAT64's raw reserved field; FAT32 has no corresponding field. */
|
||||
readonly reserved: number | null;
|
||||
}
|
||||
|
||||
export interface FatSliceRangeIssue {
|
||||
@@ -273,11 +275,30 @@ export const readFatSliceDeclarations = (
|
||||
alignmentExponent: view.getUint32(base + (wide ? 24 : 16), littleEndian),
|
||||
cpuType: view.getUint32(base, littleEndian),
|
||||
cpuSubtype: view.getUint32(base + 4, littleEndian),
|
||||
reserved: wide ? view.getUint32(base + 28, littleEndian) : null,
|
||||
});
|
||||
}
|
||||
return { status: "parsed", slices };
|
||||
};
|
||||
|
||||
/** Find an overlapping pair without changing the declarations' table order. */
|
||||
export const findOverlappingFatSlices = (
|
||||
declarations: readonly FatSliceDeclaration[],
|
||||
): readonly [FatSliceDeclaration, FatSliceDeclaration] | null => {
|
||||
const ranges = [...declarations].sort((a, b) => a.offset - b.offset);
|
||||
for (let index = 1; index < ranges.length; index++) {
|
||||
const previous = ranges[index - 1];
|
||||
const current = ranges[index];
|
||||
if (
|
||||
previous !== undefined &&
|
||||
current !== undefined &&
|
||||
current.offset < previous.offset + previous.size
|
||||
)
|
||||
return [previous, current];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Validate byte-range and table/header identity facts shared by Mach-O readers. */
|
||||
export const validateFatSlice = (
|
||||
declaration: FatSliceDeclaration,
|
||||
|
||||
@@ -25,10 +25,12 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { FileOffsetPartialObservation } from "../domain/native/fileOffsetPartialObservation.js";
|
||||
import {
|
||||
findOverlappingFatSlices,
|
||||
readFatSliceDeclarations,
|
||||
readMachoHeaderPrefixFacts,
|
||||
type FatSliceDeclaration,
|
||||
validateFatSlice,
|
||||
validateFatSliceIdentity,
|
||||
} from "../domain/apple/machoContainer.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
@@ -351,17 +353,11 @@ function fatLayout(prefix: Buffer): Result<FatLayout | null, string> {
|
||||
const table = prefix.subarray(8, tableEnd);
|
||||
const parsed = readFatSliceDeclarations(table, { count, wide, littleEndian });
|
||||
if (parsed.status !== "parsed") return err(parsed.reason);
|
||||
if (wide) {
|
||||
for (const slice of parsed.slices)
|
||||
if (
|
||||
(littleEndian
|
||||
? table.readUInt32LE(slice.index * 32 + 28)
|
||||
: table.readUInt32BE(slice.index * 32 + 28)) !== 0
|
||||
)
|
||||
return err(
|
||||
`FAT64 architecture ${slice.index} has a nonzero reserved field`,
|
||||
);
|
||||
}
|
||||
for (const slice of parsed.slices)
|
||||
if (slice.reserved !== null && slice.reserved !== 0)
|
||||
return err(
|
||||
`FAT64 architecture ${slice.index} has a nonzero reserved field`,
|
||||
);
|
||||
return ok({ slices: parsed.slices, tableEnd });
|
||||
}
|
||||
|
||||
@@ -436,17 +432,8 @@ function originalOffset(
|
||||
"Hopper did not expose the loaded Mach-O header needed to identify its FAT slice",
|
||||
);
|
||||
const matches = [];
|
||||
const ranges = [...layout.value.slices].sort((a, b) => a.offset - b.offset);
|
||||
for (let index = 1; index < ranges.length; index++) {
|
||||
const previous = ranges[index - 1];
|
||||
const current = ranges[index];
|
||||
if (
|
||||
previous !== undefined &&
|
||||
current !== undefined &&
|
||||
current.offset < previous.offset + previous.size
|
||||
)
|
||||
return err("Original FAT slices overlap");
|
||||
}
|
||||
if (findOverlappingFatSlices(layout.value.slices) !== null)
|
||||
return err("Original FAT slices overlap");
|
||||
for (const slice of layout.value.slices) {
|
||||
const reason = validateFatSlice(slice, size, layout.value.tableEnd);
|
||||
if (reason !== null) return err(reason);
|
||||
@@ -460,12 +447,7 @@ function originalOffset(
|
||||
return err(
|
||||
`FAT architecture ${slice.index} does not contain a Mach-O image`,
|
||||
);
|
||||
const identityError = validateFatSlice(
|
||||
slice,
|
||||
size,
|
||||
layout.value.tableEnd,
|
||||
embeddedIdentity,
|
||||
);
|
||||
const identityError = validateFatSliceIdentity(slice, embeddedIdentity);
|
||||
if (identityError !== null) return err(identityError);
|
||||
if (
|
||||
slice.cpuType === loadedIdentity.cpuType &&
|
||||
|
||||
@@ -14,9 +14,11 @@ import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import {
|
||||
findOverlappingFatSlices,
|
||||
readFatSliceDeclarations,
|
||||
readMachoHeaderPrefixFacts,
|
||||
validateFatSlice,
|
||||
validateFatSliceIdentity,
|
||||
type FatSliceDeclaration,
|
||||
} from "../domain/apple/machoContainer.js";
|
||||
import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js";
|
||||
@@ -366,7 +368,6 @@ const inspectHopperMachOImage = async (
|
||||
const architecture = target.architecture;
|
||||
const cpuTypes = { x86: 7, x86_64: 0x1000007, arm: 12, arm64: 0x100000c };
|
||||
const matches = [];
|
||||
const ranges = [];
|
||||
for (const slice of parsed.slices) {
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
@@ -374,15 +375,10 @@ const inspectHopperMachOImage = async (
|
||||
);
|
||||
const rangeError = validateFatSlice(slice, size, tableEnd);
|
||||
if (rangeError !== null) return invalid(rangeError);
|
||||
if (wide) {
|
||||
const reserved = littleEndian
|
||||
? table.readUInt32LE(slice.index * 32 + 28)
|
||||
: table.readUInt32BE(slice.index * 32 + 28);
|
||||
if (reserved !== 0)
|
||||
return invalid(
|
||||
`FAT64 architecture ${slice.index} has a nonzero reserved field`,
|
||||
);
|
||||
}
|
||||
if (slice.reserved !== null && slice.reserved !== 0)
|
||||
return invalid(
|
||||
`FAT64 architecture ${slice.index} has a nonzero reserved field`,
|
||||
);
|
||||
if (slice.size < 12)
|
||||
return invalid(
|
||||
`FAT architecture ${slice.index} has a truncated Mach-O header`,
|
||||
@@ -395,22 +391,12 @@ const inspectHopperMachOImage = async (
|
||||
return invalid(
|
||||
`FAT architecture ${slice.index} does not contain a Mach-O image`,
|
||||
);
|
||||
const identityError = validateFatSlice(slice, size, tableEnd, identity);
|
||||
const identityError = validateFatSliceIdentity(slice, identity);
|
||||
if (identityError !== null) return invalid(identityError);
|
||||
ranges.push(slice);
|
||||
if (slice.cpuType === cpuTypes[architecture]) matches.push(slice);
|
||||
}
|
||||
ranges.sort((a, b) => a.offset - b.offset);
|
||||
for (let index = 1; index < ranges.length; index++) {
|
||||
const previous = ranges[index - 1];
|
||||
const current = ranges[index];
|
||||
if (
|
||||
previous !== undefined &&
|
||||
current !== undefined &&
|
||||
current.offset < previous.offset + previous.size
|
||||
)
|
||||
return invalid("FAT slices overlap");
|
||||
}
|
||||
if (findOverlappingFatSlices(parsed.slices) !== null)
|
||||
return invalid("FAT slices overlap");
|
||||
const selected = selectHopperSlice(matches, architecture);
|
||||
if (selected === undefined)
|
||||
return invalid(
|
||||
|
||||
@@ -70,6 +70,25 @@ const registerThinCpuFamilyControls = (): void => {
|
||||
});
|
||||
};
|
||||
|
||||
it.each([false, true])(
|
||||
"retains the shared FAT declaration without imposing Hopper policy (wide=%s)",
|
||||
(wide) => {
|
||||
const thin = machoImage({ cpu: CPU.arm64e });
|
||||
const fat = Buffer.from(fatImage([{ cpu: CPU.arm64e, bytes: thin }], wide));
|
||||
if (wide) fat.writeUInt32BE(0x89abcdef, 8 + 28);
|
||||
expect(selectMachoSlice(fat, "arm64").fatDeclaration).toEqual({
|
||||
index: 0,
|
||||
offset: 4096,
|
||||
size: thin.byteLength,
|
||||
cpuType: CPU.arm64e.type,
|
||||
cpuSubtype: CPU.arm64e.subtype,
|
||||
alignmentExponent: 12,
|
||||
reserved: wide ? 0x89abcdef : null,
|
||||
tableEnd: 8 + (wide ? 32 : 20),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("native Mach-O slice selection", () => {
|
||||
it("rejects misaligned load commands", () => {
|
||||
expect(() =>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js";
|
||||
import {
|
||||
readFatSliceDeclarations,
|
||||
validateFatSlice,
|
||||
validateFatSliceIdentity,
|
||||
type FatSliceDeclaration,
|
||||
} from "../domain/apple/machoContainer.js";
|
||||
|
||||
@@ -34,15 +35,7 @@ export interface MachoSlice {
|
||||
slice: number;
|
||||
sliceEnd: number;
|
||||
/** FAT table identity, absent for a thin image. */
|
||||
fatDeclaration?: {
|
||||
offset: number;
|
||||
size: number;
|
||||
cpuType: number;
|
||||
cpuSubtype: number;
|
||||
alignmentExponent: number;
|
||||
index: number;
|
||||
tableEnd: number;
|
||||
};
|
||||
fatDeclaration?: FatSliceDeclaration & { readonly tableEnd: number };
|
||||
}
|
||||
|
||||
/** Parsed little-endian 64-bit Mach-O layout for the selected architecture slice. */
|
||||
@@ -193,7 +186,7 @@ export const parseMachoLayout = (
|
||||
const cpuSubtype = readHeaderUInt32(slice + 8);
|
||||
const fat = selection.fatDeclaration;
|
||||
if (fat !== undefined) {
|
||||
const issue = validateFatSlice(fat, bytes.length, fat.tableEnd, {
|
||||
const issue = validateFatSliceIdentity(fat, {
|
||||
cpuType,
|
||||
cpuSubtype,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import {
|
||||
CPU,
|
||||
fatImage,
|
||||
machoImage,
|
||||
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
|
||||
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { mapHopperFileOffset } from "../../../src/hopper/HopperFileOffset.js";
|
||||
import { resolveHopperMachOImage } from "../../../src/hopper/HopperMachOImage.js";
|
||||
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const sourceTarget = async (bytes: Uint8Array, sliceSize: number) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-hopper-fat-coordinates-",
|
||||
);
|
||||
const path = join(directory, "source.fat");
|
||||
await writeFile(path, bytes);
|
||||
return {
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "mach-o-universal",
|
||||
architecture: "arm64",
|
||||
availableArchitectures: ["arm64"],
|
||||
universalSlice: { offset: 4096, size: sliceSize },
|
||||
} satisfies BinaryTarget;
|
||||
};
|
||||
|
||||
const resolveImage = (
|
||||
target: BinaryTarget & { readonly kind: "executable" },
|
||||
) => {
|
||||
const resources = createInspectionResources();
|
||||
onTestFinished(() => resources.close());
|
||||
return resolveHopperMachOImage(target, { resourceScope: resources.owner });
|
||||
};
|
||||
|
||||
const loadedObservation = (header: Uint8Array) => ({
|
||||
address: "0x1000",
|
||||
provider_file_offset: 8,
|
||||
provider_source_path: null,
|
||||
provider_image_header_hex: Buffer.from(header)
|
||||
.subarray(0, 12)
|
||||
.toString("hex"),
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ wide: false, littleEndian: false },
|
||||
{ wide: false, littleEndian: true },
|
||||
{ wide: true, littleEndian: false },
|
||||
{ wide: true, littleEndian: true },
|
||||
])(
|
||||
"keeps launch selection separate from loaded-image coordinates (wide=$wide littleEndian=$littleEndian)",
|
||||
async ({ wide, littleEndian }) => {
|
||||
const plain = machoImage({ cpu: CPU.arm64 });
|
||||
const arm64e = machoImage({ cpu: CPU.arm64e });
|
||||
const bytes = fatImage(
|
||||
[
|
||||
{ cpu: CPU.arm64, bytes: plain },
|
||||
{ cpu: CPU.arm64e, bytes: arm64e },
|
||||
],
|
||||
wide,
|
||||
littleEndian,
|
||||
);
|
||||
const target = await sourceTarget(bytes, plain.byteLength);
|
||||
const selected = await resolveImage(target);
|
||||
expect(selected).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
method: wide ? "fat64_thin_slice" : "fat32_thin_slice",
|
||||
architecture_count: 2,
|
||||
container_table_sha256: createHash("sha256")
|
||||
.update(bytes.subarray(0, 8 + 2 * (wide ? 32 : 20)))
|
||||
.digest("hex"),
|
||||
source_offset: 4096,
|
||||
byte_length: plain.byteLength,
|
||||
cpu_type: CPU.arm64.type,
|
||||
cpu_subtype: CPU.arm64.subtype,
|
||||
},
|
||||
});
|
||||
const mapped = await mapHopperFileOffset(target, loadedObservation(arm64e));
|
||||
expect(mapped).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
address: "0x1000",
|
||||
file_offset: 8192 + 8,
|
||||
provider_file_offset: 8,
|
||||
image_base_file_offset: 8192,
|
||||
source_path: target.path,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"refuses a nonzero FAT64 reserved field in both consumers (littleEndian=%s)",
|
||||
async (littleEndian) => {
|
||||
const thin = machoImage({ cpu: CPU.arm64 });
|
||||
const bytes = fatImage(
|
||||
[{ cpu: CPU.arm64, bytes: thin }],
|
||||
true,
|
||||
littleEndian,
|
||||
);
|
||||
new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32(
|
||||
8 + 28,
|
||||
0x89abcdef,
|
||||
littleEndian,
|
||||
);
|
||||
const target = await sourceTarget(bytes, thin.byteLength);
|
||||
const reason = "FAT64 architecture 0 has a nonzero reserved field";
|
||||
expect(await resolveImage(target)).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "BinaryTargetError", reason },
|
||||
});
|
||||
expect(
|
||||
await mapHopperFileOffset(target, loadedObservation(thin)),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason,
|
||||
partialObservation: {
|
||||
...loadedObservation(thin),
|
||||
original_file_offset: { available: false, reason },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ wide: false, littleEndian: false },
|
||||
{ wide: false, littleEndian: true },
|
||||
{ wide: true, littleEndian: false },
|
||||
{ wide: true, littleEndian: true },
|
||||
])(
|
||||
"refuses overlapping valid slice headers in both consumers (wide=$wide littleEndian=$littleEndian)",
|
||||
async ({ wide, littleEndian }) => {
|
||||
const plain = machoImage({ cpu: CPU.arm64 });
|
||||
const arm64e = machoImage({ cpu: CPU.arm64e });
|
||||
const bytes = fatImage(
|
||||
[
|
||||
{ cpu: CPU.arm64, bytes: plain },
|
||||
{ cpu: CPU.arm64e, bytes: arm64e },
|
||||
],
|
||||
wide,
|
||||
littleEndian,
|
||||
);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
// Both headers and ranges remain valid; the first range contains the second.
|
||||
if (wide)
|
||||
view.setBigUint64(
|
||||
8 + 16,
|
||||
4096n + BigInt(arm64e.byteLength),
|
||||
littleEndian,
|
||||
);
|
||||
else view.setUint32(8 + 12, 4096 + arm64e.byteLength, littleEndian);
|
||||
const target = await sourceTarget(bytes, 4096 + arm64e.byteLength);
|
||||
expect(await resolveImage(target)).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "BinaryTargetError", reason: "FAT slices overlap" },
|
||||
});
|
||||
expect(
|
||||
await mapHopperFileOffset(target, loadedObservation(arm64e)),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: "Original FAT slices overlap",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("requires a unique exact raw CPU identity for loaded-image attribution", async () => {
|
||||
const arm64e = machoImage({ cpu: CPU.arm64e });
|
||||
const bytes = fatImage([
|
||||
{ cpu: CPU.arm64e, bytes: arm64e },
|
||||
{ cpu: CPU.arm64e, bytes: arm64e },
|
||||
]);
|
||||
const target = await sourceTarget(bytes, arm64e.byteLength);
|
||||
expect(
|
||||
await mapHopperFileOffset(target, loadedObservation(arm64e)),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: "Original FAT slice matching Hopper's loaded header is ambiguous",
|
||||
},
|
||||
});
|
||||
const withoutCapabilities = Buffer.from(arm64e);
|
||||
withoutCapabilities.writeUInt32LE(2, 8);
|
||||
expect(
|
||||
await mapHopperFileOffset(target, loadedObservation(withoutCapabilities)),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: "Original FAT slice matching Hopper's loaded header is missing",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("checks all 12 loaded-header bytes after matching its CPU identity", async () => {
|
||||
const thin = machoImage({ cpu: CPU.arm64 });
|
||||
const target = await sourceTarget(
|
||||
fatImage([{ cpu: CPU.arm64, bytes: thin }]),
|
||||
thin.byteLength,
|
||||
);
|
||||
const loadedHeader = Buffer.alloc(12);
|
||||
// A big-endian header claims the same CPU identity but is not the source header.
|
||||
loadedHeader.writeUInt32BE(0xfeedfacf, 0);
|
||||
loadedHeader.writeUInt32BE(CPU.arm64.type, 4);
|
||||
loadedHeader.writeUInt32BE(CPU.arm64.subtype, 8);
|
||||
expect(
|
||||
await mapHopperFileOffset(target, loadedObservation(loadedHeader)),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: "Original FAT slice header disagrees with Hopper's loaded header",
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user