Skip to main content

cargo_apple_runner/
binary.rs

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/// Extracted information about a binary.
12#[derive(Debug)]
13pub struct Binary {
14    /// The path to the binary.
15    pub path: PathBuf,
16    /// The architecture of the binary.
17    pub arch: Architecture,
18    /// `LC_ID_DYLIB`.
19    pub name: Option<Vec<u8>>,
20    /// Usually there's only one here, but we expect to see two of these in a
21    /// zippered binary (a binary that supports both macOS and Mac Catalyst).
22    versions: Vec<BuildVersion>,
23    /// Embedded `__TEXT,__info_plist` contents, if any.
24    ///
25    /// See the `embed_plist` crate for how to insert this.
26    pub info_plist_data: Option<Vec<u8>>,
27    /// Embedded `__TEXT,__entitlements` contents, if any.
28    ///
29    /// See the `embed_entitlements` crate for how to insert this.
30    pub entitlements_data: Option<Vec<u8>>,
31    /// Whether the binary is already (likely ad-hoc) signed.
32    pub signed: bool,
33    /// Whether the application should be launched instead of simply spawned.
34    ///
35    /// This is set if:
36    /// - The binary links AppKit, UIKit, WatchKit or similar UI frameworks.
37    /// - TODO: Others? Maybe if linking `UIApplicationMain` or `NSApp`? Or
38    ///   maybe there's further libraries that expect this?
39    pub gui_like: bool,
40    // TODO: Support manganis __ASSET__?
41    // Potentially also special assets like:
42    // - asset catalogs (`actool --version --output-format xml1`)
43    // - Interface builder (`ictool --version --output-format xml1`)
44    // And possibly plist/entitlement information from this too? Though also
45    // maybe nice to keep that separate.
46}
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                    // These are all used for GUI development, and usually
101                    // need the application to be launched to work.
102                    //
103                    // (AppKit is perhaps an outlier here, it might not
104                    // require launching? Yet unsure.)
105                    if contains(s, b"AppKit")
106                        || contains(s, b"Cocoa") // Re-exports AppKit
107                        || 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        // TODO: Read `__ASSETS__` that manganis inserts?
156
157        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        // TODO: Probably? Needs to be tested
188        !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/// Simplified LC_BUILD_VERSION.
200#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
201struct BuildVersion {
202    platform: Platform,
203    minos: OSVersion,
204    sdk: OSVersion,
205}
206
207impl BuildVersion {
208    /// Checks here are the same as the ones the loader does:
209    /// <https://github.com/apple-oss-distributions/dyld/blob/dyld-1340/mach_o/Header.cpp#L1113-L1176>
210    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 // old sim binary
227                        } else {
228                            Platform::IOS
229                        }
230                    }
231                    macho::LC_VERSION_MIN_TVOS => {
232                        if cputype == macho::CPU_TYPE_X86_64 {
233                            Platform::TVOSSIMULATOR // old sim binary
234                        } 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 // old sim binary
241                        } 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}