diff --git a/src/native/NativeMachoInspection.test.ts b/src/native/NativeMachoInspection.test.ts index 640e540be9..9e9ef8dd33 100644 --- a/src/native/NativeMachoInspection.test.ts +++ b/src/native/NativeMachoInspection.test.ts @@ -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, })), }); diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index 8453f38a8d..5e84da5748 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -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 = ( items: readonly Value[], exhaustive: boolean, diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 8f1ed9dda9..10804b126c 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -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 = new Map([ + ["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: diff --git a/tests/boundary/providers/native/nativeInventory.golden.test.ts b/tests/boundary/providers/native/nativeInventory.golden.test.ts index 934d241375..ff86f60caf 100644 --- a/tests/boundary/providers/native/nativeInventory.golden.test.ts +++ b/tests/boundary/providers/native/nativeInventory.golden.test.ts @@ -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 = {}; - 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"); diff --git a/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts b/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts index 8a1151df35..efd4d88734 100644 --- a/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts +++ b/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts @@ -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", diff --git a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts index 182c336121..f1597bec6f 100644 --- a/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts +++ b/tests/boundary/providers/native/nativeSelectedArchitecture.test.ts @@ -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", + }); +}); diff --git a/tests/fixtures/nativeCommands.ts b/tests/fixtures/nativeCommands.ts index 34af395f93..5be6546591 100644 --- a/tests/fixtures/nativeCommands.ts +++ b/tests/fixtures/nativeCommands.ts @@ -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");