Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Feather/Backend/Observable/OptionsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,6 +145,7 @@ struct Options: Codable, Equatable {
removeURLScheme: false,
removeProvisioning: false,
changeLanguageFilesForCustomDisplayName: false,
injectIntoExtensions: false,

// MARK: Experiments

Expand Down
11 changes: 11 additions & 0 deletions Feather/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -6893,6 +6893,17 @@
}
}
},
"Inject into Extensions" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inject into Extensions"
}
}
}
},
"Injection Folder" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
94 changes: 94 additions & 0 deletions Feather/Utilities/Handlers/TweakHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Feather/Views/Signing/SigningTweaksView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down