Skip to content

Commit 829b9f5

Browse files
authored
Update the GULObjectSwizzler to handle NSProxy objects (#2053)
1 parent ab57eed commit 829b9f5

File tree

7 files changed

+264
-3
lines changed

7 files changed

+264
-3
lines changed

GoogleUtilities/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
- Fixed an issue where GoogleUtilities would leak non-background URL sessions.
33
(#2061)
44

5+
- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in an
6+
`NSProxy`. (#1936)
7+
58
# 5.3.4
69
- Fixed a crash caused by unprotected access to sessions in
710
`GULNetworkURLSession` (#1964).

GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
// limitations under the License.
1414

1515
#import <XCTest/XCTest.h>
16+
#import <objc/runtime.h>
1617

1718
#import <GoogleUtilities/GULObjectSwizzler.h>
19+
#import <GoogleUtilities/GULProxy.h>
1820
#import <GoogleUtilities/GULSwizzledObject.h>
1921

2022
@interface GULObjectSwizzlerTest : XCTestCase
@@ -23,6 +25,16 @@ @interface GULObjectSwizzlerTest : XCTestCase
2325

2426
@implementation GULObjectSwizzlerTest
2527

28+
/** Used as a donor method to add a method that doesn't exist on the superclass. */
29+
- (NSString *)donorDescription {
30+
return @"SwizzledDonorDescription";
31+
}
32+
33+
/** Used as a donor method to add a method that exists on the superclass. */
34+
- (NSString *)description {
35+
return @"SwizzledDescription";
36+
}
37+
2638
/** Exists just as a donor method. */
2739
- (void)donorMethod {
2840
}
@@ -226,6 +238,7 @@ - (void)testSetGetAssociatedObjectRetainNonatomic {
226238
XCTAssertEqualObjects(returnedObject, associatedObject);
227239
}
228240

241+
/** Tests getting and setting an associated object with an invalid association type. */
229242
- (void)testSetGetAssociatedObjectWithoutProperAssociation {
230243
NSObject *object = [[NSObject alloc] init];
231244
NSDictionary *associatedObject = [[NSDictionary alloc] init];
@@ -235,4 +248,105 @@ - (void)testSetGetAssociatedObjectWithoutProperAssociation {
235248
XCTAssertEqualObjects(returnedObject, associatedObject);
236249
}
237250

251+
/** Tests using the GULObjectSwizzler to swizzle an object wrapped in an NSProxy. */
252+
- (void)testSwizzleProxiedObject {
253+
NSObject *object = [[NSObject alloc] init];
254+
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
255+
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
256+
257+
XCTAssertNoThrow([swizzler swizzle]);
258+
259+
XCTAssertNotEqual(object_getClass(proxyObject), [GULProxy class]);
260+
XCTAssertTrue([object_getClass(proxyObject) isSubclassOfClass:[GULProxy class]]);
261+
262+
XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_objectSwizzler)]);
263+
XCTAssertNoThrow([proxyObject performSelector:@selector(gul_objectSwizzler)]);
264+
265+
XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_class)]);
266+
XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]);
267+
}
268+
269+
/** Tests overriding a method that already exists on a proxied object works as expected. */
270+
- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod {
271+
NSObject *object = [[NSObject alloc] init];
272+
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
273+
274+
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
275+
[swizzler copySelector:@selector(description)
276+
fromClass:[GULObjectSwizzlerTest class]
277+
isClassSelector:NO];
278+
[swizzler swizzle];
279+
280+
XCTAssertEqual([proxyObject performSelector:@selector(description)], @"SwizzledDescription");
281+
}
282+
283+
/** Tests adding a method that doesn't exist on a proxied object works as expected. */
284+
- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod {
285+
NSObject *object = [[NSObject alloc] init];
286+
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
287+
288+
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
289+
[swizzler copySelector:@selector(donorDescription)
290+
fromClass:[GULObjectSwizzlerTest class]
291+
isClassSelector:NO];
292+
[swizzler swizzle];
293+
294+
XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)],
295+
@"SwizzledDonorDescription");
296+
}
297+
298+
/** Tests KVOing a proxy object that we've ISA Swizzled works as expected. */
299+
- (void)testRespondsToSelectorWorksEvenIfSwizzledProxyIsKVOd {
300+
NSObject *object = [[NSObject alloc] init];
301+
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
302+
303+
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
304+
[swizzler copySelector:@selector(donorDescription)
305+
fromClass:[GULObjectSwizzlerTest class]
306+
isClassSelector:NO];
307+
[swizzler swizzle];
308+
309+
[(NSObject *)proxyObject addObserver:self
310+
forKeyPath:NSStringFromSelector(@selector(description))
311+
options:0
312+
context:NULL];
313+
314+
XCTAssertTrue([proxyObject respondsToSelector:@selector(donorDescription)]);
315+
XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)],
316+
@"SwizzledDonorDescription");
317+
318+
[(NSObject *)proxyObject removeObserver:self
319+
forKeyPath:NSStringFromSelector(@selector(description))];
320+
}
321+
322+
/** Tests that -[NSObjectProtocol resopondsToSelector:] works as expected after someone else ISA
323+
* swizzles a proxy object that we've also ISA Swizzled.
324+
*/
325+
- (void)testRespondsToSelectorWorksEvenIfSwizzledProxyISASwizzledBySomeoneElse {
326+
NSObject *object = [[NSObject alloc] init];
327+
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
328+
329+
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
330+
[swizzler copySelector:@selector(donorDescription)
331+
fromClass:[GULObjectSwizzlerTest class]
332+
isClassSelector:NO];
333+
[swizzler swizzle];
334+
335+
// Someone else ISA Swizzles the same object after GULObjectSwizzler.
336+
Class originalClass = object_getClass(proxyObject);
337+
NSString *newClassName =
338+
[NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, NSStringFromClass(originalClass)];
339+
Class generatedClass = objc_allocateClassPair(originalClass, newClassName.UTF8String, 0);
340+
objc_registerClassPair(generatedClass);
341+
object_setClass(proxyObject, generatedClass);
342+
343+
XCTAssertTrue([proxyObject respondsToSelector:@selector(donorDescription)]);
344+
XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)],
345+
@"SwizzledDonorDescription");
346+
347+
// Clean up.
348+
object_setClass(proxyObject, originalClass);
349+
objc_disposeClassPair(generatedClass);
350+
}
351+
238352
@end

