├── .vscode └── settings.json ├── TrollFools ├── ar ├── cp ├── mv ├── rm ├── xz ├── chown ├── cp-15 ├── gzip ├── ldid ├── lzma ├── mkdir ├── mv-15 ├── tar ├── ldid-14 ├── optool ├── composedeb ├── ct_bypass ├── insert_dylib ├── composedeb-15 ├── libintl.8.dylib ├── liblzma.5.dylib ├── libmd.0.dylib ├── libxar.1.dylib ├── libz-ng.2.dylib ├── libzstd.1.dylib ├── install_name_tool ├── libcrypto.3.dylib ├── libiosexec.1.dylib ├── libplist-2.0.3.dylib ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon.jpg │ │ └── Contents.json │ ├── ae86-ios.imageset │ │ ├── icon_128x128.png │ │ └── Contents.json │ ├── tricon-default.imageset │ │ ├── icon_128x128.png │ │ └── Contents.json │ ├── AppIcon-official.appiconset │ │ ├── AppIcon-official.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── CydiaSubstrate.framework.zip ├── Option.swift ├── Version.xcconfig ├── Version.Debug.xcconfig ├── InjectorV3+Error.swift ├── FilterOptions.swift ├── InjectedPlugIn.swift ├── TrollFoolsApp.swift ├── TrollFools-Bridging-Header.h ├── Info.plist ├── LSApplicationProxy.h ├── BartyCrouch.swift ├── LSApplicationWorkspace.h ├── SuccessView.swift ├── EjectListModel.swift ├── InjectorV3+Backup.swift ├── LogsView.swift ├── FailureView.swift ├── OptionCell.swift ├── ViewControllerHost.swift ├── SettingsView.swift ├── StripedTextTableViewController.h ├── Execute.swift ├── InjectorV3+Metadata.swift ├── App.swift ├── PlugInCell.swift ├── TrollFoolsStub.m ├── InjectorV3+Eject.swift ├── OptionView.swift ├── TrollFools.entitlements ├── InjectorV3+Preprocess.swift ├── InjectorV3.swift ├── AuxiliaryExecute.swift ├── InjectorV3+MachO.swift ├── InjectView.swift ├── zh-Hans.lproj │ └── Localizable.strings ├── InjectorV3+Inject.swift ├── vi.lproj │ └── Localizable.strings ├── AppListModel.swift ├── en.lproj │ └── Localizable.strings ├── EjectListView.swift ├── InjectorV3+Bundle.swift ├── AppListCell.swift ├── InjectorV3+Command.swift └── AuxiliaryExecute+Spawn.swift ├── TrollFoolsTweak ├── TrollFoolsTweak.plist ├── Makefile └── TrollFoolsTweak.x ├── devkit ├── standardize-entitlements.sh ├── rootless.sh ├── print-version.sh ├── tipa.sh └── bump-version.sh ├── CHANGELOG.md ├── TrollFools.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── TrollFools.xcscheme ├── layout └── DEBIAN │ └── control ├── .github ├── ISSUE_TEMPLATE │ └── BugReport.md └── workflows │ ├── compile.yml │ └── release.yml ├── Makefile ├── LICENSE ├── README.md ├── .bartycrouch.toml └── .gitignore /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /TrollFools/ar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/ar -------------------------------------------------------------------------------- /TrollFools/cp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/cp -------------------------------------------------------------------------------- /TrollFools/mv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/mv -------------------------------------------------------------------------------- /TrollFools/rm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/rm -------------------------------------------------------------------------------- /TrollFools/xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/xz -------------------------------------------------------------------------------- /TrollFools/chown: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/chown -------------------------------------------------------------------------------- /TrollFools/cp-15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/cp-15 -------------------------------------------------------------------------------- /TrollFools/gzip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/gzip -------------------------------------------------------------------------------- /TrollFools/ldid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/ldid -------------------------------------------------------------------------------- /TrollFools/lzma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/lzma -------------------------------------------------------------------------------- /TrollFools/mkdir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/mkdir -------------------------------------------------------------------------------- /TrollFools/mv-15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/mv-15 -------------------------------------------------------------------------------- /TrollFools/tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/tar -------------------------------------------------------------------------------- /TrollFools/ldid-14: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/ldid-14 -------------------------------------------------------------------------------- /TrollFools/optool: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/optool -------------------------------------------------------------------------------- /TrollFools/composedeb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/composedeb -------------------------------------------------------------------------------- /TrollFools/ct_bypass: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/ct_bypass -------------------------------------------------------------------------------- /TrollFoolsTweak/TrollFoolsTweak.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "wiki.qaq.TrollFools" ); }; } 2 | -------------------------------------------------------------------------------- /TrollFools/insert_dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/insert_dylib -------------------------------------------------------------------------------- /TrollFools/composedeb-15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/composedeb-15 -------------------------------------------------------------------------------- /TrollFools/libintl.8.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libintl.8.dylib -------------------------------------------------------------------------------- /TrollFools/liblzma.5.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/liblzma.5.dylib -------------------------------------------------------------------------------- /TrollFools/libmd.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libmd.0.dylib -------------------------------------------------------------------------------- /TrollFools/libxar.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libxar.1.dylib -------------------------------------------------------------------------------- /TrollFools/libz-ng.2.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libz-ng.2.dylib -------------------------------------------------------------------------------- /TrollFools/libzstd.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libzstd.1.dylib -------------------------------------------------------------------------------- /TrollFools/install_name_tool: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/install_name_tool -------------------------------------------------------------------------------- /TrollFools/libcrypto.3.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libcrypto.3.dylib -------------------------------------------------------------------------------- /TrollFools/libiosexec.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libiosexec.1.dylib -------------------------------------------------------------------------------- /TrollFools/libplist-2.0.3.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/libplist-2.0.3.dylib -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TrollFools/CydiaSubstrate.framework.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/CydiaSubstrate.framework.zip -------------------------------------------------------------------------------- /devkit/standardize-entitlements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd $(dirname $0)/.. 4 | 5 | plutil -convert xml1 TrollFools/TrollFools.entitlements 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - Added some explanation about the “No eligible framework found” error. 2 | - Patch MachOKit to avoid potential crashes when parsing certain Mach-O files. 3 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/ae86-ios.imageset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/Assets.xcassets/ae86-ios.imageset/icon_128x128.png -------------------------------------------------------------------------------- /devkit/rootless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME=rootless 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/tricon-default.imageset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/Assets.xcassets/tricon-default.imageset/icon_128x128.png -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/AppIcon-official.appiconset/AppIcon-official.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huami1314/TrollFools/HEAD/TrollFools/Assets.xcassets/AppIcon-official.appiconset/AppIcon-official.png -------------------------------------------------------------------------------- /TrollFools/Option.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Option.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | enum Option { 9 | case attach 10 | case detach 11 | } 12 | -------------------------------------------------------------------------------- /TrollFools.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/ae86-ios.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_128x128.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/tricon-default.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_128x128.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /layout/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: wiki.qaq.trollfools 2 | Name: TrollFools 3 | Version: 2.8-16 4 | Section: Applications 5 | Depends: firmware (>= 14.0) 6 | Architecture: iphoneos-arm 7 | Author: Lessica <82flex@gmail.com> 8 | Maintainer: Lessica <82flex@gmail.com> 9 | Description: Give me 108 yuan. 10 | -------------------------------------------------------------------------------- /TrollFools/Version.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Version.xcconfig 3 | // TRApp 4 | // 5 | // Created by Lessica on 2024/8/18. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | VERSION = 2.11 12 | BUILD_NUMBER = 28 13 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TrollFools.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TrollFools/Version.Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Version.xcconfig 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/8/18. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | DEBUG_VERSION = 2.11 12 | DEBUG_BUILD_NUMBER = 20250215 13 | -------------------------------------------------------------------------------- /TrollFools/Assets.xcassets/AppIcon-official.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-official.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TrollFoolsTweak/Makefile: -------------------------------------------------------------------------------- 1 | ARCHS := arm64 arm64e 2 | TARGET := iphone:clang:latest:14.0 3 | INSTALL_TARGET_PROCESSES := TrollFools 4 | 5 | include $(THEOS)/makefiles/common.mk 6 | 7 | TWEAK_NAME = TrollFoolsTweak 8 | 9 | TrollFoolsTweak_FILES = TrollFoolsTweak.x 10 | TrollFoolsTweak_CFLAGS = -fobjc-arc 11 | 12 | include $(THEOS_MAKE_PATH)/tweak.mk 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BugReport.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: For Tweak Developer to Report Bugs 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 1. 不支持的 App 不会显示在列表里。不要稍微懂点就自作聪明觉得啊那为什么别的 XX 工具能支持,已经说过了,不支持就是不支持,别在这 **浪费时间**。 11 | 2. 普通用户请勿提交「插件无效」、「闪退」,「错误代码」等问题,请找插件开发者适配。 12 | 3. 插件开发者反馈问题请先自行排查原因,**不要上来就丢源代码**。描述问题时请详细且有条理地描述问题,不要语无伦次。 13 | -------------------------------------------------------------------------------- /TrollFoolsTweak/TrollFoolsTweak.x: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FLEXManager : NSObject 4 | + (instancetype)sharedManager; 5 | - (void)showExplorer; 6 | @end 7 | 8 | %hook UIViewController 9 | 10 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event 11 | { 12 | if (motion == UIEventSubtypeMotionShake) { 13 | [[%c(FLEXManager) sharedManager] showExplorer]; 14 | } 15 | } 16 | 17 | %end 18 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Error.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension InjectorV3 { 11 | 12 | enum Error: LocalizedError { 13 | case generic(String) 14 | 15 | var errorDescription: String? { 16 | switch self { 17 | case .generic(let reason): reason 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TrollFools/FilterOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterOptions.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FilterOptions: ObservableObject { 11 | @Published var searchKeyword = "" 12 | @Published var showPatchedOnly = false 13 | 14 | var isSearching: Bool { !searchKeyword.isEmpty } 15 | 16 | func reset() { 17 | searchKeyword = "" 18 | showPatchedOnly = false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TrollFools/InjectedPlugIn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedPlugIn.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct InjectedPlugIn: Identifiable { 11 | let id = UUID() 12 | let url: URL 13 | let createdAt: Date 14 | 15 | init(url: URL) { 16 | self.url = url 17 | let attributes = try? FileManager.default.attributesOfItem(atPath: url.path) 18 | self.createdAt = attributes?[.creationDate] as? Date ?? Date() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TrollFools/TrollFoolsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrollFoolsApp.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | let gTrollFoolsIdentifier = "wiki.qaq.TrollFools" 11 | let gTrollFoolsErrorDomain = "\(gTrollFoolsIdentifier).error" 12 | 13 | @main 14 | struct TrollFoolsApp: SwiftUI.App { 15 | 16 | init() { 17 | try? FileManager.default.removeItem(at: InjectorV3.temporaryRoot) 18 | } 19 | 20 | var body: some Scene { 21 | WindowGroup { 22 | AppListView() 23 | .environmentObject(AppListModel()) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCHS := arm64 2 | TARGET := iphone:clang:latest:14.0 3 | INSTALL_TARGET_PROCESSES := TrollFools 4 | 5 | include $(THEOS)/makefiles/common.mk 6 | 7 | XCODEPROJ_NAME += TrollFools 8 | 9 | include $(THEOS_MAKE_PATH)/xcodeproj.mk 10 | 11 | SUBPROJECTS += TrollFoolsTweak 12 | 13 | include $(THEOS_MAKE_PATH)/aggregate.mk 14 | 15 | before-all:: 16 | devkit/standardize-entitlements.sh 17 | 18 | before-package:: 19 | $(ECHO_NOTHING)ldid -STrollFools/TrollFools.entitlements $(THEOS_STAGING_DIR)/Applications/TrollFools.app$(ECHO_END) 20 | 21 | export THEOS_PACKAGE_INSTALL_PREFIX 22 | export THEOS_STAGING_DIR 23 | after-package:: 24 | devkit/tipa.sh -------------------------------------------------------------------------------- /TrollFools/TrollFools-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "LSApplicationProxy.h" 6 | #import "LSApplicationWorkspace.h" 7 | #import "StripedTextTableViewController.h" 8 | #import 9 | 10 | FOUNDATION_EXTERN void TFUtilKillAll(NSString *processPath, BOOL softly); 11 | 12 | @interface UIImage (Private) 13 | + (instancetype)_applicationIconImageForBundleIdentifier:(NSString *)bundleIdentifier 14 | format:(int)format 15 | scale:(CGFloat)scale; 16 | @end 17 | -------------------------------------------------------------------------------- /TrollFools/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeName 9 | Mach-O Binary 10 | LSHandlerRank 11 | Owner 12 | LSItemContentTypes 13 | 14 | com.apple.mach-o-binary 15 | 16 | 17 | 18 | CFBundleTypeName 19 | Public Data 20 | LSHandlerRank 21 | Default 22 | LSItemContentTypes 23 | 24 | public.data 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /devkit/print-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | cd "$(dirname "$0")" 5 | 6 | if [ ! -z "$DEBUG" ]; then 7 | XCCONFIG_NAME=../TrollFools/Version.Debug.xcconfig 8 | if [ ! -f $XCCONFIG_NAME ]; then 9 | echo "Versioning configuration not found!" 10 | exit 1 11 | fi 12 | previous_version=$(awk -F "=" '/DEBUG_VERSION/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 13 | previous_build_number=$(awk -F "=" '/DEBUG_BUILD_NUMBER/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 14 | else 15 | XCCONFIG_NAME=../TrollFools/Version.xcconfig 16 | if [ ! -f $XCCONFIG_NAME ]; then 17 | echo "Versioning configuration not found!" 18 | exit 1 19 | fi 20 | previous_version=$(awk -F "=" '/VERSION/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 21 | previous_build_number=$(awk -F "=" '/BUILD_NUMBER/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 22 | fi 23 | 24 | echo "$previous_version ($previous_build_number)" -------------------------------------------------------------------------------- /TrollFools/LSApplicationProxy.h: -------------------------------------------------------------------------------- 1 | #ifndef LSApplicationProxy_h 2 | #define LSApplicationProxy_h 3 | 4 | #import 5 | 6 | @class LSPlugInKitProxy; 7 | 8 | @interface LSApplicationProxy : NSObject 9 | 10 | + (LSApplicationProxy *)applicationProxyForIdentifier:(NSString *)bundleIdentifier; 11 | 12 | - (BOOL)installed; 13 | - (BOOL)restricted; 14 | 15 | - (NSString *)applicationIdentifier; 16 | - (NSString *)localizedName; 17 | - (NSString *)shortVersionString; 18 | - (NSString *)applicationType; 19 | - (NSString *)teamID; 20 | 21 | - (NSURL *)bundleURL; 22 | - (NSURL *)dataContainerURL; 23 | - (NSURL *)bundleContainerURL; 24 | 25 | - (NSDictionary *)groupContainerURLs; 26 | - (NSDictionary *)entitlements; 27 | 28 | - (NSArray *)plugInKitPlugins; 29 | 30 | - (BOOL)isRemoveableSystemApp; 31 | - (BOOL)isRemovedSystemApp; 32 | 33 | @end 34 | 35 | #endif /* LSApplicationProxy_h */ 36 | -------------------------------------------------------------------------------- /TrollFools/BartyCrouch.swift: -------------------------------------------------------------------------------- 1 | // This file is required in order for the `transform` task of the translation helper tool BartyCrouch to work. 2 | // See here for more details: https://github.com/FlineDev/BartyCrouch 3 | 4 | import Foundation 5 | 6 | enum BartyCrouch { 7 | enum SupportedLanguage: String { 8 | case english = "en" 9 | case chineseSimplified = "zh-Hans" 10 | case vietnamese = "vi" 11 | } 12 | 13 | static func translate(key: String, translations: [SupportedLanguage: String], comment _: String? = nil) -> String { 14 | let typeName = String(describing: BartyCrouch.self) 15 | let methodName = #function 16 | 17 | print( 18 | "Warning: [BartyCrouch]", 19 | "Untransformed \(typeName).\(methodName) method call found with key '\(key)' and base translations '\(translations)'.", 20 | "Please ensure that BartyCrouch is installed and configured correctly." 21 | ) 22 | 23 | // fall back in case something goes wrong with BartyCrouch transformation 24 | return "BC: TRANSFORMATION FAILED!" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lessica 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TrollFools/LSApplicationWorkspace.h: -------------------------------------------------------------------------------- 1 | #ifndef LSApplicationWorkspace_h 2 | #define LSApplicationWorkspace_h 3 | 4 | #import 5 | 6 | @class LSApplicationProxy; 7 | 8 | @interface LSApplicationWorkspace : NSObject 9 | 10 | + (LSApplicationWorkspace *)defaultWorkspace; 11 | - (NSArray *)allApplications; 12 | - (NSArray *)allInstalledApplications; 13 | 14 | - (void)enumerateApplicationsOfType:(NSInteger)type block:(void (^)(id))block; 15 | 16 | - (BOOL)openApplicationWithBundleID:(NSString *)bundleIdentifier; 17 | - (BOOL)installApplication:(NSURL *)ipaPath withOptions:(id)arg2 error:(NSError *__autoreleasing *)error; 18 | - (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(id)arg2; 19 | - (BOOL)uninstallApplication:(NSString *)arg1 20 | withOptions:(id)arg2 21 | error:(NSError *__autoreleasing *)arg3 22 | usingBlock:(/*^block*/ id)arg4; 23 | - (BOOL)invalidateIconCache:(id)arg1; 24 | - (BOOL)openSensitiveURL:(NSURL *)url withOptions:(id)arg2 error:(NSError *__autoreleasing *)error; 25 | 26 | - (void)removeObserver:(id)arg1; 27 | - (void)addObserver:(id)arg1; 28 | 29 | @end 30 | 31 | #endif /* LSApplicationWorkspace_h */ 32 | -------------------------------------------------------------------------------- /TrollFools/SuccessView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuccessView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SuccessView: View { 11 | 12 | let title: String 13 | let logFileURL: URL? 14 | 15 | @State private var isLogsPresented = false 16 | 17 | var body: some View { 18 | VStack(spacing: 20) { 19 | Image(systemName: "checkmark.circle.fill") 20 | .font(.system(size: 64)) 21 | .foregroundColor(.green) 22 | 23 | Text(title) 24 | .font(.title) 25 | .bold() 26 | 27 | if logFileURL != nil { 28 | Button { 29 | isLogsPresented = true 30 | } label: { 31 | Label(NSLocalizedString("View Logs", comment: ""), 32 | systemImage: "note.text") 33 | } 34 | } 35 | } 36 | .padding() 37 | .multilineTextAlignment(.center) 38 | .sheet(isPresented: $isLogsPresented) { 39 | if let logFileURL { 40 | LogsView(url: logFileURL) 41 | } 42 | } 43 | } 44 | } 45 | 46 | #Preview { 47 | SuccessView( 48 | title: "Hello, World!", 49 | logFileURL: nil 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrollFools 2 | 3 | [now-on-havoc]: https://havoc.app/package/trollfools 4 | 5 | [][now-on-havoc] 6 | 7 | In-place tweak injection with insert_dylib and ChOma. 8 | Proudly written in SwiftUI. 9 | 10 | Expected to work on all iOS versions supported by opa334’s TrollStore (i.e. iOS 14.0 - 17.0). 11 | 12 | ## Limitations 13 | 14 | - [x] Removable system applications 15 | - [x] Decrypted App Store applications (TrollStore applications) 16 | - [x] Encrypted App Store applications with bare dynamic library 17 | 18 | ## Build 19 | 20 | See GitHub Actions for the latest build status. 21 | 22 | PRs are always welcome. 23 | 24 | ## Milestones 25 | 26 | - [x] `optool` is buggy so we need to compile a statically linked `install_name_tool` or `llvm-install-name-tool` on iOS to achieve a smaller package size. 27 | - [x] Support for `.deb` or `.zip`. 28 | 29 | ## Credits 30 | 31 | This project is inspired by [Patched-TS-App](https://github.com/34306/Patched-TS-App) by **[Huy Nguyen](https://x.com/Little_34306) and [Nathan](https://x.com/dedbeddedbed)**. 32 | 33 | - [ChOma](https://github.com/opa334/ChOma) by [@opa334](https://github.com/opa334) and [@alfiecg24](https://github.com/alfiecg24) 34 | - [MachOKit](https://github.com/p-x9/MachOKit) by [@p-x9](https://github.com/p-x9) 35 | - [insert_dylib](https://github.com/tyilo/insert_dylib) by [@tyilo](https://github.com/tyilo) 36 | 37 | ## License 38 | 39 | See [LICENSE](LICENSE). 40 | -------------------------------------------------------------------------------- /TrollFools/EjectListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EjectListModel.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | final class EjectListModel: ObservableObject { 12 | let app: App 13 | private var _injectedPlugIns: [InjectedPlugIn] = [] 14 | 15 | @Published var filter = FilterOptions() 16 | @Published var filteredPlugIns: [InjectedPlugIn] = [] 17 | 18 | private var cancellables = Set() 19 | 20 | init(_ app: App) { 21 | self.app = app 22 | reload() 23 | 24 | $filter 25 | .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) 26 | .sink { [weak self] _ in 27 | withAnimation { 28 | self?.performFilter() 29 | } 30 | } 31 | .store(in: &cancellables) 32 | } 33 | 34 | func reload() { 35 | self._injectedPlugIns = InjectorV3.main.injectedAssetURLsInBundle(app.url) 36 | .map { InjectedPlugIn(url: $0) } 37 | performFilter() 38 | } 39 | 40 | func performFilter() { 41 | var filteredPlugIns = _injectedPlugIns 42 | 43 | if !filter.searchKeyword.isEmpty { 44 | filteredPlugIns = filteredPlugIns.filter { 45 | $0.url.lastPathComponent.localizedCaseInsensitiveContains(filter.searchKeyword) 46 | } 47 | } 48 | 49 | self.filteredPlugIns = filteredPlugIns 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Backup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Backup.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension InjectorV3 { 11 | 12 | // MARK: - Constants 13 | 14 | private static let alternateSuffix = "troll-fools.bak" 15 | 16 | static func alternateURL(for target: URL) -> URL { 17 | target.appendingPathExtension(Self.alternateSuffix) 18 | } 19 | 20 | // MARK: - Shared Methods 21 | 22 | func hasAlternate(_ target: URL) -> Bool { 23 | let alternateURL = Self.alternateURL(for: target) 24 | return FileManager.default.fileExists(atPath: alternateURL.path) 25 | } 26 | 27 | func makeAlternate(_ target: URL) throws { 28 | guard !hasAlternate(target) else { 29 | return 30 | } 31 | let alternateURL = Self.alternateURL(for: target) 32 | try cmdCopy(from: target, to: alternateURL) 33 | } 34 | 35 | func removeAlternate(_ target: URL) throws { 36 | guard hasAlternate(target) else { 37 | return 38 | } 39 | let alternateURL = Self.alternateURL(for: target) 40 | try cmdRemove(alternateURL) 41 | } 42 | 43 | func restoreAlternate(_ target: URL) throws { 44 | guard hasAlternate(target) else { 45 | return 46 | } 47 | let alternateURL = Self.alternateURL(for: target) 48 | try cmdMove(from: alternateURL, to: target, overwrite: true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.bartycrouch.toml: -------------------------------------------------------------------------------- 1 | [update] 2 | tasks = ["interfaces", "code", "transform", "normalize"] 3 | 4 | [update.interfaces] 5 | paths = ["TrollFools"] 6 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 7 | defaultToBase = true 8 | ignoreEmptyStrings = false 9 | unstripped = false 10 | ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] 11 | 12 | [update.code] 13 | codePaths = ["."] 14 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 15 | localizablePaths = ["TrollFools"] 16 | defaultToKeys = false 17 | additive = false 18 | customFunction = "LocalizedStringResource" 19 | unstripped = false 20 | plistArguments = true 21 | ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] 22 | overrideComments = false 23 | 24 | [update.transform] 25 | codePaths = ["TrollFools"] 26 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 27 | localizablePaths = ["TrollFools"] 28 | transformer = "foundation" 29 | supportedLanguageEnumPath = "." 30 | typeName = "BartyCrouch" 31 | translateMethodName = "translate" 32 | separateWithEmptyLine = true 33 | 34 | [update.normalize] 35 | paths = ["TrollFools"] 36 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 37 | sourceLocale = "en" 38 | harmonizeWithSource = true 39 | sortByKeys = true 40 | separateWithEmptyLine = true 41 | 42 | [lint] 43 | paths = ["TrollFools"] 44 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 45 | duplicateKeys = true 46 | emptyValues = true -------------------------------------------------------------------------------- /TrollFools/LogsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogsView.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LogsView: UIViewControllerRepresentable { 11 | 12 | let url: URL 13 | 14 | typealias UIViewControllerType = UINavigationController 15 | 16 | func makeUIViewController(context: Context) -> UINavigationController { 17 | 18 | let viewController = StripedTextTableViewController(path: url.path) 19 | 20 | viewController.autoReload = false 21 | viewController.maximumNumberOfRows = 1000 22 | viewController.maximumNumberOfLines = 20 23 | viewController.reversed = true 24 | viewController.allowTrash = false 25 | viewController.allowSearch = true 26 | viewController.allowShare = true 27 | viewController.allowMultiline = true 28 | viewController.pullToReload = false 29 | viewController.tapToCopy = true 30 | viewController.pressToCopy = true 31 | viewController.preserveEmptyLines = false 32 | viewController.removeDuplicates = true 33 | 34 | if let regex = try? NSRegularExpression(pattern: "^\\d{4}\\/\\d{2}\\/\\d{2} \\d{2}:\\d{2}:\\d{2}:\\d{3} ") { 35 | viewController.rowPrefixRegularExpression = regex 36 | } 37 | 38 | let navController = UINavigationController(rootViewController: viewController) 39 | return navController 40 | } 41 | 42 | func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /devkit/tipa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | XCCONFIG_NAME=TrollFools/Version.xcconfig 4 | VERSION=$(awk -F "=" '/VERSION/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 5 | BUILD_NUMBER=$(awk -F "=" '/BUILD_NUMBER/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 6 | 7 | mkdir -p packages $THEOS_STAGING_DIR/Payload 8 | cp -rp $THEOS_STAGING_DIR$THEOS_PACKAGE_INSTALL_PREFIX/Applications/TrollFools.app $THEOS_STAGING_DIR/Payload 9 | chmod 0644 $THEOS_STAGING_DIR/Payload/TrollFools.app/Info.plist 10 | rm $THEOS_STAGING_DIR/Payload/TrollFools.app/ldid-14 || true 11 | 12 | cd $THEOS_STAGING_DIR 13 | cp -rp /Users/huami/Downloads/TrollFools250108/packages/TrollFools-bin/* $THEOS_STAGING_DIR/Payload/TrollFools.app/ 14 | 15 | ls -a $THEOS_STAGING_DIR/Payload 16 | 17 | 7z a -tzip -mm=LZMA TrollFools_$VERSION-$BUILD_NUMBER.tipa Payload 18 | cd - 19 | 20 | cp -p TrollFools/ldid-14 $THEOS_STAGING_DIR/Payload/TrollFools.app/ldid-14 21 | cp -rp /Users/huami/Downloads/TrollFools250108/packages/TrollFools-bin/* $THEOS_STAGING_DIR/Payload 22 | cd $THEOS_STAGING_DIR 23 | zip -qr TrollFools14_$VERSION-$BUILD_NUMBER.tipa Payload 24 | cd - 25 | 26 | cp -p $THEOS_STAGING_DIR/TrollFools_$VERSION-$BUILD_NUMBER.tipa packages/TrollFools_$VERSION-$BUILD_NUMBER@huamidev.tipa 27 | cp -p $THEOS_STAGING_DIR/TrollFools14_$VERSION-$BUILD_NUMBER.tipa packages/TrollFools14_$VERSION-$BUILD_NUMBER@huamidev.tipa 28 | # ssh root@192.168.100.222 "trollinstall -u http://192.168.31.123:5501/packages/TrollFools_$VERSION-$BUILD_NUMBER@huamidev.tipa" 29 | # sleep 4.5 30 | # ssh root@192.168.100.222 "open com.huami.TrollFools" 31 | rm -rf packages/*.deb -------------------------------------------------------------------------------- /TrollFools/FailureView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailureView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FailureView: View { 11 | 12 | let title: String 13 | let error: Error? 14 | 15 | var logFileURL: URL? { 16 | (error as? NSError)?.userInfo[NSURLErrorKey] as? URL 17 | } 18 | 19 | @State private var isLogsPresented = false 20 | 21 | var body: some View { 22 | VStack(spacing: 20) { 23 | Image(systemName: "xmark.circle.fill") 24 | .font(.system(size: 64)) 25 | .foregroundColor(.red) 26 | 27 | Text(title) 28 | .font(.title) 29 | .bold() 30 | 31 | if let error { 32 | Text(error.localizedDescription) 33 | .font(.title3) 34 | } 35 | 36 | if logFileURL != nil { 37 | Button { 38 | isLogsPresented = true 39 | } label: { 40 | Label(NSLocalizedString("View Logs", comment: ""), 41 | systemImage: "note.text") 42 | } 43 | } 44 | } 45 | .padding() 46 | .multilineTextAlignment(.center) 47 | .sheet(isPresented: $isLogsPresented) { 48 | if let logFileURL { 49 | LogsView(url: logFileURL) 50 | } 51 | } 52 | } 53 | } 54 | 55 | #Preview { 56 | FailureView( 57 | title: "Hello, World!", 58 | error: nil 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /TrollFools/OptionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionCell.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OptionCell: View { 11 | let option: Option 12 | 13 | var iconName: String { 14 | if #available(iOS 16.0, *) { 15 | option == .attach ? "syringe" : "xmark.bin" 16 | } else { 17 | option == .attach ? "tray.and.arrow.down" : "xmark.bin" 18 | } 19 | } 20 | 21 | var body: some View { 22 | VStack(spacing: 12) { 23 | ZStack { 24 | Image(systemName: iconName) 25 | .resizable() 26 | .aspectRatio(contentMode: .fit) 27 | .frame(width: 32, height: 32) 28 | .foregroundColor(option == .attach 29 | ? .accentColor : .red) 30 | .padding(.all, 40) 31 | } 32 | .background( 33 | (option == .attach ? Color.accentColor : Color.red) 34 | .opacity(0.1) 35 | .clipShape(RoundedRectangle( 36 | cornerRadius: 10, 37 | style: .continuous 38 | )) 39 | ) 40 | 41 | Text(option == .attach 42 | ? NSLocalizedString("Inject", comment: "") 43 | : NSLocalizedString("Eject", comment: "")) 44 | .font(.headline) 45 | .foregroundColor(option == .attach 46 | ? .accentColor : .red) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TrollFools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cocoalumberjack", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", 7 | "state" : { 8 | "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3", 9 | "version" : "3.8.5" 10 | } 11 | }, 12 | { 13 | "identity" : "machokit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/Lessica/MachOKit.git", 16 | "state" : { 17 | "branch" : "patch/trollfools", 18 | "revision" : "43c4b5d59ab75c91ad689fb4a39a8a3913229535" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections.git", 25 | "state" : { 26 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 27 | "version" : "1.1.4" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-log", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-log", 34 | "state" : { 35 | "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", 36 | "version" : "1.6.2" 37 | } 38 | }, 39 | { 40 | "identity" : "zipfoundation", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/weichsel/ZIPFoundation.git", 43 | "state" : { 44 | "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", 45 | "version" : "0.9.19" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /TrollFools/ViewControllerHost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerHost.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class ViewControllerHost: ObservableObject { 11 | weak var viewController: UIViewController? 12 | } 13 | 14 | extension View { 15 | func onViewWillAppear(perform onViewWillAppear: @escaping ((UIViewController) -> Void)) -> some View { 16 | modifier(VCHookViewModifier(onViewWillAppear: onViewWillAppear)) 17 | } 18 | } 19 | 20 | private final class VCHookViewController: UIViewController { 21 | var onViewWillAppear: ((UIViewController) -> Void)? 22 | var didTriggered = false 23 | 24 | override func viewWillAppear(_ animated: Bool) { 25 | super.viewWillAppear(animated) 26 | guard !didTriggered else { 27 | return 28 | } 29 | onViewWillAppear?(self) 30 | didTriggered = true 31 | } 32 | } 33 | 34 | private struct VCHookView: UIViewControllerRepresentable { 35 | typealias UIViewControllerType = VCHookViewController 36 | let onViewWillAppear: ((UIViewController) -> Void) 37 | 38 | func makeUIViewController(context: Context) -> VCHookViewController { 39 | let vc = VCHookViewController() 40 | vc.onViewWillAppear = onViewWillAppear 41 | return vc 42 | } 43 | 44 | func updateUIViewController(_ uiViewController: VCHookViewController, context: Context) { } 45 | } 46 | 47 | private struct VCHookViewModifier: ViewModifier { 48 | let onViewWillAppear: ((UIViewController) -> Void) 49 | 50 | func body(content: Content) -> some View { 51 | content.background(VCHookView(onViewWillAppear: onViewWillAppear)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /TrollFools/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | let app: App 12 | 13 | init(_ app: App) { 14 | self.app = app 15 | self._useWeakReference = AppStorage(wrappedValue: true, "UseWeakReference-\(app.id)") 16 | self._preferMainExecutable = AppStorage(wrappedValue: false, "PreferMainExecutable-\(app.id)") 17 | self._injectStrategy = AppStorage(wrappedValue: .lexicographic, "InjectStrategy-\(app.id)") 18 | } 19 | 20 | @AppStorage var useWeakReference: Bool 21 | @AppStorage var preferMainExecutable: Bool 22 | @AppStorage var injectStrategy: InjectorV3.Strategy 23 | 24 | var body: some View { 25 | NavigationView { 26 | Form { 27 | Section { 28 | Picker(NSLocalizedString("Injection Strategy", comment: ""), selection: $injectStrategy) { 29 | ForEach(InjectorV3.Strategy.allCases, id: \.self) { strategy in 30 | Text(strategy.localizedDescription).tag(strategy) 31 | } 32 | } 33 | Toggle(NSLocalizedString("Prefer Main Executable", comment: ""), isOn: $preferMainExecutable) 34 | Toggle(NSLocalizedString("Use Weak Reference", comment: ""), isOn: $useWeakReference) 35 | } header: { 36 | Text(NSLocalizedString("Injection", comment: "")) 37 | } footer: { 38 | Text(NSLocalizedString("If you do not know what these options mean, please do not change them.", comment: "")) 39 | } 40 | } 41 | .navigationTitle(NSLocalizedString("Advanced Settings", comment: "")) 42 | .navigationBarTitleDisplayMode(.inline) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TrollFools/StripedTextTableViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // StripedTextTableViewController.h 3 | // CommonViewControllers 4 | // 5 | // Created by Lessica <82flex@gmail.com> on 2022/1/20. 6 | // Copyright © 2022 Zheng Wu. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class StripedTextTableViewController; 14 | 15 | @protocol StripedTextTableViewControllerDelegate 16 | @optional 17 | - (void)stripedTextTableViewRowDidCopy:(StripedTextTableViewController *)controller withText:(NSString *)text; 18 | @end 19 | 20 | @interface StripedTextTableViewController : UITableViewController 21 | 22 | @property (nonatomic, weak) id delegate; 23 | 24 | - (instancetype)initWithPath:(NSString *)path; 25 | @property (nonatomic, copy, readonly) NSString *entryPath; 26 | 27 | @property (nonatomic, assign) BOOL autoReload; 28 | 29 | @property (nonatomic, assign) BOOL reversed; 30 | @property (nonatomic, assign) BOOL removeDuplicates; 31 | @property (nonatomic, assign) BOOL allowTrash; 32 | @property (nonatomic, assign) BOOL allowSearch; 33 | @property (nonatomic, assign) BOOL allowShare; 34 | @property (nonatomic, assign) BOOL pullToReload; 35 | @property (nonatomic, assign) BOOL tapToCopy; 36 | @property (nonatomic, assign) BOOL pressToCopy; 37 | @property (nonatomic, assign) BOOL preserveEmptyLines; 38 | 39 | @property (nonatomic, assign) CGFloat rowHeight; 40 | @property (nonatomic, assign) BOOL allowMultiline; 41 | @property (nonatomic, assign) NSLineBreakMode lineBreakMode; 42 | 43 | @property (nonatomic, assign) NSUInteger maximumNumberOfLines; // default is 0, unlimited 44 | @property (nonatomic, assign) NSUInteger maximumNumberOfRows; // default is 0, unlimited 45 | 46 | @property (nonatomic, copy) NSString *rowSeparator; 47 | @property (nonatomic, copy) NSRegularExpression *rowPrefixRegularExpression; 48 | 49 | @end 50 | 51 | NS_ASSUME_NONNULL_END 52 | -------------------------------------------------------------------------------- /TrollFools/Execute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Execute.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | import Foundation 10 | 11 | enum Execute { 12 | 13 | @discardableResult 14 | static func rootSpawn( 15 | binary: String, 16 | arguments: [String] = [], 17 | environment: [String: String] = [:], 18 | ddlog: DDLog = .sharedInstance 19 | ) throws -> AuxiliaryExecute.TerminationReason { 20 | let receipt = AuxiliaryExecute.spawn( 21 | command: binary, 22 | args: arguments, 23 | environment: environment.merging([ 24 | "DISABLE_TWEAKS": "1", 25 | ], uniquingKeysWith: { $1 }), 26 | personaOptions: .init(uid: 0, gid: 0), 27 | ddlog: ddlog 28 | ) 29 | if !receipt.stdout.isEmpty { 30 | DDLogVerbose("Process \(receipt.pid) output: \(receipt.stdout)", ddlog: ddlog) 31 | } 32 | if !receipt.stderr.isEmpty { 33 | DDLogVerbose("Process \(receipt.pid) error: \(receipt.stderr)", ddlog: ddlog) 34 | } 35 | return receipt.terminationReason 36 | } 37 | 38 | static func rootSpawnWithOutputs( 39 | binary: String, 40 | arguments: [String] = [], 41 | environment: [String: String] = [:], 42 | ddlog: DDLog = .sharedInstance 43 | ) throws -> AuxiliaryExecute.ExecuteReceipt { 44 | let receipt = AuxiliaryExecute.spawn( 45 | command: binary, 46 | args: arguments, 47 | environment: environment.merging([ 48 | "DISABLE_TWEAKS": "1", 49 | ], uniquingKeysWith: { $1 }), 50 | personaOptions: .init(uid: 0, gid: 0), 51 | ddlog: ddlog 52 | ) 53 | if !receipt.stdout.isEmpty { 54 | DDLogVerbose("Process \(receipt.pid) output: \(receipt.stdout)", ddlog: ddlog) 55 | } 56 | if !receipt.stderr.isEmpty { 57 | DDLogVerbose("Process \(receipt.pid) error: \(receipt.stderr)", ddlog: ddlog) 58 | } 59 | return receipt 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Metadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Metadata.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension InjectorV3 { 11 | 12 | fileprivate static let metadataPlistName = "iTunesMetadata.plist" 13 | fileprivate static let metadataPlistBackupName = "\(metadataPlistName).bak" 14 | 15 | // MARK: - Instance Methods 16 | 17 | var isMetadataDetached: Bool { isMetadataDetachedInBundle(bundleURL) } 18 | 19 | func setMetadataDetached(_ detached: Bool) throws { 20 | let containerURL = bundleURL.deletingLastPathComponent() 21 | 22 | let metaURL = containerURL.appendingPathComponent(Self.metadataPlistName) 23 | let metaBackupURL = containerURL.appendingPathComponent(Self.metadataPlistBackupName) 24 | 25 | if detached && !isMetadataDetached { 26 | try? cmdMove(from: metaURL, to: metaBackupURL, overwrite: false) 27 | } 28 | 29 | if !detached && isMetadataDetached { 30 | try? cmdMove(from: metaBackupURL, to: metaURL, overwrite: false) 31 | } 32 | } 33 | 34 | // MARK: - Shared Methods 35 | 36 | func isMetadataDetachedInBundle(_ target: URL) -> Bool { 37 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 38 | 39 | let containerURL = target.deletingLastPathComponent() 40 | let metaBackupURL = containerURL.appendingPathComponent(Self.metadataPlistBackupName) 41 | 42 | return FileManager.default.fileExists(atPath: metaBackupURL.path) 43 | } 44 | 45 | func isAllowedToAttachOrDetachMetadataInBundle(_ target: URL) -> Bool { 46 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 47 | 48 | let containerURL = target.deletingLastPathComponent() 49 | 50 | let metaURL = containerURL.appendingPathComponent(Self.metadataPlistName) 51 | let metaBackupURL = containerURL.appendingPathComponent(Self.metadataPlistBackupName) 52 | 53 | return FileManager.default.fileExists(atPath: metaURL.path) || FileManager.default.fileExists(atPath: metaBackupURL.path) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Compile 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | env: 10 | THEOS: "" 11 | VERSION: "0.0.0-0" 12 | 13 | jobs: 14 | fetch-and-release: 15 | runs-on: macos-14 16 | steps: 17 | - name: Setup Xcode 18 | uses: maxim-lobanov/setup-xcode@v1 19 | with: 20 | xcode-version: '15.4' 21 | 22 | - name: Checkout Repository 23 | uses: actions/checkout@v4.1.1 24 | 25 | - name: Install Dependencies 26 | run: | 27 | echo /usr/local/opt/make/libexec/gnubin >> $GITHUB_PATH 28 | echo /opt/homebrew/opt/make/libexec/gnubin >> $GITHUB_PATH 29 | HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install dpkg ldid-procursus make xcbeautify openssl@3 30 | VERSION_FILE="TrollFools/Version.xcconfig" 31 | VERSION_MAIN=$(grep "VERSION" $VERSION_FILE | cut -d'=' -f2 | tr -d '[:space:]') 32 | VERSION_BUILD=$(grep "BUILD_NUMBER" $VERSION_FILE | cut -d'=' -f2 | tr -d '[:space:]') 33 | VERSION=$VERSION_MAIN-$VERSION_BUILD 34 | echo "VERSION=$VERSION" >> $GITHUB_ENV 35 | echo "Version: $VERSION" 36 | 37 | - name: Checkout THEOS 38 | uses: actions/checkout@v4.1.1 39 | with: 40 | repository: theos/theos 41 | ref: 942cd0c015f93d8c2d714ba0c69e7f420157c382 42 | path: theos 43 | submodules: recursive 44 | 45 | - name: Add THEOS environment variables 46 | run: | 47 | rm -rf $GITHUB_WORKSPACE/theos/sdks 48 | echo "THEOS=$GITHUB_WORKSPACE/theos" >> $GITHUB_ENV 49 | echo "THEOS_PACKAGE_SCHEME=rootless" >> $GITHUB_ENV 50 | echo "FINALPACKAGE=1" >> $GITHUB_ENV 51 | 52 | - name: Checkout SDKs 53 | uses: actions/checkout@v4.1.1 54 | with: 55 | repository: theos/sdks 56 | ref: master 57 | path: ${{ env.THEOS }}/sdks 58 | 59 | - name: Build Packages 60 | run: | 61 | make package 62 | cp packages/*.tipa . 63 | find .theos/obj -name "*.dSYM" -exec cp -r {} . \; 64 | -------------------------------------------------------------------------------- /TrollFools/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import Foundation 9 | 10 | final class App: Identifiable, ObservableObject { 11 | let id: String 12 | let name: String 13 | let type: String 14 | let teamID: String 15 | let url: URL 16 | let version: String? 17 | let dataurl: URL 18 | 19 | 20 | @Published var isDetached: Bool = false 21 | @Published var isAllowedToAttachOrDetach: Bool 22 | @Published var isInjected: Bool = false 23 | 24 | lazy var icon: UIImage? = UIImage._applicationIconImage(forBundleIdentifier: id, format: 0, scale: 3.0) 25 | var alternateIcon: UIImage? 26 | 27 | lazy var isUser: Bool = type == "User" 28 | lazy var isSystem: Bool = !isUser 29 | lazy var isFromApple: Bool = id.hasPrefix("com.apple.") 30 | lazy var isFromTroll: Bool = isSystem && !isFromApple 31 | lazy var isRemovable: Bool = url.path.contains("/var/containers/Bundle/Application/") 32 | 33 | weak var appList: AppListModel? 34 | 35 | init( 36 | id: String, 37 | name: String, 38 | type: String, 39 | teamID: String, 40 | url: URL, 41 | version: String? = nil, 42 | alternateIcon: UIImage? = nil, 43 | dataurl: URL 44 | ) { 45 | self.id = id 46 | self.name = name 47 | self.type = type 48 | self.teamID = teamID 49 | self.url = url 50 | self.version = version 51 | self.isDetached = InjectorV3.main.isMetadataDetachedInBundle(url) 52 | self.isAllowedToAttachOrDetach = type == "User" && InjectorV3.main.isAllowedToAttachOrDetachMetadataInBundle(url) 53 | self.isInjected = InjectorV3.main.checkIsInjectedAppBundle(url) 54 | self.alternateIcon = alternateIcon 55 | self.dataurl = dataurl 56 | } 57 | 58 | func reload() { 59 | reloadDetachedStatus() 60 | reloadInjectedStatus() 61 | } 62 | 63 | private func reloadDetachedStatus() { 64 | self.isDetached = InjectorV3.main.isMetadataDetachedInBundle(url) 65 | self.isAllowedToAttachOrDetach = isUser && InjectorV3.main.isAllowedToAttachOrDetachMetadataInBundle(url) 66 | } 67 | 68 | private func reloadInjectedStatus() { 69 | self.isInjected = InjectorV3.main.checkIsInjectedAppBundle(url) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - release 8 | 9 | env: 10 | THEOS: "" 11 | VERSION: "0.0.0-0" 12 | 13 | jobs: 14 | fetch-and-release: 15 | runs-on: macos-14 16 | steps: 17 | - name: Setup Xcode 18 | uses: maxim-lobanov/setup-xcode@v1 19 | with: 20 | xcode-version: '15.4' 21 | 22 | - name: Checkout Repository 23 | uses: actions/checkout@v4.1.1 24 | 25 | - name: Install Dependencies 26 | run: | 27 | echo /usr/local/opt/make/libexec/gnubin >> $GITHUB_PATH 28 | echo /opt/homebrew/opt/make/libexec/gnubin >> $GITHUB_PATH 29 | HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install dpkg ldid-procursus make xcbeautify openssl@3 30 | VERSION_FILE="TrollFools/Version.xcconfig" 31 | VERSION_MAIN=$(grep "VERSION" $VERSION_FILE | cut -d'=' -f2 | tr -d '[:space:]') 32 | VERSION_BUILD=$(grep "BUILD_NUMBER" $VERSION_FILE | cut -d'=' -f2 | tr -d '[:space:]') 33 | VERSION=$VERSION_MAIN-$VERSION_BUILD 34 | echo "VERSION=$VERSION" >> $GITHUB_ENV 35 | echo "Version: $VERSION" 36 | 37 | - name: Checkout THEOS 38 | uses: actions/checkout@v4.1.1 39 | with: 40 | repository: theos/theos 41 | ref: 942cd0c015f93d8c2d714ba0c69e7f420157c382 42 | path: theos 43 | submodules: recursive 44 | 45 | - name: Add THEOS environment variables 46 | run: | 47 | rm -rf $GITHUB_WORKSPACE/theos/sdks 48 | echo "THEOS=$GITHUB_WORKSPACE/theos" >> $GITHUB_ENV 49 | echo "THEOS_PACKAGE_SCHEME=rootless" >> $GITHUB_ENV 50 | echo "FINALPACKAGE=1" >> $GITHUB_ENV 51 | 52 | - name: Checkout SDKs 53 | uses: actions/checkout@v4.1.1 54 | with: 55 | repository: theos/sdks 56 | ref: master 57 | path: ${{ env.THEOS }}/sdks 58 | 59 | - name: Build Packages 60 | run: | 61 | make package 62 | cp packages/*.tipa . 63 | find .theos/obj -name "*.dSYM" -exec cp -r {} . \; 64 | 65 | - name: Upload Packages 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: packages-${{ env.VERSION }} 69 | path: | 70 | *.tipa 71 | *.dSYM 72 | 73 | - name: Release 74 | uses: softprops/action-gh-release@v0.1.15 75 | with: 76 | tag_name: v${{ env.VERSION }} 77 | body_path: CHANGELOG.md 78 | draft: false 79 | prerelease: false 80 | files: | 81 | *.tipa 82 | -------------------------------------------------------------------------------- /TrollFools/PlugInCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlugInCell.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private let gDateFormatter: DateFormatter = { 11 | let formatter = DateFormatter() 12 | formatter.dateStyle = .medium 13 | formatter.timeStyle = .short 14 | return formatter 15 | }() 16 | 17 | struct PlugInCell: View { 18 | @EnvironmentObject var ejectList: EjectListModel 19 | 20 | let plugIn: InjectedPlugIn 21 | 22 | @available(iOS 15.0, *) 23 | var highlightedName: AttributedString { 24 | let name = plugIn.url.lastPathComponent 25 | var attributedString = AttributedString(name) 26 | if let range = attributedString.range(of: ejectList.filter.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) { 27 | attributedString[range].foregroundColor = .accentColor 28 | } 29 | return attributedString 30 | } 31 | 32 | var iconName: String { 33 | let pathExt = plugIn.url.pathExtension.lowercased() 34 | if pathExt == "bundle" { 35 | return "archivebox" 36 | } 37 | if pathExt == "dylib" { 38 | return "bandage" 39 | } 40 | if pathExt == "framework" { 41 | return "shippingbox" 42 | } 43 | return "puzzlepiece" 44 | } 45 | 46 | var body: some View { 47 | HStack(spacing: 12) { 48 | Image(systemName: iconName) 49 | .resizable() 50 | .aspectRatio(contentMode: .fit) 51 | .frame(width: 24, height: 24) 52 | .foregroundColor(.accentColor) 53 | 54 | VStack(alignment: .leading) { 55 | if #available(iOS 15.0, *) { 56 | Text(highlightedName) 57 | .font(.headline) 58 | } else { 59 | Text(plugIn.url.lastPathComponent) 60 | .font(.headline) 61 | } 62 | 63 | Text(gDateFormatter.string(from: plugIn.createdAt)) 64 | .font(.subheadline) 65 | } 66 | } 67 | .contextMenu { 68 | Button { 69 | openInFilza() 70 | } label: { 71 | if isFilzaInstalled { 72 | Label(NSLocalizedString("Show in Filza", comment: ""), systemImage: "scope") 73 | } else { 74 | Label(NSLocalizedString("Filza (URL Scheme) Not Installed", comment: ""), systemImage: "xmark.octagon") 75 | } 76 | } 77 | .disabled(!isFilzaInstalled) 78 | } 79 | } 80 | 81 | var isFilzaInstalled: Bool { ejectList.app.appList?.isFilzaInstalled ?? false } 82 | 83 | private func openInFilza() { 84 | ejectList.app.appList?.openInFilza(plugIn.url) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /TrollFools/TrollFoolsStub.m: -------------------------------------------------------------------------------- 1 | // 2 | // TrollFoolsStub.m 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | #import 9 | 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | FOUNDATION_EXTERN void TFUtilKillAll(NSString *processPath, BOOL softly); 16 | 17 | static void TFUtilEnumerateProcessesUsingBlock(void (^enumerator)(pid_t pid, NSString *executablePath, BOOL *stop)) { 18 | 19 | static int kMaximumArgumentSize = 0; 20 | static dispatch_once_t onceToken; 21 | dispatch_once(&onceToken, ^{ 22 | size_t valSize = sizeof(kMaximumArgumentSize); 23 | if (sysctl((int[]){CTL_KERN, KERN_ARGMAX}, 2, &kMaximumArgumentSize, &valSize, NULL, 0) < 0) { 24 | perror("sysctl argument size"); 25 | kMaximumArgumentSize = 4096; 26 | } 27 | }); 28 | 29 | size_t procInfoLength = 0; 30 | if (sysctl((int[]){CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}, 3, NULL, &procInfoLength, NULL, 0) < 0) { 31 | return; 32 | } 33 | 34 | static struct kinfo_proc *procInfo = NULL; 35 | procInfo = (struct kinfo_proc *)realloc(procInfo, procInfoLength + 1); 36 | if (!procInfo) { 37 | return; 38 | } 39 | 40 | bzero(procInfo, procInfoLength + 1); 41 | if (sysctl((int[]){CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}, 3, procInfo, &procInfoLength, NULL, 0) < 0) { 42 | return; 43 | } 44 | 45 | static char *argBuffer = NULL; 46 | int procInfoCnt = (int)(procInfoLength / sizeof(struct kinfo_proc)); 47 | for (int i = 0; i < procInfoCnt; i++) { 48 | 49 | pid_t pid = procInfo[i].kp_proc.p_pid; 50 | if (pid <= 1) { 51 | continue; 52 | } 53 | 54 | size_t argSize = kMaximumArgumentSize; 55 | if (sysctl((int[]){CTL_KERN, KERN_PROCARGS2, pid, 0}, 3, NULL, &argSize, NULL, 0) < 0) { 56 | continue; 57 | } 58 | 59 | argBuffer = (char *)realloc(argBuffer, argSize + 1); 60 | if (!argBuffer) { 61 | continue; 62 | } 63 | 64 | bzero(argBuffer, argSize + 1); 65 | if (sysctl((int[]){CTL_KERN, KERN_PROCARGS2, pid, 0}, 3, argBuffer, &argSize, NULL, 0) < 0) { 66 | continue; 67 | } 68 | 69 | BOOL stop = NO; 70 | @autoreleasepool { 71 | enumerator(pid, [NSString stringWithUTF8String:(argBuffer + sizeof(int))], &stop); 72 | } 73 | 74 | if (stop) { 75 | break; 76 | } 77 | } 78 | } 79 | 80 | void TFUtilKillAll(NSString *processName, BOOL softly) { 81 | TFUtilEnumerateProcessesUsingBlock(^(pid_t pid, NSString *executablePath, BOOL *stop) { 82 | if ([executablePath containsString:[NSString stringWithFormat:@"/%@.app/%@", processName, processName]]) { 83 | if (softly) { 84 | kill(pid, SIGTERM); 85 | } else { 86 | kill(pid, SIGKILL); 87 | } 88 | } 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Eject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Eject.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | 10 | 11 | extension InjectorV3 { 12 | 13 | // MARK: - Instance Methods 14 | 15 | func ejectAll() throws { 16 | try eject(injectedAssetURLsInBundle(bundleURL)) 17 | } 18 | 19 | func eject(_ assetURLs: [URL]) throws { 20 | precondition(!assetURLs.isEmpty, "No asset to eject.") 21 | terminateApp() 22 | 23 | try ejectBundles(assetURLs 24 | .filter { $0.pathExtension.lowercased() == "bundle" }) 25 | 26 | try ejectDylibsAndFrameworks(assetURLs 27 | .filter { $0.pathExtension.lowercased() == "dylib" || $0.pathExtension.lowercased() == "framework" }) 28 | } 29 | 30 | // MARK: - Private Methods 31 | 32 | fileprivate func ejectBundles(_ assetURLs: [URL]) throws { 33 | guard !assetURLs.isEmpty else { 34 | return 35 | } 36 | 37 | for assetURL in assetURLs { 38 | guard checkIsInjectedBundle(assetURL) else { 39 | continue 40 | } 41 | 42 | try? cmdRemove(assetURL, recursively: true) 43 | } 44 | } 45 | 46 | fileprivate func ejectDylibsAndFrameworks(_ assetURLs: [URL]) throws { 47 | guard !assetURLs.isEmpty else { 48 | return 49 | } 50 | 51 | let targetURLs = try collectModifiedMachOs() 52 | guard !targetURLs.isEmpty else { 53 | DDLogError("Unable to find any modified Mach-Os", ddlog: logger) 54 | throw Error.generic(NSLocalizedString("No eligible framework found.", comment: "")) 55 | } 56 | 57 | DDLogInfo("Modified Mach-Os \(targetURLs.map { $0.path })", ddlog: logger) 58 | 59 | for assetURL in assetURLs { 60 | try targetURLs.forEach { 61 | try removeLoadCommandOfAsset(assetURL, from: $0) 62 | } 63 | try? cmdRemove(assetURL, recursively: checkIsDirectory(assetURL)) 64 | } 65 | 66 | try targetURLs.forEach { 67 | try cmdCoreTrustBypass($0, teamID: teamID) 68 | try cmdChangeOwnerToInstalld($0) 69 | } 70 | 71 | if !hasInjectedAsset { 72 | try targetURLs.forEach { try restoreAlternate($0) } 73 | 74 | let substrateFwkURL = bundleURL.appendingPathComponent("Frameworks/\(Self.substrateFwkName)", isDirectory: true) 75 | try? cmdRemove(substrateFwkURL, recursively: true) 76 | } 77 | } 78 | 79 | fileprivate func collectModifiedMachOs() throws -> [URL] { 80 | try frameworkMachOsInBundle(bundleURL) 81 | .filter { hasAlternate($0) }.elements 82 | } 83 | 84 | // MARK: - Load Commands 85 | 86 | fileprivate func removeLoadCommandOfAsset(_ assetURL: URL, from target: URL) throws { 87 | let name = try loadCommandNameOfAsset(assetURL) 88 | try cmdRemoveLoadCommandDylib(target, name: name) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /devkit/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is designed to increment the build number consistently across all 4 | # targets. 5 | 6 | # Usage: bump-version.sh 7 | # Example: bump-version.sh 1.0 8 | 9 | # Usage: DEBUG=1 bump-version.sh 10 | # Example: DEBUG=1 bump-version.sh 1.0 11 | 12 | set -e 13 | cd "$(dirname "$0")" 14 | 15 | if [ $# -ne 1 ]; then 16 | echo "Usage: $0 " 17 | exit 1 18 | fi 19 | 20 | VERSION=$1 21 | 22 | if [ ! -z "$DEBUG" ]; then 23 | 24 | # Navigating to the 'carbonwatchuk' directory inside the source root. 25 | XCCONFIG_NAME=../TrollFools/Version.Debug.xcconfig 26 | if [ ! -f $XCCONFIG_NAME ]; then 27 | echo "Versioning configuration not found!" 28 | exit 1 29 | fi 30 | 31 | # Get the current date in the format "YYYYMMDD". 32 | current_date=$(date "+%Y%m%d") 33 | 34 | # Parse the 'Config.xcconfig' file to retrieve the previous build number. 35 | # The 'awk' command is used to find the line containing "BUILD_NUMBER" 36 | # and the 'tr' command is used to remove any spaces. 37 | previous_build_number=$(awk -F "=" '/DEBUG_BUILD_NUMBER/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 38 | 39 | # Extract the date part and the counter part from the previous build number. 40 | previous_date="${previous_build_number:0:8}" 41 | counter="${previous_build_number:8}" 42 | 43 | # If the current date matches the date from the previous build number, 44 | # increment the counter. Otherwise, reset the counter to 1. 45 | new_counter=$((current_date == previous_date ? counter + 1 : 1)) 46 | 47 | # Combine the current date and the new counter to create the new build number. 48 | new_build_number="${current_date}${new_counter}" 49 | 50 | # Use 'sed' command to replace the previous build number with the new build 51 | # number in the 'Config.xcconfig' file. 52 | sed -i -e "/DEBUG_VERSION =/ s/= .*/= $VERSION/" $XCCONFIG_NAME 53 | sed -i -e "/DEBUG_BUILD_NUMBER =/ s/= .*/= $new_build_number/" $XCCONFIG_NAME 54 | 55 | # Remove the backup file created by 'sed' command. 56 | rm -f $XCCONFIG_NAME-e 57 | 58 | else 59 | 60 | XCCONFIG_NAME=../TrollFools/Version.xcconfig 61 | if [ ! -f $XCCONFIG_NAME ]; then 62 | echo "Versioning configuration not found!" 63 | exit 1 64 | fi 65 | 66 | previous_build_number=$(awk -F "=" '/BUILD_NUMBER/ {print $2}' $XCCONFIG_NAME | tr -d ' ') 67 | 68 | new_build_number=$((previous_build_number + 1)) 69 | 70 | sed -i -e "/VERSION =/ s/= .*/= $VERSION/" $XCCONFIG_NAME 71 | sed -i -e "/BUILD_NUMBER =/ s/= .*/= $new_build_number/" $XCCONFIG_NAME 72 | 73 | rm -f $XCCONFIG_NAME-e 74 | 75 | fi 76 | 77 | # Create the layout directory 78 | mkdir -p ../layout/DEBIAN 79 | 80 | # Write the control file 81 | cat > ../layout/DEBIAN/control << __EOF__ 82 | Package: wiki.qaq.trollfools 83 | Name: TrollFools 84 | Version: $VERSION-$new_build_number 85 | Section: Applications 86 | Depends: firmware (>= 14.0) 87 | Architecture: iphoneos-arm 88 | Author: Lessica <82flex@gmail.com> 89 | Maintainer: Lessica <82flex@gmail.com> 90 | Description: Give me 108 yuan. 91 | __EOF__ 92 | 93 | # Set permissions 94 | chmod 0644 ../layout/DEBIAN/control 95 | -------------------------------------------------------------------------------- /TrollFools/OptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OptionView: View { 11 | let app: App 12 | 13 | @State var isImporterPresented = false 14 | @State var isImporterSelected = false 15 | 16 | @State var isSettingsPresented = false 17 | 18 | @State var importerResult: Result<[URL], any Error>? 19 | 20 | init(_ app: App) { 21 | self.app = app 22 | } 23 | 24 | var body: some View { 25 | VStack(spacing: 80) { 26 | HStack { 27 | Spacer() 28 | 29 | Button { 30 | isImporterPresented = true 31 | } label: { 32 | OptionCell(option: .attach) 33 | } 34 | .accessibilityLabel(NSLocalizedString("Inject", comment: "")) 35 | 36 | Spacer() 37 | 38 | NavigationLink { 39 | EjectListView(app) 40 | } label: { 41 | OptionCell(option: .detach) 42 | } 43 | .accessibilityLabel(NSLocalizedString("Eject", comment: "")) 44 | 45 | Spacer() 46 | } 47 | 48 | Button { 49 | isSettingsPresented = true 50 | } label: { 51 | Label(NSLocalizedString("Advanced Settings", comment: ""), 52 | systemImage: "gear") 53 | } 54 | } 55 | .padding() 56 | .navigationTitle(app.name) 57 | .background(Group { 58 | NavigationLink(isActive: $isImporterSelected) { 59 | if let result = importerResult { 60 | switch result { 61 | case .success(let urls): 62 | InjectView(app, urlList: urls 63 | .sorted(by: { $0.lastPathComponent < $1.lastPathComponent })) 64 | case .failure(let error): 65 | FailureView( 66 | title: NSLocalizedString("Error", comment: ""), 67 | error: error 68 | ) 69 | } 70 | } 71 | } label: { } 72 | }) 73 | .fileImporter( 74 | isPresented: $isImporterPresented, 75 | allowedContentTypes: [ 76 | .init(filenameExtension: "dylib")!, 77 | .init(filenameExtension: "deb")!, 78 | .bundle, 79 | .framework, 80 | .package, 81 | .zip, 82 | ], 83 | allowsMultipleSelection: true 84 | ) { 85 | result in 86 | importerResult = result 87 | isImporterSelected = true 88 | } 89 | .sheet(isPresented: $isSettingsPresented) { 90 | if #available(iOS 16.0, *) { 91 | SettingsView(app) 92 | .presentationDetents([.medium, .large]) 93 | } else { 94 | SettingsView(app) 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /TrollFools.xcodeproj/xcshareddata/xcschemes/TrollFools.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /TrollFools/TrollFools.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | application-identifier 6 | 6LMF23FAGL.com.huami.TrollFools 7 | com.apple.CommCenter.fine-grained 8 | 9 | spi 10 | identity 11 | 12 | com.apple.developer.icloud-container-identifiers 13 | 14 | 6LMF23FAGL.com.huami.TrollFools.TrollFools.icloud-container 15 | 16 | com.apple.developer.icloud-services 17 | 18 | CloudDocuments 19 | 20 | com.apple.developer.notificationcenter-identifiers 21 | 22 | com.apple.developer.team-identifier 23 | 6LMF23FAGL 24 | com.apple.developer.ubiquity-container-identifiers 25 | 26 | 6LMF23FAGL.com.huami.TrollFoolsTrollFools.icloud-container 27 | 28 | com.apple.private.persona-mgmt 29 | 30 | com.apple.private.security.container-manager 31 | 32 | com.apple.private.security.container-required 33 | 34 | com.apple.private.security.disk-device-access 35 | 36 | com.apple.private.security.no-container 37 | 38 | com.apple.private.security.storage.AppBundles 39 | 40 | com.apple.private.security.storage.AppDataContainers 41 | 42 | com.apple.private.security.storage.CloudDocsDB 43 | 44 | com.apple.private.security.storage.CloudKit 45 | 46 | com.apple.private.security.storage.DocumentRevisions 47 | 48 | com.apple.private.security.storage.MobileDocuments 49 | 50 | com.apple.private.security.storage.ciconia 51 | 52 | com.apple.private.security.storage.iCloudDrive 53 | 54 | com.apple.private.tcc.allow 55 | 56 | kTCCServiceUbiquity 57 | kTCCServiceSystemPolicyDesktopFolder 58 | kTCCServiceSystemPolicyDocumentsFolder 59 | kTCCServiceSystemPolicyDownloadsFolder 60 | 61 | com.apple.private.tcc.manager 62 | 63 | com.apple.private.tcc.manager.check-by-audit-token 64 | 65 | kTCCServiceUbiquity 66 | kTCCServiceSystemPolicyDesktopFolder 67 | kTCCServiceSystemPolicyDocumentsFolder 68 | kTCCServiceSystemPolicyDownloadsFolder 69 | 70 | com.apple.security.exception.files.absolute-path.read-write 71 | 72 | / 73 | 74 | com.apple.security.exception.files.home-relative-path.read-write 75 | 76 | /Library/CloudStorage/ 77 | /Library/Mobile Documents/ 78 | 79 | com.apple.springboard.iconState 80 | 81 | com.apple.springboard.iconState.mutate 82 | 83 | com.apple.springboard.launchapplications 84 | 85 | com.apple.springboard.launchapplicationswithoptions 86 | 87 | com.apple.springboard.opensensitiveurl 88 | 89 | com.apple.springboard.openurlswhenlocked 90 | 91 | file-read-data 92 | 93 | keychain-access-groups 94 | 95 | com.huami.TrollFools 96 | 6LMF23FAGL.com.huami.TrollFools 97 | 98 | platform-application 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .theos 2 | packages 3 | devkit 4 | compile_commands.json 5 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos,xcode 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos,xcode 7 | 8 | ### macOS ### 9 | # General 10 | .DS_Store 11 | .AppleDouble 12 | .LSOverride 13 | 14 | # Icon must end with two \r 15 | Icon 16 | 17 | # Thumbnails 18 | ._* 19 | 20 | # Files that might appear in the root of a volume 21 | .DocumentRevisions-V100 22 | .fseventsd 23 | .Spotlight-V100 24 | .TemporaryItems 25 | .Trashes 26 | .VolumeIcon.icns 27 | .com.apple.timemachine.donotpresent 28 | 29 | # Directories potentially created on remote AFP share 30 | .AppleDB 31 | .AppleDesktop 32 | Network Trash Folder 33 | Temporary Items 34 | .apdisk 35 | 36 | ### macOS Patch ### 37 | # iCloud generated files 38 | *.icloud 39 | 40 | ### Swift ### 41 | # Xcode 42 | # 43 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 44 | 45 | ## User settings 46 | xcuserdata/ 47 | 48 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 49 | *.xcscmblueprint 50 | *.xccheckout 51 | 52 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 53 | build/ 54 | DerivedData/ 55 | *.moved-aside 56 | *.pbxuser 57 | !default.pbxuser 58 | *.mode1v3 59 | !default.mode1v3 60 | *.mode2v3 61 | !default.mode2v3 62 | *.perspectivev3 63 | !default.perspectivev3 64 | 65 | ## Obj-C/Swift specific 66 | *.hmap 67 | 68 | ## App packaging 69 | *.ipa 70 | *.dSYM.zip 71 | *.dSYM 72 | 73 | ## Playgrounds 74 | timeline.xctimeline 75 | playground.xcworkspace 76 | 77 | # Swift Package Manager 78 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 79 | # Packages/ 80 | # Package.pins 81 | # Package.resolved 82 | # *.xcodeproj 83 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 84 | # hence it is not needed unless you have added a package configuration file to your project 85 | # .swiftpm 86 | 87 | .build/ 88 | 89 | # CocoaPods 90 | # We recommend against adding the Pods directory to your .gitignore. However 91 | # you should judge for yourself, the pros and cons are mentioned at: 92 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 93 | # Pods/ 94 | # Add this line if you want to avoid checking in source code from the Xcode workspace 95 | # *.xcworkspace 96 | 97 | # Carthage 98 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 99 | # Carthage/Checkouts 100 | 101 | Carthage/Build/ 102 | 103 | # Accio dependency management 104 | Dependencies/ 105 | .accio/ 106 | 107 | # fastlane 108 | # It is recommended to not store the screenshots in the git repo. 109 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 110 | # For more information about the recommended setup visit: 111 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 112 | 113 | fastlane/report.xml 114 | fastlane/Preview.html 115 | fastlane/screenshots/**/*.png 116 | fastlane/test_output 117 | 118 | # Code Injection 119 | # After new code Injection tools there's a generated folder /iOSInjectionProject 120 | # https://github.com/johnno1962/injectionforxcode 121 | 122 | iOSInjectionProject/ 123 | 124 | ### Xcode ### 125 | 126 | ## Xcode 8 and earlier 127 | 128 | ### Xcode Patch ### 129 | *.xcodeproj/* 130 | !*.xcodeproj/project.pbxproj 131 | !*.xcodeproj/xcshareddata/ 132 | !*.xcodeproj/project.xcworkspace/ 133 | !*.xcworkspace/contents.xcworkspacedata 134 | /*.gcno 135 | **/xcshareddata/WorkspaceSettings.xcsettings 136 | 137 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos,xcode -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Preprocess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Preprocess.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import ZIPFoundation 9 | import CocoaLumberjackSwift 10 | 11 | extension InjectorV3 { 12 | 13 | // MARK: - Constants 14 | 15 | fileprivate static let allowedPathExtensions: Set = ["bundle", "dylib", "framework"] 16 | 17 | // MARK: - Shared Methods 18 | 19 | func preprocessAssets(_ assetURLs: [URL]) throws -> [URL] { 20 | 21 | DDLogVerbose("Preprocess \(assetURLs.map { $0.path })", ddlog: logger) 22 | 23 | var preparedAssetURLs = [URL]() 24 | var urlsToMarkAsInjected = [URL]() 25 | 26 | for assetURL in assetURLs { 27 | 28 | let lowerExt = assetURL.pathExtension.lowercased() 29 | if lowerExt == "zip" { 30 | let extractedURL = temporaryDirectoryURL 31 | .appendingPathComponent("\(UUID().uuidString)_\(assetURL.lastPathComponent)") 32 | .appendingPathExtension("extracted") 33 | 34 | try FileManager.default.createDirectory(at: extractedURL, withIntermediateDirectories: true) 35 | try FileManager.default.unzipItem(at: assetURL, to: extractedURL) 36 | 37 | let extractedItems = try FileManager.default 38 | .contentsOfDirectory(at: extractedURL, includingPropertiesForKeys: nil) 39 | .filter { Self.allowedPathExtensions.contains($0.pathExtension.lowercased()) } 40 | 41 | for extractedItem in extractedItems { 42 | if checkIsBundle(extractedItem) { 43 | urlsToMarkAsInjected.append(extractedItem) 44 | } 45 | } 46 | 47 | preparedAssetURLs.append(contentsOf: extractedItems) 48 | continue 49 | } else if lowerExt == "deb" { 50 | let extractedURL = temporaryDirectoryURL 51 | .appendingPathComponent("\(UUID().uuidString)_\(assetURL.lastPathComponent)") 52 | .appendingPathExtension("extracted") 53 | 54 | try FileManager.default.createDirectory(at: extractedURL, withIntermediateDirectories: true) 55 | try _ = decomposeDeb(at: assetURL, to: extractedURL) 56 | 57 | var dylibFiles = [URL]() 58 | var bundleFiles = [URL]() 59 | let fileManager = FileManager.default 60 | let enumerator = fileManager.enumerator(at: extractedURL, includingPropertiesForKeys: nil) 61 | 62 | while let file = enumerator?.nextObject() as? URL { 63 | let ext = file.pathExtension.lowercased() 64 | if ext == "dylib" || ext == "framework" { 65 | dylibFiles.append(file) 66 | } 67 | if ext == "bundle" { 68 | bundleFiles.append(file) 69 | urlsToMarkAsInjected.append(file) 70 | } 71 | } 72 | 73 | preparedAssetURLs.append(contentsOf: dylibFiles) 74 | preparedAssetURLs.append(contentsOf: bundleFiles) 75 | continue 76 | } 77 | 78 | else if Self.allowedPathExtensions.contains(lowerExt) { 79 | 80 | let copiedURL = temporaryDirectoryURL 81 | .appendingPathComponent(assetURL.lastPathComponent) 82 | try FileManager.default.copyItem(at: assetURL, to: copiedURL) 83 | 84 | if checkIsBundle(copiedURL) { 85 | urlsToMarkAsInjected.append(copiedURL) 86 | } 87 | 88 | preparedAssetURLs.append(copiedURL) 89 | continue 90 | } 91 | } 92 | 93 | try markBundlesAsInjected(urlsToMarkAsInjected, privileged: false) 94 | 95 | preparedAssetURLs.removeAll(where: { Self.ignoredDylibAndFrameworkNames.contains($0.lastPathComponent) }) 96 | guard !preparedAssetURLs.isEmpty else { 97 | throw Error.generic(NSLocalizedString("No valid plug-ins found.", comment: "")) 98 | } 99 | 100 | return preparedAssetURLs 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/9. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | import SwiftUI 10 | 11 | final class InjectorV3 { 12 | 13 | static let temporaryRoot: URL = FileManager.default 14 | .urls(for: .cachesDirectory, in: .userDomainMask).first! 15 | .appendingPathComponent(gTrollFoolsIdentifier, isDirectory: true) 16 | .appendingPathComponent("InjectorV3", isDirectory: true) 17 | 18 | static let main = try! InjectorV3(Bundle.main.bundleURL) 19 | 20 | let bundleURL: URL 21 | let temporaryDirectoryURL: URL 22 | 23 | var appID: String! 24 | var teamID: String! 25 | 26 | private(set) var executableURL: URL! 27 | private(set) var frameworksDirectoryURL: URL! 28 | private(set) var logsDirectoryURL: URL! 29 | 30 | private(set) var useWeakReference: AppStorage! 31 | private(set) var preferMainExecutable: AppStorage! 32 | private(set) var injectStrategy: AppStorage! 33 | 34 | let logger: DDLog 35 | 36 | private init() { fatalError("Not implemented") } 37 | 38 | init(_ bundleURL: URL) throws { 39 | 40 | self.bundleURL = bundleURL 41 | self.temporaryDirectoryURL = Self.temporaryRoot 42 | .appendingPathComponent(UUID().uuidString, isDirectory: true) 43 | try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true) 44 | 45 | self.logger = DDLog() 46 | 47 | let executableURL = try locateExecutableInBundle(bundleURL) 48 | let frameworksDirectoryURL = try locateFrameworksDirectoryInBundle(bundleURL) 49 | let appID = try identifierOfBundle(bundleURL) 50 | let teamID = try teamIdentifierOfMachO(executableURL) ?? "" 51 | 52 | self.appID = appID 53 | self.teamID = teamID 54 | self.executableURL = executableURL 55 | self.frameworksDirectoryURL = frameworksDirectoryURL 56 | self.logsDirectoryURL = temporaryDirectoryURL.appendingPathComponent("Logs/\(appID)") 57 | 58 | self.useWeakReference = AppStorage(wrappedValue: true, "UseWeakReference-\(appID)") 59 | self.preferMainExecutable = AppStorage(wrappedValue: false, "PreferMainExecutable-\(appID)") 60 | self.injectStrategy = AppStorage(wrappedValue: .lexicographic, "InjectStrategy-\(appID)") 61 | 62 | setupLoggers() 63 | } 64 | 65 | // MARK: - Instance Methods 66 | 67 | func terminateApp() { 68 | TFUtilKillAll(executableURL.lastPathComponent, true) 69 | } 70 | 71 | // MARK: - Logger 72 | 73 | private func setupLoggers() { 74 | 75 | try? FileManager.default.createDirectory(at: logsDirectoryURL, withIntermediateDirectories: true) 76 | 77 | let fileLogger = DDFileLogger(logFileManager: DDLogFileManagerDefault(logsDirectory: logsDirectoryURL.path)) 78 | 79 | fileLogger.rollingFrequency = 60 * 60 * 24 80 | fileLogger.logFileManager.maximumNumberOfLogFiles = 7 81 | fileLogger.doNotReuseLogFiles = true 82 | 83 | logger.add(fileLogger) 84 | logger.add(DDOSLogger.sharedInstance) 85 | 86 | DDLogWarn("Logger setup \(appID!)", asynchronous: false, ddlog: logger) 87 | } 88 | 89 | var latestLogFileURL: URL? { 90 | 91 | guard let enumerator = FileManager.default.enumerator( 92 | at: logsDirectoryURL, 93 | includingPropertiesForKeys: [.isRegularFileKey, .creationDateKey] 94 | ) else { 95 | return nil 96 | } 97 | 98 | var latestLogFileURL: URL? 99 | var latestCreationDate: Date? 100 | while let fileURL = enumerator.nextObject() as? URL { 101 | 102 | guard let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .creationDateKey]), 103 | let isRegularFile = resourceValues.isRegularFile, isRegularFile, 104 | let creationDate = resourceValues.creationDate 105 | else { 106 | continue 107 | } 108 | 109 | if latestCreationDate == nil || creationDate > latestCreationDate! { 110 | latestLogFileURL = fileURL 111 | latestCreationDate = creationDate 112 | } 113 | } 114 | 115 | return latestLogFileURL 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /TrollFools/AuxiliaryExecute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuxiliaryExecute.swift 3 | // MyYearWithGit 4 | // 5 | // Created by Lakr Aream on 2021/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Execute command or shell with posix, shared with AuxiliaryExecute.local 11 | public class AuxiliaryExecute { 12 | /// we do not recommend you to subclass this singleton 13 | public static let local = AuxiliaryExecute() 14 | 15 | // if binary not found when you call the shell api 16 | // we will take some time to rebuild the bianry table each time 17 | // -->>> this is a time-heavy-task 18 | // so use binaryLocationFor(command:) to cache it if needed 19 | 20 | // system path 21 | internal var currentPath: [String] = [] 22 | // system binary table 23 | internal var binaryTable: [String: String] = [:] 24 | 25 | // for you to put your own search path 26 | internal var extraSearchPath: [String] = [] 27 | // for you to set your own binary table and will be used firstly 28 | // if you set nil here 29 | // -> we will return nil even the binary found in system path 30 | internal var overwriteTable: [String: String?] = [:] 31 | 32 | // this value is used when providing 0 or negative timeout paramete 33 | internal static let maxTimeoutValue: Double = 2_147_483_647 34 | 35 | /// when reading from file pipe, must called from async queue 36 | internal static let pipeControlQueue = DispatchQueue( 37 | label: "wiki.qaq.AuxiliaryExecute.pipeRead", 38 | attributes: .concurrent 39 | ) 40 | 41 | /// when killing process or monitoring events from process, must called from async queue 42 | /// we are making this queue serial queue so won't called at the same time when timeout 43 | internal static let processControlQueue = DispatchQueue( 44 | label: "wiki.qaq.AuxiliaryExecute.processControl", 45 | attributes: [] 46 | ) 47 | 48 | /// used for setting binary table, avoid crash 49 | internal let lock = NSLock() 50 | 51 | /// nope! 52 | private init() { 53 | // no need to setup binary table 54 | // we will make call to it when you call the shell api 55 | // if you only use the spawn api 56 | // we don't need to setup the hole table cause it‘s time-heavy-task 57 | } 58 | 59 | /// Execution Error, do the localization your self 60 | public enum ExecuteError: Error, LocalizedError, Codable { 61 | // not found in path 62 | case commandNotFound 63 | // invalid, may be missing, wrong permission or any other reason 64 | case commandInvalid 65 | // fcntl failed 66 | case openFilePipeFailed 67 | // posix failed 68 | case posixSpawnFailed 69 | // waitpid failed 70 | case waitPidFailed 71 | // timeout when execute 72 | case timeout 73 | } 74 | 75 | public enum TerminationReason: Codable { 76 | case exit(Int32) 77 | case uncaughtSignal(Int32) 78 | } 79 | 80 | public struct PersonaOptions: Codable { 81 | let uid: uid_t 82 | let gid: gid_t 83 | } 84 | 85 | /// Execution Receipt 86 | public struct ExecuteReceipt: Codable { 87 | // exit code when process exit, 88 | // or signal code when process terminated by signal 89 | public let terminationReason: TerminationReason 90 | // process pid that was when it is alive 91 | // -1 means spawn failed in some situation 92 | public let pid: Int 93 | // wait result for final waitpid inside block at 94 | // processSource - eventMask.exit, usually is pid 95 | // -1 for other cases 96 | public let wait: Int 97 | // any error from us, not the command it self 98 | // DOES NOT MEAN THAT THE COMMAND DONE WELL 99 | public let error: ExecuteError? 100 | // stdout 101 | public let stdout: String 102 | // stderr 103 | public let stderr: String 104 | 105 | /// General initialization of receipt object 106 | /// - Parameters: 107 | /// - terminationReason: termination reason 108 | /// - pid: pid when process alive 109 | /// - wait: wait result on waitpid 110 | /// - error: error if any 111 | /// - stdout: stdout 112 | /// - stderr: stderr 113 | internal init( 114 | terminationReason: TerminationReason, 115 | pid: Int, 116 | wait: Int, 117 | error: AuxiliaryExecute.ExecuteError?, 118 | stdout: String, 119 | stderr: String 120 | ) { 121 | self.terminationReason = terminationReason 122 | self.pid = pid 123 | self.wait = wait 124 | self.error = error 125 | self.stdout = stdout 126 | self.stderr = stderr 127 | } 128 | 129 | /// Template for making failure receipt 130 | /// - Parameters: 131 | /// - terminationReason: default uncaught signal 0 132 | /// - pid: default -1 133 | /// - wait: default -1 134 | /// - error: error 135 | /// - stdout: default empty 136 | /// - stderr: default empty 137 | internal static func failure( 138 | terminationReason: TerminationReason = .uncaughtSignal(0), 139 | pid: Int = -1, 140 | wait: Int = -1, 141 | error: AuxiliaryExecute.ExecuteError?, 142 | stdout: String = "", 143 | stderr: String = "" 144 | ) -> ExecuteReceipt { 145 | .init( 146 | terminationReason: terminationReason, 147 | pid: pid, 148 | wait: wait, 149 | error: error, 150 | stdout: stdout, 151 | stderr: stderr 152 | ) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+MachO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+MachO.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import MachOKit 9 | import OrderedCollections 10 | 11 | extension InjectorV3 { 12 | 13 | func isMachO(_ target: URL) -> Bool { 14 | if (try? MachOKit.loadFromFile(url: target)) != nil { 15 | true 16 | } else { 17 | false 18 | } 19 | } 20 | 21 | func isProtectedMachO(_ target: URL) throws -> Bool { 22 | let machOFile = try MachOKit.loadFromFile(url: target) 23 | switch machOFile { 24 | case .machO(let machOFile): 25 | for command in machOFile.loadCommands { 26 | switch command { 27 | case .encryptionInfo(let encryptionInfoCommand): 28 | if encryptionInfoCommand.cryptid != 0 { 29 | return true 30 | } 31 | case .encryptionInfo64(let encryptionInfoCommand): 32 | if encryptionInfoCommand.cryptid != 0 { 33 | return true 34 | } 35 | default: 36 | continue 37 | } 38 | } 39 | case .fat(let fatFile): 40 | let machOFiles = try fatFile.machOFiles() 41 | for machOFile in machOFiles { 42 | for command in machOFile.loadCommands { 43 | switch command { 44 | case .encryptionInfo(let encryptionInfoCommand): 45 | if encryptionInfoCommand.cryptid != 0 { 46 | return true 47 | } 48 | case .encryptionInfo64(let encryptionInfoCommand): 49 | if encryptionInfoCommand.cryptid != 0 { 50 | return true 51 | } 52 | default: 53 | continue 54 | } 55 | } 56 | } 57 | } 58 | return false 59 | } 60 | 61 | func linkedDylibsRecursivelyOfMachO(_ target: URL, collected: OrderedSet = []) throws -> OrderedSet { 62 | if collected.contains(target) { 63 | return collected 64 | } 65 | 66 | var newCollected = collected 67 | newCollected.append(target) 68 | 69 | let loadedDylibs = (try? loadedDylibsOfMachO(target).compactMap({ resolveLoadCommand($0) })) ?? [] 70 | for dylib in loadedDylibs { 71 | newCollected = try linkedDylibsRecursivelyOfMachO(dylib, collected: newCollected) 72 | } 73 | 74 | return newCollected 75 | } 76 | 77 | func loadedDylibsOfMachO(_ target: URL) throws -> OrderedSet { 78 | var dylibs = OrderedSet() 79 | let machOFile = try MachOKit.loadFromFile(url: target) 80 | switch machOFile { 81 | case .machO(let machOFile): 82 | for command in machOFile.loadCommands { 83 | switch command { 84 | case .loadDylib(let loadDylibCommand): 85 | dylibs.append(loadDylibCommand.dylib(in: machOFile).name) 86 | case .loadWeakDylib(let loadWeakDylibCommand): 87 | dylibs.append(loadWeakDylibCommand.dylib(in: machOFile).name) 88 | default: 89 | continue 90 | } 91 | } 92 | case .fat(let fatFile): 93 | let machOFiles = try fatFile.machOFiles() 94 | for machOFile in machOFiles { 95 | for command in machOFile.loadCommands { 96 | switch command { 97 | case .loadDylib(let loadDylibCommand): 98 | dylibs.append(loadDylibCommand.dylib(in: machOFile).name) 99 | case .loadWeakDylib(let loadWeakDylibCommand): 100 | dylibs.append(loadWeakDylibCommand.dylib(in: machOFile).name) 101 | default: 102 | continue 103 | } 104 | } 105 | } 106 | } 107 | return dylibs 108 | } 109 | 110 | func runtimePathsOfMachO(_ target: URL) throws -> OrderedSet { 111 | var paths = OrderedSet() 112 | let machOFile = try MachOKit.loadFromFile(url: target) 113 | switch machOFile { 114 | case .machO(let machOFile): 115 | for command in machOFile.loadCommands { 116 | switch command { 117 | case .rpath(let rpathCommand): 118 | paths.append(rpathCommand.path(in: machOFile)) 119 | default: 120 | continue 121 | } 122 | } 123 | case .fat(let fatFile): 124 | let machOFiles = try fatFile.machOFiles() 125 | for machOFile in machOFiles { 126 | for command in machOFile.loadCommands { 127 | switch command { 128 | case .rpath(let rpathCommand): 129 | paths.append(rpathCommand.path(in: machOFile)) 130 | default: 131 | continue 132 | } 133 | } 134 | } 135 | } 136 | return paths 137 | } 138 | 139 | func teamIdentifierOfMachO(_ target: URL) throws -> String? { 140 | let machOFile = try MachOKit.loadFromFile(url: target) 141 | switch machOFile { 142 | case .machO(let machOFile): 143 | if let codeSign = machOFile.codeSign, let teamID = codeSign.codeDirectory?.teamId(in: codeSign) { 144 | return teamID 145 | } 146 | case .fat(let fatFile): 147 | let machOFiles = try fatFile.machOFiles() 148 | for machOFile in machOFiles { 149 | if let codeSign = machOFile.codeSign, let teamID = codeSign.codeDirectory?.teamId(in: codeSign) { 150 | return teamID 151 | } 152 | } 153 | } 154 | return nil 155 | } 156 | 157 | fileprivate func resolveLoadCommand(_ name: String) -> URL? { 158 | guard (name.hasPrefix("@rpath/") && !name.hasPrefix("@rpath/libswift")) || name.hasPrefix("@executable_path/") else { 159 | return nil 160 | } 161 | 162 | var resolvedName = name 163 | resolvedName = resolvedName 164 | .replacingOccurrences(of: "@executable_path/", with: executableURL.deletingLastPathComponent().path + "/") 165 | resolvedName = resolvedName 166 | .replacingOccurrences(of: "@rpath/", with: frameworksDirectoryURL.path + "/") 167 | 168 | let fileURL = URL(fileURLWithPath: resolvedName) 169 | return FileManager.default.fileExists(atPath: fileURL.path) ? fileURL : nil 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /TrollFools/InjectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/19. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | import SwiftUI 10 | import ZIPFoundation 11 | 12 | struct InjectView: View { 13 | @EnvironmentObject var appList: AppListModel 14 | 15 | let app: App 16 | let urlList: [URL] 17 | 18 | @State var injectResult: Result? 19 | @StateObject fileprivate var viewControllerHost = ViewControllerHost() 20 | 21 | init(_ app: App, urlList: [URL]) { 22 | self.app = app 23 | self.urlList = urlList 24 | } 25 | 26 | func inject() -> Result { 27 | var logFileURL: URL? 28 | 29 | do { 30 | let injector = try InjectorV3(app.url) 31 | logFileURL = injector.latestLogFileURL 32 | 33 | if injector.appID.isEmpty { 34 | injector.appID = app.id 35 | } 36 | 37 | if injector.teamID.isEmpty { 38 | injector.teamID = app.teamID 39 | } 40 | 41 | let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 42 | let backupURL = documentsURL.appendingPathComponent("TFPlugInsBackups", isDirectory: true) 43 | 44 | if !FileManager.default.fileExists(atPath: backupURL.path) { 45 | try FileManager.default.createDirectory(at: backupURL, withIntermediateDirectories: true) 46 | } 47 | 48 | let fileURLs = try FileManager.default.contentsOfDirectory( 49 | at: backupURL, 50 | includingPropertiesForKeys: nil, 51 | options: .skipsHiddenFiles 52 | ) 53 | 54 | let inputZipPath = urlList.count == 1 && urlList[0].pathExtension.lowercased() == "zip" ? urlList[0].path : nil 55 | 56 | for url in fileURLs { 57 | if url.lastPathComponent.contains(app.name) && url.pathExtension.lowercased() == "zip" { 58 | if let inputZipPath = inputZipPath, url.path == inputZipPath { 59 | continue 60 | } 61 | try? FileManager.default.removeItem(at: url) 62 | } 63 | } 64 | 65 | let dateFormatter = DateFormatter() 66 | dateFormatter.dateFormat = "MMdd-HHmmss" 67 | let timestamp = dateFormatter.string(from: Date()) 68 | let zipFileName = "\(app.name)Plugins_\(timestamp).zip" 69 | let destURL = backupURL.appendingPathComponent(zipFileName) 70 | 71 | if urlList.count == 1 && urlList[0].pathExtension.lowercased() == "zip" { 72 | try FileManager.default.copyItem(at: urlList[0], to: destURL) 73 | } else { 74 | let zipFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(zipFileName) 75 | let archive = try Archive(url: zipFileURL, accessMode: .create) 76 | 77 | for plugin in InjectorV3.main.injectedAssetURLsInBundle(app.url) { 78 | let entryPath = plugin.lastPathComponent 79 | try archive.addEntry( 80 | with: entryPath, 81 | fileURL: plugin, 82 | compressionMethod: .deflate 83 | ) 84 | } 85 | 86 | try FileManager.default.moveItem(at: zipFileURL, to: destURL) 87 | } 88 | 89 | try injector.inject(urlList) 90 | 91 | 92 | return .success(injector.latestLogFileURL) 93 | 94 | } catch { 95 | DDLogError("\(error)", ddlog: InjectorV3.main.logger) 96 | 97 | var userInfo: [String: Any] = [ 98 | NSLocalizedDescriptionKey: error.localizedDescription, 99 | ] 100 | 101 | if let logFileURL { 102 | userInfo[NSURLErrorKey] = logFileURL 103 | } 104 | 105 | return .failure(NSError(domain: gTrollFoolsErrorDomain, code: 0, userInfo: userInfo)) 106 | } 107 | } 108 | 109 | var bodyContent: some View { 110 | VStack { 111 | if let injectResult { 112 | switch injectResult { 113 | case .success(let url): 114 | SuccessView( 115 | title: NSLocalizedString("Completed", comment: ""), 116 | logFileURL: url 117 | ) 118 | case .failure(let error): 119 | FailureView( 120 | title: NSLocalizedString("Failed", comment: ""), 121 | error: error 122 | ) 123 | } 124 | } else { 125 | if #available(iOS 16.0, *) { 126 | ProgressView() 127 | .progressViewStyle(CircularProgressViewStyle()) 128 | .padding(.all, 20) 129 | .controlSize(.large) 130 | } else { 131 | // Fallback on earlier versions 132 | ProgressView() 133 | .progressViewStyle(CircularProgressViewStyle()) 134 | .padding(.all, 20) 135 | .scaleEffect(2.0) 136 | } 137 | 138 | Text(NSLocalizedString("Injecting", comment: "")) 139 | .font(.headline) 140 | } 141 | } 142 | .padding() 143 | .navigationTitle(app.name) 144 | .navigationBarTitleDisplayMode(.inline) 145 | .onViewWillAppear { viewController in 146 | viewController.navigationController? 147 | .view.isUserInteractionEnabled = false 148 | viewControllerHost.viewController = viewController 149 | } 150 | .onAppear { 151 | DispatchQueue.global(qos: .userInteractive).async { 152 | let result = inject() 153 | 154 | DispatchQueue.main.async { 155 | withAnimation { 156 | injectResult = result 157 | app.reload() 158 | appList.performFilter() 159 | appList.objectWillChange.send() 160 | viewControllerHost.viewController?.navigationController? 161 | .view.isUserInteractionEnabled = true 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | var body: some View { 169 | if appList.isSelectorMode { 170 | bodyContent 171 | .toolbar { 172 | ToolbarItem(placement: .navigationBarTrailing) { 173 | Button(NSLocalizedString("Done", comment: "")) { 174 | viewControllerHost.viewController?.navigationController? 175 | .dismiss(animated: true) 176 | } 177 | } 178 | } 179 | } else { 180 | bodyContent 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /TrollFools/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "%@ exited with code %d" = "%@ 意外退出,返回值 %d"; 3 | 4 | /* StripedTextTableViewController */ 5 | "%@ rows not loaded" = "%@ 行未加载"; 6 | 7 | /* No comment provided by engineer. */ 8 | "%@ terminated with signal %d" = "%@ 意外终止,未捕获的信号 %d"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Advanced Settings" = "高级选项"; 12 | 13 | /* No comment provided by engineer. */ 14 | "And %d more unsupported user applications." = "另有 %d 个不支持的用户应用"; 15 | 16 | /* StripedTextTableViewController */ 17 | "Cancel" = "取消"; 18 | 19 | /* No comment provided by engineer. */ 20 | "Cannot parse text with UTF-8 encoding: “%@”." = "无法使用 UTF-8 编码解析文本:“%@”。"; 21 | 22 | /* No comment provided by engineer. */ 23 | "Completed" = "已完成"; 24 | 25 | /* StripedTextTableViewController */ 26 | "Confirm" = "确认"; 27 | 28 | /* StripedTextTableViewController */ 29 | "Copy" = "拷贝"; 30 | 31 | /* No comment provided by engineer. */ 32 | "Copyright" = "版权所有"; 33 | 34 | /* StripedTextTableViewController */ 35 | "Do you want to clear this log file “%@”?" = "你确定要清空日志文件 “%@” 吗?"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Done" = "完成"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Eject" = "推出"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Eject All" = "全部推出"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Error" = "错误"; 48 | 49 | /* No comment provided by engineer. */ 50 | "Failed" = "失败"; 51 | 52 | /* No comment provided by engineer. */ 53 | "Failed to find entry CFBundleExecutable in: %@" = "找不到 %@ 中的 CFBundleExecutable。"; 54 | 55 | /* No comment provided by engineer. */ 56 | "Failed to find entry CFBundleIdentifier in: %@" = "找不到 %@ 中的 CFBundleIdentifier"; 57 | 58 | /* No comment provided by engineer. */ 59 | "Failed to locate main executable: %@" = "找不到主要可执行文件:%@"; 60 | 61 | /* No comment provided by engineer. */ 62 | "Failed to parse: %@" = "无法解析:%@"; 63 | 64 | /* No comment provided by engineer. */ 65 | "Fast" = "快速"; 66 | 67 | /* No comment provided by engineer. */ 68 | "Filza (URL Scheme) Not Installed" = "未安装支持 URL Scheme 的 Filza"; 69 | 70 | /* No comment provided by engineer. */ 71 | "If you do not know what these options mean, please do not change them." = "如果你不知道这些选项的含义,请不要更改。"; 72 | 73 | /* No comment provided by engineer. */ 74 | "Inject" = "注入"; 75 | 76 | /* No comment provided by engineer. */ 77 | "Injectable System Applications" = "可注入的系统应用"; 78 | 79 | /* No comment provided by engineer. */ 80 | "Injected Plug-Ins" = "已注入的插件"; 81 | 82 | /* No comment provided by engineer. */ 83 | "Injecting" = "注入中"; 84 | 85 | /* No comment provided by engineer. */ 86 | "Injection" = "注入"; 87 | 88 | /* No comment provided by engineer. */ 89 | "Injection Strategy" = "注入策略"; 90 | 91 | /* No comment provided by engineer. */ 92 | "Launch" = "启动"; 93 | 94 | /* No comment provided by engineer. */ 95 | "Lexicographic" = "字典序"; 96 | 97 | /* No comment provided by engineer. */ 98 | "Lock Version" = "锁定版本"; 99 | 100 | /* StripedTextTableViewController */ 101 | "Log Viewer" = "日志查看器"; 102 | 103 | /* No comment provided by engineer. */ 104 | "Made with ♥ by OwnGoal Studio" = "「乌龙工作室」倾情献制"; 105 | 106 | /* No comment provided by engineer. */ 107 | "No eligible framework found." = "没有找到符合条件的框架。"; 108 | 109 | /* No comment provided by engineer. */ 110 | "No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”." = "没有找到符合条件的框架。\n\n通常情况下,这不是 TrollFools 自身的问题,而是目标应用程序的问题。你可能需要从 App Store 重新安装该应用。你不能在通过 “爱啪思道” 或 “NoAppThinning” 等方式安装的应用程序上使用 TrollFools。"; 111 | 112 | /* No comment provided by engineer. */ 113 | "No Injected Plug-Ins" = "没有已注入的插件"; 114 | 115 | /* No comment provided by engineer. */ 116 | "No valid plug-ins found." = "没有找到有效的插件。"; 117 | 118 | /* No comment provided by engineer. */ 119 | "Only removable system applications are eligible and listed." = "仅列出可移除的系统应用,其他系统应用不支持注入。"; 120 | 121 | /* No comment provided by engineer. */ 122 | "Patched" = "已注入"; 123 | 124 | /* No comment provided by engineer. */ 125 | "Pinned Version" = "已固定版本"; 126 | 127 | /* No comment provided by engineer. */ 128 | "Plug-Ins" = "插件列表"; 129 | 130 | /* No comment provided by engineer. */ 131 | "Post-order" = "后置"; 132 | 133 | /* No comment provided by engineer. */ 134 | "Pre-order" = "前置"; 135 | 136 | /* No comment provided by engineer. */ 137 | "Prefer Main Executable" = "优先处理主要可执行文件"; 138 | 139 | /* No comment provided by engineer. */ 140 | "Rebuild Icon Cache" = "重建图标缓存"; 141 | 142 | /* No comment provided by engineer. */ 143 | "Search Patched…" = "搜索 已注入…"; 144 | 145 | /* No comment provided by engineer. */ 146 | "Search…" = "搜索…"; 147 | 148 | /* No comment provided by engineer. */ 149 | "Select Application to Inject" = "选择应用程序进行注入"; 150 | 151 | /* No comment provided by engineer. */ 152 | "Show in Filza" = "在 Filza 中显示"; 153 | 154 | /* No comment provided by engineer. */ 155 | "Show Patched Only" = "仅显示已注入的"; 156 | 157 | /* No comment provided by engineer. */ 158 | "Some plug-ins were not injected by TrollFools, please eject them with caution." = "部分插件可能并非由 TrollFools 注入,移除它们可能会造成应用程序异常,请谨慎操作。"; 159 | 160 | /* No comment provided by engineer. */ 161 | "Source Code" = "获取源代码"; 162 | 163 | /* No comment provided by engineer. */ 164 | "The content of text file “%@” is empty." = "文本文件 “%@” 的内容为空。"; 165 | 166 | /* No comment provided by engineer. */ 167 | "TrollFools" = "TrollFools"; 168 | 169 | /* No comment provided by engineer. */ 170 | "TrollStore Applications" = "巨魔商店应用"; 171 | 172 | /* No comment provided by engineer. */ 173 | "Unlock Version" = "解锁版本"; 174 | 175 | /* No comment provided by engineer. */ 176 | "Use Weak Reference" = "创建弱引用"; 177 | 178 | /* No comment provided by engineer. */ 179 | "User Applications" = "用户应用"; 180 | 181 | /* No comment provided by engineer. */ 182 | "You need to rebuild the icon cache in TrollStore to apply changes." = "你需要在 TrollStore 中重建图标缓存以应用更改。"; 183 | 184 | /* No comment provided by engineer. */ 185 | "@huamidev Add some features" = "@huamidev 添加部分功能"; 186 | 187 | /* No comment provided by engineer. */ 188 | "Name (A-Z)" = "名称 (A-Z)"; 189 | 190 | /* No comment provided by engineer. */ 191 | "Name (Z-A)" = "名称 (Z-A)"; 192 | 193 | /* No comment provided by engineer. */ 194 | "Switch to Default Icon" = "切换到默认图标"; 195 | 196 | /* No comment provided by engineer. */ 197 | "Switch to Official Icon" = "切换到官方图标"; 198 | 199 | /* No comment provided by engineer. */ 200 | "All" = "全部应用"; 201 | 202 | /* No comment provided by engineer. */ 203 | "User" = "用户应用"; 204 | 205 | /* No comment provided by engineer. */ 206 | "Troll" = "巨魔应用"; 207 | 208 | /* No comment provided by engineer. */ 209 | "System" = "系统应用"; 210 | 211 | /* No comment provided by engineer. */ 212 | "Notification" = "通知"; 213 | 214 | /* No comment provided by engineer. */ 215 | "Clear Temp Cache" = "清除临时缓存"; 216 | 217 | /* No comment provided by engineer. */ 218 | "Clear Done" = "清除完成"; 219 | 220 | /* No comment provided by engineer. */ 221 | "Clear App Cache" = "清除应用缓存"; 222 | 223 | /* No comment provided by engineer. */ 224 | "This software is an open-source project TrollFools.\nIf you have purchased it, you should understand what this means.\nVisit the project address in the footer for more information.\nModified version by huami.\nChange Lessica to huami1314 to redirect to this project.\nTG: @huamidev\nThis popup will only display once.\nThanks to the original author i_82." = "本软件为开源项目TrollFools。\n如果你是购买的 你应该明白这意味着什么。\n请访问底部中的版权以获取更多信息。\n由huami修改的版本。\n将Lessica更改为huami1314即可重定向到本项目。\nTG:@huamidev\n此弹窗只会显示一次。\n感谢原作者i_82。"; 225 | 226 | /* No comment provided by engineer. */ 227 | "Un support." = "不支持该应用。"; 228 | 229 | /* No comment provided by engineer. */ 230 | "Cancel" = "取消"; 231 | 232 | /* No comment provided by engineer. */ 233 | "%d applications are not supported." = "%d 个应用程序不被支持"; 234 | 235 | /* No comment provided by engineer. */ 236 | "View Logs" = "查看日志"; 237 | 238 | /* No comment provided by engineer. */ 239 | "You need to rebuild the icon cache in TrollStore to apply changes." = "你需要在 TrollStore 中重建图标缓存以应用更改。"; 240 | 241 | /* No comment provided by engineer. */ 242 | "Use Last Configuration" = "使用上次偏好"; -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Inject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Inject.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | 10 | 11 | extension InjectorV3 { 12 | 13 | enum Strategy: String, CaseIterable { 14 | case lexicographic 15 | case fast 16 | case preorder 17 | case postorder 18 | var localizedDescription: String { 19 | switch self { 20 | case .lexicographic: NSLocalizedString("Lexicographic", comment: "") 21 | case .fast: NSLocalizedString("Fast", comment: "") 22 | case .preorder: NSLocalizedString("Pre-order", comment: "") 23 | case .postorder: NSLocalizedString("Post-order", comment: "") 24 | } 25 | } 26 | } 27 | 28 | // MARK: - Instance Methods 29 | 30 | func inject(_ assetURLs: [URL]) throws { 31 | let preparedAssetURLs = try preprocessAssets(assetURLs) 32 | 33 | precondition(!preparedAssetURLs.isEmpty, "No asset to inject.") 34 | terminateApp() 35 | 36 | try injectBundles(preparedAssetURLs 37 | .filter { $0.pathExtension.lowercased() == "bundle" }) 38 | 39 | try injectDylibsAndFrameworks(preparedAssetURLs 40 | .filter { $0.pathExtension.lowercased() == "dylib" || $0.pathExtension.lowercased() == "framework" }) 41 | } 42 | 43 | // MARK: - Private Methods 44 | 45 | fileprivate func injectBundles(_ assetURLs: [URL]) throws { 46 | guard !assetURLs.isEmpty else { 47 | return 48 | } 49 | 50 | for assetURL in assetURLs { 51 | let targetURL = bundleURL.appendingPathComponent(assetURL.lastPathComponent) 52 | 53 | try cmdCopy(from: assetURL, to: targetURL, clone: true, overwrite: true) 54 | try cmdChangeOwnerToInstalld(targetURL, recursively: true) 55 | } 56 | } 57 | 58 | fileprivate func injectDylibsAndFrameworks(_ assetURLs: [URL]) throws { 59 | guard !assetURLs.isEmpty else { 60 | return 61 | } 62 | 63 | try assetURLs.forEach { 64 | try standardizeLoadCommandDylibToSubstrate($0) 65 | try applyCoreTrustBypass($0) 66 | } 67 | 68 | let substrateFwkURL = try prepareSubstrate() 69 | guard let targetMachO = try locateAvailableMachO() else { 70 | DDLogError("All Mach-Os are protected", ddlog: logger) 71 | throw Error.generic(NSLocalizedString("No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”.", comment: "")) 72 | } 73 | 74 | DDLogInfo("Best matched Mach-O is \(targetMachO.path)", ddlog: logger) 75 | 76 | let resourceURLs: [URL] = [substrateFwkURL] + assetURLs 77 | try makeAlternate(targetMachO) 78 | do { 79 | try copyfiles(resourceURLs) 80 | for assetURL in assetURLs { 81 | try insertLoadCommandOfAsset(assetURL, to: targetMachO) 82 | } 83 | try applyCoreTrustBypass(targetMachO) 84 | } catch { 85 | try? restoreAlternate(targetMachO) 86 | try? batchRemove(resourceURLs) 87 | throw error 88 | } 89 | } 90 | 91 | // MARK: - Core Trust 92 | 93 | fileprivate func applyCoreTrustBypass(_ target: URL) throws { 94 | let isFramework = checkIsBundle(target) 95 | 96 | let machO: URL 97 | if isFramework { 98 | machO = try locateExecutableInBundle(target) 99 | } else { 100 | machO = target 101 | } 102 | 103 | try cmdCoreTrustBypass(machO, teamID: teamID) 104 | try cmdChangeOwnerToInstalld(target, recursively: isFramework) 105 | } 106 | 107 | // MARK: - Cydia Substrate 108 | 109 | fileprivate static let substrateZipURL = Bundle.main.url(forResource: substrateFwkName, withExtension: "zip")! 110 | 111 | fileprivate func prepareSubstrate() throws -> URL { 112 | try FileManager.default.unzipItem(at: Self.substrateZipURL, to: temporaryDirectoryURL) 113 | 114 | let fwkURL = temporaryDirectoryURL.appendingPathComponent(Self.substrateFwkName) 115 | try markBundlesAsInjected([fwkURL], privileged: false) 116 | 117 | let machO = fwkURL.appendingPathComponent(Self.substrateName) 118 | 119 | try cmdCoreTrustBypass(machO, teamID: teamID) 120 | try cmdChangeOwnerToInstalld(fwkURL, recursively: true) 121 | 122 | return fwkURL 123 | } 124 | 125 | fileprivate func standardizeLoadCommandDylibToSubstrate(_ assetURL: URL) throws { 126 | let machO: URL 127 | if checkIsBundle(assetURL) { 128 | machO = try locateExecutableInBundle(assetURL) 129 | } else { 130 | machO = assetURL 131 | } 132 | 133 | let dylibs = try loadedDylibsOfMachO(machO) 134 | for dylib in dylibs { 135 | if Self.ignoredDylibAndFrameworkNames.firstIndex(where: { dylib.hasSuffix("/\($0)") }) != nil { 136 | try cmdChangeLoadCommandDylib(machO, from: dylib, to: "@executable_path/Frameworks/\(Self.substrateFwkName)/\(Self.substrateName)") 137 | } 138 | } 139 | } 140 | 141 | // MARK: - Load Commands 142 | 143 | func loadCommandNameOfAsset(_ assetURL: URL) throws -> String { 144 | var name = "@rpath/" 145 | 146 | if checkIsBundle(assetURL) { 147 | precondition(assetURL.pathExtension == "framework", "Invalid framework: \(assetURL.path)") 148 | let machO = try locateExecutableInBundle(assetURL) 149 | name += machO.pathComponents.suffix(2).joined(separator: "/") // @rpath/XXX.framework/XXX 150 | precondition(name.contains(".framework/"), "Invalid framework name: \(name)") 151 | } else { 152 | precondition(assetURL.pathExtension == "dylib", "Invalid dylib: \(assetURL.path)") 153 | name += assetURL.lastPathComponent 154 | precondition(name.hasSuffix(".dylib"), "Invalid dylib name: \(name)") // @rpath/XXX.dylib 155 | } 156 | 157 | return name 158 | } 159 | 160 | fileprivate func insertLoadCommandOfAsset(_ assetURL: URL, to target: URL) throws { 161 | let name = try loadCommandNameOfAsset(assetURL) 162 | 163 | try cmdInsertLoadCommandRuntimePath(target, name: "@executable_path/Frameworks") 164 | try cmdInsertLoadCommandDylib(target, name: name, weak: useWeakReference.wrappedValue) 165 | try standardizeLoadCommandDylib(target, to: name) 166 | } 167 | 168 | fileprivate func standardizeLoadCommandDylib(_ target: URL, to name: String) throws { 169 | precondition(name.hasPrefix("@rpath/"), "Invalid dylib name: \(name)") 170 | 171 | let itemName = String(name[name.index(name.startIndex, offsetBy: 7)...]) 172 | let dylibs = try loadedDylibsOfMachO(target) 173 | 174 | for dylib in dylibs { 175 | if dylib.hasSuffix("/" + itemName) { 176 | try cmdChangeLoadCommandDylib(target, from: dylib, to: name) 177 | } 178 | } 179 | } 180 | 181 | // MARK: - Path Clone 182 | 183 | fileprivate func copyfiles(_ assetURLs: [URL]) throws { 184 | let targetURLs = assetURLs.map { 185 | frameworksDirectoryURL.appendingPathComponent($0.lastPathComponent) 186 | } 187 | 188 | for (assetURL, targetURL) in zip(assetURLs, targetURLs) { 189 | try cmdCopy(from: assetURL, to: targetURL, clone: true, overwrite: true) 190 | try cmdChangeOwnerToInstalld(targetURL, recursively: checkIsDirectory(assetURL)) 191 | } 192 | } 193 | 194 | fileprivate func batchRemove(_ assetURLs: [URL]) throws { 195 | try assetURLs.forEach { 196 | try cmdRemove($0, recursively: checkIsDirectory($0)) 197 | } 198 | } 199 | 200 | // MARK: - Path Finder 201 | 202 | fileprivate func locateAvailableMachO() throws -> URL? { 203 | try frameworkMachOsInBundle(bundleURL) 204 | .first { try !isProtectedMachO($0) } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /TrollFools/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "%@ exited with code %d" = "%@ thoát với mã %d"; 3 | 4 | /* TODO */ 5 | "%@ rows not loaded" = "%@ rows not loaded"; 6 | 7 | /* No comment provided by engineer. */ 8 | "%@ terminated with signal %d" = "%@ đã kết thúc với tín hiệu %d"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Advanced Settings" = "Cài đặt nâng cao"; 12 | 13 | /* No comment provided by engineer. */ 14 | "And %d more unsupported user applications." = "Có %d ứng dụng người dùng không được hỗ trợ."; 15 | 16 | /* TODO */ 17 | "Cancel" = "Cancel"; 18 | 19 | /* TODO */ 20 | "Cannot parse text with UTF-8 encoding: “%@”." = "Cannot parse text with UTF-8 encoding: “%@”."; 21 | 22 | /* No comment provided by engineer. */ 23 | "Completed" = "Hoàn thành"; 24 | 25 | /* TODO */ 26 | "Confirm" = "Confirm"; 27 | 28 | /* TODO */ 29 | "Copy" = "Copy"; 30 | 31 | /* No comment provided by engineer. */ 32 | "Copyright" = "Bản quyền"; 33 | 34 | /* TODO */ 35 | "Do you want to clear this log file “%@”?" = "Do you want to clear this log file “%@”?"; 36 | 37 | /* TODO */ 38 | "Done" = "Done"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Eject" = "Gỡ bỏ"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Eject All" = "Gỡ bỏ tất cả"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Error" = "Lỗi"; 48 | 49 | /* No comment provided by engineer. */ 50 | "Failed" = "Thất bại"; 51 | 52 | /* No comment provided by engineer. */ 53 | "Failed to find entry CFBundleExecutable in: %@" = "Không tìm thấy mục CFBundleExecutable trong: %@"; 54 | 55 | /* No comment provided by engineer. */ 56 | "Failed to find entry CFBundleIdentifier in: %@" = "Không tìm thấy mục CFBundleIdentifier trong: %@"; 57 | 58 | /* No comment provided by engineer. */ 59 | "Failed to locate main executable: %@" = "Không tìm thấy tệp thực thi chính: %@"; 60 | 61 | /* No comment provided by engineer. */ 62 | "Failed to parse: %@" = "Không thể phân tích: %@"; 63 | 64 | /* TODO */ 65 | "Fast" = "Fast"; 66 | 67 | /* TODO */ 68 | "Filza (URL Scheme) Not Installed" = "Filza (URL Scheme) Not Installed"; 69 | 70 | /* No comment provided by engineer. */ 71 | "If you do not know what these options mean, please do not change them." = "Nếu bạn không biết các tùy chọn này có ý nghĩa gì, đừng thay đổi chúng."; 72 | 73 | /* No comment provided by engineer. */ 74 | "Inject" = "Tiêm"; 75 | 76 | /* No comment provided by engineer. */ 77 | "Injectable System Applications" = "Ứng dụng hệ thống có thể tiêm"; 78 | 79 | /* No comment provided by engineer. */ 80 | "Injected Plug-Ins" = "Plug-in đã tiêm"; 81 | 82 | /* No comment provided by engineer. */ 83 | "Injecting" = "Đang tiêm"; 84 | 85 | /* No comment provided by engineer. */ 86 | "Injection" = "Nâng cao"; 87 | 88 | /* TODO */ 89 | "Injection Strategy" = "Injection Strategy"; 90 | 91 | /* No comment provided by engineer. */ 92 | "Launch" = "Mở ứng dụng"; 93 | 94 | /* TODO */ 95 | "Lexicographic" = "Lexicographic"; 96 | 97 | /* No comment provided by engineer. */ 98 | "Lock Version" = "Khóa phiên bản"; 99 | 100 | /* TODO */ 101 | "Log Viewer" = "Log Viewer"; 102 | 103 | /* TODO */ 104 | "Made with ♥ by OwnGoal Studio" = "Made with ♥ by OwnGoal Studio"; 105 | 106 | /* No comment provided by engineer. */ 107 | "No eligible framework found." = "Không tìm thấy framework đủ điều kiện."; 108 | 109 | /* TODO */ 110 | "No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”." = "No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”."; 111 | 112 | /* No comment provided by engineer. */ 113 | "No Injected Plug-Ins" = "Không có plug-in nào được tiêm"; 114 | 115 | /* No comment provided by engineer. */ 116 | "No valid plug-ins found." = "Không tìm thấy plug-in hợp lệ."; 117 | 118 | /* No comment provided by engineer. */ 119 | "Only removable system applications are eligible and listed." = "Chỉ các ứng dụng hệ thống có thể gỡ bỏ và đủ điều kiện tiêm mới được liệt kê."; 120 | 121 | /* No comment provided by engineer. */ 122 | "Patched" = "Đã sửa đổi"; 123 | 124 | /* No comment provided by engineer. */ 125 | "Pinned Version" = "Phiên bản ghim"; 126 | 127 | /* No comment provided by engineer. */ 128 | "Plug-Ins" = "Plug-Ins"; 129 | 130 | /* TODO */ 131 | "Post-order" = "Post-order"; 132 | 133 | /* TODO */ 134 | "Pre-order" = "Pre-order"; 135 | 136 | /* No comment provided by engineer. */ 137 | "Prefer Main Executable" = "Ưu tiên tệp thực thi chính"; 138 | 139 | /* No comment provided by engineer. */ 140 | "Rebuild Icon Cache" = "Làm mới bộ nhớ đệm biểu tượng"; 141 | 142 | /* No comment provided by engineer. */ 143 | "Search Patched…" = "Tìm kiếm sửa đổi…"; 144 | 145 | /* No comment provided by engineer. */ 146 | "Search…" = "Tìm kiếm…"; 147 | 148 | /* TODO */ 149 | "Select Application to Inject" = "Select Application to Inject"; 150 | 151 | /* No comment provided by engineer. */ 152 | "Show in Filza" = "Hiển thị trong Filza"; 153 | 154 | /* No comment provided by engineer. */ 155 | "Show Patched Only" = "Chỉ hiển thị đã sửa đổi"; 156 | 157 | /* No comment provided by engineer. */ 158 | "Some plug-ins were not injected by TrollFools, please eject them with caution." = "Một số plug-in không được tiêm bởi TrollFools, vui lòng loại bỏ chúng một cách cẩn thận."; 159 | 160 | /* No comment provided by engineer. */ 161 | "Source Code" = "Mã nguồn"; 162 | 163 | /* TODO */ 164 | "The content of text file “%@” is empty." = "The content of text file “%@” is empty."; 165 | 166 | /* No comment provided by engineer. */ 167 | "TrollFools" = "TrollFools"; 168 | 169 | /* No comment provided by engineer. */ 170 | "TrollStore Applications" = "Ứng dụng TrollStore"; 171 | 172 | /* No comment provided by engineer. */ 173 | "Unlock Version" = "Mở khóa phiên bản"; 174 | 175 | /* No comment provided by engineer. */ 176 | "Use Weak Reference" = "Sử dụng tham chiếu yếu"; 177 | 178 | /* No comment provided by engineer. */ 179 | "User Applications" = "Ứng dụng người dùng"; 180 | 181 | /* No comment provided by engineer. */ 182 | "You need to rebuild the icon cache in TrollStore to apply changes." = "Bạn cần làm mới bộ nhớ đệm biểu tượng trong TrollStore để áp dụng các thay đổi."; 183 | 184 | /* No comment provided by engineer. */ 185 | "All" = "Tất cả"; 186 | 187 | /* No comment provided by engineer. */ 188 | "User" = "Người dùng"; 189 | 190 | /* No comment provided by engineer. */ 191 | "Troll" = "Troll"; 192 | 193 | /* No comment provided by engineer. */ 194 | "System" = "Hệ thống"; 195 | 196 | /* No comment provided by engineer. */ 197 | "Notification" = "Thông báo"; 198 | 199 | /* No comment provided by engineer. */ 200 | "@huamidev Add some features" = "@huamidev Add some features"; 201 | 202 | /* No comment provided by engineer. */ 203 | "Clear Temp Cache" = "Xóa bộ nhớ đệm tạm thời"; 204 | 205 | /* No comment provided by engineer. */ 206 | "Clear Done" = "Xóa xong"; 207 | 208 | /* No comment provided by engineer. */ 209 | "Clear App Cache" = "Xóa bộ nhớ đệm ứng dụng"; 210 | 211 | /* No comment provided by engineer. */ 212 | "This software is an open-source project TrollFools.\nIf you have purchased it, you should understand what this means.\nVisit the project address in the footer for more information.\nModified version by huami.\nChange Lessica to huami1314 to redirect to this project.\nTG: @huamidev\nThis popup will only display once.\nThanks to the original author i_82." = "Phần mềm này là một dự án mã nguồn mở của TrollFools.\nNếu bạn đã mua phần mềm này, bạn sẽ hiểu điều này có nghĩa là gì.\nTruy cập địa chỉ dự án ở chân trang để biết thêm thông tin.\nPhiên bản đã sửa đổi của huami.\nĐổi Lessica thành huami1314 để chuyển hướng đến dự án này.\nTG: @huamidev\nCửa sổ bật lên này sẽ chỉ hiển thị một lần.\nCảm ơn tác giả gốc i_82."; 213 | 214 | /* No comment provided by engineer. */ 215 | "Un support." = "Không hỗ trợ."; 216 | 217 | /* No comment provided by engineer. */ 218 | "Cancel" = "Hủy"; 219 | 220 | /* No comment provided by engineer. */ 221 | "%d applications are not supported." = "%d ứng dụng không được hỗ trợ."; 222 | 223 | /* TODO */ 224 | "View Logs" = "View Logs"; 225 | 226 | /* No comment provided by engineer. */ 227 | "You need to rebuild the icon cache in TrollStore to apply changes." = "Bạn cần làm mới bộ nhớ đệm biểu tượng trong TrollStore để áp dụng các thay đổi."; 228 | 229 | /* No comment provided by engineer. */ 230 | "Use Last Configuration" = "Sử dụng cấu hình cuối"; -------------------------------------------------------------------------------- /TrollFools/AppListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppListModel.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | final class AppListModel: ObservableObject { 12 | enum Filter { 13 | case all 14 | case user 15 | case troll 16 | case system 17 | } 18 | 19 | enum SortOrder { 20 | case ascending 21 | case descending 22 | } 23 | 24 | static let hasTrollStore: Bool = { LSApplicationProxy(forIdentifier: "com.opa334.TrollStore") != nil }() 25 | private var _allApplications: [App] = [] 26 | 27 | let selectorURL: URL? 28 | var isSelectorMode: Bool { selectorURL != nil } 29 | 30 | @Published var filter = FilterOptions() 31 | @Published var sortOrder: SortOrder = .ascending 32 | @Published var selectedFilter: Filter = .all 33 | @Published var userApplications: [App] = [] 34 | @Published var trollApplications: [App] = [] 35 | @Published var appleApplications: [App] = [] 36 | 37 | @Published var hasTrollRecorder: Bool = false 38 | @Published var unsupportedCount: Int = 0 39 | 40 | @Published var isFilzaInstalled: Bool = false 41 | private let filzaURL = URL(string: "filza://") 42 | 43 | @Published var isRebuildNeeded: Bool = false 44 | @Published var isRebuilding: Bool = false 45 | 46 | private let applicationChanged = PassthroughSubject() 47 | private var cancellables = Set() 48 | 49 | init(selectorURL: URL? = nil) { 50 | self.selectorURL = selectorURL 51 | reload() 52 | 53 | $filter 54 | .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) 55 | .sink { [weak self] _ in 56 | withAnimation { 57 | self?.performFilter() 58 | } 59 | } 60 | .store(in: &cancellables) 61 | 62 | applicationChanged 63 | .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) 64 | .sink { [weak self] _ in 65 | withAnimation { 66 | self?.reload() 67 | } 68 | } 69 | .store(in: &cancellables) 70 | 71 | let darwinCenter = CFNotificationCenterGetDarwinNotifyCenter() 72 | CFNotificationCenterAddObserver(darwinCenter, Unmanaged.passRetained(self).toOpaque(), { center, observer, name, object, userInfo in 73 | guard let observer = Unmanaged.fromOpaque(observer!).takeUnretainedValue() as AppListModel? else { 74 | return 75 | } 76 | observer.applicationChanged.send() 77 | }, "com.apple.LaunchServices.ApplicationsChanged" as CFString, nil, .coalesce) 78 | } 79 | 80 | deinit { 81 | let darwinCenter = CFNotificationCenterGetDarwinNotifyCenter() 82 | CFNotificationCenterRemoveObserver(darwinCenter, Unmanaged.passUnretained(self).toOpaque(), nil, nil) 83 | } 84 | 85 | func reload() { 86 | let allApplications = Self.fetchApplications(&hasTrollRecorder, &unsupportedCount) 87 | allApplications.forEach { $0.appList = self } 88 | self._allApplications = allApplications 89 | if let filzaURL { 90 | self.isFilzaInstalled = UIApplication.shared.canOpenURL(filzaURL) 91 | } else { 92 | self.isFilzaInstalled = false 93 | } 94 | performFilter() 95 | } 96 | 97 | func performFilter() { 98 | var filteredApplications = _allApplications 99 | 100 | if !filter.searchKeyword.isEmpty { 101 | filteredApplications = filteredApplications.filter { 102 | $0.name.localizedCaseInsensitiveContains(filter.searchKeyword) || $0.id.localizedCaseInsensitiveContains(filter.searchKeyword) 103 | } 104 | } 105 | 106 | if filter.showPatchedOnly { 107 | filteredApplications = filteredApplications.filter { $0.isInjected } 108 | } 109 | 110 | filteredApplications.sort { app1, app2 in 111 | switch sortOrder { 112 | case .ascending: 113 | return app1.name.localizedCaseInsensitiveCompare(app2.name) == .orderedAscending 114 | case .descending: 115 | return app1.name.localizedCaseInsensitiveCompare(app2.name) == .orderedDescending 116 | } 117 | } 118 | 119 | switch selectedFilter { 120 | case .all: 121 | userApplications = filteredApplications.filter { $0.isUser } 122 | trollApplications = filteredApplications.filter { $0.isFromTroll } 123 | appleApplications = filteredApplications.filter { $0.isFromApple } 124 | case .user: 125 | userApplications = filteredApplications.filter { $0.isUser } 126 | trollApplications = [] 127 | appleApplications = [] 128 | case .troll: 129 | userApplications = [] 130 | trollApplications = filteredApplications.filter { $0.isFromTroll } 131 | appleApplications = [] 132 | case .system: 133 | userApplications = [] 134 | trollApplications = [] 135 | appleApplications = filteredApplications.filter { $0.isFromApple } 136 | } 137 | } 138 | 139 | func forceRefresh() { 140 | objectWillChange.send() 141 | reload() 142 | performFilter() 143 | } 144 | 145 | private static let excludedIdentifiers: Set = [ 146 | "com.opa334.Dopamine", 147 | "org.coolstar.SileoStore", 148 | ] 149 | 150 | private static func fetchApplications(_ hasTrollRecorder: inout Bool, _ unsupportedCount: inout Int) -> [App] { 151 | let allApps: [App] = LSApplicationWorkspace.default() 152 | .allApplications() 153 | .compactMap { proxy in 154 | guard let id = proxy.applicationIdentifier(), 155 | let url = proxy.bundleURL(), 156 | let teamID = proxy.teamID(), 157 | let appType = proxy.applicationType(), 158 | let localizedName = proxy.localizedName() 159 | else { 160 | return nil 161 | } 162 | 163 | if id == "wiki.qaq.trapp" { 164 | hasTrollRecorder = true 165 | } 166 | 167 | guard !id.hasPrefix("wiki.qaq.") && !id.hasPrefix("com.82flex.") else { 168 | return nil 169 | } 170 | 171 | guard !excludedIdentifiers.contains(id) else { 172 | return nil 173 | } 174 | 175 | let shortVersionString: String? = proxy.shortVersionString() 176 | let dataurl = proxy.dataContainerURL() 177 | let app = App( 178 | id: id, 179 | name: localizedName, 180 | type: appType, 181 | teamID: teamID, 182 | url: url, 183 | version: shortVersionString, 184 | dataurl: (dataurl ?? URL(string: "about:blank"))! 185 | ) 186 | 187 | if app.isUser && app.isFromApple { 188 | return nil 189 | } 190 | 191 | guard app.isRemovable else { 192 | return nil 193 | } 194 | 195 | return app 196 | } 197 | 198 | let filteredApps = allApps 199 | .filter { $0.isSystem || InjectorV3.main.checkIsEligibleAppBundle($0.url) } 200 | .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } 201 | 202 | unsupportedCount = allApps.count - filteredApps.count 203 | 204 | return filteredApps 205 | } 206 | 207 | func openInFilza(_ url: URL) { 208 | guard let filzaURL else { 209 | return 210 | } 211 | let fileURL = filzaURL.appendingPathComponent(url.path) 212 | UIApplication.shared.open(fileURL) 213 | } 214 | 215 | func rebuildIconCache() throws { 216 | // Sadly, we can't call `trollstorehelper` directly because only TrollStore can launch it without error. 217 | LSApplicationWorkspace.default().openApplication(withBundleID: "com.opa334.TrollStore") 218 | } 219 | 220 | var displayedApplications: [App] { 221 | switch selectedFilter { 222 | case .all: 223 | return userApplications + trollApplications + appleApplications 224 | case .user: 225 | return userApplications 226 | case .troll: 227 | return trollApplications 228 | case .system: 229 | return appleApplications 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /TrollFools/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "%@ exited with code %d" = "%@ exited with code %d"; 3 | 4 | /* StripedTextTableViewController */ 5 | "%@ rows not loaded" = "%@ rows not loaded"; 6 | 7 | /* No comment provided by engineer. */ 8 | "%@ terminated with signal %d" = "%@ terminated with signal %d"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Advanced Settings" = "Advanced Settings"; 12 | 13 | /* No comment provided by engineer. */ 14 | "And %d more unsupported user applications." = "And %d more unsupported user applications."; 15 | 16 | /* StripedTextTableViewController */ 17 | "Cancel" = "Cancel"; 18 | 19 | /* No comment provided by engineer. */ 20 | "Cannot parse text with UTF-8 encoding: “%@”." = "Cannot parse text with UTF-8 encoding: “%@”."; 21 | 22 | /* No comment provided by engineer. */ 23 | "Completed" = "Completed"; 24 | 25 | /* StripedTextTableViewController */ 26 | "Confirm" = "Confirm"; 27 | 28 | /* StripedTextTableViewController */ 29 | "Copy" = "Copy"; 30 | 31 | /* No comment provided by engineer. */ 32 | "Copyright" = "Copyright"; 33 | 34 | /* StripedTextTableViewController */ 35 | "Do you want to clear this log file “%@”?" = "Do you want to clear this log file “%@”?"; 36 | 37 | /* No comment provided by engineer. */ 38 | "Done" = "Done"; 39 | 40 | /* No comment provided by engineer. */ 41 | "Eject" = "Eject"; 42 | 43 | /* No comment provided by engineer. */ 44 | "Eject All" = "Eject All"; 45 | 46 | /* No comment provided by engineer. */ 47 | "Error" = "Error"; 48 | 49 | /* No comment provided by engineer. */ 50 | "Failed" = "Failed"; 51 | 52 | /* No comment provided by engineer. */ 53 | "Failed to find entry CFBundleExecutable in: %@" = "Failed to find entry CFBundleExecutable in: %@"; 54 | 55 | /* No comment provided by engineer. */ 56 | "Failed to find entry CFBundleIdentifier in: %@" = "Failed to find entry CFBundleIdentifier in: %@"; 57 | 58 | /* No comment provided by engineer. */ 59 | "Failed to locate main executable: %@" = "Failed to locate main executable: %@"; 60 | 61 | /* No comment provided by engineer. */ 62 | "Failed to parse: %@" = "Failed to parse: %@"; 63 | 64 | /* No comment provided by engineer. */ 65 | "Fast" = "Fast"; 66 | 67 | /* No comment provided by engineer. */ 68 | "Filza (URL Scheme) Not Installed" = "Filza (URL Scheme) Not Installed"; 69 | 70 | /* No comment provided by engineer. */ 71 | "If you do not know what these options mean, please do not change them." = "If you do not know what these options mean, please do not change them."; 72 | 73 | /* No comment provided by engineer. */ 74 | "Inject" = "Inject"; 75 | 76 | /* No comment provided by engineer. */ 77 | "Injectable System Applications" = "Injectable System Applications"; 78 | 79 | /* No comment provided by engineer. */ 80 | "Injected Plug-Ins" = "Injected Plug-Ins"; 81 | 82 | /* No comment provided by engineer. */ 83 | "Injecting" = "Injecting"; 84 | 85 | /* No comment provided by engineer. */ 86 | "Injection" = "Injection"; 87 | 88 | /* No comment provided by engineer. */ 89 | "Injection Strategy" = "Injection Strategy"; 90 | 91 | /* No comment provided by engineer. */ 92 | "Launch" = "Launch"; 93 | 94 | /* No comment provided by engineer. */ 95 | "Lexicographic" = "Lexicographic"; 96 | 97 | /* No comment provided by engineer. */ 98 | "Lock Version" = "Lock Version"; 99 | 100 | /* StripedTextTableViewController */ 101 | "Log Viewer" = "Log Viewer"; 102 | 103 | /* No comment provided by engineer. */ 104 | "Made with ♥ by OwnGoal Studio" = "Made with ♥ by OwnGoal Studio"; 105 | 106 | /* No comment provided by engineer. */ 107 | "No eligible framework found." = "No eligible framework found."; 108 | 109 | /* No comment provided by engineer. */ 110 | "No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”." = "No eligible framework found.\n\nIt is usually not a bug with TrollFools itself, but rather with the target app. You may re-install that from App Store. You can’t use TrollFools with apps installed via “Asspp” or tweaks like “NoAppThinning”."; 111 | 112 | /* No comment provided by engineer. */ 113 | "No Injected Plug-Ins" = "No Injected Plug-Ins"; 114 | 115 | /* No comment provided by engineer. */ 116 | "No valid plug-ins found." = "No valid plug-ins found."; 117 | 118 | /* No comment provided by engineer. */ 119 | "Only removable system applications are eligible and listed." = "Only removable system applications are eligible and listed."; 120 | 121 | /* No comment provided by engineer. */ 122 | "Patched" = "Patched"; 123 | 124 | /* No comment provided by engineer. */ 125 | "Pinned Version" = "Pinned Version"; 126 | 127 | /* No comment provided by engineer. */ 128 | "Plug-Ins" = "Plug-Ins"; 129 | 130 | /* No comment provided by engineer. */ 131 | "Post-order" = "Post-order"; 132 | 133 | /* No comment provided by engineer. */ 134 | "Pre-order" = "Pre-order"; 135 | 136 | /* No comment provided by engineer. */ 137 | "Prefer Main Executable" = "Prefer Main Executable"; 138 | 139 | /* No comment provided by engineer. */ 140 | "Rebuild Icon Cache" = "Rebuild Icon Cache"; 141 | 142 | /* No comment provided by engineer. */ 143 | "Search Patched…" = "Search Patched…"; 144 | 145 | /* No comment provided by engineer. */ 146 | "Search…" = "Search…"; 147 | 148 | /* No comment provided by engineer. */ 149 | "Select Application to Inject" = "Select Application to Inject"; 150 | 151 | /* No comment provided by engineer. */ 152 | "Show in Filza" = "Show in Filza"; 153 | 154 | /* No comment provided by engineer. */ 155 | "Show Patched Only" = "Show Patched Only"; 156 | 157 | /* No comment provided by engineer. */ 158 | "Some plug-ins were not injected by TrollFools, please eject them with caution." = "Some plug-ins were not injected by TrollFools, please eject them with caution."; 159 | 160 | /* No comment provided by engineer. */ 161 | "Source Code" = "Source Code"; 162 | 163 | /* No comment provided by engineer. */ 164 | "The content of text file “%@” is empty." = "The content of text file “%@” is empty."; 165 | 166 | /* No comment provided by engineer. */ 167 | "TrollFools" = "TrollFools"; 168 | 169 | /* No comment provided by engineer. */ 170 | "TrollStore Applications" = "TrollStore Applications"; 171 | 172 | /* No comment provided by engineer. */ 173 | "Unlock Version" = "Unlock Version"; 174 | 175 | /* No comment provided by engineer. */ 176 | "Use Weak Reference" = "Use Weak Reference"; 177 | 178 | /* No comment provided by engineer. */ 179 | "User Applications" = "User Applications"; 180 | 181 | /* No comment provided by engineer. */ 182 | "You need to rebuild the icon cache in TrollStore to apply changes." = "You need to rebuild the icon cache in TrollStore to apply changes."; 183 | 184 | /* No comment provided by engineer. */ 185 | "@huamidev Add some features" = "@huamidev Add some features"; 186 | 187 | /* No comment provided by engineer. */ 188 | "Name (A-Z)" = "Name (A-Z)"; 189 | 190 | /* No comment provided by engineer. */ 191 | "Name (Z-A)" = "Name (Z-A)"; 192 | 193 | /* No comment provided by engineer. */ 194 | "Switch to Default Icon" = "Switch to Default Icon"; 195 | 196 | /* No comment provided by engineer. */ 197 | "All" = "All"; 198 | 199 | /* No comment provided by engineer. */ 200 | "User" = "User"; 201 | 202 | /* No comment provided by engineer. */ 203 | "Troll" = "Troll"; 204 | 205 | /* No comment provided by engineer. */ 206 | "System" = "System"; 207 | 208 | /* No comment provided by engineer. */ 209 | "Notification" = "Notification"; 210 | 211 | /* No comment provided by engineer. */ 212 | "Clear Temp Cache" = "Clear Temp Cache"; 213 | 214 | /* No comment provided by engineer. */ 215 | "Clear Done" = "Clear Done"; 216 | 217 | /* No comment provided by engineer. */ 218 | "Clear App Caches" = "Clear App Caches"; 219 | 220 | /* No comment provided by engineer. */ 221 | "This software is an open-source project TrollFools.\nIf you have purchased it, you should understand what this means.\nVisit the project address in the footer for more information.\nModified version by huami.\nChange Lessica to huami1314 to redirect to this project.\nTG: @huamidev\nThis popup will only display once.\nThanks to the original author i_82." = "This software is an open-source project TrollFools.\nIf you have purchased it, you should understand what this means.\nVisit the project address in the footer for more information.\nModified version by huami.\nChange Lessica to huami1314 to redirect to this project.\nTG: @huamidev\nThis popup will only display once.\nThanks to the original author i_82."; 222 | 223 | /* No comment provided by engineer. */ 224 | "Un support." = "Un support."; 225 | 226 | /* No comment provided by engineer. */ 227 | "Cancel" = "Cancel"; 228 | 229 | /* No comment provided by engineer. */ 230 | "%d applications are not supported." = "%d applications are not supported."; 231 | 232 | /* No comment provided by engineer. */ 233 | "View Logs" = "View Logs"; 234 | 235 | /* No comment provided by engineer. */ 236 | "You need to rebuild the icon cache in TrollStore to apply changes." = "You need to rebuild the icon cache in TrollStore to apply changes."; 237 | 238 | /* No comment provided by engineer. */ 239 | "Use Last Configuration" = "Use Last Configuration"; -------------------------------------------------------------------------------- /TrollFools/EjectListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EjectListView.swift 3 | // TrollFools 4 | // 5 | // Created by Lessica on 2024/7/20. 6 | // 7 | 8 | import SwiftUI 9 | import ZIPFoundation 10 | import UniformTypeIdentifiers 11 | import CocoaLumberjackSwift 12 | 13 | struct EjectListView: View { 14 | @StateObject var ejectList: EjectListModel 15 | 16 | init(_ app: App) { 17 | _ejectList = StateObject(wrappedValue: EjectListModel(app)) 18 | } 19 | 20 | @State var isErrorOccurred: Bool = false 21 | @State var lastError: Error? 22 | 23 | @State var isDeletingAll = false 24 | @StateObject var viewControllerHost = ViewControllerHost() 25 | 26 | @State private var isExporting = false 27 | @State private var exportURL: URL? 28 | @State private var showingExportDialog = false 29 | @State private var zipFileURL: URL? 30 | 31 | var deleteAllButtonLabel: some View { 32 | HStack { 33 | Label(NSLocalizedString("Eject All", comment: ""), systemImage: "eject") 34 | Spacer() 35 | if isDeletingAll { 36 | ProgressView() 37 | .progressViewStyle(CircularProgressViewStyle()) 38 | } 39 | } 40 | } 41 | 42 | var deleteAllButton: some View { 43 | if #available(iOS 15.0, *) { 44 | Button(role: .destructive) { 45 | deleteAll() 46 | } label: { 47 | deleteAllButtonLabel 48 | } 49 | } else { 50 | Button { 51 | deleteAll() 52 | } label: { 53 | deleteAllButtonLabel 54 | } 55 | } 56 | } 57 | 58 | var ejectListView: some View { 59 | List { 60 | Section { 61 | ForEach(ejectList.filteredPlugIns) { plugin in 62 | if #available(iOS 16.0, *) { 63 | PlugInCell(plugIn: plugin) 64 | .environmentObject(ejectList) 65 | } else { 66 | // let _ = DDLogInfo("[EjectListView] 插件: \(plugin.url.path), \(ejectList.app.name)") 67 | PlugInCell(plugIn: plugin) 68 | .environmentObject(ejectList) 69 | .padding(.vertical, 4) 70 | } 71 | } 72 | .onDelete(perform: delete) 73 | } header: { 74 | Text(ejectList.filteredPlugIns.isEmpty 75 | ? NSLocalizedString("No Injected Plug-Ins", comment: "") 76 | : NSLocalizedString("Injected Plug-Ins", comment: "")) 77 | .font(.footnote) 78 | } 79 | 80 | if !ejectList.filter.isSearching && !ejectList.filteredPlugIns.isEmpty { 81 | Section { 82 | deleteAllButton 83 | .disabled(isDeletingAll) 84 | .foregroundColor(isDeletingAll ? .secondary : .red) 85 | } footer: { 86 | if ejectList.app.isFromTroll { 87 | Text(NSLocalizedString("Some plug-ins were not injected by TrollFools, please eject them with caution.", comment: "")) 88 | .font(.footnote) 89 | } 90 | } 91 | } 92 | } 93 | .listStyle(.insetGrouped) 94 | .navigationTitle(NSLocalizedString("Plug-Ins", comment: "")) 95 | .animation(.easeOut, value: ejectList.filter.isSearching) 96 | .background(Group { 97 | NavigationLink(isActive: $isErrorOccurred) { 98 | FailureView( 99 | title: NSLocalizedString("Error", comment: ""), 100 | error: lastError 101 | ) 102 | } label: { } 103 | }) 104 | .onViewWillAppear { viewController in 105 | viewControllerHost.viewController = viewController 106 | } 107 | } 108 | 109 | var exportButton: some View { 110 | Button(action: { 111 | Task { 112 | await exportPlugIns() 113 | } 114 | }) { 115 | Image(systemName: "square.and.arrow.up") 116 | } 117 | .disabled(ejectList.filteredPlugIns.isEmpty) 118 | } 119 | 120 | func exportPlugIns() async { 121 | guard !ejectList.filteredPlugIns.isEmpty else { return } 122 | 123 | let dateFormatter = DateFormatter() 124 | dateFormatter.dateFormat = "MMdd-HHmmss" 125 | let timestamp = dateFormatter.string(from: Date()) 126 | let zipFileName = "\(ejectList.app.name)Plugins_\(timestamp)" 127 | let zipFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(zipFileName).zip") 128 | 129 | do { 130 | let fileManager = FileManager.default 131 | let archive = try Archive(url: zipFileURL, accessMode: .create) 132 | 133 | for plugin in ejectList.filteredPlugIns { 134 | let entryPath = "\(plugin.url.lastPathComponent)" 135 | 136 | guard fileManager.fileExists(atPath: plugin.url.path) else { 137 | DDLogInfo("File not found: \(plugin.url.path)") 138 | continue 139 | } 140 | 141 | try archive.addEntry( 142 | with: entryPath, 143 | fileURL: plugin.url, 144 | compressionMethod: .deflate 145 | ) 146 | } 147 | 148 | await MainActor.run { 149 | showDocumentPicker(for: zipFileURL) 150 | } 151 | } catch { 152 | DDLogInfo("Export error: \(error)") 153 | await MainActor.run { 154 | lastError = error 155 | isErrorOccurred = true 156 | } 157 | } 158 | } 159 | 160 | func showDocumentPicker(for url: URL) { 161 | guard let viewController = viewControllerHost.viewController else { return } 162 | 163 | let documentPicker = UIDocumentPickerViewController(forExporting: [url]) 164 | documentPicker.modalPresentationStyle = .formSheet 165 | 166 | viewController.present(documentPicker, animated: true) 167 | } 168 | 169 | var body: some View { 170 | if #available(iOS 15.0, *) { 171 | ejectListView 172 | .refreshable { 173 | withAnimation { 174 | ejectList.reload() 175 | } 176 | } 177 | .searchable( 178 | text: $ejectList.filter.searchKeyword, 179 | placement: .automatic, 180 | prompt: NSLocalizedString("Search…", comment: "") 181 | ) 182 | .textInputAutocapitalization(.never) 183 | .autocorrectionDisabled(true) 184 | .toolbar { 185 | ToolbarItem(placement: .navigationBarTrailing) { 186 | exportButton 187 | } 188 | } 189 | } else { 190 | ejectListView 191 | } 192 | } 193 | 194 | func delete(at offsets: IndexSet) { 195 | do { 196 | let plugInsToRemove = offsets.map { ejectList.filteredPlugIns[$0] } 197 | let plugInURLsToRemove = plugInsToRemove.map { $0.url } 198 | try InjectorV3(ejectList.app.url).eject(plugInURLsToRemove) 199 | 200 | ejectList.app.reload() 201 | ejectList.reload() 202 | } catch { 203 | DDLogError("\(error)", ddlog: InjectorV3.main.logger) 204 | lastError = error 205 | isErrorOccurred = true 206 | } 207 | } 208 | 209 | func deleteAll() { 210 | do { 211 | let injector = try InjectorV3(ejectList.app.url) 212 | 213 | let view = viewControllerHost.viewController? 214 | .navigationController?.view 215 | 216 | view?.isUserInteractionEnabled = false 217 | 218 | withAnimation { 219 | isDeletingAll = true 220 | } 221 | 222 | DispatchQueue.global(qos: .userInteractive).async { 223 | defer { 224 | DispatchQueue.main.async { 225 | withAnimation { 226 | ejectList.app.reload() 227 | ejectList.reload() 228 | isDeletingAll = false 229 | } 230 | 231 | view?.isUserInteractionEnabled = true 232 | } 233 | } 234 | 235 | do { 236 | try injector.ejectAll() 237 | } catch { 238 | DispatchQueue.main.async { 239 | withAnimation { 240 | isDeletingAll = false 241 | } 242 | 243 | DDLogError("\(error)", ddlog: InjectorV3.main.logger) 244 | lastError = error 245 | isErrorOccurred = true 246 | } 247 | } 248 | } 249 | } catch { 250 | lastError = error 251 | isErrorOccurred = true 252 | } 253 | } 254 | } 255 | 256 | struct ShareSheet: UIViewControllerRepresentable { 257 | let activityItems: [Any] 258 | 259 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { 260 | let controller = UIActivityViewController( 261 | activityItems: activityItems, 262 | applicationActivities: nil 263 | ) 264 | 265 | if #available(iOS 15.0, *) { 266 | if let sheet = controller.sheetPresentationController { 267 | sheet.detents = [.medium()] 268 | sheet.prefersGrabberVisible = true 269 | } 270 | } 271 | 272 | return controller 273 | } 274 | 275 | func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {} 276 | } 277 | 278 | struct ZIPDocument: FileDocument { 279 | var url: URL 280 | 281 | static var readableContentTypes: [UTType] { [.zip] } 282 | 283 | init(url: URL) { 284 | self.url = url 285 | } 286 | 287 | init(configuration: ReadConfiguration) throws { 288 | url = URL(fileURLWithPath: "") 289 | } 290 | 291 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { 292 | return try FileWrapper(url: url, options: .immediate) 293 | } 294 | } 295 | 296 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Bundle.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/10. 6 | // 7 | 8 | import OrderedCollections 9 | import CocoaLumberjackSwift 10 | 11 | extension InjectorV3 { 12 | 13 | // MARK: - Constants 14 | 15 | static let ignoredDylibAndFrameworkNames: Set = [ 16 | "cydiasubstrate", 17 | "cydiasubstrate.framework", 18 | "CydiaSubstrate", 19 | "CydiaSubstrate.framework", 20 | "ellekit", 21 | "ellekit.framework", 22 | "ElleKit", 23 | "ElleKit.framework", 24 | "libsubstrate.dylib", 25 | "libSubstrate.dylib", 26 | "libsubstitute.dylib", 27 | "libSubstitute.dylib", 28 | "libellekit.dylib", 29 | "libElleKit.dylib", 30 | ] 31 | 32 | static let substrateName = "CydiaSubstrate" 33 | static let substrateFwkName = "CydiaSubstrate.framework" 34 | 35 | fileprivate static let infoPlistName = "Info.plist" 36 | fileprivate static let injectedMarkerName = ".troll-fools" 37 | 38 | // MARK: - Instance Methods 39 | 40 | var hasInjectedAsset: Bool { 41 | !injectedAssetURLsInBundle(bundleURL).isEmpty 42 | } 43 | 44 | // MARK: - Shared Methods 45 | 46 | func frameworkMachOsInBundle(_ target: URL) throws -> OrderedSet { 47 | 48 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 49 | 50 | let executableURL = try locateExecutableInBundle(target) 51 | precondition(isMachO(executableURL), "Not a Mach-O: \(executableURL.path)") 52 | 53 | let frameworksURL = target.appendingPathComponent("Frameworks") 54 | let linkedDylibs = try linkedDylibsRecursivelyOfMachO(executableURL) 55 | 56 | var enumeratedURLs = OrderedSet() 57 | if let enumerator = FileManager.default.enumerator( 58 | at: frameworksURL, 59 | includingPropertiesForKeys: [.fileSizeKey], 60 | options: [.skipsHiddenFiles] 61 | ) { 62 | for case let itemURL as URL in enumerator { 63 | if checkIsInjectedBundle(itemURL) || enumerator.level > 2 { 64 | enumerator.skipDescendants() 65 | continue 66 | } 67 | if enumerator.level == 2 { 68 | enumeratedURLs.append(itemURL) 69 | } 70 | } 71 | } 72 | 73 | let machOs = linkedDylibs.intersection(enumeratedURLs) 74 | var sortedMachOs: [URL] = 75 | switch injectStrategy.wrappedValue { 76 | case .lexicographic: 77 | machOs.sorted { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending } 78 | case .fast: 79 | try machOs 80 | .sorted { url1, url2 in 81 | let size1 = (try url1.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0 82 | let size2 = (try url2.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0 83 | return if size1 == size2 { 84 | url1.lastPathComponent.localizedStandardCompare(url2.lastPathComponent) == .orderedAscending 85 | } else { 86 | size1 < size2 87 | } 88 | } 89 | case .preorder: 90 | machOs.elements 91 | case .postorder: 92 | machOs.reversed() 93 | } 94 | 95 | if let appIndex = sortedMachOs.firstIndex(where: { $0.lastPathComponent == "App" }) { 96 | let appURL = sortedMachOs.remove(at: appIndex) 97 | sortedMachOs.insert(appURL, at: 0) 98 | } 99 | 100 | DDLogWarn("Strategy \(injectStrategy.wrappedValue.rawValue)", ddlog: logger) 101 | DDLogInfo("Sorted Mach-Os \(sortedMachOs.map { $0.lastPathComponent })", ddlog: logger) 102 | 103 | if preferMainExecutable.wrappedValue { 104 | sortedMachOs.insert(executableURL, at: 0) 105 | DDLogWarn("Prefer main executable", ddlog: logger) 106 | } else { 107 | sortedMachOs.append(executableURL) 108 | } 109 | 110 | return OrderedSet(sortedMachOs) 111 | } 112 | 113 | func injectedAssetURLsInBundle(_ target: URL) -> [URL] { 114 | return (injectedBundleURLsInBundle(target) + injectedDylibAndFrameworkURLsInBundle(target)) 115 | .sorted(by: { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }) 116 | } 117 | 118 | func injectedBundleURLsInBundle(_ target: URL) -> [URL] { 119 | 120 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 121 | 122 | guard let bundleContentURLs = try? FileManager.default.contentsOfDirectory(at: target, includingPropertiesForKeys: [.isDirectoryKey]) else { 123 | return [] 124 | } 125 | 126 | let bundleURLs = bundleContentURLs 127 | .filter { 128 | $0.pathExtension.lowercased() == "bundle" && 129 | !Self.ignoredDylibAndFrameworkNames.contains($0.lastPathComponent) 130 | } 131 | .filter { 132 | (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false 133 | } 134 | .filter { 135 | checkIsInjectedBundle($0) 136 | } 137 | .sorted(by: { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }) 138 | 139 | return bundleURLs 140 | } 141 | 142 | func injectedDylibAndFrameworkURLsInBundle(_ target: URL) -> [URL] { 143 | 144 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 145 | 146 | let frameworksURL = target.appendingPathComponent("Frameworks") 147 | guard let frameworksContentURLs = try? FileManager.default.contentsOfDirectory(at: frameworksURL, includingPropertiesForKeys: nil) else { 148 | return [] 149 | } 150 | 151 | let dylibURLs = frameworksContentURLs 152 | .filter { 153 | $0.pathExtension.lowercased() == "dylib" && 154 | !$0.lastPathComponent.hasPrefix("libswift") && 155 | !Self.ignoredDylibAndFrameworkNames.contains($0.lastPathComponent) 156 | } 157 | .sorted(by: { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }) 158 | 159 | let frameworkURLs = frameworksContentURLs 160 | .filter { 161 | $0.pathExtension.lowercased() == "framework" && 162 | !Self.ignoredDylibAndFrameworkNames.contains($0.lastPathComponent) 163 | } 164 | .filter { 165 | (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false 166 | } 167 | .filter { 168 | checkIsInjectedBundle($0) 169 | } 170 | .sorted(by: { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }) 171 | 172 | return dylibURLs + frameworkURLs 173 | } 174 | 175 | func markBundlesAsInjected(_ bundleURLs: [URL], privileged: Bool) throws { 176 | 177 | let filteredURLs = bundleURLs.filter { checkIsBundle($0) } 178 | precondition(filteredURLs.count == bundleURLs.count, "Not all urls are bundles") 179 | 180 | if privileged { 181 | let markerURL = temporaryDirectoryURL.appendingPathComponent(Self.injectedMarkerName) 182 | try Data().write(to: markerURL, options: .atomic) 183 | try cmdChangeOwnerToInstalld(markerURL, recursively: false) 184 | 185 | try filteredURLs.forEach { 186 | try cmdCopy( 187 | from: markerURL, 188 | to: $0.appendingPathComponent(Self.injectedMarkerName), 189 | clone: true, 190 | overwrite: true 191 | ) 192 | } 193 | } else { 194 | try filteredURLs.forEach { 195 | try Data().write(to: $0.appendingPathComponent(Self.injectedMarkerName), options: .atomic) 196 | } 197 | } 198 | } 199 | 200 | func identifierOfBundle(_ target: URL) throws -> String { 201 | 202 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 203 | 204 | if let bundleIdentifier = Bundle(url: target)?.bundleIdentifier { 205 | return bundleIdentifier 206 | } 207 | 208 | let infoPlistURL = target.appendingPathComponent(Self.infoPlistName) 209 | let infoPlistData = try Data(contentsOf: infoPlistURL) 210 | 211 | guard let infoPlist = try PropertyListSerialization.propertyList(from: infoPlistData, options: [], format: nil) as? [String: Any] 212 | else { 213 | throw Error.generic(String(format: NSLocalizedString("Failed to parse: %@", comment: ""), infoPlistURL.path)) 214 | } 215 | 216 | guard let bundleIdentifier = infoPlist["CFBundleIdentifier"] as? String else { 217 | throw Error.generic(String(format: NSLocalizedString("Failed to find entry CFBundleIdentifier in: %@", comment: ""), infoPlistURL.path)) 218 | } 219 | 220 | return bundleIdentifier 221 | } 222 | 223 | func locateFrameworksDirectoryInBundle(_ target: URL) throws -> URL { 224 | 225 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 226 | 227 | let frameworksDirectoryURL = target.appendingPathComponent("Frameworks") 228 | if !FileManager.default.fileExists(atPath: frameworksDirectoryURL.path) { 229 | try? cmdMakeDirectory(at: frameworksDirectoryURL) 230 | } 231 | 232 | return frameworksDirectoryURL 233 | } 234 | 235 | func locateExecutableInBundle(_ target: URL) throws -> URL { 236 | 237 | precondition(checkIsBundle(target), "Not a bundle: \(target.path)") 238 | 239 | if let executableURL = Bundle(url: target)?.executableURL { 240 | return executableURL 241 | } 242 | 243 | let infoPlistURL = target.appendingPathComponent(Self.infoPlistName) 244 | let infoPlistData = try Data(contentsOf: infoPlistURL) 245 | 246 | guard let infoPlist = try PropertyListSerialization.propertyList(from: infoPlistData, options: [], format: nil) as? [String: Any] 247 | else { 248 | throw Error.generic(String(format: NSLocalizedString("Failed to parse: %@", comment: ""), infoPlistURL.path)) 249 | } 250 | 251 | guard let executableName = infoPlist["CFBundleExecutable"] as? String else { 252 | throw Error.generic(String(format: NSLocalizedString("Failed to find entry CFBundleExecutable in: %@", comment: ""), infoPlistURL.path)) 253 | } 254 | 255 | let executableURL = target.appendingPathComponent(executableName) 256 | guard FileManager.default.fileExists(atPath: executableURL.path) else { 257 | throw Error.generic(String(format: NSLocalizedString("Failed to locate main executable: %@", comment: ""), executableURL.path)) 258 | } 259 | 260 | return executableURL 261 | } 262 | 263 | func checkIsEligibleAppBundle(_ target: URL) -> Bool { 264 | guard checkIsBundle(target) else { 265 | return false 266 | } 267 | 268 | let frameworksURL = target.appendingPathComponent("Frameworks") 269 | return !((try? FileManager.default.contentsOfDirectory(at: frameworksURL, includingPropertiesForKeys: nil).isEmpty) ?? true) 270 | } 271 | 272 | func checkIsInjectedAppBundle(_ target: URL) -> Bool { 273 | guard checkIsBundle(target) else { 274 | return false 275 | } 276 | 277 | let frameworksURL = target.appendingPathComponent("Frameworks") 278 | let substrateFwkURL = frameworksURL.appendingPathComponent(Self.substrateFwkName) 279 | 280 | return FileManager.default.fileExists(atPath: substrateFwkURL.path) 281 | } 282 | 283 | func checkIsInjectedBundle(_ target: URL) -> Bool { 284 | guard checkIsBundle(target) else { 285 | return false 286 | } 287 | 288 | let markerURL = target.appendingPathComponent(Self.injectedMarkerName) 289 | return FileManager.default.fileExists(atPath: markerURL.path) 290 | } 291 | 292 | func checkIsBundle(_ target: URL) -> Bool { 293 | let values = try? target.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey]) 294 | let isDirectory = values?.isDirectory ?? false 295 | let isPackage = values?.isPackage ?? false 296 | let pathExt = target.pathExtension.lowercased() 297 | return isPackage || (isDirectory && (pathExt == "app" || pathExt == "bundle" || pathExt == "framework")) 298 | } 299 | 300 | func checkIsDirectory(_ target: URL) -> Bool { 301 | let values = try? target.resourceValues(forKeys: [.isDirectoryKey]) 302 | return values?.isDirectory ?? false 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /TrollFools/AppListCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppListCell.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2024/10/30. 6 | // 7 | 8 | import SwiftUI 9 | import CocoaLumberjackSwift 10 | import ZIPFoundation 11 | 12 | struct AppListCell: View { 13 | @EnvironmentObject var appList: AppListModel 14 | 15 | @StateObject var app: App 16 | @State var isErrorOccurred: Bool = false 17 | @State var lastError: Error? 18 | @State var isInjectingConfiguration: Bool = false 19 | @State var latestBackupURL: URL? 20 | 21 | @available(iOS 15.0, *) 22 | var highlightedName: AttributedString { 23 | let name = app.name 24 | var attributedString = AttributedString(name) 25 | if let range = attributedString.range(of: appList.filter.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) { 26 | attributedString[range].foregroundColor = .accentColor 27 | } 28 | return attributedString 29 | } 30 | 31 | @available(iOS 15.0, *) 32 | var highlightedId: AttributedString { 33 | let id = app.id 34 | var attributedString = AttributedString(id) 35 | if let range = attributedString.range(of: appList.filter.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) { 36 | attributedString[range].foregroundColor = .accentColor 37 | } 38 | return attributedString 39 | } 40 | 41 | @ViewBuilder 42 | var cellContextMenu: some View { 43 | Button { 44 | launch() 45 | } label: { 46 | Label(NSLocalizedString("Launch", comment: ""), systemImage: "command") 47 | } 48 | 49 | Button { 50 | openInFilza() 51 | } label: { 52 | if isFilzaInstalled { 53 | Label(NSLocalizedString("Show in Filza", comment: ""), systemImage: "scope") 54 | } else { 55 | Label(NSLocalizedString("Filza (URL Scheme) Not Installed", comment: ""), systemImage: "xmark.octagon") 56 | } 57 | } 58 | .disabled(!isFilzaInstalled) 59 | 60 | if AppListModel.hasTrollStore && app.isAllowedToAttachOrDetach { 61 | if app.isDetached { 62 | Button { 63 | do { 64 | _ = try InjectorV3(app.url) 65 | try InjectorV3(app.url).setMetadataDetached(false) 66 | withAnimation { 67 | app.reload() 68 | appList.isRebuildNeeded = true 69 | } 70 | } catch { DDLogError("\(error)", ddlog: InjectorV3.main.logger) } 71 | } label: { 72 | Label(NSLocalizedString("Unlock Version", comment: ""), systemImage: "lock.open") 73 | } 74 | } else { 75 | Button { 76 | do { 77 | _ = try InjectorV3(app.url) 78 | try InjectorV3(app.url).setMetadataDetached(true) 79 | withAnimation { 80 | app.reload() 81 | appList.isRebuildNeeded = true 82 | } 83 | } catch { DDLogError("\(error)", ddlog: InjectorV3.main.logger) } 84 | } label: { 85 | Label(NSLocalizedString("Lock Version", comment: ""), systemImage: "lock") 86 | } 87 | } 88 | 89 | Button { 90 | clearAppCache() 91 | } label: { 92 | Label(NSLocalizedString("Clear App Cache", comment: ""), systemImage: "trash") 93 | } 94 | Button { 95 | useLastConfiguration() 96 | } label: { 97 | Label(NSLocalizedString("Use Last Configuration", comment: ""), systemImage: "gear") 98 | } 99 | } 100 | } 101 | 102 | @ViewBuilder 103 | var cellContextMenuWrapper: some View { 104 | if #available(iOS 16.0, *) { 105 | // iOS 16 106 | cellContextMenu 107 | } else { 108 | if #available(iOS 15.0, *) { } 109 | else { 110 | // iOS 14 111 | cellContextMenu 112 | } 113 | } 114 | } 115 | 116 | @ViewBuilder 117 | var cellBackground: some View { 118 | if #available(iOS 15.0, *) { 119 | if #available(iOS 16.0, *) { } 120 | else { 121 | // iOS 15 122 | Color.clear 123 | .contextMenu { 124 | if !appList.isSelectorMode { 125 | cellContextMenu 126 | } 127 | } 128 | .id(app.isDetached) 129 | } 130 | } 131 | } 132 | 133 | var body: some View { 134 | HStack(spacing: 12) { 135 | Image(uiImage: app.alternateIcon ?? app.icon ?? UIImage()) 136 | .resizable() 137 | .frame(width: 32, height: 32) 138 | 139 | VStack(alignment: .leading, spacing: 2) { 140 | HStack(spacing: 4) { 141 | if #available(iOS 15.0, *) { 142 | Text(highlightedName) 143 | .font(.headline) 144 | .lineLimit(1) 145 | } else { 146 | Text(app.name) 147 | .font(.headline) 148 | .lineLimit(1) 149 | } 150 | 151 | if app.isInjected { 152 | Image(systemName: "bandage") 153 | .font(.subheadline) 154 | .foregroundColor(.orange) 155 | .accessibilityLabel(NSLocalizedString("Patched", comment: "")) 156 | .transition(.opacity) 157 | .animation(.easeInOut, value: app.isInjected) 158 | } 159 | } 160 | 161 | if #available(iOS 15.0, *) { 162 | Text(highlightedId) 163 | .font(.subheadline) 164 | .lineLimit(1) 165 | } else { 166 | Text(app.id) 167 | .font(.subheadline) 168 | .lineLimit(1) 169 | } 170 | } 171 | 172 | Spacer() 173 | 174 | if let version = app.version { 175 | if app.isUser && app.isDetached { 176 | HStack(spacing: 4) { 177 | Image(systemName: "lock") 178 | .font(.subheadline) 179 | .foregroundColor(.red) 180 | .accessibilityLabel(NSLocalizedString("Pinned Version", comment: "")) 181 | 182 | Text(version) 183 | .font(.subheadline) 184 | .foregroundColor(.secondary) 185 | .lineLimit(1) 186 | } 187 | } else { 188 | Text(version) 189 | .font(.subheadline) 190 | .foregroundColor(.secondary) 191 | .lineLimit(1) 192 | } 193 | } 194 | } 195 | .contextMenu { 196 | if !appList.isSelectorMode { 197 | cellContextMenuWrapper 198 | } 199 | } 200 | .background(cellBackground) 201 | .background( 202 | ZStack { 203 | if isInjectingConfiguration, let backupURL = latestBackupURL { 204 | NavigationLink(destination: InjectView(app, urlList: [backupURL]), isActive: $isInjectingConfiguration) { 205 | EmptyView() 206 | } 207 | .frame(width: 0, height: 0) 208 | .hidden() 209 | } 210 | 211 | if isErrorOccurred { 212 | NavigationLink(destination: FailureView( 213 | title: NSLocalizedString("Error", comment: ""), 214 | error: lastError 215 | ), isActive: $isErrorOccurred) { 216 | EmptyView() 217 | } 218 | .frame(width: 0, height: 0) 219 | .hidden() 220 | } 221 | } 222 | ) 223 | } 224 | 225 | private func launch() { 226 | LSApplicationWorkspace.default().openApplication(withBundleID: app.id) 227 | } 228 | 229 | var isFilzaInstalled: Bool { appList.isFilzaInstalled } 230 | 231 | private func openInFilza() { 232 | appList.openInFilza(app.url) 233 | } 234 | 235 | private func useLastConfiguration() { 236 | do { 237 | let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 238 | let backupURL = documentsURL.appendingPathComponent("TFPlugInsBackups", isDirectory: true) 239 | 240 | guard FileManager.default.fileExists(atPath: backupURL.path) else { 241 | DDLogError("Backup directory not found", ddlog: InjectorV3.main.logger) 242 | let error = NSError( 243 | domain: gTrollFoolsErrorDomain, 244 | code: 1, 245 | userInfo: [NSLocalizedDescriptionKey: "Backup directory not found. Please inject plugins first."] 246 | ) 247 | throw error 248 | } 249 | 250 | let fileURLs: [URL] 251 | do { 252 | fileURLs = try FileManager.default.contentsOfDirectory( 253 | at: backupURL, 254 | includingPropertiesForKeys: [.contentModificationDateKey], 255 | options: .skipsHiddenFiles 256 | ) 257 | } catch { 258 | DDLogError("Failed to read backup directory: \(error)", ddlog: InjectorV3.main.logger) 259 | let readError = NSError( 260 | domain: gTrollFoolsErrorDomain, 261 | code: 3, 262 | userInfo: [NSLocalizedDescriptionKey: "Failed to read backup directory: \(error.localizedDescription)"] 263 | ) 264 | throw readError 265 | } 266 | 267 | let appBackups = fileURLs.filter { url in 268 | url.lastPathComponent.hasPrefix("\(app.name)Plugins_") && 269 | url.pathExtension.lowercased() == "zip" 270 | } 271 | 272 | guard !appBackups.isEmpty else { 273 | DDLogError("No backup found for \(app.name)", ddlog: InjectorV3.main.logger) 274 | let error = NSError( 275 | domain: gTrollFoolsErrorDomain, 276 | code: 2, 277 | userInfo: [NSLocalizedDescriptionKey: "No backup found for \(app.name). Please inject plugins first."] 278 | ) 279 | throw error 280 | } 281 | 282 | let sortedBackups: [URL] 283 | do { 284 | sortedBackups = try appBackups.sorted { url1, url2 in 285 | let date1 = try url1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate ?? Date.distantPast 286 | let date2 = try url2.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate ?? Date.distantPast 287 | return date1 > date2 288 | } 289 | } catch { 290 | DDLogError("Failed to sort backups: \(error)", ddlog: InjectorV3.main.logger) 291 | let sortError = NSError( 292 | domain: gTrollFoolsErrorDomain, 293 | code: 4, 294 | userInfo: [NSLocalizedDescriptionKey: "Failed to sort backups: \(error.localizedDescription)"] 295 | ) 296 | throw sortError 297 | } 298 | 299 | let latestBackup = sortedBackups[0] 300 | DDLogInfo("Found latest backup: \(latestBackup.path)", ddlog: InjectorV3.main.logger) 301 | 302 | latestBackupURL = latestBackup 303 | isInjectingConfiguration = true 304 | 305 | } catch { 306 | DDLogError("\(error)", ddlog: InjectorV3.main.logger) 307 | lastError = error 308 | isErrorOccurred = true 309 | } 310 | } 311 | 312 | private func clearAppCache() { 313 | do { 314 | let injector = try InjectorV3(app.url) 315 | 316 | let cachePaths = [ 317 | URL(fileURLWithPath: app.dataurl.path.replacingOccurrences(of: "/private/", with: "")).appendingPathComponent("Library/Caches"), 318 | URL(fileURLWithPath: app.dataurl.path.replacingOccurrences(of: "/private/", with: "")).appendingPathComponent("tmp") 319 | ] 320 | 321 | for path in cachePaths { 322 | DDLogInfo("Cleaning path: \(path.path)", ddlog: InjectorV3.main.logger) 323 | try injector.cmdRemove(path, recursively: true) 324 | } 325 | 326 | DDLogInfo("Cache cleanup completed", ddlog: InjectorV3.main.logger) 327 | } catch { 328 | DDLogError("Failed to clean cache: \(error.localizedDescription)", ddlog: InjectorV3.main.logger) 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /TrollFools/InjectorV3+Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectorV3+Command.swift 3 | // TrollFools 4 | // 5 | // Created by 82Flex on 2025/1/9. 6 | // 7 | 8 | import MachOKit 9 | 10 | extension InjectorV3 { 11 | 12 | // MARK: - chown 13 | 14 | fileprivate static let chownBinaryURL = Bundle.main.url(forResource: "chown", withExtension: nil)! 15 | 16 | func cmdChangeOwner(_ target: URL, owner: String, groupOwner: String? = nil, recursively: Bool = false) throws { 17 | var args = [String]() 18 | if recursively { 19 | args.append("-R") 20 | } 21 | if let groupOwner { 22 | args.append(String(format: "%@:%@", owner, groupOwner)) 23 | } else { 24 | args.append(owner) 25 | } 26 | args.append(target.path) 27 | let retCode = try Execute.rootSpawn(binary: Self.chownBinaryURL.path, arguments: args, ddlog: logger) 28 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 29 | try throwCommandFailure("chown", reason: retCode) 30 | } 31 | } 32 | 33 | func cmdChangeOwnerToInstalld(_ target: URL, recursively: Bool = false) throws { 34 | try cmdChangeOwner(target, owner: "33", groupOwner: "33", recursively: recursively) // _installd 35 | } 36 | 37 | // MARK: - cp 38 | 39 | fileprivate static let cpBinaryURL: URL = { 40 | if #available(iOS 16.0, *) { 41 | Bundle.main.url(forResource: "cp", withExtension: nil)! 42 | } else { 43 | Bundle.main.url(forResource: "cp-15", withExtension: nil)! 44 | } 45 | }() 46 | //Mark: - Decompose Deb 47 | fileprivate static let composeBinaryURL: URL = { 48 | if #available(iOS 16.0, *) { 49 | Bundle.main.url(forResource: "composedeb", withExtension: nil)! 50 | } else { 51 | Bundle.main.url(forResource: "composedeb-15", withExtension: nil)! 52 | } 53 | }() 54 | 55 | func cmdCopy(from srcURL: URL, to destURL: URL, clone: Bool = false, overwrite: Bool = false) throws { 56 | if overwrite { 57 | try? cmdRemove(destURL, recursively: true) 58 | } 59 | var args = [String]() 60 | if clone { 61 | args.append("--reflink=auto") 62 | } 63 | args += ["-rfp", srcURL.path, destURL.path] 64 | let retCode = try Execute.rootSpawn(binary: Self.cpBinaryURL.path, arguments: args, ddlog: logger) 65 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 66 | try throwCommandFailure("cp", reason: retCode) 67 | } 68 | } 69 | 70 | // MARK: - ldid 71 | 72 | fileprivate static let ldidBinaryURL: URL = { 73 | if #available(iOS 15.0, *) { 74 | Bundle.main.url(forResource: "ldid", withExtension: nil)! 75 | } else { 76 | Bundle.main.url(forResource: "ldid-14", withExtension: nil)! 77 | } 78 | }() 79 | 80 | func cmdPseudoSign(_ target: URL, force: Bool = false) throws { 81 | 82 | var hasCodeSign = false 83 | var preservesEntitlements = false 84 | 85 | let targetFile = try MachOKit.loadFromFile(url: target) 86 | switch targetFile { 87 | case .machO(let machOFile): 88 | preservesEntitlements = machOFile.header.fileType == .execute 89 | for command in machOFile.loadCommands { 90 | switch command { 91 | case .codeSignature(_): 92 | hasCodeSign = true 93 | break 94 | default: 95 | continue 96 | } 97 | } 98 | case .fat(let fatFile): 99 | let machOFiles = try fatFile.machOFiles() 100 | for machOFile in machOFiles { 101 | preservesEntitlements = machOFile.header.fileType == .execute 102 | for command in machOFile.loadCommands { 103 | switch command { 104 | case .codeSignature(_): 105 | hasCodeSign = true 106 | break 107 | default: 108 | continue 109 | } 110 | } 111 | } 112 | } 113 | 114 | guard force || !hasCodeSign else { 115 | return 116 | } 117 | 118 | if preservesEntitlements { 119 | 120 | var receipt: AuxiliaryExecute.ExecuteReceipt 121 | 122 | receipt = try Execute.rootSpawnWithOutputs(binary: Self.ldidBinaryURL.path, arguments: [ 123 | "-e", target.path, 124 | ], ddlog: logger) 125 | 126 | guard case .exit(let code) = receipt.terminationReason, code == EXIT_SUCCESS else { 127 | try throwCommandFailure("ldid", reason: receipt.terminationReason) 128 | } 129 | 130 | let xmlContent = receipt.stdout 131 | let xmlURL = temporaryDirectoryURL 132 | .appendingPathComponent("\(UUID().uuidString)_\(target.lastPathComponent)") 133 | .appendingPathExtension("xml") 134 | 135 | try xmlContent.write(to: xmlURL, atomically: true, encoding: .utf8) 136 | 137 | receipt = try Execute.rootSpawnWithOutputs(binary: Self.ldidBinaryURL.path, arguments: [ 138 | "-S\(xmlURL.path)", target.path, 139 | ], ddlog: logger) 140 | 141 | guard case .exit(let code) = receipt.terminationReason, code == EXIT_SUCCESS else { 142 | try throwCommandFailure("ldid", reason: receipt.terminationReason) 143 | } 144 | } else { 145 | 146 | let retCode = try Execute.rootSpawn(binary: Self.ldidBinaryURL.path, arguments: [ 147 | "-S", target.path, 148 | ], ddlog: logger) 149 | 150 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 151 | try throwCommandFailure("ldid", reason: retCode) 152 | } 153 | } 154 | } 155 | 156 | // MARK: - mkdir 157 | 158 | fileprivate static let mkdirBinaryURL = Bundle.main.url(forResource: "mkdir", withExtension: nil)! 159 | 160 | func cmdMakeDirectory(at target: URL, withIntermediateDirectories: Bool = false) throws { 161 | var args = [String]() 162 | if withIntermediateDirectories { 163 | args.append("-p") 164 | } 165 | args.append(target.path) 166 | let retCode = try Execute.rootSpawn(binary: Self.mkdirBinaryURL.path, arguments: args, ddlog: logger) 167 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 168 | try throwCommandFailure("mkdir", reason: retCode) 169 | } 170 | } 171 | 172 | // MARK: - mv 173 | 174 | fileprivate static let mvBinaryURL: URL = { 175 | if #available(iOS 16.0, *) { 176 | Bundle.main.url(forResource: "mv", withExtension: nil)! 177 | } else { 178 | Bundle.main.url(forResource: "mv-15", withExtension: nil)! 179 | } 180 | }() 181 | 182 | func cmdMove(from srcURL: URL, to destURL: URL, overwrite: Bool = false) throws { 183 | if overwrite { 184 | try? cmdRemove(destURL, recursively: true) 185 | } 186 | var args = [String]() 187 | if overwrite { 188 | args.append("-f") 189 | } 190 | args += [srcURL.path, destURL.path] 191 | let retCode = try Execute.rootSpawn(binary: Self.mvBinaryURL.path, arguments: args, ddlog: logger) 192 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 193 | try throwCommandFailure("mv", reason: retCode) 194 | } 195 | } 196 | 197 | // MARK: - rm 198 | 199 | fileprivate static let rmBinaryURL = Bundle.main.url(forResource: "rm", withExtension: nil)! 200 | 201 | public func cmdRemove(_ target: URL, recursively: Bool = false) throws { 202 | let retCode = try Execute.rootSpawn(binary: Self.rmBinaryURL.path, arguments: [ 203 | recursively ? "-rf" : "-f", target.path], ddlog: logger) 204 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 205 | try throwCommandFailure("rm", reason: retCode) 206 | } 207 | } 208 | 209 | //Mark: - Decompose Deb 210 | public func decomposeDeb(at sourceURL: URL, to destinationURL: URL) throws -> String { 211 | let composedebPath = Bundle.main.url(forResource: "composedeb", withExtension: nil)!.path 212 | let executablePath = (composedebPath as NSString).deletingLastPathComponent 213 | 214 | let environment = [ 215 | "PATH": "\(executablePath):\(ProcessInfo.processInfo.environment["PATH"] ?? "")" 216 | ] 217 | 218 | let logFilePath = destinationURL.appendingPathComponent("decomposeDeb.log").path 219 | let logFileHandle: FileHandle? 220 | 221 | if FileManager.default.fileExists(atPath: logFilePath) { 222 | logFileHandle = FileHandle(forWritingAtPath: logFilePath) 223 | logFileHandle?.seekToEndOfFile() 224 | } else { 225 | FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) 226 | logFileHandle = FileHandle(forWritingAtPath: logFilePath) 227 | } 228 | 229 | guard let logHandle = logFileHandle else { 230 | throw NSError(domain: "DecomposeDebErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create log file handle"]) 231 | } 232 | 233 | func log(_ message: String) { 234 | if let data = (message + "\n").data(using: .utf8) { 235 | logHandle.write(data) 236 | } 237 | } 238 | 239 | do { 240 | log("Starting decomposeDeb for file \(sourceURL.lastPathComponent)") 241 | log("Using composedeb at path \(composedebPath)") 242 | log("Executable path: \(executablePath)") 243 | 244 | let receipt = try Execute.rootSpawnWithOutputs(binary: InjectorV3.composeBinaryURL.path, arguments: [ 245 | sourceURL.path, 246 | destinationURL.path, 247 | Bundle.main.bundlePath, 248 | ], environment: environment) 249 | 250 | guard case .exit(let code) = receipt.terminationReason, code == 0 else { 251 | let errorMessage = "Command failed with reason: \(receipt.terminationReason) and status: \(receipt.terminationReason)" 252 | log(errorMessage) 253 | log("Standard Error: \(receipt.stderr)") 254 | throw NSError(domain: "DecomposeDebErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "Command failed: \(receipt.stderr)"]) 255 | } 256 | 257 | log("Command Output: \(receipt.stdout)") 258 | log("Standard Error: \(receipt.stderr)") 259 | log("Decompose Deb File \(sourceURL.lastPathComponent) done") 260 | // DDLogInfo("Decompose Deb File \(sourceURL.lastPathComponent) done") 261 | 262 | return receipt.stdout 263 | } catch { 264 | log("Error occurred: \(error.localizedDescription)") 265 | throw error 266 | } 267 | } 268 | 269 | // MARK: - ct_bypass 270 | 271 | fileprivate static let ctBypassBinaryURL = Bundle.main.url(forResource: "ct_bypass", withExtension: nil)! 272 | 273 | func cmdCoreTrustBypass(_ target: URL, teamID: String) throws { 274 | try cmdPseudoSign(target) 275 | let retCode = try Execute.rootSpawn(binary: Self.ctBypassBinaryURL.path, arguments: [ 276 | "-r", "-i", target.path, "-t", teamID], ddlog: logger) 277 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 278 | try throwCommandFailure("ct_bypass", reason: retCode) 279 | } 280 | } 281 | 282 | // MARK: - insert_dylib 283 | 284 | fileprivate static let insertDylibBinaryURL = Bundle.main.url(forResource: "insert_dylib", withExtension: nil)! 285 | 286 | func cmdInsertLoadCommandDylib(_ target: URL, name: String, weak: Bool = false) throws { 287 | let dylibs = try loadedDylibsOfMachO(target) 288 | if dylibs.contains(name) { 289 | return 290 | } 291 | var args = [ 292 | name, target.path, 293 | "--inplace", "--overwrite", "--no-strip-codesig", "--all-yes", 294 | ] 295 | if weak { 296 | args.append("--weak") 297 | } 298 | let retCode = try Execute.rootSpawn(binary: Self.insertDylibBinaryURL.path, arguments: args) 299 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 300 | try throwCommandFailure("insert_dylib", reason: retCode) 301 | } 302 | } 303 | 304 | // MARK: - install_name_tool 305 | 306 | fileprivate static let installNameToolBinaryURL = Bundle.main.url(forResource: "install_name_tool", withExtension: nil)! 307 | 308 | func cmdInsertLoadCommandRuntimePath(_ target: URL, name: String) throws { 309 | let rpaths = try runtimePathsOfMachO(target) 310 | if rpaths.contains(name) { 311 | return 312 | } 313 | try cmdPseudoSign(target, force: true) 314 | let retCode = try Execute.rootSpawn(binary: Self.installNameToolBinaryURL.path, arguments: [ 315 | "-add_rpath", name, target.path], ddlog: logger) 316 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 317 | try throwCommandFailure("install_name_tool", reason: retCode) 318 | } 319 | } 320 | 321 | func cmdChangeLoadCommandDylib(_ target: URL, from srcName: String, to destName: String) throws { 322 | try cmdPseudoSign(target, force: true) 323 | let retCode = try Execute.rootSpawn(binary: Self.installNameToolBinaryURL.path, arguments: [ 324 | "-change", srcName, destName, target.path], ddlog: logger) 325 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 326 | try throwCommandFailure("install-name-tool", reason: retCode) 327 | } 328 | } 329 | 330 | // MARK: - optool 331 | 332 | fileprivate static let optoolBinaryURL = Bundle.main.url(forResource: "optool", withExtension: nil)! 333 | 334 | func cmdRemoveLoadCommandDylib(_ target: URL, name: String) throws { 335 | let dylibs = try loadedDylibsOfMachO(target) 336 | guard dylibs.contains(name) else { 337 | return 338 | } 339 | let retCode = try Execute.rootSpawn(binary: Self.optoolBinaryURL.path, arguments: [ 340 | "uninstall", "-p", name, "-t", target.path], ddlog: logger) 341 | guard case .exit(let code) = retCode, code == EXIT_SUCCESS else { 342 | try throwCommandFailure("optool", reason: retCode) 343 | } 344 | } 345 | 346 | // MARK: - Error Handling 347 | 348 | fileprivate func throwCommandFailure(_ command: String, reason: AuxiliaryExecute.TerminationReason) throws -> Never { 349 | switch reason { 350 | case .exit(let code): 351 | throw Error.generic(String(format: NSLocalizedString("%@ exited with code %d", comment: ""), command, code)) 352 | case .uncaughtSignal(let signal): 353 | throw Error.generic(String(format: NSLocalizedString("%@ terminated with signal %d", comment: ""), command, signal)) 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /TrollFools/AuxiliaryExecute+Spawn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuxiliaryExecute+Spawn.swift 3 | // AuxiliaryExecute 4 | // 5 | // Created by Lakr Aream on 2021/12/6. 6 | // 7 | 8 | import CocoaLumberjackSwift 9 | import Foundation 10 | 11 | @discardableResult 12 | @_silgen_name("posix_spawn_file_actions_addchdir_np") 13 | private func posix_spawn_file_actions_addchdir_np( 14 | _ attr: UnsafeMutablePointer, 15 | _ dir: UnsafePointer 16 | ) -> Int32 17 | 18 | @discardableResult 19 | @_silgen_name("posix_spawnattr_set_persona_np") 20 | private func posix_spawnattr_set_persona_np( 21 | _ attr: UnsafeMutablePointer, 22 | _ persona_id: uid_t, 23 | _ flags: UInt32 24 | ) -> Int32 25 | 26 | @discardableResult 27 | @_silgen_name("posix_spawnattr_set_persona_uid_np") 28 | private func posix_spawnattr_set_persona_uid_np( 29 | _ attr: UnsafeMutablePointer, 30 | _ persona_id: uid_t 31 | ) -> Int32 32 | 33 | @discardableResult 34 | @_silgen_name("posix_spawnattr_set_persona_gid_np") 35 | private func posix_spawnattr_set_persona_gid_np( 36 | _ attr: UnsafeMutablePointer, 37 | _ persona_id: gid_t 38 | ) -> Int32 39 | 40 | private func WIFEXITED(_ status: Int32) -> Bool { 41 | _WSTATUS(status) == 0 42 | } 43 | 44 | private func _WSTATUS(_ status: Int32) -> Int32 { 45 | status & 0x7f 46 | } 47 | 48 | private func WIFSIGNALED(_ status: Int32) -> Bool { 49 | (_WSTATUS(status) != 0) && (_WSTATUS(status) != 0x7f) 50 | } 51 | 52 | private func WEXITSTATUS(_ status: Int32) -> Int32 { 53 | (status >> 8) & 0xff 54 | } 55 | 56 | private func WTERMSIG(_ status: Int32) -> Int32 { 57 | status & 0x7f 58 | } 59 | 60 | private let POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE = UInt32(1) 61 | 62 | public extension AuxiliaryExecute { 63 | /// call posix spawn to begin execute 64 | /// - Parameters: 65 | /// - command: full path of the binary file. eg: "/bin/cat" 66 | /// - args: arg to pass to the binary, exclude argv[0] which is the path itself. eg: ["nya"] 67 | /// - environment: any environment to be appended/overwrite when calling posix spawn. eg: ["mua" : "nya"] 68 | /// - workingDirectory: chdir 69 | /// - timeout: any wall timeout if lager than 0, in seconds. eg: 6 70 | /// - output: a block call from pipeControlQueue in background when buffer from stdout or stderr available for read 71 | /// - Returns: execution receipt, see it's definition for details 72 | @discardableResult 73 | static func spawn( 74 | command: String, 75 | args: [String] = [], 76 | environment: [String: String] = [:], 77 | workingDirectory: String? = nil, 78 | personaOptions: PersonaOptions? = nil, 79 | timeout: Double = 0, 80 | ddlog: DDLog = .sharedInstance, 81 | setPid: ((pid_t) -> Void)? = nil, 82 | output: ((String) -> Void)? = nil 83 | ) 84 | -> ExecuteReceipt 85 | { 86 | let outputLock = NSLock() 87 | let result = spawn( 88 | command: command, 89 | args: args, 90 | environment: environment, 91 | workingDirectory: workingDirectory, 92 | personaOptions: personaOptions, 93 | timeout: timeout, 94 | ddlog: ddlog, 95 | setPid: setPid 96 | ) { str in 97 | outputLock.lock() 98 | output?(str) 99 | outputLock.unlock() 100 | } stderrBlock: { str in 101 | outputLock.lock() 102 | output?(str) 103 | outputLock.unlock() 104 | } 105 | return result 106 | } 107 | 108 | /// call posix spawn to begin execute and block until the process exits 109 | /// - Parameters: 110 | /// - command: full path of the binary file. eg: "/bin/cat" 111 | /// - args: arg to pass to the binary, exclude argv[0] which is the path itself. eg: ["nya"] 112 | /// - environment: any environment to be appended/overwrite when calling posix spawn. eg: ["mua" : "nya"] 113 | /// - workingDirectory: chdir 114 | /// - timeout: any wall timeout if lager than 0, in seconds. eg: 6 115 | /// - stdout: a block call from pipeControlQueue in background when buffer from stdout available for read 116 | /// - stderr: a block call from pipeControlQueue in background when buffer from stderr available for read 117 | /// - Returns: execution receipt, see it's definition for details 118 | static func spawn( 119 | command: String, 120 | args: [String] = [], 121 | environment: [String: String] = [:], 122 | workingDirectory: String? = nil, 123 | personaOptions: PersonaOptions? = nil, 124 | timeout: Double = 0, 125 | ddlog: DDLog = .sharedInstance, 126 | setPid: ((pid_t) -> Void)? = nil, 127 | stdoutBlock: ((String) -> Void)? = nil, 128 | stderrBlock: ((String) -> Void)? = nil 129 | ) -> ExecuteReceipt { 130 | let sema = DispatchSemaphore(value: 0) 131 | var receipt: ExecuteReceipt! 132 | spawn( 133 | command: command, 134 | args: args, 135 | environment: environment, 136 | workingDirectory: workingDirectory, 137 | personaOptions: personaOptions, 138 | timeout: timeout, 139 | ddlog: ddlog, 140 | setPid: setPid, 141 | stdoutBlock: stdoutBlock, 142 | stderrBlock: stderrBlock 143 | ) { 144 | receipt = $0 145 | sema.signal() 146 | } 147 | sema.wait() 148 | return receipt 149 | } 150 | 151 | /// call posix spawn to begin execute 152 | /// - Parameters: 153 | /// - command: full path of the binary file. eg: "/bin/cat" 154 | /// - args: arg to pass to the binary, exclude argv[0] which is the path itself. eg: ["nya"] 155 | /// - environment: any environment to be appended/overwrite when calling posix spawn. eg: ["mua" : "nya"] 156 | /// - workingDirectory: chdir file action 157 | /// - personaOptions: persona options 158 | /// - timeout: any wall timeout if lager than 0, in seconds. eg: 6 159 | /// - setPid: called sync when pid available 160 | /// - stdoutBlock: a block call from pipeControlQueue in background when buffer from stdout available for read 161 | /// - stderrBlock: a block call from pipeControlQueue in background when buffer from stderr available for read 162 | /// - completionBlock: a block called from processControlQueue or current queue when the process is finished or an error occurred 163 | static func spawn( 164 | command: String, 165 | args: [String] = [], 166 | environment: [String: String] = [:], 167 | workingDirectory: String? = nil, 168 | personaOptions: PersonaOptions? = nil, 169 | timeout: Double = 0, 170 | ddlog: DDLog = .sharedInstance, 171 | setPid: ((pid_t) -> Void)? = nil, 172 | stdoutBlock: ((String) -> Void)? = nil, 173 | stderrBlock: ((String) -> Void)? = nil, 174 | completionBlock: ((ExecuteReceipt) -> Void)? = nil 175 | ) { 176 | // MARK: PREPARE ATTRIBUTE - 177 | 178 | var attrs: posix_spawnattr_t? 179 | posix_spawnattr_init(&attrs) 180 | defer { posix_spawnattr_destroy(&attrs) } 181 | 182 | if let personaOptions { 183 | posix_spawnattr_set_persona_np(&attrs, 99, POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE) 184 | posix_spawnattr_set_persona_uid_np(&attrs, personaOptions.uid) 185 | posix_spawnattr_set_persona_gid_np(&attrs, personaOptions.gid) 186 | } 187 | 188 | // MARK: PREPARE FILE PIPE - 189 | 190 | var pipestdout: [Int32] = [0, 0] 191 | var pipestderr: [Int32] = [0, 0] 192 | 193 | let bufsiz = Int(exactly: BUFSIZ) ?? 65535 194 | 195 | pipe(&pipestdout) 196 | pipe(&pipestderr) 197 | 198 | guard fcntl(pipestdout[0], F_SETFL, O_NONBLOCK) != -1 else { 199 | let receipt = ExecuteReceipt.failure(error: .openFilePipeFailed) 200 | completionBlock?(receipt) 201 | return 202 | } 203 | guard fcntl(pipestderr[0], F_SETFL, O_NONBLOCK) != -1 else { 204 | let receipt = ExecuteReceipt.failure(error: .openFilePipeFailed) 205 | completionBlock?(receipt) 206 | return 207 | } 208 | 209 | // MARK: PREPARE FILE ACTION - 210 | 211 | var fileActions: posix_spawn_file_actions_t? 212 | posix_spawn_file_actions_init(&fileActions) 213 | posix_spawn_file_actions_addclose(&fileActions, pipestdout[0]) 214 | posix_spawn_file_actions_addclose(&fileActions, pipestderr[0]) 215 | posix_spawn_file_actions_adddup2(&fileActions, pipestdout[1], STDOUT_FILENO) 216 | posix_spawn_file_actions_adddup2(&fileActions, pipestderr[1], STDERR_FILENO) 217 | posix_spawn_file_actions_addclose(&fileActions, pipestdout[1]) 218 | posix_spawn_file_actions_addclose(&fileActions, pipestderr[1]) 219 | 220 | if let workingDirectory = workingDirectory { 221 | posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory) 222 | } 223 | 224 | defer { posix_spawn_file_actions_destroy(&fileActions) } 225 | 226 | // MARK: PREPARE ENV - 227 | 228 | var realEnvironmentBuilder: [String] = [] 229 | // before building the environment, we need to read from the existing environment 230 | do { 231 | var envBuilder = [String: String]() 232 | var currentEnv = environ 233 | while let rawStr = currentEnv.pointee { 234 | defer { currentEnv += 1 } 235 | // get the env 236 | let str = String(cString: rawStr) 237 | guard let key = str.components(separatedBy: "=").first else { 238 | continue 239 | } 240 | if !(str.count >= "\(key)=".count) { 241 | continue 242 | } 243 | // this is to aviod any problem with mua=nya=nya= that ending with = 244 | let value = String(str.dropFirst("\(key)=".count)) 245 | envBuilder[key] = value 246 | } 247 | // now, let's overwrite the environment specified in parameters 248 | for (key, value) in environment { 249 | envBuilder[key] = value 250 | } 251 | // now, package those items 252 | for (key, value) in envBuilder { 253 | realEnvironmentBuilder.append("\(key)=\(value)") 254 | } 255 | } 256 | // making it a c shit 257 | let realEnv: [UnsafeMutablePointer?] = realEnvironmentBuilder.map { $0.withCString(strdup) } 258 | defer { for case let env? in realEnv { free(env) } } 259 | 260 | // MARK: PREPARE ARGS - 261 | 262 | let args = [command] + args 263 | let argv: [UnsafeMutablePointer?] = args.map { $0.withCString(strdup) } 264 | defer { for case let arg? in argv { free(arg) } } 265 | 266 | // MARK: NOW POSIX_SPAWN - 267 | 268 | var pid: pid_t = 0 269 | let spawnStatus = posix_spawn(&pid, command, &fileActions, &attrs, argv + [nil], realEnv + [nil]) 270 | if spawnStatus != 0 { 271 | let receipt = ExecuteReceipt.failure(error: .posixSpawnFailed) 272 | completionBlock?(receipt) 273 | return 274 | } 275 | 276 | DDLogInfo("Spawned process \(pid) command \(args.joined(separator: " "))", ddlog: ddlog) 277 | setPid?(pid) 278 | 279 | close(pipestdout[1]) 280 | close(pipestderr[1]) 281 | 282 | var stdoutStr = "" 283 | var stderrStr = "" 284 | 285 | // MARK: OUTPUT BRIDGE - 286 | 287 | let stdoutSource = DispatchSource.makeReadSource(fileDescriptor: pipestdout[0], queue: pipeControlQueue) 288 | let stderrSource = DispatchSource.makeReadSource(fileDescriptor: pipestderr[0], queue: pipeControlQueue) 289 | 290 | let stdoutSem = DispatchSemaphore(value: 0) 291 | let stderrSem = DispatchSemaphore(value: 0) 292 | 293 | stdoutSource.setCancelHandler { 294 | close(pipestdout[0]) 295 | stdoutSem.signal() 296 | } 297 | stderrSource.setCancelHandler { 298 | close(pipestderr[0]) 299 | stderrSem.signal() 300 | } 301 | 302 | stdoutSource.setEventHandler { 303 | let buffer = UnsafeMutablePointer.allocate(capacity: bufsiz) 304 | defer { buffer.deallocate() } 305 | let bytesRead = read(pipestdout[0], buffer, bufsiz) 306 | guard bytesRead > 0 else { 307 | if bytesRead == -1, errno == EAGAIN { 308 | return 309 | } 310 | stdoutSource.cancel() 311 | return 312 | } 313 | 314 | let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)] 315 | array.withUnsafeBufferPointer { ptr in 316 | let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer.self)) 317 | stdoutStr += str 318 | stdoutBlock?(str) 319 | } 320 | } 321 | stderrSource.setEventHandler { 322 | let buffer = UnsafeMutablePointer.allocate(capacity: bufsiz) 323 | defer { buffer.deallocate() } 324 | 325 | let bytesRead = read(pipestderr[0], buffer, bufsiz) 326 | guard bytesRead > 0 else { 327 | if bytesRead == -1, errno == EAGAIN { 328 | return 329 | } 330 | stderrSource.cancel() 331 | return 332 | } 333 | 334 | let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)] 335 | array.withUnsafeBufferPointer { ptr in 336 | let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer.self)) 337 | stderrStr += str 338 | stderrBlock?(str) 339 | } 340 | } 341 | 342 | stdoutSource.resume() 343 | stderrSource.resume() 344 | 345 | // MARK: WAIT + TIMEOUT CONTROL - 346 | 347 | let realTimeout = timeout > 0 ? timeout : maxTimeoutValue 348 | let wallTimeout = DispatchTime.now() + ( 349 | TimeInterval(exactly: realTimeout) ?? maxTimeoutValue 350 | ) 351 | var status: Int32 = 0 352 | var wait: pid_t = 0 353 | var isTimeout = false 354 | 355 | let timerSource = DispatchSource.makeTimerSource(flags: [], queue: processControlQueue) 356 | timerSource.setEventHandler { 357 | isTimeout = true 358 | kill(pid, SIGKILL) 359 | } 360 | 361 | let processSource = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit, queue: processControlQueue) 362 | processSource.setEventHandler { 363 | wait = waitpid(pid, &status, 0) 364 | 365 | processSource.cancel() 366 | timerSource.cancel() 367 | 368 | stdoutSem.wait() 369 | stderrSem.wait() 370 | 371 | let terminationReason: TerminationReason 372 | if WIFSIGNALED(status) { 373 | let signal = WTERMSIG(status) 374 | DDLogError("Process \(pid) terminated with uncaught signal \(signal)", ddlog: ddlog) 375 | terminationReason = .uncaughtSignal(signal) 376 | } else { 377 | assert(WIFEXITED(status)) 378 | 379 | let exitCode = WEXITSTATUS(status) 380 | if exitCode == EXIT_SUCCESS { 381 | DDLogInfo("Process \(pid) exited successfully", ddlog: ddlog) 382 | } else { 383 | DDLogWarn("Process \(pid) exited with code \(exitCode)", ddlog: ddlog) 384 | } 385 | 386 | terminationReason = .exit(exitCode) 387 | } 388 | 389 | // by using exactly method, we won't crash it! 390 | let receipt = ExecuteReceipt( 391 | terminationReason: terminationReason, 392 | pid: Int(exactly: pid) ?? -1, 393 | wait: Int(exactly: wait) ?? -1, 394 | error: isTimeout ? .timeout : nil, 395 | stdout: stdoutStr, 396 | stderr: stderrStr 397 | ) 398 | completionBlock?(receipt) 399 | } 400 | processSource.resume() 401 | 402 | // timeout control 403 | timerSource.schedule(deadline: wallTimeout) 404 | timerSource.resume() 405 | } 406 | } 407 | --------------------------------------------------------------------------------