use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use object::{
Architecture, Endian, File, Object, ObjectSection, macho, read::macho::LoadCommandVariant,
};
use tracing::{debug, warn};
use crate::OSVersion;
#[derive(Debug)]
pub struct Binary {
pub path: PathBuf,
pub arch: Architecture,
pub name: Option<Vec<u8>>,
versions: Vec<BuildVersion>,
pub info_plist_data: Option<Vec<u8>>,
pub entitlements_data: Option<Vec<u8>>,
pub signed: bool,
pub gui_like: bool,
}
impl Binary {
pub fn parse(path: &Path) -> Result<Self> {
let file = std::fs::read(path).context("failed reading")?;
let file = File::parse(&*file).context("failed parsing")?;
let (endianness, cputype, filetype) = match &file {
File::MachO32(m) => (
m.endian(),
m.macho_header().cputype.get(m.endian()),
m.macho_header().filetype.get(m.endian()),
),
File::MachO64(m) => (
m.endian(),
m.macho_header().cputype.get(m.endian()),
m.macho_header().filetype.get(m.endian()),
),
_ => bail!("not a Mach-O file: {file:?}"),
};
if filetype != macho::MH_EXECUTE {
warn!("unsupported file type {filetype:02x}");
}
let load_commands = match &file {
File::MachO32(m) => m.macho_load_commands(),
File::MachO64(m) => m.macho_load_commands(),
_ => bail!("not a Mach-O file"),
};
debug!("vtool -show-build {path:?}");
debug!("dyld_info -linked_dylibs {path:?} | grep -E 'AppKit|UIKit'");
let mut versions = Vec::new();
let mut signed = false;
let mut name = None;
let mut gui_like = false;
for cmd in load_commands.context("failed reading load command")? {
let cmd = cmd.context("failed reading load command")?;
if let Ok(variant) = cmd.variant() {
if let Some(v) = BuildVersion::from_load_command(variant, cputype, endianness) {
versions.push(v);
}
if let LoadCommandVariant::IdDylib(dylib_cmd) = variant {
let s = cmd
.string(endianness, dylib_cmd.dylib.name)
.context("failed reading LC_ID_DYLIB")?;
name = Some(s.to_vec());
}
if let LoadCommandVariant::Dylib(dylib_cmd) = variant {
let s = cmd
.string(endianness, dylib_cmd.dylib.name)
.context("failed reading dylib")?;
if contains(s, b"AppKit")
|| contains(s, b"Cocoa") || contains(s, b"UIKit")
|| contains(s, b"WatchKit")
|| contains(s, b"SwiftUI")
|| contains(s, b"WatchKit")
{
gui_like = true;
}
}
}
if cmd.cmd() == macho::LC_CODE_SIGNATURE {
signed = true;
}
}
match versions.len() {
0 => warn!("binary had no version information"),
1 => {}
_ => warn!("zippered binaries aren't yet properly supported"),
}
debug!("segedit {path:?} -extract __TEXT __info_plist /dev/stdout");
let info_plist_data = if let Some(section) = file.section_by_name("__info_plist") {
let segment_name = section
.segment_name_bytes()
.context("failed reading segment name")?;
if segment_name != Some(b"__TEXT") {
warn!("__info_plist was not in __TEXT segment");
}
let data = section.data().context("failed reading section contents")?;
Some(data.to_vec())
} else {
None
};
debug!("segedit {path:?} -extract __TEXT __entitlements /dev/stdout");
let entitlements_data = if let Some(section) = file.section_by_name("__entitlements") {
let segment_name = section
.segment_name_bytes()
.context("failed reading segment name")?;
if segment_name != Some(b"__TEXT") {
warn!("__entitlements was not in __TEXT segment");
}
let data = section.data().context("failed reading section contents")?;
Some(data.to_vec())
} else {
None
};
Ok(Self {
path: path.to_owned(),
arch: file.architecture(),
name,
versions,
info_plist_data,
entitlements_data,
signed,
gui_like,
})
}
fn version(&self) -> BuildVersion {
self.versions.first().copied().unwrap_or_default()
}
pub fn platform(&self) -> Platform {
self.version().platform
}
pub fn minos(&self) -> OSVersion {
self.version().minos
}
pub fn sdk(&self) -> OSVersion {
self.version().sdk
}
#[expect(dead_code)]
pub(crate) fn needs_info_plist(&self) -> bool {
!matches!(
self.version().platform,
Platform::MACOS
| Platform::IOSSIMULATOR
| Platform::TVOSSIMULATOR
| Platform::WATCHOSSIMULATOR
| Platform::VISIONOSSIMULATOR
)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
struct BuildVersion {
platform: Platform,
minos: OSVersion,
sdk: OSVersion,
}
impl BuildVersion {
fn from_load_command<E: Endian>(
variant: LoadCommandVariant<'_, E>,
cputype: u32,
endianness: E,
) -> Option<Self> {
match variant {
LoadCommandVariant::BuildVersion(version) => Some(BuildVersion {
platform: Platform(version.platform.get(endianness)),
minos: OSVersion::from_packed(version.minos.get(endianness)),
sdk: OSVersion::from_packed(version.sdk.get(endianness)),
}),
LoadCommandVariant::VersionMin(version) => Some(BuildVersion {
platform: match version.cmd.get(endianness) {
macho::LC_VERSION_MIN_MACOSX => Platform::MACOS,
macho::LC_VERSION_MIN_IPHONEOS => {
if matches!(cputype, macho::CPU_TYPE_X86_64 | macho::CPU_TYPE_X86) {
Platform::IOSSIMULATOR } else {
Platform::IOS
}
}
macho::LC_VERSION_MIN_TVOS => {
if cputype == macho::CPU_TYPE_X86_64 {
Platform::TVOSSIMULATOR } else {
Platform::TVOS
}
}
macho::LC_VERSION_MIN_WATCHOS => {
if matches!(cputype, macho::CPU_TYPE_X86_64 | macho::CPU_TYPE_X86) {
Platform::WATCHOSSIMULATOR } else {
Platform::WATCHOS
}
}
_ => unreachable!(),
},
minos: OSVersion::from_packed(version.version.get(endianness)),
sdk: OSVersion::from_packed(version.sdk.get(endianness)),
}),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Platform(u32);
impl Platform {
pub const MACOS: Self = Self(macho::PLATFORM_MACOS);
pub const IOS: Self = Self(macho::PLATFORM_IOS);
pub const TVOS: Self = Self(macho::PLATFORM_TVOS);
pub const WATCHOS: Self = Self(macho::PLATFORM_WATCHOS);
pub const BRIDGEOS: Self = Self(macho::PLATFORM_BRIDGEOS);
pub const MACCATALYST: Self = Self(macho::PLATFORM_MACCATALYST);
pub const IOSSIMULATOR: Self = Self(macho::PLATFORM_IOSSIMULATOR);
pub const TVOSSIMULATOR: Self = Self(macho::PLATFORM_TVOSSIMULATOR);
pub const WATCHOSSIMULATOR: Self = Self(macho::PLATFORM_WATCHOSSIMULATOR);
pub const DRIVERKIT: Self = Self(macho::PLATFORM_DRIVERKIT);
pub const VISIONOS: Self = Self(macho::PLATFORM_XROS);
pub const VISIONOSSIMULATOR: Self = Self(macho::PLATFORM_XROSSIMULATOR);
pub fn is_simulator(self) -> bool {
matches!(
self,
Self::IOSSIMULATOR
| Self::TVOSSIMULATOR
| Self::WATCHOSSIMULATOR
| Self::VISIONOSSIMULATOR
)
}
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}