diff --git a/.github/workflows/swiftlint_analyze.yml b/.github/workflows/swiftlint_analyze.yml index 9f47030..5c3c583 100644 --- a/.github/workflows/swiftlint_analyze.yml +++ b/.github/workflows/swiftlint_analyze.yml @@ -8,7 +8,7 @@ on: - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' - - 'Tests/**/*.swift' + - '!Tests/**/*.swift' - '!Tests/LinuxMain.swift' pull_request: paths: @@ -16,7 +16,7 @@ on: - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' - - 'Tests/**/*.swift' + - '!Tests/**/*.swift' - '!Tests/LinuxMain.swift' jobs: diff --git a/.swiftlint.yml b/.swiftlint.yml index 882fb5c..4ff71f0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,8 +2,6 @@ included: - Sources - Tests analyzer_rules: - - unused_import - - unused_declaration line_length: 120 identifier_name: excluded: diff --git a/Example/InterposeExample.xcodeproj/project.pbxproj b/Example/InterposeExample.xcodeproj/project.pbxproj index ae90a14..b4c355e 100644 --- a/Example/InterposeExample.xcodeproj/project.pbxproj +++ b/Example/InterposeExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */; }; 7880B124248280B300AD2251 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B123248280B300AD2251 /* AppDelegate.swift */; }; 7880B126248280B300AD2251 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B125248280B300AD2251 /* SceneDelegate.swift */; }; 7880B128248280B300AD2251 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B127248280B300AD2251 /* ViewController.swift */; }; @@ -14,10 +15,6 @@ 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12C248280B500AD2251 /* Assets.xcassets */; }; 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12E248280B500AD2251 /* LaunchScreen.storyboard */; }; 78C39DDC2483363300B46395 /* InterposeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 78C39DDB2483363300B46395 /* InterposeKit */; }; - 78C39DE22483366B00B46395 /* Defaults-Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DDE2483366B00B46395 /* Defaults-Release.xcconfig */; }; - 78C39DE32483366B00B46395 /* Defaults-Testing.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DDF2483366B00B46395 /* Defaults-Testing.xcconfig */; }; - 78C39DE42483366B00B46395 /* Defaults.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DE02483366B00B46395 /* Defaults.xcconfig */; }; - 78C39DE52483366B00B46395 /* Defaults-Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DE12483366B00B46395 /* Defaults-Debug.xcconfig */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -31,6 +28,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterposeExampleTests.swift; sourceTree = ""; }; + 781095BE248D8AD7008A943C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7880B120248280B300AD2251 /* InterposeExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7880B123248280B300AD2251 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7880B125248280B300AD2251 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -67,11 +66,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 781095BC248D8AD7008A943C /* InterposeExampleTests */ = { + isa = PBXGroup; + children = ( + 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */, + 781095BE248D8AD7008A943C /* Info.plist */, + ); + path = InterposeExampleTests; + sourceTree = ""; + }; 7880B117248280B300AD2251 = { isa = PBXGroup; children = ( 78C39DD8248335B100B46395 /* Interpose */, 7880B122248280B300AD2251 /* InterposeExample */, + 781095BC248D8AD7008A943C /* InterposeExampleTests */, 7880B121248280B300AD2251 /* Products */, 7880B14E248281D000AD2251 /* Frameworks */, ); @@ -170,7 +179,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1150; + LastUpgradeCheck = 1160; ORGANIZATIONNAME = "PSPDFKit GmbH"; TargetAttributes = { 7880B11F248280B300AD2251 = { @@ -210,10 +219,6 @@ files = ( 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */, 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */, - 78C39DE22483366B00B46395 /* Defaults-Release.xcconfig in Resources */, - 78C39DE52483366B00B46395 /* Defaults-Debug.xcconfig in Resources */, - 78C39DE32483366B00B46395 /* Defaults-Testing.xcconfig in Resources */, - 78C39DE42483366B00B46395 /* Defaults.xcconfig in Resources */, 7880B12B248280B300AD2251 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -242,6 +247,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -404,6 +410,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExample/Info.plist; @@ -423,6 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExample/Info.plist; @@ -442,6 +450,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExampleTests/Info.plist; @@ -464,6 +473,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExampleTests/Info.plist; diff --git a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme index 4bacafa..c579df2 100644 --- a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme +++ b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme @@ -1,6 +1,6 @@ + + + + AnyObject).self)(`self`, store.selector) - }} as @convention(block) (AnyObject) -> AnyObject}) - - try $0.hook("setDocumentState:", { store in { `self`, newValue in - lock.sync { - store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue) - }} as @convention(block) (AnyObject, AnyObject) -> Void}) + + try $0.hook("documentState") { (store: TypedHook<@convention(c) (AnyObject, Selector) -> AnyObject, @convention(block) (AnyObject) -> AnyObject>) in { `self` in + lock.sync { store.original(`self`, store.selector) } + } + } + + try $0.hook("setDocumentState:") { (store: TypedHook<@convention(c) (AnyObject, Selector, AnyObject) -> Void, @convention(block) (AnyObject, AnyObject) -> Void>) in { `self`, newValue in + lock.sync { store.original(`self`, store.selector, newValue) } + } + } } } catch { print("Failed to fix input system: \(error).") diff --git a/Example/InterposeExampleTests/Info.plist b/Example/InterposeExampleTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Example/InterposeExampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Example/InterposeExampleTests/InterposeExampleTests.swift b/Example/InterposeExampleTests/InterposeExampleTests.swift new file mode 100644 index 0000000..4422873 --- /dev/null +++ b/Example/InterposeExampleTests/InterposeExampleTests.swift @@ -0,0 +1,33 @@ +// +// InterposeExampleTests.swift +// InterposeExampleTests +// +// Created by Peter Steinberger on 30.05.20. +// + +import XCTest +@testable import InterposeExample + +class InterposeExampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 5974105..bd0926d 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -7,14 +7,40 @@ objects = { /* Begin PBXBuildFile section */ + 7810959E248D43DC008A943C /* ClassHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959D248D43DC008A943C /* ClassHook.swift */; }; + 781095A0248D50C1008A943C /* Watcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959F248D50C1008A943C /* Watcher.swift */; }; + 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A7248D6DFB008A943C /* AppDelegate.swift */; }; + 781095AC248D6DFB008A943C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095AB248D6DFB008A943C /* ViewController.swift */; }; + 781095AF248D6DFB008A943C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095AD248D6DFB008A943C /* Main.storyboard */; }; + 781095B1248D6DFD008A943C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 781095B0248D6DFD008A943C /* Assets.xcassets */; }; + 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */; }; + 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; + 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; + 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; + 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; - 78EDB8D5248B9BB500D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; + 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */; }; + 78E20D952497B3480021552C /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78E20D922497B3470021552C /* ITKSuperBuilder.h */; }; + 78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78E20D942497B3470021552C /* ITKSuperBuilder.m */; }; + 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; + 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; + 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */; }; + 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */; }; + 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 781095F1248E7C72008A943C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 78863EC52464B2F900BA3762; + remoteInfo = InterposeKit; + }; 78C39D7D2482CC7D00B46395 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; @@ -24,10 +50,37 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 781095F7248E7C91008A943C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 7810959D248D43DC008A943C /* ClassHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ClassHook.swift; path = Sources/InterposeKit/ClassHook.swift; sourceTree = SOURCE_ROOT; }; + 7810959F248D50C1008A943C /* Watcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Watcher.swift; path = Sources/InterposeKit/Watcher.swift; sourceTree = SOURCE_ROOT; }; + 781095A5248D6DFB008A943C /* InterposeTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 781095A7248D6DFB008A943C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 781095AB248D6DFB008A943C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 781095AE248D6DFB008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 781095B0248D6DFD008A943C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = ""; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; - 78C39D772482CC7D00B46395 /* InterposeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; + 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; + 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Sources/InterposeKit/InterposeError.swift; sourceTree = SOURCE_ROOT; }; + 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; 78C39D902483165600B46395 /* InterposeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InterposeKit.swift; path = Sources/InterposeKit/InterposeKit.swift; sourceTree = SOURCE_ROOT; }; @@ -36,10 +89,25 @@ 78C39DBF248317B400B46395 /* Defaults-Testing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Testing.xcconfig"; sourceTree = ""; }; 78C39DC0248317B400B46395 /* Defaults.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Defaults.xcconfig; sourceTree = ""; }; 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; + 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MultipleInterposing.swift; path = Tests/InterposeKitTests/MultipleInterposing.swift; sourceTree = SOURCE_ROOT; }; + 78E20D922497B3470021552C /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; + 78E20D942497B3470021552C /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyHook.swift; path = Sources/InterposeKit/AnyHook.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObjectHook.swift; path = Sources/InterposeKit/ObjectHook.swift; sourceTree = SOURCE_ROOT; }; + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinuxCompileSupport.swift; path = Sources/InterposeKit/LinuxCompileSupport.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 781095A2248D6DFB008A943C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC32464B2F900BA3762 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -58,13 +126,37 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 781095A6248D6DFB008A943C /* InterposeTestHost */ = { + isa = PBXGroup; + children = ( + 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */, + 781095A7248D6DFB008A943C /* AppDelegate.swift */, + 781095AB248D6DFB008A943C /* ViewController.swift */, + 781095AD248D6DFB008A943C /* Main.storyboard */, + 781095B0248D6DFD008A943C /* Assets.xcassets */, + 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */, + 781095B5248D6DFD008A943C /* Info.plist */, + ); + path = InterposeTestHost; + sourceTree = ""; + }; + 781095F3248E7C76008A943C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 78863EBC2464B2F900BA3762 = { isa = PBXGroup; children = ( + 78A2F26824967DB500F5AC5F /* SuperBuilder */, 78863EC82464B2F900BA3762 /* InterposeKit */, 78C39D782482CC7D00B46395 /* InterposeTests */, 78C39DBD248317B400B46395 /* Configuration */, + 781095A6248D6DFB008A943C /* InterposeTestHost */, 78863EC72464B2F900BA3762 /* Products */, + 781095F3248E7C76008A943C /* Frameworks */, ); sourceTree = ""; }; @@ -72,7 +164,8 @@ isa = PBXGroup; children = ( 78863EC62464B2F900BA3762 /* InterposeKit.framework */, - 78C39D772482CC7D00B46395 /* InterposeTests.xctest */, + 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */, + 781095A5248D6DFB008A943C /* InterposeTestHost.app */, ); name = Products; sourceTree = ""; @@ -83,15 +176,35 @@ 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, 78C39D902483165600B46395 /* InterposeKit.swift */, + 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */, + 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, + 7810959D248D43DC008A943C /* ClassHook.swift */, + 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, + 7810959F248D50C1008A943C /* Watcher.swift */, ); path = InterposeKit; sourceTree = ""; }; + 78A2F26824967DB500F5AC5F /* SuperBuilder */ = { + isa = PBXGroup; + children = ( + 78E20D912497B3470021552C /* include */, + 78E20D932497B3470021552C /* src */, + ); + name = SuperBuilder; + path = Sources/SuperBuilder; + sourceTree = ""; + }; 78C39D782482CC7D00B46395 /* InterposeTests */ = { isa = PBXGroup; children = ( - 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, + 78C39D922483169300B46395 /* InterposeKitTests.swift */, + 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, + 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, + 78A2F264249635B100F5AC5F /* KVOTests.swift */, + 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */, 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, ); path = InterposeTests; @@ -108,6 +221,22 @@ path = Configuration; sourceTree = ""; }; + 78E20D912497B3470021552C /* include */ = { + isa = PBXGroup; + children = ( + 78E20D922497B3470021552C /* ITKSuperBuilder.h */, + ); + path = include; + sourceTree = ""; + }; + 78E20D932497B3470021552C /* src */ = { + isa = PBXGroup; + children = ( + 78E20D942497B3470021552C /* ITKSuperBuilder.m */, + ); + path = src; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -116,12 +245,32 @@ buildActionMask = 2147483647; files = ( 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */, + 78E20D952497B3480021552C /* ITKSuperBuilder.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 781095A4248D6DFB008A943C /* InterposeTestHost */ = { + isa = PBXNativeTarget; + buildConfigurationList = 781095B6248D6DFD008A943C /* Build configuration list for PBXNativeTarget "InterposeTestHost" */; + buildPhases = ( + 781095A1248D6DFB008A943C /* Sources */, + 781095A2248D6DFB008A943C /* Frameworks */, + 781095A3248D6DFB008A943C /* Resources */, + 781095F7248E7C91008A943C /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 781095F2248E7C72008A943C /* PBXTargetDependency */, + ); + name = InterposeTestHost; + productName = InterposeTestHost; + productReference = 781095A5248D6DFB008A943C /* InterposeTestHost.app */; + productType = "com.apple.product-type.application"; + }; 78863EC52464B2F900BA3762 /* InterposeKit */ = { isa = PBXNativeTarget; buildConfigurationList = 78863ECE2464B2F900BA3762 /* Build configuration list for PBXNativeTarget "InterposeKit" */; @@ -140,9 +289,9 @@ productReference = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; productType = "com.apple.product-type.framework"; }; - 78C39D762482CC7D00B46395 /* InterposeTests */ = { + 78C39D762482CC7D00B46395 /* InterposeKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeTests" */; + buildConfigurationList = 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeKitTests" */; buildPhases = ( 78C39D732482CC7D00B46395 /* Sources */, 78C39D742482CC7D00B46395 /* Frameworks */, @@ -153,9 +302,9 @@ dependencies = ( 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */, ); - name = InterposeTests; + name = InterposeKitTests; productName = InterposeTests; - productReference = 78C39D772482CC7D00B46395 /* InterposeTests.xctest */; + productReference = 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -168,6 +317,9 @@ LastUpgradeCheck = 1150; ORGANIZATIONNAME = "PSPDFKit GmbH"; TargetAttributes = { + 781095A4248D6DFB008A943C = { + CreatedOnToolsVersion = 11.5; + }; 78863EC52464B2F900BA3762 = { CreatedOnToolsVersion = 11.5; LastSwiftMigration = 1150; @@ -192,12 +344,23 @@ projectRoot = ""; targets = ( 78863EC52464B2F900BA3762 /* InterposeKit */, - 78C39D762482CC7D00B46395 /* InterposeTests */, + 78C39D762482CC7D00B46395 /* InterposeKitTests */, + 781095A4248D6DFB008A943C /* InterposeTestHost */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 781095A3248D6DFB008A943C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */, + 781095B1248D6DFD008A943C /* Assets.xcassets in Resources */, + 781095AF248D6DFB008A943C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC42464B2F900BA3762 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -215,12 +378,27 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 781095A1248D6DFB008A943C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781095AC248D6DFB008A943C /* ViewController.swift in Sources */, + 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC22464B2F900BA3762 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78EDB8D5248B9BB500D2F6C1 /* TestClass.swift in Sources */, + 781095A0248D50C1008A943C /* Watcher.swift in Sources */, + 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */, + 78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */, + 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, + 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, + 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */, + 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,12 +407,22 @@ buildActionMask = 2147483647; files = ( 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */, + 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */, + 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, + 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, + 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */, + 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 781095F2248E7C72008A943C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 78863EC52464B2F900BA3762 /* InterposeKit */; + targetProxy = 781095F1248E7C72008A943C /* PBXContainerItemProxy */; + }; 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 78863EC52464B2F900BA3762 /* InterposeKit */; @@ -242,7 +430,88 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 781095AD248D6DFB008A943C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 781095AE248D6DFB008A943C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 781095B3248D6DFD008A943C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + 781095B7248D6DFD008A943C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = Y5PE65HELJ; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = InterposeTestHost/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 781095B8248D6DFD008A943C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = Y5PE65HELJ; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = InterposeTestHost/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 78863ECC2464B2F900BA3762 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */; @@ -274,6 +543,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -331,6 +601,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_NS_ASSERTIONS = NO; @@ -355,9 +626,10 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -378,9 +650,10 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -402,8 +675,10 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -412,6 +687,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -421,8 +698,10 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -431,12 +710,23 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 781095B6248D6DFD008A943C /* Build configuration list for PBXNativeTarget "InterposeTestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 781095B7248D6DFD008A943C /* Debug */, + 781095B8248D6DFD008A943C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 78863EC02464B2F900BA3762 /* Build configuration list for PBXProject "InterposeKit" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -455,7 +745,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeTests" */ = { + 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 78C39D802482CC7D00B46395 /* Debug */, diff --git a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme index 5b12cf9..ab6c07d 100644 --- a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme +++ b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme index 2627530..5e2ce04 100644 --- a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme +++ b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme @@ -1,6 +1,6 @@ @@ -52,8 +52,8 @@ diff --git a/InterposeTestHost/AppDelegate.swift b/InterposeTestHost/AppDelegate.swift new file mode 100644 index 0000000..6dbf796 --- /dev/null +++ b/InterposeTestHost/AppDelegate.swift @@ -0,0 +1,17 @@ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()! + (window!.rootViewController as? UINavigationController)?.topViewController?.title = "Test Host" + window!.makeKeyAndVisible() + return true + } + +} + diff --git a/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/InterposeTestHost/Assets.xcassets/Contents.json b/InterposeTestHost/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/InterposeTestHost/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/InterposeTestHost/Base.lproj/LaunchScreen.storyboard b/InterposeTestHost/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/InterposeTestHost/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InterposeTestHost/Base.lproj/Main.storyboard b/InterposeTestHost/Base.lproj/Main.storyboard new file mode 100644 index 0000000..0a52cef --- /dev/null +++ b/InterposeTestHost/Base.lproj/Main.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InterposeTestHost/Info.plist b/InterposeTestHost/Info.plist new file mode 100644 index 0000000..1aeceb5 --- /dev/null +++ b/InterposeTestHost/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.developer-tools + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InterposeTestHost/InterposeTestHost.entitlements b/InterposeTestHost/InterposeTestHost.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/InterposeTestHost/InterposeTestHost.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/InterposeTestHost/ViewController.swift b/InterposeTestHost/ViewController.swift new file mode 100644 index 0000000..580d46e --- /dev/null +++ b/InterposeTestHost/ViewController.swift @@ -0,0 +1,17 @@ +// +// ViewController.swift +// InterposeTestHost +// +// Created by Peter Steinberger on 07.06.20. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + } + +} + diff --git a/Package.swift b/Package.swift index 7479323..5508959 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:5.2 - +// swift-tools-version:5.0 import PackageDescription let package = Package( @@ -11,18 +10,11 @@ let package = Package( .watchOS(.v5) ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "InterposeKit", - targets: ["InterposeKit"]), + .library(name: "InterposeKit", targets: ["InterposeKit"]), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. - .target( - name: "InterposeKit"), - .testTarget( - name: "InterposeKitTests", - dependencies: ["InterposeKit"]), + .target(name: "SuperBuilder"), + .target(name: "InterposeKit", dependencies: ["SuperBuilder"]), + .testTarget(name: "InterposeKitTests", dependencies: ["InterposeKit"]), ] ) diff --git a/README.md b/README.md index 14aa075..b8217a4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -InterposeKit is a modern library to swizzle elegantly in Swift. It is [well-documented](http://interposekit.com/), [tested](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift 5.2 and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). +InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is [well-documented](http://interposekit.com/), [tested](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift 5.2 and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). @@ -32,25 +32,33 @@ class TestClass: NSObject { } let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - - print("Before Interposing \(`self`)") - - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - let string = origCall(`self`, store.selector) - - print("After Interposing \(`self`)") - - return string + " and Interpose" - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + + // You're free to skip calling the original implementation. + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + + return string + "and Interpose" + } + } } // Don't need the hook anymore? Undo is built-in! interposer.revert() + +// Want to hook just a single instance? No problem! +let hook = try testObj.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature:(@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + "just this instance" + } +} ``` Here's what we get when calling `print(TestClass().sayHi())` @@ -62,15 +70,52 @@ After Interposing Hi there 👋 and Interpose ``` -## Key Facts +## Key Features -- Interpose directly modifies the implementation of a `Method`, which is [better than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). +- Interpose directly modifies the implementation of a `Method`, which is [safer than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). +- Interpose works on classes and individual objects. - Hooks can easily be undone via calling `revert()`. This also checks and errors if someone else changed stuff in between. -- Pure Swift, no `NSInvocation`, which requires boxing and can be slow. +- Mostly Swift, no `NSInvocation`, which requires boxing and can be slow. - No Type checking. If you have a typo or forget a `convention` part, this will crash at runtime. -- Yes, you have to type the resulting type twice This is a tradeoff, else we need NSInvocation or assembly. +- Yes, you have to type the resulting type twice This is a tradeoff, else we need `NSInvocation`. - Delayed Interposing helps when a class is loaded at runtime. This is useful for [Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/). +## Object Hooking + +InterposeKit can hook classes and object. Class hooking is similar to swizzling, but object-based hooking offers a variety of new ways to set hooks. This is achieved via creating a dynamic subclass at runtime. + +Caveat: Hooking will fail with an error if the object uses KVO. The KVO machinery is fragile and it's to easy to cause a crash. Using KVO after a hook was created is supported and will not cause issues. + +## Various ways to define the signature + +Next to using `methodSignature` and `hookSignature`, following variants to define the signature are also possible: + +### methodSignature + casted block +``` +let interposer = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in + let string = store.original(`self`, store.selector) + return string + testString + } as @convention(block) (AnyObject) -> String } +} +``` + +### Define type via store object +``` +// Functions need to be `@objc dynamic` to be hookable. +let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + + // You're free to skip calling the original implementation. + let int = store.original($0, store.selector) + return int + returnIntOverrideOffset + } + } +} +``` + ## Delayed Hooking Sometimes it can be necessary to hook a class deep in a system framework, which is loaded at a later time. Interpose has a solution for this and uses a hook in the dynamic linker to be notified whenever new classes are loaded. @@ -90,13 +135,14 @@ try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { } ``` + ## FAQ ### Why didn't you call it Interpose? "Kit" feels so old-school. Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even. ### I want to hook into Swift! You made another ObjC swizzle thingy, why? -UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. +UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.) ### Can I ship this? Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". @@ -125,11 +171,15 @@ Add `github "steipete/InterposeKit"` to your `Cartfile`. - Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). - Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. -- Add object-based hooking with dynamic subclassing (Aspects again) +- Add a way to get all current hooks from an object/class. +- Add a way to revert hooks without super helper. +- Add a way to apply multiple hooks to classes +- Enable hooking of class methods. - Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions - Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. - Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ - Test against Swift Nightly as Cron Job +- Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). - I'm sure there's more - Pull Requests or [comments](https://twitter.com/steipete) very welcome! Make this happen: diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift new file mode 100644 index 0000000..829b74d --- /dev/null +++ b/Sources/InterposeKit/AnyHook.swift @@ -0,0 +1,97 @@ +import Foundation + +/// Base class, represents a hook to exactly one method. +public class AnyHook { + /// The class this hook is based on. + public let `class`: AnyClass + + /// The selector this hook interposes. + public let selector: Selector + + /// The current state of the hook. + public internal(set) var state = State.prepared + + // else we validate init order + var replacementIMP: IMP! + + // fetched at apply time, changes late, thus class requirement + var origIMP: IMP? + + /// The possible task states + public enum State: Equatable { + /// The task is prepared to be nterposed. + case prepared + + /// The method has been successfully interposed. + case interposed + + /// An error happened while interposing a method. + indirect case error(InterposeError) + } + + init(`class`: AnyClass, selector: Selector) throws { + self.selector = selector + self.class = `class` + + // Check if method exists + try validate() + } + + func replaceImplementation() throws { + preconditionFailure("Not implemented") + } + + func resetImplementation() throws { + preconditionFailure("Not implemented") + } + + /// Apply the interpose hook. + @discardableResult public func apply() throws -> AnyHook { + try execute(newState: .interposed) { try replaceImplementation() } + return self + } + + /// Revert the interpose hoook. + @discardableResult public func revert() throws -> AnyHook { + try execute(newState: .prepared) { try resetImplementation() } + return self + } + + /// Validate that the selector exists on the active class. + @discardableResult func validate(expectedState: State = .prepared) throws -> Method { + guard let method = class_getInstanceMethod(`class`, selector) else { throw InterposeError.methodNotFound(`class`, selector)} + guard state == expectedState else { throw InterposeError.invalidState(expectedState: expectedState) } + return method + } + + private func execute(newState: State, task: () throws -> Void) throws { + do { + try task() + state = newState + } catch let error as InterposeError { + state = .error(error) + throw error + } + } + + /// Release the hook block if possible. + public func cleanup() { + switch state { + case .prepared: + Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + imp_removeBlock(replacementIMP) + case .interposed: + Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + case let .error(error): + Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") + } + } +} + +/// Hook baseclass with generic signatures. +public class TypedHook: AnyHook { + /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. + public var original: MethodSignature { + preconditionFailure("Always override") + } +} diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift new file mode 100644 index 0000000..53cde26 --- /dev/null +++ b/Sources/InterposeKit/ClassHook.swift @@ -0,0 +1,40 @@ +import Foundation + +extension Interpose { + /// A hook to an instance method and stores both the original and new implementation. + final public class ClassHook: TypedHook { + /// Initialize a new hook to interpose an instance method. + public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { + try super.init(class: `class`, selector: selector) + replacementIMP = imp_implementationWithBlock(implementation(self) as Any) + } + + override func replaceImplementation() throws { + let method = try validate() + origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector) } + Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } + + override func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } + + /// The original implementation is cached at hook time. + public override var original: MethodSignature { + unsafeBitCast(origIMP, to: MethodSignature.self) + } + } +} + +#if DEBUG +extension Interpose.ClassHook: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(selector) -> \(String(describing: origIMP))" + } +} +#endif diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift new file mode 100644 index 0000000..e19a372 --- /dev/null +++ b/Sources/InterposeKit/InterposeError.swift @@ -0,0 +1,79 @@ +import Foundation + +/// The list of errors while hooking a method. +public enum InterposeError: LocalizedError { + /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. + case methodNotFound(AnyClass, Selector) + + /// The implementation could not be found. Class must be in a weird state for this to happen. + case nonExistingImplementation(AnyClass, Selector) + + /// Someone else changed the implementation; reverting removed this implementation. + /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. + case unexpectedImplementation(AnyClass, Selector, IMP?) + + /// Unable to register subclass for object-based interposing. + case failedToAllocateClassPair(class: AnyClass, subclassName: String) + + /// Unable to add method for object-based interposing. + case unableToAddMethod(AnyClass, Selector) + + /// Object-based hooking does not work if an object is using KVO. + /// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides. + /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. + case keyValueObservationDetected(AnyObject) + + /// Object is lying about it's actual class metadata. + /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. + /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. + /// + /// @note Printing classes in Swift uses the class posing mechanism. + /// Use `NSClassFromString` to get the correct name. + case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) + + /// Can't revert or apply if already done so. + case invalidState(expectedState: AnyHook.State) + + /// Unable to remove hook. + case resetUnsupported(_ reason: String) + + /// Generic failure + case unknownError(_ reason: String) +} + +extension InterposeError: Equatable { + // Lazy equating via string compare + public static func == (lhs: InterposeError, rhs: InterposeError) -> Bool { + return lhs.errorDescription == rhs.errorDescription + } + + public var errorDescription: String? { + switch self { + case .methodNotFound(let klass, let selector): + return "Method not found: -[\(klass) \(selector)]" + case .nonExistingImplementation(let klass, let selector): + return "Implementation not found: -[\(klass) \(selector)]" + case .unexpectedImplementation(let klass, let selector, let IMP): + return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" + case .failedToAllocateClassPair(let klass, let subclassName): + return "Failed to allocate class pair: \(klass), \(subclassName)" + case .unableToAddMethod(let klass, let selector): + return "Unable to add method: -[\(klass) \(selector)]" + case .keyValueObservationDetected(let obj): + return "Unable to hook object that uses Key Value Observing: \(obj)" + case .objectPosingAsDifferentClass(let obj, let actualClass): + return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" + case .invalidState(let expectedState): + return "Invalid State. Expected: \(expectedState)" + case .resetUnsupported(let reason): + return "Reset Unsupported: \(reason)" + case .unknownError(let reason): + return reason + } + } + + @discardableResult func log() -> InterposeError { + Interpose.log(self.errorDescription!) + return self + } +} diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 9fac914..f57305d 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,27 +1,64 @@ -// -// Interpose.swift -// InterposeKit -// -// Copyright © 2020 Peter Steinberger. All rights reserved. -// - import Foundation -#if !os(Linux) -import MachO.dyld -#endif +extension NSObject { + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> AnyHook { + + if let klass = self as? AnyClass { + return try Interpose.ClassHook(class: klass, selector: selector, implementation: implementation).apply() + } else { + return try Interpose.ObjectHook(object: self, selector: selector, implementation: implementation).apply() + } + } + + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public class func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> AnyHook { + return try Interpose.ClassHook(class: self as AnyClass, selector: selector, implementation: implementation).apply() + } +} -/// Helper to swizzle methods the right way, via replacing the IMP. +/// Interpose is a modern library to swizzle elegantly in Swift. +/// +/// Methods are hooked via replacing the implementation, instead of the usual exchange. +/// Supports both swizzling classes and individual objects. final public class Interpose { - /// Stores swizzle tasks and executes them at once. + /// Stores swizzle hooks and executes them at once. public let `class`: AnyClass - /// Lists all tasks for the current interpose class object. - public private(set) var tasks: [Task] = [] + /// Lists all hooks for the current interpose class object. + public private(set) var hooks: [AnyHook] = [] + + /// If Interposing is object-based, this is set. + public let object: AnyObject? + + // Checks if a object is posing as a different class + // via implementing 'class' and returning something else. + private func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? { + let perceivedClass: AnyClass = type(of: object) + let actualClass: AnyClass = object_getClass(object)! + if actualClass != perceivedClass { + return actualClass + } + return nil + } + + // This is based on observation, there is no documented way + private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { + NSStringFromClass(klass).hasPrefix("NSKVO") + } /// Initializes an instance of Interpose for a specific class. /// If `builder` is present, `apply()` is automatically called. public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws { self.class = `class` + self.object = nil // Only apply if a builder is present if let builder = builder { @@ -29,170 +66,82 @@ final public class Interpose { } } - deinit { - tasks.forEach({ $0.cleanup() }) + /// Initialize with a single object to interpose. + public init(_ object: NSObject, builder: ((Interpose) throws -> Void)? = nil) throws { + self.object = object + self.class = type(of: object) + + if let actualClass = checkObjectPosingAsDifferentClass(object) { + if isKVORuntimeGeneratedClass(actualClass) { + throw InterposeError.keyValueObservationDetected(object) + } else { + throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) + } + } + + // Only apply if a builder is present + if let builder = builder { + try apply(builder) + } } + deinit { + hooks.forEach({ $0.cleanup() }) + } + /// Hook an `@objc dynamic` instance method via selector name on the current class. - @discardableResult public func hook(_ selName: String, - _ implementation: (Task) -> Any) throws -> Task { - try hook(NSSelectorFromString(selName), implementation) + @discardableResult public func hook( + _ selName: String, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> TypedHook { + try hook(NSSelectorFromString(selName), methodSignature: methodSignature, hookSignature: hookSignature, implementation) } /// Hook an `@objc dynamic` instance method via selector on the current class. - @discardableResult public func hook(_ selector: Selector, - _ implementation: (Task) -> Any) throws -> Task { - let task = try Task(class: `class`, selector: selector, implementation: implementation) - tasks.append(task) - return task + @discardableResult public func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> TypedHook { + + var hook: TypedHook + if let object = self.object { + hook = try ObjectHook(object: object, selector: selector, implementation: implementation) + } else { + hook = try ClassHook(class: `class`, selector: selector, implementation: implementation) + } + hooks.append(hook) + return hook } /// Apply all stored hooks. - @discardableResult public func apply(_ task: ((Interpose) throws -> Void)? = nil) throws -> Interpose { - try execute(task) { try $0.apply() } + @discardableResult public func apply(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { + try execute(hook) { try $0.apply() } } /// Revert all stored hooks. - @discardableResult public func revert(_ task: ((Interpose) throws -> Void)? = nil) throws -> Interpose { - try execute(task, expectedState: .interposed) { try $0.revert() } + @discardableResult public func revert(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { + try execute(hook, expectedState: .interposed) { try $0.revert() } } private func execute(_ task: ((Interpose) throws -> Void)? = nil, - expectedState: Task.State = .prepared, - executor: ((Task) throws -> Void)) throws -> Interpose { + expectedState: AnyHook.State = .prepared, + executor: ((AnyHook) throws -> Void)) throws -> Interpose { // Run pre-apply code first if let task = task { try task(self) } // Validate all tasks, stop if anything is not valid - guard tasks.allSatisfy({ + guard hooks.allSatisfy({ (try? $0.validate(expectedState: expectedState)) != nil }) else { - throw Error.invalidState + throw InterposeError.invalidState(expectedState: expectedState) } // Execute all tasks - try tasks.forEach(executor) + try hooks.forEach(executor) return self } - - /// The list of errors while hooking a method. - public enum Error: Swift.Error { - /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. - case methodNotFound - - /// The implementation could not be found. Class must be in a weird state for this to happen. - case nonExistingImplementation - - /// Someone else changed the implementation; reverting removed this implementation. - /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. - case unexpectedImplementation - - /// Can't revert or apply if already done so. - case invalidState - } -} - -// MARK: Interpose Task - -extension Interpose { - /// A task represents a hook to an instance method and stores both the original and new implementation. - final public class Task { - /// The class this tasks operates on. - public let `class`: AnyClass - - /// The selector this tasks operates on. - public let selector: Selector - - /// The original implementation is set once the swizzling is complete. - public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement - - /// The replacement implementation is created on initialization time. - public private(set) var replacementIMP: IMP! // else we validate init order - - /// The state of the interpose operation. - public private(set) var state = State.prepared - - /// The possible task states. - public enum State: Equatable { - /// The task is prepared to be interposed. - case prepared - - /// The method has been successfully interposed. - case interposed - - /// An error happened while interposing a method. - case error(Error) - } - - /// Initialize a new task to interpose an instance method. - public init(`class`: AnyClass, selector: Selector, implementation: (Task) -> Any) throws { - self.selector = selector - self.class = `class` - // Check if method exists - try validate() - replacementIMP = imp_implementationWithBlock(implementation(self)) - } - - /// Validate that the selector exists on the active class. - @discardableResult func validate(expectedState: State = .prepared) throws -> Method { - guard let method = class_getInstanceMethod(`class`, selector) else { throw Error.methodNotFound } - guard state == expectedState else { throw Error.invalidState } - return method - } - - /// Apply the interpose hook. - public func apply() throws { - try execute(newState: .interposed) { try replaceImplementation() } - } - - /// Revert the interpose hoook. - public func revert() throws { - try execute(newState: .prepared) { try resetImplementation() } - } - - /// Release the hook block if possible. - fileprivate func cleanup() { - switch state { - case .prepared: - Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - imp_removeBlock(replacementIMP) - case .interposed: - Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - case let .error(error): - Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") - } - } - - private func execute(newState: State, task: () throws -> Void) throws { - do { - try task() - state = newState - } catch let error as Error { - state = .error(error) - throw error - } - } - - private func replaceImplementation() throws { - let method = try validate() - origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw Error.nonExistingImplementation } - Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") - } - - private func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw Error.unexpectedImplementation } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") - } - - /// Convenience to call the original implementation. - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) - } - } } // MARK: Logging @@ -202,144 +151,9 @@ extension Interpose { public static var isLoggingEnabled = false /// Simple log wrapper for print. - fileprivate class func log(_ object: Any) { + class func log(_ object: Any) { if isLoggingEnabled { print("[Interposer] \(object)") } } } - -// MARK: Interpose Class Load Watcher - -extension Interpose { - // Separate definitions to have more eleveant calling syntax when completion is not needed. - - /// Interpose a class once available. Class is passed via `classParts` string array. - @discardableResult public class func whenAvailable(_ classParts: [String], - builder: @escaping (Interpose) throws -> Void) throws -> Waiter { - try whenAvailable(classParts, builder: builder, completion: nil) - } - - /// Interpose a class once available. Class is passed via `classParts` string array, with completion handler. - @discardableResult public class func whenAvailable(_ classParts: [String], - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws -> Waiter { - try whenAvailable(classParts.joined(), builder: builder, completion: completion) - } - - /// Interpose a class once available. Class is passed via `className` string. - @discardableResult public class func whenAvailable(_ className: String, - builder: @escaping (Interpose) throws -> Void) throws -> Waiter { - try whenAvailable(className, builder: builder, completion: nil) - } - - /// Interpose a class once available. Class is passed via `className` string, with completion handler. - @discardableResult public class func whenAvailable(_ className: String, - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws -> Waiter { - try Waiter(className: className, builder: builder, completion: completion) - } - - /// Helper that stores tasks to a specific class and executes them once the class becomes available. - public struct Waiter { - fileprivate let className: String - private var builder: ((Interpose) throws -> Void)? - private var completion: (() -> Void)? - - /// Initialize waiter object. - @discardableResult init(className: String, - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws { - self.className = className - self.builder = builder - self.completion = completion - - // Immediately try to execute task. If not there, install waiter. - if try tryExecute() == false { - InterposeWatcher.append(waiter: self) - } - } - - func tryExecute() throws -> Bool { - guard let `class` = NSClassFromString(className), let builder = self.builder else { return false } - try Interpose(`class`).apply(builder) - if let completion = self.completion { - completion() - } - return true - } - } -} - -// dyld C function cannot capture class context so we pack it in a static struct. -private struct InterposeWatcher { - // Global list of waiters; can be multiple per class - private static var globalWatchers: [Interpose.Waiter] = { - // Register after Swift global registers to not deadlock - DispatchQueue.main.async { InterposeWatcher.installGlobalImageLoadWatcher() } - return [] - }() - - fileprivate static func append(waiter: Interpose.Waiter) { - InterposeWatcher.globalWatcherQueue.sync { - globalWatchers.append(waiter) - } - } - - // Register hook when dyld loads an image - private static let globalWatcherQueue = DispatchQueue(label: "com.steipete.global-image-watcher") - private static func installGlobalImageLoadWatcher() { - _dyld_register_func_for_add_image { _, _ in - InterposeWatcher.globalWatcherQueue.sync { - // this is called on the thread the image is loaded. - InterposeWatcher.globalWatchers = InterposeWatcher.globalWatchers.filter { waiter -> Bool in - do { - if try waiter.tryExecute() == false { - return true // only collect if this fails because class is not there yet - } else { - Interpose.log("\(waiter.className) was successful.") - } - } catch { - Interpose.log("Error while executing task: \(error).") - // We can't bubble up the throw into the C context. - #if DEBUG - // Instead of silently eating, it's better to crash in DEBUG. - fatalError("Error while executing task: \(error).") - #endif - } - return false - } - } - } - } -} - -// MARK: Debug Helper - -#if DEBUG -extension Interpose.Task: CustomDebugStringConvertible { - public var debugDescription: String { - return "\(selector) -> \(String(describing: origIMP))" - } -} -#endif - -#if os(Linux) -// Linux is used to create Jazzy docs -/// :nodoc: Selector -public struct Selector {} -/// :nodoc: IMP -public struct IMP: Equatable {} -/// :nodoc: Method -public struct Method {} -func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector() } -func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } -// swiftlint:disable:next line_length -func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } -// swiftlint:disable:next identifier_name -func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } -// swiftlint:disable:next identifier_name -func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} -func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } -func imp_removeBlock(_ anImp: IMP) -> Bool { false } -#endif diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift new file mode 100644 index 0000000..43f901c --- /dev/null +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -0,0 +1,65 @@ +import Foundation + +// Linux is used to create Jazzy docs +#if os(Linux) +/// :nodoc: Selector +public struct Selector: Equatable { + var name: String? + init(_ name: String) { self.name = name } +} +/// :nodoc: IMP +public struct IMP: Equatable {} +/// :nodoc: Method +public struct Method {} +func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector("") } +func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } +func class_getMethodImplementation(_ cls: AnyClass?, _ name: Selector) -> IMP? { return nil } +func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, + _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } +func class_addMethod(_ cls: AnyClass?, _ name: Selector, + _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } +func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } +func object_getClass(_ obj: Any?) -> AnyClass? { return nil } +@discardableResult func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } +func method_getName(_ method: Method) -> Selector { Selector("") } +func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } +// swiftlint:disable:next identifier_name +func method_getTypeEncoding(_ method: Method) -> UnsafePointer? { return nil } +func method_getImplementation(_ method: Method) -> IMP { IMP() } +// swiftlint:disable:next identifier_name +func _dyld_register_func_for_add_image(_ func: + (@convention(c) (UnsafePointer?, Int) -> Void)!) {} +func objc_allocateClassPair(_ superclass: AnyClass?, + _ name: UnsafePointer, + _ extraBytes: Int) -> AnyClass? { return nil } +func objc_registerClassPair(_ cls: AnyClass) {} +func objc_getClass(_: UnsafePointer!) -> Any! { return nil } +func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } +func imp_getBlock(_ anImp: IMP) -> Any? { return nil } +@discardableResult func imp_removeBlock(_ anImp: IMP) -> Bool { false } +@objc class NSError: NSObject {} +// AutoreleasingUnsafeMutablePointer is not available on Linux. +typealias NSErrorPointer = UnsafeMutablePointer? +extension NSObject { + /// :nodoc: value + open func value(forKey key: String) -> Any? { return nil } +} +/// :nodoc: objc_AssociationPolicy +// swiftlint:disable:next type_name +enum objc_AssociationPolicy: UInt { + // swiftlint:disable:next identifier_name + case OBJC_ASSOCIATION_ASSIGN = 0 + // swiftlint:disable:next identifier_name + case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 + // swiftlint:disable:next identifier_name + case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 + // swiftlint:disable:next identifier_name + case OBJC_ASSOCIATION_RETAIN = 769 + // swiftlint:disable:next identifier_name + case OBJC_ASSOCIATION_COPY = 771 +} +func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, + _ value: Any?, _ policy: objc_AssociationPolicy) {} +func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? +{ return nil } +#endif diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift new file mode 100644 index 0000000..85bd0b0 --- /dev/null +++ b/Sources/InterposeKit/ObjectHook.swift @@ -0,0 +1,322 @@ +import Foundation + +extension Interpose { + + private enum Constants { + static let subclassSuffix = "InterposeKit_" + } + + enum ObjCSelector { + static let getClass = Selector((("class"))) + } + + enum ObjCMethodEncoding { + static let getClass = extract("#@:") + + private static func extract(_ string: StaticString) -> UnsafePointer { + return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) + } + } + + struct AssociatedKeys { + static var hookForBlock: UInt8 = 0 + } + + class WeakObjectContainer: NSObject { + private weak var _object: T? + + var object: T? { + return _object + } + init(with object: T?) { + _object = object + } + } + + private static func isSupportedArchitectureForSuper() -> Bool { + #if os(Linux) + return false + #else + return NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false + #endif + } + + /// A hook to an instance method of a single object, stores both the original and new implementation. + /// Think about: Multiple hooks for one object + final public class ObjectHook: TypedHook { + + /// The object that is being hooked. + public let object: AnyObject + + /// Subclass that we create on the fly + var dynamicSubclass: AnyClass? + + // Logic switch to use super builder + let generatesSuperIMP = isSupportedArchitectureForSuper() + + /// Initialize a new hook to interpose an instance method. + public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { + self.object = object + try super.init(class: type(of: object), selector: selector) + let block = implementation(self) as AnyObject + replacementIMP = imp_implementationWithBlock(block) + guard replacementIMP != nil else { + throw InterposeError.unknownError("imp_implementationWithBlock failed for \(block) - slots exceeded?") + } + + // Weakly store reference to hook inside the block of the IMP. + objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, WeakObjectContainer(with: self), .OBJC_ASSOCIATION_RETAIN) + } + + // Finds the hook to a given implementation. + private func hookForIMP(_ imp: IMP) -> ObjectHook? { + // Get the block that backs our IMP replacement + guard let block = imp_getBlock(imp) else { return nil } + let container = objc_getAssociatedObject(block, &AssociatedKeys.hookForBlock) as? WeakObjectContainer> + return container?.object + } + + // /// Release the hook block if possible. + // public override func cleanup() { + // // remove subclass! + // super.cleanup() + // } + + /// We need to reuse a dynamic subclass if the object already has one. + private func getExistingSubclass() -> AnyClass? { + let actualClass: AnyClass = object_getClass(object)! + if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) { + return actualClass + } + return nil + } + + /// Creates a unique dynamic subclass of the current object + private func createSubclass() throws -> AnyClass { + + // If the class has been altered (e.g. via NSKVONotifying_ KVO logic) + // then perceived and actual class don't match. + // + // Making KVO and Object-based hooking work at the same time is difficult. + // If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp. + + let perceivedClass: AnyClass = `class` + let actualClass: AnyClass = object_getClass(object)! + + let className = NSStringFromClass(perceivedClass) + // Right now we are wasteful. Might be able to optimize for shared IMP? + let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") + let subclassName = Constants.subclassSuffix + className + uuid + + let subclass: AnyClass? = subclassName.withCString { cString in + // swiftlint:disable:next force_cast + if let existingClass = objc_getClass(cString) as! AnyClass? { + return existingClass + } else { + if let subclass: AnyClass = objc_allocateClassPair(actualClass, cString, 0) { + replaceGetClass(in: subclass, decoy: perceivedClass) + objc_registerClassPair(subclass) + return subclass + } else { + return nil + } + } + } + + guard let nonnullSubclass = subclass else { + throw InterposeError.failedToAllocateClassPair(class: perceivedClass, subclassName: subclassName) + } + + object_setClass(object, nonnullSubclass) + Interpose.log("Generated \(NSStringFromClass(nonnullSubclass)) for object (was: \(NSStringFromClass(class_getSuperclass(object_getClass(object)!)!)))") + return nonnullSubclass + } + + private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { + #if !os(Linux) // crashes on linux + let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in + perceivedClass + } + let impl = imp_implementationWithBlock(getClass as Any) + _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + #endif + } + + #if !os(Linux) + private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { + let handle = dlopen(nil, RTLD_LAZY) + let imp = dlsym(handle, "IKTAddSuperImplementationToClass") + return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self) + }() + + private func addSuperTrampolineMethod(subclass: AnyClass) { + var error: NSError? + if addSuperImpl(subclass, self.selector, &error) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]: \(error!)") + } else { + let imp = class_getMethodImplementation(subclass, self.selector)! + Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") + } + } + #else + private func addSuperTrampolineMethod(subclass: AnyClass) { } + #endif + + /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. + public override var original: MethodSignature { + // If we switched implementations, return stored. + if let savedOrigIMP = origIMP { + return unsafeBitCast(savedOrigIMP, to: MethodSignature.self) + } + // Else, perform a dynamic lookup + guard let origIMP = lookupOrigIMP else { InterposeError.nonExistingImplementation(`class`, selector).log() + preconditionFailure("IMP must be found for call") + } + return origIMP + } + + /// We look for the parent IMP dynamically, so later modifications to the class are no problem. + private var lookupOrigIMP: MethodSignature? { + var currentClass: AnyClass? = self.class + repeat { + if let currentClass = currentClass, + let method = class_getInstanceMethod(currentClass, self.selector) { + let origIMP = method_getImplementation(method) + return unsafeBitCast(origIMP, to: MethodSignature.self) + } + currentClass = class_getSuperclass(currentClass) + } while currentClass != nil + return nil + } + + /// Looks for an instance method in the exact class, without looking up the hierarchy. + func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { + var methodCount : CUnsignedInt = 0 + guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } + defer { free(methodsInAClass) } + for index in 0 ..< Int(methodCount) { + let method = methodsInAClass[index] + if method_getName(method) == selector { + return true + } + } + return false + } + + override func replaceImplementation() throws { + let method = try validate() + + // Check if there's an existing subclass we can reuse. + // Create one at runtime if there is none. + dynamicSubclass = try getExistingSubclass() ?? createSubclass() + + // The implementation of the call that is hooked must exist. + guard lookupOrigIMP != nil else { + throw InterposeError.nonExistingImplementation(`class`, selector).log() + } + + // This function searches superclasses for implementations + let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass!, selector) + let encoding = method_getTypeEncoding(method) + + if self.generatesSuperIMP { + // If the subclass is empty, we create a super trampoline first. + // If a hook already exists, we must skip this. + if !hasExistingMethod { + addSuperTrampolineMethod(subclass: dynamicSubclass!) + } + + // Replace IMP (by now we guarantee that it exists) + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) + guard origIMP != nil else { + throw InterposeError.nonExistingImplementation(dynamicSubclass!, selector) + } + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } else { + // Could potentially be unified in the code paths + if hasExistingMethod { + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if origIMP != nil { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") + } else { + Interpose.log("Unable to replace: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + throw InterposeError.unableToAddMethod(`class`, selector) + } + } else { + let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if didAddMethod { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + } else { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + throw InterposeError.unableToAddMethod(`class`, selector) + } + } + } + } + + // Find the hook above us (not necessarily topmost) + private func findNextHook(_ topmostIMP: IMP) -> ObjectHook? { + // We are not topmost hook, so find the hook above us! + var impl: IMP? = topmostIMP + var currentHook: ObjectHook? + repeat { + // get topmost hook + let hook = hookForIMP(impl!) + if hook === self { + // return parent + return currentHook + } + // crawl down the chain until we find ourselves + currentHook = hook + impl = hook?.origIMP + } while impl != nil + return nil + } + + override func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + + guard super.origIMP != nil else { + // Removing methods at runtime is not supported. + // https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 + // + // This codepath will be hit if the super helper is missing. + // We could recreate the whole class at runtime and rebuild all hooks, + // but that seesm excessive when we have a trampoline at our disposal. + Interpose.log("Reset of -[\(`class`).\(selector)] not supported. No Original IMP") + throw InterposeError.resetUnsupported("No Original IMP found. SuperBuilder missing?") + } + + guard let currentIMP = class_getMethodImplementation(dynamicSubclass!, selector) else { + throw InterposeError.unknownError("No Implementation found") + } + + // We are the topmost hook, replace method. + if currentIMP == replacementIMP { + let previousIMP = class_replaceMethod(dynamicSubclass!, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(dynamicSubclass!, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } else { + let nextHook = findNextHook(currentIMP) + // Replace next's original IMP + nextHook?.origIMP = self.origIMP + } + + // FUTURE: remove class pair! + // This might fail if we get KVO observed. + // objc_disposeClassPair does not return a bool but logs if it fails. + // + // objc_disposeClassPair(dynamicSubclass) + // self.dynamicSubclass = nil + } + } +} + +#if DEBUG +extension Interpose.ObjectHook: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(selector) of \(object) -> \(String(describing: original))" + } +} +#endif diff --git a/Sources/InterposeKit/Watcher.swift b/Sources/InterposeKit/Watcher.swift new file mode 100644 index 0000000..c985fd8 --- /dev/null +++ b/Sources/InterposeKit/Watcher.swift @@ -0,0 +1,110 @@ +import Foundation + +#if !os(Linux) +import MachO.dyld +#endif + +// MARK: Interpose Class Load Watcher + +extension Interpose { + // Separate definitions to have more eleveant calling syntax when completion is not needed. + + /// Interpose a class once available. Class is passed via `classParts` string array. + @discardableResult public class func whenAvailable(_ classParts: [String], + builder: @escaping (Interpose) throws -> Void) throws -> Waiter { + try whenAvailable(classParts, builder: builder, completion: nil) + } + + /// Interpose a class once available. Class is passed via `classParts` string array, with completion handler. + @discardableResult public class func whenAvailable(_ classParts: [String], + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws -> Waiter { + try whenAvailable(classParts.joined(), builder: builder, completion: completion) + } + + /// Interpose a class once available. Class is passed via `className` string. + @discardableResult public class func whenAvailable(_ className: String, + builder: @escaping (Interpose) throws -> Void) throws -> Waiter { + try whenAvailable(className, builder: builder, completion: nil) + } + + /// Interpose a class once available. Class is passed via `className` string, with completion handler. + @discardableResult public class func whenAvailable(_ className: String, + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws -> Waiter { + try Waiter(className: className, builder: builder, completion: completion) + } + + /// Helper that stores hooks to a specific class and executes them once the class becomes available. + public struct Waiter { + fileprivate let className: String + private var builder: ((Interpose) throws -> Void)? + private var completion: (() -> Void)? + + /// Initialize waiter object. + @discardableResult init(className: String, + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws { + self.className = className + self.builder = builder + self.completion = completion + + // Immediately try to execute task. If not there, install waiter. + if try tryExecute() == false { + InterposeWatcher.append(waiter: self) + } + } + + func tryExecute() throws -> Bool { + guard let `class` = NSClassFromString(className), let builder = self.builder else { return false } + try Interpose(`class`).apply(builder) + if let completion = self.completion { + completion() + } + return true + } + } +} + +// dyld C function cannot capture class context so we pack it in a static struct. +private struct InterposeWatcher { + // Global list of waiters; can be multiple per class + private static var globalWatchers: [Interpose.Waiter] = { + // Register after Swift global registers to not deadlock + DispatchQueue.main.async { InterposeWatcher.installGlobalImageLoadWatcher() } + return [] + }() + + fileprivate static func append(waiter: Interpose.Waiter) { + InterposeWatcher.globalWatcherQueue.sync { + globalWatchers.append(waiter) + } + } + + // Register hook when dyld loads an image + private static let globalWatcherQueue = DispatchQueue(label: "com.steipete.global-image-watcher") + private static func installGlobalImageLoadWatcher() { + _dyld_register_func_for_add_image { _, _ in + InterposeWatcher.globalWatcherQueue.sync { + // this is called on the thread the image is loaded. + InterposeWatcher.globalWatchers = InterposeWatcher.globalWatchers.filter { waiter -> Bool in + do { + if try waiter.tryExecute() == false { + return true // only collect if this fails because class is not there yet + } else { + Interpose.log("\(waiter.className) was successful.") + } + } catch { + Interpose.log("Error while executing task: \(error).") + // We can't bubble up the throw into the C context. + #if DEBUG + // Instead of silently eating, it's better to crash in DEBUG. + fatalError("Error while executing task: \(error).") + #endif + } + return false + } + } + } + } +} diff --git a/Sources/SuperBuilder/include/ITKSuperBuilder.h b/Sources/SuperBuilder/include/ITKSuperBuilder.h new file mode 100644 index 0000000..8a988bb --- /dev/null +++ b/Sources/SuperBuilder/include/ITKSuperBuilder.h @@ -0,0 +1,75 @@ +#if __APPLE__ +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** +Adds an empty super implementation instance method to originalClass. +If a method already exists, this will return NO and a descriptive error message. + +Example: You have an empty UIViewController subclass and call this with viewDidLoad as selector. +The result will be code that looks similar to this: + +override func viewDidLoad() { + super.viewDidLoad() +} + +What the compiler creates in following code: + +- (void)viewDidLoad { + struct objc_super _super = { + .receiver = self, + .super_class = object_getClass(obj); + }; + objc_msgSendSuper2(&_super, _cmd); +} + +There are a few important details: + +1) We use objc_msgSendSuper2, not objc_msgSendSuper. + The difference is minor, but important. + objc_msgSendSuper starts looking at the current class, which would cause an endless loop + objc_msgSendSuper2 looks for the superclass. + +2) This uses a completely dynamic lookup. + While slightly slower, this is resilient even if you change superclasses later on. + +3) The resolution method calls out to C, so it could be customized to jump over specific implementations. + (Such API is not currently exposed) + +4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. + This is currently implemented architectures are x86_64 and arm64. + armv7 was dropped in OS 11 and i386 with macOS Catalina. + +@see https://steipete.com/posts/calling-super-at-runtime/ +*/ +@interface SuperBuilder : NSObject + +/// Adds an empty super implementation instance method to originalClass. +/// If a method already exists, this will return NO and a descriptive error message. ++ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; + +/// Check if the instance method in `originalClass` is a super trampoline. ++ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector; + +/// x86-64 and ARM64 are currently supported. +@property(class, readonly) BOOL isSupportedArchitecure; + +#if (defined (__arm64__) || defined (__x86_64__)) && __APPLE__ +/// Helper that does not exist if architecture is not supported. ++ (BOOL)isCompileTimeSupportedArchitecure; +#endif + +@end + +NSString *const SuperBuilderErrorDomain; + +typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { + SuperBuilderErrorCodeArchitectureNotSupported, + SuperBuilderErrorCodeNoSuperClass, + SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, + SuperBuilderErrorCodeFailedToAddMethod +}; + +NS_ASSUME_NONNULL_END diff --git a/Sources/SuperBuilder/src/ITKSuperBuilder.m b/Sources/SuperBuilder/src/ITKSuperBuilder.m new file mode 100644 index 0000000..9101052 --- /dev/null +++ b/Sources/SuperBuilder/src/ITKSuperBuilder.m @@ -0,0 +1,348 @@ +#if __APPLE__ +#import "ITKSuperBuilder.h" + +@import ObjectiveC.message; +@import ObjectiveC.runtime; + +NS_ASSUME_NONNULL_BEGIN + +NSString *const SuperBuilderErrorDomain = @"com.steipete.superbuilder"; + +void msgSendSuperTrampoline(void); +void msgSendSuperStretTrampoline(void); + +#define let const __auto_type +#define var __auto_type + +static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { + BOOL requiresStructDispatch = NO; + #if defined (__arm64__) + // ARM64 doesn't use stret dispatch. Yay! + #elif defined (__x86_64__) + // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers + // + // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. + NSUInteger returnTypeActualSize = 0; + NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); + requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2); + #else + // Unknown architecture + // https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/ + // watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386. + // See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html + #endif + + return requiresStructDispatch ? (IMP)msgSendSuperStretTrampoline : (IMP)msgSendSuperTrampoline; +} + +// Helper for binding with Swift +BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error); +BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error) { + return [SuperBuilder addSuperInstanceMethodToClass:originalClass selector:selector error:error]; +} + +#define ERROR_AND_RETURN(CODE, STRING)\ +if (error) { *error = [NSError errorWithDomain:SuperBuilderErrorDomain code:CODE userInfo:@{NSLocalizedDescriptionKey: STRING}];} return NO; + +@implementation SuperBuilder + ++ (BOOL)isSupportedArchitecure { +#if defined (__arm64__) || defined (__x86_64__) + return YES; +#else + return NO; +#endif +} + +#if defined (__arm64__) || defined (__x86_64__) ++ (BOOL)isCompileTimeSupportedArchitecure { + return [self isSupportedArchitecure]; +} +#endif + ++ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector { + // No architecture check needed - will just be NO. + let method = class_getInstanceMethod(originalClass, selector); + return ITKMethodIsSuperTrampoline(method); +} + ++ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error { + if (!self.isSupportedArchitecure) { + let msg = @"Unsupported Architecture. (Support includes ARM64 and x86-64 )"; + ERROR_AND_RETURN(SuperBuilderErrorCodeArchitectureNotSupported, msg) + } + + // Check that class has a superclass + let superClass = class_getSuperclass(originalClass); + if (superClass == nil) { + let msg = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeNoSuperClass, msg) + } + + // Fetch method called with super + let method = class_getInstanceMethod(superClass, selector); + if (method == NULL) { + let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg) + } + + // Add trampoline + let typeEncoding = method_getTypeEncoding(method); + let trampoline = ITKGetTrampolineForTypeEncoding(typeEncoding); + let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding); + if (!methodAdded) { + let msg = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeFailedToAddMethod, msg) + } + return methodAdded; +} + +// Control if the trampoline should also push/pop the floating point registers. +// This is slightly slower and not needed for our simple implementation +// However, even if you just use memcpy, you will want to enable this. +// We keep this enabled to be doubly safe. +#define PROTECT_FLOATING_POINT_REGISTERS 1 + +// One thread local per thread should be enough +_Thread_local struct objc_super _threadSuperStorage; + +static BOOL ITKMethodIsSuperTrampoline(Method method) { + let methodIMP = method_getImplementation(method); + return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline; +} + +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) { + /** + Assume you have a class hierarchy made of four classes `Level1` <- `Level2` <- `Level3` <- `Level4`, + with `Level1` implementing a method called `-sayHello`, not implemented elsewhere in descendants classes. + + If you use: `[SuperBuilder addSuperInstanceMethodToClass:Level2.class selector:@selector(sayHello) error:NULL];` + to inject a _dummy_ implementation at `Level2`, the following will happen: + + - Calling `-[Level2 sayHello]` works. The trampoline is called, the `super_class ` is found to be `Level1`, and the `-sayHello` parent implementation is called. + - Calling `-[LevelN sayHello]` for any N > 2 ends in an infinite recursion. Since the `obj` passed to the trampoline is a descendant of `Level2`, `objc_msgSendSuper2` will of course call the injected implementation on `Level2`, which in turn will call itself with the same arguments, again and again. + + This is fixed by walking up the hierarchy until we find the class implementing the method. + + Looking at the method implementation we can also skip subsequent super calls. + */ + Class clazz = object_getClass(obj); + Class superclazz = class_getSuperclass(clazz); + do { + let superclassMethod = class_getInstanceMethod(superclazz, _cmd); + let sameMethods = class_getInstanceMethod(clazz, _cmd) == superclassMethod; + if (!sameMethods && !ITKMethodIsSuperTrampoline(superclassMethod)) { + break; + } + clazz = superclazz; + superclazz = class_getSuperclass(clazz); + }while (1); + + struct objc_super *_super = &_threadSuperStorage; + _super->receiver = obj; + _super->super_class = clazz; + return _super; +} + +@end + +/** + Inline assembly is used to perfectly forward all parameters to objc_msgSendSuper, + while also looking up the target on-the-fly. + + Assembly is hard, here are some useful resources: + + https://azeria-labs.com/functions-and-the-stack-part-7/ + https://github.com/DavidGoldman/InspectiveC/blob/master/InspectiveCarm64.mm + https://blog.nelhage.com/2010/10/amd64-and-va_arg/ + https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html + https://c9x.me/compile/bib/abi-arm64.pdf + http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801a/BABBDBAD.html + https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/using-the-stack-in-aarch64-implementing-push-and-pop + https://www.cs.yale.edu/flint/cs421/papers/x86-asm/asm.html + https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64 + https://en.wikipedia.org/wiki/Calling_convention#x86_(32-bit) + https://bob.cs.sonoma.edu/IntroCompOrg-RPi/sec-varstack.html + https://azeria-labs.com/functions-and-the-stack-part-7/ + */ + +#if defined(__arm64__) + +__attribute__((__naked__)) +void msgSendSuperTrampoline(void) { + asm volatile ( + +#if PROTECT_FLOATING_POINT_REGISTERS + // push {q0-q7} floating point registers + "stp q6, q7, [sp, #-32]!\n" + "stp q4, q5, [sp, #-32]!\n" + "stp q2, q3, [sp, #-32]!\n" + "stp q0, q1, [sp, #-32]!\n" +#endif + + // push {x0-x8, lr} (call params are: x0-x7) + // stp: store pair of registers: from, from, to, via indexed write + "stp x8, lr, [sp, #-16]!\n" // push lr (link register == x30), then x8 + "stp x6, x7, [sp, #-16]!\n" + "stp x4, x5, [sp, #-16]!\n" + "stp x2, x3, [sp, #-16]!\n" // push x3, then x2 + "stp x0, x1, [sp, #-16]!\n" // push x1, then x0 + + // fetch filled struct objc_super, call with self + _cmd + "bl _ITKReturnThreadSuper \n" + + // first param is now struct objc_super (x0) + // protect returned new value when we restore the pairs + "mov x9, x0\n" + + // pop {x0-x8, lr} + "ldp x0, x1, [sp], #16\n" + "ldp x2, x3, [sp], #16\n" + "ldp x4, x5, [sp], #16\n" + "ldp x6, x7, [sp], #16\n" + "ldp x8, lr, [sp], #16\n" + +#if PROTECT_FLOATING_POINT_REGISTERS + // pop {q0-q7} + "ldp q6, q7, [sp], #32\n" + "ldp q4, q5, [sp], #32\n" + "ldp q2, q3, [sp], #32\n" + "ldp q0, q1, [sp], #32\n" +#endif + + // get new return (adr of the objc_super class) + "mov x0, x9\n" + // tail call + "b _objc_msgSendSuper2 \n" + : : : "x0", "x1"); +} + +// arm64 doesn't use _stret variants. +void msgSendSuperStretTrampoline(void) {} + +#elif defined(__x86_64__) + +__attribute__((__naked__)) +void msgSendSuperTrampoline(void) { + asm volatile ( + // push frame pointer + "pushq %%rbp \n" + // set stack to frame pointer + "movq %%rsp, %%rbp \n" + +#if PROTECT_FLOATING_POINT_REGISTERS + // reserve 48+4*16 = 112 byte on the stack (need 16 byte alignment) + "subq $112, %%rsp \n" + + "movdqu %%xmm0, -64(%%rbp) \n" + "movdqu %%xmm1, -80(%%rbp) \n" + "movdqu %%xmm2, -96(%%rbp) \n" + "movdqu %%xmm3, -112(%%rbp) \n" +#else + // reserve 48 byte on the stack (need 16 byte alignment) + "subq $48, %%rsp \n" +#endif + + // Save call params: rdi, rsi, rdx, rcx, r8, r9 + // + // First parameter can be avoided, + // but we need to keep the stack 16-byte algined. + //"movq %%rdi, -8(%%rbp) \n" // self po *(id *) + "movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi + "movq %%rdx, -24(%%rbp) \n" // param 1 + "movq %%rcx, -32(%%rbp) \n" // param 2 + "movq %%r8, -40(%%rbp) \n" // param 3 + "movq %%r9, -48(%%rbp) \n" // param 4 (rest goes on stack) + + // fetch filled struct objc_super, call with self + _cmd + "callq _ITKReturnThreadSuper \n" + // first param is now struct objc_super + "movq %%rax, %%rdi \n" + +#if PROTECT_FLOATING_POINT_REGISTERS + "movdqu -64(%%rbp), %%xmm0 \n" + "movdqu -80(%%rbp), %%xmm1 \n" + "movdqu -96(%%rbp), %%xmm2 \n" + "movdqu -112(%%rbp), %%xmm3 \n" +#endif + + // Restore call params + // do not restore first parameter: super class + "movq -16(%%rbp), %%rsi \n" + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" + + // debug stack via print *(int *) ($rsp+8) + // remove 112/48 byte from stack +#if PROTECT_FLOATING_POINT_REGISTERS + "addq $112, %%rsp \n" +#else + "addq $48, %%rsp \n" +#endif + // pop frame pointer + "popq %%rbp \n" + + // tail call time! + "jmp _objc_msgSendSuper2 \n" + : : : "rsi", "rdi"); +} + + +__attribute__((__naked__)) +void msgSendSuperStretTrampoline(void) { + asm volatile ( + // push frame pointer + "pushq %%rbp \n" + // set stack to frame pointer + "movq %%rsp, %%rbp \n" + // reserve 48 byte on the stack (need 16 byte alignment) + "subq $48, %%rsp \n" + + // Save call params: rdi, rsi, rdx, rcx, r8, r9 + "movq %%rdi, -8(%%rbp) \n" // struct return + "movq %%rsi, -16(%%rbp) \n" // self + "movq %%rdx, -24(%%rbp) \n" // _cmd + "movq %%rcx, -32(%%rbp) \n" // param 1 + "movq %%r8, -40(%%rbp) \n" // param 2 + "movq %%r9, -48(%%rbp) \n" // param 3 (rest goes on stack) + + // fetch filled struct objc_super, call with self + _cmd + // Since stret offsets, we move back by one + "movq -16(%%rbp), %%rdi \n" + "movq -24(%%rbp), %%rsi \n" + "callq _ITKReturnThreadSuper \n" + // second param is now struct objc_super + "movq %%rax, %%rsi \n" + // First is our struct return + + // Restore call params + "movq -8(%%rbp), %%rdi \n" + // do not restore second parameter: super class + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" + + // debug stack via print *(int *) ($rsp+8) + // remove 64 byte from stack + "addq $48, %%rsp \n" + // pop frame pointer + "popq %%rbp \n" + + // tail call time! + "jmp _objc_msgSendSuper2_stret \n" + : : : "rsi", "rdi"); +} + +#else +// Unknown architecture - time to write some assembly :) +void msgSendSuperTrampoline(void) {} +void msgSendSuperStretTrampoline(void) {} +#endif + +NS_ASSUME_NONNULL_END +#endif diff --git a/Tests/InterposeKitTests/InterposeKitTestCase.swift b/Tests/InterposeKitTests/InterposeKitTestCase.swift new file mode 100644 index 0000000..674fc7f --- /dev/null +++ b/Tests/InterposeKitTests/InterposeKitTestCase.swift @@ -0,0 +1,37 @@ +import XCTest +@testable import InterposeKit + +class InterposeKitTestCase: XCTestCase { + override func setUpWithError() throws { + Interpose.isLoggingEnabled = true + } +} + +extension InterposeKitTestCase { + /// Assert that a specific error is thrown. + func assert( + _ expression: @autoclosure () throws -> T, + throws error: E, + in file: StaticString = #file, + line: UInt = #line + ) { + // https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/ + var thrownError: Error? + + XCTAssertThrowsError(try expression(), + file: file, line: line) { + thrownError = $0 + } + + XCTAssertTrue( + thrownError is E, + "Unexpected error type: \(type(of: thrownError))", + file: file, line: line + ) + + XCTAssertEqual( + thrownError as? E, error, + file: file, line: line + ) + } +} diff --git a/Tests/InterposeKitTests/InterposeKitTests.swift b/Tests/InterposeKitTests/InterposeKitTests.swift index 3f1c8be..2e7a1a9 100644 --- a/Tests/InterposeKitTests/InterposeKitTests.swift +++ b/Tests/InterposeKitTests/InterposeKitTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import InterposeKit -final class InterposeKitTests: XCTestCase { +final class InterposeKitTests: InterposeKitTestCase { override func setUpWithError() throws { Interpose.isLoggingEnabled = true @@ -13,31 +13,30 @@ final class InterposeKitTests: XCTestCase { // Functions need to be `@objc dynamic` to be hookable. let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - - print("Before Interposing \(`self`)") - - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - let string = origCall(`self`, store.selector) - - print("After Interposing \(`self`)") - - return string + testSwizzleAddition - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + + // You're free to skip calling the original implementation. + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + + return string + testString + } + } } print(TestClass().sayHi()) // Test various apply/revert's - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) try interposer.revert() XCTAssertEqual(testObj.sayHi(), testClassHi) try interposer.apply() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertThrowsError(try interposer.apply()) XCTAssertThrowsError(try interposer.apply()) try interposer.revert() @@ -53,29 +52,37 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposed = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - return origCall(`self`, store.selector) + testSwizzleAddition - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + return store.original(`self`, store.selector) + testString + } + } } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) try interposed.revert() XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) try interposed.apply() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) // Swizzle subclass, automatically applys let interposedSubclass = try Interpose(TestSubclass.self) { - try $0.hook(#selector(TestSubclass.sayHi), { store in { blockSelf in - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - return origCall(blockSelf, store.selector) + testSwizzleAddition - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestSubclass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + return store.original(`self`, store.selector) + testString + } + } } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass + testString) try interposed.revert() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass + testString) try interposedSubclass.revert() XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) } @@ -90,11 +97,15 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.doNothing), { store in { `self` in - tracker.keep() - let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self) - return origCall(`self`, store.selector) - } as @convention(block) (AnyObject) -> Void }) + try $0.hook( + #selector(TestClass.doNothing), + methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, + hookSignature: (@convention(block) (AnyObject) -> Void).self) { + store in { `self` in + tracker.keep() + return store.original(`self`, store.selector) + } + } } // Dealloc interposer without removing hooks @@ -115,11 +126,15 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.doNothing), { store in { `self` in - tracker.keep() - let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self) - return origCall(`self`, store.selector) - } as @convention(block) (AnyObject) -> Void }) + try $0.hook( + #selector(TestClass.doNothing), + methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, + hookSignature: (@convention(block) (AnyObject) -> Void).self) { + store in { `self` in + tracker.keep() + return store.original(`self`, store.selector) + } + } } try interposer.revert() diff --git a/Tests/InterposeKitTests/KVOTests.swift b/Tests/InterposeKitTests/KVOTests.swift new file mode 100644 index 0000000..7de46bf --- /dev/null +++ b/Tests/InterposeKitTests/KVOTests.swift @@ -0,0 +1,65 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class KVOTests: InterposeKitTestCase { + + // Helper observer that wraps a token and removes it on deinit. + class TestClassObserver { + var kvoToken: NSKeyValueObservation? + var didCallObserver = false + + func observe(obj: TestClass) { + kvoToken = obj.observe(\.age, options: .new) { [weak self] obj, change in + guard let age = change.newValue else { return } + print("New age is: \(age)") + self?.didCallObserver = true + } + } + + deinit { + kvoToken?.invalidate() + } + } + + func testBasicKVO() throws { + let testObj = TestClass() + + // KVO before hooking works, but hooking will fail + try withExtendedLifetime(TestClassObserver()) { observer in + observer.observe(obj: testObj) + XCTAssertEqual(testObj.age, 1) + testObj.age = 2 + XCTAssertEqual(testObj.age, 2) + // Hooking is expected to fail + assert(try Interpose(testObj), throws: InterposeError.keyValueObservationDetected(testObj)) + XCTAssertEqual(testObj.age, 2) + } + + // Hook without KVO! + let hook = try testObj.hook( + #selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 3 + } + } + XCTAssertEqual(testObj.age, 3) + try hook.revert() + XCTAssertEqual(testObj.age, 2) + try hook.apply() + XCTAssertEqual(testObj.age, 3) + + // Now we KVO after hooking! + withExtendedLifetime(TestClassObserver()) { observer in + observer.observe(obj: testObj) + XCTAssertEqual(testObj.age, 3) + // Setter is fine but won't change outcome + XCTAssertFalse(observer.didCallObserver) + testObj.age = 4 + XCTAssertTrue(observer.didCallObserver) + XCTAssertEqual(testObj.age, 3) + } + } +} diff --git a/Tests/InterposeKitTests/MultipleInterposing.swift b/Tests/InterposeKitTests/MultipleInterposing.swift new file mode 100644 index 0000000..d4ed3c1 --- /dev/null +++ b/Tests/InterposeKitTests/MultipleInterposing.swift @@ -0,0 +1,67 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class MultipleInterposingTests: InterposeKitTestCase { + + func testInterposeSingleObjectMultipleTimes() throws { + let testObj = TestClass() + let testObj2 = TestClass() + + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + testString + } + } + } + + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + try testObj.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + testString2 + } + } + + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testString2) + try interposer.revert() + XCTAssertEqual(testObj.sayHi(), testClassHi + testString2) + } + + func testInterposeAgeAndRevert() throws { + let testObj = TestClass() + XCTAssertEqual(testObj.age, 1) + + let interpose = try Interpose(testObj) { + try $0.hook(#selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 3 + } + } + } + XCTAssertEqual(testObj.age, 3) + + try interpose.hook(#selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 5 + } + }.apply() + XCTAssertEqual(testObj.age, 5) + try interpose.revert() + XCTAssertEqual(testObj.age, 1) + } +} diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift new file mode 100644 index 0000000..44c7c87 --- /dev/null +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -0,0 +1,159 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class ObjectInterposeTests: InterposeKitTestCase { + + func testInterposeSingleObject() throws { + let testObj = TestClass() + let testObj2 = TestClass() + + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + let hook = try testObj.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature:(@convention(block) (AnyObject) -> String).self) { store in { `self` in + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + return string + testString + } + } + + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + try hook.revert() + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + try hook.apply() + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + } + + func testInterposeSingleObjectInt() throws { + let testObj = TestClass() + let returnIntDefault = testObj.returnInt() + let returnIntOverrideOffset = 2 + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + + let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + let int = store.original($0, store.selector) + return int + returnIntOverrideOffset + } + } + + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + try hook.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + try hook.apply() + // ensure we really don't leak into another object + let testObj2 = TestClass() + XCTAssertEqual(testObj2.returnInt(), returnIntDefault) + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + try hook.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + } + + func testDoubleIntegerInterpose() throws { + let testObj = TestClass() + let returnIntDefault = testObj.returnInt() + let returnIntOverrideOffset = 2 + let returnIntClassMultiplier = 4 + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + + // Functions need to be `@objc dynamic` to be hookable. + let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + // You're free to skip calling the original implementation. + store.original($0, store.selector) + returnIntOverrideOffset + } + } + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + + // Interpose on TestClass itself! + let classInterposer = try Interpose(TestClass.self) { + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + store.original($0, store.selector) * returnIntClassMultiplier + } + } + } + + XCTAssertEqual(testObj.returnInt(), (returnIntDefault * returnIntClassMultiplier) + returnIntOverrideOffset) + + // ensure we really don't leak into another object + let testObj2 = TestClass() + XCTAssertEqual(testObj2.returnInt(), returnIntDefault * returnIntClassMultiplier) + + try hook.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault * returnIntClassMultiplier) + try classInterposer.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + } + + func test3IntParameters() throws { + let testObj = TestClass() + XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3) + + // Functions need to be `@objc dynamic` to be hookable. + let hook = try testObj.hook(#selector(TestClass.calculate)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3) + return orig + 1 + } + } + XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3 + 1) + try hook.revert() + } + + func test6IntParameters() throws { + let testObj = TestClass() + + XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6) + + // Functions need to be `@objc dynamic` to be hookable. + let hook = try testObj.hook(#selector(TestClass.calculate2)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6) + return orig + 1 + } + } + XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) + try hook.revert() + } + + func testObjectCallReturn() throws { + let testObj = TestClass() + let str = "foo" + XCTAssertEqual(testObj.doubleString(string: str), str + str) + + // Functions need to be `@objc dynamic` to be hookable. + let hook = try testObj.hook(#selector(TestClass.doubleString)) { (store: TypedHook<@convention(c) (AnyObject, Selector, String) -> String, @convention(block) (AnyObject, String) -> String>) in { + store.original($0, store.selector, $1) + str + } + } + XCTAssertEqual(testObj.doubleString(string: str), str + str + str) + try hook.revert() + XCTAssertEqual(testObj.doubleString(string: str), str + str) + } + + func testLargeStructReturn() throws { + let testObj = TestClass() + let transform = CATransform3D() + XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) + + func transformMatrix(_ matrix: CATransform3D) -> CATransform3D { + matrix.translated(x: 10, y: 5, z: 2) + } + + // Functions need to be `@objc dynamic` to be hookable. + let hook = try testObj.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook<@convention(c) (AnyObject, Selector, CATransform3D) -> CATransform3D, @convention(block) (AnyObject, CATransform3D) -> CATransform3D>) in { + let matrix = store.original($0, store.selector, $1) + return transformMatrix(matrix) + } + } + XCTAssertEqual(testObj.invert3DTransform(transform), transformMatrix(transform.inverted)) + try hook.revert() + XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) + } +} diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index aeff1e2..6130efc 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -1,20 +1,69 @@ import Foundation +import QuartzCore let testClassHi = "Hi from TestClass!" -let testSwizzleAddition = " and Interpose" +let testString = " and Interpose" +let testString2 = " testString2" let testSubclass = "Subclass is here!" +public func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool { + return CATransform3DEqualToTransform(lhs, rhs) +} + +extension CATransform3D: Equatable { } + +public extension CATransform3D { + + // swiftlint:disable:next identifier_name + func translated(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CATransform3D { + return CATransform3DTranslate(self, x, y, z) + } + + var inverted: CATransform3D { + return CATransform3DInvert(self) + } +} + class TestClass: NSObject { + + @objc dynamic var age: Int = 1 + @objc dynamic var name: String = "Tim Apple" + @objc dynamic func sayHi() -> String { print(testClassHi) return testClassHi } @objc dynamic func doNothing() { } + + @objc dynamic func doubleString(string: String) -> String { + string + string + } + + @objc dynamic func returnInt() -> Int { + 7 + } + + @objc dynamic func calculate(var1: Int, var2: Int, var3: Int) -> Int { + var1 + var2 + var3 + } + + @objc dynamic func calculate2(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { + var1 + var2 + var3 + var4 + var5 + var6 + } + + // This requires _objc_msgSendSuper_stret on x64, returns a large struct + @objc dynamic func invert3DTransform(_ input: CATransform3D) -> CATransform3D { + input.inverted + } } class TestSubclass: TestClass { override func sayHi() -> String { return super.sayHi() + testSubclass } + + override func doNothing() { + super.doNothing() + } }