mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
* feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers inspect_go_binary and inspect_rust_binary refused universal Mach-O files and offered no slice selection, so hosts without lipo could not inspect them. Both now accept architecture (x86, x86_64, arm, arm64). One selector reads REA's own FAT table, validates the slice range, alignment and header CPU, and returns the thin image with its macho-selected-image-v1 identity. The universal file stays the artifact; results add selected_slice and keep every source range a file offset. Missing, absent, duplicate or misplaced selections fail on architecture and list the slices present. Subject format mach-o-universal now has one owner shared by the services and the retained Go/Rust views. Fixes #4102 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): reject damaged universal files and self-contradicting slice reports A FAT table whose slice could not be read was treated as a non-universal file, so callers were told to omit architecture. Its reason now fails on path. Go and Rust reports with a selected slice must be Mach-O and name the slice's CPU family, through one shared agreement check. The CLI architecture flag is described in help. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import {
|
|
classifyFatContainerIdentity,
|
|
readFatContainerHeader,
|
|
readMachoHeaderPrefixFacts,
|
|
validateFatSlice,
|
|
} from "../../domain/apple/machoContainer.js";
|
|
import {
|
|
MACHO_CPU_TYPES,
|
|
type MachoSliceArchitecture,
|
|
type MachoTargetSelection,
|
|
} from "../../domain/apple/machoTargetSelection.js";
|
|
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
|
|
const cpuName = (cpuType: number, cpuSubtype: number): string => {
|
|
const name = Object.entries(MACHO_CPU_TYPES).find(
|
|
([, type]) => type === cpuType,
|
|
)?.[0];
|
|
const subtype = `0x${cpuSubtype.toString(16)}`;
|
|
return name === undefined
|
|
? `cpu type 0x${cpuType.toString(16)} subtype ${subtype}`
|
|
: `${name} (subtype ${subtype})`;
|
|
};
|
|
|
|
/** The one thin image an inspector reads, and where it lies in the selected file. */
|
|
export interface SelectedNativeImage {
|
|
readonly bytes: Buffer;
|
|
/** File offset of `bytes`; add it to image-relative offsets to locate them in the file. */
|
|
readonly fileOffset: number;
|
|
/** Selected universal slice identity; null when the file is not a universal Mach-O. */
|
|
readonly selection: MachoTargetSelection | null;
|
|
}
|
|
|
|
/** Why no image could be selected; `architecture` issues are the caller's to correct. */
|
|
export interface MachoSliceSelectionIssue {
|
|
readonly kind: "architecture" | "malformed";
|
|
readonly message: string;
|
|
}
|
|
|
|
/**
|
|
* Select the thin image to inspect without lipo. A universal Mach-O needs the
|
|
* caller's architecture and yields exactly one validated slice; any other file
|
|
* is returned whole and rejects an architecture rather than ignoring it.
|
|
*/
|
|
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")
|
|
)
|
|
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(", ");
|
|
if (architecture === undefined)
|
|
return err({
|
|
kind: "architecture",
|
|
message: `Universal Mach-O holds ${available}; select one architecture.`,
|
|
});
|
|
const matches = fat.slices.filter(
|
|
({ cpuType }) => cpuType === MACHO_CPU_TYPES[architecture],
|
|
);
|
|
const [slice] = matches;
|
|
if (slice === undefined || matches.length > 1)
|
|
return err({
|
|
kind: "architecture",
|
|
message:
|
|
slice === undefined
|
|
? `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 });
|
|
return ok({
|
|
bytes: image,
|
|
fileOffset: slice.offset,
|
|
selection: {
|
|
kind: "macho-selected-image-v1",
|
|
container: "fat",
|
|
offset: slice.offset,
|
|
byte_length: slice.size,
|
|
cpu_type: slice.cpuType,
|
|
cpu_subtype: slice.cpuSubtype,
|
|
container_table_sha256: createHash("sha256")
|
|
.update(bytes.subarray(0, fat.tableEnd))
|
|
.digest("hex"),
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Select the image for one inspection, or fail with the input to correct:
|
|
* `architecture` for a missing or mismatched selection, `path` for a malformed
|
|
* universal container.
|
|
*/
|
|
export const requireSelectedNativeImage = (
|
|
operation: string,
|
|
path: string,
|
|
bytes: Buffer,
|
|
architecture: MachoSliceArchitecture | undefined,
|
|
): SelectedNativeImage => {
|
|
const selected = selectNativeImage(bytes, architecture);
|
|
if (selected.ok) return selected.value;
|
|
const { kind, message } = selected.error;
|
|
throw new AnalysisInputError(operation, undefined, [
|
|
kind === "architecture"
|
|
? { path: ["architecture"], reason: "invalid_value", message }
|
|
: {
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: `Selected artifact ${path}: ${message}`,
|
|
},
|
|
]);
|
|
};
|