From 3f6b086ce45d56380559a3a42054be3f501d3d99 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 6 Nov 2018 17:23:30 -0800 Subject: [PATCH 01/11] Initial fix and tests for assertion error when GULObjectSwizzler tries to swizzle a NSProxy object. --- .../Tests/Swizzler/GULObjectSwizzlerTest.m | 55 +++++++++++ .../ISASwizzler/GULObjectSwizzler.m | 4 +- .../SwizzlerTestHelpers/GULProxy.h | 23 +++++ .../SwizzlerTestHelpers/GULProxy.m | 94 +++++++++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 GoogleUtilities/SwizzlerTestHelpers/GULProxy.h create mode 100644 GoogleUtilities/SwizzlerTestHelpers/GULProxy.m diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index 0f8a8039950..791cb5a639e 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -13,9 +13,11 @@ // limitations under the License. #import +#import #import #import +#import @interface GULObjectSwizzlerTest : XCTestCase @@ -23,6 +25,16 @@ @interface GULObjectSwizzlerTest : XCTestCase @implementation GULObjectSwizzlerTest +/** Used as a donor method to add a method that doesn't exist on the superclass. */ +- (NSString *)donorDescription { + return @"SwizzledDonorDescription"; +} + +/** Used as a donor method to add a method that exists on superclass. */ +- (NSString *)description { + return @"SwizzledDescription"; +} + /** Exists just as a donor method. */ - (void)donorMethod { } @@ -235,4 +247,47 @@ - (void)testSetGetAssociatedObjectWithoutProperAssociation { XCTAssertEqualObjects(returnedObject, associatedObject); } +- (void)testSwizzleProxiedObject { + NSObject *object = [[NSObject alloc] init]; + GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; + + XCTAssertNoThrow([swizzler swizzle]); + + XCTAssertNotEqual(object_getClass(proxyObject), [GULProxy class]); + XCTAssertTrue([object_getClass(proxyObject) isSubclassOfClass:[GULProxy class]]); + XCTAssertNoThrow([proxyObject performSelector:@selector(gul_objectSwizzler)]); + XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]); + + Class proxyObjectClass = object_getClass(proxyObject); + XCTAssertTrue([proxyObjectClass instancesRespondToSelector:@selector(gul_class)]); +} + +- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { + NSObject *object = [[NSObject alloc] init]; + GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + + GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; + [swizzler copySelector:@selector(description) + fromClass:[GULObjectSwizzlerTest class] + isClassSelector:NO]; + [swizzler swizzle]; + + XCTAssertEqual([proxyObject performSelector:@selector(description)], @"SwizzledDescription"); +} + +- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod { + NSObject *object = [[NSObject alloc] init]; + GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + + GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; + [swizzler copySelector:@selector(donorDescription) + fromClass:[GULObjectSwizzlerTest class] + isClassSelector:NO]; + [swizzler swizzle]; + + XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)], + @"SwizzledDonorDescription"); +} + @end diff --git a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m index 6b4e4111d09..eb73757ab25 100644 --- a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m +++ b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m @@ -80,7 +80,7 @@ - (instancetype)initWithObject:(id)object { __strong id swizzledObject = object; if (swizzledObject) { _swizzledObject = swizzledObject; - _originalClass = [swizzledObject class]; + _originalClass = object_getClass(object); NSString *newClassName = [NSString stringWithFormat:@"fir_%p_%@", swizzledObject, NSStringFromClass(_originalClass)]; _generatedClass = objc_allocateClassPair(_originalClass, newClassName.UTF8String, 0); @@ -134,7 +134,7 @@ - (void)swizzle { [GULSwizzledObject copyDonorSelectorsUsingObjectSwizzler:self]; - NSAssert(_originalClass == [_swizzledObject class], + NSAssert(_originalClass == object_getClass(swizzledObject), @"The original class is not the reported class now."); NSAssert(class_getInstanceSize(_originalClass) == class_getInstanceSize(_generatedClass), @"The instance size of the generated class must be equal to the original class."); diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h new file mode 100644 index 00000000000..e92aa2855b2 --- /dev/null +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@interface GULProxy : NSProxy + ++ (instancetype)proxyWithTarget:(id)target; + +@end diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m new file mode 100644 index 00000000000..178bde43be4 --- /dev/null +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GULProxy.h" + +@interface GULProxy () + +@property(nonatomic, strong) id target; + +@end + +@implementation GULProxy + +- (instancetype)initWithTarget:(id)target { + _target = target; + return self; +} + ++ (instancetype)proxyWithTarget:(id)target { + return [[GULProxy alloc] initWithTarget:target]; +} + +- (id)forwardingTargetForSelector:(SEL)selector { + return _target; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + if (_target != nil) { + [invocation setTarget:_target]; + [invocation invoke]; + } +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + return [_target respondsToSelector:aSelector]; +} + +- (BOOL)isEqual:(id)object { + return [_target isEqual:object]; +} + +- (NSUInteger)hash { + return [_target hash]; +} + +- (Class)superclass { + return [_target superclass]; +} + +- (Class)class { + return [_target class]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + return [_target isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + return [_target isMemberOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + return [_target conformsToProtocol:aProtocol]; +} + +- (BOOL)isProxy { + return YES; +} + +- (NSString *)description { + return [_target description]; +} +- (NSString *)debugDescription { + return [_target debugDescription]; +} + +@end From 9308b627f0da25d66dbc9de65f647835d5dee1bc Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 6 Nov 2018 18:50:35 -0800 Subject: [PATCH 02/11] Add logic to modify respondsToSelector: in case of Proxies --- .../Tests/Swizzler/GULObjectSwizzlerTest.m | 7 ++++--- .../ISASwizzler/GULObjectSwizzler.m | 4 ++++ .../ISASwizzler/GULSwizzledObject.m | 19 +++++++++++++++++++ .../ISASwizzler/Private/GULObjectSwizzler.h | 3 +++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index 791cb5a639e..8da85ce7054 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -256,11 +256,12 @@ - (void)testSwizzleProxiedObject { XCTAssertNotEqual(object_getClass(proxyObject), [GULProxy class]); XCTAssertTrue([object_getClass(proxyObject) isSubclassOfClass:[GULProxy class]]); + + XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_objectSwizzler)]); XCTAssertNoThrow([proxyObject performSelector:@selector(gul_objectSwizzler)]); - XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]); - Class proxyObjectClass = object_getClass(proxyObject); - XCTAssertTrue([proxyObjectClass instancesRespondToSelector:@selector(gul_class)]); + XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_class)]); + XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]); } - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { diff --git a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m index eb73757ab25..274fd6ddc94 100644 --- a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m +++ b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m @@ -154,4 +154,8 @@ - (void)dealloc { objc_disposeClassPair(_generatedClass); } +- (BOOL)isSwizzlingProxyObject { + return [_swizzledObject isProxy]; +} + @end diff --git a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m index 98a23698812..952a2051865 100644 --- a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m +++ b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#import + #import "Private/GULSwizzledObject.h" #import "Private/GULObjectSwizzler.h" @@ -26,6 +28,17 @@ @implementation GULSwizzledObject + (void)copyDonorSelectorsUsingObjectSwizzler:(GULObjectSwizzler *)objectSwizzler { [objectSwizzler copySelector:@selector(gul_objectSwizzler) fromClass:self isClassSelector:NO]; [objectSwizzler copySelector:@selector(gul_class) fromClass:self isClassSelector:NO]; + + // This is needed because NSProxy objects ususally override + // -[NSObjectProtocol respondsToSelector:] and ask this question to the underlying object. + // Since we don't swizzle the underlying object but swizzle the proxy, when someone calls + // -[NSObjectProtocol respondsToSelector:] on the proxy, the answer ends up being NO even if we + // added new methods to the subclass through ISA Swizzling. To solve that, we override + // -[NSObjectProtocol respondsToSelector:] in such a way that takes into account the fact that + // we've added new methods. + if ([objectSwizzler isSwizzlingProxyObject]) { + [objectSwizzler copySelector:@selector(respondsToSelector:) fromClass:self isClassSelector:NO]; + } } - (instancetype)init { @@ -43,4 +56,10 @@ - (Class)gul_class { return [[self gul_objectSwizzler] generatedClass]; } +// Only added to a class when we detect it is a proxy. +- (BOOL)respondsToSelector:(SEL)aSelector { + Class selfClass = object_getClass(self); + return [selfClass instancesRespondToSelector:aSelector] || [super respondsToSelector:aSelector]; +} + @end diff --git a/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h b/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h index 13e4d011dcc..d20ada1ff33 100644 --- a/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h +++ b/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h @@ -115,6 +115,9 @@ typedef OBJC_ENUM(uintptr_t, GUL_ASSOCIATION){ * the class pair. */ - (void)swizzle; +/** @return The value of -[objectBeingSwizzled isProxy] */ + - (BOOL)isSwizzlingProxyObject; + @end NS_ASSUME_NONNULL_END From 4a4135c37fec7f9c0e61ed186b9cf07981944aea Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 6 Nov 2018 18:56:50 -0800 Subject: [PATCH 03/11] Clean up the sample NSProxy --- .../Tests/Swizzler/GULObjectSwizzlerTest.m | 6 +-- .../SwizzlerTestHelpers/GULProxy.h | 3 +- .../SwizzlerTestHelpers/GULProxy.m | 38 +++++++++---------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index 8da85ce7054..948d00caa10 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -249,7 +249,7 @@ - (void)testSetGetAssociatedObjectWithoutProperAssociation { - (void)testSwizzleProxiedObject { NSObject *object = [[NSObject alloc] init]; - GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; XCTAssertNoThrow([swizzler swizzle]); @@ -266,7 +266,7 @@ - (void)testSwizzleProxiedObject { - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { NSObject *object = [[NSObject alloc] init]; - GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; [swizzler copySelector:@selector(description) @@ -279,7 +279,7 @@ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod { NSObject *object = [[NSObject alloc] init]; - GULProxy *proxyObject = [GULProxy proxyWithTarget:object]; + GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; [swizzler copySelector:@selector(donorDescription) diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h index e92aa2855b2..1d36f6b9fc3 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.h @@ -16,8 +16,9 @@ #import +/** An example NSProxy that could be used to wrap an object that we have to ISA Swizzle. */ @interface GULProxy : NSProxy -+ (instancetype)proxyWithTarget:(id)target; ++ (instancetype)proxyWithDelegate:(id)delegate; @end diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m index 178bde43be4..2d61a178daa 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m @@ -18,66 +18,66 @@ @interface GULProxy () -@property(nonatomic, strong) id target; +@property(nonatomic, strong) id delegateObject; @end @implementation GULProxy -- (instancetype)initWithTarget:(id)target { - _target = target; +- (instancetype)initWithDelegate:(id)delegate { + _delegateObject = delegate; return self; } -+ (instancetype)proxyWithTarget:(id)target { - return [[GULProxy alloc] initWithTarget:target]; ++ (instancetype)proxyWithDelegate:(id)delegate { + return [[GULProxy alloc] initWithDelegate:delegate]; } - (id)forwardingTargetForSelector:(SEL)selector { - return _target; + return _delegateObject; } - (void)forwardInvocation:(NSInvocation *)invocation { - if (_target != nil) { - [invocation setTarget:_target]; + if (_delegateObject != nil) { + [invocation setTarget:_delegateObject]; [invocation invoke]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { - return [NSObject instanceMethodSignatureForSelector:@selector(init)]; + return [_delegateObject instanceMethodSignatureForSelector:selector]; } - (BOOL)respondsToSelector:(SEL)aSelector { - return [_target respondsToSelector:aSelector]; + return [_delegateObject respondsToSelector:aSelector]; } - (BOOL)isEqual:(id)object { - return [_target isEqual:object]; + return [_delegateObject isEqual:object]; } - (NSUInteger)hash { - return [_target hash]; + return [_delegateObject hash]; } - (Class)superclass { - return [_target superclass]; + return [_delegateObject superclass]; } - (Class)class { - return [_target class]; + return [_delegateObject class]; } - (BOOL)isKindOfClass:(Class)aClass { - return [_target isKindOfClass:aClass]; + return [_delegateObject isKindOfClass:aClass]; } - (BOOL)isMemberOfClass:(Class)aClass { - return [_target isMemberOfClass:aClass]; + return [_delegateObject isMemberOfClass:aClass]; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { - return [_target conformsToProtocol:aProtocol]; + return [_delegateObject conformsToProtocol:aProtocol]; } - (BOOL)isProxy { @@ -85,10 +85,10 @@ - (BOOL)isProxy { } - (NSString *)description { - return [_target description]; + return [_delegateObject description]; } - (NSString *)debugDescription { - return [_target debugDescription]; + return [_delegateObject debugDescription]; } @end From c3ff6a4cc65f7cb3eb30db80e16dd801d511e1c7 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 7 Nov 2018 11:34:53 -0800 Subject: [PATCH 04/11] Fix the respondsToSelector: logic to handle case where someone else might ISA Swizzle after us. --- .../Tests/Swizzler/GULObjectSwizzlerTest.m | 49 +++++++++++++++++++ .../ISASwizzler/GULSwizzledObject.m | 17 +++---- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index 948d00caa10..aa3409ccc39 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -291,4 +291,53 @@ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod { @"SwizzledDonorDescription"); } +- (void)testRespondsToSelectorWorksEvenIfSwizzledProxyIsKVOd { + NSObject *object = [[NSObject alloc] init]; + GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; + + GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; + [swizzler copySelector:@selector(donorDescription) + fromClass:[GULObjectSwizzlerTest class] + isClassSelector:NO]; + [swizzler swizzle]; + + [(NSObject *)proxyObject addObserver:self + forKeyPath:NSStringFromSelector(@selector(description)) + options:0 context:NULL]; + + XCTAssertTrue([proxyObject respondsToSelector:@selector(donorDescription)]); + XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)], + @"SwizzledDonorDescription"); + + [(NSObject *)proxyObject removeObserver:self + forKeyPath:NSStringFromSelector(@selector(description))]; +} + +- (void)testRespondsToSelectorWorksEvenIfSwizzledProxyISASwizzledBySomeoneElse { + NSObject *object = [[NSObject alloc] init]; + GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; + + GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject]; + [swizzler copySelector:@selector(donorDescription) + fromClass:[GULObjectSwizzlerTest class] + isClassSelector:NO]; + [swizzler swizzle]; + + // Someone else ISA Swizzles the same object after GULObjectSwizzler. + Class originalClass = object_getClass(proxyObject); + NSString *newClassName = [NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, + NSStringFromClass(originalClass)]; + Class generatedClass = objc_allocateClassPair(originalClass, newClassName.UTF8String, 0); + objc_registerClassPair(generatedClass); + object_setClass(proxyObject, generatedClass); + + XCTAssertTrue([proxyObject respondsToSelector:@selector(donorDescription)]); + XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)], + @"SwizzledDonorDescription"); + + // Clean up. + object_setClass(proxyObject, originalClass); + objc_disposeClassPair(generatedClass); +} + @end diff --git a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m index 952a2051865..d7f75601ae8 100644 --- a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m +++ b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m @@ -29,13 +29,12 @@ + (void)copyDonorSelectorsUsingObjectSwizzler:(GULObjectSwizzler *)objectSwizzle [objectSwizzler copySelector:@selector(gul_objectSwizzler) fromClass:self isClassSelector:NO]; [objectSwizzler copySelector:@selector(gul_class) fromClass:self isClassSelector:NO]; - // This is needed because NSProxy objects ususally override - // -[NSObjectProtocol respondsToSelector:] and ask this question to the underlying object. - // Since we don't swizzle the underlying object but swizzle the proxy, when someone calls - // -[NSObjectProtocol respondsToSelector:] on the proxy, the answer ends up being NO even if we - // added new methods to the subclass through ISA Swizzling. To solve that, we override - // -[NSObjectProtocol respondsToSelector:] in such a way that takes into account the fact that - // we've added new methods. + // This is needed because NSProxy objects usually override -[NSObjectProtocol respondsToSelector:] + // and ask this question to the underlying object. Since we don't swizzle the underlying object + // but swizzle the proxy, when someone calls -[NSObjectProtocol respondsToSelector:] on the proxy, + // the answer ends up being NO even if we added new methods to the subclass through ISA Swizzling. + // To solve that, we override -[NSObjectProtocol respondsToSelector:] in such a way that takes + // into account the fact that we've added new methods. if ([objectSwizzler isSwizzlingProxyObject]) { [objectSwizzler copySelector:@selector(respondsToSelector:) fromClass:self isClassSelector:NO]; } @@ -58,8 +57,8 @@ - (Class)gul_class { // Only added to a class when we detect it is a proxy. - (BOOL)respondsToSelector:(SEL)aSelector { - Class selfClass = object_getClass(self); - return [selfClass instancesRespondToSelector:aSelector] || [super respondsToSelector:aSelector]; + Class gulClass = [[self gul_objectSwizzler] generatedClass]; + return [gulClass instancesRespondToSelector:aSelector] || [super respondsToSelector:aSelector]; } @end From ac6ca4ea8d55823fa16bdfb5aff8b9336de2289e Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 7 Nov 2018 11:46:42 -0800 Subject: [PATCH 05/11] Style --- .../Example/Tests/Swizzler/GULObjectSwizzlerTest.m | 7 ++++--- GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index aa3409ccc39..db754a023bd 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -303,7 +303,8 @@ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyIsKVOd { [(NSObject *)proxyObject addObserver:self forKeyPath:NSStringFromSelector(@selector(description)) - options:0 context:NULL]; + options:0 + context:NULL]; XCTAssertTrue([proxyObject respondsToSelector:@selector(donorDescription)]); XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)], @@ -325,8 +326,8 @@ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyISASwizzledBySomeoneElse { // Someone else ISA Swizzles the same object after GULObjectSwizzler. Class originalClass = object_getClass(proxyObject); - NSString *newClassName = [NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, - NSStringFromClass(originalClass)]; + NSString *newClassName = + [NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, NSStringFromClass(originalClass)]; Class generatedClass = objc_allocateClassPair(originalClass, newClassName.UTF8String, 0); objc_registerClassPair(generatedClass); object_setClass(proxyObject, generatedClass); diff --git a/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h b/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h index d20ada1ff33..b0a692a3302 100644 --- a/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h +++ b/GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h @@ -116,7 +116,7 @@ typedef OBJC_ENUM(uintptr_t, GUL_ASSOCIATION){ - (void)swizzle; /** @return The value of -[objectBeingSwizzled isProxy] */ - - (BOOL)isSwizzlingProxyObject; +- (BOOL)isSwizzlingProxyObject; @end From 5edd8586cef2ed1a8d1a049de4a918321a4f5db6 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 7 Nov 2018 11:50:34 -0800 Subject: [PATCH 06/11] Style --- GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m | 2 +- GoogleUtilities/SwizzlerTestHelpers/GULProxy.m | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index db754a023bd..34d80c0cddb 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -327,7 +327,7 @@ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyISASwizzledBySomeoneElse { // Someone else ISA Swizzles the same object after GULObjectSwizzler. Class originalClass = object_getClass(proxyObject); NSString *newClassName = - [NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, NSStringFromClass(originalClass)]; + [NSString stringWithFormat:@"gul_test_%p_%@", proxyObject, NSStringFromClass(originalClass)]; Class generatedClass = objc_allocateClassPair(originalClass, newClassName.UTF8String, 0); objc_registerClassPair(generatedClass); object_setClass(proxyObject, generatedClass); diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m index 2d61a178daa..d9bd693181f 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m @@ -87,6 +87,7 @@ - (BOOL)isProxy { - (NSString *)description { return [_delegateObject description]; } + - (NSString *)debugDescription { return [_delegateObject debugDescription]; } From 37fd6c8092948a7aca5abe62035ac29f3c34976f Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 7 Nov 2018 12:21:00 -0800 Subject: [PATCH 07/11] style --- .../Example/Tests/Swizzler/GULObjectSwizzlerTest.m | 2 +- GoogleUtilities/ISASwizzler/GULSwizzledObject.m | 2 +- GoogleUtilities/SwizzlerTestHelpers/GULProxy.m | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index 34d80c0cddb..c50da84205f 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -16,8 +16,8 @@ #import #import -#import #import +#import @interface GULObjectSwizzlerTest : XCTestCase diff --git a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m index d7f75601ae8..7dd27f77f9c 100644 --- a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m +++ b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m @@ -14,8 +14,8 @@ #import -#import "Private/GULSwizzledObject.h" #import "Private/GULObjectSwizzler.h" +#import "Private/GULSwizzledObject.h" NSString *kSwizzlerAssociatedObjectKey = @"gul_objectSwizzler"; diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m index d9bd693181f..7f9ea9f8e56 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m +++ b/GoogleUtilities/SwizzlerTestHelpers/GULProxy.m @@ -64,11 +64,11 @@ - (Class)superclass { return [_delegateObject superclass]; } -- (Class)class { +- (Class) class { return [_delegateObject class]; } -- (BOOL)isKindOfClass:(Class)aClass { + - (BOOL)isKindOfClass : (Class)aClass { return [_delegateObject isKindOfClass:aClass]; } From 0c1787e2e6e902e35372f1966936cddfc48ef14d Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 8 Nov 2018 10:57:56 -0800 Subject: [PATCH 08/11] Addressing readability review comments. --- .../Example/Tests/Swizzler/GULObjectSwizzlerTest.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m index c50da84205f..368516a73e4 100644 --- a/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m @@ -30,7 +30,7 @@ - (NSString *)donorDescription { return @"SwizzledDonorDescription"; } -/** Used as a donor method to add a method that exists on superclass. */ +/** Used as a donor method to add a method that exists on the superclass. */ - (NSString *)description { return @"SwizzledDescription"; } @@ -238,6 +238,7 @@ - (void)testSetGetAssociatedObjectRetainNonatomic { XCTAssertEqualObjects(returnedObject, associatedObject); } +/** Tests getting and setting an associated object with an invalid association type. */ - (void)testSetGetAssociatedObjectWithoutProperAssociation { NSObject *object = [[NSObject alloc] init]; NSDictionary *associatedObject = [[NSDictionary alloc] init]; @@ -247,6 +248,7 @@ - (void)testSetGetAssociatedObjectWithoutProperAssociation { XCTAssertEqualObjects(returnedObject, associatedObject); } +/** Tests using the GULObjectSwizzler to swizzle an object wrapped in an NSProxy. */ - (void)testSwizzleProxiedObject { NSObject *object = [[NSObject alloc] init]; GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; @@ -264,6 +266,7 @@ - (void)testSwizzleProxiedObject { XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]); } +/** Tests overriding a method that already exists on a proxied object works as expected. */ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { NSObject *object = [[NSObject alloc] init]; GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; @@ -277,6 +280,7 @@ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod { XCTAssertEqual([proxyObject performSelector:@selector(description)], @"SwizzledDescription"); } +/** Tests adding a method that doesn't exist on a proxied object works as expected. */ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod { NSObject *object = [[NSObject alloc] init]; GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; @@ -291,6 +295,7 @@ - (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod { @"SwizzledDonorDescription"); } +/** Tests KVOing a proxy object that we've ISA Swizzled works as expected. */ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyIsKVOd { NSObject *object = [[NSObject alloc] init]; GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; @@ -314,6 +319,9 @@ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyIsKVOd { forKeyPath:NSStringFromSelector(@selector(description))]; } +/** Tests that -[NSObjectProtocol resopondsToSelector:] works as expected after someone else ISA + * swizzles a proxy object that we've also ISA Swizzled. + */ - (void)testRespondsToSelectorWorksEvenIfSwizzledProxyISASwizzledBySomeoneElse { NSObject *object = [[NSObject alloc] init]; GULProxy *proxyObject = [GULProxy proxyWithDelegate:object]; From 0132d13710c135f38962513c873eb411f6f21190 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Fri, 9 Nov 2018 14:38:13 -0800 Subject: [PATCH 09/11] Update CHANGELOG --- GoogleUtilities/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index e3047d700b5..9df70acd4bf 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in an + `NSProxy`. (#1936) + # 5.3.4 - Fixed a crash caused by unprotected access to sessions in `GULNetworkURLSession` (#1964). From 86f10788a82c412acc4983c8ab315be328c6179b Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Fri, 9 Nov 2018 14:39:31 -0800 Subject: [PATCH 10/11] Fix CHANGELOG typo --- GoogleUtilities/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index 9df70acd4bf..29003029a52 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in an +- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in a `NSProxy`. (#1936) # 5.3.4 From c6a1a667c46547ff6ba84b7dccd105d9981dca88 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 12 Nov 2018 16:46:45 -0800 Subject: [PATCH 11/11] Fix CHANGELOG meta typo --- GoogleUtilities/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index 29003029a52..9df70acd4bf 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in a +- Fixed a crash caused due to `NSURLConnection` delegates being wrapped in an `NSProxy`. (#1936) # 5.3.4