Skip to content

Commit b29b46f

Browse files
committed
Implemented support for hooking class methods
1 parent 3502ed0 commit b29b46f

File tree

6 files changed

+154
-52
lines changed

6 files changed

+154
-52
lines changed

Sources/InterposeKit/Hooks/Hook.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ extension Hook: CustomDebugStringConvertible {
223223
case .failed: description += "Failed"
224224
}
225225

226-
description.append(" hook for -[\(self.class) \(self.selector)]")
226+
let symbolPrefix = self.scope.methodKind.symbolPrefix
227+
description.append(" hook for \(symbolPrefix)[\(self.class) \(self.selector)]")
227228

228229
if case .object(let object) = self.scope {
229230
description.append(" on \(Unmanaged.passUnretained(object).toOpaque())")
@@ -237,16 +238,6 @@ extension Hook: CustomDebugStringConvertible {
237238
}
238239
}
239240

240-
public enum HookScope {
241-
242-
/// The scope that targets a method on a class type (instance or class method).
243-
case `class`(MethodKind)
244-
245-
/// The scope that targets a specific object instance.
246-
case object(NSObject)
247-
248-
}
249-
250241
public enum HookState: Equatable {
251242

252243
/// The hook is ready to be applied.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ObjectiveC
2+
3+
public enum HookScope {
4+
5+
/// The scope that targets a method on a class type (instance or class method).
6+
case `class`(MethodKind)
7+
8+
/// The scope that targets a specific object instance.
9+
case object(NSObject)
10+
11+
}
12+
13+
extension HookScope {
14+
15+
/// Returns the kind of the method targeted by the hook scope.
16+
public var methodKind: MethodKind {
17+
switch self {
18+
case .class(let methodKind):
19+
return methodKind
20+
case .object:
21+
return .instance
22+
}
23+
}
24+
25+
}

Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,41 @@ internal final class ClassHookStrategy: HookStrategy {
3636
private(set) internal var appliedHookIMP: IMP?
3737
private(set) internal var storedOriginalIMP: IMP?
3838

39+
// ============================================================================ //
40+
// MARK: Target Class
41+
// ============================================================================ //
42+
43+
/// The target class resolved for the configured method kind.
44+
///
45+
/// This is the class itself for instance methods, or the metaclass for class methods.
46+
private lazy var targetClass: AnyClass = {
47+
switch self.methodKind {
48+
case .instance:
49+
return self.class
50+
case .class:
51+
return object_getClass(self.class)
52+
}
53+
}()
54+
3955
// ============================================================================ //
4056
// MARK: Validation
4157
// ============================================================================ //
4258

4359
internal func validate() throws {
4460
// Ensure that the method exists.
45-
guard class_getInstanceMethod(self.class, self.selector) != nil else {
61+
guard class_getInstanceMethod(self.targetClass, self.selector) != nil else {
4662
throw InterposeError.methodNotFound(
4763
class: self.class,
64+
kind: self.methodKind,
4865
selector: self.selector
4966
)
5067
}
5168

5269
// Ensure that the class directly implements the method.
53-
guard class_implementsInstanceMethod(self.class, self.selector) else {
70+
guard class_implementsInstanceMethod(self.targetClass, self.selector) else {
5471
throw InterposeError.methodNotDirectlyImplemented(
5572
class: self.class,
73+
kind: self.methodKind,
5674
selector: self.selector
5775
)
5876
}
@@ -65,33 +83,38 @@ internal final class ClassHookStrategy: HookStrategy {
6583
internal func replaceImplementation() throws {
6684
let hookIMP = self.makeHookIMP()
6785

68-
guard let method = class_getInstanceMethod(self.class, self.selector) else {
86+
guard let method = class_getInstanceMethod(self.targetClass, self.selector) else {
6987
// This should not happen under normal circumstances, as we perform validation upon
7088
// creating the hook strategy, which itself checks for the presence of the method.
7189
throw InterposeError.methodNotFound(
7290
class: self.class,
91+
kind: self.methodKind,
7392
selector: self.selector
7493
)
7594
}
7695

7796
guard let originalIMP = class_replaceMethod(
78-
self.class,
97+
self.targetClass,
7998
self.selector,
8099
hookIMP,
81100
method_getTypeEncoding(method)
82101
) else {
83102
// This should not happen under normal circumstances, as we perform validation upon
84103
// creating the hook strategy, which checks if the class directly implements the method.
85104
throw InterposeError.implementationNotFound(
86-
class: self.class,
105+
class: self.targetClass,
106+
kind: self.methodKind,
87107
selector: self.selector
88108
)
89109
}
90110

91111
self.appliedHookIMP = hookIMP
92112
self.storedOriginalIMP = originalIMP
93113

94-
Interpose.log("Replaced implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP) -> \(hookIMP)")
114+
Interpose.log({
115+
let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]"
116+
return "Replaced implementation for \(selector) IMP: \(originalIMP) -> \(hookIMP)"
117+
}())
95118
}
96119

97120
internal func restoreImplementation() throws {
@@ -104,15 +127,16 @@ internal final class ClassHookStrategy: HookStrategy {
104127
self.storedOriginalIMP = nil
105128
}
106129

107-
guard let method = class_getInstanceMethod(self.class, self.selector) else {
130+
guard let method = class_getInstanceMethod(self.targetClass, self.selector) else {
108131
throw InterposeError.methodNotFound(
109132
class: self.class,
133+
kind: self.methodKind,
110134
selector: self.selector
111135
)
112136
}
113137

114138
let previousIMP = class_replaceMethod(
115-
self.class,
139+
self.targetClass,
116140
self.selector,
117141
originalIMP,
118142
method_getTypeEncoding(method)
@@ -121,12 +145,16 @@ internal final class ClassHookStrategy: HookStrategy {
121145
guard previousIMP == hookIMP else {
122146
throw InterposeError.revertCorrupted(
123147
class: self.class,
148+
kind: self.methodKind,
124149
selector: self.selector,
125150
imp: previousIMP
126151
)
127152
}
128153

129-
Interpose.log("Restored implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP)")
154+
Interpose.log({
155+
let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]"
156+
return "Restored implementation for \(selector) IMP: \(originalIMP)"
157+
}())
130158
}
131159

132160
}

Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ internal final class ObjectHookStrategy: HookStrategy {
5050
guard class_getInstanceMethod(self.class, self.selector) != nil else {
5151
throw InterposeError.methodNotFound(
5252
class: self.class,
53+
kind: .instance,
5354
selector: self.selector
5455
)
5556
}
@@ -58,6 +59,7 @@ internal final class ObjectHookStrategy: HookStrategy {
5859
guard self.lookUpIMP() != nil else {
5960
throw InterposeError.implementationNotFound(
6061
class: self.class,
62+
kind: .instance,
6163
selector: self.selector
6264
)
6365
}
@@ -92,6 +94,7 @@ internal final class ObjectHookStrategy: HookStrategy {
9294
guard let method = class_getInstanceMethod(self.class, self.selector) else {
9395
throw InterposeError.methodNotFound(
9496
class: self.class,
97+
kind: .instance,
9598
selector: self.selector
9699
)
97100
}
@@ -137,6 +140,7 @@ internal final class ObjectHookStrategy: HookStrategy {
137140
// or an existing hook.
138141
throw InterposeError.implementationNotFound(
139142
class: subclass,
143+
kind: .instance,
140144
selector: self.selector
141145
)
142146
}
@@ -171,13 +175,15 @@ internal final class ObjectHookStrategy: HookStrategy {
171175
guard let method = class_getInstanceMethod(self.class, self.selector) else {
172176
throw InterposeError.methodNotFound(
173177
class: self.class,
178+
kind: .instance,
174179
selector: self.selector
175180
)
176181
}
177182

178183
guard let currentIMP = class_getMethodImplementation(dynamicSubclass, self.selector) else {
179184
throw InterposeError.implementationNotFound(
180185
class: self.class,
186+
kind: .instance,
181187
selector: self.selector
182188
)
183189
}
@@ -194,6 +200,7 @@ internal final class ObjectHookStrategy: HookStrategy {
194200
guard previousIMP == hookIMP else {
195201
throw InterposeError.revertCorrupted(
196202
class: dynamicSubclass,
203+
kind: .instance,
197204
selector: self.selector,
198205
imp: previousIMP
199206
)

Sources/InterposeKit/InterposeError.swift

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,38 @@ public enum InterposeError: Error, @unchecked Sendable {
55
/// A hook operation failed and the hook is no longer usable.
66
case hookInFailedState
77

8-
/// No instance method found for the selector on the specified class.
8+
/// No method of the given kind found for the selector on the specified class.
99
///
10-
/// This typically occurs when mistyping a stringified selector or attempting to interpose
11-
/// a class method, which is not supported.
10+
/// This typically occurs when mistyping a stringified selector or attempting to hook
11+
/// a method that does not exist on the class.
1212
case methodNotFound(
1313
class: AnyClass,
14+
kind: MethodKind,
1415
selector: Selector
1516
)
16-
17-
/// The method for the selector is not directly implemented on the specified class
18-
/// but inherited from a superclass.
17+
18+
/// The method for the selector is inherited from a superclass rather than directly implemented
19+
/// by the specified class.
1920
///
20-
/// Class-based interposing only supports instance methods implemented directly by the class
21-
/// itself. This restriction ensures safe reverting via `revert()`, which cannot remove
22-
/// dynamically added methods.
21+
/// Class-based interposing only supports methods directly implemented by the class itself.
22+
/// This restriction ensures safe reverting via `revert()`, which cannot remove dynamically
23+
/// added methods.
2324
///
2425
/// To interpose this method, consider hooking the superclass that provides the implementation,
2526
/// or use object-based hooking on a specific instance instead.
2627
case methodNotDirectlyImplemented(
2728
class: AnyClass,
29+
kind: MethodKind,
2830
selector: Selector
2931
)
30-
31-
/// No implementation found for the method matching the specified selector on the class.
32+
33+
/// No implementation found for a method of the given kind matching the selector on the class.
3234
///
3335
/// This should not occur under normal conditions and may indicate an invalid or misconfigured
3436
/// runtime state.
3537
case implementationNotFound(
3638
class: AnyClass,
39+
kind: MethodKind,
3740
selector: Selector
3841
)
3942

@@ -44,6 +47,7 @@ public enum InterposeError: Error, @unchecked Sendable {
4447
/// In such cases, `Hook.revert()` is unsafe and should be avoided.
4548
case revertCorrupted(
4649
class: AnyClass,
50+
kind: MethodKind,
4751
selector: Selector,
4852
imp: IMP?
4953
)
@@ -102,34 +106,43 @@ public enum InterposeError: Error, @unchecked Sendable {
102106
extension InterposeError: Equatable {
103107
public static func == (lhs: Self, rhs: Self) -> Bool {
104108
switch lhs {
105-
case let .methodNotFound(lhsClass, lhsSelector):
109+
case let .methodNotFound(lhsClass, lhsKind, lhsSelector):
106110
switch rhs {
107-
case let .methodNotFound(rhsClass, rhsSelector):
108-
return lhsClass == rhsClass && lhsSelector == rhsSelector
111+
case let .methodNotFound(rhsClass, rhsKind, rhsSelector):
112+
return lhsClass == rhsClass
113+
&& lhsKind == rhsKind
114+
&& lhsSelector == rhsSelector
109115
default:
110116
return false
111117
}
112118

113-
case let .methodNotDirectlyImplemented(lhsClass, lhsSelector):
119+
case let .methodNotDirectlyImplemented(lhsClass, lhsKind, lhsSelector):
114120
switch rhs {
115-
case let .methodNotDirectlyImplemented(rhsClass, rhsSelector):
116-
return lhsClass == rhsClass && lhsSelector == rhsSelector
121+
case let .methodNotDirectlyImplemented(rhsClass, rhsKind, rhsSelector):
122+
return lhsClass == rhsClass
123+
&& lhsKind == rhsKind
124+
&& lhsSelector == rhsSelector
117125
default:
118126
return false
119127
}
120128

121-
case let .implementationNotFound(lhsClass, lhsSelector):
129+
case let .implementationNotFound(lhsClass, lhsKind, lhsSelector):
122130
switch rhs {
123-
case let .implementationNotFound(rhsClass, rhsSelector):
124-
return lhsClass == rhsClass && lhsSelector == rhsSelector
131+
case let .implementationNotFound(rhsClass, rhsKind, rhsSelector):
132+
return lhsClass == rhsClass
133+
&& lhsKind == rhsKind
134+
&& lhsSelector == rhsSelector
125135
default:
126136
return false
127137
}
128138

129-
case let .revertCorrupted(lhsClass, lhsSelector, lhsIMP):
139+
case let .revertCorrupted(lhsClass, lhsKind, lhsSelector, lhsIMP):
130140
switch rhs {
131-
case let .revertCorrupted(rhsClass, rhsSelector, rhsIMP):
132-
return lhsClass == rhsClass && lhsSelector == rhsSelector && lhsIMP == rhsIMP
141+
case let .revertCorrupted(rhsClass, rhsKind, rhsSelector, rhsIMP):
142+
return lhsClass == rhsClass
143+
&& lhsKind == rhsKind
144+
&& lhsSelector == rhsSelector
145+
&& lhsIMP == rhsIMP
133146
default:
134147
return false
135148
}

0 commit comments

Comments
 (0)