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:
1 parent
57e76599c0
commit
44521d3d75
8 files changed
+55
-39
No files matched your search
@@ -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 +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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user