GoogleUtilities/ISASwizzler/GULObjectSwizzler.m

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ - (instancetype)initWithObject:(id)object {
8080
__strong id swizzledObject = object;
8181
if (swizzledObject) {
8282
_swizzledObject = swizzledObject;
83-
_originalClass = [swizzledObject class];
83+
_originalClass = object_getClass(object);
8484
NSString *newClassName = [NSString
8585
stringWithFormat:@"fir_%p_%@", swizzledObject, NSStringFromClass(_originalClass)];
8686
_generatedClass = objc_allocateClassPair(_originalClass, newClassName.UTF8String, 0);
@@ -134,7 +134,7 @@ - (void)swizzle {
134134

135135
[GULSwizzledObject copyDonorSelectorsUsingObjectSwizzler:self];
136136

137-
NSAssert(_originalClass == [_swizzledObject class],
137+
NSAssert(_originalClass == object_getClass(swizzledObject),
138138
@"The original class is not the reported class now.");
139139
NSAssert(class_getInstanceSize(_originalClass) == class_getInstanceSize(_generatedClass),
140140
@"The instance size of the generated class must be equal to the original class.");
@@ -154,4 +154,8 @@ - (void)dealloc {
154154
objc_disposeClassPair(_generatedClass);
155155
}
156156

157+
- (BOOL)isSwizzlingProxyObject {
158+
return [_swizzledObject isProxy];
159+
}
160+
157161
@end

GoogleUtilities/ISASwizzler/GULSwizzledObject.m

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
#import "Private/GULSwizzledObject.h"
15+
#import <objc/runtime.h>
16+
1617
#import "Private/GULObjectSwizzler.h"
18+
#import "Private/GULSwizzledObject.h"
1719

1820
NSString *kSwizzlerAssociatedObjectKey = @"gul_objectSwizzler";
1921

@@ -26,6 +28,16 @@ @implementation GULSwizzledObject
2628
+ (void)copyDonorSelectorsUsingObjectSwizzler:(GULObjectSwizzler *)objectSwizzler {
2729
[objectSwizzler copySelector:@selector(gul_objectSwizzler) fromClass:self isClassSelector:NO];
2830
[objectSwizzler copySelector:@selector(gul_class) fromClass:self isClassSelector:NO];
31+
32+
// This is needed because NSProxy objects usually override -[NSObjectProtocol respondsToSelector:]
33+
// and ask this question to the underlying object. Since we don't swizzle the underlying object
34+
// but swizzle the proxy, when someone calls -[NSObjectProtocol respondsToSelector:] on the proxy,
35+
// the answer ends up being NO even if we added new methods to the subclass through ISA Swizzling.
36+
// To solve that, we override -[NSObjectProtocol respondsToSelector:] in such a way that takes
37+
// into account the fact that we've added new methods.
38+
if ([objectSwizzler isSwizzlingProxyObject]) {
39+
[objectSwizzler copySelector:@selector(respondsToSelector:) fromClass:self isClassSelector:NO];
40+
}
2941
}
3042

3143
- (instancetype)init {
@@ -43,4 +55,10 @@ - (Class)gul_class {
4355
return [[self gul_objectSwizzler] generatedClass];
4456
}
4557

58+
// Only added to a class when we detect it is a proxy.
59+
- (BOOL)respondsToSelector:(SEL)aSelector {
60+
Class gulClass = [[self gul_objectSwizzler] generatedClass];
61+
return [gulClass instancesRespondToSelector:aSelector] || [super respondsToSelector:aSelector];
62+
}
63+
4664
@end

GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ typedef OBJC_ENUM(uintptr_t, GUL_ASSOCIATION){
115115
* the class pair. */
116116
- (void)swizzle;
117117

118+
/** @return The value of -[objectBeingSwizzled isProxy] */
119+
- (BOOL)isSwizzlingProxyObject;
120+
118121
@end
119122

120123
NS_ASSUME_NONNULL_END
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
19+
/** An example NSProxy that could be used to wrap an object that we have to ISA Swizzle. */
20+
@interface GULProxy : NSProxy
21+
22+
+ (instancetype)proxyWithDelegate:(id)delegate;
23+
24+
@end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "GULProxy.h"
18+
19+
@interface GULProxy ()
20+
21+
@property(nonatomic, strong) id delegateObject;
22+
23+
@end
24+
25+
@implementation GULProxy
26+
27+
- (instancetype)initWithDelegate:(id)delegate {
28+
_delegateObject = delegate;
29+
return self;
30+
}
31+
32+
+ (instancetype)proxyWithDelegate:(id)delegate {
33+
return [[GULProxy alloc] initWithDelegate:delegate];
34+
}
35+
36+
- (id)forwardingTargetForSelector:(SEL)selector {
37+
return _delegateObject;
38+
}
39+
40+
- (void)forwardInvocation:(NSInvocation *)invocation {
41+
if (_delegateObject != nil) {
42+
[invocation setTarget:_delegateObject];
43+
[invocation invoke];
44+
}
45+
}
46+
47+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
48+
return [_delegateObject instanceMethodSignatureForSelector:selector];
49+
}
50+
51+
- (BOOL)respondsToSelector:(SEL)aSelector {
52+
return [_delegateObject respondsToSelector:aSelector];
53+
}
54+
55+
- (BOOL)isEqual:(id)object {
56+
return [_delegateObject isEqual:object];
57+
}
58+
59+
- (NSUInteger)hash {
60+
return [_delegateObject hash];
61+
}
62+
63+
- (Class)superclass {
64+
return [_delegateObject superclass];
65+
}
66+
67+
- (Class) class {
68+
return [_delegateObject class];
69+
}
70+
71+
- (BOOL)isKindOfClass : (Class)aClass {
72+
return [_delegateObject isKindOfClass:aClass];
73+
}
74+
75+
- (BOOL)isMemberOfClass:(Class)aClass {
76+
return [_delegateObject isMemberOfClass:aClass];
77+
}
78+
79+
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
80+
return [_delegateObject conformsToProtocol:aProtocol];
81+
}
82+
83+
- (BOOL)isProxy {
84+
return YES;
85+
}
86+
87+
- (NSString *)description {
88+
return [_delegateObject description];
89+
}
90+
91+
- (NSString *)debugDescription {
92+
return [_delegateObject debugDescription];
93+
}
94+
95+
@end

0 commit comments

Comments
 (0)