diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts index 87942cf51e..04d5b27c1b 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.test.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts @@ -108,6 +108,7 @@ it.each([ { command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.i386, platform: 7 }, { command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.x86_64, platform: 7 }, { command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.arm64, platform: 2 }, + { command: LC.VERSION_MIN_TVOS, cpu: CPU.i386, platform: 3 }, { command: LC.VERSION_MIN_TVOS, cpu: CPU.x86_64, platform: 8 }, { command: LC.VERSION_MIN_TVOS, cpu: CPU.arm64, platform: 3 }, { command: LC.VERSION_MIN_WATCHOS, cpu: CPU.i386, platform: 9 }, diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts index f08b1bd302..7b2141a6e0 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -4,6 +4,10 @@ import type { MachoSlice, } from "../../domain/apple/dylibResolution.js"; import { machoCpuSubtypeBase } from "../../domain/apple/machoCpuIdentity.js"; +import { + legacyMachoPlatform, + type MachoVersionMinPlatform, +} from "../../domain/apple/machoPlatform.js"; import { machoLoadCommandAlignment, classifyFatContainerIdentity, @@ -53,53 +57,18 @@ const LC_LOAD_UPWARD_DYLIB = 0x80000023; const LC_RPATH = 0x8000001c; const LC_DYLD_ENVIRONMENT = 0x27; const LC_CODE_SIGNATURE = 0x1d; -const LC_VERSION_MIN_MACOSX = 0x24; -const LC_VERSION_MIN_IPHONEOS = 0x25; -const LC_VERSION_MIN_TVOS = 0x2f; -const LC_VERSION_MIN_WATCHOS = 0x30; const LC_BUILD_VERSION = 0x32; -// `mach-o/loader.h` `PLATFORM_*` numbers observed via Apple linker output. -const PLATFORM_MACOS = 1; -const PLATFORM_IOS = 2; -const PLATFORM_TVOS = 3; -const PLATFORM_WATCHOS = 4; -const PLATFORM_IOSSIMULATOR = 7; -const PLATFORM_TVOSSIMULATOR = 8; -const PLATFORM_WATCHOSSIMULATOR = 9; - -const CPU_TYPE_I386 = 7; -const CPU_TYPE_X86_64 = 0x01000007; const CPU_TYPE_ARM64 = 0x0100000c; -/** - * Legacy `LC_VERSION_MIN_*` commands predate explicit simulator platform - * IDs. Per Apple's dyld rules the image CPU type distinguishes simulator - * from device: Intel images are simulator, ARM images are device - * (tvOS simulator applies to x86_64 only). `LC_BUILD_VERSION` platform - * numbers are preserved verbatim; unknown future numbers stay unknown - * downstream, never mapped to nearest. - */ -const legacyVersionMinPlatform = ( - command: number, - cpuType: number, -): number | undefined => { - const isIntel = cpuType === CPU_TYPE_I386 || cpuType === CPU_TYPE_X86_64; - switch (command) { - case LC_VERSION_MIN_MACOSX: - return PLATFORM_MACOS; - case LC_VERSION_MIN_IPHONEOS: - return isIntel ? PLATFORM_IOSSIMULATOR : PLATFORM_IOS; - case LC_VERSION_MIN_TVOS: - return cpuType === CPU_TYPE_X86_64 - ? PLATFORM_TVOSSIMULATOR - : PLATFORM_TVOS; - case LC_VERSION_MIN_WATCHOS: - return isIntel ? PLATFORM_WATCHOSSIMULATOR : PLATFORM_WATCHOS; - default: - return undefined; - } -}; +/** loader.h command codes and their device platform identities. */ +const VERSION_MIN_PLATFORMS: ReadonlyMap = + new Map([ + [0x24, 1], // LC_VERSION_MIN_MACOSX + [0x25, 2], // LC_VERSION_MIN_IPHONEOS + [0x2f, 3], // LC_VERSION_MIN_TVOS + [0x30, 4], // LC_VERSION_MIN_WATCHOS + ]); /** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */ const DYLIB_USE_MARKER = 0x1a741800; @@ -691,14 +660,14 @@ const decodeCommand = ( slice.platforms.push(platform); return; } - const legacyPlatform = legacyVersionMinPlatform(command, slice.cpu_type); + const legacyPlatform = VERSION_MIN_PLATFORMS.get(command); if (legacyPlatform !== undefined) { if (body.byteLength < 16) throw new MachoFormatIssue( "malformed", `load command ${index} is too short for a version-min command`, ); - slice.platforms.push(legacyPlatform); + slice.platforms.push(legacyMachoPlatform(legacyPlatform, slice.cpu_type)); return; } if (command === LC_LAZY_LOAD_DYLIB_INFO) diff --git a/src/domain/apple/machoPlatform.ts b/src/domain/apple/machoPlatform.ts new file mode 100644 index 0000000000..997f3c6c63 --- /dev/null +++ b/src/domain/apple/machoPlatform.ts @@ -0,0 +1,19 @@ +/** Device platform encoded by a legacy LC_VERSION_MIN_* command. */ +export type MachoVersionMinPlatform = 1 | 2 | 3 | 4; + +/** + * dyld derives legacy simulator platforms from the CPU type. tvOS uses + * x86_64 only; iOS and watchOS also recognize i386. Explicit LC_BUILD_VERSION + * platforms do not use this rule. + */ +export const legacyMachoPlatform = ( + platform: MachoVersionMinPlatform, + cpuType: number | null, +): number => { + if (platform === 3) return cpuType === 0x01000007 ? 8 : 3; + if (cpuType === 7 || cpuType === 0x01000007) { + if (platform === 2) return 7; + if (platform === 4) return 9; + } + return platform; +}; diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 37ade5ac33..f3fc0dfb56 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -1,3 +1,8 @@ +import { + legacyMachoPlatform, + type MachoVersionMinPlatform, +} from "../../domain/apple/machoPlatform.js"; + interface ParsedSection { readonly segment: string; readonly name: string; @@ -70,6 +75,14 @@ const DYLIB_FIELDS = [ const DYLIB_ENCODING_FIELDS = ["time stamp", "options"]; const EMPTY_VALUE_FIELDS = new Set(["options", "segname", "sectname"]); +const VERSION_MIN_PLATFORMS: ReadonlyMap = + new Map([ + ["LC_VERSION_MIN_MACOSX", 1], + ["LC_VERSION_MIN_IPHONEOS", 2], + ["LC_VERSION_MIN_TVOS", 3], + ["LC_VERSION_MIN_WATCHOS", 4], + ]); + /** * Non-verbose otool prints LC_BUILD_VERSION platform and tool codes as * numbers. Name the codes declared in the SDK's , without @@ -408,8 +421,16 @@ const collectBuild = ( } if (kind.startsWith("LC_VERSION_MIN_")) { const platform = kind.slice("LC_VERSION_MIN_".length) || null; + const devicePlatform = VERSION_MIN_PLATFORMS.get(kind); + const derivedPlatform = + devicePlatform === undefined + ? undefined + : legacyMachoPlatform(devicePlatform, state.cpuType); state.builds.push({ - platform: legacySimulatorPlatform(platform, state.cpuType), + platform: + derivedPlatform === undefined || derivedPlatform === devicePlatform + ? platform + : buildCodeName(BUILD_PLATFORM_NAMES, String(derivedPlatform)), minimum_os: stringField(fields, "version"), sdk: stringField(fields, "sdk"), tools: [], @@ -417,24 +438,6 @@ const collectBuild = ( } }; -// Apple dyld interprets legacy iOS/tvOS/watchOS minimum-version commands on -// Intel slices as simulator platforms. Newer commands encode this directly. -const legacySimulatorPlatform = ( - platform: string | null, - cpuType: number | null, -): string | null => { - if (![7, 0x01000007].includes(cpuType ?? -1)) return platform; - return ( - ( - { - IPHONEOS: "IOSSIMULATOR", - TVOS: "TVOSSIMULATOR", - WATCHOS: "WATCHOSSIMULATOR", - } as const - )[platform as "IPHONEOS" | "TVOS" | "WATCHOS"] ?? platform - ); -}; - const collectDependency = ( kind: string, fields: Readonly>, diff --git a/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts b/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts index 61722e921b..aeae2b523d 100644 --- a/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts +++ b/tests/boundary/providers/native/nativeMachoHeader.golden.test.ts @@ -269,33 +269,66 @@ Section }); it.each([ - ["LC_VERSION_MIN_IPHONEOS", "IOSSIMULATOR"], - ["LC_VERSION_MIN_TVOS", "TVOSSIMULATOR"], - ["LC_VERSION_MIN_WATCHOS", "WATCHOSSIMULATOR"], -])("classifies legacy Intel simulator command %s", (command, platform) => { + ["LC_VERSION_MIN_IPHONEOS", 7, "IOSSIMULATOR"], + ["LC_VERSION_MIN_IPHONEOS", 16777223, "IOSSIMULATOR"], + ["LC_VERSION_MIN_IPHONEOS", 16777228, "IPHONEOS"], + ["LC_VERSION_MIN_TVOS", 7, "TVOS"], + ["LC_VERSION_MIN_TVOS", 16777223, "TVOSSIMULATOR"], + ["LC_VERSION_MIN_TVOS", 16777228, "TVOS"], + ["LC_VERSION_MIN_WATCHOS", 7, "WATCHOSSIMULATOR"], + ["LC_VERSION_MIN_WATCHOS", 16777223, "WATCHOSSIMULATOR"], + ["LC_VERSION_MIN_WATCHOS", 16777228, "WATCHOS"], + ["LC_VERSION_MIN_MACOSX", 7, "MACOSX"], + ["LC_VERSION_MIN_MACOSX", 16777223, "MACOSX"], + ["LC_VERSION_MIN_MACOSX", 16777228, "MACOSX"], +])( + "classifies legacy command %s on CPU %s as %s", + (command, cpuType, platform) => { + const text = [ + "Mach header", + " magic cputype cpusubtype caps filetype ncmds sizeofcmds flags", + ` ${cpuType === 7 ? "0xfeedface" : "0xfeedfacf"} ${cpuType} 3 0x00 2 1 16 0x0`, + "Load command 0", + ` cmd ${command}`, + " cmdsize 16", + " version 12.0", + " sdk 12.1", + ].join("\n"); + expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform); + }, +); + +it.each([ + ["3", "TVOS"], + ["4294967295", "4294967295"], +])("preserves explicit build platform %s on Intel", (code, platform) => { + const text = [ + "Mach header", + " magic cputype cpusubtype caps filetype ncmds sizeofcmds flags", + " 0xfeedfacf 16777223 3 0x00 2 1 24 0x0", + "Load command 0", + " cmd LC_BUILD_VERSION", + " cmdsize 24", + ` platform ${code}`, + " minos 12.0", + " sdk 12.1", + " ntools 0", + ].join("\n"); + expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform); +}); + +it("preserves an unknown legacy platform spelling on Intel", () => { const text = [ "Mach header", " magic cputype cpusubtype caps filetype ncmds sizeofcmds flags", " 0xfeedfacf 16777223 3 0x00 2 1 16 0x0", "Load command 0", - ` cmd ${command}`, + " cmd LC_VERSION_MIN_FUTURE", " cmdsize 16", " version 12.0", " sdk 12.1", ].join("\n"); - expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe(platform); -}); - -it("keeps legacy device platforms on arm64 slices", () => { - const text = [ - "Mach header", - " magic cputype cpusubtype caps filetype ncmds sizeofcmds flags", - " 0xfeedfacf 16777228 0 0x00 2 1 16 0x0", - "Load command 0", - " cmd LC_VERSION_MIN_IPHONEOS", - " cmdsize 16", - " version 12.0", - " sdk 12.1", - ].join("\n"); - expect(parseOtoolLoadCommands(text).builds[0]?.platform).toBe("IPHONEOS"); + const result = parseOtoolLoadCommands(text); + expect(result.builds[0]?.platform).toBe("FUTURE"); + expect(result.commands[0]?.kind).toBe("LC_VERSION_MIN_FUTURE"); });