#![deny(missing_docs)]
extern crate self as objc;
pub use macho_core::{format, model};
pub mod error;
pub(crate) use error::Error;
pub use error::{ObjcError, ObjcErrorKind, Result};
pub mod category;
pub mod class;
pub mod compat;
pub mod encoding;
pub mod imp;
pub mod ivar;
pub mod method;
pub mod property;
pub mod protocol;
pub mod resolve;
#[cfg(feature = "strict-rtti")]
pub mod strict;
pub mod types;
pub use imp::{
ObjCMethodImp, ObjCMethodKind, ObjCMethodRecord, ObjCMethodRecordEncoding,
ObjCMethodRecordProvenance, ObjCRelativeSelectorEncoding, fold_method_imps,
fold_method_imps_from_source, fold_method_records, fold_method_records_from_source,
};
pub use resolve::ObjCPointerProvenance;
pub use types::{ObjCCategory, ObjCClass, ObjCIvar, ObjCMethod, ObjCProperty, ObjCProtocol};
use crate::model::ext::MachoExt;
use crate::model::macho_file::MachoFile;
use resolve::ObjCResolver;
pub struct ObjCMetadata {
pub classes: Vec<ObjCClass>,
pub categories: Vec<ObjCCategory>,
pub protocols: Vec<ObjCProtocol>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjCRecordKind {
Class,
Category,
Protocol,
}
#[derive(Debug, Clone)]
pub struct ObjCRecordObservation {
pub kind: ObjCRecordKind,
pub ordinal: usize,
pub pointer_file_offset: u64,
pub pointer_provenance: ObjCPointerProvenance,
pub runtime_address: Option<u64>,
pub parsed_name: Option<String>,
pub error: Option<String>,
pub raw: Vec<u8>,
}
#[derive(Debug)]
pub struct ObjCMetadataScan {
pub metadata: ObjCMetadata,
pub observations: Vec<ObjCRecordObservation>,
}
impl<'data> MachoExt<'data> for ObjCMetadata {
type Error = ObjcError;
fn parse<'mf>(macho: &'mf MachoFile<'data>) -> Result<Self>
where
'data: 'mf,
{
parse_objc_metadata(macho)
}
}
pub fn parse_objc_metadata(macho: &MachoFile<'_>) -> Result<ObjCMetadata> {
Ok(scan_objc_metadata(macho)?.metadata)
}
pub fn parse_objc_metadata_from_source<S>(source: &S) -> Result<ObjCMetadata>
where
S: AsRef<[u8]> + ?Sized,
{
let macho = parse_source(source)?;
parse_objc_metadata(&macho)
}
pub fn scan_objc_metadata(macho: &MachoFile<'_>) -> Result<ObjCMetadataScan> {
if !macho.is_64bit() {
return Err(Error::unsupported(
"ObjC metadata parsing is only supported for 64-bit binaries",
));
}
let resolver = ObjCResolver::new(macho)?;
let mut observations = Vec::new();
let mut classes = Vec::new();
let mut categories = Vec::new();
let mut protocols = Vec::new();
for (kind, section_name) in [
(ObjCRecordKind::Class, "__objc_classlist"),
(ObjCRecordKind::Category, "__objc_catlist"),
(ObjCRecordKind::Protocol, "__objc_protolist"),
] {
if !macho
.all_sections()
.any(|section| section.section_name() == section_name)
{
continue;
}
let offsets = parse_pointer_list(macho, section_name)?;
for (ordinal, pointer_file_offset) in offsets.into_iter().enumerate() {
let pointer_raw = macho
.bytes()
.get(pointer_file_offset as usize..pointer_file_offset as usize + 8)
.unwrap_or_default()
.to_vec();
let runtime_address = match resolver.read_pointer_at_offset(pointer_file_offset) {
Ok(Some(address)) => address,
Ok(None) => {
observations.push(ObjCRecordObservation {
kind,
ordinal,
pointer_file_offset,
pointer_provenance: resolver
.pointer_provenance_at_offset(pointer_file_offset),
runtime_address: None,
parsed_name: None,
error: Some("runtime pointer is null or unresolved".to_owned()),
raw: pointer_raw,
});
continue;
}
Err(error) => {
observations.push(ObjCRecordObservation {
kind,
ordinal,
pointer_file_offset,
pointer_provenance: resolver
.pointer_provenance_at_offset(pointer_file_offset),
runtime_address: None,
parsed_name: None,
error: Some(error.to_string()),
raw: pointer_raw,
});
continue;
}
};
let raw_size = match kind {
ObjCRecordKind::Class => 40,
ObjCRecordKind::Category => 48,
ObjCRecordKind::Protocol => 72,
};
let raw = macho
.read_bytes_at_va(runtime_address, raw_size)
.map(ToOwned::to_owned)
.unwrap_or(pointer_raw);
let parsed = match kind {
ObjCRecordKind::Class => {
class::parse_class(&resolver, runtime_address).map(|value| {
let name = value.name.clone();
if !value.is_meta {
classes.push(value);
}
name
})
}
ObjCRecordKind::Category => category::parse_category(&resolver, runtime_address)
.map(|value| {
let name = value.name.clone();
categories.push(value);
name
}),
ObjCRecordKind::Protocol => protocol::parse_protocol(&resolver, runtime_address)
.map(|value| {
let name = value.name.clone();
protocols.push(value);
name
}),
};
observations.push(match parsed {
Ok(parsed_name) => ObjCRecordObservation {
kind,
ordinal,
pointer_file_offset,
pointer_provenance: resolver.pointer_provenance_at_offset(pointer_file_offset),
runtime_address: Some(runtime_address.0),
parsed_name: Some(parsed_name),
error: None,
raw,
},
Err(error) => ObjCRecordObservation {
kind,
ordinal,
pointer_file_offset,
pointer_provenance: resolver.pointer_provenance_at_offset(pointer_file_offset),
runtime_address: Some(runtime_address.0),
parsed_name: None,
error: Some(error.to_string()),
raw,
},
});
}
}
Ok(ObjCMetadataScan {
metadata: ObjCMetadata {
classes,
categories,
protocols,
},
observations,
})
}
pub fn scan_objc_metadata_from_source<S>(source: &S) -> Result<ObjCMetadataScan>
where
S: AsRef<[u8]> + ?Sized,
{
let macho = parse_source(source)?;
scan_objc_metadata(&macho)
}
fn parse_source<'data, S>(source: &'data S) -> Result<MachoFile<'data>>
where
S: AsRef<[u8]> + ?Sized,
{
match macho_core::parse(source.as_ref())? {
macho_core::model::container::MachoContainer::Thin(macho) => Ok(macho),
macho_core::model::container::MachoContainer::Fat(_) => Err(Error::unsupported(
"borrowed source contains a universal Mach-O; select an architecture explicitly",
)),
}
}
fn parse_pointer_list(macho: &MachoFile<'_>, sect_name: &str) -> Result<Vec<u64>> {
let section = macho
.all_sections()
.find(|s| s.section_name() == sect_name)
.ok_or_else(|| Error::format(format!("section {sect_name} not found")))?;
let offset = section.offset().0;
let size = section.size();
if size % 8 != 0 {
return Err(Error::format(format!(
"section {sect_name} size {size:#x} is not pointer-aligned"
)));
}
let end = offset
.checked_add(size)
.ok_or_else(|| Error::address(format!("section {sect_name} file range overflows")))?;
if end > macho.file_size() as u64 {
return Err(Error::bounds(offset, size, macho.file_size() as u64));
}
let count = (size / 8) as usize;
let mut offsets = Vec::with_capacity(count.min(100_000));
for i in 0..count {
offsets.push(offset + i as u64 * 8);
}
Ok(offsets)
}
impl std::fmt::Debug for ObjCMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ObjCMetadata")
.field("classes", &self.classes.len())
.field("categories", &self.categories.len())
.field("protocols", &self.protocols.len())
.finish()
}
}