Skip to main content

macho_objc/
compat.rs

1//! Objective-C method signature compatibility comparison.
2//!
3//! Given two [`ObjCMethodSignature`] values (parsed from type encoding strings),
4//! determine whether a provider method is ABI-compatible with a target method
5//! for purposes such as method swizzling, replace hooks, or intercept hooks.
6
7use super::encoding::{ObjCMethodSignature, ObjCQualifiedType, ObjCType, TypeQualifier};
8
9/// Result of comparing two ObjC method signatures.
10#[derive(Debug, Clone)]
11pub struct SignatureCompat {
12    /// Whether the signatures are considered compatible (no `Error`-severity issues).
13    pub compatible: bool,
14    /// Individual findings from the comparison.
15    pub findings: Vec<SignatureIssue>,
16}
17
18/// A single compatibility finding.
19#[derive(Debug, Clone)]
20pub struct SignatureIssue {
21    /// The severity field.
22    pub severity: IssueSeverity,
23    /// The message field.
24    pub message: String,
25}
26
27/// Severity level for a compatibility finding.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum IssueSeverity {
31    /// Definite ABI mismatch — will crash or corrupt at runtime.
32    Error,
33    /// Possible mismatch or loss of safety, but may work in practice.
34    Warning,
35}
36
37/// Compare two ObjC method signatures for ABI compatibility.
38///
39/// The `target` is the method being hooked/replaced.
40/// The `provider` is the replacement implementation.
41///
42/// Returns a [`SignatureCompat`] indicating whether the provider can safely
43/// replace the target at the ABI level.
44pub fn compare_method_signatures(
45    target: &ObjCMethodSignature,
46    provider: &ObjCMethodSignature,
47) -> SignatureCompat {
48    let mut findings = Vec::new();
49
50    // --- Argument count ---
51    if target.arguments.len() != provider.arguments.len() {
52        findings.push(SignatureIssue {
53            severity: IssueSeverity::Error,
54            message: format!(
55                "argument count mismatch: target has {}, provider has {}",
56                target.arguments.len(),
57                provider.arguments.len(),
58            ),
59        });
60    } else {
61        // --- Per-argument type compatibility ---
62        for (i, (t_arg, p_arg)) in target.arguments.iter().zip(&provider.arguments).enumerate() {
63            if !types_compatible(&t_arg.ty, &p_arg.ty) {
64                let sev = if types_width_compatible(&t_arg.ty, &p_arg.ty) {
65                    IssueSeverity::Warning
66                } else {
67                    IssueSeverity::Error
68                };
69                findings.push(SignatureIssue {
70                    severity: sev,
71                    message: format!(
72                        "argument {i} type mismatch: target '{}', provider '{}'",
73                        t_arg.ty.render(),
74                        p_arg.ty.render(),
75                    ),
76                });
77            }
78
79            // Qualifier differences are warnings, not errors.
80            let qual_diff = qualifier_diff(&t_arg.ty.qualifiers, &p_arg.ty.qualifiers);
81            if !qual_diff.is_empty() {
82                findings.push(SignatureIssue {
83                    severity: IssueSeverity::Warning,
84                    message: format!("argument {i} qualifier difference: {qual_diff}"),
85                });
86            }
87        }
88    }
89
90    // --- Return type ---
91    if !types_compatible(&target.return_type, &provider.return_type) {
92        let sev = if types_width_compatible(&target.return_type, &provider.return_type) {
93            IssueSeverity::Warning
94        } else {
95            IssueSeverity::Error
96        };
97        findings.push(SignatureIssue {
98            severity: sev,
99            message: format!(
100                "return type mismatch: target '{}', provider '{}'",
101                target.return_type.render(),
102                provider.return_type.render(),
103            ),
104        });
105    }
106
107    let compatible = !findings.iter().any(|f| f.severity == IssueSeverity::Error);
108
109    SignatureCompat {
110        compatible,
111        findings,
112    }
113}
114
115/// Check structural type compatibility between two ObjC types.
116///
117/// This is a recursive check:
118/// - Primitive types must match exactly.
119/// - `id` is compatible with any object type (and vice versa).
120/// - Pointers must match in depth and pointee compatibility.
121/// - Struct/union names must match.
122/// - Qualifiers are ignored here (checked separately).
123pub fn types_compatible(a: &ObjCQualifiedType, b: &ObjCQualifiedType) -> bool {
124    type_cores_compatible(&a.ty, &b.ty)
125}
126
127fn type_cores_compatible(a: &ObjCType, b: &ObjCType) -> bool {
128    match (a, b) {
129        // Exact match on primitives.
130        (ObjCType::Void, ObjCType::Void)
131        | (ObjCType::Bool, ObjCType::Bool)
132        | (ObjCType::Char, ObjCType::Char)
133        | (ObjCType::UnsignedChar, ObjCType::UnsignedChar)
134        | (ObjCType::Short, ObjCType::Short)
135        | (ObjCType::UnsignedShort, ObjCType::UnsignedShort)
136        | (ObjCType::Int, ObjCType::Int)
137        | (ObjCType::UnsignedInt, ObjCType::UnsignedInt)
138        | (ObjCType::Long, ObjCType::Long)
139        | (ObjCType::UnsignedLong, ObjCType::UnsignedLong)
140        | (ObjCType::LongLong, ObjCType::LongLong)
141        | (ObjCType::UnsignedLongLong, ObjCType::UnsignedLongLong)
142        | (ObjCType::Float, ObjCType::Float)
143        | (ObjCType::Double, ObjCType::Double)
144        | (ObjCType::CString, ObjCType::CString)
145        | (ObjCType::CharPtr, ObjCType::CharPtr)
146        | (ObjCType::Selector, ObjCType::Selector)
147        | (ObjCType::Class, ObjCType::Class) => true,
148
149        // Object types: `id` (no class name) is compatible with any object type.
150        (
151            ObjCType::Object {
152                class_name: a_cls,
153                is_block: a_blk,
154                ..
155            },
156            ObjCType::Object {
157                class_name: b_cls,
158                is_block: b_blk,
159                ..
160            },
161        ) => {
162            // Both blocks → compatible.
163            if *a_blk && *b_blk {
164                return true;
165            }
166            // `id` (class_name = None) is compatible with any object.
167            if a_cls.is_none() || b_cls.is_none() {
168                return true;
169            }
170            // Named classes must match.
171            a_cls == b_cls
172        }
173
174        // Pointers: recursive check on pointee.
175        (ObjCType::Pointer(a_inner), ObjCType::Pointer(b_inner)) => {
176            types_compatible(a_inner, b_inner)
177        }
178
179        // Struct/union: names must match.
180        (ObjCType::Struct { name: a_name, .. }, ObjCType::Struct { name: b_name, .. }) => {
181            a_name == b_name
182        }
183        (ObjCType::Union { name: a_name, .. }, ObjCType::Union { name: b_name, .. }) => {
184            a_name == b_name
185        }
186
187        // Arrays: element type and length must match.
188        (
189            ObjCType::Array {
190                len: a_len,
191                element: a_el,
192            },
193            ObjCType::Array {
194                len: b_len,
195                element: b_el,
196            },
197        ) => a_len == b_len && types_compatible(a_el, b_el),
198
199        // BitFields: width must match.
200        (ObjCType::BitField(a_bits), ObjCType::BitField(b_bits)) => a_bits == b_bits,
201
202        // CString ↔ CharPtr: compatible (both are `char *`).
203        (ObjCType::CString, ObjCType::CharPtr) | (ObjCType::CharPtr, ObjCType::CString) => true,
204
205        // Everything else is incompatible.
206        _ => false,
207    }
208}
209
210/// Check if two types have the same ABI width even if semantically different.
211///
212/// For example, `int` and `unsigned int` have the same width. This is used
213/// to downgrade errors to warnings when the mismatch is unlikely to cause
214/// a crash (same register / stack slot size).
215///
216/// Returns `false` for pointer/struct mismatches where same width is
217/// coincidental rather than meaningful.
218fn types_width_compatible(a: &ObjCQualifiedType, b: &ObjCQualifiedType) -> bool {
219    // Don't downgrade pointer-vs-different-pointer mismatches — same width
220    // but passing the wrong level of indirection will crash.
221    if is_pointer_family(&a.ty) != is_pointer_family(&b.ty) {
222        return false;
223    }
224    // Both are pointers but pointees differ — structural mismatch, not width.
225    if is_pointer_family(&a.ty) && is_pointer_family(&b.ty) {
226        return false;
227    }
228    let a_w = type_width(&a.ty);
229    let b_w = type_width(&b.ty);
230    match (a_w, b_w) {
231        (Some(aw), Some(bw)) => aw == bw,
232        _ => false,
233    }
234}
235
236fn is_pointer_family(ty: &ObjCType) -> bool {
237    matches!(
238        ty,
239        ObjCType::Pointer(_)
240            | ObjCType::Object { .. }
241            | ObjCType::CharPtr
242            | ObjCType::CString
243            | ObjCType::Selector
244            | ObjCType::Class
245    )
246}
247
248/// Estimate the ABI width of a type in bytes (on 64-bit).
249fn type_width(ty: &ObjCType) -> Option<usize> {
250    match ty {
251        ObjCType::Void => Some(0),
252        ObjCType::Bool | ObjCType::Char | ObjCType::UnsignedChar => Some(1),
253        ObjCType::Short | ObjCType::UnsignedShort => Some(2),
254        ObjCType::Int | ObjCType::UnsignedInt | ObjCType::Float => Some(4),
255        ObjCType::Long
256        | ObjCType::UnsignedLong
257        | ObjCType::LongLong
258        | ObjCType::UnsignedLongLong
259        | ObjCType::Double => Some(8),
260        ObjCType::Pointer(_)
261        | ObjCType::Object { .. }
262        | ObjCType::CharPtr
263        | ObjCType::CString
264        | ObjCType::Selector
265        | ObjCType::Class => Some(8),
266        _ => None,
267    }
268}
269
270/// Describe qualifier differences between two qualifier lists.
271fn qualifier_diff(a: &[TypeQualifier], b: &[TypeQualifier]) -> String {
272    let mut diffs = Vec::new();
273    for q in a {
274        if !b.contains(q) {
275            diffs.push(format!("target has {q:?}"));
276        }
277    }
278    for q in b {
279        if !a.contains(q) {
280            diffs.push(format!("provider has {q:?}"));
281        }
282    }
283    diffs.join(", ")
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::objc::encoding::ObjCMethodArg;
290
291    fn make_sig(ret: ObjCType, args: Vec<ObjCType>) -> ObjCMethodSignature {
292        ObjCMethodSignature {
293            return_type: ObjCQualifiedType {
294                qualifiers: vec![],
295                ty: ret,
296            },
297            return_offset: None,
298            self_type: None,
299            cmd_type: None,
300            arguments: args
301                .into_iter()
302                .map(|ty| ObjCMethodArg {
303                    ty: ObjCQualifiedType {
304                        qualifiers: vec![],
305                        ty,
306                    },
307                    stack_offset: None,
308                })
309                .collect(),
310        }
311    }
312
313    #[test]
314    fn identical_signatures_compatible() {
315        let sig = make_sig(ObjCType::Void, vec![ObjCType::Int, ObjCType::Bool]);
316        let result = compare_method_signatures(&sig, &sig);
317        assert!(result.compatible);
318        assert!(result.findings.is_empty());
319    }
320
321    #[test]
322    fn arg_count_mismatch() {
323        let target = make_sig(ObjCType::Void, vec![ObjCType::Int]);
324        let provider = make_sig(ObjCType::Void, vec![ObjCType::Int, ObjCType::Bool]);
325        let result = compare_method_signatures(&target, &provider);
326        assert!(!result.compatible);
327        assert!(
328            result
329                .findings
330                .iter()
331                .any(|f| f.severity == IssueSeverity::Error)
332        );
333    }
334
335    #[test]
336    fn return_type_mismatch_is_error() {
337        let target = make_sig(ObjCType::Void, vec![]);
338        let provider = make_sig(ObjCType::Int, vec![]);
339        let result = compare_method_signatures(&target, &provider);
340        assert!(!result.compatible);
341    }
342
343    #[test]
344    fn same_width_mismatch_is_warning() {
345        // int vs unsigned int → same width (4 bytes) → warning, not error.
346        let target = make_sig(ObjCType::Int, vec![]);
347        let provider = make_sig(ObjCType::UnsignedInt, vec![]);
348        let result = compare_method_signatures(&target, &provider);
349        assert!(result.compatible); // warning, not error
350        assert!(!result.findings.is_empty());
351    }
352
353    #[test]
354    fn id_compatible_with_named_object() {
355        let id_type = ObjCType::Object {
356            class_name: None,
357            protocols: vec![],
358            is_block: false,
359        };
360        let named_type = ObjCType::Object {
361            class_name: Some("NSString".into()),
362            protocols: vec![],
363            is_block: false,
364        };
365        let target = make_sig(id_type.clone(), vec![named_type.clone()]);
366        let provider = make_sig(named_type, vec![id_type]);
367        let result = compare_method_signatures(&target, &provider);
368        assert!(result.compatible);
369    }
370
371    #[test]
372    fn pointer_depth_mismatch() {
373        let ptr = ObjCType::Pointer(Box::new(ObjCQualifiedType {
374            qualifiers: vec![],
375            ty: ObjCType::Int,
376        }));
377        let double_ptr = ObjCType::Pointer(Box::new(ObjCQualifiedType {
378            qualifiers: vec![],
379            ty: ptr.clone(),
380        }));
381        let target = make_sig(ObjCType::Void, vec![ptr]);
382        let provider = make_sig(ObjCType::Void, vec![double_ptr]);
383        let result = compare_method_signatures(&target, &provider);
384        assert!(!result.compatible);
385    }
386
387    #[test]
388    fn struct_name_mismatch() {
389        let a = ObjCType::Struct {
390            name: "CGRect".into(),
391            fields: vec![],
392        };
393        let b = ObjCType::Struct {
394            name: "CGSize".into(),
395            fields: vec![],
396        };
397        let target = make_sig(ObjCType::Void, vec![a]);
398        let provider = make_sig(ObjCType::Void, vec![b]);
399        let result = compare_method_signatures(&target, &provider);
400        assert!(!result.compatible);
401    }
402
403    #[test]
404    fn cstring_and_charptr_compatible() {
405        let target = make_sig(ObjCType::CString, vec![]);
406        let provider = make_sig(ObjCType::CharPtr, vec![]);
407        let result = compare_method_signatures(&target, &provider);
408        assert!(result.compatible);
409    }
410}