1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result, bail};
4use object::{
5 Architecture, Endian, File, Object, ObjectSection, macho, read::macho::LoadCommandVariant,
6};
7use tracing::{debug, warn};
8
9use crate::OSVersion;
10
11#[derive(Debug)]
13pub struct Binary {
14 pub path: PathBuf,
16 pub arch: Architecture,
18 pub name: Option<Vec<u8>>,
20 versions: Vec<BuildVersion>,
23 pub info_plist_data: Option<Vec<u8>>,
27 pub entitlements_data: Option<Vec<u8>>,
31 pub signed: bool,
33 pub gui_like: bool,
40 }
47
48impl Binary {
49 pub fn parse(path: &Path) -> Result<Self> {
50 let file = std::fs::read(path).context("failed reading")?;
51
52 let file = File::parse(&*file).context("failed parsing")?;
53
54 let (endianness, cputype, filetype) = match &file {
55 File::MachO32(m) => (
56 m.endian(),
57 m.macho_header().cputype.get(m.endian()),
58 m.macho_header().filetype.get(m.endian()),
59 ),
60 File::MachO64(m) => (
61 m.endian(),
62 m.macho_header().cputype.get(m.endian()),
63 m.macho_header().filetype.get(m.endian()),
64 ),
65 _ => bail!("not a Mach-O file: {file:?}"),
66 };
67
68 if filetype != macho::MH_EXECUTE {
69 warn!("unsupported file type {filetype:02x}");
70 }
71
72 let load_commands = match &file {
73 File::MachO32(m) => m.macho_load_commands(),
74 File::MachO64(m) => m.macho_load_commands(),
75 _ => bail!("not a Mach-O file"),
76 };
77
78 debug!("vtool -show-build {path:?}");
79 debug!("dyld_info -linked_dylibs {path:?} | grep -E 'AppKit|UIKit'");
80 let mut versions = Vec::new();
81 let mut signed = false;
82 let mut name = None;
83 let mut gui_like = false;
84 for cmd in load_commands.context("failed reading load command")? {
85 let cmd = cmd.context("failed reading load command")?;
86 if let Ok(variant) = cmd.variant() {
87 if let Some(v) = BuildVersion::from_load_command(variant, cputype, endianness) {
88 versions.push(v);
89 }
90 if let LoadCommandVariant::IdDylib(dylib_cmd) = variant {
91 let s = cmd
92 .string(endianness, dylib_cmd.dylib.name)
93 .context("failed reading LC_ID_DYLIB")?;
94 name = Some(s.to_vec());
95 }
96 if let LoadCommandVariant::Dylib(dylib_cmd) = variant {
97 let s = cmd
98 .string(endianness, dylib_cmd.dylib.name)
99 .context("failed reading dylib")?;
100 if contains(s, b"AppKit")
106 || contains(s, b"Cocoa") || contains(s, b"UIKit")
108 || contains(s, b"WatchKit")
109 || contains(s, b"SwiftUI")
110 || contains(s, b"WatchKit")
111 {
112 gui_like = true;
113 }
114 }
115 }
116 if cmd.cmd() == macho::LC_CODE_SIGNATURE {
117 signed = true;
118 }
119 }
120
121 match versions.len() {
122 0 => warn!("binary had no version information"),
123 1 => {}
124 _ => warn!("zippered binaries aren't yet properly supported"),
125 }
126
127 debug!("segedit {path:?} -extract __TEXT __info_plist /dev/stdout");
128 let info_plist_data = if let Some(section) = file.section_by_name("__info_plist") {
129 let segment_name = section
130 .segment_name_bytes()
131 .context("failed reading segment name")?;
132 if segment_name != Some(b"__TEXT") {
133 warn!("__info_plist was not in __TEXT segment");
134 }
135 let data = section.data().context("failed reading section contents")?;
136 Some(data.to_vec())
137 } else {
138 None
139 };
140
141 debug!("segedit {path:?} -extract __TEXT __entitlements /dev/stdout");
142 let entitlements_data = if let Some(section) = file.section_by_name("__entitlements") {
143 let segment_name = section
144 .segment_name_bytes()
145 .context("failed reading segment name")?;
146 if segment_name != Some(b"__TEXT") {
147 warn!("__entitlements was not in __TEXT segment");
148 }
149 let data = section.data().context("failed reading section contents")?;
150 Some(data.to_vec())
151 } else {
152 None
153 };
154
155 Ok(Self {
158 path: path.to_owned(),
159 arch: file.architecture(),
160 name,
161 versions,
162 info_plist_data,
163 entitlements_data,
164 signed,
165 gui_like,
166 })
167 }
168
169 fn version(&self) -> BuildVersion {
170 self.versions.first().copied().unwrap_or_default()
171 }
172
173 pub fn platform(&self) -> Platform {
174 self.version().platform
175 }
176
177 pub fn minos(&self) -> OSVersion {
178 self.version().minos
179 }
180
181 pub fn sdk(&self) -> OSVersion {
182 self.version().sdk
183 }
184
185 #[expect(dead_code)]
186 pub(crate) fn needs_info_plist(&self) -> bool {
187 !matches!(
189 self.version().platform,
190 Platform::MACOS
191 | Platform::IOSSIMULATOR
192 | Platform::TVOSSIMULATOR
193 | Platform::WATCHOSSIMULATOR
194 | Platform::VISIONOSSIMULATOR
195 )
196 }
197}
198
199#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
201struct BuildVersion {
202 platform: Platform,
203 minos: OSVersion,
204 sdk: OSVersion,
205}
206
207impl BuildVersion {
208 fn from_load_command<E: Endian>(
211 variant: LoadCommandVariant<'_, E>,
212 cputype: u32,
213 endianness: E,
214 ) -> Option<Self> {
215 match variant {
216 LoadCommandVariant::BuildVersion(version) => Some(BuildVersion {
217 platform: Platform(version.platform.get(endianness)),
218 minos: OSVersion::from_packed(version.minos.get(endianness)),
219 sdk: OSVersion::from_packed(version.sdk.get(endianness)),
220 }),
221 LoadCommandVariant::VersionMin(version) => Some(BuildVersion {
222 platform: match version.cmd.get(endianness) {
223 macho::LC_VERSION_MIN_MACOSX => Platform::MACOS,
224 macho::LC_VERSION_MIN_IPHONEOS => {
225 if matches!(cputype, macho::CPU_TYPE_X86_64 | macho::CPU_TYPE_X86) {
226 Platform::IOSSIMULATOR } else {
228 Platform::IOS
229 }
230 }
231 macho::LC_VERSION_MIN_TVOS => {
232 if cputype == macho::CPU_TYPE_X86_64 {
233 Platform::TVOSSIMULATOR } else {
235 Platform::TVOS
236 }
237 }
238 macho::LC_VERSION_MIN_WATCHOS => {
239 if matches!(cputype, macho::CPU_TYPE_X86_64 | macho::CPU_TYPE_X86) {
240 Platform::WATCHOSSIMULATOR } else {
242 Platform::WATCHOS
243 }
244 }
245 _ => unreachable!(),
246 },
247 minos: OSVersion::from_packed(version.version.get(endianness)),
248 sdk: OSVersion::from_packed(version.sdk.get(endianness)),
249 }),
250 _ => None,
251 }
252 }
253}
254
255#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
256pub struct Platform(u32);
257
258impl Platform {
259 pub const MACOS: Self = Self(macho::PLATFORM_MACOS);
260 pub const IOS: Self = Self(macho::PLATFORM_IOS);
261 pub const TVOS: Self = Self(macho::PLATFORM_TVOS);
262 pub const WATCHOS: Self = Self(macho::PLATFORM_WATCHOS);
263 pub const BRIDGEOS: Self = Self(macho::PLATFORM_BRIDGEOS);
264 pub const MACCATALYST: Self = Self(macho::PLATFORM_MACCATALYST);
265 pub const IOSSIMULATOR: Self = Self(macho::PLATFORM_IOSSIMULATOR);
266 pub const TVOSSIMULATOR: Self = Self(macho::PLATFORM_TVOSSIMULATOR);
267 pub const WATCHOSSIMULATOR: Self = Self(macho::PLATFORM_WATCHOSSIMULATOR);
268 pub const DRIVERKIT: Self = Self(macho::PLATFORM_DRIVERKIT);
269 pub const VISIONOS: Self = Self(macho::PLATFORM_XROS);
270 pub const VISIONOSSIMULATOR: Self = Self(macho::PLATFORM_XROSSIMULATOR);
271
272 pub fn is_simulator(self) -> bool {
273 matches!(
274 self,
275 Self::IOSSIMULATOR
276 | Self::TVOSSIMULATOR
277 | Self::WATCHOSSIMULATOR
278 | Self::VISIONOSSIMULATOR
279 )
280 }
281}
282
283fn contains(haystack: &[u8], needle: &[u8]) -> bool {
284 haystack.windows(needle.len()).any(|w| w == needle)
285}