Skip to content

Commit b8a9541

Browse files
committed
feat: inject more frameworks
1 parent eb6689a commit b8a9541

File tree

5 files changed

+403
-17
lines changed

5 files changed

+403
-17
lines changed

TrollFools.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
61EFA37B2D31165700159442 /* InjectorV3+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EFA37A2D31165000159442 /* InjectorV3+Backup.swift */; };
6666
61EFA37D2D311DA800159442 /* InjectorV3+Inject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EFA37C2D311DA300159442 /* InjectorV3+Inject.swift */; };
6767
61F595622D6B42340034DD83 /* SwiftUIIntrospect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 61F595612D6B42340034DD83 /* SwiftUIIntrospect-Static */; };
68+
86B80B3B2E4CE82500543291 /* LibraryManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */; };
6869
CC0D662D2D7F11A2000EADED /* ArArchiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D662C2D7F11A2000EADED /* ArArchiveKit */; };
6970
CC0D662F2D7F11AC000EADED /* ArArchiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D662E2D7F11AC000EADED /* ArArchiveKit */; };
7071
CC0D66322D7F13A9000EADED /* SWCompression in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D66312D7F13A9000EADED /* SWCompression */; };
@@ -171,6 +172,7 @@
171172
61EFA3782D3108B700159442 /* InjectorV3+Eject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Eject.swift"; sourceTree = "<group>"; };
172173
61EFA37A2D31165000159442 /* InjectorV3+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Backup.swift"; sourceTree = "<group>"; };
173174
61EFA37C2D311DA300159442 /* InjectorV3+Inject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Inject.swift"; sourceTree = "<group>"; };
175+
86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManagerView.swift; sourceTree = "<group>"; };
174176
CC0E80FA2C54F84000B137B4 /* mv-15 */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = "mv-15"; sourceTree = "<group>"; };
175177
CC1548CF2C4A6B8200A4173E /* ct_bypass */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = ct_bypass; sourceTree = "<group>"; };
176178
CC1548D22C4A743200A4173E /* SuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuccessView.swift; sourceTree = "<group>"; };
@@ -280,6 +282,7 @@
280282
6124A0722CD2327E00C52253 /* View */ = {
281283
isa = PBXGroup;
282284
children = (
285+
86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */,
283286
6124A0732CD2328600C52253 /* AppListCell.swift */,
284287
CCF4706D2C4A4BAB008D8197 /* AppListView.swift */,
285288
61C7D54B2D82DBAC0064D626 /* DisclaimerView.swift */,
@@ -673,6 +676,7 @@
673676
61EFA3732D30347A00159442 /* InjectorV3+MachO.swift in Sources */,
674677
614C0C682D7C72AD007E9184 /* AppListSearchModel.swift in Sources */,
675678
614C0C622D7C2EC0007E9184 /* Constants.swift in Sources */,
679+
86B80B3B2E4CE82500543291 /* LibraryManagerView.swift in Sources */,
676680
);
677681
runOnlyForDeploymentPostprocessing = 0;
678682
};

TrollFools/AppListView.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct AppListView: View {
2020

2121
@State var selectorOpenedURL: URLIdentifiable? = nil
2222
@State var selectedIndex: String? = nil
23+
@State private var isLibraryManagerPresented: Bool = false
2324

2425
@State var isWarningPresented = false
2526
@State var temporaryOpenedURL: URLIdentifiable? = nil
@@ -259,6 +260,14 @@ struct AppListView: View {
259260
}
260261
}
261262
}
263+
ToolbarItem(placement: .navigationBarLeading) {
264+
Button {
265+
isLibraryManagerPresented = true
266+
} label: {
267+
Image(systemName: "shippingbox")
268+
}
269+
.accessibilityLabel(NSLocalizedString("Libraries", comment: ""))
270+
}
262271
ToolbarItem(placement: .navigationBarTrailing) {
263272
Button {
264273
appList.filter.showPatchedOnly.toggle()
@@ -276,6 +285,11 @@ struct AppListView: View {
276285
.accessibilityLabel(NSLocalizedString("Show Patched Only", comment: ""))
277286
}
278287
}
288+
.sheet(isPresented: $isLibraryManagerPresented) {
289+
NavigationView {
290+
LibraryManagerView()
291+
}
292+
}
279293
}
280294

281295
var allAppGroup: some View {

TrollFools/InjectorV3+Inject.swift

Lines changed: 157 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
import CocoaLumberjackSwift
99
import 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+
1123
extension 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

Comments
 (0)