refactor(macho): share header declarations across native readers

This commit is contained in:
morluto committed 2026-10-26 14:20:01 +08:00
1 parent c11fb48c98
commit ed7b3a2a10
3 files changed
+50 -20

No files matched your search

+17 -6
View File
@@ -10,8 +10,8 @@ export interface MachoLoadCommandTableHeader {
readonly commandBytes: number;
}
/** Read only the thin header's load-command framing; other formats return null. */
export const readMachoLoadCommandTableHeader = (
/** Decode thin-header declarations without admitting their table extent. */
export const readMachoLoadCommandDeclarations = (
bytes: Uint8Array,
fileSize: number,
): Result<MachoLoadCommandTableHeader | null, string> => {
@@ -28,10 +28,6 @@ export const readMachoLoadCommandTableHeader = (
);
const littleEndian = magic === 0xcefaedfe || magic === 0xcffaedfe;
const commandBytes = view.getUint32(20, littleEndian);
if (commandBytes > fileSize - headerSize)
return err(
`truncated Mach-O load commands: the header declares ${commandBytes} bytes after its ${headerSize}-byte header; the file has ${fileSize}`,
);
return ok({
headerSize,
wide,
@@ -41,6 +37,21 @@ export const readMachoLoadCommandTableHeader = (
});
};
/** Admit the declared command table within the complete file's extent. */
export const readMachoLoadCommandTableHeader = (
bytes: Uint8Array,
fileSize: number,
): Result<MachoLoadCommandTableHeader | null, string> => {
const parsed = readMachoLoadCommandDeclarations(bytes, fileSize);
if (!parsed.ok || parsed.value === null) return parsed;
const { commandBytes, headerSize } = parsed.value;
if (commandBytes > fileSize - headerSize)
return err(
`truncated Mach-O load commands: the header declares ${commandBytes} bytes after its ${headerSize}-byte header; the file has ${fileSize}`,
);
return parsed;
};
/** One owner's count, alignment and extent proof for a sequential command walk. */
export class MachoLoadCommandCursor {
#index = 0;
+12 -14
View File
@@ -8,18 +8,21 @@ import {
type LoadRegion,
type ZeroFillMapping,
} from "./BinaryContainer.js";
import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js";
import {
MachoLoadCommandCursor,
readMachoLoadCommandDeclarations,
} from "../../domain/apple/machoLoadCommands.js";
/** Read thin Mach-O images, retaining the slice's exact file-backed mappings. */
export const readMachoImage = (bytes: Buffer): BinaryContainer => {
const magic = bytes.readUInt32BE(0);
const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
const bits = magic === 0xfeedfacf || magic === 0xcffaedfe ? 64 : 32;
const parsed = readMachoLoadCommandDeclarations(bytes, bytes.length);
if (!parsed.ok) throw new BinaryFormatFailure("malformed", parsed.error);
if (parsed.value === null)
throw new BinaryFormatFailure("malformed", "Unrecognized Mach-O header");
const table = parsed.value;
const { headerSize, commandBytes, littleEndian: little } = table;
const bits = table.wide ? 64 : 32;
const reader = new BinaryReader(bytes, little);
const headerSize = bits === 64 ? 32 : 28;
reader.range(0, headerSize, "Mach-O header");
const count = reader.u32(16);
const commandBytes = reader.u32(20);
const commandStart = reader.table(
BigInt(headerSize),
commandBytes,
@@ -32,12 +35,7 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
const zeroFills: ZeroFillMapping[] = [];
const sections: ImageSection[] = [];
let chainedFixups = false;
const commands = new MachoLoadCommandCursor({
wide: bits === 64,
littleEndian: little,
commandCount: count,
commandBytes,
});
const commands = new MachoLoadCommandCursor(table);
while (commands.nextOffset !== null) {
const offset = commandStart + commands.nextOffset;
const frame = commands.read(bytes.subarray(offset, offset + 8));
@@ -2,6 +2,9 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { BinaryResourceFailure } from "../../../src/native/metadata/BinaryContainer.js";
import { readMachoLoadCommandTableHeader } from "../../../src/domain/apple/machoLoadCommands.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
import { parseExecutableHeader } from "../../../src/domain/binaryTarget.js";
@@ -46,6 +49,7 @@ it.each([
{ label: "undersized", field: 36, value: 4 },
{ label: "unaligned", field: 36, value: 12 },
{ label: "outside table", field: 36, value: 0xffffffff },
{ label: "truncated declared table", field: 20, value: 24 },
{ label: "too many commands", field: 16, value: 2 },
{ label: "unclaimed table bytes", field: 16, value: 0 },
{ label: "trailing table bytes", field: 36, value: 8 },
@@ -60,6 +64,7 @@ it.each([
);
await writeFile(path, bytes);
expect(parseExecutableHeader(bytes, "arm64").ok).toBe(false);
expect(() => readBinaryContainer(bytes)).toThrow(/load command/iu);
const resolved = await parseBinaryTarget(path);
expect(resolved.ok).toBe(false);
if (resolved.ok || !(resolved.error instanceof BinaryTargetError))
@@ -86,6 +91,11 @@ it.each([
"image",
);
await writeFile(path, bytes);
expect(readBinaryContainer(bytes)).toMatchObject({
format: "macho",
bits: wide ? 64 : 32,
byte_order: littleEndian ? "little" : "big",
});
expect(await parseBinaryTarget(path)).toMatchObject({
ok: true,
value: { format: "mach-o", architecture: wide ? "arm64" : "x86" },
@@ -150,3 +160,14 @@ it.each([null, 0, 1])(
});
},
);
it("preserves native capacity priority when the command table also exceeds the file", () => {
const bytes = commandImage(true, true, [16]);
bytes.writeUInt32LE(16 * 1024 * 1024 + 1, 20);
expect(() => readBinaryContainer(bytes)).toThrow(BinaryResourceFailure);
const bounded = readMachoLoadCommandTableHeader(bytes, bytes.length);
expect(bounded).toMatchObject({
ok: false,
error: expect.stringContaining("truncated Mach-O load commands"),
});
});