From 672fc3db0cfb052b1bbac389e1bb781254855ecd Mon Sep 17 00:00:00 2001 From: waru <156133757+waruhachi@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:45:31 -0600 Subject: [PATCH 1/2] feat: add support for injecting into all extensions --- .../Backend/Observable/OptionsManager.swift | 5 +- Feather/Resources/Localizable.xcstrings | 11 +++ Feather/Utilities/Handlers/TweakHandler.swift | 94 +++++++++++++++++++ Feather/Views/Signing/SigningTweaksView.swift | 4 + 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/Feather/Backend/Observable/OptionsManager.swift b/Feather/Backend/Observable/OptionsManager.swift index 8a3f96c4..d186f4bb 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 339d0762..253f9500 100644 --- a/Feather/Resources/Localizable.xcstrings +++ b/Feather/Resources/Localizable.xcstrings @@ -6875,6 +6875,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..54fe2ed0 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..962071f3 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")) { From 3a4c08f9c809c4c3b546401118dc8a149b46cf8f Mon Sep 17 00:00:00 2001 From: waruhachi <156133757+waruhachi@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:09:32 -0600 Subject: [PATCH 2/2] chore: make sure formatting is consistent --- .../Backend/Observable/OptionsManager.swift | 6 +- Feather/Utilities/Handlers/TweakHandler.swift | 186 +++++++++--------- Feather/Views/Signing/SigningTweaksView.swift | 8 +- 3 files changed, 100 insertions(+), 100 deletions(-) diff --git a/Feather/Backend/Observable/OptionsManager.swift b/Feather/Backend/Observable/OptionsManager.swift index d186f4bb..3db261c3 100644 --- a/Feather/Backend/Observable/OptionsManager.swift +++ b/Feather/Backend/Observable/OptionsManager.swift @@ -100,8 +100,8 @@ 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 + /// If tweaks should be injected into all app extensions (PlugIns and Extensions) + var injectIntoExtensions: Bool // MARK: Experiments @@ -145,7 +145,7 @@ struct Options: Codable, Equatable { removeURLScheme: false, removeProvisioning: false, changeLanguageFilesForCustomDisplayName: false, - injectIntoExtensions: false, + injectIntoExtensions: false, // MARK: Experiments diff --git a/Feather/Utilities/Handlers/TweakHandler.swift b/Feather/Utilities/Handlers/TweakHandler.swift index 54fe2ed0..53e37bab 100644 --- a/Feather/Utilities/Handlers/TweakHandler.swift +++ b/Feather/Utilities/Handlers/TweakHandler.swift @@ -14,7 +14,7 @@ class TweakHandler { private let _fileManager = FileManager.default private var _urlsToInject: [URL] = [] private var _directoriesToCheck: [URL] = [] - private var _injectedDylibNames: [String] = [] + private var _injectedDylibNames: [String] = [] private let _app: URL private var _options: Options @@ -95,11 +95,11 @@ class TweakHandler { try await _handleExtractedDirectoryContents(at: _urlsToInject) } } - - // inject into all extensions if enabled - if !_injectedDylibNames.isEmpty { - _injectIntoAllExtensions(dylibNames: _injectedDylibNames) - } + + // inject into all extensions if enabled + if !_injectedDylibNames.isEmpty { + _injectIntoAllExtensions(dylibNames: _injectedDylibNames) + } } // finally, handle extracted contents @@ -163,8 +163,8 @@ class TweakHandler { appExecutable: appexe.path, with: "\(_options.injectPath.rawValue)\(injectFolder.rawValue)\(destinationURL.lastPathComponent)" ) - - _injectedDylibNames.append(destinationURL.lastPathComponent) + + _injectedDylibNames.append(destinationURL.lastPathComponent) } // Inject imported framework dir @@ -258,91 +258,91 @@ 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) - } - } - } + // 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 962071f3..8b892550 100644 --- a/Feather/Views/Signing/SigningTweaksView.swift +++ b/Feather/Views/Signing/SigningTweaksView.swift @@ -30,10 +30,10 @@ struct SigningTweaksView: View { selection: $options.injectFolder, values: Options.InjectFolder.allCases ) - - Toggle(isOn: $options.injectIntoExtensions) { - Label(.localized("Inject into Extensions"), systemImage: "syringe") - } + + Toggle(isOn: $options.injectIntoExtensions) { + Label(.localized("Inject into Extensions"), systemImage: "syringe") + } } NBSection(.localized("Tweaks")) {