refactor(macho): consolidate universal image selection
This commit is contained in:
1 parent
df8c90df8f
commit
c7afedc3d2
4 files changed
+80
-70
No files matched your search
@@ -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 });
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user