mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Supports installing Fugu14-based jailbreaks
If a jailbreak app contains the relevant Fugu14 entries in its Info.plist, AltStore will automatically guide the user through the Fugu14 untether process before installing the jailbreak.
This commit is contained in:
@@ -347,9 +347,16 @@
|
||||
BFF7C90F257844C900E55F36 /* AltXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = BFF7C904257844C900E55F36 /* AltXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
BFF7C920257844FA00E55F36 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
|
||||
BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
|
||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; };
|
||||
D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; };
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; };
|
||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
|
||||
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; };
|
||||
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; };
|
||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; };
|
||||
D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; };
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; };
|
||||
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -797,8 +804,17 @@
|
||||
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; };
|
||||
C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; };
|
||||
D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; };
|
||||
D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; };
|
||||
D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; };
|
||||
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
|
||||
D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = "<group>"; };
|
||||
D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = "<group>"; };
|
||||
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = "<group>"; };
|
||||
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = "<group>"; };
|
||||
D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = "<group>"; };
|
||||
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
|
||||
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -869,8 +885,11 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
|
||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
|
||||
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
||||
BF088D332501A4FF008082D9 /* OpenSSL.xcframework in Frameworks */,
|
||||
D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */,
|
||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
||||
2A77E3D272F3D92436FAC272 /* Pods_AltStore.framework in Frameworks */,
|
||||
);
|
||||
@@ -1341,6 +1360,21 @@
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF7B44062725A4B8005288A4 /* Patch App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D593F1932717749A006E82DE /* PatchAppOperation.swift */,
|
||||
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */,
|
||||
D57DF637271E32F000677701 /* PatchApp.storyboard */,
|
||||
D57DF63D271E51E400677701 /* ALTAppPatcher.h */,
|
||||
D57DF63E271E51E400677701 /* ALTAppPatcher.m */,
|
||||
D533E8B82727B61400A9B5DD /* fragmentzip.h */,
|
||||
D533E8BD2727BBF800A9B5DD /* libcurl.a */,
|
||||
D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */,
|
||||
);
|
||||
path = "Patch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF98916C250AABF3002ACF50 /* AltWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1493,6 +1527,7 @@
|
||||
BFD247852284BB3300981D42 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */,
|
||||
BF088D322501A4FF008082D9 /* OpenSSL.xcframework */,
|
||||
BF580497246A3D19008AE704 /* UIKit.framework */,
|
||||
BF4588872298DD3F00BD7491 /* libxml2.tbd */,
|
||||
@@ -1626,6 +1661,7 @@
|
||||
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */,
|
||||
BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */,
|
||||
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */,
|
||||
BF7B44062725A4B8005288A4 /* Patch App */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
@@ -2101,6 +2137,7 @@
|
||||
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */,
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */,
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||
@@ -2528,6 +2565,7 @@
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
||||
BF41B808233433C100C593A3 /* LoadingState.swift in Sources */,
|
||||
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
|
||||
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */,
|
||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||
BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */,
|
||||
@@ -2541,6 +2579,7 @@
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */,
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||
@@ -2558,6 +2597,7 @@
|
||||
BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */,
|
||||
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
|
||||
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */,
|
||||
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */,
|
||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
||||
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
||||
@@ -3286,6 +3326,7 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = AltStore/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -3293,6 +3334,11 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5b;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Dependencies/fragmentzip",
|
||||
"$(PROJECT_DIR)/Dependencies/libcurl",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -3314,6 +3360,7 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = AltStore/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -3321,6 +3368,11 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5b;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Dependencies/fragmentzip",
|
||||
"$(PROJECT_DIR)/Dependencies/libcurl",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -3,3 +3,6 @@
|
||||
//
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include "fragmentzip.h"
|
||||
|
||||
@@ -498,7 +498,7 @@ extension AppViewController
|
||||
{
|
||||
guard self.app.installedApp == nil else { return }
|
||||
|
||||
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
@@ -522,8 +522,8 @@ extension AppViewController
|
||||
}
|
||||
}
|
||||
|
||||
self.bannerView.button.progress = progress
|
||||
self.navigationBarDownloadButton.progress = progress
|
||||
self.bannerView.button.progress = group.progress
|
||||
self.navigationBarDownloadButton.progress = group.progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
|
||||
@@ -49,7 +49,7 @@ private extension RefreshAltStoreViewController
|
||||
}
|
||||
|
||||
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
|
||||
let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||
let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: self.completionHandler?(.success(()))
|
||||
@@ -71,7 +71,7 @@ private extension RefreshAltStoreViewController
|
||||
}
|
||||
}
|
||||
|
||||
sender.progress = progress
|
||||
sender.progress = group.progress
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
|
||||
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
|
||||
<segue destination="Qo4-72-Hmr" kind="presentation" identifier="presentSources" id="Qd6-ba-dIo"/>
|
||||
<segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
||||
@@ -608,6 +609,14 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="962" y="-17"/>
|
||||
</scene>
|
||||
<!--PatchApp-->
|
||||
<scene sceneID="vdC-wt-hvX">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="PatchApp" id="bTL-bY-9Yq" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="NyZ-z6-R2q" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1" y="545"/>
|
||||
</scene>
|
||||
<!--My Apps-->
|
||||
<scene sceneID="nhh-BJ-XiT">
|
||||
<objects>
|
||||
@@ -1036,7 +1045,7 @@ World</string>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="de9-NH-aec"/>
|
||||
<segue reference="Qd6-ba-dIo"/>
|
||||
<segue reference="cnd-KK-o60"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ARKit
|
||||
|
||||
extension UIDevice
|
||||
{
|
||||
@@ -27,4 +28,23 @@ extension UIDevice
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
var supportsFugu14: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
// Fugu14 is supported on devices with an A12 processor or better.
|
||||
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
|
||||
return ARBodyTrackingConfiguration.isSupported
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
var isUntetheredJailbreakRequired: Bool {
|
||||
let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0)
|
||||
|
||||
let isUntetheredJailbreakRequired = ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4)
|
||||
return isUntetheredJailbreakRequired
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ class AppManagerPublisher: ObservableObject
|
||||
fileprivate(set) var refreshProgress = [String: Progress]()
|
||||
}
|
||||
|
||||
private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool
|
||||
{
|
||||
return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion)
|
||||
}
|
||||
|
||||
class AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
@@ -143,6 +148,12 @@ extension AppManager
|
||||
// This UTI is not declared by any apps, which means this app has been deleted by the user.
|
||||
// This app is also not a legacy sideloaded app, so we can assume it's fine to delete it.
|
||||
context.delete(app)
|
||||
|
||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: app.bundleIdentifier)
|
||||
{
|
||||
patchedApps.remove(at: index)
|
||||
UserDefaults.standard.patchedApps = patchedApps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,13 +395,13 @@ extension AppManager
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func install<T: AppProtocol>(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
func install<T: AppProtocol>(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> RefreshGroup
|
||||
{
|
||||
let group = RefreshGroup(context: context)
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
|
||||
completionHandler(result)
|
||||
}
|
||||
catch
|
||||
@@ -402,7 +413,7 @@ extension AppManager
|
||||
let operation = AppOperation.install(app)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
|
||||
return group.progress
|
||||
return group
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -630,6 +641,67 @@ extension AppManager
|
||||
self.run([enableJITOperation], context: context, requiresSerialQueue: true)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
|
||||
{
|
||||
class Context: InstallAppOperationContext, PatchAppContext
|
||||
{
|
||||
}
|
||||
|
||||
guard let originalBundleID = resignedApp.bundle.infoDictionary?[Bundle.Info.altBundleID] as? String else {
|
||||
let context = Context(bundleIdentifier: resignedApp.bundleIdentifier, authenticatedContext: authContext)
|
||||
completionHandler(.failure(OperationError.invalidApp))
|
||||
|
||||
return PatchAppOperation(context: context)
|
||||
}
|
||||
|
||||
let context = Context(bundleIdentifier: originalBundleID, authenticatedContext: authContext)
|
||||
context.resignedApp = resignedApp
|
||||
|
||||
let patchAppOperation = PatchAppOperation(context: context)
|
||||
let sendAppOperation = SendAppOperation(context: context)
|
||||
let installOperation = InstallAppOperation(context: context)
|
||||
|
||||
let installationProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
installationProgress.addChild(sendAppOperation.progress, withPendingUnitCount: 40)
|
||||
installationProgress.addChild(installOperation.progress, withPendingUnitCount: 60)
|
||||
|
||||
/* Patch */
|
||||
patchAppOperation.resultHandler = { [weak patchAppOperation] (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): context.error = error
|
||||
case .success:
|
||||
// Kinda hacky that we're calling patchAppOperation's progressHandler manually, but YOLO.
|
||||
patchAppOperation?.progressHandler?(installationProgress, NSLocalizedString("Patching placeholder app...", comment: ""))
|
||||
}
|
||||
}
|
||||
|
||||
/* Send */
|
||||
sendAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): context.error = error
|
||||
case .success(let installationConnection): context.installationConnection = installationConnection
|
||||
}
|
||||
}
|
||||
sendAppOperation.addDependency(patchAppOperation)
|
||||
|
||||
|
||||
/* Install */
|
||||
installOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let installedApp): completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
installOperation.addDependency(sendAppOperation)
|
||||
|
||||
self.run([patchAppOperation, sendAppOperation, installOperation], context: context.authenticatedContext)
|
||||
return patchAppOperation
|
||||
}
|
||||
|
||||
func installationProgress(for app: AppProtocol) -> Progress?
|
||||
{
|
||||
let progress = self.installationProgress[app.bundleIdentifier]
|
||||
@@ -950,6 +1022,78 @@ private extension AppManager
|
||||
deactivateAppsOperation.addDependency(verifyOperation)
|
||||
|
||||
|
||||
/* Patch App */
|
||||
let patchAppOperation = RSTAsyncBlockOperation { operation in
|
||||
do
|
||||
{
|
||||
// Only attempt to patch app if we're installing a new app, not refreshing existing app.
|
||||
// Post reboot, we install the correct jailbreak app by refreshing the patched app,
|
||||
// so this check avoids infinite recursion.
|
||||
guard case .install = appOperation else {
|
||||
operation.finish()
|
||||
return
|
||||
}
|
||||
|
||||
guard let presentingViewController = context.presentingViewController, #available(iOS 14, *) else { return operation.finish() }
|
||||
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let app = context.app else { throw OperationError.invalidParameters }
|
||||
|
||||
guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool,
|
||||
let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String,
|
||||
let maximumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMaximumiOSVersion] as? String,
|
||||
case let minimumiOSVersion = OperatingSystemVersion(string: minimumiOSVersionString),
|
||||
case let maximumiOSVersion = OperatingSystemVersion(string: maximumiOSVersionString)
|
||||
else { return operation.finish() }
|
||||
|
||||
let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let iOSVersionSupported = ProcessInfo.processInfo.isOperatingSystemAtLeast(minimumiOSVersion) &&
|
||||
(!ProcessInfo.processInfo.isOperatingSystemAtLeast(maximumiOSVersion) || maximumiOSVersion == iOSVersion)
|
||||
|
||||
guard isUntetherRequired, iOSVersionSupported, UIDevice.current.supportsFugu14 else { return operation.finish() }
|
||||
|
||||
guard let patchAppLink = app.bundle.infoDictionary?[Bundle.Info.untetherURL] as? String,
|
||||
let patchAppURL = URL(string: patchAppLink)
|
||||
else { throw OperationError.invalidApp }
|
||||
|
||||
let patchApp = AnyApp(name: app.name, bundleIdentifier: app.bundleIdentifier, url: patchAppURL)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
|
||||
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
|
||||
|
||||
let patchViewController = navigationController.topViewController as! PatchViewController
|
||||
patchViewController.patchApp = patchApp
|
||||
patchViewController.completionHandler = { [weak presentingViewController] (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled): break // Ignore
|
||||
case .failure(let error): group.context.error = error
|
||||
case .success: group.context.error = OperationError.cancelled
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
presentingViewController.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
group.context.error = error
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
patchAppOperation.addDependency(deactivateAppsOperation)
|
||||
|
||||
|
||||
/* Refresh Anisette Data */
|
||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||
@@ -959,7 +1103,7 @@ private extension AppManager
|
||||
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
||||
}
|
||||
}
|
||||
refreshAnisetteDataOperation.addDependency(deactivateAppsOperation)
|
||||
refreshAnisetteDataOperation.addDependency(patchAppOperation)
|
||||
|
||||
|
||||
/* Fetch Provisioning Profiles */
|
||||
@@ -1028,7 +1172,7 @@ private extension AppManager
|
||||
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
||||
installOperation.addDependency(sendAppOperation)
|
||||
|
||||
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
||||
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
||||
group.add(operations)
|
||||
self.run(operations, context: group.context)
|
||||
|
||||
|
||||
@@ -861,7 +861,7 @@ private extension MyAppsViewController
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
let progress = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
let group = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
@@ -869,7 +869,7 @@ private extension MyAppsViewController
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
installProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
installProgress.addChild(group.progress, withPendingUnitCount: 100)
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -107,11 +107,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
installedApp.appExtensions = installedExtensions
|
||||
|
||||
self.context.beginInstallationHandler?(installedApp)
|
||||
|
||||
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
||||
self.cleanUp()
|
||||
|
||||
self.context.beginInstallationHandler?(installedApp)
|
||||
|
||||
var activeProfiles: Set<String>?
|
||||
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
||||
{
|
||||
|
||||
19
AltStore/Operations/Patch App/ALTAppPatcher.h
Normal file
19
AltStore/Operations/Patch App/ALTAppPatcher.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ALTAppPatcher.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/18/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTAppPatcher : NSObject
|
||||
|
||||
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
143
AltStore/Operations/Patch App/ALTAppPatcher.m
Normal file
143
AltStore/Operations/Patch App/ALTAppPatcher.m
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// ALTAppPatcher.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/18/21.
|
||||
// Copied with minor modifications from sample code provided by Linus Henze.
|
||||
//
|
||||
|
||||
#import "ALTAppPatcher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@import Roxas;
|
||||
|
||||
#define CPU_SUBTYPE_PAC 0x80000000
|
||||
#define FAT_MAGIC 0xcafebabe
|
||||
|
||||
#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF))))
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t cpuType;
|
||||
uint32_t cpuSubType;
|
||||
// Incomplete, we don't need anything else
|
||||
} MachOHeader;
|
||||
|
||||
typedef struct {
|
||||
uint32_t cpuType;
|
||||
uint32_t cpuSubType;
|
||||
uint32_t fileOffset;
|
||||
uint32_t size;
|
||||
uint32_t alignment;
|
||||
} FatArch;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t archCount;
|
||||
FatArch archs[0];
|
||||
} FatHeader;
|
||||
|
||||
// Given two MachO files, return a FAT file with the following properties:
|
||||
// 1. installd will still see the original MachO and validate it's code signature
|
||||
// 2. The kernel will only see the injected MachO instead
|
||||
//
|
||||
// Only arm64e for now
|
||||
void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) {
|
||||
*outputSize = 0;
|
||||
|
||||
// First validate the App to inject: It must be an arm64e application
|
||||
if (appToInjectSize < sizeof(MachOHeader)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
MachOHeader *injectedHeader = (MachOHeader*) appToInject;
|
||||
if (injectedHeader->cpuType != CPU_TYPE_ARM64) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ok, the App to inject is ok
|
||||
// Now build a fat header
|
||||
size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize);
|
||||
size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize);
|
||||
size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded;
|
||||
|
||||
void *fatBuf = malloc(totalSize);
|
||||
if (fatBuf == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bzero(fatBuf, totalSize);
|
||||
|
||||
FatHeader *fatHeader = (FatHeader*) fatBuf;
|
||||
fatHeader->magic = htonl(FAT_MAGIC);
|
||||
fatHeader->archCount = htonl(2);
|
||||
|
||||
// Write first arch (original app)
|
||||
fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64);
|
||||
fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */
|
||||
fatHeader->archs[0].fileOffset = htonl(0x4000);
|
||||
fatHeader->archs[0].size = htonl(originalAppSize);
|
||||
fatHeader->archs[0].alignment = htonl(0xE);
|
||||
|
||||
// Write second arch (injected app)
|
||||
fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64);
|
||||
fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC);
|
||||
fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded);
|
||||
fatHeader->archs[1].size = htonl(appToInjectSize);
|
||||
fatHeader->archs[1].alignment = htonl(0xE);
|
||||
|
||||
// Ok, now write the MachOs
|
||||
memcpy(fatBuf + 0x4000, originalApp, originalAppSize);
|
||||
memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize);
|
||||
|
||||
// We're done!
|
||||
*outputSize = totalSize;
|
||||
return fatBuf;
|
||||
}
|
||||
|
||||
@implementation ALTAppPatcher
|
||||
|
||||
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error
|
||||
{
|
||||
NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error];
|
||||
if (originalApp == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error];
|
||||
if (injectedApp == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
size_t outputSize = 0;
|
||||
void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize);
|
||||
if (output == NULL)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
// If injectApp fails, it means the patch app is in the wrong format.
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES];
|
||||
if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
124
AltStore/Operations/Patch App/PatchApp.storyboard
Normal file
124
AltStore/Operations/Patch App/PatchApp.storyboard
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="WBb-E1-bN8">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="dx2-fp-qDX">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="WBb-E1-bN8" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="cVa-8m-fW6" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="barTintColor" name="SettingsBackground"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="idH-XF-rK8" kind="relationship" relationship="rootViewController" id="hSJ-tL-4nB"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="i7K-pi-SRe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="137.68115942028987" y="137.94642857142856"/>
|
||||
</scene>
|
||||
<!--Patch View Controller-->
|
||||
<scene sceneID="gJ4-4F-79r">
|
||||
<objects>
|
||||
<viewController id="idH-XF-rK8" customClass="PatchViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="4bV-S5-z7S">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RHK-C5-7wu" customClass="RSTPlaceholderView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="PjB-Dc-9n3">
|
||||
<rect key="frame" x="20" y="736.5" width="374" height="117.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GQF-6P-Fit">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" name="Text"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SNn-Ad-ICf" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28.5" width="374" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="s4X-uf-nl9"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||
<color key="tintColor" name="SettingsHighlighted"/>
|
||||
<state key="normal" title="Install Untethered Jailbreak">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="performButtonAction" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="FxO-1Y-IML"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="leJ-09-giz">
|
||||
<rect key="frame" x="0.0" y="87.5" width="374" height="30"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" title="Install Without Untethering">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="installRegularJailbreak" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="1XB-11-Kdn"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="H6g-xA-DdL"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="RHK-C5-7wu" firstAttribute="top" secondItem="4bV-S5-z7S" secondAttribute="top" id="CVK-6E-iA6"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="PjB-Dc-9n3" secondAttribute="trailing" id="GOg-JU-LIP"/>
|
||||
<constraint firstItem="RHK-C5-7wu" firstAttribute="bottom" secondItem="4bV-S5-z7S" secondAttribute="bottom" id="LPh-J8-IVx"/>
|
||||
<constraint firstItem="PjB-Dc-9n3" firstAttribute="leading" secondItem="4bV-S5-z7S" secondAttribute="leadingMargin" id="Rlg-PC-5ZN"/>
|
||||
<constraint firstItem="RHK-C5-7wu" firstAttribute="trailing" secondItem="H6g-xA-DdL" secondAttribute="trailing" id="XdZ-36-6yS"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="PjB-Dc-9n3" secondAttribute="bottom" id="hTS-nX-0xv"/>
|
||||
<constraint firstItem="RHK-C5-7wu" firstAttribute="leading" secondItem="H6g-xA-DdL" secondAttribute="leading" id="lzV-fG-Xv6"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" largeTitleDisplayMode="always" id="0J1-80-RD8">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="vtw-PQ-Dk1">
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<connections>
|
||||
<action selector="cancel" destination="idH-XF-rK8" id="4Wk-dv-RYG"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="cancelBarButtonItem" destination="vtw-PQ-Dk1" id="8Mh-GU-KD5"/>
|
||||
<outlet property="cancelButton" destination="leJ-09-giz" id="BNh-I3-vXc"/>
|
||||
<outlet property="pillButton" destination="SNn-Ad-ICf" id="iJg-TC-p8q"/>
|
||||
<outlet property="placeholderView" destination="RHK-C5-7wu" id="5x0-sg-HAH"/>
|
||||
<outlet property="taskDescriptionLabel" destination="GQF-6P-Fit" id="C4c-xy-kvU"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8ev-19-qsi" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1001" y="138"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="SettingsHighlighted">
|
||||
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="Text">
|
||||
<color red="1" green="1" blue="1" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
223
AltStore/Operations/Patch App/PatchAppOperation.swift
Normal file
223
AltStore/Operations/Patch App/PatchAppOperation.swift
Normal file
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// PatchAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/13/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import AppleArchive
|
||||
import System
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@available(iOS 14, *)
|
||||
protocol PatchAppContext
|
||||
{
|
||||
var bundleIdentifier: String { get }
|
||||
var temporaryDirectory: URL { get }
|
||||
|
||||
var resignedApp: ALTApplication? { get }
|
||||
var error: Error? { get }
|
||||
}
|
||||
|
||||
enum PatchAppError: LocalizedError
|
||||
{
|
||||
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unsupportedOperatingSystemVersion(let osVersion):
|
||||
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
|
||||
if osVersion.patchVersion != 0
|
||||
{
|
||||
osVersionString += ".\(osVersion.patchVersion)"
|
||||
}
|
||||
|
||||
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
|
||||
return errorDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OTAUpdate
|
||||
{
|
||||
var url: URL
|
||||
var archivePath: String
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
class PatchAppOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: PatchAppContext
|
||||
|
||||
var progressHandler: ((Progress, String) -> Void)?
|
||||
|
||||
private let appPatcher = ALTAppPatcher()
|
||||
private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true)
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init(context: PatchAppContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 100
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
|
||||
|
||||
self.cancellable = self.fetchOTAUpdate()
|
||||
.flatMap { self.downloadArchive(from: $0) }
|
||||
.flatMap { self.extractSpotlightFromArchive(at: $0) }
|
||||
.flatMap { self.patch(resignedApp, withBinaryAt: $0) }
|
||||
.tryMap { try FileManager.default.zipAppBundle(at: $0) }
|
||||
.tryMap { (fileURL) in
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
|
||||
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||
}
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { completion in
|
||||
switch completion
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .finished: self.finish(.success(()))
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
self.cancellable?.cancel()
|
||||
self.cancellable = nil
|
||||
}
|
||||
}
|
||||
|
||||
private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in
|
||||
guard let progress = Progress.current() else { return }
|
||||
|
||||
if percentageComplete == 100 && progress.completedUnitCount == 0
|
||||
{
|
||||
// Ignore first percentageComplete, which is always 100.
|
||||
return
|
||||
}
|
||||
|
||||
progress.completedUnitCount = Int64(percentageComplete)
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private extension PatchAppOperation
|
||||
{
|
||||
func fetchOTAUpdate() -> AnyPublisher<OTAUpdate, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
switch (osVersion.majorVersion, osVersion.minorVersion)
|
||||
{
|
||||
#if DEBUG
|
||||
case (14, 3): fallthrough
|
||||
#endif
|
||||
|
||||
case (14, 4):
|
||||
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!,
|
||||
archivePath: "AssetData/payloadv2/payload.042")
|
||||
|
||||
case (14, 5):
|
||||
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!,
|
||||
archivePath: "AssetData/payloadv2/payload.043")
|
||||
|
||||
default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion)
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func downloadArchive(from update: OTAUpdate) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive")
|
||||
archiveURL.withUnsafeFileSystemRepresentation { archivePath in
|
||||
let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!)
|
||||
defer { fragmentzip_close(fz) }
|
||||
|
||||
self.progress.becomeCurrent(withPendingUnitCount: 100)
|
||||
fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback)
|
||||
self.progress.resignCurrent()
|
||||
}
|
||||
|
||||
print("Downloaded OTA archive.")
|
||||
return archiveURL
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
let spotlightPath = "Applications/Spotlight.app/Spotlight"
|
||||
let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath)
|
||||
|
||||
guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)),
|
||||
let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream),
|
||||
let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream),
|
||||
let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path))
|
||||
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) }
|
||||
|
||||
_ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in
|
||||
guard filePath == FilePath(spotlightPath) else { return .skip }
|
||||
return .ok
|
||||
}
|
||||
|
||||
print("Extracted Spotlight from OTA archive.")
|
||||
return spotlightFileURL
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher<URL, Error>
|
||||
{
|
||||
Just(()).tryMap {
|
||||
// executableURL may be nil, so use infoDictionary instead to determine executable name.
|
||||
// guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp }
|
||||
guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp }
|
||||
|
||||
let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true)
|
||||
try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL)
|
||||
|
||||
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
|
||||
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
|
||||
|
||||
print("Patched \(app.name).")
|
||||
return temporaryAppURL
|
||||
}
|
||||
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
499
AltStore/Operations/Patch App/PatchViewController.swift
Normal file
499
AltStore/Operations/Patch App/PatchViewController.swift
Normal file
@@ -0,0 +1,499 @@
|
||||
//
|
||||
// PatchViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/20/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension PatchViewController
|
||||
{
|
||||
enum Step
|
||||
{
|
||||
case confirm
|
||||
case install
|
||||
case openApp
|
||||
case patchApp
|
||||
case reboot
|
||||
case refresh
|
||||
case finish
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class PatchViewController: UIViewController
|
||||
{
|
||||
var patchApp: AnyApp?
|
||||
var installedApp: InstalledApp?
|
||||
|
||||
var completionHandler: ((Result<Void, Error>) -> Void)?
|
||||
|
||||
private let context = AuthenticatedOperationContext()
|
||||
|
||||
private var currentStep: Step = .confirm {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var buttonHandler: (() -> Void)?
|
||||
private var resignedApp: ALTApplication?
|
||||
|
||||
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
private var didEnterBackgroundObservation: NSObjectProtocol?
|
||||
private weak var cancellableProgress: Progress?
|
||||
|
||||
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
||||
@IBOutlet private var taskDescriptionLabel: UILabel!
|
||||
@IBOutlet private var pillButton: PillButton!
|
||||
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet private var cancelButton: UIButton!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.isModalInPresentation = true
|
||||
|
||||
self.placeholderView.stackView.spacing = 20
|
||||
self.placeholderView.textLabel.textColor = .white
|
||||
|
||||
self.placeholderView.detailTextLabel.textAlignment = .left
|
||||
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
self?.startProcess()
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to create temporary directory:", error)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if self.installedApp != nil
|
||||
{
|
||||
self.refreshApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
||||
switch self.currentStep
|
||||
{
|
||||
case .confirm:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
if UIDevice.current.isUntetheredJailbreakRequired
|
||||
{
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
||||
}
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
|
||||
|
||||
self.cancelButton.alpha = 1.0
|
||||
|
||||
case .install:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
|
||||
|
||||
case .openApp:
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .patchApp:
|
||||
guard let app = self.patchApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .reboot:
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
||||
|
||||
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
||||
|
||||
case .refresh:
|
||||
guard let installedApp = self.installedApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
|
||||
|
||||
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
|
||||
|
||||
case .finish:
|
||||
guard let installedApp = self.installedApp else { break }
|
||||
|
||||
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
|
||||
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
|
||||
|
||||
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func present(_ error: Error, title: String)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let nsError = error as NSError
|
||||
|
||||
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func setProgress(_ progress: Progress?, description: String?)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.pillButton.progress = progress
|
||||
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
|
||||
}
|
||||
}
|
||||
|
||||
func finish(with result: Result<Void, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove temporary directory:", error)
|
||||
}
|
||||
|
||||
if let observation = self.didEnterBackgroundObservation
|
||||
{
|
||||
NotificationCenter.default.removeObserver(observation)
|
||||
}
|
||||
|
||||
self.completionHandler?(result)
|
||||
self.completionHandler = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
@IBAction func performButtonAction()
|
||||
{
|
||||
self.buttonHandler?()
|
||||
}
|
||||
|
||||
@IBAction func cancel()
|
||||
{
|
||||
self.finish(with: .success(()))
|
||||
|
||||
self.cancellableProgress?.cancel()
|
||||
}
|
||||
|
||||
@IBAction func installRegularJailbreak()
|
||||
{
|
||||
guard let app = self.patchApp else { return }
|
||||
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
if UIDevice.current.isUntetheredJailbreakRequired
|
||||
{
|
||||
title = NSLocalizedString("Untethering Required", comment: "")
|
||||
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
title = NSLocalizedString("Untethering Recommended", comment: "")
|
||||
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
|
||||
self.finish(with: .failure(OperationError.cancelled))
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private extension PatchViewController
|
||||
{
|
||||
func startProcess()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.currentStep = .install
|
||||
|
||||
if let progress = AppManager.shared.installationProgress(for: patchApp)
|
||||
{
|
||||
// Cancel pending jailbreak app installation so we can start a new one.
|
||||
progress.cancel()
|
||||
}
|
||||
|
||||
let appURL = InstalledApp.fileURL(for: patchApp)
|
||||
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
|
||||
|
||||
do
|
||||
{
|
||||
// Make copy of original app, so we can replace the cached patch app with it later.
|
||||
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
|
||||
return
|
||||
}
|
||||
|
||||
var unzippingError: Error?
|
||||
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
if let unzippingError = unzippingError
|
||||
{
|
||||
throw unzippingError
|
||||
}
|
||||
|
||||
// Replace cached patch app with original app so we can resume installing it post-reboot.
|
||||
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
|
||||
|
||||
self.openApp()
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
}
|
||||
refreshGroup.beginInstallationHandler = { (installedApp) in
|
||||
do
|
||||
{
|
||||
// Replace patch app name with correct name.
|
||||
installedApp.name = patchApp.name
|
||||
|
||||
let ipaURL = installedApp.refreshedIPAURL
|
||||
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
|
||||
|
||||
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error unzipping app bundle:", error)
|
||||
unzippingError = error
|
||||
}
|
||||
}
|
||||
self.setProgress(refreshGroup.progress, description: nil)
|
||||
|
||||
self.cancellableProgress = refreshGroup.progress
|
||||
}
|
||||
|
||||
func openApp()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .openApp
|
||||
|
||||
// This observation is willEnterForeground because patching starts immediately upon return.
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
self.patchApplication()
|
||||
}
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
let openURL = InstalledApp.openAppURL(for: patchApp)
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func patchApplication()
|
||||
{
|
||||
guard let resignedApp = self.resignedApp else { return }
|
||||
|
||||
self.currentStep = .patchApp
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
self?.patchApplication()
|
||||
}
|
||||
|
||||
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
|
||||
case .success: self.rebootDevice()
|
||||
}
|
||||
}
|
||||
patchAppOperation.progressHandler = { (progress, description) in
|
||||
self.setProgress(progress, description: description)
|
||||
}
|
||||
self.cancellableProgress = patchAppOperation.progress
|
||||
}
|
||||
|
||||
func rebootDevice()
|
||||
{
|
||||
guard let patchApp = self.patchApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .reboot
|
||||
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
|
||||
var patchedApps = UserDefaults.standard.patchedApps ?? []
|
||||
if !patchedApps.contains(patchApp.bundleIdentifier)
|
||||
{
|
||||
patchedApps.append(patchApp.bundleIdentifier)
|
||||
UserDefaults.standard.patchedApps = patchedApps
|
||||
}
|
||||
|
||||
self.finish(with: .success(()))
|
||||
}
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
let openURL = InstalledApp.openAppURL(for: patchApp)
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func refreshApp()
|
||||
{
|
||||
guard let installedApp = self.installedApp else { return }
|
||||
|
||||
self.currentStep = .refresh
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
||||
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
tempApp.needsResign = true
|
||||
|
||||
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
// Refreshing ensures we don't attempt to patch the app again,
|
||||
// since that is only checked when installing a new app.
|
||||
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
|
||||
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
|
||||
guard let self = self else { return }
|
||||
|
||||
do
|
||||
{
|
||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
|
||||
_ = try result.get()
|
||||
|
||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
||||
{
|
||||
patchedApps.remove(at: index)
|
||||
UserDefaults.standard.patchedApps = patchedApps
|
||||
}
|
||||
|
||||
self.finish()
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: errorTitle)
|
||||
}
|
||||
}
|
||||
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.present(error, title: errorTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finish()
|
||||
{
|
||||
guard let installedApp = self.installedApp else { return }
|
||||
|
||||
self.setProgress(nil, description: nil)
|
||||
self.currentStep = .finish
|
||||
|
||||
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
||||
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
||||
self.finish(with: .success(()))
|
||||
}
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let appName = installedApp.name
|
||||
let openURL = installedApp.openAppURL
|
||||
|
||||
self.buttonHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
|
||||
UIApplication.shared.open(openURL) { success in
|
||||
guard !success else { return }
|
||||
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
AltStore/Operations/Patch App/fragmentzip.h
Normal file
18
AltStore/Operations/Patch App/fragmentzip.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// fragmentzip.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 10/25/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef fragmentzip_h
|
||||
#define fragmentzip_h
|
||||
|
||||
typedef void fragmentzip_t;
|
||||
typedef void (*fragmentzip_process_callback_t)(unsigned int progress);
|
||||
fragmentzip_t *fragmentzip_open(const char *url);
|
||||
int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback);
|
||||
void fragmentzip_close(fragmentzip_t *info);
|
||||
|
||||
#endif /* fragmentzip_h */
|
||||
@@ -14,13 +14,13 @@ import AltStoreCore
|
||||
@objc(SendAppOperation)
|
||||
class SendAppOperation: ResultOperation<ServerConnection>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
|
||||
|
||||
private var serverConnection: ServerConnection?
|
||||
|
||||
init(context: AppOperationContext)
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<ServerConnection>
|
||||
return
|
||||
}
|
||||
|
||||
guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let resignedApp = self.context.resignedApp, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
|
||||
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
// Connect to server.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AltStoreCore
|
||||
|
||||
extension TabBarController
|
||||
{
|
||||
@@ -45,18 +46,44 @@ class TabBarController: UITabBarController
|
||||
self.initialSegue = nil
|
||||
self.performSegue(withIdentifier: identifier, sender: sender)
|
||||
}
|
||||
else if let patchedApps = UserDefaults.standard.patchedApps, !patchedApps.isEmpty
|
||||
{
|
||||
// Check if we need to finish installing untethered jailbreak.
|
||||
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||
guard let patchedApp = activeApps.first(where: { patchedApps.contains($0.bundleIdentifier) }) else { return }
|
||||
|
||||
self.performSegue(withIdentifier: "finishJailbreak", sender: patchedApp)
|
||||
}
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
{
|
||||
guard segue.identifier == "presentSources",
|
||||
let notification = sender as? Notification,
|
||||
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
|
||||
else { return }
|
||||
guard let identifier = segue.identifier else { return }
|
||||
|
||||
let navigationController = segue.destination as! UINavigationController
|
||||
let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController
|
||||
sourcesViewController.deepLinkSourceURL = sourceURL
|
||||
switch identifier
|
||||
{
|
||||
case "presentSources":
|
||||
guard let notification = sender as? Notification,
|
||||
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
|
||||
else { return }
|
||||
|
||||
let navigationController = segue.destination as! UINavigationController
|
||||
let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController
|
||||
sourcesViewController.deepLinkSourceURL = sourceURL
|
||||
|
||||
case "finishJailbreak":
|
||||
guard let installedApp = sender as? InstalledApp, #available(iOS 14, *) else { return }
|
||||
|
||||
let navigationController = segue.destination as! UINavigationController
|
||||
|
||||
let patchViewController = navigationController.viewControllers.first as! PatchViewController
|
||||
patchViewController.installedApp = installedApp
|
||||
patchViewController.completionHandler = { [weak self] _ in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
override func performSegue(withIdentifier identifier: String, sender: Any?)
|
||||
|
||||
@@ -35,6 +35,8 @@ public extension UserDefaults
|
||||
|
||||
@NSManaged var localServerSupportsRefreshing: Bool
|
||||
|
||||
@NSManaged var patchedApps: [String]?
|
||||
|
||||
var activeAppsLimit: Int? {
|
||||
get {
|
||||
return self._activeAppsLimit?.intValue
|
||||
|
||||
@@ -16,6 +16,20 @@ public protocol AppProtocol
|
||||
var url: URL { get }
|
||||
}
|
||||
|
||||
public struct AnyApp: AppProtocol
|
||||
{
|
||||
public var name: String
|
||||
public var bundleIdentifier: String
|
||||
public var url: URL
|
||||
|
||||
public init(name: String, bundleIdentifier: String, url: URL)
|
||||
{
|
||||
self.name = name
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
extension ALTApplication: AppProtocol
|
||||
{
|
||||
public var url: URL {
|
||||
|
||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 2483f45ce6...ac85b05592
BIN
Dependencies/fragmentzip/libfragmentzip.a
vendored
Normal file
BIN
Dependencies/fragmentzip/libfragmentzip.a
vendored
Normal file
Binary file not shown.
BIN
Dependencies/libcurl/libcurl.a
vendored
Normal file
BIN
Dependencies/libcurl/libcurl.a
vendored
Normal file
Binary file not shown.
@@ -20,6 +20,11 @@ public extension Bundle
|
||||
|
||||
public static let urlTypes = "CFBundleURLTypes"
|
||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||
|
||||
public static let untetherURL = "ALTFugu14UntetherURL"
|
||||
public static let untetherRequired = "ALTFugu14UntetherRequired"
|
||||
public static let untetherMinimumiOSVersion = "ALTFugu14UntetherMinimumVersion"
|
||||
public static let untetherMaximumiOSVersion = "ALTFugu14UntetherMaximumVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user