diff --git a/src/domain/apple/machoImageSelection.ts b/src/domain/apple/machoImageSelection.ts new file mode 100644 index 0000000000..081c065159 --- /dev/null +++ b/src/domain/apple/machoImageSelection.ts @@ -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; +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 => { + 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 => { + 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 }); +}; diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts index 5c4c288391..050ac35a2e 100644 --- a/src/hopper/HopperMachOImage.ts +++ b/src/hopper/HopperMachOImage.ts @@ -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"); diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts index 20cf05a953..dc653060d2 100644 --- a/src/il2cpp/Il2cppBinaryImage.ts +++ b/src/il2cpp/Il2cppBinaryImage.ts @@ -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 { diff --git a/src/native/metadata/MachoSliceSelection.ts b/src/native/metadata/MachoSliceSelection.ts index cbc6e37fb5..c3c525b18c 100644 --- a/src/native/metadata/MachoSliceSelection.ts +++ b/src/native/metadata/MachoSliceSelection.ts @@ -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 => { - 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",