Skip to main content

macho_objc/
imp.rs

1use macho_core::format::io::pod::{
2    self, RawClassRoT64, RawMethodListHeader, RawMethodT, RawRelativeMethodT,
3};
4use macho_core::model::addr::{ThinFileOffset, Va};
5use macho_core::model::macho_file::MachoFile;
6use macho_core::model::section::Section;
7
8use crate::error::{Error, Result};
9use crate::resolve::{ObjCPointerProvenance, ObjCResolver};
10use crate::types::{
11    METHOD_LIST_ENTSIZE_MASK, METHOD_LIST_USES_DIRECT_SELECTOR_OFFSETS,
12    METHOD_LIST_USES_RELATIVE_OFFSETS,
13};
14
15/// Objective-C method dispatch kind.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ObjCMethodKind {
18    /// Instance method (`-`).
19    Instance,
20    /// Class method (`+`).
21    Class,
22}
23
24/// Storage and decoding provenance for one Objective-C method-list entry.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ObjCMethodRecordProvenance {
27    /// Exact thin-file offset containing the method record.
28    pub record_file_offset: ThinFileOffset,
29    /// Runtime address corresponding to the first byte of the record.
30    pub record_va: Va,
31    /// Encoded record size selected by the method-list header.
32    pub record_size: u64,
33    /// Absolute-pointer or relative-field decoding details.
34    pub encoding: ObjCMethodRecordEncoding,
35}
36
37/// Pointer or relative-field representation used by one method record.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ObjCMethodRecordEncoding {
40    /// Three pointer-sized fields, each retaining its fixup provenance.
41    Absolute {
42        /// Selector-name pointer provenance.
43        selector_pointer: ObjCPointerProvenance,
44        /// Type-encoding pointer provenance.
45        type_encoding_pointer: ObjCPointerProvenance,
46        /// Implementation pointer provenance.
47        implementation_pointer: ObjCPointerProvenance,
48    },
49    /// Three signed 32-bit offsets relative to their own field addresses.
50    Relative {
51        /// Runtime address used as the selector-offset basis.
52        selector_field_va: Va,
53        /// Runtime address used as the type-encoding-offset basis.
54        type_encoding_field_va: Va,
55        /// Runtime address used as the implementation-offset basis.
56        implementation_field_va: Va,
57        /// Direct-string or indirect-selector-reference representation.
58        selector: ObjCRelativeSelectorEncoding,
59    },
60}
61
62/// Selector representation selected by a relative method-list header.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ObjCRelativeSelectorEncoding {
65    /// The relative offset reaches the selector string directly.
66    DirectString {
67        /// Exact file offset of the selector cstring.
68        selector_string_file_offset: ThinFileOffset,
69    },
70    /// The relative offset reaches a pointer to the selector string.
71    IndirectReference {
72        /// Exact file offset of the selector-reference pointer.
73        selector_reference_file_offset: ThinFileOffset,
74        /// Fixup provenance of that pointer.
75        selector_reference_pointer: ObjCPointerProvenance,
76    },
77}
78
79/// One parsed Objective-C method record with exact storage provenance.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ObjCMethodRecord {
82    /// Runtime address of the owning class, metaclass, or category record.
83    pub owner_va: Va,
84    /// Owning class name.
85    pub class_name: String,
86    /// Owning category name, when the method comes from a category.
87    pub category_name: Option<String>,
88    /// Selector spelling.
89    pub method_name: String,
90    /// Raw Objective-C type encoding.
91    pub type_encoding: String,
92    /// Instance or class dispatch kind.
93    pub kind: ObjCMethodKind,
94    /// Resolved implementation virtual address.
95    pub imp: Va,
96    /// Exact record storage and pointer/relative decoding provenance.
97    pub provenance: ObjCMethodRecordProvenance,
98}
99
100/// One parsed Objective-C implementation entry.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ObjCMethodImp {
103    /// Owning class name.
104    pub class_name: String,
105    /// Owning category name, when the method comes from a category.
106    pub category_name: Option<String>,
107    /// Selector spelling.
108    pub method_name: String,
109    /// Instance or class dispatch kind.
110    pub kind: ObjCMethodKind,
111    /// Implementation virtual address.
112    pub imp: Va,
113}
114
115/// Fold class and category method implementations into caller-owned state.
116///
117/// Runtime pointer lists and method lists are traversed directly; no complete
118/// Objective-C graph or method vector is materialized. The state is returned
119/// only after every nested pointer, name, method entry, and implementation
120/// address parses successfully. A malformed suffix therefore drops the partial
121/// accumulator and returns only the typed parse error.
122pub fn fold_method_imps<State>(
123    macho: &MachoFile<'_>,
124    state: State,
125    mut folder: impl FnMut(&mut State, ObjCMethodImp) -> Result<()>,
126) -> Result<State> {
127    fold_method_records(macho, state, |state, record| {
128        folder(
129            state,
130            ObjCMethodImp {
131                class_name: record.class_name,
132                category_name: record.category_name,
133                method_name: record.method_name,
134                kind: record.kind,
135                imp: record.imp,
136            },
137        )
138    })
139}
140
141/// Fold strict method records with exact storage and decoding provenance.
142pub fn fold_method_records<State>(
143    macho: &MachoFile<'_>,
144    mut state: State,
145    mut folder: impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
146) -> Result<State> {
147    if !macho.is_64bit() {
148        return Err(Error::unsupported(
149            "ObjC method implementation parsing is only supported for 64-bit binaries",
150        ));
151    }
152    let resolver = ObjCResolver::new(macho)?;
153    let (class_list, category_list) = runtime_lists(macho);
154    if let Some(section) = class_list {
155        fold_pointer_list(macho, &resolver, section, |class_va| {
156            fold_class(&resolver, class_va, &mut state, &mut folder)
157        })?;
158    }
159    if let Some(section) = category_list {
160        fold_pointer_list(macho, &resolver, section, |category_va| {
161            fold_category(&resolver, category_va, &mut state, &mut folder)
162        })?;
163    }
164    Ok(state)
165}
166
167/// Fold method implementations from one borrowed thin Mach-O byte source.
168///
169/// The source is not copied and may be a byte slice, vector, or caller-owned
170/// read-only memory map. Universal binaries require explicit architecture
171/// selection through [`macho_core::parse`] and [`fold_method_imps`].
172pub fn fold_method_imps_from_source<S, State>(
173    source: &S,
174    state: State,
175    folder: impl FnMut(&mut State, ObjCMethodImp) -> Result<()>,
176) -> Result<State>
177where
178    S: AsRef<[u8]> + ?Sized,
179{
180    let macho = crate::parse_source(source)?;
181    fold_method_imps(&macho, state, folder)
182}
183
184/// Fold strict method records from one borrowed thin Mach-O byte source.
185pub fn fold_method_records_from_source<S, State>(
186    source: &S,
187    state: State,
188    folder: impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
189) -> Result<State>
190where
191    S: AsRef<[u8]> + ?Sized,
192{
193    let macho = crate::parse_source(source)?;
194    fold_method_records(&macho, state, folder)
195}
196
197fn runtime_lists<'macho>(
198    macho: &'macho MachoFile<'_>,
199) -> (Option<&'macho Section>, Option<&'macho Section>) {
200    let mut classes = None;
201    let mut categories = None;
202    for section in macho.all_sections() {
203        if section.section_name() == "__objc_classlist" && classes.is_none() {
204            classes = Some(section);
205        } else if section.section_name() == "__objc_catlist" && categories.is_none() {
206            categories = Some(section);
207        }
208    }
209    (classes, categories)
210}
211
212fn fold_pointer_list(
213    macho: &MachoFile<'_>,
214    resolver: &ObjCResolver<'_>,
215    section: &Section,
216    mut visitor: impl FnMut(Va) -> Result<()>,
217) -> Result<()> {
218    let offset = section.offset().0;
219    let size = section.size();
220    if size % 8 != 0 {
221        return Err(Error::format(format!(
222            "section {} size {size:#x} is not pointer-aligned",
223            section.section_name()
224        )));
225    }
226    let end = offset
227        .checked_add(size)
228        .ok_or_else(|| Error::address("Objective-C pointer-list range overflows"))?;
229    if end > macho.file_size() as u64 {
230        return Err(Error::bounds(offset, size, macho.file_size() as u64));
231    }
232    for ordinal in 0..size / 8 {
233        let pointer_offset = offset
234            .checked_add(ordinal * 8)
235            .ok_or_else(|| Error::address("Objective-C pointer offset overflows"))?;
236        let runtime_va = resolver
237            .read_pointer_at_offset(pointer_offset)?
238            .filter(|va| va.0 != 0)
239            .ok_or_else(|| {
240                Error::format(format!(
241                    "{}[{ordinal}] has a null or unresolved runtime pointer",
242                    section.section_name()
243                ))
244            })?;
245        visitor(runtime_va)?;
246    }
247    Ok(())
248}
249
250fn fold_class<State>(
251    resolver: &ObjCResolver<'_>,
252    class_va: Va,
253    state: &mut State,
254    folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
255) -> Result<()> {
256    let class_offset = resolver.va_to_offset(class_va)?.0;
257    let data_va = required_pointer(
258        resolver,
259        checked_field_offset(class_offset, 32, "class data")?,
260        "class data",
261    )?;
262    let ro_offset = resolver
263        .va_to_offset(Va(data_va.0 & crate::types::CLASS_DATA_POINTER_MASK))?
264        .0;
265    let _: RawClassRoT64 = pod::read_pod(resolver.macho().bytes(), ro_offset as usize)?;
266    let class_name = required_cstring_pointer(
267        resolver,
268        checked_field_offset(ro_offset, 24, "class name")?,
269        "class name",
270    )?;
271
272    fold_optional_method_list(
273        resolver,
274        checked_field_offset(ro_offset, 32, "class methods")?,
275        class_va,
276        &class_name,
277        None,
278        ObjCMethodKind::Instance,
279        state,
280        folder,
281    )?;
282    if let Some(meta_va) = resolver.read_pointer_at_offset(class_offset)? {
283        let meta_offset = resolver.va_to_offset(meta_va)?.0;
284        let meta_data = required_pointer(
285            resolver,
286            checked_field_offset(meta_offset, 32, "metaclass data")?,
287            "metaclass data",
288        )?;
289        let meta_ro_offset = resolver
290            .va_to_offset(Va(meta_data.0 & crate::types::CLASS_DATA_POINTER_MASK))?
291            .0;
292        let _: RawClassRoT64 = pod::read_pod(resolver.macho().bytes(), meta_ro_offset as usize)?;
293        fold_optional_method_list(
294            resolver,
295            checked_field_offset(meta_ro_offset, 32, "metaclass methods")?,
296            meta_va,
297            &class_name,
298            None,
299            ObjCMethodKind::Class,
300            state,
301            folder,
302        )?;
303    }
304    Ok(())
305}
306
307fn fold_category<State>(
308    resolver: &ObjCResolver<'_>,
309    category_va: Va,
310    state: &mut State,
311    folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
312) -> Result<()> {
313    let offset = resolver.va_to_offset(category_va)?.0;
314    let category_name = required_cstring_pointer(resolver, offset, "category name")?;
315    let class_name =
316        strict_class_ref_name(resolver, checked_field_offset(offset, 8, "category class")?)?;
317    fold_optional_method_list(
318        resolver,
319        checked_field_offset(offset, 16, "category instance methods")?,
320        category_va,
321        &class_name,
322        Some(&category_name),
323        ObjCMethodKind::Instance,
324        state,
325        folder,
326    )?;
327    fold_optional_method_list(
328        resolver,
329        checked_field_offset(offset, 24, "category class methods")?,
330        category_va,
331        &class_name,
332        Some(&category_name),
333        ObjCMethodKind::Class,
334        state,
335        folder,
336    )
337}
338
339#[allow(clippy::too_many_arguments)]
340fn fold_optional_method_list<State>(
341    resolver: &ObjCResolver<'_>,
342    pointer_offset: u64,
343    owner_va: Va,
344    class_name: &str,
345    category_name: Option<&str>,
346    kind: ObjCMethodKind,
347    state: &mut State,
348    folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
349) -> Result<()> {
350    let Some(list_va) = resolver.read_pointer_at_offset(pointer_offset)? else {
351        return Ok(());
352    };
353    if list_va.0 == 0 {
354        return Ok(());
355    }
356    fold_method_list(resolver, list_va, |record| {
357        folder(
358            state,
359            ObjCMethodRecord {
360                owner_va,
361                class_name: class_name.to_owned(),
362                category_name: category_name.map(str::to_owned),
363                method_name: record.method_name,
364                type_encoding: record.type_encoding,
365                kind,
366                imp: record.imp,
367                provenance: record.provenance,
368            },
369        )
370    })
371}
372
373struct ParsedMethodRecord {
374    method_name: String,
375    type_encoding: String,
376    imp: Va,
377    provenance: ObjCMethodRecordProvenance,
378}
379
380fn fold_method_list(
381    resolver: &ObjCResolver<'_>,
382    list_va: Va,
383    mut visitor: impl FnMut(ParsedMethodRecord) -> Result<()>,
384) -> Result<()> {
385    let offset = resolver.va_to_offset(list_va)?.as_usize();
386    let data = resolver.macho().bytes();
387    let endian = resolver.endian();
388    let header: RawMethodListHeader = pod::read_pod(data, offset)?;
389    let flags = endian.interpret_u32(header.entsize_and_flags);
390    let count = endian.interpret_u32(header.count) as usize;
391    let relative = flags & METHOD_LIST_USES_RELATIVE_OFFSETS != 0;
392    let direct_selectors = flags & METHOD_LIST_USES_DIRECT_SELECTOR_OFFSETS != 0;
393    let minimum = if relative {
394        size_of::<RawRelativeMethodT>()
395    } else {
396        size_of::<RawMethodT>()
397    };
398    let encoded_size = (flags & METHOD_LIST_ENTSIZE_MASK) as usize;
399    let entry_size = if encoded_size == 0 {
400        minimum
401    } else {
402        encoded_size
403    };
404    if entry_size < minimum {
405        return Err(Error::format(format!(
406            "method entry size {entry_size} is smaller than {minimum}"
407        )));
408    }
409    let entries_start = offset
410        .checked_add(size_of::<RawMethodListHeader>())
411        .ok_or_else(|| Error::address("method-list header range overflows"))?;
412    let entries_size = count
413        .checked_mul(entry_size)
414        .ok_or_else(|| Error::address("method-list entry range overflows"))?;
415    let entries_end = entries_start
416        .checked_add(entries_size)
417        .ok_or_else(|| Error::address("method-list end overflows"))?;
418    if entries_end > data.len() {
419        return Err(Error::bounds(
420            entries_start as u64,
421            entries_size as u64,
422            data.len() as u64,
423        ));
424    }
425
426    for ordinal in 0..count {
427        let entry_offset = entries_start + ordinal * entry_size;
428        let record = if relative {
429            parse_relative_method(resolver, entry_offset, entry_size, direct_selectors)?
430        } else {
431            parse_absolute_method(resolver, entry_offset, entry_size)?
432        };
433        visitor(record)?;
434    }
435    Ok(())
436}
437
438fn parse_relative_method(
439    resolver: &ObjCResolver<'_>,
440    entry_offset: usize,
441    entry_size: usize,
442    direct_selectors: bool,
443) -> Result<ParsedMethodRecord> {
444    let data = resolver.macho().bytes();
445    let endian = resolver.endian();
446    let raw: RawRelativeMethodT = pod::read_pod(data, entry_offset)?;
447    let name_relative = endian.interpret_i32(raw.name_offset) as isize;
448    let types_relative = endian.interpret_i32(raw.types_offset) as isize;
449    let imp_relative = endian.interpret_i32(raw.imp_offset) as i64;
450    let selector_offset = entry_offset
451        .checked_add_signed(name_relative)
452        .ok_or_else(|| Error::address("relative method selector offset overflows"))?;
453    let (name, selector) = if direct_selectors {
454        (
455            read_cstring_at_file_offset(data, selector_offset)?.to_owned(),
456            ObjCRelativeSelectorEncoding::DirectString {
457                selector_string_file_offset: ThinFileOffset(selector_offset as u64),
458            },
459        )
460    } else {
461        let string_va = resolver
462            .read_pointer_at_offset(selector_offset as u64)?
463            .ok_or_else(|| Error::address("relative method selector reference is unresolved"))?;
464        (
465            resolver.read_cstring(string_va)?.to_owned(),
466            ObjCRelativeSelectorEncoding::IndirectReference {
467                selector_reference_file_offset: ThinFileOffset(selector_offset as u64),
468                selector_reference_pointer: resolver
469                    .pointer_provenance_at_offset(selector_offset as u64),
470            },
471        )
472    };
473    let types_field_offset = entry_offset
474        .checked_add(4)
475        .ok_or_else(|| Error::address("relative method type-encoding field overflows"))?;
476    let types_offset = types_field_offset
477        .checked_add_signed(types_relative)
478        .ok_or_else(|| Error::address("relative method type-encoding offset overflows"))?;
479    let type_encoding = read_cstring_at_file_offset(data, types_offset)?.to_owned();
480    let imp_field_offset = entry_offset
481        .checked_add(8)
482        .ok_or_else(|| Error::address("relative method IMP field overflows"))?;
483    let imp_field_va = resolver
484        .macho()
485        .address_map()
486        .thin_offset_to_va(ThinFileOffset(imp_field_offset as u64))?;
487    let imp = imp_field_va
488        .0
489        .checked_add_signed(imp_relative)
490        .filter(|value| *value != 0)
491        .map(Va)
492        .ok_or_else(|| Error::address("relative method IMP address is invalid"))?;
493    let address_map = resolver.macho().address_map();
494    let record_file_offset = ThinFileOffset(entry_offset as u64);
495    let record_va = address_map.thin_offset_to_va(record_file_offset)?;
496    Ok(ParsedMethodRecord {
497        method_name: name,
498        type_encoding,
499        imp,
500        provenance: ObjCMethodRecordProvenance {
501            record_file_offset,
502            record_va,
503            record_size: entry_size as u64,
504            encoding: ObjCMethodRecordEncoding::Relative {
505                selector_field_va: record_va,
506                type_encoding_field_va: address_map
507                    .thin_offset_to_va(ThinFileOffset(types_field_offset as u64))?,
508                implementation_field_va: imp_field_va,
509                selector,
510            },
511        },
512    })
513}
514
515fn parse_absolute_method(
516    resolver: &ObjCResolver<'_>,
517    entry_offset: usize,
518    entry_size: usize,
519) -> Result<ParsedMethodRecord> {
520    let name = required_cstring_pointer(resolver, entry_offset as u64, "method name")?;
521    let types_offset = checked_field_offset(entry_offset as u64, 8, "method type encoding")?;
522    let type_encoding = required_cstring_pointer(resolver, types_offset, "method type encoding")?;
523    let imp_offset = checked_field_offset(entry_offset as u64, 16, "method IMP")?;
524    let imp = required_pointer(resolver, imp_offset, "method IMP")?;
525    if imp.0 == 0 {
526        return Err(Error::address("method IMP is null"));
527    }
528    let record_file_offset = ThinFileOffset(entry_offset as u64);
529    Ok(ParsedMethodRecord {
530        method_name: name,
531        type_encoding,
532        imp,
533        provenance: ObjCMethodRecordProvenance {
534            record_file_offset,
535            record_va: resolver
536                .macho()
537                .address_map()
538                .thin_offset_to_va(record_file_offset)?,
539            record_size: entry_size as u64,
540            encoding: ObjCMethodRecordEncoding::Absolute {
541                selector_pointer: resolver.pointer_provenance_at_offset(entry_offset as u64),
542                type_encoding_pointer: resolver.pointer_provenance_at_offset(types_offset),
543                implementation_pointer: resolver.pointer_provenance_at_offset(imp_offset),
544            },
545        },
546    })
547}
548
549fn strict_class_ref_name(resolver: &ObjCResolver<'_>, pointer_offset: u64) -> Result<String> {
550    if let Some(bind_name) = resolver.bind_name_at_offset(pointer_offset) {
551        return Ok(bind_name
552            .strip_prefix("_OBJC_CLASS_$_")
553            .unwrap_or(bind_name)
554            .to_owned());
555    }
556    let class_va = required_pointer(resolver, pointer_offset, "category class")?;
557    let class_offset = resolver.va_to_offset(class_va)?.0;
558    let data_va = required_pointer(
559        resolver,
560        checked_field_offset(class_offset, 32, "category class data")?,
561        "category class data",
562    )?;
563    let ro_offset = resolver
564        .va_to_offset(Va(data_va.0 & crate::types::CLASS_DATA_POINTER_MASK))?
565        .0;
566    required_cstring_pointer(
567        resolver,
568        checked_field_offset(ro_offset, 24, "category class name")?,
569        "category class name",
570    )
571}
572
573fn checked_field_offset(base: u64, delta: u64, field: &str) -> Result<u64> {
574    base.checked_add(delta)
575        .ok_or_else(|| Error::address(format!("{field} offset overflows")))
576}
577
578fn required_pointer(resolver: &ObjCResolver<'_>, offset: u64, field: &str) -> Result<Va> {
579    resolver
580        .read_pointer_at_offset(offset)?
581        .filter(|va| va.0 != 0)
582        .ok_or_else(|| Error::format(format!("{field} pointer is null or unresolved")))
583}
584
585fn required_cstring_pointer(
586    resolver: &ObjCResolver<'_>,
587    offset: u64,
588    field: &str,
589) -> Result<String> {
590    let va = required_pointer(resolver, offset, field)?;
591    resolver.read_cstring(va).map(str::to_owned)
592}
593
594fn read_cstring_at_file_offset(data: &[u8], offset: usize) -> Result<&str> {
595    let slice = data
596        .get(offset..)
597        .ok_or_else(|| Error::bounds(offset as u64, 1, data.len() as u64))?;
598    let end = slice
599        .iter()
600        .position(|byte| *byte == 0)
601        .ok_or_else(|| Error::format(format!("unterminated string at file offset {offset:#x}")))?;
602    std::str::from_utf8(&slice[..end])
603        .map_err(|error| Error::format(format!("invalid UTF-8 at offset {offset:#x}: {error}")))
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    fn thin(bytes: &[u8]) -> &MachoFile<'_> {
611        let container = Box::leak(Box::new(macho_core::parse(bytes).unwrap()));
612        match container {
613            macho_core::model::container::MachoContainer::Thin(macho) => macho,
614            macho_core::model::container::MachoContainer::Fat(_) => panic!("expected thin image"),
615        }
616    }
617
618    #[test]
619    fn folds_class_and_category_method_imps_without_collecting_a_graph() {
620        let class_bytes = macho_test_support::disassembly_objc_boundary();
621        let class_imps = fold_method_imps(thin(&class_bytes), Vec::new(), |items, item| {
622            items.push(item);
623            Ok(())
624        })
625        .unwrap();
626        assert!(
627            class_imps.iter().any(|item| {
628                item.class_name == "Fixture"
629                    && item.category_name.is_none()
630                    && item.method_name == "next"
631                    && item.kind == ObjCMethodKind::Instance
632            }),
633            "{class_imps:?}"
634        );
635        let class_records = fold_method_records(thin(&class_bytes), Vec::new(), |items, item| {
636            items.push(item);
637            Ok(())
638        })
639        .unwrap();
640        let class_method = class_records
641            .iter()
642            .find(|item| item.method_name == "next")
643            .unwrap();
644        assert_eq!(class_method.type_encoding, "v@:");
645        assert_eq!(class_method.provenance.record_file_offset.0, 0x2c8);
646        assert_eq!(class_method.provenance.record_va.0, 0x1_0000_02c8);
647        assert_eq!(class_method.provenance.record_size, 24);
648        assert_eq!(
649            class_method.provenance.encoding,
650            ObjCMethodRecordEncoding::Absolute {
651                selector_pointer: ObjCPointerProvenance::Direct,
652                type_encoding_pointer: ObjCPointerProvenance::Direct,
653                implementation_pointer: ObjCPointerProvenance::Direct,
654            }
655        );
656
657        let category_bytes = macho_test_support::disassembly_objc_category_labels();
658        let category_imps = fold_method_imps(thin(&category_bytes), Vec::new(), |items, item| {
659            items.push(item);
660            Ok(())
661        })
662        .unwrap();
663        assert!(
664            category_imps
665                .iter()
666                .any(|item| item.category_name.as_deref() == Some("Fixture")
667                    && item.kind == ObjCMethodKind::Instance)
668        );
669        assert!(
670            category_imps
671                .iter()
672                .any(|item| item.category_name.as_deref() == Some("Fixture")
673                    && item.kind == ObjCMethodKind::Class)
674        );
675    }
676
677    #[test]
678    fn relative_method_records_retain_each_field_basis_and_selector_reference() {
679        let mut bytes = macho_test_support::disassembly_objc_boundary();
680        const IMAGE_BASE: u64 = 0x1_0000_0000;
681        const ENTRY: usize = 0x2c8;
682        const SELECTOR_REFERENCE: usize = 0x2f8;
683        bytes[0x2c0..0x2c4].copy_from_slice(&0x8000_000cu32.to_le_bytes());
684        bytes[ENTRY..ENTRY + 4].copy_from_slice(
685            &i32::try_from(SELECTOR_REFERENCE - ENTRY)
686                .unwrap()
687                .to_le_bytes(),
688        );
689        bytes[ENTRY + 4..ENTRY + 8]
690            .copy_from_slice(&i32::try_from(0x2f0 - (ENTRY + 4)).unwrap().to_le_bytes());
691        bytes[ENTRY + 8..ENTRY + 12].copy_from_slice(&(-0xcci32).to_le_bytes());
692        bytes[SELECTOR_REFERENCE..SELECTOR_REFERENCE + 8]
693            .copy_from_slice(&(IMAGE_BASE + 0x2e8).to_le_bytes());
694
695        let records = fold_method_records(thin(&bytes), Vec::new(), |items, item| {
696            items.push(item);
697            Ok(())
698        })
699        .unwrap();
700        let record = records
701            .iter()
702            .find(|item| item.method_name == "next")
703            .unwrap();
704        assert_eq!(record.type_encoding, "v@:");
705        assert_eq!(record.imp.0, IMAGE_BASE + 0x204);
706        assert_eq!(record.provenance.record_file_offset.0, ENTRY as u64);
707        assert_eq!(record.provenance.record_size, 12);
708        assert_eq!(
709            record.provenance.encoding,
710            ObjCMethodRecordEncoding::Relative {
711                selector_field_va: Va(IMAGE_BASE + ENTRY as u64),
712                type_encoding_field_va: Va(IMAGE_BASE + ENTRY as u64 + 4),
713                implementation_field_va: Va(IMAGE_BASE + ENTRY as u64 + 8),
714                selector: ObjCRelativeSelectorEncoding::IndirectReference {
715                    selector_reference_file_offset: ThinFileOffset(SELECTOR_REFERENCE as u64),
716                    selector_reference_pointer: ObjCPointerProvenance::Direct,
717                },
718            }
719        );
720
721        bytes[0x2c0..0x2c4].copy_from_slice(&0xc000_000cu32.to_le_bytes());
722        bytes[ENTRY..ENTRY + 4]
723            .copy_from_slice(&i32::try_from(0x2e8 - ENTRY).unwrap().to_le_bytes());
724        let direct_records = fold_method_records(thin(&bytes), Vec::new(), |items, item| {
725            items.push(item);
726            Ok(())
727        })
728        .unwrap();
729        let direct = direct_records
730            .iter()
731            .find(|item| item.method_name == "next")
732            .unwrap();
733        assert!(matches!(
734            direct.provenance.encoding,
735            ObjCMethodRecordEncoding::Relative {
736                selector: ObjCRelativeSelectorEncoding::DirectString {
737                    selector_string_file_offset: ThinFileOffset(0x2e8),
738                },
739                ..
740            }
741        ));
742    }
743
744    #[test]
745    fn damaged_chained_fixups_reject_instead_of_falling_back_to_legacy_metadata() {
746        let mut bytes = macho_test_support::disassembly_objc_boundary();
747        const HEADER_SIZE: usize = 32;
748        const SEGMENT_COMMAND_SIZE: usize = 72 + 2 * 80;
749        const SYMTAB_COMMAND_SIZE: usize = 24;
750        const CHAINED_COMMAND_SIZE: usize = 16;
751        const COMMAND_OFFSET: usize = HEADER_SIZE + SEGMENT_COMMAND_SIZE + SYMTAB_COMMAND_SIZE;
752
753        bytes[16..20].copy_from_slice(&3u32.to_le_bytes());
754        bytes[20..24].copy_from_slice(
755            &((SEGMENT_COMMAND_SIZE + SYMTAB_COMMAND_SIZE + CHAINED_COMMAND_SIZE) as u32)
756                .to_le_bytes(),
757        );
758        bytes[COMMAND_OFFSET..COMMAND_OFFSET + 4].copy_from_slice(&0x8000_0034u32.to_le_bytes());
759        bytes[COMMAND_OFFSET + 4..COMMAND_OFFSET + 8]
760            .copy_from_slice(&(CHAINED_COMMAND_SIZE as u32).to_le_bytes());
761        bytes[COMMAND_OFFSET + 8..COMMAND_OFFSET + 12].copy_from_slice(&u32::MAX.to_le_bytes());
762        bytes[COMMAND_OFFSET + 12..COMMAND_OFFSET + 16].copy_from_slice(&28u32.to_le_bytes());
763
764        let error = fold_method_records(thin(&bytes), (), |_, _| Ok(()))
765            .expect_err("damaged chained-fixup payload must reject");
766        assert!(error.to_string().contains("offset"), "{error}");
767    }
768
769    #[test]
770    fn malformed_suffix_drops_an_accumulator_after_a_valid_imp() {
771        let mut bytes = macho_test_support::disassembly_objc_boundary();
772        // Extend __objc_classlist over the class object's isa word. The first
773        // class method list is valid and invokes the folder; its malformed isa
774        // then fails before the accumulator can be returned.
775        bytes[224..232].copy_from_slice(&16u64.to_le_bytes());
776        bytes[0x248..0x250].copy_from_slice(&u64::MAX.to_le_bytes());
777        let callback_count = std::cell::Cell::new(0usize);
778        let result = fold_method_imps(thin(&bytes), 0usize, |count, _| {
779            *count += 1;
780            callback_count.set(callback_count.get() + 1);
781            Ok(())
782        });
783        assert_eq!(callback_count.get(), 1);
784        assert!(result.is_err());
785    }
786}