mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(native): derive Mach-O word size and byte order from the selected slice (#3163)
* fix(native): derive Mach-O word size and byte order from the selected slice inspect_macho read endian and word_size from `file -b` for the whole file. macOS `file` never prints a Mach-O byte order, so endian was null for every target. For a universal binary, word_size matched "64-bit" anywhere in the summary instead of describing the selected slice, and it was null for a thin 32-bit slice. Both now come from the selected slice's otool header: the magic gives the word size and the CPU family gives the byte order (x86 and ARM little, PowerPC big, other families unknown). The file capture is no longer needed by inspect_macho, so it is no longer run there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(native): select a 32-bit slice beside a 64-bit slice through inspect_macho Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
131b2d85f3
commit
3f5b703379
7 files changed
+84
-23
No files matched your search
@@ -53,7 +53,6 @@ const inspect = async (lipo: string, architecture: "x86" | "arm64") => {
|
||||
run: (tool, args) => {
|
||||
commands.push({ tool, args });
|
||||
if (tool === "lipo") return Promise.resolve(ok(capture(tool, lipo)));
|
||||
if (tool === "file") return Promise.resolve(ok(capture(tool, "Mach-O")));
|
||||
return Promise.resolve(err(new AnalysisCancelledError("inspect_native")));
|
||||
},
|
||||
invocation: () => {
|
||||
@@ -72,7 +71,7 @@ describe("Mach-O tool architecture selection", () => {
|
||||
_tag: "AnalysisOutputError",
|
||||
reason: "lipo did not list i386; refusing to read another slice",
|
||||
});
|
||||
expect(commands.map(({ tool }) => tool)).toEqual(["file", "lipo"]);
|
||||
expect(commands.map(({ tool }) => tool)).toEqual(["lipo"]);
|
||||
});
|
||||
|
||||
it("passes the arm64e slice lipo listed for an arm64 target", async () => {
|
||||
@@ -264,7 +263,7 @@ describe("Mach-O slice command scheduling", () => {
|
||||
let maxInFlight = 0;
|
||||
let launched = 0;
|
||||
const run = async (tool: string, args: readonly string[]) => {
|
||||
if (tool === "file" || tool === "lipo") return fixtures.run(tool, args);
|
||||
if (tool === "lipo") return fixtures.run(tool, args);
|
||||
const order = launched++;
|
||||
inFlight += 1;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
@@ -300,7 +299,7 @@ describe("Mach-O slice command scheduling", () => {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(runner.maxInFlight()).toBe(SLICE_TOOLS.length + 1);
|
||||
expect(result.value.result).toMatchObject({
|
||||
provenance: ["file", "lipo", ...SLICE_TOOLS, "vtool"].map((tool) => ({
|
||||
provenance: ["lipo", ...SLICE_TOOLS, "vtool"].map((tool) => ({
|
||||
tool,
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -39,10 +39,7 @@ interface NativeMachoInspectionContext {
|
||||
) => NativeCommandInvocation;
|
||||
}
|
||||
|
||||
const DISCOVERY_COMMANDS = [
|
||||
["file", ["-b"]],
|
||||
["lipo", ["-detailed_info"]],
|
||||
] as const;
|
||||
const DISCOVERY_COMMANDS = [["lipo", ["-detailed_info"]]] as const;
|
||||
|
||||
const REQUIRED_SLICE_COMMANDS = [
|
||||
["otool", ["-h", "-l"]],
|
||||
@@ -247,8 +244,8 @@ const normalizeMacho = (
|
||||
return inspectMachoSchema.parse({
|
||||
format: "mach-o",
|
||||
selected_architecture: selectedArchitecture,
|
||||
endian: parseEndian(byTool("file").stdout),
|
||||
word_size: parseWordSize(byTool("file").stdout),
|
||||
endian: load.endian,
|
||||
word_size: load.wordSize,
|
||||
file_type: load.fileType,
|
||||
flags: load.flags,
|
||||
uuid: uuid.value,
|
||||
@@ -308,16 +305,6 @@ const parseNmExports = (output: string) =>
|
||||
source: "nm",
|
||||
}));
|
||||
|
||||
const parseEndian = (output: string): "little" | "big" | null =>
|
||||
/little-endian/iu.test(output)
|
||||
? "little"
|
||||
: /big-endian/iu.test(output)
|
||||
? "big"
|
||||
: null;
|
||||
|
||||
const parseWordSize = (output: string): 32 | 64 | null =>
|
||||
/64-bit/iu.test(output) ? 64 : /32-bit/iu.test(output) ? 32 : null;
|
||||
|
||||
const covered = <Value>(
|
||||
items: readonly Value[],
|
||||
exhaustive: boolean,
|
||||
|
||||
@@ -134,6 +134,8 @@ export const parseOtoolLoadCommands = (raw: string) => {
|
||||
parseLoadCommand(block, state);
|
||||
validateLoadCommandFraming(header, state.commands);
|
||||
return {
|
||||
wordSize: WORD_SIZES.get(header.magic ?? "") ?? null,
|
||||
endian: header.cpuType === null ? null : cpuEndian(header.cpuType),
|
||||
fileType: header.fileType,
|
||||
flags: [...state.flags].sort(),
|
||||
uuid: state.uuid,
|
||||
@@ -145,10 +147,27 @@ export const parseOtoolLoadCommands = (raw: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** otool prints the selected slice's magic in host order, symbolic with -v. */
|
||||
const WORD_SIZES: ReadonlyMap<string, 32 | 64> = new Map<string, 32 | 64>([
|
||||
["0xfeedface", 32],
|
||||
["0xfeedfacf", 64],
|
||||
["MH_MAGIC", 32],
|
||||
["MH_MAGIC_64", 64],
|
||||
]);
|
||||
|
||||
/** Mach-O byte order by CPU family, ignoring the ABI bits; others stay unknown. */
|
||||
const cpuEndian = (cpuType: number): "little" | "big" | null => {
|
||||
const family = cpuType & 0x00ffffff;
|
||||
if (family === 7 || family === 12) return "little";
|
||||
return family === 18 ? "big" : null;
|
||||
};
|
||||
|
||||
const parseHeader = (
|
||||
output: string,
|
||||
):
|
||||
| {
|
||||
magic: string | null;
|
||||
cpuType: number | null;
|
||||
fileType: string | null;
|
||||
flags: string[];
|
||||
commandCount: number;
|
||||
@@ -175,7 +194,10 @@ const parseHeader = (
|
||||
: fileTypeIndex;
|
||||
const flagsEnd =
|
||||
flagsIndex >= 0 ? values.length - (columns.length - flagsIndex - 1) : 0;
|
||||
const magicIndex = columns.indexOf("magic");
|
||||
return {
|
||||
magic: magicIndex < 0 ? null : (values[magicIndex] ?? null),
|
||||
cpuType: headerValue(columns, values, "cputype"),
|
||||
commandCount,
|
||||
commandBytes,
|
||||
fileType:
|
||||
|
||||
@@ -142,7 +142,7 @@ it("rejects nonzero supplemental codesign output that is not an unsigned observa
|
||||
|
||||
it("retains native inventory facts from captured Apple tool output", async () => {
|
||||
const outputs: Record<string, string> = {};
|
||||
for (const tool of ["file", "lipo", "otool", "nm", "dwarfdump", "vtool"])
|
||||
for (const tool of ["lipo", "otool", "nm", "dwarfdump", "vtool"])
|
||||
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
|
||||
outputs.otool = withOtoolMachHeader(outputs.otool ?? "");
|
||||
outputs["dyld_info:-imports"] = await fixture("dyld-inventory/imports.txt");
|
||||
|
||||
@@ -40,6 +40,24 @@ it("retains the delayed dependency in the complete real framed otool output", as
|
||||
expect(parseOtoolLoadCommands(output).commands).toHaveLength(23);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["0xfeedfacf 16777228", 64, "little"],
|
||||
["0xfeedfacf 16777223", 64, "little"],
|
||||
["0xfeedface 7", 32, "little"],
|
||||
["0xfeedface 18", 32, "big"],
|
||||
["0xfeedface 6", 32, null],
|
||||
["0xfeedfade 16777228", null, "little"],
|
||||
] as const)(
|
||||
"derives word size and byte order from the selected slice header %s",
|
||||
async (header, wordSize, endian) => {
|
||||
const output = (await nativeFixture("otool-dylib-use.txt")).replace(
|
||||
"0xfeedfacf 16777228",
|
||||
header,
|
||||
);
|
||||
expect(parseOtoolLoadCommands(output)).toMatchObject({ wordSize, endian });
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a repeated options field even when the duplicate has no value", async () => {
|
||||
const output = (await nativeFixture("otool-dylib-use.txt")).replace(
|
||||
" options delay-init",
|
||||
|
||||
@@ -27,7 +27,7 @@ const sliceSelectingRunner = (
|
||||
calls.push({ tool, arguments_ });
|
||||
const expected = tool === "dyld_info" ? dyldArchitecture : selected;
|
||||
if (
|
||||
!["file", "lipo"].includes(tool) &&
|
||||
tool !== "lipo" &&
|
||||
!arguments_.some(
|
||||
(argument, index) =>
|
||||
argument === expected ||
|
||||
@@ -313,3 +313,38 @@ it.each([
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("reports the selected 32-bit slice's word size and byte order beside a 64-bit slice", async () => {
|
||||
const i386Header = (await nativeFixture("otool-load.txt")).replace(
|
||||
"0xfeedfacf 16777228",
|
||||
"0xfeedface 7",
|
||||
);
|
||||
const fixtures = new NativeFixtureRunner({
|
||||
lipo: [
|
||||
"architecture x86_64\n cputype 16777223\n cpusubtype 3\n offset 0\n size 8192\n align 2^14 (16384)",
|
||||
"architecture i386\n cputype 7\n cpusubtype 3\n offset 16384\n size 8192\n align 2^14 (16384)",
|
||||
].join("\n"),
|
||||
"otool:-h": i386Header,
|
||||
dwarfdump:
|
||||
"UUID: 01234567-89AB-CDEF-0123-456789ABCDEF (i386) /owned/universal\n",
|
||||
});
|
||||
const base = nativeMachoTarget("/owned/universal");
|
||||
const result = await new NativeMacOSProvider(
|
||||
{},
|
||||
sliceSelectingRunner(fixtures, "i386", "i386"),
|
||||
"darwin",
|
||||
)
|
||||
.createClient({
|
||||
...base,
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86_64", "x86"],
|
||||
})
|
||||
.execute("inspect_macho", {});
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
selected_architecture: { name: "i386", container_file_offset: 16384 },
|
||||
word_size: 32,
|
||||
endian: "little",
|
||||
});
|
||||
});
|
||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ const outputFor = async (
|
||||
if (tool === "file")
|
||||
return arguments_.at(-1)?.endsWith(".plist") === true
|
||||
? "XML 1.0 document text\n"
|
||||
: "Mach-O 64-bit executable arm64, little-endian\n";
|
||||
: "Mach-O 64-bit executable arm64\n";
|
||||
if (tool === "vtool") return "Load command 3 LC_BUILD_VERSION\n";
|
||||
if (tool === "swift-demangle") return nativeFixture("demangle.txt");
|
||||
if (tool === "plutil") return nativeFixture("plist.json");
|
||||
|
||||
Reference in new issue
Block a user