├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .swift-version ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── App │ ├── Commons │ │ ├── Concurrent.swift │ │ ├── CpuId.swift │ │ ├── Paths.swift │ │ ├── Pipe.swift │ │ ├── Profiling.swift │ │ ├── Range+Sugar.swift │ │ ├── Regex.swift │ │ ├── Savable.swift │ │ ├── Sequence+uniq.swift │ │ ├── String+capitalizedOnFirstLetter.swift │ │ └── URL+containsURL.swift │ ├── Controller │ │ ├── Image+Savable.swift │ │ ├── Image+machs.swift │ │ ├── ImageLoaderProtocol.swift │ │ ├── Logger │ │ │ ├── LoggerProtocol.swift │ │ │ ├── SoutLogger+withOptions.swift │ │ │ └── SoutLogger.swift │ │ ├── Obfuscator.swift │ │ ├── SimpleImageLoader+DependencyNodeLoader.swift │ │ ├── SimpleImageLoader+ImageLoader.swift │ │ ├── SimpleImageLoader+SymbolsSourceLoader.swift │ │ └── SimpleImageLoader.swift │ ├── DataAccess │ │ ├── Data+Magic.swift │ │ ├── Data+Mapping.swift │ │ ├── Data+Structs.swift │ │ ├── Data+nullify.swift │ │ ├── UnsafeRawPointer+CString.swift │ │ ├── UnsafeRawPointer+Leb128.swift │ │ └── UnsafeRawPointer+Structs.swift │ ├── DependencyAnalysis │ │ ├── DependencyNode.swift │ │ ├── DependencyNodeLoader+isMachOExecutable.swift │ │ ├── DependencyNodeLoader.swift │ │ ├── FileRepository+DylibLocationResolving.swift │ │ ├── FileRepository+FrameworkLocationResolving.swift │ │ ├── FileRepository.swift │ │ ├── ObfuscableFilesFilter.swift │ │ ├── ObfuscationPaths+Building.swift │ │ ├── ObfuscationPaths.swift │ │ └── RpathsAccumulator.swift │ ├── HeadersParsing │ │ ├── NSRegularExpression+matches.swift │ │ ├── ObjectSymbols.swift │ │ ├── RecursiveSourceSymbolsLoader.swift │ │ ├── String+objCMethodNames.swift │ │ ├── String+objCPropertyNames.swift │ │ ├── String+objCTypeNames.swift │ │ ├── String+subscriptNSRange.swift │ │ ├── String+subscriptRegexp.swift │ │ └── String+withoutComments.swift │ ├── Mach │ │ ├── ExportTrie │ │ │ ├── Trie+Loading.swift │ │ │ ├── Trie+Parsing.swift │ │ │ └── Trie.swift │ │ ├── Image+updateMachs.swift │ │ ├── ImportStack │ │ │ ├── ImportStack+Loading.swift │ │ │ ├── ImportStack+Parsing.swift │ │ │ └── ImportStack.swift │ │ ├── Mach+Dumping.swift │ │ ├── Mach+Erasing.swift │ │ ├── Mach+Loading.swift │ │ ├── Mach+Replacing.swift │ │ ├── Mach+Saving.swift │ │ ├── Mach+SectionsLookup.swift │ │ ├── Mach.swift │ │ ├── MachO+StringsParsing.swift │ │ ├── ObjC │ │ │ ├── MachStrings.swift │ │ │ ├── ObjcStructs.swift │ │ │ ├── ObjcSymbols+Impl.swift │ │ │ └── ObjcSymbols.swift │ │ └── Parsing │ │ │ ├── Mach+classNames.swift │ │ │ ├── Mach+cstrings.swift │ │ │ ├── Mach+exportTrie.swift │ │ │ ├── Mach+importStack.swift │ │ │ ├── Mach+properties.swift │ │ │ └── Mach+selectors.swift │ ├── Nib │ │ ├── Formats │ │ │ ├── NIBArchive │ │ │ │ ├── NibArchive+Loading.swift │ │ │ │ ├── NibArchive+Nib.swift │ │ │ │ ├── NibArchive+Print.swift │ │ │ │ ├── NibArchive+Saving.swift │ │ │ │ └── NibArchive.swift │ │ │ └── NIBPlist │ │ │ │ ├── CFKeyedArchiverUIDGetValue.swift │ │ │ │ ├── NibPlist+Loading.swift │ │ │ │ ├── NibPlist+Nib.swift │ │ │ │ ├── NibPlist+Saving.swift │ │ │ │ └── NibPlist.swift │ │ ├── Nib.swift │ │ └── URL+NibLoading.swift │ ├── Options │ │ └── Options.swift │ ├── SymbolMangling │ │ ├── CaesarMangler │ │ │ ├── CaesarCypher.swift │ │ │ ├── CaesarExportTrieMangler.swift │ │ │ ├── CaesarMangler.swift │ │ │ └── CaesarStringMangler.swift │ │ ├── RealWordsMangler │ │ │ ├── RealWordsExportTrieMangler.swift │ │ │ ├── RealWordsMangler.swift │ │ │ ├── SentenceGenerator.swift │ │ │ └── Words.swift │ │ ├── SymbolManglers.swift │ │ ├── SymbolMangling.swift │ │ ├── SymbolManglingMap.swift │ │ └── SymbolsReporting.swift │ ├── SymbolsCollecting │ │ ├── ObfuscationSymbols+Building.swift │ │ ├── ObfuscationSymbols+Finding.swift │ │ ├── ObfuscationSymbols.swift │ │ ├── SymbolsSource.swift │ │ └── SymbolsSourceLoader.swift │ ├── SymbolsListParsing │ │ └── TextFileSymbolListLoader.swift │ └── run.swift └── Run │ └── main.swift ├── Tests └── AppTests │ ├── Commons │ ├── String+asURL.swift │ └── URL+tempFile.swift │ ├── CommonsTests │ ├── Concurrent_Tests.swift │ ├── CpuId_Tests.swift │ ├── Range+Sugar_Tests.swift │ ├── Sequence+uniq_Tests.swift │ ├── String+capitalizedOnFirstLetter_Tests.swift │ └── URL+containsURL_Tests.swift │ ├── ControllerTests │ └── Obfuscator_Tests.swift │ ├── DataAccessTests │ ├── Data+Magic_Tests.swift │ ├── Data+Mapping_replaceBytes_Tests.swift │ ├── Data+Mapping_replaceStrings_Tests.swift │ ├── Data+Structs_getCString_Tests.swift │ ├── Data+Structs_getStruct_Tests.swift │ ├── Data+Structs_getStructs_Tests.swift │ ├── Data+nullify_Tests.swift │ ├── UnsafeRawPointer+CString_Tests.swift │ ├── UnsafeRawPointer+Leb128_readNibSleb128_Tests.swift │ ├── UnsafeRawPointer+Leb128_readNibUleb128_Tests.swift │ ├── UnsafeRawPointer+Leb128_readSleb128_Tests.swift │ ├── UnsafeRawPointer+Leb128_readUleb128_Tests.swift │ ├── UnsafeRawPointer+Structs_readStruct_Tests.swift │ └── UnsafeRawPointer+Structs_readStructs_Tests.swift │ ├── DependencyAnalysisTests │ ├── ObfuscationPaths+Building_forAllExecutables_Tests.swift │ └── ObfuscationPathsTestRepository.swift │ ├── HeaderParsingTests │ ├── RecursiveSourceSymbolsLoaderMock.swift │ ├── RecursiveSourceSymbolsLoader_loadFromFrameworkURL_allSystemFrameworks_Tests.swift │ ├── RecursiveSourceSymbolsLoader_loadFromFrameworkURL_craftedFramework_Tests.swift │ ├── RecursiveSourceSymbolsLoader_loadFromFrameworkURL_systemLikeFramework_Tests.swift │ ├── RecursiveSourceSymbolsLoader_loadFromSourcesURL_Tests.swift │ └── Samples │ │ ├── CraftedFramework.framework │ │ └── Headers │ │ │ ├── TestMethodNames.h │ │ │ ├── TestPropertyNames.h │ │ │ └── TestTypeNames.h │ │ ├── LibrarySourceCode.bundle │ │ ├── File1.h │ │ └── File1.m │ │ ├── SystemLikeFramework.framework │ │ └── Headers │ │ │ ├── NSDictionary.h │ │ │ ├── NSOrderedSet.h │ │ │ └── UIApplication.h │ │ └── URL+SampleFrameworks.swift │ ├── MachTests │ ├── Commons │ │ └── Mach+Tests.swift │ ├── ExportTrieTests │ │ ├── Trie+Loading_Tests.swift │ │ └── Trie+Parsing_Tests.swift │ ├── Image+updateMachs_Tests.swift │ ├── ImportStackTests │ │ ├── ImportStack_addOpcodesData_Tests.swift │ │ └── ImportStack_resolveMissingDylibOrdinals_Tests.swift │ ├── Mach+Loading_FatIos_Tests.swift │ ├── Mach+Loading_MachoIos12_0_Tests.swift │ ├── Mach+Loading_MachoMac10_14_Tests.swift │ ├── Mach+Loading_MachoMac_Tests.swift │ ├── Mach+Parsing_FatIos_Tests.swift │ ├── Mach+Parsing_MachoMac_Tests.swift │ ├── Mach+Replacing_classname_Tests.swift │ ├── Mach+Replacing_cstrings_Tests.swift │ ├── Mach+Replacing_importEntries_Tests.swift │ ├── Mach+Replacing_methtype_Tests.swift │ ├── Mach+Replacing_propertyattributes_Tests.swift │ └── Samples │ │ ├── SampleFatIosExecutable │ │ ├── SampleMachoIos12_0Executable │ │ ├── SampleMachoMac10_14Executable │ │ ├── SampleMachoMacExecutable │ │ └── URL+SampleImages.swift │ ├── NibTests │ ├── FormatsTests │ │ ├── NIBArchiveTests │ │ │ ├── NibArchive+Loading_Tests.swift │ │ │ └── NibArchive+Nib_Tests.swift │ │ └── NibPlistTests │ │ │ ├── NibPlist+Loading_Tests.swift │ │ │ └── NibPlist+Nib_Tests.swift │ ├── NibModifying_TestsBase.swift │ ├── Samples │ │ ├── IosView.nib │ │ ├── MacView.nib │ │ └── URL+SampleNibs.swift │ ├── SamplesSource │ │ ├── IosView.xib │ │ ├── MacView.xib │ │ └── compile.sh │ └── URL+NibLoading_loadNib_Tests.swift │ ├── OptionsTests │ ├── Options+ObfuscableFilesFilter_Tests.swift │ ├── OptionsTestsSupport.swift │ └── Options_Tests.swift │ ├── SampleAppSources │ ├── SampleIosApp │ │ ├── SampleIosApp.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── SampleIosApp │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── SampleClass+Cat.h │ │ │ ├── SampleClass+Cat.m │ │ │ ├── SampleClass.h │ │ │ ├── SampleClass.m │ │ │ └── ViewController.swift │ │ ├── SampleIosAppModel │ │ │ ├── Info.plist │ │ │ ├── SampleIosAppModel.h │ │ │ ├── SampleModel.h │ │ │ └── SampleModel.m │ │ └── SampleIosAppViewModel │ │ │ ├── Info.plist │ │ │ ├── SampleIosAppViewModel.h │ │ │ └── ViewModel.swift │ └── SampleMacApp │ │ ├── SampleMacApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── SampleMacApp │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SampleClass+Cat.h │ │ ├── SampleClass+Cat.m │ │ ├── SampleClass.h │ │ ├── SampleClass.m │ │ ├── SampleMacApp.entitlements │ │ └── ViewController.swift │ │ ├── SampleMacAppModel │ │ ├── Info.plist │ │ ├── SampleMacAppModel.h │ │ ├── SampleModel.h │ │ └── SampleModel.m │ │ └── SampleMacAppViewModel │ │ ├── Info.plist │ │ ├── SampleMacAppViewModel.h │ │ └── ViewModel.swift │ ├── SymbolManglingTests │ ├── ArraySentenceGenerator.swift │ ├── CaesarMangler_Tests.swift │ ├── RealWordsExportTrieMangler_Tests.swift │ ├── RealWordsExportTrieMangler_emptyLabeledNodes_Tests.swift │ └── RealWordsMangler_Tests.swift │ ├── SymbolsCollectingTests │ ├── ObfuscationSymbols+Building_buildForObfuscationPaths_Tests.swift │ └── SymbolsSourceLoaderMock.swift │ └── SymbolsListParsing │ ├── TextFileSymbolListLoaderMock.swift │ └── TextFileSymbolListLoader_load_Tests.swift ├── WordList ├── english_top_1000.txt └── generate_swift.rb ├── obfuscate.sh ├── readme_resource ├── classes_after.png ├── classes_after_titled.png ├── classes_before.png ├── classes_before_titled.png ├── machobfuscator_demo.cast ├── machobfuscator_demo.gif ├── selectors_after.png ├── selectors_after_titled.png ├── selectors_before.png └── selectors_before_titled.png └── resign.sh /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | .DS_Store 3 | .swiftpm 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kamil Borzym 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MachObfuscator", 8 | platforms: [ 9 | .macOS(.v10_13) 10 | ], 11 | products: [ 12 | .executable( 13 | name: "MachObfuscator", 14 | targets: ["Run"] 15 | ), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Run", 20 | dependencies: ["App"] 21 | ), 22 | .target( 23 | name: "App" 24 | ), 25 | .testTarget( 26 | name: "AppTests", 27 | dependencies: ["App"], 28 | exclude: [ 29 | "SampleAppSources", 30 | "NibTests/SamplesSource" 31 | ], 32 | resources: [ 33 | .copy("HeaderParsingTests/Samples/CraftedFramework.framework"), 34 | .copy("HeaderParsingTests/Samples/LibrarySourceCode.bundle"), 35 | .copy("HeaderParsingTests/Samples/SystemLikeFramework.framework"), 36 | .copy("MachTests/Samples/SampleFatIosExecutable"), 37 | .copy("MachTests/Samples/SampleMachoIos12_0Executable"), 38 | .copy("MachTests/Samples/SampleMachoMac10_14Executable"), 39 | .copy("MachTests/Samples/SampleMachoMacExecutable"), 40 | ] 41 | ) 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /Sources/App/Commons/Concurrent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Concurrent.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | extension Array { 9 | // Building block for concurrent operations 10 | // This implementation is best for realativly small arrays with costly per-item computation 11 | // 12 | // Insipired by https://talk.objc.io/episodes/S01E90-concurrent-map 13 | fileprivate func concurrentMap_impl(_ transform: @escaping (Element) -> B?) -> [B?] { 14 | var result = [B?](repeating: nil, count: count) 15 | let q = DispatchQueue(label: "sync queue") 16 | DispatchQueue.concurrentPerform(iterations: count) { idx in 17 | let element = self[idx] 18 | let transformed = transform(element) 19 | q.sync { 20 | result[idx] = transformed 21 | } 22 | } 23 | return result 24 | } 25 | 26 | func concurrentMap(_ transform: @escaping (Element) -> B) -> [B] { 27 | return concurrentMap_impl(transform).map { $0! } 28 | } 29 | 30 | func concurrentCompactMap(_ transform: @escaping (Element) -> B?) -> [B] { 31 | return concurrentMap_impl(transform).compactMap { $0 } 32 | } 33 | } 34 | 35 | extension Set { 36 | // Implementation for realativly small sets with costly per-item computation 37 | func concurrentMap(_ transform: @escaping (Element) -> B) -> [B] { 38 | return Array(self).concurrentMap(transform) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/App/Commons/CpuId.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias CpuId = Int64 4 | 5 | extension Mach.Cpu { 6 | var asCpuId: CpuId { 7 | return (Int64(UInt32(bitPattern: type)) << 32) | Int64(UInt32(bitPattern: subtype)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/App/Commons/Paths.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Paths { 4 | static let separator: Character = "/" 5 | 6 | private static func xcode_select() -> String? { 7 | let task = Process() 8 | 9 | task.executableURL = URL(fileURLWithPath: "/usr/bin/xcode-select") 10 | task.arguments = ["--print-path"] 11 | let outputPipe = Pipe() 12 | task.standardOutput = outputPipe 13 | try! task.run() 14 | task.waitUntilExit() 15 | guard task.terminationStatus == 0 else { 16 | LOGGER.warn("Unable to invoke xcode-select") 17 | return nil 18 | } 19 | 20 | let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 21 | let output = String(decoding: outputData, as: UTF8.self) 22 | let path = output.trimmingCharacters(in: .newlines) 23 | 24 | // Test just in case, protects also against unexpected output 25 | guard FileManager.default.fileExists(atPath: path) else { 26 | LOGGER.warn("xcode-select returned non-existing path: \(path)") 27 | return nil 28 | } 29 | 30 | return path 31 | } 32 | 33 | static let xcodeRoot: String = { 34 | let root = xcode_select() ?? 35 | // Default value if xcode-select fails 36 | "/Applications/Xcode.app/Contents/Developer" 37 | 38 | LOGGER.info("Determined Xcode root directory: \(root)") 39 | return root 40 | }() 41 | 42 | static let macosFrameworksRoot: String = xcodeRoot + "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" 43 | static let iosFrameworksRoot: String = xcodeRoot + "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk" 44 | static let watchosFrameworksRoot: String = xcodeRoot + "/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk" 45 | static let tvosFrameworksRoot: String = xcodeRoot + "/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk" 46 | 47 | static let iosRuntimeRoot: String = { 48 | guard let iosRoot = possibleIosRuntimeRoots.first(where: FileManager.default.fileExists(atPath:)) else { 49 | fatalError("Could not find iOS runtime root. Make sure you have Xcode (10/11) installed.") 50 | } 51 | return iosRoot 52 | }() 53 | 54 | private static let possibleIosRuntimeRoots: [String] = [ 55 | // Xcode 10 56 | xcodeRoot + "/Platforms/iPhoneOS.platform/" 57 | + "Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot", 58 | // Xcode 11 59 | xcodeRoot + "/Platforms/iPhoneOS.platform/" 60 | + "Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /Sources/App/Commons/Pipe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | precedencegroup ForwardApplication { 4 | associativity: left 5 | } 6 | 7 | infix operator |>: ForwardApplication 8 | 9 | func |> (value: T, function: (T) -> U) -> U { 10 | return function(value) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Commons/Profiling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Profiling.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | func time(withTag tag: String, of closure: () -> R) -> R { 9 | let start = Date() 10 | let result = closure() 11 | let end = Date() 12 | LOGGER.info("#\(tag) - execution took \(end.timeIntervalSince(start)) seconds") 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /Sources/App/Commons/Range+Sugar.swift: -------------------------------------------------------------------------------- 1 | extension Range where Bound: BinaryInteger { 2 | init(offset: Bound, count: Bound) { 3 | self = offset ..< (offset + count) 4 | } 5 | 6 | var intRange: Range { 7 | return Int(lowerBound) ..< Int(upperBound) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/App/Commons/Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Regex.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | extension Collection where Element == String { 9 | /// Filters collection, only elements matching at least one of the regular expressions are returned. 10 | func matching(regexes: [NSRegularExpression]) -> [Self.Element] { 11 | guard !regexes.isEmpty else { 12 | // nothing to do 13 | return [] 14 | } 15 | return filter { string in 16 | regexes.contains(where: { regex in 17 | regex.firstMatch(in: string) != nil 18 | }) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/App/Commons/Savable.swift: -------------------------------------------------------------------------------- 1 | protocol Savable { 2 | func save() 3 | } 4 | -------------------------------------------------------------------------------- /Sources/App/Commons/Sequence+uniq.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Sequence where Element: Hashable { 4 | var uniq: Set { 5 | return Set(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/App/Commons/String+capitalizedOnFirstLetter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var capitalizedOnFirstLetter: String { 5 | return prefix(1).capitalized + dropFirst() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/App/Commons/URL+containsURL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | func contains(_ other: URL) -> Bool { 5 | precondition(isFileURL) 6 | precondition(other.isFileURL) 7 | let c1 = pathComponents 8 | let c2 = other.pathComponents 9 | return c1.count <= c2.count 10 | && zip(c1, c2).first(where: !=) == nil 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/Controller/Image+Savable.swift: -------------------------------------------------------------------------------- 1 | extension Image: Savable {} 2 | -------------------------------------------------------------------------------- /Sources/App/Controller/Image+machs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Image { 4 | var machs: [Mach] { 5 | switch contents { 6 | case let .fat(fat): 7 | return fat.architectures.map { $0.mach } 8 | case let .mach(mach): 9 | return [mach] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/Controller/ImageLoaderProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol ImageLoader { 4 | func load(forURL url: URL) throws -> Image 5 | } 6 | -------------------------------------------------------------------------------- /Sources/App/Controller/Logger/LoggerProtocol.swift: -------------------------------------------------------------------------------- 1 | protocol Logger { 2 | func debug(_: @autoclosure () -> String) 3 | func info(_: @autoclosure () -> String) 4 | func warn(_: @autoclosure () -> String) 5 | } 6 | 7 | var LOGGER: Logger = VoidLogger() 8 | 9 | private class VoidLogger: Logger { 10 | func debug(_: () -> String) {} 11 | func info(_: () -> String) {} 12 | func warn(_: () -> String) {} 13 | } 14 | -------------------------------------------------------------------------------- /Sources/App/Controller/Logger/SoutLogger+withOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension SoutLogger { 4 | convenience init(options: Options) { 5 | let verbosity: SoutLogger.Verbosity 6 | 7 | if options.quiet { 8 | verbosity = .quiet 9 | } else if options.debug { 10 | verbosity = .debug 11 | } else if options.verbose { 12 | verbosity = .info 13 | } else { 14 | verbosity = .warning 15 | } 16 | 17 | self.init(verbosity: verbosity) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/App/Controller/Logger/SoutLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class SoutLogger { 4 | enum Verbosity: Int { 5 | case quiet 6 | case warning 7 | case info 8 | case debug 9 | } 10 | 11 | private let verbosity: Verbosity 12 | 13 | init(verbosity: Verbosity) { 14 | self.verbosity = verbosity 15 | } 16 | } 17 | 18 | extension SoutLogger: Logger { 19 | func debug(_ text: @autoclosure () -> String) { 20 | log(text: "DEBUG: \(text())", level: .debug) 21 | } 22 | 23 | func info(_ text: @autoclosure () -> String) { 24 | log(text: text(), level: .info) 25 | } 26 | 27 | func warn(_ text: @autoclosure () -> String) { 28 | log(text: "WARN: \(text())", level: .warning) 29 | } 30 | 31 | private func log(text: @autoclosure () -> String, level: Verbosity) { 32 | guard level.rawValue <= verbosity.rawValue else { 33 | return 34 | } 35 | print(text()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/App/Controller/SimpleImageLoader+DependencyNodeLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach: DependencyNode { 4 | var isExecutable: Bool { 5 | return type == .executable 6 | } 7 | } 8 | 9 | extension SimpleImageLoader: DependencyNodeLoader { 10 | func load(forURL url: URL) throws -> [DependencyNode] { 11 | return (try load(forURL: url) as Image).machs 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/App/Controller/SimpleImageLoader+ImageLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension SimpleImageLoader: ImageLoader {} 4 | -------------------------------------------------------------------------------- /Sources/App/Controller/SimpleImageLoader+SymbolsSourceLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach: SymbolsSource {} 4 | extension SimpleImageLoader: SymbolsSourceLoader { 5 | func load(forURL url: URL) throws -> [SymbolsSource] { 6 | return (try load(forURL: url)).machs 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/Controller/SimpleImageLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // TODO: caching mach loader 4 | class SimpleImageLoader { 5 | func load(forURL url: URL) throws -> Image { 6 | let data = try Data(contentsOf: url) 7 | return try Image(data: data, url: url) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/Data+Magic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | var magic: UInt32? { 5 | guard count >= MemoryLayout.size else { 6 | return nil 7 | } 8 | return withUnsafeBytes { $0.load(as: UInt32.self) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/Data+Mapping.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | private func stringRangesPerString(inRange range: Range) -> [String: [Range]] { 5 | let enumeratedBytes = Array(enumerated())[range] 6 | let chunksOfEnumeratedBytes = enumeratedBytes.split { _, data in data == 0 } 7 | let stringWithRangePairs: [(String, Range)] = chunksOfEnumeratedBytes.compactMap { chunk in 8 | let chunkBytes = chunk.map { _, data in data } 9 | guard let string = String(bytes: chunkBytes, encoding: .utf8) else { 10 | return nil 11 | } 12 | let chunkArray = Array(chunk) 13 | let range = (chunkArray.first!.offset ..< (chunkArray.last!.offset + 1)) 14 | return (string, range) 15 | } 16 | return Dictionary(grouping: stringWithRangePairs) { string, _ in string } 17 | .mapValues { $0.map { _, range in range } } 18 | } 19 | 20 | mutating func replaceStrings(inRange range: Range, withMapping mapping: [String: String]) { 21 | let rangesPerString = stringRangesPerString(inRange: range) 22 | mapping.forEach { originalString, mappedString in 23 | precondition(originalString.utf8.count == mappedString.utf8.count) 24 | if let stringRanges = rangesPerString[originalString] { 25 | stringRanges.forEach { stringRange in 26 | let targetData = mappedString.data(using: .utf8)! 27 | precondition(targetData.count == stringRange.count) 28 | replaceSubrange(stringRange, with: targetData) 29 | } 30 | } 31 | } 32 | } 33 | 34 | mutating func replaceStrings(inRange range: Range, withMapping mapping: (String) -> String, withFilter filter: (String) -> Bool = { _ in true }) { 35 | let rangesPerString = stringRangesPerString(inRange: range) 36 | rangesPerString.filter { filter($0.key) }.forEach { originalString, ranges in 37 | let mappedString = mapping(originalString) 38 | precondition(originalString.utf8.count >= mappedString.utf8.count) 39 | ranges.forEach { 40 | replaceRangeWithPadding($0, with: mappedString) 41 | } 42 | } 43 | } 44 | 45 | mutating func replaceBytes(inRange range: Range, withBytes bytes: [UInt8]) { 46 | precondition(range.count == bytes.count) 47 | replaceSubrange(range, with: Data(bytes)) 48 | } 49 | 50 | mutating func replaceRangeWithPadding(_ range: Range, with targetValue: String) { 51 | let targetData = targetValue.data(using: .utf8)! 52 | precondition(range.count >= targetData.count) 53 | let targetDataWithPadding = targetData + Array(repeating: UInt8(0), count: range.count - targetData.count) 54 | assert(range.count == targetDataWithPadding.count) 55 | replaceSubrange(range, with: targetDataWithPadding) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/Data+Structs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | func getStruct(atOffset offset: Int) -> T { 5 | return withUnsafeBytes { 6 | $0.baseAddress! 7 | .advanced(by: offset) 8 | .getStruct() 9 | } 10 | } 11 | 12 | func getStructs(atOffset offset: Int, count: Int) -> [T] { 13 | return withUnsafeBytes { 14 | $0.baseAddress! 15 | .advanced(by: offset) 16 | .getStructs(count: count) 17 | } 18 | } 19 | 20 | func getStructs(fromRange range: Range) -> [T] { 21 | return getStructs(atOffset: range.startIndex, count: range.count / MemoryLayout.stride) 22 | } 23 | 24 | func getCString(atOffset offset: Int) -> String { 25 | return withUnsafeBytes { 26 | $0.bindMemory(to: UInt8.self) 27 | .baseAddress! 28 | .advanced(by: offset) 29 | |> String.init(cString:) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/Data+nullify.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | mutating func nullify(range: Range) { 5 | let nullReplacement = Data(repeating: 0, count: range.count) 6 | replaceSubrange(range, with: nullReplacement) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/UnsafeRawPointer+CString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UnsafeRawPointer { 4 | mutating func readStringBytes() -> [UInt8] { 5 | let basePtr = self 6 | while load(as: UInt8.self) != 0 { 7 | self = advanced(by: 1) 8 | } 9 | defer { 10 | self = advanced(by: 1) // skip terminal 0 11 | } 12 | return [UInt8](UnsafeRawBufferPointer(start: basePtr, count: basePtr.distance(to: self))) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/UnsafeRawPointer+Leb128.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UnsafeRawPointer { 4 | mutating func readSleb128() -> Int64 { 5 | // continuation bit is 1 in default leb128 implementation 6 | return readLeb128(continuationBitState: true) 7 | } 8 | 9 | mutating func readNibSleb128() -> Int64 { 10 | // Nib file format uses uleb-like coding, but uses 0 as a continuation bit 11 | return readLeb128(continuationBitState: false) 12 | } 13 | 14 | mutating func readUleb128() -> UInt64 { 15 | // continuation bit is 1 in default leb128 implementation 16 | return readLeb128(continuationBitState: true) 17 | } 18 | 19 | mutating func readNibUleb128() -> UInt64 { 20 | // Nib file format uses uleb-like coding, but uses 0 as a continuation bit 21 | return readLeb128(continuationBitState: false) 22 | } 23 | 24 | private mutating func readLeb128(continuationBitState: Bool) -> T { 25 | var accumulator: T = 0 26 | var group: UInt8 27 | var shift: Int = 0 28 | let maxShift = (MemoryLayout.size * 8) - 1 29 | repeat { 30 | if shift > maxShift { 31 | fatalError("sleb128 too long to be represented as \(T.self)") 32 | } 33 | group = load(as: UInt8.self) 34 | accumulator |= T(group & 0x7F) << shift 35 | shift += 7 36 | self = advanced(by: 1) 37 | } while (group & 0x80 != 0) == continuationBitState 38 | 39 | if T.isSigned { 40 | let isNegative = group >> 6 & 0x01 != 0 41 | if isNegative { 42 | accumulator |= ~T(0) << min(shift, maxShift) // 1-bit padding 43 | } else { 44 | accumulator &= T.max // clear sign bit (possible 1-bits overflow) 45 | } 46 | } 47 | 48 | return accumulator 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/App/DataAccess/UnsafeRawPointer+Structs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UnsafeRawPointer { 4 | func getStruct() -> T { 5 | return bindMemory(to: T.self, capacity: 1) 6 | .pointee 7 | } 8 | 9 | func getStructs(count: Int) -> [T] { 10 | return bindMemory(to: T.self, capacity: count) 11 | |> { UnsafeBufferPointer(start: $0, count: count) } 12 | |> [T].init 13 | } 14 | } 15 | 16 | extension UnsafeRawPointer { 17 | mutating func readStruct() -> T { 18 | defer { 19 | // TODO: size? 20 | self = advanced(by: MemoryLayout.stride) 21 | } 22 | return getStruct() 23 | } 24 | 25 | mutating func readStructs(count: Int) -> [T] { 26 | defer { 27 | self = advanced(by: MemoryLayout.stride * count) 28 | } 29 | return getStructs(count: count) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/DependencyNode.swift: -------------------------------------------------------------------------------- 1 | protocol DependencyNode { 2 | var isExecutable: Bool { get } 3 | var platform: Mach.Platform { get } 4 | var rpaths: [String] { get } 5 | var dylibs: [String] { get } 6 | var cstrings: [String] { get } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/DependencyNodeLoader+isMachOExecutable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DependencyNodeLoader { 4 | func isMachOFile(atURL url: URL) -> Bool { 5 | do { 6 | let nodes = try load(forURL: url) 7 | return !nodes.isEmpty 8 | } catch { 9 | return false 10 | } 11 | } 12 | 13 | func isMachOExecutable(atURL url: URL) -> Bool { 14 | return autoreleasepool { 15 | do { 16 | let nodes = try load(forURL: url) 17 | return nodes.contains(where: { $0.isExecutable }) 18 | } catch { 19 | return false 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/DependencyNodeLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol DependencyNodeLoader { 4 | func load(forURL url: URL) throws -> [DependencyNode] 5 | } 6 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/FileRepository+FrameworkLocationResolving.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileRepository { 4 | func resolvedSystemFrameworkLocations(dylibEntry: String, referencingURL: URL, dependencyNodeLoader: DependencyNodeLoader) -> [URL] { 5 | guard dylibEntry.isSystemFrameworkEntry else { 6 | return [] 7 | } 8 | let dylibEntryComponents = dylibEntry.components(separatedBy: "/") 9 | guard let frameworkComponentIndex = dylibEntryComponents.firstIndex(where: { $0.hasSuffix(".framework") }) else { 10 | return [] 11 | } 12 | let frameworkPath = dylibEntryComponents[0 ... frameworkComponentIndex].joined(separator: "/") 13 | return dependencyNodeLoader.platforms(forURL: referencingURL) 14 | .map { $0.translated(path: frameworkPath) } 15 | .map(URL.init(fileURLWithPath:)) 16 | .filter(fileExists(atURL:)) 17 | } 18 | } 19 | 20 | private extension String { 21 | var isSystemFrameworkEntry: Bool { 22 | return hasPrefix("/") && contains(".framework") 23 | } 24 | } 25 | 26 | private extension DependencyNodeLoader { 27 | func platforms(forURL url: URL) -> [Mach.Platform] { 28 | return autoreleasepool { 29 | let nodes: [DependencyNode] 30 | do { 31 | nodes = try load(forURL: url) 32 | } catch { 33 | fatalError("Failed loading \(url) because: \(error)") 34 | } 35 | return nodes.map { $0.platform } 36 | } 37 | } 38 | } 39 | 40 | private extension Mach.Platform { 41 | func translated(path: String) -> String { 42 | switch self { 43 | case .ios: 44 | return Paths.iosFrameworksRoot.appending(path) 45 | case .macos: 46 | return Paths.macosFrameworksRoot.appending(path) 47 | case .watchos: 48 | return Paths.watchosFrameworksRoot.appending(path) 49 | case .tvos: 50 | return Paths.tvosFrameworksRoot.appending(path) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/FileRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol FileRepository { 4 | func listFilesRecursively(atURL url: URL) -> [URL] 5 | func fileExists(atURL url: URL) -> Bool 6 | } 7 | 8 | extension FileManager: FileRepository { 9 | func listFilesRecursively(atURL url: URL) -> [URL] { 10 | guard let enumerator = enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) else { 11 | fatalError("Could not enumerate files in \(url)") 12 | } 13 | return enumerator.compactMap { $0 as? URL } 14 | .map { $0.resolvingSymlinksInPath() } 15 | .filter { $0.isRegularFile } 16 | } 17 | 18 | func fileExists(atURL url: URL) -> Bool { 19 | return fileExists(atPath: url.path) 20 | } 21 | } 22 | 23 | private extension URL { 24 | var isRegularFile: Bool { 25 | do { 26 | let value = try resourceValues(forKeys: [.isRegularFileKey]) 27 | return value.isRegularFile ?? false 28 | } catch { 29 | fatalError("Could not read \(self)") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/ObfuscableFilesFilter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ObfuscableFilesFilter { 4 | let isObfuscable: (URL) -> Bool 5 | } 6 | 7 | extension ObfuscableFilesFilter { 8 | func and(_ other: ObfuscableFilesFilter) -> ObfuscableFilesFilter { 9 | return ObfuscableFilesFilter { url in 10 | self.isObfuscable(url) && other.isObfuscable(url) 11 | } 12 | } 13 | 14 | func negate() -> ObfuscableFilesFilter { 15 | return ObfuscableFilesFilter { !self.isObfuscable($0) } 16 | } 17 | 18 | func or(_ other: ObfuscableFilesFilter) -> ObfuscableFilesFilter { 19 | return ObfuscableFilesFilter { url in 20 | self.isObfuscable(url) || other.isObfuscable(url) 21 | } 22 | } 23 | 24 | /// Filter that does not match any files 25 | static func none() -> ObfuscableFilesFilter { 26 | return ObfuscableFilesFilter { _ in false } 27 | } 28 | 29 | static func defaultObfuscableFilesFilter() -> ObfuscableFilesFilter { 30 | // > Swift apps no longer include dynamically linked libraries 31 | // > for the Swift standard library and Swift SDK overlays in 32 | // > build variants for devices running iOS 12.2, watchOS 5.2, 33 | // > and tvOS 12.2. 34 | // -- https://developer.apple.com/documentation/xcode_release_notes/xcode_10_2_beta_release_notes/swift_5_release_notes_for_xcode_10_2_beta 35 | return skipSwiftLibrary() 36 | } 37 | 38 | static func skipSwiftLibrary() -> ObfuscableFilesFilter { 39 | return ObfuscableFilesFilter { url in 40 | !url.lastPathComponent.starts(with: "libswift") 41 | } 42 | } 43 | 44 | static func only(file: URL) -> ObfuscableFilesFilter { 45 | return ObfuscableFilesFilter { url in 46 | url == file 47 | } 48 | } 49 | 50 | static func onlyFiles(in obfuscableDirectory: URL) -> ObfuscableFilesFilter { 51 | return ObfuscableFilesFilter { url in 52 | obfuscableDirectory.standardizedFileURL.contains(url.standardizedFileURL) 53 | } 54 | } 55 | 56 | static func isFramework(framework: String) -> ObfuscableFilesFilter { 57 | let frameworkComponent = framework + ".framework" 58 | return ObfuscableFilesFilter { url in 59 | url.pathComponents.contains(frameworkComponent) 60 | } 61 | } 62 | 63 | static func skipFramework(framework: String) -> ObfuscableFilesFilter { 64 | return isFramework(framework: framework).negate() 65 | } 66 | 67 | static func skipAllFrameworks() -> ObfuscableFilesFilter { 68 | return ObfuscableFilesFilter { url in 69 | !url.pathComponents.contains("Frameworks") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/App/DependencyAnalysis/ObfuscationPaths.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ObfuscationPaths { 4 | var obfuscableImages: Set = [] 5 | var unobfuscableDependencies: Set = [] 6 | var systemFrameworks: Set = [] 7 | var resolvedDylibMapPerImageURL: [URL: [String: URL]] = [:] 8 | var nibs: Set = [] 9 | } 10 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/NSRegularExpression+matches.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSRegularExpression { 4 | func firstMatch(in string: String) -> NSTextCheckingResult? { 5 | return firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.count)) 6 | } 7 | 8 | func matches(in string: String) -> [NSTextCheckingResult] { 9 | return matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/ObjectSymbols.swift: -------------------------------------------------------------------------------- 1 | struct ObjectSymbols { 2 | var selectors: Set 3 | var classNames: Set 4 | } 5 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/RecursiveSourceSymbolsLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol RecursiveSourceSymbolsLoaderProtocol { 4 | func load(fromDirectory url: URL) -> ObjectSymbols 5 | } 6 | 7 | class RecursiveSourceSymbolsLoader: RecursiveSourceSymbolsLoaderProtocol { 8 | func load(fromDirectory url: URL) -> ObjectSymbols { 9 | do { 10 | LOGGER.info("Collecting symbols from source directory \(url)") 11 | return try load(fromDirectory: url, fileManager: FileManager.default) 12 | } catch { 13 | fatalError("Error while reading symbols from source directory '\(url)': \(error)") 14 | } 15 | } 16 | 17 | func load(fromDirectory url: URL, fileManager: FileManager) throws -> ObjectSymbols { 18 | let headers = fileManager.listSourceFilesRecursively(atURL: url) 19 | return headers.map(ObjectSymbols.load(url:)).flatten() 20 | } 21 | } 22 | 23 | private extension FileManager { 24 | func listSourceFilesRecursively(atURL url: URL) -> [URL] { 25 | return listFilesRecursively(atURL: url) 26 | .filter { $0.isSourceFile } 27 | } 28 | } 29 | 30 | private extension URL { 31 | private static let sourceFileExtensionSet: Set = ["h", "m"] 32 | var isSourceFile: Bool { 33 | return URL.sourceFileExtensionSet.contains(pathExtension) 34 | } 35 | } 36 | 37 | private extension ObjectSymbols { 38 | static func load(url: URL) -> ObjectSymbols { 39 | let sourceContents: String 40 | do { 41 | sourceContents = try String(contentsOf: url, encoding: .ascii) 42 | } catch { 43 | fatalError("Could not read \(url) because: \(error.localizedDescription)") 44 | } 45 | let selectors = Set(sourceContents.objCMethodNames) 46 | .union(sourceContents.objCPropertyNames) 47 | let classNames = Set(sourceContents.objCTypeNames) 48 | return ObjectSymbols(selectors: selectors, classNames: classNames) 49 | } 50 | } 51 | 52 | extension Sequence where Element == ObjectSymbols { 53 | func flatten() -> ObjectSymbols { 54 | return reduce(into: ObjectSymbols(selectors: [], classNames: [])) { result, nextSymbols in 55 | result.classNames.formUnion(nextSymbols.classNames) 56 | result.selectors.formUnion(nextSymbols.selectors) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+objCMethodNames.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Remove __unsafe_unretained, _Nullable, _Nonnull and their variations and arrays, eg. [_Nonnull] 4 | private let removedParameterAttributes = try! NSRegularExpression(pattern: "\\b(__unsafe_unretained)|(\\[?__?[nN]ullable\\]?)|(\\[?__?[nN]onnull\\]?)\\b") 5 | private let methodStartRegexp = try! NSRegularExpression(pattern: "^\\s*[-+]\\s*\\([^\\(\\)]*(\\([^\\(\\)]+\\)[^\\(\\)]*)*[^\\(\\)]*\\)", options: [.anchorsMatchLines]) 6 | private let methodSuffixRegexp = try! NSRegularExpression(pattern: "\\s([A-Z_]+_[A-Z_]+\\b)|(__[a-z_]+\\b)", options: []) 7 | private let namedParameterRegexp = try! NSRegularExpression(pattern: "\\b(\\w+:)", options: []) 8 | private let parameterlessMethodNameRegexp = try! NSRegularExpression(pattern: "\\b(\\w+)\\b", options: []) 9 | 10 | private let methodNameDelimiters = CharacterSet([";", "{", "}"]) 11 | 12 | extension String { 13 | var objCMethodNames: [String] { 14 | let headerWithoutComments = withoutComments 15 | let allLines = headerWithoutComments.components(separatedBy: methodNameDelimiters) 16 | .map { $0.withoutOccurences(of: removedParameterAttributes) } 17 | return allLines.compactMap { $0.objCMethodNameFromLine } 18 | } 19 | 20 | private var objCMethodNameRange: NSRange? { 21 | guard let methodStartMatch = methodStartRegexp.firstMatch(in: self) else { 22 | return nil 23 | } 24 | let methodNameLowerBound = methodStartMatch.range.upperBound 25 | let methodNameUpperBoundSearchRange = 26 | NSRange(location: methodNameLowerBound, length: count - methodNameLowerBound) 27 | let methodNameUpperBound = 28 | methodSuffixRegexp.firstMatch(in: self, 29 | options: [], 30 | range: methodNameUpperBoundSearchRange)? 31 | .range.lowerBound 32 | ?? count 33 | return NSRange(location: methodNameLowerBound, 34 | length: methodNameUpperBound - methodNameLowerBound) 35 | } 36 | 37 | private var objCMethodNameFromLine: String? { 38 | guard let methodNameRange = objCMethodNameRange else { 39 | return nil 40 | } 41 | let namedParameterMatches = namedParameterRegexp.matches(in: self, options: [], range: methodNameRange) 42 | if !namedParameterMatches.isEmpty { 43 | return namedParameterMatches 44 | .map { self[$0.range(at: 1)] } 45 | .joined() 46 | } 47 | 48 | return parameterlessMethodNameRegexp 49 | .firstMatch(in: self, options: [], range: methodNameRange) 50 | .flatMap { self[$0.range(at: 1)] } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+objCPropertyNames.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let propertyPrefixRegexp = try! NSRegularExpression(pattern: "@property\\s*(\\([^)]+\\))?", options: []) 4 | private let propertySuffixRegexp = try! NSRegularExpression(pattern: "\\s[A-Z_]+_[A-Z_]+\\b", options: []) 5 | private let blockPropertyRegexp = try! NSRegularExpression(pattern: "\\([*^](\\w+)\\)", options: []) 6 | private let pointerPropertRegexp = try! NSRegularExpression(pattern: "((\\*?\\s*\\w+,\\s*)*\\*?\\s*\\w+)\\s*$", options: []) 7 | 8 | let spacesAndAsterisks = CharacterSet.whitespacesAndNewlines 9 | .union(CharacterSet(charactersIn: "*")) 10 | 11 | extension String { 12 | var objCPropertyNames: [String] { 13 | let headerWithoutComments = withoutComments 14 | let allLines = headerWithoutComments.components(separatedBy: ";") 15 | let propertyLines = allLines.filter { $0.contains("@property") } 16 | return propertyLines.flatMap { $0.objCPropertyNameFromPropertyLine } 17 | } 18 | 19 | private var objCPropertyNameFromPropertyLine: [String] { 20 | let propertyBodyRange = objCPropertyBodyRangeFromPropertyLine 21 | let matcherConfigurations: [(regexp: NSRegularExpression, group: Int)] = [ 22 | (regexp: blockPropertyRegexp, group: 1), 23 | (regexp: pointerPropertRegexp, group: 1), 24 | ] 25 | let matchedNames: [String] = matcherConfigurations.flatMap { c in 26 | c.regexp.matches(in: self, options: [], range: propertyBodyRange) 27 | .map { (match: NSTextCheckingResult) -> String in 28 | self[match.range(at: c.group)] 29 | } 30 | } 31 | if matchedNames.isEmpty { 32 | fatalError("Couldn't resolve property name for line: '\(self)'") 33 | } 34 | if matchedNames.count > 1 { 35 | fatalError("Property name resolved ambiguously for line: '\(self)'. Matched names: \(matchedNames)") 36 | } 37 | let matchedName = matchedNames[0].trimmingCharacters(in: spacesAndAsterisks) 38 | if matchedName.contains(",") { 39 | return matchedName 40 | .components(separatedBy: ",") 41 | .map { $0.trimmingCharacters(in: spacesAndAsterisks) } 42 | } 43 | return [matchedName] 44 | } 45 | 46 | private var objCPropertyBodyRangeFromPropertyLine: NSRange { 47 | let propertyBodyLowerBoundSearchRange = NSRange(location: 0, length: count) 48 | let propertyBodyLowerBound = 49 | propertyPrefixRegexp.firstMatch(in: self, 50 | options: [], 51 | range: propertyBodyLowerBoundSearchRange)! 52 | .range.upperBound 53 | let propertyBodyUpperBoundSearchRange = 54 | NSRange(location: propertyBodyLowerBound, length: count - propertyBodyLowerBound) 55 | let propertyBodyUpperBound = 56 | propertySuffixRegexp.firstMatch(in: self, 57 | options: [], 58 | range: propertyBodyUpperBoundSearchRange)? 59 | .range.lowerBound 60 | ?? count 61 | return NSRange(location: propertyBodyLowerBound, 62 | length: propertyBodyUpperBound - propertyBodyLowerBound) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+objCTypeNames.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let interfaceNameRegexp = try! NSRegularExpression(pattern: "@interface\\s+(\\w+)\\b", options: []) 4 | private let protocolNameRegexp = try! NSRegularExpression(pattern: "@protocol\\s+(\\w+)\\b", options: []) 5 | private let classNameRegexp = try! NSRegularExpression(pattern: "@class\\s+(.*);", options: []) 6 | private let wordRegexp = try! NSRegularExpression(pattern: "(\\w+)\\b", options: []) 7 | 8 | extension String { 9 | var objCTypeNames: [String] { 10 | let headerWithoutComments = withoutComments 11 | let allLines = headerWithoutComments.components(separatedBy: "\n") 12 | let typeLines = allLines.filter { $0.contains("@interface") || $0.contains("@protocol") || $0.contains("@class") } 13 | return typeLines.flatMap { $0.objCTypesFromTypeLine } 14 | } 15 | 16 | private var objCTypesFromTypeLine: [String] { 17 | return 18 | (objCInterfaceNamesFromTypeLine.flatMap { [$0] } ?? []) 19 | + (objCProtocolNamesFromTypeLine.flatMap { [$0] } ?? []) 20 | + objCClassNamesFromTypeLine 21 | } 22 | 23 | private var objCInterfaceNamesFromTypeLine: String? { 24 | return self[interfaceNameRegexp, 1] 25 | } 26 | 27 | private var objCProtocolNamesFromTypeLine: String? { 28 | return self[protocolNameRegexp, 1] 29 | } 30 | 31 | private var objCClassNamesFromTypeLine: [String] { 32 | guard let matchingClassBody = self[classNameRegexp, 1] else { 33 | return [] 34 | } 35 | return matchingClassBody 36 | .components(separatedBy: ",") 37 | .map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } 38 | .compactMap { $0[wordRegexp, 1] } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+subscriptNSRange.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | subscript(nsRange: NSRange) -> String { 5 | let range = Range(nsRange, in: self)! 6 | return String(self[range]) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+subscriptRegexp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | subscript(regexp: NSRegularExpression, group: Int) -> String? { 5 | let matchingRange = regexp 6 | .firstMatch(in: self)? 7 | .range(at: group) 8 | 9 | return matchingRange.flatMap { self[$0] } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/HeadersParsing/String+withoutComments.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let singleLineCommentExpr = try! NSRegularExpression(pattern: "^//.*$", options: [.anchorsMatchLines]) 4 | private let multilineCommentExpr = try! NSRegularExpression(pattern: "/\\*.*?\\*/", options: [.anchorsMatchLines, .dotMatchesLineSeparators]) 5 | 6 | extension String { 7 | var withoutComments: String { 8 | return withoutOccurences(of: singleLineCommentExpr) 9 | .withoutOccurences(of: multilineCommentExpr) 10 | } 11 | 12 | func withoutOccurences(of regex: NSRegularExpression) -> String { 13 | let fullRange = NSRange(location: 0, length: count) 14 | return regex 15 | .matches(in: self, options: [], range: fullRange) 16 | .map { $0.range } 17 | .reversed() 18 | .reduce(into: self) { str, range -> Void in 19 | let r = Range(range, in: str)! 20 | str.removeSubrange(r) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/Mach/ExportTrie/Trie+Loading.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Trie { 4 | init(data: Data, rootNodeOffset: Int) { 5 | self.init(data: data, rootNodeOffset: rootNodeOffset, nodeOffset: rootNodeOffset, label: [], labelRange: 0 ..< 0) 6 | } 7 | 8 | private init(data: Data, rootNodeOffset: Int, nodeOffset: Int, label: [UInt8], labelRange: Range) { 9 | precondition(labelRange.count == label.count) 10 | self.label = label 11 | self.labelRange = labelRange 12 | (exportsSymbol, children) = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> (Bool, [Trie]) in 13 | var cursorPtr = bytes.baseAddress!.advanced(by: nodeOffset) // cursorPtr at node start 14 | let terminalSize = cursorPtr.readUleb128() // cursorPtr after terminal size 15 | let exportsSymbol = terminalSize != 0 16 | cursorPtr = cursorPtr.advanced(by: Int(terminalSize)) // cursorPtr at children count 17 | let childrenCount = cursorPtr.load(as: UInt8.self) 18 | cursorPtr = cursorPtr.advanced(by: 1) // cursorPtr at first child count 19 | var children: [Trie] = [] 20 | for _ in 0 ..< childrenCount { 21 | let childLabelStart = bytes.baseAddress!.distance(to: cursorPtr) 22 | let childLabel = cursorPtr.readStringBytes() 23 | let childLabelRange = Range(offset: UInt64(childLabelStart), count: UInt64(childLabel.count)) 24 | precondition(childLabelRange.upperBound <= data.count) 25 | let childOffset = rootNodeOffset + Int(cursorPtr.readUleb128()) 26 | children.append(Trie(data: data, rootNodeOffset: rootNodeOffset, nodeOffset: childOffset, label: childLabel, labelRange: childLabelRange)) 27 | } 28 | return (exportsSymbol, children) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/App/Mach/ExportTrie/Trie+Parsing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Trie { 4 | var exportedLabelStrings: [String] { 5 | return exportedLabels.compactMap { String(bytes: $0, encoding: .utf8) } 6 | } 7 | 8 | private var exportedLabels: [[UInt8]] { 9 | let childrenLabels = children.flatMap { $0.exportedLabels }.map { label + $0 } 10 | if exportsSymbol { 11 | return [label] + childrenLabels 12 | } else { 13 | return childrenLabels 14 | } 15 | } 16 | 17 | var flatNodes: [Trie] { 18 | var result = [self] 19 | var queue = [self] 20 | while let nextNode = queue.popLast() { 21 | let children = nextNode.children 22 | result += children 23 | queue += children 24 | } 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/App/Mach/ExportTrie/Trie.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Trie { 4 | var exportsSymbol: Bool 5 | // In case of Mach-O trie binary-representation, a trie node doesn't contain a label. A trie node contains an array 6 | // of (child-node-label, child-node-address) entries. MachObfuscator uses different, more natuaral in-memory 7 | // representation. 8 | var labelRange: Range 9 | var label: [UInt8] 10 | var children: [Trie] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Mach/Image+updateMachs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Image { 4 | mutating func updateMachs(block: (inout Mach) -> Void) { 5 | switch contents { 6 | case let .fat(fat): 7 | var mutableFat = fat 8 | mutableFat.architectures = fat.architectures.map { arch in 9 | var mutableArch = arch 10 | block(&mutableArch.mach) 11 | return mutableArch 12 | } 13 | for arch in mutableFat.architectures { 14 | let archRangeInFat = Range(offset: Int(arch.offset), count: arch.mach.data.count) 15 | mutableFat.data.replaceSubrange(archRangeInFat, with: arch.mach.data) 16 | } 17 | contents = .fat(mutableFat) 18 | case let .mach(mach): 19 | var mutableMach = mach 20 | block(&mutableMach) 21 | contents = .mach(mutableMach) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/App/Mach/ImportStack/ImportStack+Parsing.swift: -------------------------------------------------------------------------------- 1 | extension ImportStackEntry { 2 | var symbolString: String { 3 | return String(bytes: symbol, encoding: .utf8)! 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/App/Mach/ImportStack/ImportStack.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias ImportStack = [ImportStackEntry] 4 | 5 | struct ImportStackEntry { 6 | var dylibOrdinal: Int 7 | var symbol: [UInt8] 8 | var symbolRange: Range 9 | var weak: Bool // should be skipped when dylib is missing 10 | } 11 | -------------------------------------------------------------------------------- /Sources/App/Mach/Mach+Dumping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mach+Dumping.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | extension Image { 9 | func dumpMetadata() { 10 | machs.forEach { $0.dumpMetadata() } 11 | } 12 | } 13 | 14 | extension Mach { 15 | func dumpMetadata() { 16 | // Dump objc symbols 17 | LOGGER.info("===== Objc metadata start =====") 18 | LOGGER.info("\(objcClasses.map { "\($0)" }.joined(separator: "\n"))") 19 | LOGGER.info("\(objcCategories.map { "\($0)" }.joined(separator: "\n"))") 20 | LOGGER.info("\(objcProtocols.map { "\($0)" }.joined(separator: "\n"))") 21 | LOGGER.info("===== Objc metadata end =====") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/Mach/Mach+Erasing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Image { 4 | mutating func eraseSymtab() { 5 | updateMachs { $0.eraseSymtab() } 6 | } 7 | 8 | mutating func eraseSwiftReflectiveSections() { 9 | updateMachs { $0.eraseSwiftReflectiveSections() } 10 | } 11 | 12 | mutating func eraseMethTypeSection() { 13 | updateMachs { $0.eraseMethTypeSection() } 14 | } 15 | 16 | mutating func eraseSection(_ sectionName: String, segment segmentName: String) { 17 | updateMachs { $0.eraseSection(sectionName, segment: segmentName) } 18 | } 19 | 20 | // cstring contain filenames that are generated by #file (in Swift) or __FILE__ (C/C++/ObjC) 21 | // In particular they appear when using fatalError in Swift. 22 | // They are stored in binary near class names, so they are deleted to not give original class name this way. 23 | // TODO: more robust means of detecting paths could be used (regex?) 24 | mutating func eraseFilePaths(_ prefixes: [String], usingReplacement replacement: String) { 25 | updateMachs { $0.eraseFilePaths(prefixes, usingReplacement: replacement) } 26 | } 27 | } 28 | 29 | private extension Mach { 30 | mutating func eraseSymtab() { 31 | guard let symtabRange = symtab?.stringTableRange else { 32 | return 33 | } 34 | data.nullify(range: symtabRange.intRange) 35 | } 36 | 37 | mutating func eraseSwiftReflectiveSections() { 38 | for section in swiftReflectionSections { 39 | data.nullify(range: section.range.intRange) 40 | } 41 | } 42 | 43 | mutating func eraseMethTypeSection() { 44 | guard let methTypeSectionRange = objcMethTypeSection?.range else { 45 | return 46 | } 47 | 48 | data.nullify(range: methTypeSectionRange.intRange) 49 | } 50 | 51 | mutating func eraseSection(_ sectionName: String, segment segmentName: String) { 52 | guard let sect = section(sectionName, segment: segmentName) else { 53 | return 54 | } 55 | data.nullify(range: sect.range.intRange) 56 | } 57 | 58 | mutating func eraseFilePaths(_ prefixes: [String], usingReplacement replacement: String) { 59 | guard !prefixes.isEmpty else { 60 | // nothing to do 61 | return 62 | } 63 | 64 | guard let cstrings = cstringSection else { 65 | return 66 | } 67 | 68 | data.replaceStrings(inRange: cstrings.range.intRange, withMapping: { 69 | LOGGER.debug("Removing filename \($0)") 70 | return replacement 71 | }, withFilter: { 72 | cstring in cstring.utf8.count >= replacement.utf8.count && 73 | prefixes.contains(where: { prefix in cstring.starts(with: prefix) }) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/App/Mach/Mach+Saving.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Image { 4 | func save() { 5 | switch contents { 6 | case let .fat(fat): 7 | try! fat.data.write(to: url) 8 | case let .mach(mach): 9 | try! mach.data.write(to: url) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/Mach/Mach+SectionsLookup.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | func section(_ sectionName: String, segment segmentName: String) -> Section? { 5 | return segments 6 | .first(where: { $0.name == segmentName })? 7 | .sections 8 | .first(where: { $0.name == sectionName }) 9 | } 10 | 11 | var objcMethNameSection: Section? { 12 | return section("__objc_methname", segment: "__TEXT") 13 | } 14 | 15 | var objcMethTypeSection: Section? { 16 | return section("__objc_methtype", segment: "__TEXT") 17 | } 18 | 19 | var objcClassNameSection: Section? { 20 | return section("__objc_classname", segment: "__TEXT") 21 | } 22 | 23 | var objcClasslistSection: Section? { 24 | return section("__objc_classlist", segment: "__DATA") 25 | } 26 | 27 | var objcProtocollistSection: Section? { 28 | return section("__objc_protolist", segment: "__DATA") 29 | } 30 | 31 | var objcCatlistSection: Section? { 32 | return section("__objc_catlist", segment: "__DATA") 33 | } 34 | 35 | var cstringSection: Section? { 36 | return section("__cstring", segment: "__TEXT") 37 | } 38 | } 39 | 40 | extension Mach { 41 | private static let swiftReflectiveSections: Set = [ 42 | "__swift3_typeref", 43 | "__swift3_reflstr", 44 | "__swift4_typeref", 45 | "__swift4_reflstr", 46 | "__swift5_typeref", 47 | "__swift5_reflstr", 48 | ] 49 | 50 | var swiftReflectionSections: [Section] { 51 | return segments.first { $0.name == "__TEXT" }? 52 | .sections 53 | .filter { Mach.swiftReflectiveSections.contains($0.name) } 54 | ?? [] 55 | } 56 | } 57 | 58 | extension Mach { 59 | func fileOffset(fromVmOffset vmOffset: I) -> Int { 60 | guard let segment = segments.first(where: { $0.vmRange.contains(UInt64(vmOffset)) }) else { 61 | fatalError("vmOffset \(vmOffset) does not exist in the image") 62 | } 63 | 64 | let fileOffset = Int(segment.fileRange.lowerBound + (UInt64(vmOffset) - segment.vmRange.lowerBound)) 65 | return fileOffset 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/App/Mach/Mach.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Image { 4 | var url: URL 5 | 6 | enum Contents { 7 | case fat(Fat) 8 | case mach(Mach) 9 | } 10 | 11 | var contents: Contents 12 | } 13 | 14 | struct Fat { 15 | var data: Data 16 | 17 | struct Architecture { 18 | var offset: UInt64 19 | var mach: Mach 20 | } 21 | 22 | var architectures: [Architecture] 23 | } 24 | 25 | struct Mach { 26 | var data: Data 27 | 28 | enum MachType { 29 | case executable 30 | case other 31 | } 32 | 33 | var type: MachType 34 | 35 | struct Cpu: Equatable, Hashable { 36 | var type: Int32 37 | var subtype: Int32 38 | } 39 | 40 | var cpu: Cpu 41 | 42 | enum Platform { 43 | case macos 44 | case ios 45 | case watchos 46 | case tvos 47 | } 48 | 49 | var platform: Platform 50 | 51 | var rpaths: [String] 52 | var dylibs: [String] 53 | 54 | struct Section: Equatable { 55 | var name: String 56 | var range: Range 57 | } 58 | 59 | struct Segment: Equatable { 60 | var name: String 61 | var vmRange: Range 62 | var fileRange: Range 63 | var sections: [Section] 64 | } 65 | 66 | var segments: [Segment] 67 | 68 | struct Symtab: Equatable { 69 | var offser: UInt64 70 | var numberOfSymbols: UInt64 71 | var stringTableRange: Range 72 | } 73 | 74 | var symtab: Symtab? 75 | 76 | struct DyldInfo: Equatable { 77 | var bind: Range 78 | var weakBind: Range 79 | var lazyBind: Range 80 | var exportRange: Range 81 | } 82 | 83 | var dyldInfo: DyldInfo? 84 | } 85 | -------------------------------------------------------------------------------- /Sources/App/Mach/MachO+StringsParsing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MachO 3 | 4 | extension segment_command_64 { 5 | var name: String { 6 | return String(bytesTuple: segname) 7 | } 8 | } 9 | 10 | extension segment_command { 11 | var name: String { 12 | return String(bytesTuple: segname) 13 | } 14 | } 15 | 16 | extension section_64 { 17 | var name: String { 18 | return String(bytesTuple: sectname) 19 | } 20 | } 21 | 22 | extension section { 23 | var name: String { 24 | return String(bytesTuple: sectname) 25 | } 26 | } 27 | 28 | extension String { 29 | init(bytesTuple: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)) { 30 | var table = [Int8](repeating: 0, count: 17) 31 | withUnsafePointer(to: bytesTuple) { ptr in 32 | ptr.withMemoryRebound(to: Int8.self, capacity: 16) { ptr in 33 | for i in 0 ..< 16 { 34 | table[i] = ptr[i] 35 | } 36 | } 37 | } 38 | self.init(cString: table) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/App/Mach/ObjC/MachStrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mach+Strings.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | /// Value that is loaded from Mach-O image and is stored at a specific location in it 9 | protocol ContainedInData { 10 | associatedtype ValueType 11 | init(value: ValueType, range: Range) 12 | var value: ValueType { get } 13 | var range: Range { get } 14 | } 15 | 16 | extension CustomStringConvertible where Self: ContainedInData { 17 | var description: String { return "'\(value)'[\(range)]" } 18 | } 19 | 20 | struct PlainStringInData: ContainedInData, CustomStringConvertible { 21 | let value: String 22 | let range: Range 23 | 24 | init(value: String, range: Range) { 25 | precondition(value.utf8.count == range.count) 26 | 27 | self.value = value 28 | self.range = range 29 | } 30 | 31 | static let empty = PlainStringInData(value: "", range: 0 ..< 0) 32 | } 33 | 34 | /// Class/protocol/etc name class in objc class definition that can also contain names of Swift types that are visible from ObjC 35 | struct MangledObjcClassNameInData: ContainedInData, CustomStringConvertible { 36 | let value: String 37 | let range: Range 38 | 39 | init(value: String, range: Range) { 40 | precondition(value.utf8.count == range.count) 41 | 42 | self.value = value 43 | self.range = range 44 | } 45 | } 46 | 47 | extension MangledObjcClassNameInData { 48 | /// Checks if this is Swift name. 49 | /// It looks like for Swift classes it always start with "_Tt" and is mangled, where plain ObjC classes are not mangled 50 | var isSwiftName: Bool { 51 | return value.starts(with: "_Tt") 52 | } 53 | } 54 | 55 | extension Data { 56 | func getCString(atOffset offset: Int) -> R where R: ContainedInData, R.ValueType == String { 57 | let value = getCString(atOffset: offset) 58 | let range = offset ..< offset + value.utf8.count 59 | return R(value: value, range: range) 60 | } 61 | } 62 | 63 | extension Mach.Section { 64 | func contains(data: ContainedInDataType) -> Bool { 65 | return range.intRange.overlaps(data.range) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/App/Mach/ObjC/ObjcSymbols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcSymbols.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | /// These protocols provide architecture independent and quite rich interface 9 | /// for accessing ObjC metadata. 10 | 11 | protocol ObjcIvar { 12 | var name: PlainStringInData { get } 13 | var type: PlainStringInData { get } 14 | } 15 | 16 | protocol ObjcProperty { 17 | var name: PlainStringInData { get } 18 | var attributes: PlainStringInData { get } 19 | } 20 | 21 | extension ObjcProperty { 22 | var attributeValues: [String] { 23 | return attributes.value.split(separator: ",").map { String($0) } 24 | } 25 | 26 | /// Property type string. 27 | /// Format is desribed in https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW5 28 | /// but it is outdated. 29 | /// However it seems that property types are encoded in the same way as in methtypes, ie. they are surrounded by quotation marks, brackets, etc. 30 | var typeAttribute: String { 31 | guard let typeattr = attributeValues.first, typeattr.starts(with: "T") else { 32 | fatalError("Type attribute missing or in unexpected format for property \(name.value)") 33 | } 34 | return typeattr 35 | } 36 | } 37 | 38 | protocol ObjcMethod { 39 | var name: PlainStringInData { get } 40 | var methType: PlainStringInData { get } 41 | } 42 | 43 | protocol ObjcClass { 44 | var ivarLayout: PlainStringInData? { get } 45 | var name: MangledObjcClassNameInData { get } 46 | var ivars: [ObjcIvar] { get } 47 | var methods: [ObjcMethod] { get } 48 | var properties: [ObjcProperty] { get } 49 | } 50 | 51 | protocol ObjcCategory { 52 | var name: MangledObjcClassNameInData { get } 53 | /// Class to which this category is related 54 | var cls: ObjcClass? { get } 55 | var methods: [ObjcMethod] { get } 56 | var properties: [ObjcProperty] { get } 57 | } 58 | 59 | protocol ObjcProtocol { 60 | var name: MangledObjcClassNameInData { get } 61 | var methods: [ObjcMethod] { get } 62 | var properties: [ObjcProperty] { get } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+classNames.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | // Only ObjC class names 5 | var classNamesInData: [MangledObjcClassNameInData] { 6 | // TODO: should category names be treated as classnames? Currently they are because they used to be, 7 | // but it may be not the best solution. 8 | return (objcClasses.map { $0.name } + objcProtocols.map { $0.name }).filter { !$0.isSwiftName } + pureObjcCategoryNames 9 | } 10 | 11 | // Only ObjC class names 12 | var classNames: [String] { 13 | return classNamesInData.map { $0.value } 14 | } 15 | 16 | private var pureObjcCategoryNames: [MangledObjcClassNameInData] { 17 | return objcCategories.map { $0.name }.filter(isPureObjCCategory(_:)) 18 | } 19 | 20 | func isPureObjCCategory(_ name: MangledObjcClassNameInData) -> Bool { 21 | return objcClassNameSection?.contains(data: name) ?? false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+cstrings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | var cstrings: [String] { 5 | guard let cstringSection = cstringSection, 6 | !cstringSection.range.isEmpty 7 | else { return [] } 8 | let cstringData = data.subdata(in: cstringSection.range.intRange) 9 | return cstringData.split(separator: 0).compactMap { String(bytes: $0, encoding: .utf8) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+exportTrie.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | var exportedTrie: Trie? { 5 | guard let dyldInfo = dyldInfo, 6 | !dyldInfo.exportRange.isEmpty 7 | else { return nil } 8 | return Trie(data: data, rootNodeOffset: Int(dyldInfo.exportRange.lowerBound)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+importStack.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | var importStack: ImportStack { 5 | guard let dyldInfo = dyldInfo else { 6 | return [] 7 | } 8 | var importStack = ImportStack() 9 | if !dyldInfo.bind.isEmpty { 10 | importStack.add(opcodesData: data, range: dyldInfo.bind.intRange, weakly: false) 11 | } 12 | if !dyldInfo.weakBind.isEmpty { 13 | importStack.add(opcodesData: data, range: dyldInfo.weakBind.intRange, weakly: true) 14 | } 15 | if !dyldInfo.lazyBind.isEmpty { 16 | importStack.add(opcodesData: data, range: dyldInfo.lazyBind.intRange, weakly: false) 17 | } 18 | importStack.resolveMissingDylibOrdinals() 19 | return importStack 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+properties.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | // TODO: custom setter and getter names 5 | var dynamicPropertyNames: [String] { 6 | return properties 7 | .filter { $0.isDynamic } 8 | .map { $0.name.value } 9 | } 10 | 11 | /// All properties of all object kinds 12 | var properties: [ObjcProperty] { 13 | return classProperties + categoryProperties + protocolProperties 14 | } 15 | 16 | private var classProperties: [ObjcProperty] { 17 | return objcClasses.flatMap { $0.properties } 18 | } 19 | 20 | private var categoryProperties: [ObjcProperty] { 21 | return objcCategories.flatMap { $0.properties } 22 | } 23 | 24 | private var protocolProperties: [ObjcProperty] { 25 | return objcProtocols.flatMap { $0.properties } 26 | } 27 | } 28 | 29 | private extension ObjcProperty { 30 | var isDynamic: Bool { 31 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW6 32 | return attributeValues.contains("D") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/App/Mach/Parsing/Mach+selectors.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mach { 4 | // See `sel_init` function in https://opensource.apple.com/source/objc4/objc4-750.1/runtime/objc-sel.mm.auto.html 5 | // These selectors are usually loaded from libobjc.A.dylib and blacklisted as system selectors, 6 | // this list is important only in case of using the `xx-no-analyze-dependencies` 7 | static var libobjcSelectors: Set { return [ 8 | "load", 9 | "initialize", 10 | "resolveInstanceMethod:", 11 | "resolveClassMethod:", 12 | ".cxx_construct", 13 | ".cxx_destruct", 14 | "retain", 15 | "release", 16 | "autorelease", 17 | "retainCount", 18 | "alloc", 19 | "allocWithZone:", 20 | "dealloc", 21 | "copy", 22 | "new", 23 | "forwardInvocation:", 24 | "_tryRetain", 25 | "_isDeallocating", 26 | "retainWeakReference", 27 | "allowsWeakReference", 28 | ] 29 | } 30 | 31 | var selectors: [String] { 32 | guard let methNameSection = objcMethNameSection, 33 | !methNameSection.range.isEmpty 34 | else { return [] } 35 | let methodNamesData = data.subdata(in: methNameSection.range.intRange) 36 | return methodNamesData.split(separator: 0).compactMap { String(bytes: $0, encoding: .utf8) } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBArchive/NibArchive+Print.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // TODO: this is only for debug, remove 4 | 5 | extension NibArchive { 6 | func humanReadable() { 7 | var visitedObjects: Set = [] 8 | objects.enumerated().forEach { idx, object in 9 | if !visitedObjects.contains(idx) { 10 | print("=== object #\(idx) ===") 11 | print(object.humanReadable(forNib: self, 12 | visitedObjects: &visitedObjects, 13 | objectsStack: [idx])) 14 | } 15 | } 16 | } 17 | } 18 | 19 | private extension NibArchive.Object { 20 | func humanReadable(forNib nib: NibArchive, visitedObjects: inout Set, objectsStack: [Int]) -> String { 21 | let className = nib.classes[Int(classIndex)] 22 | var lines: [String] = ["object \(className.value) {"] 23 | 24 | let range = Range(offset: UInt64(valuesIndex), count: UInt64(valuesCount)).intRange 25 | let objectValues = nib.values[range] 26 | objectValues.forEach { value in 27 | let key = nib.keys[Int(value.keyIndex)].value 28 | let valueStr = value.value 29 | .humanReadable(forNib: nib, visitedObjects: &visitedObjects, objectsStack: objectsStack) 30 | .split(separator: "\n") 31 | .enumerated() 32 | .map { offset, element in offset == 0 ? element : " " + element } 33 | .joined(separator: "\n") 34 | lines.append(" \(key) -> \(valueStr)") 35 | } 36 | 37 | lines.append("}") 38 | return lines.joined(separator: "\n") 39 | } 40 | } 41 | 42 | extension NibArchive.Value.ValueType { 43 | func humanReadable(forNib nib: NibArchive, visitedObjects: inout Set, objectsStack: [Int]) -> String { 44 | switch self { 45 | case let .int8(i): 46 | return "\(i)" 47 | case let .int16(i): 48 | return "\(i)" 49 | case let .int32(i): 50 | return "\(i)" 51 | case let .int64(i): 52 | return "\(i)" 53 | case .boolTrue: 54 | return "true" 55 | case .boolFalse: 56 | return "false" 57 | case let .float(f): 58 | return "\(f)" 59 | case let .double(d): 60 | return "\(d)" 61 | case let .data(d): 62 | return String(bytes: d, encoding: .utf8) ?? d.description 63 | case .null: 64 | return "(null)" 65 | case let .object(o): 66 | let object = nib.objects[o] 67 | if objectsStack.contains(o) { 68 | let className = nib.classes[Int(object.classIndex)] 69 | return "object #\(o) \(className.value) (cycle)" 70 | } else { 71 | visitedObjects.insert(Int(o)) 72 | return object.humanReadable(forNib: nib, visitedObjects: &visitedObjects, objectsStack: objectsStack + [o]) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBArchive/NibArchive+Saving.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NibArchive { 4 | func save() { 5 | try! data.write(to: url) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBArchive/NibArchive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct NibArchive { 4 | var url: URL 5 | var data: Data 6 | 7 | // Those properties do not update when modyfing data. They are kind of cache. 8 | // TODO: what to do with them? 9 | let objects: [Object] 10 | let values: [Value] 11 | let keys: [RangedString] 12 | let classes: [RangedString] 13 | 14 | struct Object { 15 | var classIndex: Int 16 | var valuesIndex: Int 17 | var valuesCount: Int 18 | } 19 | 20 | struct Value { 21 | enum ValueType { 22 | case int8(Int8) 23 | case int16(Int16) 24 | case int32(Int32) 25 | case int64(Int64) 26 | case boolTrue 27 | case boolFalse 28 | case float(Float) 29 | case double(Double) 30 | case data(Data) 31 | case null 32 | case object(Int) 33 | } 34 | 35 | var keyIndex: Int 36 | var value: ValueType 37 | var valueRange: Range 38 | } 39 | 40 | struct RangedString { 41 | var value: String 42 | var range: Range 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBPlist/CFKeyedArchiverUIDGetValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Based on https://gist.github.com/NSProgrammer/8c2ce755d15777e62079788a7d788394 4 | // 5 | // CFUUID has a very nice feature in that it's structure 6 | // is always the CFRuntimeBase struct (which we don't have access to) 7 | // followed by a UUID in bytes. 8 | // By simply traversing the CFUUID structs byte layout until we find 9 | // the matching UUID bytes, we can determine the canonical size 10 | // of the CFRuntimeBase at runtime! 11 | // This is crucial since CFRuntimeBase is not guaranteed to stay 12 | // the same size for any given OS release, and runtime inspection is 13 | // necessary. 14 | 15 | private var CFRuntimeBaseSize: Int = calculateCFRuntimeBaseSize() 16 | 17 | private func calculateCFRuntimeBaseSize() -> Int { 18 | let uuidRef = CFUUIDCreate(nil)! 19 | let uuidBytes = CFUUIDGetUUIDBytes(uuidRef) 20 | let uuidBytesRawArray: [UInt8] = withUnsafeBytes(of: uuidBytes) { [UInt8]($0) } 21 | let uuidBytesCount = uuidBytesRawArray.count 22 | let index: Int? = 23 | withUnsafePointer(to: uuidRef) { uuidRefPtr in 24 | uuidRefPtr.withMemoryRebound(to: UnsafeRawPointer.self, 25 | capacity: 1) { uuidPtr in 26 | let ptr = uuidPtr.pointee 27 | return (0 ..< uuidBytesCount * 3).first(where: { offset in 28 | let bytesAtCuror: [UInt8] = ptr.advanced(by: offset).getStructs(count: uuidBytesCount) 29 | return bytesAtCuror == uuidBytesRawArray 30 | }) 31 | } 32 | } 33 | return index! 34 | } 35 | 36 | // Based on https://opensource.apple.com/source/CF/CF-368/Parsing.subproj/CFBinaryPList.c.auto.html 37 | // 38 | // struct __CFKeyedArchiverUID { 39 | // CFRuntimeBase _base; 40 | // uint32_t _value; 41 | // }; 42 | 43 | func CFKeyedArchiverUIDGetValue(_ uid: Any) -> Int { 44 | var mutableUid = uid 45 | return withUnsafePointer(to: &mutableUid) { ptr in 46 | ptr.withMemoryRebound(to: UnsafePointer.self, capacity: 1) { ptr in 47 | ptr.pointee.advanced(by: CFRuntimeBaseSize).withMemoryRebound(to: UInt32.self, capacity: 1) { ptr in 48 | Int(ptr.pointee) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBPlist/NibPlist+Loading.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NibPlist { 4 | private static let magicBytes: [UInt8] = [UInt8]("bplist00".utf8) 5 | 6 | static func canLoad(from url: URL) -> Bool { 7 | guard let data = try? Data(contentsOf: url) else { 8 | fatalError("Could not read \(url)") 9 | } 10 | return data.starts(with: magicBytes) 11 | } 12 | 13 | static func load(from url: URL) -> Nib { 14 | let data = try! Data(contentsOf: url) 15 | var format: PropertyListSerialization.PropertyListFormat = .binary 16 | let nibObject = try! PropertyListSerialization.propertyList(from: data, options: [], format: &format) 17 | let nib = nibObject as! [String: Any] 18 | return NibPlist(url: url, format: format, contents: nib) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBPlist/NibPlist+Saving.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NibPlist { 4 | func save() { 5 | let data = try! PropertyListSerialization.data(fromPropertyList: contents, format: format, options: 0) 6 | try! data.write(to: url) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/Nib/Formats/NIBPlist/NibPlist.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct NibPlist { 4 | var url: URL 5 | var format: PropertyListSerialization.PropertyListFormat 6 | var contents: [String: Any] 7 | } 8 | -------------------------------------------------------------------------------- /Sources/App/Nib/Nib.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // TODO: Savable protocol should be moved to some commons? 4 | protocol Nib: Savable { 5 | static func canLoad(from url: URL) -> Bool 6 | static func load(from url: URL) -> Nib 7 | 8 | var selectors: [String] { get } 9 | var classNames: [String] { get } 10 | 11 | mutating func modifySelectors(withMapping map: [String: String]) 12 | mutating func modifyClassNames(withMapping map: [String: String]) 13 | 14 | func save() 15 | } 16 | -------------------------------------------------------------------------------- /Sources/App/Nib/URL+NibLoading.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let supportedNibFormats: [Nib.Type] = [ 4 | NibPlist.self, 5 | NibArchive.self, 6 | ] 7 | 8 | extension URL { 9 | func loadNib() -> Nib { 10 | guard let supportedNibFormat = supportedNibFormats.first(where: { 11 | $0.canLoad(from: self) 12 | }) else { 13 | fatalError("unsupported NIB format in \(self)") 14 | } 15 | return supportedNibFormat.load(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/CaesarMangler/CaesarCypher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CaesarCypher { 4 | private let asciiRange = UInt8(33) ... UInt8(126) 5 | 6 | func encrypt(element: UInt8, key: UInt8) -> UInt8 { 7 | if representsAsciiSemicolon(element) { 8 | return element 9 | } 10 | 11 | if asciiRange.contains(element) { 12 | let elementShiftedByKey: UInt8 = element + key 13 | return asciiRange.contains(elementShiftedByKey) ? elementShiftedByKey 14 | : elementShiftedByKey - UInt8(asciiRange.count) 15 | } 16 | 17 | return element 18 | } 19 | 20 | private func representsAsciiSemicolon(_ value: UInt8) -> Bool { 21 | return value == 0x3A 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/CaesarMangler/CaesarExportTrieMangler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol CaesarExportTrieMangling: AnyObject { 4 | func mangle(trie: Trie, withCaesarCypherKey cypherKey: UInt8) -> Trie 5 | } 6 | 7 | final class CaesarExportTrieMangler: CaesarExportTrieMangling { 8 | private let caesarCypher = CaesarCypher() 9 | 10 | func mangle(trie: Trie, withCaesarCypherKey key: UInt8) -> Trie { 11 | var trieCopy = trie 12 | 13 | trieCopy.label = trieCopy.label.map { 14 | caesarCypher.encrypt(element: $0, key: key) 15 | } 16 | 17 | trieCopy.children = trieCopy.children.map { trie -> Trie in 18 | mangle(trie: trie, withCaesarCypherKey: key) 19 | } 20 | 21 | return trieCopy 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/CaesarMangler/CaesarMangler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CaesarMangler: SymbolMangling { 4 | private let exportTrieMangler: CaesarExportTrieMangling 5 | 6 | private let caesarStringMangler: CaesarStringMangling = CaesarStringMangler() 7 | 8 | private let cypherKey: UInt8 = 13 9 | 10 | init(exportTrieMangler: CaesarExportTrieMangling) { 11 | self.exportTrieMangler = exportTrieMangler 12 | } 13 | 14 | func mangleSymbols(_ symbols: ObfuscationSymbols) -> SymbolManglingMap { 15 | let mangledClasses = symbols.whitelist.classes.map { 16 | ($0, caesarStringMangler.mangle($0, usingCypherKey: cypherKey)) 17 | } 18 | 19 | let selectorsManglingEntryProvider: [(String, String)] = symbols.whitelist.selectors.map { 20 | ($0, caesarStringMangler.mangle($0, usingCypherKey: cypherKey)) 21 | } 22 | 23 | let mangledSelectors = selectorsManglingEntryProvider 24 | 25 | let classesMap = Dictionary(uniqueKeysWithValues: mangledClasses) 26 | let selectorsMap = Dictionary(uniqueKeysWithValues: mangledSelectors) 27 | 28 | if let clashedSymbol = classesMap.values.first(where: { symbols.blacklist.classes.contains($0) }) 29 | ?? selectorsMap.values.first(where: { symbols.blacklist.selectors.contains($0) }) { 30 | fatalError("ReverseMangler clashed on symbol '\(clashedSymbol)'") 31 | } 32 | 33 | let triesPerCpuAtUrl: [URL: SymbolManglingMap.TriePerCpu] = symbols.exportTriesPerCpuIdPerURL.mapValues { 34 | $0.mapValues { 35 | SymbolManglingMap.ObfuscationTriePair(unobfuscated: $0, 36 | obfuscated: exportTrieMangler.mangle(trie: $0, withCaesarCypherKey: cypherKey)) 37 | } 38 | } 39 | 40 | return SymbolManglingMap(selectors: selectorsMap, classNames: classesMap, exportTrieObfuscationMap: triesPerCpuAtUrl) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/CaesarMangler/CaesarStringMangler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol CaesarStringMangling: AnyObject { 4 | func mangle(_ word: String, usingCypherKey cypherKey: UInt8) -> String 5 | } 6 | 7 | final class CaesarStringMangler: CaesarStringMangling { 8 | private let caesarCypher = CaesarCypher() 9 | 10 | func mangle(_ word: String, usingCypherKey cypherKey: UInt8) -> String { 11 | if word.hasPrefix("set") { 12 | let startIndexShiftedByThree = word.index(word.startIndex, offsetBy: 3)... 13 | let wordSubstring: String = String(word[startIndexShiftedByThree]) 14 | return "set" + mangle(wordSubstring, usingCypherKey: cypherKey) 15 | } 16 | 17 | return String(bytes: word.utf8.map { caesarCypher.encrypt(element: $0, key: cypherKey) }, encoding: .utf8)! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/RealWordsMangler/RealWordsExportTrieMangler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol RealWordsExportTrieMangling: AnyObject { 4 | func mangle(trie: Trie) -> Trie 5 | } 6 | 7 | final class RealWordsExportTrieMangler: RealWordsExportTrieMangling { 8 | private let minimumFillValue: UInt8 9 | 10 | init(machOViewDoomEnabled: Bool) { 11 | minimumFillValue = machOViewDoomEnabled ? 0 : 1 12 | } 13 | 14 | func mangle(trie: Trie) -> Trie { 15 | // In case of Mach-O trie binary-representation, a root trie node doesn't even have a space to store its label. 16 | // That's why it is always empty in the `Trie` struct. But it feels unsafe to pass `0` for 17 | // `fillingRootLabelWith`, because `0` is a cstring end marker. 18 | var mutableTrie = trie 19 | _ = mutableTrie.fillRecursively(startingWithFillValue: minimumFillValue, 20 | minimumFillValue: minimumFillValue) 21 | return mutableTrie 22 | } 23 | } 24 | 25 | private extension Trie { 26 | struct FillResult { 27 | var finalFillValue: UInt8 28 | } 29 | 30 | mutating func fillRecursively(startingWithFillValue initialFillValue: UInt8, 31 | minimumFillValue: UInt8) -> FillResult { 32 | label = Array(repeating: initialFillValue, count: label.count) 33 | var childFillValue = label.isEmpty 34 | ? initialFillValue // children won't get any prefix from their parent, need to iterate the parent's fillValue 35 | : minimumFillValue // children are safe to be filled with independent enumeration 36 | for childIdx in children.indices { 37 | let fillResult = children[childIdx].fillRecursively(startingWithFillValue: childFillValue, 38 | minimumFillValue: minimumFillValue) 39 | childFillValue = fillResult.finalFillValue // child decides about syncing the iterator back 40 | } 41 | 42 | if label.isEmpty { 43 | // need to sync parent fillValue iterator back 44 | return FillResult(finalFillValue: childFillValue) 45 | } else { 46 | // children use independent iteration, just increment parent's fillValue iterator 47 | let addResult = initialFillValue.addingReportingOverflow(1) 48 | guard !addResult.overflow else { 49 | fatalError("Trie label values probably exhausted at \(labelRange)") 50 | } 51 | return FillResult(finalFillValue: addResult.partialValue) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/RealWordsMangler/SentenceGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // TODO: 4 | // Concatenate 5 | // 0-1 adverb https://www.talkenglish.com/vocabulary/top-250-adverbs.aspx 6 | // with 7 | // 1-* nouns https://www.talkenglish.com/vocabulary/top-1500-nouns.aspx 8 | 9 | protocol SentenceGenerator { 10 | func getUniqueSentence(length: Int) -> String? 11 | } 12 | 13 | final class EnglishSentenceGenerator: SentenceGenerator { 14 | private var previousSentences: Set = [] 15 | 16 | // Returns unique random sentence, or nil when unique sentence can not be generated. 17 | func getUniqueSentence(length: Int) -> String? { 18 | // try 20 times before giving up 19 | for _ in 0 ..< 20 { 20 | let sentence = getSentence(length: length) 21 | if !previousSentences.contains(sentence) { 22 | previousSentences.insert(sentence) 23 | return sentence 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | private func getSentence(length: Int) -> String { 30 | var randomWords: [String] = [] 31 | var remainingLength = length 32 | while remainingLength > 0 { 33 | var nextWord = Words.multiletterWords.random! 34 | if nextWord.count > remainingLength { 35 | nextWord = Words.englishWordsPerLength[remainingLength]!.random! 36 | } 37 | randomWords.append(nextWord) 38 | remainingLength -= nextWord.count 39 | } 40 | let randomCapitalizedWords = randomWords.isEmpty 41 | ? [] 42 | : randomWords.prefix(upTo: 1) + randomWords.suffix(from: 1).map { $0.capitalizedOnFirstLetter } 43 | return randomCapitalizedWords.joined() 44 | } 45 | } 46 | 47 | private extension Array { 48 | var random: Element? { 49 | guard !isEmpty else { 50 | return nil 51 | } 52 | return self[count.random] 53 | } 54 | } 55 | 56 | private extension Int { 57 | var random: Int { 58 | return Int(arc4random_uniform(UInt32(self))) 59 | } 60 | } 61 | 62 | private extension Words { 63 | static let englishWordsPerLength: [Int: [String]] = Dictionary(grouping: englishTop1000, by: { $0.count }) 64 | static let multiletterWords: [String] = englishTop1000.filter { $0.count >= 2 } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/SymbolManglers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum SymbolManglers: String, CaseIterable { 4 | case caesar 5 | case realWords 6 | 7 | static var helpSummary: String { 8 | return "Available manglers by mangler_key:\n" 9 | + (SymbolManglers.allCases.map { " \($0) - \($0.helpDescription)" } 10 | .joined(separator: "\n")) 11 | } 12 | 13 | static var defaultMangler: SymbolManglers { 14 | return SymbolManglers.realWords 15 | } 16 | 17 | static var defaultManglerKey: String { 18 | return defaultMangler.rawValue 19 | } 20 | 21 | var helpDescription: String { 22 | switch self { 23 | case .realWords: 24 | return "replace objc symbols with random words and fill dyld info symbols with numbers" 25 | case .caesar: 26 | return "ROT13 all objc symbols and dyld info" 27 | } 28 | } 29 | 30 | func resolveMangler(machOViewDoomEnabled: Bool = false) -> SymbolMangling { 31 | switch self { 32 | case .realWords: 33 | let realWordsExportTrieMangler = RealWordsExportTrieMangler(machOViewDoomEnabled: machOViewDoomEnabled) 34 | return RealWordsMangler(exportTrieMangler: realWordsExportTrieMangler) 35 | case .caesar: 36 | return CaesarMangler(exportTrieMangler: CaesarExportTrieMangler()) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/SymbolMangling.swift: -------------------------------------------------------------------------------- 1 | protocol SymbolMangling { 2 | func mangleSymbols(_: ObfuscationSymbols) -> SymbolManglingMap 3 | } 4 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/SymbolManglingMap.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SymbolManglingMap { 4 | typealias ObfuscationTriePair = (unobfuscated: Trie, obfuscated: Trie) 5 | 6 | typealias TriePerCpu = [CpuId: ObfuscationTriePair] 7 | 8 | var selectors: [String: String] 9 | 10 | var classNames: [String: String] 11 | 12 | var exportTrieObfuscationMap: [URL: TriePerCpu] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/App/SymbolMangling/SymbolsReporting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SymbolsReporting.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | protocol SymbolsReporter { 9 | func report(options: Options) 10 | func reportObjcMangling(map: SymbolManglingMap) 11 | func reportBlacklistedSymbols(symbolKind: String, symbols: [String]) 12 | } 13 | 14 | extension SymbolsReporter { 15 | func report(options _: Options) {} 16 | func reportObjcMangling(map _: SymbolManglingMap) {} 17 | func reportBlacklistedSymbols(symbolKind _: String, symbols _: [String]) {} 18 | } 19 | 20 | class NoReporter: SymbolsReporter {} 21 | 22 | class ConsoleReporter: SymbolsReporter { 23 | func report(options: Options) { 24 | // Try to make this text more readable by splitting in to multiple lines. 25 | // Hopefully this will not break anything, but commas in values will be replaced 26 | let optionsString = "\(options)".replacingOccurrences(of: ", ", with: "\n") 27 | .replacingOccurrences(of: "Options(", with: "", options: .anchored, range: nil) 28 | .dropLast(1) 29 | LOGGER.info("===== Obfuscator options =====\n\(optionsString)") 30 | } 31 | 32 | func reportObjcMangling(map: SymbolManglingMap) { 33 | LOGGER.info("===== ObjC obfuscation report =====") 34 | LOGGER.info("Classes mapping:\n\(map.classNames.sorted(by: <).map { "\($0) -> \($1)" }.joined(separator: "\n"))") 35 | LOGGER.info("Selectors mapping:\n\(map.selectors.sorted(by: <).map { "\($0) -> \($1)" }.joined(separator: "\n"))") 36 | } 37 | 38 | func reportBlacklistedSymbols(symbolKind: String, symbols: [String]) { 39 | if !symbols.isEmpty { 40 | // Very long lines cause problems with some tools 41 | LOGGER.info("\(symbolKind) removed by blacklist:\n\(symbols.sorted().chunked(into: 10).map { "\($0.joined(separator: ", "))" }.joined(separator: ",\n"))") 42 | } 43 | } 44 | } 45 | 46 | private extension Array { 47 | func chunked(into size: Int) -> [[Element]] { 48 | return stride(from: 0, to: count, by: size).map { 49 | Array(self[$0 ..< Swift.min($0 + size, count)]) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/App/SymbolsCollecting/ObfuscationSymbols+Finding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObfuscationSymbols+Finding.swift 3 | // MachObfuscator 4 | // 5 | 6 | import Foundation 7 | 8 | extension ObfuscationSymbols { 9 | static func findSymbol(obfuscationPaths: ObfuscationPaths, 10 | loader: SymbolsSourceLoader, 11 | symbol: String) -> [(URL, String)] { 12 | return (obfuscationPaths.unobfuscableDependencies.union(obfuscationPaths.obfuscableImages)) 13 | .concurrentMap { (url) -> [(URL, String)] in 14 | try! loader.load(forURL: url).flatMap { (source) -> [(URL, String)] in 15 | var result: [(URL, String)] = [] 16 | if source.cstrings.contains(symbol) { 17 | result.append((url, "cstrings")) 18 | } 19 | if source.classNames.contains(symbol) { 20 | result.append((url, "classNames")) 21 | } 22 | if source.selectors.contains(symbol) { 23 | result.append((url, "selectors")) 24 | } 25 | return result 26 | } 27 | }.flatMap { $0 } 28 | } 29 | 30 | // Simpler for using in debugger 31 | static func findSymbolToString(obfuscationPaths: ObfuscationPaths, 32 | loader: SymbolsSourceLoader, 33 | symbol: String) -> [String] { 34 | return findSymbol(obfuscationPaths: obfuscationPaths, loader: loader, symbol: symbol).map { "\($0.0) -> \($0.1)" } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/App/SymbolsCollecting/ObfuscationSymbols.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ObfuscationSymbols { 4 | var whitelist: ObjCSymbols // those symbols should be obfuscated 5 | var blacklist: ObjCSymbols // mangling algorighms should avoid using blacklisted symbols 6 | var removedList: ObjCSymbols // symbols removed by blacklist 7 | var exportTriesPerCpuIdPerURL: [URL: [CpuId: Trie]] // export tries to be fully obfuscated 8 | } 9 | 10 | struct ObjCSymbols { 11 | var selectors: Set 12 | var classes: Set 13 | } 14 | -------------------------------------------------------------------------------- /Sources/App/SymbolsCollecting/SymbolsSource.swift: -------------------------------------------------------------------------------- 1 | protocol SymbolsSource { 2 | var selectors: [String] { get } 3 | var classNames: [String] { get } 4 | var cstrings: [String] { get } 5 | var dynamicPropertyNames: [String] { get } 6 | var exportedTrie: Trie? { get } 7 | var cpu: Mach.Cpu { get } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/SymbolsCollecting/SymbolsSourceLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol SymbolsSourceLoader { 4 | func load(forURL url: URL) throws -> [SymbolsSource] 5 | } 6 | -------------------------------------------------------------------------------- /Sources/App/SymbolsListParsing/TextFileSymbolListLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol TextFileSymbolListLoaderProtocol { 4 | func load(fromTextFile url: URL) -> ObjectSymbols 5 | } 6 | 7 | class TextFileSymbolListLoader: TextFileSymbolListLoaderProtocol { 8 | func load(fromTextFile url: URL) -> ObjectSymbols { 9 | do { 10 | LOGGER.info("Collecting symbols from text file \(url)") 11 | return try load(fromTextFile: url, stringWithContentsOf: String.init(contentsOf:)) 12 | } catch { 13 | fatalError("Error while reading symbols from text file '\(url)': \(error)") 14 | } 15 | } 16 | 17 | func load(fromTextFile url: URL, stringWithContentsOf: (URL) throws -> String) throws -> ObjectSymbols { 18 | let contents = try stringWithContentsOf(url) 19 | let lines = contents.split(separator: "\n") 20 | .map { $0.trimmingCharacters(in: .whitespaces) } 21 | .uniq 22 | 23 | // Following lines filtering is just an optimisation, removing those filters will not affect the MachObfuscator 24 | // output. Why optimising? `strings` produces a lot of garbage. 25 | return ObjectSymbols(selectors: lines.filter { $0.couldBeSelector }, 26 | classNames: lines.filter { $0.couldBeClassName }) 27 | } 28 | } 29 | 30 | private extension StringProtocol { 31 | var couldBeSelector: Bool { 32 | return rangeOfCharacter(from: CharacterSet.selectorForbidden) == nil 33 | } 34 | 35 | var couldBeClassName: Bool { 36 | return rangeOfCharacter(from: CharacterSet.classNameForbidden) == nil 37 | } 38 | } 39 | 40 | private extension CharacterSet { 41 | static let selectorForbidden = 42 | CharacterSet.whitespaces 43 | .union(.init(charactersIn: "!@#$%^&*()+-={}[]\"|;'\\<>?,./")) 44 | static let classNameForbidden = 45 | CharacterSet.whitespaces 46 | .union(.init(charactersIn: "!@#$%^&*()+-={}[]:\"|;'\\<>?,./")) 47 | } 48 | -------------------------------------------------------------------------------- /Sources/App/run.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func run() { 4 | let options = Options.fromCommandLine() 5 | guard !options.help, !options.unknownOption, let manglerType = options.manglerType, let appDirectoryOrFile = options.appDirectoryOrFile else { 6 | print(Options.usage) 7 | if options.help { 8 | return 9 | } else { 10 | exit(EXIT_FAILURE) 11 | } 12 | } 13 | 14 | LOGGER = SoutLogger(options: options) 15 | let mangler = manglerType.resolveMangler(machOViewDoomEnabled: options.machOViewDoom) 16 | let obfuscator = Obfuscator(directoryOrFileURL: appDirectoryOrFile, 17 | mangler: mangler, 18 | options: options) 19 | obfuscator.run() 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Run/main.swift: -------------------------------------------------------------------------------- 1 | import App 2 | 3 | App.run() 4 | -------------------------------------------------------------------------------- /Tests/AppTests/Commons/String+asURL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var asURL: URL { 5 | return URL(fileURLWithPath: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/AppTests/Commons/URL+tempFile.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | static var tempFile: URL { 5 | return URL(fileURLWithPath: NSTemporaryDirectory()) 6 | .appendingPathComponent(UUID().uuidString) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/Concurrent_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | class Concurrent_Tests: XCTestCase { 6 | func test_shouldMapEmptyArray() { 7 | let testArray: [String] = [] 8 | 9 | XCTAssertEqual(testArray.concurrentMap { $0 + " mapped" }, []) 10 | } 11 | 12 | func test_shouldMapAllElementsInCorrectOrder() { 13 | let testArray = [1, 2, 3, 4, 5] 14 | 15 | XCTAssertEqual(testArray.concurrentMap { $0 + 1 }, [2, 3, 4, 5, 6]) 16 | } 17 | 18 | func test_shouldMapAllElementsInCorrectOrder_WhenProcessingTakesLong() { 19 | let testArray = [1, 2, 3, 4, 5] 20 | 21 | XCTAssertEqual(testArray.concurrentMap { val in 22 | // sleep for 0 to 0.4 seconds 23 | usleep(UInt32((5 - val) * 100_000)) 24 | return val + 1 25 | }, [2, 3, 4, 5, 6]) 26 | } 27 | 28 | func test_shouldCompactMapEmptyArray() { 29 | let testArray: [String] = [] 30 | 31 | XCTAssertEqual(testArray.concurrentCompactMap { $0 + " mapped" }, []) 32 | } 33 | 34 | func test_shouldCompactMapEmptyArray_WhenNilIsReturned() { 35 | let testArray = [1, 2, 3, 4, 5] 36 | 37 | XCTAssertEqual(testArray.concurrentCompactMap { val in 38 | val > 2 ? nil : val + 1 39 | }, [2, 3]) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/CpuId_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class CpuId_Tests: XCTestCase { 5 | func test_shouldConcatenateCpuTypeAndSubtype() { 6 | // Given 7 | let cpu = Mach.Cpu(type: 0x1234_5678, subtype: 0x789A_BCDE) 8 | 9 | // Expect 10 | XCTAssertEqual(cpu.asCpuId, 11 | 0x1234_5678_789A_BCDE) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/Range+Sugar_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Range_Sugar_Tests: XCTestCase { 5 | func test_initWithOffsetAndCount_shouldReturnCalculatedRange() { 6 | // When 7 | let range = Range(offset: 13, count: 29) 8 | 9 | // Then 10 | XCTAssertEqual(range, 13 ..< 42) 11 | } 12 | 13 | func test_intRange_shouldConvertNonIntRange() { 14 | // Given 15 | let int8Range: Range = (3 as Int8) ..< (9 as Int8) 16 | // When 17 | let intRange: Range = int8Range.intRange 18 | 19 | // Then 20 | XCTAssertEqual(intRange, 3 ..< 9) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/Sequence+uniq_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Sequence_uniq_Tests: XCTestCase { 5 | let array: [UInt8] = [1, 2, 8, 2, 5, 0, 0, 2] 6 | 7 | func test_shouldMakeArrayElementsUnique() { 8 | // Expect 9 | XCTAssertEqual(array.uniq, [0, 1, 2, 5, 8]) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/String+capitalizedOnFirstLetter_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class String_capitalizedOnFirstLetter_Tests: XCTestCase { 5 | func test_shouldCapitalizeFirstLetterInLongSentence() { 6 | XCTAssertEqual("feed my face".capitalizedOnFirstLetter, 7 | "Feed my face") 8 | } 9 | 10 | func test_shouldNotChangeAlreadyCapitalizedString() { 11 | let str = "Poland" 12 | XCTAssertEqual(str.capitalizedOnFirstLetter, str) 13 | } 14 | 15 | func test_shouldCapitalizeOneLetterString() { 16 | XCTAssertEqual("a".capitalizedOnFirstLetter, "A") 17 | } 18 | 19 | func test_shouldNotChangeEmptyString() { 20 | XCTAssertEqual("".capitalizedOnFirstLetter, "") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AppTests/CommonsTests/URL+containsURL_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class URL_containsURL_Tests: XCTestCase { 5 | func test_shouldContainSelf() { 6 | // Given 7 | let url = URL(fileURLWithPath: "/tmp/dir1/nested") 8 | 9 | // Expect 10 | XCTAssert(url.contains(url)) 11 | } 12 | 13 | func test_shouldContainChild() { 14 | // Given 15 | let url = URL(fileURLWithPath: "/tmp/dir1/nested") 16 | let child = URL(fileURLWithPath: "/tmp/dir1/nested/leaf") 17 | 18 | // Expect 19 | XCTAssert(url.contains(child)) 20 | } 21 | 22 | func test_shouldNotContainSibling() { 23 | // Given 24 | let url = URL(fileURLWithPath: "/tmp/dir1/nested") 25 | let sibling = URL(fileURLWithPath: "/tmp/dir1/sibling") 26 | 27 | // Expect 28 | XCTAssertFalse(url.contains(sibling)) 29 | } 30 | 31 | func test_shouldNotContainUnrelated() { 32 | // Given 33 | let url = URL(fileURLWithPath: "/tmp/dir1/nested") 34 | let unrelated = URL(fileURLWithPath: "/tmp/dir2") 35 | 36 | // Expect 37 | XCTAssertFalse(url.contains(unrelated)) 38 | } 39 | 40 | func test_shouldNotContainUnrelatedChild() { 41 | // Given 42 | let url = URL(fileURLWithPath: "/tmp/dir1/nested") 43 | let unrelated = URL(fileURLWithPath: "/tmp/dir2/nested/leaf") 44 | 45 | // Expect 46 | XCTAssertFalse(url.contains(unrelated)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/AppTests/ControllerTests/Obfuscator_Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class Obfuscator_Tests: XCTestCase { 4 | // TODO: tests missing 5 | } 6 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Magic_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Magic_Tests: XCTestCase { 5 | func test_magicFromData() { 6 | // Given 7 | let data = Data([0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04]) 8 | 9 | // Expect 10 | XCTAssertEqual(data.magic, 0xBEBA_FECA) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Mapping_replaceBytes_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Mapping_replaceBytes_Tests: XCTestCase { 5 | func test_shouldReplaceBytes() { 6 | // Given 7 | var data = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]) 8 | 9 | // When 10 | data.replaceBytes(inRange: 2 ..< 5, withBytes: [0x10, 0x11, 0x12]) 11 | 12 | // Then 13 | XCTAssertEqual(data, Data([0x01, 0x02, 0x10, 0x11, 0x12, 0x06])) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Mapping_replaceStrings_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Mapping_replaceStrings_Tests: XCTestCase { 5 | func test_shouldNotChangeData_whenNoMatchingMapping() { 6 | // Given 7 | let data = "string\0".data(using: .utf8)! 8 | var sut = data 9 | 10 | // When 11 | sut.replaceStrings(inRange: 0 ..< data.count, withMapping: ["str": "int"]) 12 | 13 | // Then 14 | XCTAssertEqual(sut, data) 15 | } 16 | 17 | func test_shouldReplaceString_whenMappingMatches() { 18 | // Given 19 | let data = "string\0array\0".data(using: .utf8)! 20 | var sut = data 21 | 22 | // When 23 | sut.replaceStrings(inRange: 0 ..< data.count, withMapping: ["string": "double"]) 24 | 25 | // Then 26 | XCTAssertEqual(sut, "double\0array\0".data(using: .utf8)) 27 | } 28 | 29 | func test_shouldReplaceMultipleStrings() { 30 | // Given 31 | let data = "a1\0a2\0\0\0a3\0\0\0\0a5\0a6\0".data(using: .utf8)! 32 | var sut = data 33 | 34 | let mapping = ["a1": "b1", 35 | "a3": "b3", 36 | "a6": "b6"] 37 | 38 | // When 39 | sut.replaceStrings(inRange: 0 ..< data.count, withMapping: mapping) 40 | 41 | // Then 42 | XCTAssertEqual(sut, "b1\0a2\0\0\0b3\0\0\0\0a5\0b6\0".data(using: .utf8)) 43 | } 44 | 45 | func test_shouldReplaceStringWithNonasciiCharacters_whenMappingMatches() { 46 | // Given 47 | let data = "zażółć:gęślą:jaźń\0array\0".data(using: .utf8)! 48 | var sut = data 49 | 50 | // When 51 | sut.replaceStrings(inRange: 0 ..< data.count, withMapping: ["zażółć:gęślą:jaźń": "zazzoollccXgeesslaaXjazznn"]) 52 | 53 | // Then 54 | XCTAssertEqual(sut, "zazzoollccXgeesslaaXjazznn\0array\0".data(using: .utf8)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Structs_getCString_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Structs_getCString_Tests: XCTestCase { 5 | let data = Data([0x64, 0x6F, 0x6E, 0x27, 0x74, 0x20, 0x6F, 0x62, 0x66, 0x75, 0x73, 0x63, 0x61, 0x74, 0x65, 0x20, 0x6D, 0x65, 0x00, 0x6D, 0x65, 0x6D, 0x6F, 0x72, 0x79, 0x20, 0x63, 0x6F, 0x72, 0x72, 0x75, 0x70, 0x74, 0x69, 0x6F, 0x6E]) 6 | 7 | func test_shouldReadCString() { 8 | // Expect 9 | XCTAssertEqual(data.getCString(atOffset: 6), 10 | "obfuscate me") 11 | } 12 | 13 | func test_shouldReadEmptyCString() { 14 | // Expect 15 | XCTAssertEqual(data.getCString(atOffset: 18), 16 | "") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Structs_getStruct_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Structs_getStruct_Tests: XCTestCase { 5 | private struct Sample { 6 | var b1: UInt16 7 | var b2: UInt16 8 | } 9 | 10 | func test_shouldReadSampleStruct() { 11 | // Given 12 | let data = Data([0x01, 0xAB, 0xCD, 0xEF, 0x02, 0x03]) 13 | 14 | // When 15 | let sample: Sample = data.getStruct(atOffset: 1) 16 | 17 | // Then 18 | XCTAssertEqual(sample.b1, 0xCDAB) 19 | XCTAssertEqual(sample.b2, 0x02EF) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+Structs_getStructs_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Data_Structs_getStructs_Tests: XCTestCase { 5 | private struct Sample { 6 | var b1: UInt16 7 | var b2: UInt16 8 | } 9 | 10 | func test_shouldReadSampleStructs() { 11 | // Given 12 | let data = Data([0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) 13 | 14 | // When 15 | let samples: [Sample] = data.getStructs(atOffset: 2, count: 3) 16 | 17 | // Then 18 | XCTAssertEqual(samples.count, 3) 19 | XCTAssertEqual(samples[0].b1, 0xF4F3) 20 | XCTAssertEqual(samples[0].b2, 0xF6F5) 21 | XCTAssertEqual(samples[1].b1, 0xF8F7) 22 | XCTAssertEqual(samples[1].b2, 0xFAF9) 23 | XCTAssertEqual(samples[2].b1, 0xFCFB) 24 | XCTAssertEqual(samples[2].b2, 0xFEFD) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/Data+nullify_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | class Data_nullify_Tests: XCTestCase { 6 | func test_shouldSetZerosInRange() { 7 | // Given 8 | var sut = Data(repeating: 0xAB, count: 20) 9 | 10 | // When 11 | sut.nullify(range: 8 ..< 15) 12 | 13 | // Then 14 | let expectedData = Data( 15 | [UInt8](repeating: 0xAB, count: 8) 16 | + [UInt8](repeating: 0x00, count: 7) 17 | + [UInt8](repeating: 0xAB, count: 5) 18 | ) 19 | XCTAssertEqual(sut, expectedData) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/UnsafeRawPointer+CString_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class UnsafeRawPointer_CString_Tests: XCTestCase { 5 | func template(bytes: [UInt8], expectedStringBytes: [UInt8], expectedPointerOffset: Int, file: StaticString = #file, line: UInt = #line) { 6 | let (number, offset) = bytes.withUnsafeBytes { bytes -> ([UInt8], Int) in 7 | var baseAddress = bytes.baseAddress! 8 | let stringBytes = baseAddress.readStringBytes() 9 | let offset = bytes.baseAddress!.distance(to: baseAddress) 10 | return (stringBytes, offset) 11 | } 12 | 13 | XCTAssertEqual(number, expectedStringBytes, "Unexpected string bytes", file: file, line: line) 14 | XCTAssertEqual(offset, expectedPointerOffset, "Unexpected offset", file: file, line: line) 15 | } 16 | 17 | func test_zeroBytes() { 18 | template(bytes: [0, 1, 2, 3], 19 | expectedStringBytes: [], 20 | expectedPointerOffset: 1) 21 | } 22 | 23 | func test_oneBytes() { 24 | template(bytes: [1, 0, 2, 3], 25 | expectedStringBytes: [1], 26 | expectedPointerOffset: 2) 27 | } 28 | 29 | func test_twoBytes() { 30 | template(bytes: [1, 2, 0, 3], 31 | expectedStringBytes: [1, 2], 32 | expectedPointerOffset: 3) 33 | } 34 | 35 | func test_threeBytes() { 36 | template(bytes: [1, 2, 3, 0, 4, 5, 0], 37 | expectedStringBytes: [1, 2, 3], 38 | expectedPointerOffset: 4) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/UnsafeRawPointer+Leb128_readNibUleb128_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class UnsafeRawPointer_Leb128_readNibUleb128_Tests: XCTestCase { 5 | func template(bytes: [UInt8], expectedNumber: UInt64, expectedPointerOffset: Int, file: StaticString = #file, line: UInt = #line) { 6 | let (number, offset) = bytes.withUnsafeBytes { bytes -> (UInt64, Int) in 7 | var cursor = bytes.baseAddress! 8 | let number = cursor.readNibUleb128() 9 | let offset = bytes.baseAddress!.distance(to: cursor) 10 | return (number, offset) 11 | } 12 | 13 | XCTAssertEqual(number, expectedNumber, "Unexpected number", file: file, line: line) 14 | XCTAssertEqual(offset, expectedPointerOffset, "Unexpected offset", file: file, line: line) 15 | } 16 | 17 | func test_oneByte() { 18 | template(bytes: [0b1011_0010, 0b0110_0110], 19 | expectedNumber: 0b0011_0010, 20 | expectedPointerOffset: 1) 21 | } 22 | 23 | func test_twoBytes() { 24 | template(bytes: [0b0000_0000, 0b1100_1110, 0b0100_0010], 25 | expectedNumber: 0b010_0111_0000_0000, 26 | expectedPointerOffset: 2) 27 | } 28 | 29 | func test_threeBytes() { 30 | template(bytes: [0b0110_0101, 0b0000_1110, 0b1010_0110, 0b0001_0010], 31 | expectedNumber: 0b00_1001_1000_0111_0110_0101, 32 | expectedPointerOffset: 3) 33 | } 34 | 35 | func test_nineBytes() { 36 | template(bytes: [0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0xFF], 37 | expectedNumber: 0x7FFF_FFFF_FFFF_FFFF, 38 | expectedPointerOffset: 9) 39 | } 40 | 41 | func test_tenBytes() { 42 | template(bytes: [0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x81], 43 | expectedNumber: 0xFFFF_FFFF_FFFF_FFFF, 44 | expectedPointerOffset: 10) 45 | template(bytes: [0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x80], 46 | expectedNumber: 0x7FFF_FFFF_FFFF_FFFF, 47 | expectedPointerOffset: 10) 48 | } 49 | 50 | func test_tenBytes_shouldIgnoreOverflow() { 51 | template(bytes: [0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0xFF], 52 | expectedNumber: 0xFFFF_FFFF_FFFF_FFFF, 53 | expectedPointerOffset: 10) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/UnsafeRawPointer+Leb128_readUleb128_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class UnsafeRawPointer_Leb128_readUleb128_Tests: XCTestCase { 5 | func template(bytes: [UInt8], expectedNumber: UInt64, expectedPointerOffset: Int, file: StaticString = #file, line: UInt = #line) { 6 | let (number, offset) = bytes.withUnsafeBytes { bytes -> (UInt64, Int) in 7 | var cursor = bytes.baseAddress! 8 | let number = cursor.readUleb128() 9 | let offset = bytes.baseAddress!.distance(to: cursor) 10 | return (number, offset) 11 | } 12 | 13 | XCTAssertEqual(number, expectedNumber, "Unexpected number", file: file, line: line) 14 | XCTAssertEqual(offset, expectedPointerOffset, "Unexpected offset", file: file, line: line) 15 | } 16 | 17 | func test_oneByte() { 18 | template(bytes: [0b0011_0010, 0b0110_0110], 19 | expectedNumber: 0b0011_0010, 20 | expectedPointerOffset: 1) 21 | } 22 | 23 | func test_twoBytes() { 24 | template(bytes: [0b1000_0000, 0b0100_1110, 0b0100_0010], 25 | expectedNumber: 0b010_0111_0000_0000, 26 | expectedPointerOffset: 2) 27 | } 28 | 29 | func test_threeBytes() { 30 | template(bytes: [0b1110_0101, 0b1000_1110, 0b0010_0110, 0b0001_0010], 31 | expectedNumber: 0b00_1001_1000_0111_0110_0101, 32 | expectedPointerOffset: 3) 33 | } 34 | 35 | func test_nineBytes() { 36 | template(bytes: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F], 37 | expectedNumber: 0x7FFF_FFFF_FFFF_FFFF, 38 | expectedPointerOffset: 9) 39 | } 40 | 41 | func test_tenBytes() { 42 | template(bytes: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01], 43 | expectedNumber: 0xFFFF_FFFF_FFFF_FFFF, 44 | expectedPointerOffset: 10) 45 | template(bytes: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00], 46 | expectedNumber: 0x7FFF_FFFF_FFFF_FFFF, 47 | expectedPointerOffset: 10) 48 | } 49 | 50 | func test_tenBytes_shouldIgnoreOverflow() { 51 | template(bytes: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F], 52 | expectedNumber: 0xFFFF_FFFF_FFFF_FFFF, 53 | expectedPointerOffset: 10) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/UnsafeRawPointer+Structs_readStruct_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class UnsafeRawPointer_Structs_readStruct_Tests: XCTestCase { 5 | private struct Sample { 6 | var b1: UInt16 7 | var b2: UInt16 8 | } 9 | 10 | let bytes: [UInt8] = [0xAB, 0xCD, 0xEF, 0x02, 0x03] 11 | 12 | func test_shouldReadSampleStruct() { 13 | // Given 14 | bytes.withUnsafeBytes { bytes in 15 | var cursor = bytes.baseAddress! 16 | 17 | // When 18 | let sample: Sample = cursor.readStruct() 19 | 20 | // Then 21 | XCTAssertEqual(sample.b1, 0xCDAB) 22 | XCTAssertEqual(sample.b2, 0x02EF) 23 | XCTAssertEqual(bytes.baseAddress!.distance(to: cursor), 4) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AppTests/DataAccessTests/UnsafeRawPointer+Structs_readStructs_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class UnsafeRawPointer_Structs_readStructs_Tests: XCTestCase { 5 | private struct Sample { 6 | var b1: UInt16 7 | var b2: UInt16 8 | } 9 | 10 | let bytes: [UInt8] = [0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF] 11 | 12 | func test_shouldReadSampleStructs() { 13 | // Given 14 | bytes.withUnsafeBytes { bytes in 15 | var cursor = bytes.baseAddress! 16 | 17 | // When 18 | let samples: [Sample] = cursor.readStructs(count: 3) 19 | 20 | // Then 21 | XCTAssertEqual(samples.count, 3) 22 | XCTAssertEqual(samples[0].b1, 0xF2F1) 23 | XCTAssertEqual(samples[0].b2, 0xF4F3) 24 | XCTAssertEqual(samples[1].b1, 0xF6F5) 25 | XCTAssertEqual(samples[1].b2, 0xF8F7) 26 | XCTAssertEqual(samples[2].b1, 0xFAF9) 27 | XCTAssertEqual(samples[2].b2, 0xFCFB) 28 | XCTAssertEqual(bytes.baseAddress!.distance(to: cursor), 12) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/AppTests/DependencyAnalysisTests/ObfuscationPathsTestRepository.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | class ObfuscationPathsTestRepository { 6 | private enum Entry { 7 | case MachO(platform: Mach.Platform, isExecutable: Bool, dylibs: [String], rpaths: [String], cstrings: [String]) 8 | case File 9 | } 10 | 11 | private enum Error: Swift.Error { 12 | case noEntryForPath 13 | } 14 | 15 | private var entryPerPath: [String: Entry] = [:] 16 | var expectedRoot: URL? 17 | 18 | func addFilePath(_ path: String) { 19 | entryPerPath[path] = .File 20 | } 21 | 22 | func addMachOPath(_ path: String, 23 | platform: Mach.Platform = .macos, 24 | isExecutable: Bool, 25 | dylibs: [String] = [], 26 | rpaths: [String] = [], 27 | cstrings: [String] = []) { 28 | entryPerPath[path] = .MachO(platform: platform, 29 | isExecutable: isExecutable, 30 | dylibs: dylibs, 31 | rpaths: rpaths, 32 | cstrings: cstrings) 33 | } 34 | } 35 | 36 | extension ObfuscationPathsTestRepository: FileRepository { 37 | func listFilesRecursively(atURL url: URL) -> [URL] { 38 | XCTAssertEqual(url, expectedRoot) 39 | return entryPerPath.keys.map(URL.init(fileURLWithPath:)) 40 | } 41 | 42 | func fileExists(atURL url: URL) -> Bool { 43 | return entryPerPath.keys.contains( 44 | url.resolvingSymlinksInPath().path 45 | ) 46 | } 47 | } 48 | 49 | extension ObfuscationPathsTestRepository: DependencyNodeLoader { 50 | func load(forURL url: URL) throws -> [DependencyNode] { 51 | switch entryPerPath[url.resolvingSymlinksInPath().path] { 52 | case let .MachO(platform: platform, 53 | isExecutable: isExecutable, 54 | dylibs: dylibs, 55 | rpaths: rpaths, 56 | cstrings: cstrings)?: 57 | let mach = DependencyNodeMock(isExecutable: isExecutable, 58 | platform: platform, 59 | rpaths: rpaths, 60 | dylibs: dylibs, 61 | cstrings: cstrings) 62 | return [mach] 63 | default: 64 | throw Error.noEntryForPath 65 | } 66 | } 67 | } 68 | 69 | private struct DependencyNodeMock: DependencyNode { 70 | var isExecutable: Bool 71 | var platform: Mach.Platform 72 | var rpaths: [String] 73 | var dylibs: [String] 74 | var cstrings: [String] 75 | } 76 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/RecursiveSourceSymbolsLoaderMock.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | 4 | class RecursiveSourceSymbolsLoaderMock { 5 | private var symbolsPerUrl: [String: ObjectSymbols] = [:] 6 | 7 | subscript(path: String) -> ObjectSymbols? { 8 | get { 9 | return symbolsPerUrl[path] 10 | } 11 | set { 12 | symbolsPerUrl[path] = newValue 13 | } 14 | } 15 | } 16 | 17 | extension RecursiveSourceSymbolsLoaderMock: RecursiveSourceSymbolsLoaderProtocol { 18 | func load(fromDirectory url: URL) -> ObjectSymbols { 19 | let path = url.resolvingSymlinksInPath().path 20 | if let symbols = symbolsPerUrl[path] { 21 | return symbols 22 | } else { 23 | return ObjectSymbols(selectors: [], classNames: []) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/RecursiveSourceSymbolsLoader_loadFromFrameworkURL_allSystemFrameworks_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RecursiveSourceSymbolsLoader_loadFromFrameworkURL_allSystemFrameworks_Tests: XCTestCase { 5 | // Disabled because it is very slow. 6 | func DISABLED_test_shouldParseSelectors() { 7 | // Given 8 | let sut = RecursiveSourceSymbolsLoader() 9 | 10 | // When 11 | let header = sut.load(fromDirectory: Paths.iosFrameworksRoot.asURL) 12 | 13 | // Assert 14 | XCTAssertFalse(header.selectors.isEmpty) 15 | XCTAssertFalse(header.classNames.isEmpty) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/RecursiveSourceSymbolsLoader_loadFromFrameworkURL_craftedFramework_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RecursiveSourceSymbolsLoader_loadFromFrameworkURL_craftedFramework_Tests: XCTestCase { 5 | var symbols: ObjectSymbols! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | let sut = RecursiveSourceSymbolsLoader() 11 | symbols = sut.load(fromDirectory: URL.craftedFramework) 12 | } 13 | 14 | override func tearDown() { 15 | symbols = nil 16 | 17 | super.tearDown() 18 | } 19 | 20 | func test_shouldParseSelectors() { 21 | let expectedMethods: Set = [ 22 | "instanceMethod", 23 | "classMethod", 24 | "methodWithLeadingInset", 25 | "methodWithLotOfSpaces", 26 | "methodWithoutSpaces", 27 | "methodThatReturnsBlock", 28 | "methodThatReturnsBlock:andTakesArguments:", 29 | "methodThatReturnsTypedefedBlock", 30 | "methodThatTakesInt:andString:andVoid:", 31 | "methodWithMacros", 32 | "methodWithDeprecationMsg:", 33 | ] 34 | 35 | let expectedPropertyNames: Set = [ 36 | "intProperty", 37 | "propertyWithAttributes", 38 | "propertyWithLotOfSpaces", 39 | "propertyWithoutSpaces", 40 | "pointerProperty", 41 | "blockProperty", 42 | "typedeffedBlockProperty", 43 | "propertyWithGenerics", 44 | "propertyWithMacros", 45 | "propertyWithDeprecationMsg", 46 | "property1", 47 | "property2", 48 | "property3", 49 | ] 50 | 51 | let expectedSelectors = 52 | expectedMethods.union(expectedPropertyNames) 53 | 54 | expectedSelectors.forEach { 55 | XCTAssert(symbols.selectors.contains($0), "Should contain: \($0)") 56 | } 57 | let unexpectedSelectors = symbols.selectors.subtracting(expectedSelectors) 58 | XCTAssertEqual(unexpectedSelectors, [], "Detected unexpected selectors") 59 | } 60 | 61 | func test_shouldParceClassNames() { 62 | let expectedClassNames: Set = [ 63 | "InterfaceWithNSObject", 64 | "RootInterface", 65 | "ProtocolWithoutConformance", 66 | "ProtocolWithConformance", 67 | "SampleClass_ForwardDeclaration", 68 | "SampleProtocol_ForwardDeclaration", 69 | ] 70 | XCTAssertEqual(symbols.classNames.symmetricDifference(expectedClassNames), []) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/RecursiveSourceSymbolsLoader_loadFromFrameworkURL_systemLikeFramework_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RecursiveSourceSymbolsLoader_loadFromFrameworkURL_systemLikeFramework_Tests: XCTestCase { 5 | var symbols: ObjectSymbols! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | let sut = RecursiveSourceSymbolsLoader() 11 | symbols = sut.load(fromDirectory: .systemLikeFramework) 12 | } 13 | 14 | override func tearDown() { 15 | symbols = nil 16 | 17 | super.tearDown() 18 | } 19 | 20 | func test_shouldParseAllClasses() { 21 | let expectedClassNames: Set = [ 22 | "UIApplication", 23 | "UIUserNotificationSettings", 24 | "UIApplicationDelegate", 25 | "NSDictionary", 26 | "NSOrderedSet", 27 | ] 28 | XCTAssertEqual(symbols.classNames, expectedClassNames) 29 | } 30 | 31 | func test_shouldParseAllSelectors() { 32 | let expectedSelectors: Set = [ 33 | "sharedApplication", 34 | "delegate", 35 | "openURL:", 36 | "canOpenURL:", 37 | "registerForRemoteNotificationTypes:", 38 | "applicationDidFinishLaunching:", 39 | "application:didFinishLaunchingWithOptions:", 40 | "application:handleActionWithIdentifier:forLocalNotification:completionHandler:", 41 | // "NSDictionary.h" 42 | "countByEnumeratingWithState:objects:count:", 43 | // "NSOrderedSet.h" 44 | "count", 45 | "objectAtIndex:", 46 | "indexOfObject:", 47 | "init", 48 | "initWithObjects:count:", 49 | "initWithCoder:", 50 | ] 51 | XCTAssertEqual(symbols.selectors, expectedSelectors) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/RecursiveSourceSymbolsLoader_loadFromSourcesURL_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RecursiveSourceSymbolsLoader_loadFromSourcesURL_Tests: XCTestCase { 5 | var symbols: ObjectSymbols! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | let sut = RecursiveSourceSymbolsLoader() 11 | symbols = sut.load(fromDirectory: URL.librarySourceCode) 12 | } 13 | 14 | override func tearDown() { 15 | symbols = nil 16 | 17 | super.tearDown() 18 | } 19 | 20 | func test_shouldParseSelectors() { 21 | let expectedMethods: Set = [ 22 | "publicMethod", 23 | "privateMethod", 24 | ] 25 | 26 | let expectedPropertyNames: Set = [ 27 | "publicProperty", 28 | "privateProperty", 29 | ] 30 | 31 | let expectedSelectors = 32 | expectedMethods.union(expectedPropertyNames) 33 | 34 | expectedSelectors.forEach { 35 | XCTAssert(symbols.selectors.contains($0), "Should contain: \($0)") 36 | } 37 | let unexpectedSelectors = symbols.selectors.subtracting(expectedSelectors) 38 | XCTAssertEqual(unexpectedSelectors, [], "Detected unexpected selectors") 39 | } 40 | 41 | func test_shouldParceClassNames() { 42 | let expectedClassNames: Set = [ 43 | "PublicClass", 44 | "PrivateClass", 45 | ] 46 | XCTAssertEqual(symbols.classNames.symmetricDifference(expectedClassNames), []) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/CraftedFramework.framework/Headers/TestMethodNames.h: -------------------------------------------------------------------------------- 1 | - (void)instanceMethod; 2 | + (void)classMethod; 3 | - (void)methodWithLeadingInset; 4 | - (void) methodWithLotOfSpaces ; 5 | -(void)methodWithoutSpaces; 6 | -(NSString *(^)(NSString *))methodThatReturnsBlock; 7 | -(NSString *(^)(NSString *))methodThatReturnsBlock:(BOOL)b andTakesArguments:(BOOL)b; 8 | -(TypedeffedBlock)methodThatReturnsTypedefedBlock; 9 | 10 | - (int)methodThatTakesInt:(int)integer andString:(NSString *)string andVoid:(void *)v; 11 | 12 | // - (int)methodFromComments; 13 | 14 | - (int)methodWithMacros NS_AVAILABLE_IOS(10_0); 15 | - (int)methodWithDeprecationMsg: __deprecated_msg("Use shouldNotBeParsed: instead"); 16 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/CraftedFramework.framework/Headers/TestPropertyNames.h: -------------------------------------------------------------------------------- 1 | @property int intProperty; 2 | @property (nonatomic, class) int propertyWithAttributes; 3 | @property ( nonatomic , class ) int propertyWithLotOfSpaces; 4 | @property(nonatomic,class)int propertyWithoutSpaces; 5 | @property NSString *pointerProperty; 6 | @property (nonatomic) NSString *(^blockProperty)(NSString *); 7 | @property (nonatomic) TypedeffedBlock typedeffedBlockProperty; 8 | @property (nonatomic) NSArray *> *propertyWithGenerics; 9 | 10 | // @property int propertyFromComments; 11 | 12 | @property NSString *propertyWithMacros NS_AVAILABLE_IOS(10_0); 13 | @property NSString *propertyWithDeprecationMsg NS_DEPRECATED_IOS(4_2,10_0, "Use @property NSString* shouldNotBeParsed instead."); 14 | 15 | @property (nonatomic) NSString *property1, *property2, *property3; 16 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/CraftedFramework.framework/Headers/TestTypeNames.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface InterfaceWithNSObject: NSObject 4 | 5 | @end 6 | 7 | @interface RootInterface 8 | 9 | @end 10 | 11 | @protocol ProtocolWithoutConformance 12 | 13 | @end 14 | 15 | @protocol ProtocolWithConformance 16 | 17 | @end 18 | 19 | @class SampleClass_ForwardDeclaration; 20 | @protocol SampleProtocol_ForwardDeclaration; 21 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/LibrarySourceCode.bundle/File1.h: -------------------------------------------------------------------------------- 1 | @interface PublicClass : NSObject 2 | 3 | @property (nonatomic, readonly) NSString *publicProperty; 4 | 5 | - (void)publicMethod; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/LibrarySourceCode.bundle/File1.m: -------------------------------------------------------------------------------- 1 | #import "File1.h" 2 | 3 | @interface PrivateClass : NSObject 4 | 5 | @property (nonatomic, readonly) NSString *privateProperty; 6 | 7 | - (void)privateMethod; 8 | 9 | @end 10 | 11 | @implementation PublicClass 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/SystemLikeFramework.framework/Headers/NSDictionary.h: -------------------------------------------------------------------------------- 1 | NS_ASSUME_NONNULL_BEGIN 2 | 3 | @interface NSDictionary (NSGenericFastEnumeraiton) 4 | - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(K __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len; 5 | @end 6 | 7 | NS_ASSUME_NONNULL_END 8 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/SystemLikeFramework.framework/Headers/NSOrderedSet.h: -------------------------------------------------------------------------------- 1 | NS_ASSUME_NONNULL_BEGIN 2 | 3 | API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)) 4 | @interface NSOrderedSet<__covariant ObjectType> : NSObject 5 | 6 | @property (readonly) NSUInteger count; 7 | - (ObjectType)objectAtIndex:(NSUInteger)idx; 8 | - (NSUInteger)indexOfObject:(ObjectType)object; 9 | - (instancetype)init NS_DESIGNATED_INITIALIZER; 10 | - (instancetype)initWithObjects:(const ObjectType _Nonnull [_Nullable])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER; 11 | - (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; 12 | 13 | @end 14 | 15 | NS_ASSUME_NONNULL_END 16 | 17 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/SystemLikeFramework.framework/Headers/UIApplication.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | typedef NS_ENUM(NSInteger, UIStatusBarStyle) { 6 | UIStatusBarStyleDefault = 0, // Dark content, for use on light backgrounds 7 | UIStatusBarStyleLightContent NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds 8 | 9 | UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1, 10 | UIStatusBarStyleBlackOpaque NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2, 11 | } __TVOS_PROHIBITED; 12 | 13 | NS_CLASS_AVAILABLE_IOS(2_0) @interface UIApplication : UIResponder 14 | 15 | #if UIKIT_DEFINE_AS_PROPERTIES 16 | @property(class, nonatomic, readonly) UIApplication *sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead."); 17 | #else 18 | + (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead."); 19 | #endif 20 | 21 | @property(nullable, nonatomic, assign) id delegate; 22 | 23 | - (BOOL)openURL:(NSURL*)url NS_DEPRECATED_IOS(2_0, 10_0, "Please use openURL:options:completionHandler: instead") NS_EXTENSION_UNAVAILABLE_IOS(""); 24 | - (BOOL)canOpenURL:(NSURL *)url NS_AVAILABLE_IOS(3_0); 25 | 26 | @end 27 | 28 | @interface UIApplication (UIRemoteNotifications) 29 | 30 | - (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types NS_DEPRECATED_IOS(3_0, 8_0, "Use -[UIApplication registerForRemoteNotifications] and UserNotifications Framework's -[UNUserNotificationCenter requestAuthorizationWithOptions:completionHandler:]") __TVOS_PROHIBITED; 31 | 32 | @end 33 | 34 | @class UIUserNotificationSettings; 35 | 36 | @protocol UIApplicationDelegate 37 | 38 | @optional 39 | 40 | - (void)applicationDidFinishLaunching:(UIApplication *)application; 41 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions NS_AVAILABLE_IOS(3_0); 42 | 43 | - (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void(^)())completionHandler NS_DEPRECATED_IOS(8_0, 10_0, "Use UserNotifications Framework's -[UNUserNotificationCenterDelegate didReceiveNotificationResponse:withCompletionHandler:]") __TVOS_PROHIBITED; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Tests/AppTests/HeaderParsingTests/Samples/URL+SampleFrameworks.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | static var craftedFramework: URL { 5 | return Bundle.module.url(forResource: "CraftedFramework", withExtension: "framework")! 6 | } 7 | 8 | static var systemLikeFramework: URL { 9 | return Bundle.module.url(forResource: "SystemLikeFramework", withExtension: "framework")! 10 | } 11 | 12 | static var librarySourceCode: URL { 13 | return Bundle.module.url(forResource: "LibrarySourceCode", withExtension: "bundle")! 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Commons/Mach+Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | 4 | extension Mach { 5 | mutating func setEmptyRegionFor(section sectionName: String, segment segmentName: String) { 6 | let segmentIndex = segments.firstIndex(where: { $0.name == segmentName })! 7 | let sectionIndex = segments[segmentIndex].sections.firstIndex(where: { $0.name == sectionName })! 8 | segments[segmentIndex].sections[sectionIndex].range = 0 ..< 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/ExportTrieTests/Trie+Parsing_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Trie_Parsing_Tests: XCTestCase { 5 | let samplePayload = Data([ 6 | // Garbage 7 | 0xAB, 0xAB, 0xAB, 0xAB, 8 | 9 | // Root node exported symbol information length 10 | 0x00, 11 | // Root node child count 12 | 0x02, 13 | // #0 edge label 14 | 0x41, 0x42, 0x00, 15 | // #0 node offset 16 | 0x0A, 17 | // #1 edge label 18 | 0x41, 0x43, 0x00, 19 | // #1 node offset 20 | 0x18, 21 | 22 | // #0 exported symbol information length 23 | 0x03, 24 | // #0 exported symbol information 25 | 0x00, 0x90, 0x4E, 26 | // #0 child count 27 | 0x01, 28 | // #2 edge label 29 | 0x43, 0x44, 0x00, 30 | // #2 node offset 31 | 0x13, 32 | 33 | // #2 exported symbol information length 34 | 0x03, 35 | // #2 exported symbol information 36 | 0x00, 0x90, 0x4E, 37 | // #2 child count 38 | 0x00, 39 | 40 | // #1 exported symbol information length 41 | 0x00, 42 | // #1 child count 43 | 0x02, 44 | // #3 edge label 45 | 0x44, 0x45, 0x00, 46 | // #3 node offset 47 | 0x22, 48 | // #4 edge label 49 | 0x46, 0x47, 0x00, 50 | // #4 node offset 51 | 0x27, 52 | 53 | // #3 exported symbol information length 54 | 0x03, 55 | // #3 exported symbol information 56 | 0x00, 0x90, 0x4E, 57 | // #3 child count 58 | 0x00, 59 | 60 | // #4 exported symbol information length 61 | 0x03, 62 | // #4 exported symbol information 63 | 0x00, 0x90, 0x4E, 64 | // #4 child count 65 | 0x00, 66 | ]) 67 | 68 | func test_exportedLabelsString_shouldListFullExportedSymbols() { 69 | // Given 70 | let sut = Trie(data: samplePayload, rootNodeOffset: 4) 71 | 72 | // Expect 73 | XCTAssertEqual(sut.exportedLabelStrings, 74 | ["AB", "ABCD", "ACDE", "ACFG"]) 75 | } 76 | 77 | func test_flatNodes_shouldReturnFlatNodesList() { 78 | // Given 79 | let sut = Trie(data: samplePayload, rootNodeOffset: 4) 80 | 81 | // When 82 | let nodes = sut.flatNodes 83 | 84 | // Then 85 | XCTAssertEqual(nodes.count, 6) 86 | XCTAssertEqual(nodes.map { $0.labelString }, 87 | ["", "AB", "AC", "DE", "FG", "CD"]) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Image+updateMachs_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Image_updateMachs_Tests: XCTestCase { 5 | func test_shouldUpdateFlatMach() { 6 | // Given 7 | var sut = try! Image.load(url: URL.machoMacExecutable) 8 | 9 | // Then 10 | sut.updateMachs { mach in 11 | mach.data[0] = 0x42 12 | } 13 | 14 | // Then 15 | guard case let .mach(mach) = sut.contents else { 16 | XCTFail("Unexpected contents") 17 | return 18 | } 19 | XCTAssertEqual(mach.data[0], 0x42) 20 | } 21 | 22 | func test_shouldUpdateFatImage() { 23 | // Given 24 | var sut = try! Image.load(url: URL.fatIosExecutable) 25 | 26 | // When 27 | sut.updateMachs { mach in 28 | mach.data[0] = 0x42 29 | } 30 | 31 | // Then 32 | guard case let .fat(fat) = sut.contents else { 33 | XCTFail("Unexpected contents") 34 | return 35 | } 36 | fat.architectures.forEach { arch in 37 | XCTAssertEqual(arch.mach.data[0], 0x42) 38 | XCTAssertEqual(fat.data[Int(arch.offset)], 0x42) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/ImportStackTests/ImportStack_resolveMissingDylibOrdinals_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class ImportStack_resolveMissingDylibOrdinals_Tests: XCTestCase { 5 | var sut: ImportStack! = ImportStack() 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | // Given 11 | sut.append(ImportStackEntry(dylibOrdinal: 1, symbol: [0x41], symbolRange: 0 ..< 1, weak: false)) 12 | sut.append(ImportStackEntry(dylibOrdinal: 2, symbol: [0x42], symbolRange: 0 ..< 1, weak: false)) 13 | sut.append(ImportStackEntry(dylibOrdinal: 3, symbol: [0x43], symbolRange: 0 ..< 1, weak: false)) 14 | sut.append(ImportStackEntry(dylibOrdinal: 4, symbol: [0x43], symbolRange: 0 ..< 1, weak: false)) 15 | sut.append(ImportStackEntry(dylibOrdinal: 0, symbol: [0x42], symbolRange: 0 ..< 1, weak: false)) 16 | sut.append(ImportStackEntry(dylibOrdinal: 0, symbol: [0x43], symbolRange: 0 ..< 1, weak: false)) 17 | sut.append(ImportStackEntry(dylibOrdinal: 0, symbol: [0x43], symbolRange: 0 ..< 1, weak: true)) 18 | sut.append(ImportStackEntry(dylibOrdinal: 0, symbol: [0x44], symbolRange: 0 ..< 1, weak: true)) 19 | 20 | // When 21 | sut.resolveMissingDylibOrdinals() 22 | } 23 | 24 | func test_shouldUpdateZeroDylibOrdinalsWithAlreadyResolvedOrdinals() { 25 | // Then 26 | XCTAssertEqual(sut[4].dylibOrdinal, 2) 27 | XCTAssertEqual(sut[5].dylibOrdinal, 3) 28 | XCTAssertEqual(sut[6].dylibOrdinal, 3) 29 | } 30 | 31 | func test_shouldNotChangeAlreadyResolvedOrdinals() { 32 | // Then 33 | XCTAssertEqual(sut[0].dylibOrdinal, 1) 34 | XCTAssertEqual(sut[1].dylibOrdinal, 2) 35 | XCTAssertEqual(sut[2].dylibOrdinal, 3) 36 | XCTAssertEqual(sut[3].dylibOrdinal, 4) 37 | } 38 | 39 | func test_shouldNotFailWhenUnresolvableWeakEntry() { 40 | // Then 41 | XCTAssertEqual(sut[7].dylibOrdinal, 0) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Mach+Loading_MachoIos12_0_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Mach_Loading_MachoIos12_0_Tests: XCTestCase { 5 | let sut = try! Image.load(url: URL.machoIos12_0Executable) 6 | 7 | func test_shouldDetectCorrectPlatform() { 8 | XCTAssertEqual(sut.url, URL.machoIos12_0Executable) 9 | guard case let .mach(mach) = sut.contents else { 10 | XCTFail("Unexpected contents") 11 | return 12 | } 13 | XCTAssertEqual(mach.data, try! Data(contentsOf: URL.machoIos12_0Executable)) 14 | XCTAssertEqual(mach.type, .executable) 15 | XCTAssertEqual(mach.platform, .ios) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Mach+Loading_MachoMac10_14_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Mach_Loading_MachoMac10_14_Tests: XCTestCase { 5 | let sut = try! Image.load(url: URL.machoMac10_14Executable) 6 | 7 | func test_shouldDetectCorrectPlatform() { 8 | XCTAssertEqual(sut.url, URL.machoMac10_14Executable) 9 | guard case let .mach(mach) = sut.contents else { 10 | XCTFail("Unexpected contents") 11 | return 12 | } 13 | XCTAssertEqual(mach.data, try! Data(contentsOf: URL.machoMac10_14Executable)) 14 | XCTAssertEqual(mach.type, .executable) 15 | XCTAssertEqual(mach.platform, .macos) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Mach+Replacing_classname_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | private extension Mach { 6 | var classNamesInSection: [String] { 7 | guard let classnameSection = objcClassNameSection 8 | else { return [] } 9 | let classnameData = data.subdata(in: classnameSection.range.intRange) 10 | return classnameData.split(separator: 0).compactMap { String(bytes: $0, encoding: .utf8) } 11 | } 12 | } 13 | 14 | class Mach_Replacing_classname_Tests: XCTestCase { 15 | func test_shouldReplaceClassnamesInMach_WhenMatchingMangling() { 16 | // Given 17 | var sut = try! Image.load(url: URL.machoMacExecutable) 18 | 19 | // Prepare test obfuscation configuration satisfying requirements of replaceSymbols 20 | // 21 | // "Cat" is a category name, but it is not referenced in compiled metadata, 22 | // so use it as example of __objc_classname section entry that should not be changed. 23 | let map = SymbolManglingMap(selectors: [:], classNames: ["SampleClass": "ObfsctClazz", "Cat": "Bad", "NotExistingClass": "ShouldNotUse"], exportTrieObfuscationMap: [ 24 | URL.machoMacExecutable: [ 25 | sut.machs[0].cpu.asCpuId: 26 | (unobfuscated: Trie.empty, 27 | obfuscated: Trie.empty), 28 | ], 29 | ]) 30 | var paths = ObfuscationPaths() 31 | paths.resolvedDylibMapPerImageURL = [URL.machoMacExecutable: [:]] 32 | 33 | // When 34 | sut.replaceSymbols(withMap: map, paths: paths) 35 | 36 | // Then 37 | XCTAssertFalse(sut.machs[0].classNamesInSection.contains("SampleClass"), "Should obfuscate class name") 38 | XCTAssertTrue(sut.machs[0].classNamesInSection.contains("ObfsctClazz"), "Should obfuscate class name") 39 | XCTAssertTrue(sut.machs[0].classNamesInSection.contains("Cat"), "Should not change not referenced name") 40 | XCTAssertFalse(sut.machs[0].classNamesInSection.contains("Bad"), "Should not change not referenced name") 41 | XCTAssertFalse(sut.machs[0].classNamesInSection.contains("ShouldNotUse"), "Should not use not existing name") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Mach+Replacing_methtype_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | private extension Mach { 5 | var methTypes: [String] { 6 | guard let methtypeSection = objcMethTypeSection 7 | else { return [] } 8 | let methtypeData = data.subdata(in: methtypeSection.range.intRange) 9 | return methtypeData.split(separator: 0).compactMap { String(bytes: $0, encoding: .utf8) } 10 | } 11 | } 12 | 13 | class Mach_Replacing_methtype_Tests: XCTestCase { 14 | let testMap = SymbolManglingMap(selectors: [:], classNames: ["MyClass": "ObfClss", "MyClassLongerName": "OtherObfuscation1"], exportTrieObfuscationMap: [:]) 15 | var testObfuscator: MethTypeObfuscator { return MethTypeObfuscator(withMap: testMap) } 16 | 17 | func test_shouldNotReplaceClassnameInQuotationMarks_WhenNoMatchingMangling() { 18 | XCTAssertEqual(#"@"MyNotObfuscatedClassName""#, testObfuscator.generateObfuscatedMethType(methType: #"@"MyNotObfuscatedClassName""#)) 19 | } 20 | 21 | func test_shouldNotDestroyBuiltinTypes_WhenNoMatchingMangling() { 22 | XCTAssertEqual(#"v32@0:8@16Q24"#, testObfuscator.generateObfuscatedMethType(methType: #"v32@0:8@16Q24"#)) 23 | } 24 | 25 | func test_shouldReplaceClassnameInQuotationMarks() { 26 | XCTAssertEqual(#"@"OtherObfuscation1""#, testObfuscator.generateObfuscatedMethType(methType: #"@"MyClassLongerName""#)) 27 | } 28 | 29 | func test_shouldReplaceClassnameInPointyBracketsAndQuotationMarks() { 30 | XCTAssertEqual(#"@"""#, testObfuscator.generateObfuscatedMethType(methType: #"@"""#)) 31 | } 32 | 33 | func test_shouldReplaceOneClassnameInQuotationMarks() { 34 | XCTAssertEqual(#"v48@0:8@"ObfClss"16@"NSString"24@"UILocalNotification"32@?40"#, testObfuscator.generateObfuscatedMethType(methType: #"v48@0:8@"MyClass"16@"NSString"24@"UILocalNotification"32@?40"#)) 35 | } 36 | 37 | func test_shouldReplacManyClassnamesInQuotationMarks() { 38 | XCTAssertEqual(#"v48@0:8@"ObfClss"16@"NSString"24@"OtherObfuscation1"32@?40"#, testObfuscator.generateObfuscatedMethType(methType: #"v48@0:8@"MyClass"16@"NSString"24@"MyClassLongerName"32@?40"#)) 39 | } 40 | 41 | func test_shouldNotReplaceClassnamesInMach_WhenNoMatchingMangling() { 42 | // Given 43 | var sut = try! Image.load(url: URL.machoMacExecutable) 44 | 45 | // Prepare test obfuscation configuration satisfying requirements of replaceSymbols 46 | let map = SymbolManglingMap(selectors: [:], classNames: testMap.classNames, exportTrieObfuscationMap: [ 47 | URL.machoMacExecutable: [ 48 | sut.machs[0].cpu.asCpuId: 49 | (unobfuscated: Trie.empty, 50 | obfuscated: Trie.empty), 51 | ], 52 | ]) 53 | var paths = ObfuscationPaths() 54 | paths.resolvedDylibMapPerImageURL = [URL.machoMacExecutable: [:]] 55 | 56 | // When 57 | sut.replaceSymbols(withMap: map, paths: paths) 58 | 59 | // Then 60 | XCTAssertEqual(["v24@0:8@16", "@16@0:8"], sut.machs[0].methTypes) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Mach+Replacing_propertyattributes_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | private extension ObjcClass { 6 | func property(_ name: String) -> ObjcProperty? { 7 | return properties.first { $0.name.value == name } 8 | } 9 | } 10 | 11 | class Mach_Replacing_propertyattributes_Tests: XCTestCase { 12 | func test_shouldReplaceClassnamesInMach_WhenMatchingMangling() { 13 | // Given 14 | var sut = try! Image.load(url: URL.machoIos12_0Executable) 15 | 16 | // Prepare test obfuscation configuration satisfying requirements of replaceSymbols 17 | // 18 | // In normal case NSString would not be obfuscated. In test we use it because it is referenced in property types 19 | let map = SymbolManglingMap(selectors: [:], classNames: ["NSString": "ObfStrng"], exportTrieObfuscationMap: [ 20 | URL.machoIos12_0Executable: [ 21 | sut.machs[0].cpu.asCpuId: 22 | (unobfuscated: Trie.empty, 23 | obfuscated: Trie.empty), 24 | ], 25 | ]) 26 | var paths = ObfuscationPaths() 27 | paths.resolvedDylibMapPerImageURL = [URL.machoIos12_0Executable: [:]] 28 | 29 | // When 30 | sut.replaceSymbols(withMap: map, paths: paths) 31 | 32 | // Then 33 | let sampleClass = sut.machs[0].objcClasses.first { $0.name.value == "SampleClass" } 34 | 35 | XCTAssertEqual(sampleClass?.property("dynamicSampleProperty")?.typeAttribute, "T@\"ObfStrng\"") 36 | XCTAssertEqual(sampleClass?.property("additionalDynamicProperty")?.typeAttribute, "T@\"ObfStrng\"") 37 | XCTAssertEqual(sampleClass?.property("additionalNonDynamicProperty")?.typeAttribute, "T@\"ObfStrng\"") 38 | 39 | let viewController = sut.machs[0].objcClasses.first { $0.name.value == "_TtC12SampleIosApp14ViewController" } 40 | XCTAssertEqual(viewController?.property("counterLabel")?.typeAttribute, "T@\"UILabel\"", "Should not change property type that is not obfuscated") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Samples/SampleFatIosExecutable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/MachTests/Samples/SampleFatIosExecutable -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Samples/SampleMachoIos12_0Executable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/MachTests/Samples/SampleMachoIos12_0Executable -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Samples/SampleMachoMac10_14Executable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/MachTests/Samples/SampleMachoMac10_14Executable -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Samples/SampleMachoMacExecutable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/MachTests/Samples/SampleMachoMacExecutable -------------------------------------------------------------------------------- /Tests/AppTests/MachTests/Samples/URL+SampleImages.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | static var fatIosExecutable: URL { 5 | return Bundle.module.url(forResource: "SampleFatIosExecutable", withExtension: nil)! 6 | } 7 | 8 | static var machoMacExecutable: URL { 9 | return Bundle.module.url(forResource: "SampleMachoMacExecutable", withExtension: nil)! 10 | } 11 | 12 | // To obtain this executable compile SampleMacApp with MACOSX_DEPLOYMENT_TARGET = 10.14 13 | static var machoMac10_14Executable: URL { 14 | return Bundle.module.url(forResource: "SampleMachoMac10_14Executable", withExtension: nil)! 15 | } 16 | 17 | // To obtain this executable compile SampleIosApp with IPHONEOS_DEPLOYMENT_TARGET = 12.0 18 | static var machoIos12_0Executable: URL { 19 | return Bundle.module.url(forResource: "SampleMachoIos12_0Executable", withExtension: nil)! 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/FormatsTests/NIBArchiveTests/NibArchive+Loading_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | class NibArchive_Loading_Tests: XCTestCase { 6 | func test_canLoad_shouldBeTrueForIosNib() { 7 | XCTAssert(NibArchive.canLoad(from: URL.iosNib)) 8 | } 9 | 10 | func test_canLoad_shouldBeFalseForMacNib() { 11 | XCTAssertFalse(NibArchive.canLoad(from: URL.macNib)) 12 | } 13 | 14 | func test_load_shouldNotThrowForIosNib() { 15 | XCTAssertNoThrow(NibArchive.load(from: URL.iosNib)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/FormatsTests/NIBArchiveTests/NibArchive+Nib_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class NibArchive_Nib_Tests: NibModifying_TestsBase { 5 | override func setUp() { 6 | super.setUp() 7 | createSut(type: NibArchive.self, fromURL: URL.iosNib) 8 | } 9 | 10 | func test_selectors_shouldReturnAllOutletsAndActions() { 11 | let expectedSelectors = [ 12 | "actionButton", 13 | "actionWasTapped:", 14 | "associatedLabel", 15 | "titleLabel", 16 | ] 17 | XCTAssertEqual(sut.selectors.sorted(), 18 | expectedSelectors.sorted()) 19 | } 20 | 21 | func test_classNames_shouldReturnAllClassNames() { 22 | let expectedClassNames = [ 23 | "CustomObjCButton", 24 | "_TtC19MachObfuscatorTests7IosView", 25 | ] 26 | XCTAssertEqual(sut.classNames.sorted(), 27 | expectedClassNames.sorted()) 28 | } 29 | 30 | func test_modifySelectors_shouldChangeOnlyMatchingOutletsAndActions() { 31 | // When 32 | sut.modifySelectors(withMapping: [ 33 | "titleLabel": "doneButton", 34 | "noSuch": "selector", 35 | "actionWasTapped:": "timerElapsed", 36 | ]) 37 | 38 | // Then 39 | sut.save() 40 | sut = NibArchive.load(from: sutURL) 41 | let expectedSelectors = [ 42 | "actionButton", 43 | "timerElapsed", 44 | "associatedLabel", 45 | "doneButton", 46 | ] 47 | XCTAssertEqual(sut.selectors.sorted(), 48 | expectedSelectors.sorted()) 49 | } 50 | 51 | func test_modifyClassNames_shouldChangeOnlyMatchingClasses() { 52 | // When 53 | sut.modifyClassNames(withMapping: [ 54 | "_TtC19MachObfuscatorTests7IosView": "bar", 55 | "noSuch": "class", 56 | ]) 57 | 58 | // Then 59 | sut.save() 60 | sut = NibArchive.load(from: sutURL) 61 | let expectedClassNames = [ 62 | "CustomObjCButton", 63 | "bar", 64 | ] 65 | XCTAssertEqual(sut.classNames.sorted(), 66 | expectedClassNames.sorted()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/FormatsTests/NibPlistTests/NibPlist+Loading_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | class NibPlist_Loading_Tests: XCTestCase { 6 | func test_canLoad_shouldBeTrueForMacNib() { 7 | XCTAssert(NibPlist.canLoad(from: URL.macNib)) 8 | } 9 | 10 | func test_canLoad_shouldBeFalseForIosNib() { 11 | XCTAssertFalse(NibPlist.canLoad(from: URL.iosNib)) 12 | } 13 | 14 | func test_load_shouldNotThrowForMacNib() { 15 | XCTAssertNoThrow(NibPlist.load(from: URL.macNib)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/FormatsTests/NibPlistTests/NibPlist+Nib_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class NibPlist_Nib_Tests: NibModifying_TestsBase { 5 | override func setUp() { 6 | super.setUp() 7 | createSut(type: NibPlist.self, fromURL: URL.macNib) 8 | } 9 | 10 | func test_selectors_shouldReturnAllOutletsAndActions() { 11 | let expectedSelectors = [ 12 | "actionButton", 13 | "titleLabel", 14 | "nextKeyView", 15 | "nextKeyView", 16 | "actionWasTapped:", 17 | ] 18 | XCTAssertEqual(sut.selectors.sorted(), 19 | expectedSelectors.sorted()) 20 | } 21 | 22 | func test_classNames_shouldReturnAllClassNames() { 23 | let expectedClassNames = [ 24 | "NSApplication", 25 | "NSObject", 26 | "_TtC19MachObfuscatorTests7MacView", 27 | "CustomObjCButton", 28 | ] 29 | XCTAssertEqual(sut.classNames.sorted(), 30 | expectedClassNames.sorted()) 31 | } 32 | 33 | func test_modifySelectors_shouldChangeOnlyMatchingOutletsAndActions() { 34 | // When 35 | sut.modifySelectors(withMapping: [ 36 | "nextKeyView": "nkv", 37 | "noSuch": "selector", 38 | "actionWasTapped:": "memoryMonitor", 39 | ]) 40 | 41 | // Then 42 | sut.save() 43 | sut = NibPlist.load(from: sutURL) 44 | let expectedSelectors = [ 45 | "actionButton", 46 | "titleLabel", 47 | "nkv", 48 | "nkv", 49 | "memoryMonitor", 50 | ] 51 | XCTAssertEqual(sut.selectors.sorted(), 52 | expectedSelectors.sorted()) 53 | } 54 | 55 | func test_modifyClassNames_shouldChangeOnlyMatchingClasses() { 56 | // When 57 | sut.modifyClassNames(withMapping: [ 58 | "_TtC19MachObfuscatorTests7MacView": "foo", 59 | "noSuch": "class", 60 | ]) 61 | 62 | // Then 63 | sut.save() 64 | sut = NibPlist.load(from: sutURL) 65 | let expectedClassNames = [ 66 | "NSApplication", 67 | "NSObject", 68 | "foo", 69 | "CustomObjCButton", 70 | ] 71 | XCTAssertEqual(sut.classNames.sorted(), 72 | expectedClassNames.sorted()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/NibModifying_TestsBase.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class NibModifying_TestsBase: XCTestCase { 5 | let sutURL = URL.tempFile 6 | var sut: Nib! 7 | 8 | func createSut(type: Nib.Type, fromURL url: URL) { 9 | try! FileManager.default.copyItem(at: url, to: sutURL) 10 | sut = type.load(from: sutURL) 11 | } 12 | 13 | override func tearDown() { 14 | sut = nil 15 | try! FileManager.default.removeItem(at: sutURL) 16 | super.tearDown() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/Samples/IosView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/NibTests/Samples/IosView.nib -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/Samples/MacView.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/Tests/AppTests/NibTests/Samples/MacView.nib -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/Samples/URL+SampleNibs.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | static var iosNib: URL { 5 | return Bundle.module.url(forResource: "IosView", withExtension: "nib")! 6 | } 7 | 8 | static var macNib: URL { 9 | return Bundle.module.url(forResource: "MacView", withExtension: "nib")! 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/SamplesSource/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 4 | 5 | ibtool --compile ../Samples/IosView.nib IosView.xib 6 | ibtool --compile ../Samples/MacView.nib MacView.xib 7 | -------------------------------------------------------------------------------- /Tests/AppTests/NibTests/URL+NibLoading_loadNib_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class URL_NibLoading_loadNib_Tests: XCTestCase { 5 | func test_shouldLoadIosNib() { 6 | // When 7 | let nib = URL.iosNib.loadNib() 8 | 9 | // Then 10 | XCTAssertEqual(nib.classNames.count, 2) 11 | } 12 | 13 | func test_shouldLoadMacNib() { 14 | // When 15 | let nib = URL.macNib.loadNib() 16 | 17 | // Then 18 | XCTAssertEqual(nib.classNames.count, 4) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AppTests/OptionsTests/OptionsTestsSupport.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | import XCTest 4 | 5 | /// Helper for preparing `argc` and `argv` for options tests. 6 | class OptionsTestsSupport: XCTestCase { 7 | var argc: Int32! 8 | var argv: [String]! 9 | var unsafePtr: UnsafeMutablePointer?>! 10 | 11 | func setUp(with _argv: [String]) { 12 | // First `argv` is path to executable. Add it here to not bother user in each `setUp` invocation. 13 | let argv = ["/path/to/obfuscator"] + _argv 14 | argc = Int32(argv.count) 15 | self.argv = argv 16 | let unsafePtr = UnsafeMutablePointer?>.allocate(capacity: argv.count) 17 | argv.enumerated().forEach { offset, element in 18 | let nestedPtr = UnsafeMutablePointer.allocate(capacity: element.count + 1) 19 | nestedPtr.initialize(repeating: 0, count: element.count + 1) 20 | element.enumerated().forEach { charOffset, char in 21 | nestedPtr.advanced(by: charOffset).pointee = Int8(char.unicodeScalars.first!.value) 22 | } 23 | unsafePtr.advanced(by: offset).pointee = nestedPtr 24 | } 25 | self.unsafePtr = unsafePtr 26 | 27 | // Reset `getopt_long` for each test. According to man getopt(3): 28 | // 29 | // The variable optind is the index of the next element to be processed in argv. The system initializes this value to 1. 30 | // The caller can reset it to 1 to restart scanning of the same argv, or when scanning a new argument vector. 31 | optind = 1 32 | } 33 | 34 | override func tearDown() { 35 | (0 ..< argv.count).forEach { index in 36 | unsafePtr.advanced(by: index).pointee?.deallocate() 37 | } 38 | unsafePtr.deallocate() 39 | 40 | super.tearDown() 41 | } 42 | 43 | func createSut() -> Options { 44 | return Options(argc: argc, 45 | unsafeArgv: unsafePtr, 46 | argv: argv) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/AppTests/OptionsTests/Options_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class Options_Tests: OptionsTestsSupport { 5 | func test_init_withCommandLineParams_shouldLeaveDefaultParams_whenEmptyArgv() { 6 | // Given 7 | setUp(with: []) 8 | 9 | // When 10 | let sut = createSut() 11 | 12 | // Then 13 | XCTAssertFalse(sut.quiet) 14 | XCTAssertFalse(sut.verbose) 15 | XCTAssertNil(sut.appDirectoryOrFile) 16 | } 17 | 18 | func test_init_withCommandLineParams_shouldSetQuiet_whenQSwitchPresent() { 19 | // Given 20 | setUp(with: ["-q"]) 21 | 22 | // When 23 | let sut = createSut() 24 | 25 | // Then 26 | XCTAssertTrue(sut.quiet) 27 | } 28 | 29 | func test_init_withCommandLineParams_shouldSetVerbose_whenVSwitchPresent() { 30 | // Given 31 | setUp(with: ["-v"]) 32 | 33 | // When 34 | let sut = createSut() 35 | 36 | // Then 37 | XCTAssertTrue(sut.verbose) 38 | } 39 | 40 | func test_init_withCommandLineParams_shouldSetAppDirectory_whenAdditionalArgumentPresent() { 41 | // Given 42 | let expectedAppDirectory = "/some/path" 43 | setUp(with: ["-v", expectedAppDirectory]) 44 | 45 | // When 46 | let sut = createSut() 47 | 48 | // Then 49 | XCTAssertEqual(sut.appDirectoryOrFile?.path, expectedAppDirectory) 50 | } 51 | 52 | func test_init_withCommandLineParams_shouldSetSkippedSymbolsLists() { 53 | // Given 54 | let expectedFilePath = "/some/path/to/file.txt" 55 | setUp(with: ["--skip-symbols-from-list", expectedFilePath]) 56 | 57 | // When 58 | let sut = createSut() 59 | 60 | // Then 61 | XCTAssertEqual(sut.skippedSymbolsLists, [URL(fileURLWithPath: expectedFilePath)]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | } 7 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/SampleClass+Cat.h: -------------------------------------------------------------------------------- 1 | #import "SampleClass.h" 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface SampleClass (Cat) 6 | 7 | @property (nonatomic) NSString *additionalDynamicProperty; 8 | @property (nonatomic) NSString *additionalNonDynamicProperty; 9 | 10 | @end 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/SampleClass+Cat.m: -------------------------------------------------------------------------------- 1 | #import "SampleClass+Cat.h" 2 | 3 | @implementation SampleClass (Cat) 4 | 5 | @dynamic additionalDynamicProperty; 6 | 7 | - (void)setAdditionalNonDynamicProperty:(NSString *)additionalNonDynamicProperty { 8 | 9 | } 10 | 11 | - (NSString *)additionalNonDynamicProperty { 12 | return @"test"; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/SampleClass.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | // Just to add some class names to executable 4 | @interface SampleClass : NSObject 5 | 6 | @property (nonatomic) NSString *dynamicSampleProperty; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/SampleClass.m: -------------------------------------------------------------------------------- 1 | #import "SampleClass.h" 2 | 3 | @implementation SampleClass 4 | 5 | @dynamic dynamicSampleProperty; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | import SampleIosAppViewModel 2 | import UIKit 3 | 4 | class ViewController: UIViewController { 5 | let viewModel = ViewModel() 6 | 7 | @IBOutlet var counterLabel: UILabel! 8 | 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | counterLabel.text = viewModel.counterText 13 | viewModel.delegate = self 14 | } 15 | 16 | @IBAction func didTapIncrement(_: UIButton) { 17 | viewModel.increment() 18 | } 19 | } 20 | 21 | extension ViewController: ViewModelDelegate { 22 | func viewModelDidChange(counterText: String) { 23 | counterLabel.text = counterText 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppModel/SampleIosAppModel.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for SampleIosAppModel. 4 | FOUNDATION_EXPORT double SampleIosAppModelVersionNumber; 5 | 6 | //! Project version string for SampleIosAppModel. 7 | FOUNDATION_EXPORT const unsigned char SampleIosAppModelVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | #import 12 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppModel/SampleModel.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface SampleModel : NSObject 4 | 5 | @property (nonatomic, readonly) NSInteger counter; 6 | 7 | - (void)increment; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppModel/SampleModel.m: -------------------------------------------------------------------------------- 1 | #import "SampleModel.h" 2 | 3 | @interface SampleModel () 4 | 5 | @property (nonatomic) NSInteger counter; 6 | 7 | @end 8 | 9 | @implementation SampleModel 10 | 11 | - (void)increment { 12 | ++self.counter; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppViewModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppViewModel/SampleIosAppViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // SampleIosAppViewModel.h 3 | // SampleIosAppViewModel 4 | // 5 | // Created by Kamil Borzym on 16/08/2018. 6 | // Copyright © 2018 Kamil Borzym. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SampleIosAppViewModel. 12 | FOUNDATION_EXPORT double SampleIosAppViewModelVersionNumber; 13 | 14 | //! Project version string for SampleIosAppViewModel. 15 | FOUNDATION_EXPORT const unsigned char SampleIosAppViewModelVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleIosApp/SampleIosAppViewModel/ViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SampleIosAppModel 3 | 4 | public protocol ViewModelDelegate: AnyObject { 5 | func viewModelDidChange(counterText: String) 6 | } 7 | 8 | public class ViewModel { 9 | private let model: SampleModel 10 | private var observation: NSKeyValueObservation? 11 | 12 | public private(set) var counterText: String = "" { 13 | didSet { 14 | delegate?.viewModelDidChange(counterText: counterText) 15 | } 16 | } 17 | 18 | public weak var delegate: ViewModelDelegate? 19 | 20 | public init(model: SampleModel = SampleModel()) { 21 | self.model = model 22 | observation = self.model.observe(\SampleModel.counter, options: .initial) { [weak self] model, _ in 23 | self?.counterText = String(model.counter) 24 | } 25 | } 26 | 27 | deinit { 28 | observation?.invalidate() 29 | } 30 | 31 | public func increment() { 32 | model.increment() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | class AppDelegate: NSObject, NSApplicationDelegate {} 5 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2018 Kamil Borzym. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/SampleClass+Cat.h: -------------------------------------------------------------------------------- 1 | #import "SampleClass.h" 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface SampleClass (Cat) 6 | 7 | @property (nonatomic) NSString *additionalDynamicProperty; 8 | @property (nonatomic) NSString *additionalNonDynamicProperty; 9 | 10 | @end 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/SampleClass+Cat.m: -------------------------------------------------------------------------------- 1 | #import "SampleClass+Cat.h" 2 | 3 | @implementation SampleClass (Cat) 4 | 5 | @dynamic additionalDynamicProperty; 6 | 7 | - (void)setAdditionalNonDynamicProperty:(NSString *)additionalNonDynamicProperty { 8 | 9 | } 10 | 11 | - (NSString *)additionalNonDynamicProperty { 12 | return @"test"; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/SampleClass.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | // Just to add some class names to executable 4 | @interface SampleClass : NSObject 5 | 6 | @property (nonatomic) NSString *dynamicSampleProperty; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/SampleClass.m: -------------------------------------------------------------------------------- 1 | #import "SampleClass.h" 2 | 3 | @implementation SampleClass 4 | 5 | @dynamic dynamicSampleProperty; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/SampleMacApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SampleMacAppViewModel 3 | 4 | class ViewController: NSViewController { 5 | let viewModel = ViewModel() 6 | 7 | @IBOutlet var counterLabel: NSTextField! 8 | 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | counterLabel.stringValue = viewModel.counterText 13 | viewModel.delegate = self 14 | } 15 | 16 | @IBAction func didTapIncrement(_: NSButton) { 17 | viewModel.increment() 18 | } 19 | } 20 | 21 | extension ViewController: ViewModelDelegate { 22 | func viewModelDidChange(counterText: String) { 23 | counterLabel.stringValue = counterText 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Kamil Borzym. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppModel/SampleMacAppModel.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for SampleMacAppModel. 4 | FOUNDATION_EXPORT double SampleMacAppModelVersionNumber; 5 | 6 | //! Project version string for SampleMacAppModel. 7 | FOUNDATION_EXPORT const unsigned char SampleMacAppModelVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | #import 12 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppModel/SampleModel.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | @interface SampleModel : NSObject 4 | 5 | @property (nonatomic, readonly) NSInteger counter; 6 | 7 | - (void)increment; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppModel/SampleModel.m: -------------------------------------------------------------------------------- 1 | #import "SampleModel.h" 2 | 3 | @interface SampleModel () 4 | 5 | @property (nonatomic) NSInteger counter; 6 | 7 | @end 8 | 9 | @implementation SampleModel 10 | 11 | - (void)increment { 12 | ++self.counter; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppViewModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Kamil Borzym. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppViewModel/SampleMacAppViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // SampleMacAppViewModel.h 3 | // SampleMacAppViewModel 4 | // 5 | // Created by Kamil Borzym on 14/08/2018. 6 | // Copyright © 2018 Kamil Borzym. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SampleMacAppViewModel. 12 | FOUNDATION_EXPORT double SampleMacAppViewModelVersionNumber; 13 | 14 | //! Project version string for SampleMacAppViewModel. 15 | FOUNDATION_EXPORT const unsigned char SampleMacAppViewModelVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/AppTests/SampleAppSources/SampleMacApp/SampleMacAppViewModel/ViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SampleMacAppModel 3 | 4 | public protocol ViewModelDelegate: AnyObject { 5 | func viewModelDidChange(counterText: String) 6 | } 7 | 8 | public class ViewModel { 9 | private let model: SampleModel 10 | private var observation: NSKeyValueObservation? 11 | 12 | public private(set) var counterText: String = "" { 13 | didSet { 14 | delegate?.viewModelDidChange(counterText: counterText) 15 | } 16 | } 17 | 18 | public weak var delegate: ViewModelDelegate? 19 | 20 | public init(model: SampleModel = SampleModel()) { 21 | self.model = model 22 | observation = self.model.observe(\SampleModel.counter, options: .initial) { [weak self] model, _ in 23 | self?.counterText = String(model.counter) 24 | } 25 | } 26 | 27 | deinit { 28 | observation?.invalidate() 29 | } 30 | 31 | public func increment() { 32 | model.increment() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolManglingTests/ArraySentenceGenerator.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | 4 | class ArraySentenceGenerator: SentenceGenerator { 5 | var sentences: [String] = [] 6 | func getUniqueSentence(length: Int) -> String? { 7 | guard let sentenceIndex = sentences.firstIndex(where: { $0.count == length }) else { 8 | return nil 9 | } 10 | 11 | return sentences.remove(at: sentenceIndex) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolManglingTests/CaesarMangler_Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class CaesarMangler_Tests: XCTestCase { 4 | // TODO: test missing 5 | } 6 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolManglingTests/RealWordsExportTrieMangler_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RealWordsExportTrieMangler_Tests: XCTestCase { 5 | private var sut: RealWordsExportTrieMangler! 6 | private var rootTrie: Trie! 7 | 8 | override func setUp() { 9 | super.setUp() 10 | sut = RealWordsExportTrieMangler(machOViewDoomEnabled: false) 11 | 12 | rootTrie = Trie.testTrie(levels: [ 13 | (labelLength: 1, childrenCount: 3), 14 | (labelLength: 3, childrenCount: 3), 15 | (labelLength: 2, childrenCount: 2), 16 | (labelLength: 2, childrenCount: 2), 17 | (labelLength: 2, childrenCount: 0), 18 | ]) 19 | } 20 | 21 | override func tearDown() { 22 | sut = nil 23 | rootTrie = nil 24 | super.tearDown() 25 | } 26 | 27 | func test_exportTrieObfuscationWithoutMachoViewDoom() { 28 | let obfuscatedTrie = sut.mangle(trie: rootTrie) 29 | XCTAssertEqual(obfuscatedTrie.label, [1]) 30 | XCTAssertEqual(obfuscatedTrie.children[0].label, [1, 1, 1]) 31 | XCTAssertEqual(obfuscatedTrie.children[0].children[1].label, [2, 2]) 32 | XCTAssertEqual(obfuscatedTrie.children[1].label, [2, 2, 2]) 33 | XCTAssertEqual(obfuscatedTrie.children[2].label, [3, 3, 3]) 34 | XCTAssertEqual(obfuscatedTrie.children[2].children[0].label, [1, 1]) 35 | XCTAssertEqual(obfuscatedTrie.children[2].children[1].label, [2, 2]) 36 | } 37 | 38 | func test_exportTrieObfuscationWithMachoViewDoom() { 39 | sut = RealWordsExportTrieMangler(machOViewDoomEnabled: true) 40 | let obfuscatedTrie = sut.mangle(trie: rootTrie) 41 | XCTAssertEqual(obfuscatedTrie.label, [0]) 42 | XCTAssertEqual(obfuscatedTrie.children[0].label, [0, 0, 0]) 43 | XCTAssertEqual(obfuscatedTrie.children[0].children[1].label, [1, 1]) 44 | XCTAssertEqual(obfuscatedTrie.children[0].label, [0, 0, 0]) 45 | XCTAssertEqual(obfuscatedTrie.children[2].label, [2, 2, 2]) 46 | XCTAssertEqual(obfuscatedTrie.children[2].children[0].label, [0, 0]) 47 | XCTAssertEqual(obfuscatedTrie.children[2].children[1].label, [1, 1]) 48 | } 49 | } 50 | 51 | private typealias TrieLevel = (labelLength: Int, childrenCount: Int) 52 | 53 | private extension Trie { 54 | static func testTrie(levels: [TrieLevel]) -> Trie { 55 | guard !levels.isEmpty else { 56 | return Trie(exportsSymbol: true, 57 | labelRange: 0 ..< 3, 58 | label: [UInt8].random(count: 3), 59 | children: []) 60 | } 61 | 62 | let headLevel = levels[0] 63 | let tailLevels = Array(levels.suffix(from: 1)) 64 | let children = tailLevels.isEmpty 65 | ? [] 66 | : (0 ..< headLevel.childrenCount).map { _ in testTrie(levels: tailLevels) } 67 | return Trie(exportsSymbol: tailLevels.count % 2 == 0, 68 | labelRange: 0 ..< UInt64(headLevel.labelLength), 69 | label: [UInt8].random(count: headLevel.labelLength), 70 | children: children) 71 | } 72 | } 73 | 74 | private extension Array where Element == UInt8 { 75 | static func random(count: Int) -> [UInt8] { 76 | return (0 ..< count).map { _ in 77 | UInt8.random(in: 1 ... UInt8.max) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolManglingTests/RealWordsExportTrieMangler_emptyLabeledNodes_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class RealWordsExportTrieMangler_emptyLabeledNodes_Tests: XCTestCase { 5 | private var sut: RealWordsExportTrieMangler! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | sut = RealWordsExportTrieMangler(machOViewDoomEnabled: false) 10 | } 11 | 12 | override func tearDown() { 13 | sut = nil 14 | super.tearDown() 15 | } 16 | 17 | func test_shouldProduceUniqueSymbols() { 18 | // Given 19 | let trie = 20 | node([3], [ 21 | node([5], [ 22 | node([8]), 23 | node([13]), 24 | node([], [ 25 | node([21]), 26 | node([], [ 27 | node([34]), 28 | ]), 29 | node([55]), 30 | ]), 31 | node([], [ 32 | node([89], [ 33 | node([144]), 34 | node([233]), 35 | ]), 36 | ]), 37 | ]), 38 | node([250]), 39 | ]) 40 | 41 | let expectedTrie = 42 | node([1], [ 43 | node([1], [ 44 | node([1]), 45 | node([2]), 46 | node([], [ 47 | node([3]), 48 | node([], [ 49 | node([4]), 50 | ]), 51 | node([5]), 52 | ]), 53 | node([], [ 54 | node([6], [ 55 | node([1]), 56 | node([2]), 57 | ]), 58 | ]), 59 | ]), 60 | node([2]), 61 | ]) 62 | 63 | // When 64 | let obfuscatedTrie = sut.mangle(trie: trie) 65 | 66 | // Then 67 | XCTAssert(obfuscatedTrie.exportedLabelStrings.containsUniqueElements) 68 | assertEqual(obfuscatedTrie, expectedTrie) 69 | } 70 | } 71 | 72 | private extension RealWordsExportTrieMangler_emptyLabeledNodes_Tests { 73 | func assertEqual(_ trie1: Trie, _ trie2: Trie, pathSoFar: [Int] = [], file: StaticString = #file, line: UInt = #line) { 74 | XCTAssertEqual(trie1.label, trie2.label, "Unexpected labels at path \(pathSoFar)", file: file, line: line) 75 | zip(trie1.children, trie2.children).enumerated().forEach { idx, correspondingChildren in 76 | assertEqual(correspondingChildren.0, correspondingChildren.1, pathSoFar: pathSoFar + [idx], file: file, line: line) 77 | } 78 | } 79 | } 80 | 81 | private extension Array where Element: Hashable { 82 | var containsUniqueElements: Bool { 83 | return count == Set(self).count 84 | } 85 | } 86 | 87 | private func node(_ label: [UInt8], _ children: [Trie] = []) -> Trie { 88 | return Trie(exportsSymbol: !label.isEmpty, 89 | labelRange: 0 ..< UInt64(label.count), 90 | label: label, 91 | children: children) 92 | } 93 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolsCollectingTests/SymbolsSourceLoaderMock.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | 4 | struct SymbolsSourceMock: SymbolsSource { 5 | var selectors: [String] 6 | var classNames: [String] 7 | var cstrings: [String] 8 | var dynamicPropertyNames: [String] 9 | var exportedTrie: Trie? 10 | var cpu: Mach.Cpu 11 | } 12 | 13 | extension SymbolsSourceMock { 14 | static func with(selectors: [String] = [], 15 | classNames: [String] = [], 16 | cstrings: [String] = [], 17 | dynamicPropertyNames: [String] = [], 18 | exportedTrie: Trie? = nil, 19 | cpuType: Int32 = 0x17, cpuSubtype: Int32 = 0x42) -> SymbolsSourceMock { 20 | return SymbolsSourceMock(selectors: selectors, 21 | classNames: classNames, 22 | cstrings: cstrings, 23 | dynamicPropertyNames: dynamicPropertyNames, 24 | exportedTrie: exportedTrie, 25 | cpu: Mach.Cpu(type: cpuType, subtype: cpuSubtype)) 26 | } 27 | } 28 | 29 | extension Trie { 30 | static func with(label: String) -> Trie { 31 | let labelBytes: [UInt8] = [UInt8](label.utf8) 32 | return Trie(exportsSymbol: false, 33 | labelRange: UInt64(0) ..< UInt64(0), 34 | label: labelBytes, 35 | children: []) 36 | } 37 | 38 | var labelString: String? { 39 | return String(bytes: label, encoding: .utf8) 40 | } 41 | } 42 | 43 | class SymbolsSourceLoaderMock { 44 | private enum Error: Swift.Error { 45 | case noEntryForPath 46 | } 47 | 48 | private var sourcesPerUrl: [String: [SymbolsSource]] = [:] 49 | 50 | subscript(path: String) -> [SymbolsSource] { 51 | get { 52 | return sourcesPerUrl[path] ?? [] 53 | } 54 | set { 55 | sourcesPerUrl[path] = newValue 56 | } 57 | } 58 | } 59 | 60 | extension SymbolsSourceLoaderMock: SymbolsSourceLoader { 61 | func load(forURL url: URL) throws -> [SymbolsSource] { 62 | let path = url.resolvingSymlinksInPath().path 63 | if let sources = sourcesPerUrl[path] { 64 | return sources 65 | } else { 66 | throw Error.noEntryForPath 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolsListParsing/TextFileSymbolListLoaderMock.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import Foundation 3 | 4 | class TextFileSymbolListLoaderMock { 5 | private var symbolsPerUrl: [String: ObjectSymbols] = [:] 6 | 7 | subscript(path: String) -> ObjectSymbols? { 8 | get { 9 | return symbolsPerUrl[path] 10 | } 11 | set { 12 | symbolsPerUrl[path] = newValue 13 | } 14 | } 15 | } 16 | 17 | extension TextFileSymbolListLoaderMock : TextFileSymbolListLoaderProtocol { 18 | func load(fromTextFile url: URL) -> ObjectSymbols { 19 | let path = url.resolvingSymlinksInPath().path 20 | if let symbols = symbolsPerUrl[path] { 21 | return symbols 22 | } else { 23 | return ObjectSymbols(selectors: [], classNames: []) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AppTests/SymbolsListParsing/TextFileSymbolListLoader_load_Tests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTest 3 | 4 | class TextFileSymbolListLoader_load_Tests: XCTestCase { 5 | var sut: TextFileSymbolListLoader! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | sut = TextFileSymbolListLoader() 11 | } 12 | 13 | override func tearDown() { 14 | sut = nil 15 | 16 | super.tearDown() 17 | } 18 | 19 | func test_load_shouldTryToReadTheFileStringContents() throws { 20 | // Given 21 | let exp = expectation(description: "stringWithContentsOf expected") 22 | var capturedUrl: URL! 23 | // When 24 | _ = try sut.load(fromTextFile: .sample, stringWithContentsOf: { url in 25 | capturedUrl = url 26 | exp.fulfill() 27 | return "contents" 28 | }) 29 | 30 | // Then 31 | waitForExpectations(timeout: 0.1, handler: nil) 32 | XCTAssertEqual(URL.sample, capturedUrl) 33 | } 34 | 35 | func test_load_shouldReturnOnlyPotentialClassNames() throws { 36 | // When 37 | let result = try sut.load(fromTextFile: .sample, stringWithContentsOf: { _ in 38 | """ 39 | Class_1_żółć_ok 40 | Class WithSpace 41 | Cl!ass 42 | Cl@ass 43 | Cl#ass 44 | Cl$ass 45 | Cl%ass 46 | Cl^ass 47 | Cl&ass 48 | Cl*ass 49 | Cl(ass 50 | Cl)ass 51 | Cl:ass 52 | Cl?ass 53 | """ 54 | }) 55 | 56 | // Then 57 | XCTAssertEqual(result.classNames, ["Class_1_żółć_ok"]) 58 | } 59 | 60 | func test_load_shouldReturnOnlyPotentialSelectors() throws { 61 | // When 62 | let result = try sut.load(fromTextFile: .sample, stringWithContentsOf: { _ in 63 | """ 64 | getter 65 | selectors_are_great:lol: 66 | selector withSpace 67 | se!lector 68 | se@lector 69 | se#lector 70 | se$lector 71 | se%lector 72 | se^lector 73 | se&lector 74 | se*lector 75 | se(lector 76 | se)lector 77 | se?lector 78 | """ 79 | }) 80 | 81 | // Then 82 | XCTAssertEqual(result.selectors, ["getter", "selectors_are_great:lol:"]) 83 | } 84 | } 85 | 86 | private extension URL { 87 | static let sample = URL(fileURLWithPath: "/tmp/sample.txt") 88 | } 89 | -------------------------------------------------------------------------------- /WordList/generate_swift.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | txt_lines = IO.readlines("english_top_1000.txt").map { |w| w.strip } 4 | code_lines = txt_lines.map { |w| " \"#{w}\"," } 5 | swift_lines = ["enum Words {", " static let englishTop1000 = ["] << code_lines << " ]" << "}" << "" 6 | swift_text = swift_lines.join("\n") 7 | IO.write("../MachObfuscator/SymbolMangling/RealWordsMangler/Words.swift", swift_text) 8 | -------------------------------------------------------------------------------- /obfuscate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Arguments 6 | 7 | if [ "$#" -lt 2 ] ; then 8 | cat <&2 36 | exit 1 37 | fi 38 | 39 | # Paths 40 | APP_FILENAME=`basename "$SRC_APP"` 41 | APP_DIR=`dirname "$SRC_APP"` 42 | 43 | OBF_APP_FILENAME="${APP_FILENAME}_obf.ipa" 44 | OBF_APP="$APP_DIR/$OBF_APP_FILENAME" 45 | 46 | TMP_DIR=/tmp/obf 47 | UNPACKED="$TMP_DIR/Payload" 48 | 49 | rm -rf "$TMP_DIR" || true 50 | mkdir -p "$TMP_DIR" 51 | rm "$OBF_APP" || true 52 | 53 | echo "Unzipping..." 54 | 55 | unzip -qd "$TMP_DIR" "$SRC_APP" 56 | 57 | echo "Obfuscating..." 58 | ls -Ll "$MACH_OBFUSCATOR" 59 | time "$MACH_OBFUSCATOR" -m realWords "$@" "$UNPACKED" 60 | 61 | echo "Zipping..." 62 | 63 | (cd "$TMP_DIR" && zip -qr "$OBF_APP_FILENAME" .) 64 | mv "$TMP_DIR/$OBF_APP_FILENAME" "$APP_DIR" 65 | 66 | if [ "$CERT" != "NO_RESIGN" ] ; then 67 | echo "Resigning..." 68 | if [ -f "$XRESIGN" ]; then 69 | echo "XReSign exists" 70 | #git pull 71 | else 72 | git clone https://github.com/xndrs/XReSign.git 73 | #fix permissions 74 | chmod +x "$XRESIGN" 75 | fi 76 | 77 | #Xresign has problems with relative paths, only absolute work well, 78 | #but still, tmp directory is not deleted afterwards. 79 | #This means that this script must be given an absolute path 80 | 81 | "$XRESIGN" -s "$OBF_APP" -c "$CERT" 82 | else 83 | echo "Not resigning app" 84 | fi 85 | 86 | echo "Done." 87 | 88 | -------------------------------------------------------------------------------- /readme_resource/classes_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/classes_after.png -------------------------------------------------------------------------------- /readme_resource/classes_after_titled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/classes_after_titled.png -------------------------------------------------------------------------------- /readme_resource/classes_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/classes_before.png -------------------------------------------------------------------------------- /readme_resource/classes_before_titled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/classes_before_titled.png -------------------------------------------------------------------------------- /readme_resource/machobfuscator_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/machobfuscator_demo.gif -------------------------------------------------------------------------------- /readme_resource/selectors_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/selectors_after.png -------------------------------------------------------------------------------- /readme_resource/selectors_after_titled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/selectors_after_titled.png -------------------------------------------------------------------------------- /readme_resource/selectors_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/selectors_before.png -------------------------------------------------------------------------------- /readme_resource/selectors_before_titled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kam800/MachObfuscator/3b0e899a9f22a8032abcaf972ca17bce32a0b708/readme_resource/selectors_before_titled.png -------------------------------------------------------------------------------- /resign.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # MachObfuscator doesn't support resigning yet. This script can be used to 4 | # resign all images in the app bundle. 5 | 6 | if [ $# != 2 ] 7 | then 8 | echo "usage: $0 appPath identity" 9 | echo "eg.: $0 ~/SampleApp.app '-'" 10 | echo " $0 ~/SampleApp.app 'iPhone Developer'" 11 | exit 1 12 | fi 13 | 14 | find "$1" -name libswift* | while read FILE; do codesign -f -s "$2" "$FILE"; done 15 | find "$1" -name _CodeSignature | while read FILE; do codesign -f -s "$2" "$FILE/.."; done 16 | --------------------------------------------------------------------------------