├── .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 |
--------------------------------------------------------------------------------