refactor(macho): consolidate universal image selection

This commit is contained in:
morluto committed 2026-10-27 13:04:22 +08:00
1 parent df8c90df8f
commit c7afedc3d2
4 files changed
+80 -70

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import { err, ok, type Result } from "../result.js";
import {
classifyFatContainerEvidence,
readFatContainerHeader,
readMachoHeaderPrefixFacts,
validateFatSlice,
type FatContainerHeader,
type FatSliceDeclaration,
} from "./machoContainer.js";
type UniversalMachoHeader = Extract<FatContainerHeader, { state: "parsed" }>;
interface MachoImageSelectionFailure {
readonly kind: "malformed";
readonly message: string;
}
/** Admit a complete in-memory universal image; FAT-shaped JVM data remains unselected. */
export const readUniversalMachoImage = (
bytes: Uint8Array,
): Result<UniversalMachoHeader | null, MachoImageSelectionFailure> => {
const fat = readFatContainerHeader(bytes, bytes.byteLength);
if (fat.state === "not-fat") return ok(null);
if (fat.state !== "parsed")
return err({
kind: "malformed",
message:
fat.state === "malformed"
? fat.reason
: "Universal Mach-O header is truncated",
});
const identity = classifyFatContainerEvidence(fat, bytes);
if (identity.state === "malformed")
return err({ kind: "malformed", message: identity.reason });
return ok(identity.state === "mach-o" ? fat : null);
};
/** Validate the selected declaration against the complete embedded CPU identity. */
export const readUniversalMachoSlice = (
bytes: Buffer,
header: UniversalMachoHeader,
slice: FatSliceDeclaration,
): Result<Buffer, MachoImageSelectionFailure> => {
const image = bytes.subarray(slice.offset, slice.offset + slice.size);
const identity = readMachoHeaderPrefixFacts(image).identity;
const message =
identity === null
? `FAT architecture ${slice.index} has a truncated Mach-O identity`
: validateFatSlice(slice, bytes.length, header.tableEnd, identity);
return message === null ? ok(image) : err({ kind: "malformed", message });
};
+2 -2
View File
@@ -25,6 +25,7 @@ import {
validateFatSliceIdentity,
type FatSliceDeclaration,
} from "../domain/apple/machoContainer.js";
import { MACHO_CPU_TYPES } from "../domain/apple/machoTargetSelection.js";
import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js";
import {
HopperCancelledError,
@@ -366,7 +367,6 @@ const inspectHopperMachOImage = async (
)
return invalid("Hopper cannot select the admitted Mach-O architecture");
const architecture = target.architecture;
const cpuTypes = { x86: 7, x86_64: 0x1000007, arm: 12, arm64: 0x100000c };
const matches = [];
for (const slice of parsed.slices) {
if (signal?.aborted)
@@ -393,7 +393,7 @@ const inspectHopperMachOImage = async (
);
const identityError = validateFatSliceIdentity(slice, identity);
if (identityError !== null) return invalid(identityError);
if (slice.cpuType === cpuTypes[architecture]) matches.push(slice);
if (slice.cpuType === MACHO_CPU_TYPES[architecture]) matches.push(slice);
}
if (findOverlappingFatSlices(parsed.slices) !== null)
return invalid("FAT slices overlap");
+18 -30
View File
@@ -1,9 +1,7 @@
import {
classifyFatContainerEvidence,
readFatContainerHeader,
readMachoHeaderPrefixFacts,
validateFatSlice,
} from "../domain/apple/machoContainer.js";
readUniversalMachoImage,
readUniversalMachoSlice,
} from "../domain/apple/machoImageSelection.js";
import { binaryMetadataArchitecture } from "../domain/binaryMetadataArchitecture.js";
import {
BinaryFormatFailure,
@@ -35,17 +33,14 @@ const selectImage = (
bytes: Buffer,
architecture: string | undefined,
): Buffer => {
const fat = readFatContainerHeader(bytes, bytes.length);
const identity =
fat.state === "parsed"
? classifyFatContainerEvidence(fat, bytes)
: undefined;
if (identity?.state === "malformed")
throw new Il2cppBinaryFormatFailure("malformed-container", identity.reason);
if (
fat.state === "not-fat" ||
(fat.state === "parsed" && identity?.state !== "mach-o")
) {
const container = readUniversalMachoImage(bytes);
if (!container.ok)
throw new Il2cppBinaryFormatFailure(
"malformed-container",
container.error.message,
);
const fat = container.value;
if (fat === null) {
if (architecture !== undefined)
throw new Il2cppBinaryFormatFailure(
"slice-not-found",
@@ -53,11 +48,6 @@ const selectImage = (
);
return bytes;
}
if (fat.state !== "parsed")
throw new Il2cppBinaryFormatFailure(
"malformed-container",
fat.state === "malformed" ? fat.reason : "universal header is truncated",
);
const available = fat.slices.map(
(slice) => MACHO_CPU.get(slice.cpuType) ?? `macho-cpu-${slice.cpuType}`,
);
@@ -78,15 +68,13 @@ const selectImage = (
"slice-not-found",
`universal Mach-O holds ${available.join(", ")}, not ${architecture}`,
);
const image = bytes.subarray(slice.offset, slice.offset + slice.size);
const embedded = readMachoHeaderPrefixFacts(image).identity;
const issue =
embedded === null
? `FAT architecture ${slice.index} has a truncated Mach-O identity`
: validateFatSlice(slice, bytes.length, fat.tableEnd, embedded);
if (issue !== null)
throw new Il2cppBinaryFormatFailure("malformed-container", issue);
return image;
const selected = readUniversalMachoSlice(bytes, fat, slice);
if (!selected.ok)
throw new Il2cppBinaryFormatFailure(
"malformed-container",
selected.error.message,
);
return selected.value;
};
export interface NativeImage {
+10 -38
View File
@@ -1,11 +1,9 @@
import { createHash } from "node:crypto";
import {
classifyFatContainerIdentity,
readFatContainerHeader,
readMachoHeaderPrefixFacts,
validateFatSlice,
} from "../../domain/apple/machoContainer.js";
readUniversalMachoImage,
readUniversalMachoSlice,
} from "../../domain/apple/machoImageSelection.js";
import {
MACHO_CPU_TYPES,
type MachoSliceArchitecture,
@@ -48,37 +46,16 @@ export const selectNativeImage = (
bytes: Buffer,
architecture: MachoSliceArchitecture | undefined,
): Result<SelectedNativeImage, MachoSliceSelectionIssue> => {
const fat = readFatContainerHeader(bytes, bytes.length);
const identity =
fat.state === "parsed"
? classifyFatContainerIdentity(fat, (offset) =>
offset + 4 > bytes.length ? null : bytes.readUInt32BE(offset),
)
: undefined;
// A table whose slices declare known Mach-O CPUs but cannot be read is a
// damaged universal file; no architecture selection can repair it.
if (identity?.state === "malformed")
return err({ kind: "malformed", message: identity.reason });
// A FAT-shaped table is not a universal Mach-O until its slices prove it;
// JVM class files share the magic. Leave such files to the image reader.
if (
fat.state === "not-fat" ||
(fat.state === "parsed" && identity?.state !== "mach-o")
)
const container = readUniversalMachoImage(bytes);
if (!container.ok) return container;
const fat = container.value;
if (fat === null)
return architecture === undefined
? ok({ bytes, fileOffset: 0, selection: null })
: err({
kind: "architecture",
message: `architecture ${architecture} was selected, but the file is not a universal Mach-O; omit architecture.`,
});
if (fat.state !== "parsed")
return err({
kind: "malformed",
message:
fat.state === "malformed"
? fat.reason
: "Universal Mach-O header is truncated",
});
const available = fat.slices
.map(({ cpuType, cpuSubtype }) => cpuName(cpuType, cpuSubtype))
.join(", ");
@@ -99,15 +76,10 @@ export const selectNativeImage = (
? `Universal Mach-O holds ${available}, not ${architecture}.`
: `Universal Mach-O holds several ${architecture} slices (${available}); no slice was selected.`,
});
const image = bytes.subarray(slice.offset, slice.offset + slice.size);
const embedded = readMachoHeaderPrefixFacts(image).identity;
const issue =
embedded === null
? `FAT architecture ${String(slice.index)} does not begin with a Mach-O header`
: validateFatSlice(slice, bytes.length, fat.tableEnd, embedded);
if (issue !== null) return err({ kind: "malformed", message: issue });
const selected = readUniversalMachoSlice(bytes, fat, slice);
if (!selected.ok) return selected;
return ok({
bytes: image,
bytes: selected.value,
fileOffset: slice.offset,
selection: {
kind: "macho-selected-image-v1",