Skip to main content

macho_objc/
lib.rs

1#![deny(missing_docs)]
2//! Objective-C runtime metadata parsing.
3//!
4//! Depend on this crate directly for ObjC metadata without the `macho` façade:
5//! parse a [`macho_core::MachoFile`] with [`parse_objc_metadata`] or borrow a
6//! raw byte source with [`parse_objc_metadata_from_source`].
7
8extern crate self as objc;
9
10pub use macho_core::{format, model};
11/// The error module.
12pub mod error;
13pub(crate) use error::Error;
14pub use error::{ObjcError, ObjcErrorKind, Result};
15
16/// The category module.
17pub mod category;
18/// The class module.
19pub mod class;
20pub mod compat;
21/// The encoding module.
22pub mod encoding;
23/// Bounded Objective-C method-implementation traversal.
24pub mod imp;
25/// The ivar module.
26pub mod ivar;
27/// The method module.
28pub mod method;
29/// The property module.
30pub mod property;
31/// The protocol module.
32pub mod protocol;
33/// The resolve module.
34pub mod resolve;
35#[cfg(feature = "strict-rtti")]
36pub mod strict;
37/// The types module.
38pub mod types;
39
40pub use imp::{
41    ObjCMethodImp, ObjCMethodKind, ObjCMethodRecord, ObjCMethodRecordEncoding,
42    ObjCMethodRecordProvenance, ObjCRelativeSelectorEncoding, fold_method_imps,
43    fold_method_imps_from_source, fold_method_records, fold_method_records_from_source,
44};
45pub use resolve::ObjCPointerProvenance;
46pub use types::{ObjCCategory, ObjCClass, ObjCIvar, ObjCMethod, ObjCProperty, ObjCProtocol};
47
48use crate::model::ext::MachoExt;
49use crate::model::macho_file::MachoFile;
50use resolve::ObjCResolver;
51
52/// Parsed ObjC metadata from a Mach-O binary.
53pub struct ObjCMetadata {
54    /// The classes field.
55    pub classes: Vec<ObjCClass>,
56    /// The categories field.
57    pub categories: Vec<ObjCCategory>,
58    /// The protocols field.
59    pub protocols: Vec<ObjCProtocol>,
60}
61
62/// Runtime-list source for one Objective-C metadata observation.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ObjCRecordKind {
65    /// `__objc_classlist` entry.
66    Class,
67    /// `__objc_catlist` entry.
68    Category,
69    /// `__objc_protolist` entry.
70    Protocol,
71}
72
73/// Lossless accounting for one runtime-list entry.
74#[derive(Debug, Clone)]
75pub struct ObjCRecordObservation {
76    /// Runtime-list source.
77    pub kind: ObjCRecordKind,
78    /// Zero-based list ordinal.
79    pub ordinal: usize,
80    /// File offset of the pointer-list entry.
81    pub pointer_file_offset: u64,
82    /// Exact on-disk mechanism that supplied the runtime pointer.
83    pub pointer_provenance: ObjCPointerProvenance,
84    /// Resolved runtime object address, when readable.
85    pub runtime_address: Option<u64>,
86    /// Parsed entity name, when the record was decoded.
87    pub parsed_name: Option<String>,
88    /// Typed parser failure rendered for diagnostics.
89    pub error: Option<String>,
90    /// Bounded raw runtime record bytes, or pointer bytes when unresolved.
91    pub raw: Vec<u8>,
92}
93
94/// Parsed metadata plus a conservation ledger for every runtime-list entry.
95#[derive(Debug)]
96pub struct ObjCMetadataScan {
97    /// Successfully decoded semantic metadata.
98    pub metadata: ObjCMetadata,
99    /// Every class/category/protocol list entry, including malformed entries.
100    pub observations: Vec<ObjCRecordObservation>,
101}
102
103impl<'data> MachoExt<'data> for ObjCMetadata {
104    type Error = ObjcError;
105
106    fn parse<'mf>(macho: &'mf MachoFile<'data>) -> Result<Self>
107    where
108        'data: 'mf,
109    {
110        parse_objc_metadata(macho)
111    }
112}
113
114/// Performs parse_objc_metadata.
115pub fn parse_objc_metadata(macho: &MachoFile<'_>) -> Result<ObjCMetadata> {
116    Ok(scan_objc_metadata(macho)?.metadata)
117}
118
119/// Parse Objective-C metadata from one borrowed thin Mach-O byte source.
120///
121/// The source is not copied and may be a byte slice, vector, or caller-owned
122/// read-only memory map. Universal binaries are rejected because selecting an
123/// architecture is a caller decision; parse those with [`macho_core::parse`]
124/// and pass the selected image to [`parse_objc_metadata`].
125pub fn parse_objc_metadata_from_source<S>(source: &S) -> Result<ObjCMetadata>
126where
127    S: AsRef<[u8]> + ?Sized,
128{
129    let macho = parse_source(source)?;
130    parse_objc_metadata(&macho)
131}
132
133/// Scans Objective-C runtime lists without silently dropping malformed records.
134pub fn scan_objc_metadata(macho: &MachoFile<'_>) -> Result<ObjCMetadataScan> {
135    if !macho.is_64bit() {
136        return Err(Error::unsupported(
137            "ObjC metadata parsing is only supported for 64-bit binaries",
138        ));
139    }
140    let resolver = ObjCResolver::new(macho)?;
141    let mut observations = Vec::new();
142    let mut classes = Vec::new();
143    let mut categories = Vec::new();
144    let mut protocols = Vec::new();
145
146    for (kind, section_name) in [
147        (ObjCRecordKind::Class, "__objc_classlist"),
148        (ObjCRecordKind::Category, "__objc_catlist"),
149        (ObjCRecordKind::Protocol, "__objc_protolist"),
150    ] {
151        if !macho
152            .all_sections()
153            .any(|section| section.section_name() == section_name)
154        {
155            continue;
156        }
157        let offsets = parse_pointer_list(macho, section_name)?;
158        for (ordinal, pointer_file_offset) in offsets.into_iter().enumerate() {
159            let pointer_raw = macho
160                .bytes()
161                .get(pointer_file_offset as usize..pointer_file_offset as usize + 8)
162                .unwrap_or_default()
163                .to_vec();
164            let runtime_address = match resolver.read_pointer_at_offset(pointer_file_offset) {
165                Ok(Some(address)) => address,
166                Ok(None) => {
167                    observations.push(ObjCRecordObservation {
168                        kind,
169                        ordinal,
170                        pointer_file_offset,
171                        pointer_provenance: resolver
172                            .pointer_provenance_at_offset(pointer_file_offset),
173                        runtime_address: None,
174                        parsed_name: None,
175                        error: Some("runtime pointer is null or unresolved".to_owned()),
176                        raw: pointer_raw,
177                    });
178                    continue;
179                }
180                Err(error) => {
181                    observations.push(ObjCRecordObservation {
182                        kind,
183                        ordinal,
184                        pointer_file_offset,
185                        pointer_provenance: resolver
186                            .pointer_provenance_at_offset(pointer_file_offset),
187                        runtime_address: None,
188                        parsed_name: None,
189                        error: Some(error.to_string()),
190                        raw: pointer_raw,
191                    });
192                    continue;
193                }
194            };
195            let raw_size = match kind {
196                ObjCRecordKind::Class => 40,
197                ObjCRecordKind::Category => 48,
198                ObjCRecordKind::Protocol => 72,
199            };
200            let raw = macho
201                .read_bytes_at_va(runtime_address, raw_size)
202                .map(ToOwned::to_owned)
203                .unwrap_or(pointer_raw);
204            let parsed = match kind {
205                ObjCRecordKind::Class => {
206                    class::parse_class(&resolver, runtime_address).map(|value| {
207                        let name = value.name.clone();
208                        if !value.is_meta {
209                            classes.push(value);
210                        }
211                        name
212                    })
213                }
214                ObjCRecordKind::Category => category::parse_category(&resolver, runtime_address)
215                    .map(|value| {
216                        let name = value.name.clone();
217                        categories.push(value);
218                        name
219                    }),
220                ObjCRecordKind::Protocol => protocol::parse_protocol(&resolver, runtime_address)
221                    .map(|value| {
222                        let name = value.name.clone();
223                        protocols.push(value);
224                        name
225                    }),
226            };
227            observations.push(match parsed {
228                Ok(parsed_name) => ObjCRecordObservation {
229                    kind,
230                    ordinal,
231                    pointer_file_offset,
232                    pointer_provenance: resolver.pointer_provenance_at_offset(pointer_file_offset),
233                    runtime_address: Some(runtime_address.0),
234                    parsed_name: Some(parsed_name),
235                    error: None,
236                    raw,
237                },
238                Err(error) => ObjCRecordObservation {
239                    kind,
240                    ordinal,
241                    pointer_file_offset,
242                    pointer_provenance: resolver.pointer_provenance_at_offset(pointer_file_offset),
243                    runtime_address: Some(runtime_address.0),
244                    parsed_name: None,
245                    error: Some(error.to_string()),
246                    raw,
247                },
248            });
249        }
250    }
251
252    Ok(ObjCMetadataScan {
253        metadata: ObjCMetadata {
254            classes,
255            categories,
256            protocols,
257        },
258        observations,
259    })
260}
261
262/// Scan Objective-C metadata from one borrowed thin Mach-O byte source.
263///
264/// This is the lossless-observation counterpart to
265/// [`parse_objc_metadata_from_source`] and has the same borrowing and universal
266/// binary behavior.
267pub fn scan_objc_metadata_from_source<S>(source: &S) -> Result<ObjCMetadataScan>
268where
269    S: AsRef<[u8]> + ?Sized,
270{
271    let macho = parse_source(source)?;
272    scan_objc_metadata(&macho)
273}
274
275fn parse_source<'data, S>(source: &'data S) -> Result<MachoFile<'data>>
276where
277    S: AsRef<[u8]> + ?Sized,
278{
279    match macho_core::parse(source.as_ref())? {
280        macho_core::model::container::MachoContainer::Thin(macho) => Ok(macho),
281        macho_core::model::container::MachoContainer::Fat(_) => Err(Error::unsupported(
282            "borrowed source contains a universal Mach-O; select an architecture explicitly",
283        )),
284    }
285}
286
287/// Find a section by name across all segments and return file offsets
288/// for each pointer-sized entry.
289fn parse_pointer_list(macho: &MachoFile<'_>, sect_name: &str) -> Result<Vec<u64>> {
290    // Search in __DATA_CONST first, then __DATA
291    let section = macho
292        .all_sections()
293        .find(|s| s.section_name() == sect_name)
294        .ok_or_else(|| Error::format(format!("section {sect_name} not found")))?;
295
296    let offset = section.offset().0;
297    let size = section.size();
298    if size % 8 != 0 {
299        return Err(Error::format(format!(
300            "section {sect_name} size {size:#x} is not pointer-aligned"
301        )));
302    }
303    let end = offset
304        .checked_add(size)
305        .ok_or_else(|| Error::address(format!("section {sect_name} file range overflows")))?;
306    if end > macho.file_size() as u64 {
307        return Err(Error::bounds(offset, size, macho.file_size() as u64));
308    }
309    let count = (size / 8) as usize; // each entry is a pointer (8 bytes for 64-bit)
310
311    let mut offsets = Vec::with_capacity(count.min(100_000));
312    for i in 0..count {
313        offsets.push(offset + i as u64 * 8);
314    }
315
316    Ok(offsets)
317}
318
319impl std::fmt::Debug for ObjCMetadata {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.debug_struct("ObjCMetadata")
322            .field("classes", &self.classes.len())
323            .field("categories", &self.categories.len())
324            .field("protocols", &self.protocols.len())
325            .finish()
326    }
327}