diff --git a/Feather/Backend/Observable/OptionsManager.swift b/Feather/Backend/Observable/OptionsManager.swift index 8a3f96c4..3db261c3 100644 --- a/Feather/Backend/Observable/OptionsManager.swift +++ b/Feather/Backend/Observable/OptionsManager.swift @@ -100,7 +100,9 @@ struct Options: Codable, Equatable { var removeProvisioning: Bool /// Forcefully rename string files for App name var changeLanguageFilesForCustomDisplayName: Bool - + /// If tweaks should be injected into all app extensions (PlugIns and Extensions) + var injectIntoExtensions: Bool + // MARK: Experiments /// Modifies app to support liquid glass @@ -143,6 +145,7 @@ struct Options: Codable, Equatable { removeURLScheme: false, removeProvisioning: false, changeLanguageFilesForCustomDisplayName: false, + injectIntoExtensions: false, // MARK: Experiments diff --git a/Feather/Resources/Localizable.xcstrings b/Feather/Resources/Localizable.xcstrings index 6b487663..024cbeb1 100644 --- a/Feather/Resources/Localizable.xcstrings +++ b/Feather/Resources/Localizable.xcstrings @@ -6893,6 +6893,17 @@ } } }, + "Inject into Extensions" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inject into Extensions" + } + } + } + }, "Injection Folder" : { "extractionState" : "manual", "localizations" : { diff --git a/Feather/Utilities/Handlers/TweakHandler.swift b/Feather/Utilities/Handlers/TweakHandler.swift index 0e98c932..53e37bab 100644 --- a/Feather/Utilities/Handlers/TweakHandler.swift +++ b/Feather/Utilities/Handlers/TweakHandler.swift @@ -14,6 +14,7 @@ class TweakHandler { private let _fileManager = FileManager.default private var _urlsToInject: [URL] = [] private var _directoriesToCheck: [URL] = [] + private var _injectedDylibNames: [String] = [] private let _app: URL private var _options: Options @@ -94,6 +95,11 @@ class TweakHandler { try await _handleExtractedDirectoryContents(at: _urlsToInject) } } + + // inject into all extensions if enabled + if !_injectedDylibNames.isEmpty { + _injectIntoAllExtensions(dylibNames: _injectedDylibNames) + } } // finally, handle extracted contents @@ -157,6 +163,8 @@ class TweakHandler { appExecutable: appexe.path, with: "\(_options.injectPath.rawValue)\(injectFolder.rawValue)\(destinationURL.lastPathComponent)" ) + + _injectedDylibNames.append(destinationURL.lastPathComponent) } // Inject imported framework dir @@ -249,6 +257,92 @@ class TweakHandler { } } } + + // Discovers all .appex bundles in the app's PlugIns and Extensions directories + private func _discoverAppExtensions() -> [URL] { + var extensions: [URL] = [] + + let plugInsPath = _app.appendingPathComponent("PlugIns") + let extensionsPath = _app.appendingPathComponent("Extensions") + + for directory in [plugInsPath, extensionsPath] { + guard _fileManager.fileExists(atPath: directory.path) else { continue } + + do { + let contents = try _fileManager.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + let appexBundles = contents.filter { url in + url.pathExtension.lowercased() == "appex" && url.hasDirectoryPath + } + + extensions.append(contentsOf: appexBundles) + } catch { + Logger.misc.warning("Failed to enumerate \(directory.path): \(error.localizedDescription)") + } + } + + return extensions + } + + // Injects a dylib into an extension's executable + private func _injectIntoExtension(extensionURL: URL, dylibName: String) { + guard let extensionBundle = Bundle(url: extensionURL), + let extensionExecutable = extensionBundle.executableURL else { + Logger.misc.warning("Skipping \(extensionURL.lastPathComponent): couldn't read bundle") + return + } + + var injectFolder = _options.injectFolder + if _options.injectPath == .rpath && _options.injectFolder == .frameworks { + injectFolder = .root + } + + let injectPath: String + if _options.injectPath == .rpath { + injectPath = "@rpath/\(dylibName)" + } else { + if injectFolder == .frameworks { + injectPath = "@executable_path/../../Frameworks/\(dylibName)" + } else { + injectPath = "@executable_path/../../\(dylibName)" + } + } + + let success = Zsign.injectDyLib( + appExecutable: extensionExecutable.path, + with: injectPath + ) + + if success { + Logger.misc.info("Injected \(dylibName) into extension: \(extensionURL.lastPathComponent)") + } else { + Logger.misc.warning("Failed to inject into extension: \(extensionURL.lastPathComponent)") + } + } + + // Injects all dylibs into all discovered extensions + private func _injectIntoAllExtensions(dylibNames: [String]) { + guard _options.injectIntoExtensions else { return } + + let extensions = _discoverAppExtensions() + + guard !extensions.isEmpty else { + Logger.misc.info("No app extensions found for injection") + return + } + + Logger.misc.info("Found \(extensions.count) app extension(s) for injection") + + for extensionURL in extensions { + for dylibName in dylibNames { + _injectIntoExtension(extensionURL: extensionURL, dylibName: dylibName) + } + } + } } // MARK: - Find correct files in debs diff --git a/Feather/Views/Signing/SigningTweaksView.swift b/Feather/Views/Signing/SigningTweaksView.swift index d3cba7d4..8b892550 100644 --- a/Feather/Views/Signing/SigningTweaksView.swift +++ b/Feather/Views/Signing/SigningTweaksView.swift @@ -30,6 +30,10 @@ struct SigningTweaksView: View { selection: $options.injectFolder, values: Options.InjectFolder.allCases ) + + Toggle(isOn: $options.injectIntoExtensions) { + Label(.localized("Inject into Extensions"), systemImage: "syringe") + } } NBSection(.localized("Tweaks")) {