refactor(macho): share thin header CPU interpretation

Give partial thin-header facts one domain owner and reuse them for standalone target admission and Mach-O load-command inspection. Preserve each consumer endianness policy and header/command checks; remove the FAT-only parser names.

Refs #5542. All 246 focused checks and six public CLI archive/image controls passed; typecheck and lint passed.
This commit is contained in:
morluto committed 2026-10-20 22:37:25 +08:00
1 parent 57e76599c0
commit 44521d3d75
8 files changed
+55 -39

No files matched your search

+5 -5
View File
@@ -55,9 +55,9 @@ import {
} from "../domain/binaryTarget.js";
import {
readFatContainerHeader,
readFatSliceHeaderFacts,
readMachoHeaderPrefixFacts,
type FatSliceHeaderEvidence,
type FatSliceHeaderFacts,
type MachoHeaderPrefixFacts,
} from "../domain/apple/machoContainer.js";
import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -608,14 +608,14 @@ const readFatSliceHeaderEvidence = async (
}
if (header.state !== "parsed") return undefined;
const sliceHeadersByOffset = new Map<number, FatSliceHeaderFacts>();
const sliceHeadersByOffset = new Map<number, MachoHeaderPrefixFacts>();
for (let index = 0; index < header.slices.length; index += 1) {
const slice = header.slices[index];
if (slice === undefined) continue;
if (header.rangeIssues[index] !== null) {
sliceHeadersByOffset.set(
slice.offset,
readFatSliceHeaderFacts(Buffer.alloc(0)),
readMachoHeaderPrefixFacts(Buffer.alloc(0)),
);
continue;
}
@@ -636,7 +636,7 @@ const readFatSliceHeaderEvidence = async (
}
sliceHeadersByOffset.set(
slice.offset,
readFatSliceHeaderFacts(thinHeader.subarray(0, bytesRead)),
readMachoHeaderPrefixFacts(thinHeader.subarray(0, bytesRead)),
);
}
return { header, sliceHeadersByOffset };
+9 -9
View File
@@ -9,9 +9,9 @@ import {
import {
readFatContainerHeader,
readFatContainerTable,
readFatSliceHeaderFacts,
readMachoHeaderPrefixFacts,
type FatContainerTable,
type FatSliceHeaderFacts,
type MachoHeaderPrefixFacts,
type FatSliceHeaderEvidence,
} from "../domain/apple/machoContainer.js";
@@ -27,7 +27,7 @@ export interface ArtifactClassificationEvidence {
const observeFatSliceHeaders = (input: {
readonly table: FatContainerTable | undefined;
readonly observedHeaders: Map<number, FatSliceHeaderFacts>;
readonly observedHeaders: Map<number, MachoHeaderPrefixFacts>;
readonly partialHeaders: Map<
number,
{ readonly bytes: Buffer; count: number }
@@ -58,7 +58,7 @@ const observeFatSliceHeaders = (input: {
pending.count += end - start;
observedHeaders.set(
slice.offset,
readFatSliceHeaderFacts(
readMachoHeaderPrefixFacts(
pending.bytes.subarray(0, Math.min(pending.count, headerLength)),
),
);
@@ -72,7 +72,7 @@ const finishClassificationEvidence = (
prefix: Buffer,
archiveBytes: number,
finishAsar: (archiveBytes: number) => FinalAsarHeaderProbeObservation,
sliceHeadersByOffset: Map<number, FatSliceHeaderFacts>,
sliceHeadersByOffset: Map<number, MachoHeaderPrefixFacts>,
): ArtifactClassificationEvidence => {
const finalHeader = readFatContainerHeader(prefix, archiveBytes);
if (finalHeader.state === "parsed") {
@@ -87,7 +87,7 @@ const finishClassificationEvidence = (
if (available === 0) continue;
sliceHeadersByOffset.set(
slice.offset,
readFatSliceHeaderFacts(
readMachoHeaderPrefixFacts(
prefix.subarray(slice.offset, slice.offset + available),
),
);
@@ -112,8 +112,8 @@ export const createArtifactClassificationProbe = () => {
let retainedBytes = 0;
let observedBytes = 0;
let fatTable: FatContainerTable | undefined;
const fatSliceHeadersByOffset = new Map<number, FatSliceHeaderFacts>();
const partialFatMagics = new Map<
const fatSliceHeadersByOffset = new Map<number, MachoHeaderPrefixFacts>();
const partialFatHeaders = new Map<
number,
{ readonly bytes: Buffer; count: number }
>();
@@ -186,7 +186,7 @@ export const createArtifactClassificationProbe = () => {
observeFatSliceHeaders({
table: fatTable,
observedHeaders: fatSliceHeadersByOffset,
partialHeaders: partialFatMagics,
partialHeaders: partialFatHeaders,
streamOffset,
bytes,
});
+14 -2
View File
@@ -8,6 +8,7 @@ import {
machoLoadCommandAlignment,
classifyFatContainerIdentity,
readFatContainerHeader,
readMachoHeaderPrefixFacts,
validateFatSliceIdentity,
type FatSliceDeclaration,
} from "../../domain/apple/machoContainer.js";
@@ -259,8 +260,19 @@ const readSlice = async (
} | null = null,
): Promise<MachoSlice> => {
const header = viewOf(await readExact(readAt, base, 28, "Mach-O header"));
const headerBytes = new Uint8Array(
header.buffer,
header.byteOffset,
header.byteLength,
);
const identity = readMachoHeaderPrefixFacts(headerBytes).identity;
const magic = header.getUint32(0, true);
if (magic === MH_MAGIC_64 || magic === MH_MAGIC) {
if (identity === null)
throw new MachoFormatIssue(
"malformed",
`slice at offset ${base} has no Mach-O header`,
);
const wide = magic === MH_MAGIC_64;
const headerSize = wide ? 32 : 28;
const commandCount = header.getUint32(16, true);
@@ -283,8 +295,8 @@ const readSlice = async (
);
const slice = decodeCommands(commands, {
wide,
cpuType: header.getUint32(4, true),
cpuSubtype: header.getUint32(8, true),
cpuType: identity.cpuType,
cpuSubtype: identity.cpuSubtype,
fileType: header.getUint32(12, true),
commandCount,
sliceOffset: base,
+5 -5
View File
@@ -13,7 +13,7 @@ import { parseExecutableHeader } from "../../domain/binaryTarget.js";
import {
classifyFatContainerIdentity,
readFatContainerHeader,
readFatSliceHeaderFacts,
readMachoHeaderPrefixFacts,
} from "../../domain/apple/machoContainer.js";
const classify = (bytes: Uint8Array) => {
@@ -105,25 +105,25 @@ describe("FAT Mach-O identity", () => {
bigEndian.writeUInt32BE(0xfeedfacf, 0);
bigEndian.writeUInt32BE(CPU.x86_64.type, 4);
bigEndian.writeUInt32BE(CPU.x86_64.subtype, 8);
expect(readFatSliceHeaderFacts(littleEndian.subarray(0, 12))).toEqual({
expect(readMachoHeaderPrefixFacts(littleEndian.subarray(0, 12))).toEqual({
magic: 0xcffaedfe,
identity: {
cpuType: CPU.arm64.type,
cpuSubtype: CPU.arm64.subtype,
},
});
expect(readFatSliceHeaderFacts(bigEndian)).toEqual({
expect(readMachoHeaderPrefixFacts(bigEndian)).toEqual({
magic: 0xfeedfacf,
identity: {
cpuType: CPU.x86_64.type,
cpuSubtype: CPU.x86_64.subtype,
},
});
expect(readFatSliceHeaderFacts(littleEndian.subarray(0, 11))).toEqual({
expect(readMachoHeaderPrefixFacts(littleEndian.subarray(0, 11))).toEqual({
magic: 0xcffaedfe,
identity: null,
});
expect(readFatSliceHeaderFacts(littleEndian.subarray(0, 3))).toEqual({
expect(readMachoHeaderPrefixFacts(littleEndian.subarray(0, 3))).toEqual({
magic: null,
identity: null,
});
@@ -33,7 +33,7 @@ import {
import {
classifyFatContainerIdentity,
readFatContainerHeader,
readFatSliceHeaderFacts,
readMachoHeaderPrefixFacts,
} from "../../domain/apple/machoContainer.js";
/** Mutable internal occurrence used until root-bound IDs are known. */
@@ -617,8 +617,9 @@ const classifyFatArtifact = (
0,
Math.min(slice.size, prefix.length - offset, 12),
);
return readFatSliceHeaderFacts(prefix.subarray(offset, offset + available))
.magic;
return readMachoHeaderPrefixFacts(
prefix.subarray(offset, offset + available),
).magic;
});
return identity.state === "mach-o" ? "mach-o-universal" : "unknown";
};
+2 -2
View File
@@ -24,7 +24,7 @@ import {
} from "./hashStableRootArtifact.js";
import type { ArtifactInventoryOptions } from "./types.js";
import type { FatSliceHeaderEvidence } from "../../domain/apple/machoContainer.js";
import { readFatSliceHeaderFacts } from "../../domain/apple/machoContainer.js";
import { readMachoHeaderPrefixFacts } from "../../domain/apple/machoContainer.js";
interface RootClassification {
readonly format: ArtifactOccurrence["artifact_format"];
@@ -225,7 +225,7 @@ const readRootFatSliceHeaders = async (
}
sliceHeadersByOffset.set(
slice.offset,
readFatSliceHeaderFacts(thinHeader.subarray(0, bytesRead)),
readMachoHeaderPrefixFacts(thinHeader.subarray(0, bytesRead)),
);
}
return { header, sliceHeadersByOffset };
+5 -5
View File
@@ -37,11 +37,11 @@ export type FatContainerHeader =
/** Exact slice-header observations collected by a complete, bounded reader. */
export interface FatSliceHeaderEvidence {
readonly header: Extract<FatContainerHeader, { readonly state: "parsed" }>;
readonly sliceHeadersByOffset: ReadonlyMap<number, FatSliceHeaderFacts>;
readonly sliceHeadersByOffset: ReadonlyMap<number, MachoHeaderPrefixFacts>;
}
/** Parsed observations from the bounded prefix of one declared slice header. */
export interface FatSliceHeaderFacts {
/** Parsed observations from the bounded prefix of a thin Mach-O header. */
export interface MachoHeaderPrefixFacts {
/** Raw format evidence; null means fewer than four bytes were observed. */
readonly magic: number | null;
/** Present only for a recognized Mach-O magic and a complete 12-byte prefix. */
@@ -52,9 +52,9 @@ export interface FatSliceHeaderFacts {
}
/** Parse only facts present in the observed prefix of a thin Mach-O header. */
export const readFatSliceHeaderFacts = (
export const readMachoHeaderPrefixFacts = (
bytes: Uint8Array,
): FatSliceHeaderFacts => {
): MachoHeaderPrefixFacts => {
if (bytes.byteLength < 4) return { magic: null, identity: null };
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const magic = view.getUint32(0, false);
+11 -8
View File
@@ -19,7 +19,7 @@ import {
import {
classifyFatContainerIdentity,
readFatContainerHeader,
readFatSliceHeaderFacts,
readMachoHeaderPrefixFacts,
validateFatSliceIdentity,
type FatSliceHeaderEvidence,
} from "./apple/machoContainer.js";
@@ -115,10 +115,12 @@ const parseThinMachO = (
return err(
`truncated Mach-O header: a ${is64 ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`,
);
const identity = readMachoHeaderPrefixFacts(bytes.subarray(0, 12)).identity;
if (identity === null) return err("unsupported Mach-O architecture");
const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
const read = (offset: number): number =>
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
const architecture = machArchitecture(read(4));
const architecture = machArchitecture(identity.cpuType);
if (architecture === undefined) return err("unsupported Mach-O architecture");
const commandBytes = read(20);
if (headerSize + commandBytes > fileSize)
@@ -195,21 +197,21 @@ const fatSliceIdentityError = (
>,
sliceHeadersByOffset?: ReadonlyMap<
number,
ReturnType<typeof readFatSliceHeaderFacts>
ReturnType<typeof readMachoHeaderPrefixFacts>
>,
): string | null => {
for (let index = 0; index < header.slices.length; index += 1) {
const declaration = header.slices[index];
if (declaration === undefined || header.rangeIssues[index] !== null)
continue;
let facts: ReturnType<typeof readFatSliceHeaderFacts> | undefined;
let facts: ReturnType<typeof readMachoHeaderPrefixFacts> | undefined;
if (sliceHeadersByOffset?.has(declaration.offset)) {
facts = sliceHeadersByOffset.get(declaration.offset);
} else if (
declaration.size >= 12 &&
declaration.offset <= bytes.length - 12
) {
facts = readFatSliceHeaderFacts(
facts = readMachoHeaderPrefixFacts(
bytes.subarray(declaration.offset, declaration.offset + 12),
);
}
@@ -247,7 +249,7 @@ const fatIdentityError = (
>,
sliceHeadersByOffset?: ReadonlyMap<
number,
ReturnType<typeof readFatSliceHeaderFacts>
ReturnType<typeof readMachoHeaderPrefixFacts>
>,
): string | null => {
const identity = classifyFatContainerIdentity(header, (offset) => {
@@ -261,8 +263,9 @@ const fatIdentityError = (
0,
Math.min(slice.size, bytes.length - offset, 12),
);
return readFatSliceHeaderFacts(bytes.subarray(offset, offset + available))
.magic;
return readMachoHeaderPrefixFacts(
bytes.subarray(offset, offset + available),
).magic;
});
if (identity.state === "needs-slice-header")
return "FAT slice identity is outside the bounded prefix";