1#![deny(missing_docs)]
2extern crate self as objc;
9
10pub use macho_core::{format, model};
11pub mod error;
13pub(crate) use error::Error;
14pub use error::{ObjcError, ObjcErrorKind, Result};
15
16pub mod category;
18pub mod class;
20pub mod compat;
21pub mod encoding;
23pub mod imp;
25pub mod ivar;
27pub mod method;
29pub mod property;
31pub mod protocol;
33pub mod resolve;
35#[cfg(feature = "strict-rtti")]
36pub mod strict;
37pub 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
52pub struct ObjCMetadata {
54 pub classes: Vec<ObjCClass>,
56 pub categories: Vec<ObjCCategory>,
58 pub protocols: Vec<ObjCProtocol>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ObjCRecordKind {
65 Class,
67 Category,
69 Protocol,
71}
72
73#[derive(Debug, Clone)]
75pub struct ObjCRecordObservation {
76 pub kind: ObjCRecordKind,
78 pub ordinal: usize,
80 pub pointer_file_offset: u64,
82 pub pointer_provenance: ObjCPointerProvenance,
84 pub runtime_address: Option<u64>,
86 pub parsed_name: Option<String>,
88 pub error: Option<String>,
90 pub raw: Vec<u8>,
92}
93
94#[derive(Debug)]
96pub struct ObjCMetadataScan {
97 pub metadata: ObjCMetadata,
99 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
114pub fn parse_objc_metadata(macho: &MachoFile<'_>) -> Result<ObjCMetadata> {
116 Ok(scan_objc_metadata(macho)?.metadata)
117}
118
119pub 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
133pub 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
262pub 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
287fn parse_pointer_list(macho: &MachoFile<'_>, sect_name: &str) -> Result<Vec<u64>> {
290 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; 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}