88import CocoaLumberjackSwift
99import Foundation
1010
11+ fileprivate var gCachedLibraryIndex : [ String : InjectorV3 . LibraryModuleEntry ] = [ : ]
12+ fileprivate var gPreparedLibraryURLs : [ ObjectIdentifier : [ String : URL ] ] = [ : ]
13+ fileprivate let gLibraryAliasMap : [ String : String ] = [
14+ " ellekit " : " CydiaSubstrate " ,
15+ " ellekit.framework " : " CydiaSubstrate " ,
16+ " libellekit.dylib " : " CydiaSubstrate " ,
17+ " libsubstitute.dylib " : " CydiaSubstrate " ,
18+ " libsubstrate.dylib " : " CydiaSubstrate " ,
19+ " cydiasubstrate " : " CydiaSubstrate " ,
20+ " cydiasubstrate.framework " : " CydiaSubstrate " ,
21+ ]
22+
1123extension InjectorV3 {
1224 enum Strategy : String , CaseIterable {
1325 case lexicographic
@@ -60,12 +72,29 @@ extension InjectorV3 {
6072 return
6173 }
6274
75+ Self . cachedLibraryIndex = [ : ]
76+ Self . buildLibraryIndexIfNeeded ( )
77+
6378 try assetURLs. forEach {
64- try standardizeLoadCommandDylibToSubstrate ( $0)
79+ try standardizeLoadCommandDylibToLocalLibrary ( $0)
6580 try applyCoreTrustBypass ( $0)
6681 }
6782
68- let substrateFwkURL = try prepareSubstrate ( )
83+ var allNeededKeys : Set < String > = [ ]
84+ for assetURL in assetURLs {
85+ let machO : URL = try checkIsBundle ( assetURL) ? locateExecutableInBundle ( assetURL) : assetURL
86+ let dylibs = try loadedDylibsOfMachO ( machO)
87+ for imported in dylibs {
88+ if let ( rawKey, _) = libraryKey ( fromImportedPath: imported) {
89+ let lowered = rawKey. lowercased ( )
90+ let destKey = Self . libraryAliasMap [ lowered] ?? rawKey
91+ if Self . cachedLibraryIndex [ destKey. lowercased ( ) ] != nil {
92+ allNeededKeys. insert ( destKey)
93+ }
94+ }
95+ }
96+ }
97+ let preparedLibs = try prepareLibraryModulesIfNeeded ( keys: allNeededKeys)
6998 guard let targetMachO = try locateAvailableMachO ( ) else {
7099 DDLogError ( " All Mach-Os are protected " , ddlog: logger)
71100
@@ -74,7 +103,7 @@ extension InjectorV3 {
74103
75104 DDLogInfo ( " Best matched Mach-O is \( targetMachO. path) " , ddlog: logger)
76105
77- let resourceURLs : [ URL ] = [ substrateFwkURL ] + assetURLs
106+ let resourceURLs : [ URL ] = preparedLibs + assetURLs
78107 try makeAlternate ( targetMachO)
79108 do {
80109 try copyfiles ( resourceURLs)
@@ -105,25 +134,117 @@ extension InjectorV3 {
105134 try cmdChangeOwnerToInstalld ( target, recursively: isFramework)
106135 }
107136
108- // MARK: - Cydia Substrate
137+ // MARK: - Library Replace
138+
139+ fileprivate struct LibraryModuleEntry {
140+ enum Kind { case framework, dylib }
141+ let kind : Kind
142+ let key : String
143+ let zipURL : URL
144+ }
145+
146+ fileprivate static var cachedLibraryIndex : [ String : LibraryModuleEntry ] {
147+ get { gCachedLibraryIndex }
148+ set { gCachedLibraryIndex = newValue }
149+ }
150+ fileprivate var preparedLibraryURLs : [ String : URL ] {
151+ get { gPreparedLibraryURLs [ ObjectIdentifier ( self ) ] ?? [ : ] }
152+ set { gPreparedLibraryURLs [ ObjectIdentifier ( self ) ] = newValue }
153+ }
109154
110- fileprivate static let substrateZipURL = findResource ( substrateFwkName , fileExtension : " zip " )
155+ fileprivate static var libraryAliasMap : [ String : String ] { gLibraryAliasMap }
111156
112- fileprivate func prepareSubstrate ( ) throws -> URL {
113- try FileManager . default . unzipItem ( at : Self . substrateZipURL , to : temporaryDirectoryURL )
157+ fileprivate static func buildLibraryIndexIfNeeded ( ) {
158+ if !cachedLibraryIndex . isEmpty { return }
114159
115- let fwkURL = temporaryDirectoryURL. appendingPathComponent ( Self . substrateFwkName)
116- try markBundlesAsInjected ( [ fwkURL] , privileged: false )
160+ var index : [ String : LibraryModuleEntry ] = [ : ]
117161
118- let machO = fwkURL. appendingPathComponent ( Self . substrateName)
162+ let searchRoots : [ URL ] = [ Bundle . main. bundleURL, userLibrariesDirectoryURL]
163+ for root in searchRoots {
164+ if root == userLibrariesDirectoryURL {
165+ // 确保用户库目录存在
166+ try ? FileManager . default. createDirectory ( at: root, withIntermediateDirectories: true )
167+ }
168+ guard let enumerator = FileManager . default. enumerator ( at: root, includingPropertiesForKeys: [ . isRegularFileKey] ) else { continue }
169+ for case let fileURL as URL in enumerator {
170+ guard let isRegular = try ? fileURL. resourceValues ( forKeys: [ . isRegularFileKey] ) . isRegularFile, isRegular == true else { continue }
171+ let name = fileURL. lastPathComponent
172+ if name. hasSuffix ( " .framework.zip " ) {
173+ let moduleName = String ( name. dropLast ( " .framework.zip " . count) )
174+ // 用户库优先覆盖内置
175+ index [ moduleName. lowercased ( ) ] = LibraryModuleEntry ( kind: . framework, key: moduleName, zipURL: fileURL)
176+ } else if name. hasSuffix ( " .dylib.zip " ) {
177+ let dylibName = String ( name. dropLast ( " .zip " . count) )
178+ index [ dylibName. lowercased ( ) ] = LibraryModuleEntry ( kind: . dylib, key: dylibName, zipURL: fileURL)
179+ }
180+ }
181+ }
119182
120- try cmdCoreTrustBypass ( machO, teamID: teamID)
121- try cmdChangeOwnerToInstalld ( fwkURL, recursively: true )
183+ cachedLibraryIndex = index
184+ }
185+
186+ /// 用户自定义库目录:App Support/<bundle-id>/Libraries
187+ fileprivate static var userLibrariesDirectoryURL : URL {
188+ let base = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
189+ return base. appendingPathComponent ( gTrollFoolsIdentifier, isDirectory: true )
190+ . appendingPathComponent ( " Libraries " , isDirectory: true )
191+ }
192+
193+ fileprivate func libraryKey( fromImportedPath imported: String ) -> ( key: String , kind: LibraryModuleEntry . Kind ) ? {
194+ let lower = imported. lowercased ( )
195+ if let range = lower. range ( of: " .framework/ " ) {
196+ let prefix = lower [ ..< range. lowerBound]
197+ if let lastSlash = prefix. lastIndex ( of: " / " ) {
198+ let start = lower. index ( after: lastSlash)
199+ let name = lower [ start..< range. lowerBound]
200+ return ( String ( name) , . framework)
201+ } else {
202+ let name = lower [ ..< range. lowerBound]
203+ return ( String ( name) , . framework)
204+ }
205+ }
206+ if let lastSlash = lower. lastIndex ( of: " / " ) {
207+ let fileName = String ( lower [ lower. index ( after: lastSlash) ... ] )
208+ if fileName. hasSuffix ( " .dylib " ) { return ( fileName, . dylib) }
209+ } else if lower. hasSuffix ( " .dylib " ) {
210+ return ( lower, . dylib)
211+ }
212+ return nil
213+ }
122214
123- return fwkURL
215+ fileprivate func prepareLibraryModulesIfNeeded( keys: Set < String > ) throws -> [ URL ] {
216+ Self . buildLibraryIndexIfNeeded ( )
217+ var prepared : [ URL ] = [ ]
218+ for rawKey in keys {
219+ let key = rawKey. lowercased ( )
220+ guard let entry = Self . cachedLibraryIndex [ key] else { continue }
221+ if let existing = preparedLibraryURLs [ key] {
222+ prepared. append ( existing)
223+ continue
224+ }
225+ try FileManager . default. unzipItem ( at: entry. zipURL, to: temporaryDirectoryURL)
226+ let targetURL : URL
227+ switch entry. kind {
228+ case . framework:
229+ let fwkURL = temporaryDirectoryURL. appendingPathComponent ( " \( entry. key) .framework " )
230+ targetURL = fwkURL
231+ try markBundlesAsInjected ( [ fwkURL] , privileged: false )
232+ let macho = fwkURL. appendingPathComponent ( entry. key)
233+ try cmdCoreTrustBypass ( macho, teamID: teamID)
234+ try cmdChangeOwnerToInstalld ( fwkURL, recursively: true )
235+ case . dylib:
236+ let dylibURL = temporaryDirectoryURL. appendingPathComponent ( entry. key)
237+ targetURL = dylibURL
238+ try cmdCoreTrustBypass ( dylibURL, teamID: teamID)
239+ try cmdChangeOwnerToInstalld ( dylibURL, recursively: false )
240+ }
241+ preparedLibraryURLs [ key] = targetURL
242+ prepared. append ( targetURL)
243+ }
244+ return prepared
124245 }
125246
126- fileprivate func standardizeLoadCommandDylibToSubstrate ( _ assetURL: URL ) throws {
247+ fileprivate func standardizeLoadCommandDylibToLocalLibrary ( _ assetURL: URL ) throws {
127248 let machO : URL
128249 if checkIsBundle ( assetURL) {
129250 machO = try locateExecutableInBundle ( assetURL)
@@ -132,10 +253,29 @@ extension InjectorV3 {
132253 }
133254
134255 let dylibs = try loadedDylibsOfMachO ( machO)
135- for dylib in dylibs {
136- if Self . ignoredDylibAndFrameworkNames. firstIndex ( where: { dylib. lowercased ( ) . hasSuffix ( " / \( $0) " ) } ) != nil {
137- try cmdChangeLoadCommandDylib ( machO, from: dylib, to: " @executable_path/Frameworks/ \( Self . substrateFwkName) / \( Self . substrateName) " )
256+ var neededKeys : Set < String > = [ ]
257+ for imported in dylibs {
258+ if let ( rawKey, _) = libraryKey ( fromImportedPath: imported) {
259+ let lower = rawKey. lowercased ( )
260+ let destKey = Self . libraryAliasMap [ lower] ?? rawKey
261+ if Self . cachedLibraryIndex [ destKey. lowercased ( ) ] != nil {
262+ neededKeys. insert ( destKey)
263+ }
264+ }
265+ }
266+ let _ = try prepareLibraryModulesIfNeeded ( keys: neededKeys)
267+ for imported in dylibs {
268+ guard let ( rawKey, _) = libraryKey ( fromImportedPath: imported) else { continue }
269+ let destKey = Self . libraryAliasMap [ rawKey. lowercased ( ) ] ?? rawKey
270+ guard let entry = Self . cachedLibraryIndex [ destKey. lowercased ( ) ] else { continue }
271+ let newName : String
272+ switch entry. kind {
273+ case . framework:
274+ newName = " @executable_path/Frameworks/ \( entry. key) .framework/ \( entry. key) "
275+ case . dylib:
276+ newName = " @executable_path/Frameworks/ \( entry. key) "
138277 }
278+ try cmdChangeLoadCommandDylib ( machO, from: imported, to: newName)
139279 }
140280 }
141281
0 commit comments