diff --git a/AltServer/AltServer-Bridging-Header.h b/AltServer/AltServer-Bridging-Header.h
index c42bab66..2145ae89 100644
--- a/AltServer/AltServer-Bridging-Header.h
+++ b/AltServer/AltServer-Bridging-Header.h
@@ -11,5 +11,6 @@
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "AltXPCProtocol.h"
+#import "ALTWrappedError.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"
diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift
index cde13ae3..952cea94 100644
--- a/AltServer/AppDelegate.swift
+++ b/AltServer/AppDelegate.swift
@@ -56,6 +56,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var isAltPluginUpdateAvailable = false
+ private var popoverController: NSPopover?
+ private var popoverError: NSError?
+ private var errorAlert: NSAlert?
+
func applicationDidFinishLaunching(_ aNotification: Notification)
{
UserDefaults.standard.registerDefaults()
@@ -159,8 +163,9 @@ private extension AppDelegate
DispatchQueue.main.async {
switch result
{
- case .failure(let error):
- self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("JIT compilation could not be enabled for %@.", comment: ""), app.name))
+ case .failure(let error as NSError):
+ let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
+ self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
case .success:
let alert = NSAlert()
@@ -250,13 +255,13 @@ private extension AppDelegate
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
- case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
+ case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error):
DispatchQueue.main.async {
- self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("Could not install app to %@.", comment: ""), device.name))
+ self.showErrorAlert(error: error)
}
}
}
@@ -278,7 +283,10 @@ private extension AppDelegate
self.pluginManager.isUpdateAvailable { result in
switch result
{
- case .failure(let error): finish(.failure(error))
+ case .failure(let error):
+ let error = (error as NSError).withLocalizedTitle(NSLocalizedString("Could not check for Mail plug-in updates.", comment: ""))
+ finish(.failure(error))
+
case .success(let isUpdateAvailable):
self.isAltPluginUpdateAvailable = isUpdateAvailable
@@ -302,85 +310,61 @@ private extension AppDelegate
}
}
- func showErrorAlert(error: Error, localizedFailure: String)
+ func showErrorAlert(error: Error)
{
+ self.popoverError = error as NSError
+
let nsError = error as NSError
- let alert = NSAlert()
- alert.alertStyle = .critical
- alert.messageText = localizedFailure
-
- var messageComponents = [String]()
-
- let separator: String
- switch error
- {
- case ALTServerError.maximumFreeAppLimitReached: separator = "\n\n"
- default: separator = " "
- }
-
- if let errorFailure = nsError.localizedFailure
- {
- if let debugDescription = nsError.localizedDebugDescription
- {
- alert.messageText = errorFailure
- messageComponents.append(debugDescription)
- }
- else if let failureReason = nsError.localizedFailureReason
- {
- if nsError.localizedDescription.starts(with: errorFailure)
- {
- alert.messageText = errorFailure
- messageComponents.append(failureReason)
- }
- else
- {
- alert.messageText = errorFailure
- messageComponents.append(nsError.localizedDescription)
- }
- }
- else
- {
- // No failure reason given.
-
- if nsError.localizedDescription.starts(with: errorFailure)
- {
- // No need to duplicate errorFailure in both title and message.
- alert.messageText = localizedFailure
- messageComponents.append(nsError.localizedDescription)
- }
- else
- {
- alert.messageText = errorFailure
- messageComponents.append(nsError.localizedDescription)
- }
- }
- }
- else
- {
- alert.messageText = localizedFailure
-
- if let debugDescription = nsError.localizedDebugDescription
- {
- messageComponents.append(debugDescription)
- }
- else
- {
- messageComponents.append(nsError.localizedDescription)
- }
- }
-
+ var messageComponents = [error.localizedDescription]
if let recoverySuggestion = nsError.localizedRecoverySuggestion
{
messageComponents.append(recoverySuggestion)
}
- let informativeText = messageComponents.joined(separator: separator)
- alert.informativeText = informativeText
+ let title = nsError.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
+ let message = messageComponents.joined(separator: "\n\n")
+
+ let alert = NSAlert()
+ alert.alertStyle = .critical
+ alert.messageText = title
+ alert.informativeText = message
+ alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
+ alert.addButton(withTitle: NSLocalizedString("View More Details", comment: ""))
+
+ if let viewMoreButton = alert.buttons.last
+ {
+ viewMoreButton.target = self
+ viewMoreButton.action = #selector(AppDelegate.showDetailedErrorDescription)
+
+ self.errorAlert = alert
+ }
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
-
+
alert.runModal()
+
+ self.popoverController = nil
+ self.errorAlert = nil
+ self.popoverError = nil
+ }
+
+ @objc func showDetailedErrorDescription()
+ {
+ guard let errorAlert, let contentView = errorAlert.window.contentView else { return }
+
+ let errorDetailsViewController = NSStoryboard(name: "Main", bundle: .main).instantiateController(withIdentifier: "errorDetailsViewController") as! ErrorDetailsViewController
+ errorDetailsViewController.error = self.popoverError
+
+ let fittingSize = errorDetailsViewController.view.fittingSize
+ errorDetailsViewController.view.frame.size = fittingSize
+
+ let popoverController = NSPopover()
+ popoverController.contentViewController = errorDetailsViewController
+ popoverController.contentSize = fittingSize
+ popoverController.behavior = .transient
+ popoverController.show(relativeTo: contentView.bounds, of: contentView, preferredEdge: .maxX)
+ self.popoverController = popoverController
}
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
diff --git a/AltServer/Base.lproj/Main.storyboard b/AltServer/Base.lproj/Main.storyboard
index d01f7ec6..3c867eac 100644
--- a/AltServer/Base.lproj/Main.storyboard
+++ b/AltServer/Base.lproj/Main.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -384,5 +384,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AltServer/Connections/ALTDebugConnection.mm b/AltServer/Connections/ALTDebugConnection.mm
index e7e2598a..a32052b5 100644
--- a/AltServer/Connections/ALTDebugConnection.mm
+++ b/AltServer/Connections/ALTDebugConnection.mm
@@ -108,9 +108,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
- (void)_enableUnsignedCodeExecutionForProcessWithName:(nullable NSString *)processName pid:(int32_t)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
{
dispatch_async(self.connectionQueue, ^{
- NSString *name = processName ?: NSLocalizedString(@"this app", @"");
- NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"JIT could not be enabled for %@.", comment: @""), name];
-
NSString *attachCommand = nil;
if (processName)
@@ -133,7 +130,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name;
- userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError);
@@ -145,7 +141,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name;
- userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError);
@@ -249,7 +244,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
{
if (error)
{
- *error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: response}];
+ *error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
+ NSLocalizedFailureReasonErrorKey: response,
+ ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
+ ALTSourceLineErrorKey: @(__LINE__)
+ }];
}
return NO;
@@ -292,7 +291,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
break;
default:
- *error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: response}];
+ *error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
+ NSLocalizedFailureReasonErrorKey: response,
+ ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
+ ALTSourceLineErrorKey: @(__LINE__)
+ }];
break;
}
}
diff --git a/AltServer/DeveloperDiskManager.swift b/AltServer/DeveloperDiskManager.swift
index 3830c912..83448c6f 100644
--- a/AltServer/DeveloperDiskManager.swift
+++ b/AltServer/DeveloperDiskManager.swift
@@ -10,13 +10,14 @@ import Foundation
import AltSign
-enum DeveloperDiskError: LocalizedError
+typealias DeveloperDiskError = DeveloperDiskErrorCode.Error
+enum DeveloperDiskErrorCode: Int, ALTErrorEnum
{
case unknownDownloadURL
case unsupportedOperatingSystem
case downloadedDiskNotFound
- var errorDescription: String? {
+ var errorFailureReason: String {
switch self
{
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
@@ -88,14 +89,14 @@ class DeveloperDiskManager
{
do
{
- guard let osName = device.type.osName else { throw DeveloperDiskError.unsupportedOperatingSystem }
+ guard let osName = device.type.osName else { throw DeveloperDiskError(.unsupportedOperatingSystem) }
let osKeyPath: KeyPath
switch device.type
{
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
- default: throw DeveloperDiskError.unsupportedOperatingSystem
+ default: throw DeveloperDiskError(.unsupportedOperatingSystem)
}
var osVersion = device.osVersion
@@ -146,7 +147,7 @@ class DeveloperDiskManager
do
{
let developerDiskURLs = try result.get()
- guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError.unknownDownloadURL }
+ guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError(.unknownDownloadURL) }
switch diskURL
{
@@ -201,7 +202,7 @@ private extension DeveloperDiskManager
{
guard let data = data else { throw error! }
- let response = try JSONDecoder().decode(FetchURLsResponse.self, from: data)
+ let response = try Foundation.JSONDecoder().decode(FetchURLsResponse.self, from: data)
completionHandler(.success(response.disks))
}
catch
@@ -244,7 +245,7 @@ private extension DeveloperDiskManager
}
}
- guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError.downloadedDiskNotFound }
+ guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError(.downloadedDiskNotFound) }
completionHandler(.success((diskFileURL, signatureFileURL)))
}
@@ -318,7 +319,7 @@ private extension DeveloperDiskManager
}
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
- return completionHandler(.failure(downloadError ?? DeveloperDiskError.downloadedDiskNotFound))
+ return completionHandler(.failure(downloadError ?? DeveloperDiskError(.downloadedDiskNotFound)))
}
completionHandler(.success((diskFileURL, signatureFileURL)))
diff --git a/AltServer/Devices/ALTDeviceManager+Installation.swift b/AltServer/Devices/ALTDeviceManager+Installation.swift
index 74fbe3ab..5d1ee0ae 100644
--- a/AltServer/Devices/ALTDeviceManager+Installation.swift
+++ b/AltServer/Devices/ALTDeviceManager+Installation.swift
@@ -14,34 +14,21 @@ private let appGroupsSemaphore = DispatchSemaphore(value: 1)
private let developerDiskManager = DeveloperDiskManager()
-enum InstallError: Int, LocalizedError, _ObjectiveCBridgeableError
+typealias OperationError = OperationErrorCode.Error
+enum OperationErrorCode: Int, ALTErrorEnum
{
case cancelled
case noTeam
case missingPrivateKey
case missingCertificate
- var errorDescription: String? {
+ var errorFailureReason: String {
switch self
{
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
- case .noTeam: return "You are not a member of any developer teams."
- case .missingPrivateKey: return "The developer certificate's private key could not be found."
- case .missingCertificate: return "The developer certificate could not be found."
- }
- }
-
- init?(_bridgedNSError error: NSError)
- {
- guard error.domain == InstallError.cancelled._domain else { return nil }
-
- if let installError = InstallError(rawValue: error.code)
- {
- self = installError
- }
- else
- {
- return nil
+ case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "")
+ case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "")
+ case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
}
}
}
@@ -54,16 +41,18 @@ extension ALTDeviceManager
var appName = (url.isFileURL) ? url.deletingPathExtension().lastPathComponent : NSLocalizedString("AltStore", comment: "")
- func finish(_ result: Result, title: String = "")
+ func finish(_ result: Result, failure: String? = nil)
{
DispatchQueue.main.async {
switch result
{
case .success(let app): completion(.success(app))
case .failure(var error as NSError):
- if error.localizedFailure == nil
+ error = error.withLocalizedTitle(String(format: NSLocalizedString("%@ could not be installed onto %@.", comment: ""), appName, altDevice.name))
+
+ if let failure, error.localizedFailure == nil
{
- error = error.withLocalizedFailure(String(format: NSLocalizedString("Could not install %@ to %@.", comment: ""), appName, altDevice.name))
+ error = error.withLocalizedFailure(failure)
}
completion(.failure(error))
@@ -144,24 +133,25 @@ extension ALTDeviceManager
let profiles = try result.get()
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
- finish(result.map { application }, title: "Failed to Install AltStore")
+ finish(result.map { application })
}
}
catch
{
- finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
+ finish(.failure(error), failure: NSLocalizedString("AltServer could not fetch new provisioning profiles.", comment: ""))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Refresh Anisette Data")
+ finish(.failure(error))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Download AltStore")
+ let failure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), appName)
+ finish(.failure(error), failure: failure)
}
}
}
@@ -169,31 +159,31 @@ extension ALTDeviceManager
}
catch
{
- finish(.failure(error), title: "Failed to Fetch Certificate")
+ finish(.failure(error), failure: NSLocalizedString("A valid signing certificate could not be created.", comment: ""))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Register Device")
+ finish(.failure(error), failure: NSLocalizedString("Your device could not be registered with your development team.", comment: ""))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Fetch Team")
+ finish(.failure(error))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Authenticate")
+ finish(.failure(error), failure: NSLocalizedString("AltServer could not sign in with your Apple ID.", comment: ""))
}
}
}
catch
{
- finish(.failure(error), title: "Failed to Fetch Anisette Data")
+ finish(.failure(error))
}
}
}
@@ -306,7 +296,7 @@ private extension ALTDeviceManager
}
else
{
- completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
+ completionHandler(.failure(error ?? ALTAppleAPIError.unknown()))
}
}
}
@@ -332,7 +322,7 @@ private extension ALTDeviceManager
}
else
{
- throw InstallError.noTeam
+ throw OperationError(.noTeam)
}
}
catch
@@ -384,7 +374,7 @@ private extension ALTDeviceManager
}
}
- guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
+ guard !isCancelled else { return completionHandler(.failure(OperationError(.cancelled))) }
}
func addCertificate()
@@ -393,7 +383,7 @@ private extension ALTDeviceManager
do
{
let certificate = try Result(certificate, error).get()
- guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
+ guard let privateKey = certificate.privateKey else { throw OperationError(.missingPrivateKey) }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
@@ -401,7 +391,7 @@ private extension ALTDeviceManager
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
- throw InstallError.missingCertificate
+ throw OperationError(.missingCertificate)
}
certificate.privateKey = privateKey
@@ -454,7 +444,7 @@ private extension ALTDeviceManager
}
}
- guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
+ guard !isCancelled else { return completionHandler(.failure(OperationError(.cancelled))) }
}
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm
index a7d6aff7..eca5d55f 100644
--- a/AltServer/Devices/ALTDeviceManager.mm
+++ b/AltServer/Devices/ALTDeviceManager.mm
@@ -1217,7 +1217,11 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
// ALTServerErrorUnderlyingError uses its underlying error's failure reason as its error description (if one exists),
// so assign our recovery suggestion to NSLocalizedFailureReasonErrorKey to make sure it's always displayed on client.
- NSError *underlyingError = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: recoverySuggestion}];
+ NSError *underlyingError = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
+ NSLocalizedFailureReasonErrorKey: recoverySuggestion,
+ ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
+ ALTSourceLineErrorKey: @(__LINE__)
+ }];
returnError = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{NSUnderlyingErrorKey: underlyingError}];
}
diff --git a/AltServer/ErrorDetailsViewController.swift b/AltServer/ErrorDetailsViewController.swift
new file mode 100644
index 00000000..6f9fd1c7
--- /dev/null
+++ b/AltServer/ErrorDetailsViewController.swift
@@ -0,0 +1,48 @@
+//
+// ErrorDetailsViewController.swift
+// AltServer
+//
+// Created by Riley Testut on 10/4/22.
+// Copyright © 2022 Riley Testut. All rights reserved.
+//
+
+import AppKit
+
+class ErrorDetailsViewController: NSViewController
+{
+ var error: NSError? {
+ didSet {
+ self.update()
+ }
+ }
+
+ @IBOutlet private var errorCodeLabel: NSTextField!
+ @IBOutlet private var detailedDescriptionLabel: NSTextField!
+
+ override func viewDidLoad()
+ {
+ super.viewDidLoad()
+
+ self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
+ }
+}
+
+private extension ErrorDetailsViewController
+{
+ func update()
+ {
+ if !self.isViewLoaded
+ {
+ self.loadView()
+ }
+
+ guard let error = self.error else { return }
+
+ self.errorCodeLabel.stringValue = error.localizedErrorCode
+
+ let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
+ let detailedDescription = error.formattedDetailedDescription(with: font)
+ self.detailedDescriptionLabel.attributedStringValue = detailedDescription
+ }
+}
+
diff --git a/AltServer/Plugin/PluginManager.swift b/AltServer/Plugin/PluginManager.swift
index fb2e9c09..3b2f2202 100644
--- a/AltServer/Plugin/PluginManager.swift
+++ b/AltServer/Plugin/PluginManager.swift
@@ -15,24 +15,71 @@ import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
-enum PluginError: LocalizedError
+extension PluginError
{
- case cancelled
- case unknown
- case notFound
- case mismatchedHash(hash: String, expectedHash: String)
- case taskError(String)
- case taskErrorCode(Int)
+ enum Code: Int, ALTErrorCode
+ {
+ typealias Error = PluginError
+
+ case cancelled
+ case unknown
+ case notFound
+ case mismatchedHash
+ case taskError
+ case taskErrorCode
+ }
- var errorDescription: String? {
- switch self
+ static let cancelled = PluginError(code: .cancelled)
+ static let notFound = PluginError(code: .notFound)
+
+ static func unknown(file: String = #fileID, line: UInt = #line) -> PluginError { PluginError(code: .unknown, sourceFile: file, sourceLine: line) }
+ static func mismatchedHash(hash: String, expectedHash: String) -> PluginError { PluginError(code: .mismatchedHash, hash: hash, expectedHash: expectedHash) }
+ static func taskError(output: String) -> PluginError { PluginError(code: .taskError, taskErrorOutput: output) }
+ static func taskErrorCode(_ code: Int) -> PluginError { PluginError(code: .taskErrorCode, taskErrorCode: code) }
+}
+
+struct PluginError: ALTLocalizedError
+{
+ let code: Code
+
+ var errorTitle: String?
+ var errorFailure: String?
+ var sourceFile: String?
+ var sourceLine: UInt?
+
+ var hash: String?
+ var expectedHash: String?
+ var taskErrorOutput: String?
+ var taskErrorCode: Int?
+
+ var errorFailureReason: String {
+ switch self.code
{
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
- case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
- case .taskError(let output): return output
- case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
+ case .mismatchedHash:
+ let baseMessage = NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.", comment: "")
+ guard let hash = self.hash, let expectedHash = self.expectedHash else { return baseMessage }
+
+ let additionalInfo = String(format: NSLocalizedString("Hash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
+ return baseMessage + "\n\n" + additionalInfo
+
+ case .taskError:
+ if let output = self.taskErrorOutput
+ {
+ return output
+ }
+
+ // Use .taskErrorCode base message as fallback.
+ fallthrough
+
+ case .taskErrorCode:
+ let baseMessage = NSLocalizedString("There was an error installing the Mail plug-in.", comment: "")
+ guard let errorCode = self.taskErrorCode else { return baseMessage }
+
+ let additionalInfo = String(format: NSLocalizedString("(Error Code: %@)", comment: ""), NSNumber(value: errorCode))
+ return baseMessage + " " + additionalInfo
}
}
}
@@ -160,7 +207,7 @@ extension PluginManager
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
- guard self.isMailPluginInstalled else { throw PluginError.unknown }
+ guard self.isMailPluginInstalled else { throw PluginError.unknown() }
// Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
@@ -360,13 +407,13 @@ private extension PluginManager
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
- throw PluginError.taskError(outputString)
+ throw PluginError.taskError(output: outputString)
}
throw PluginError.taskErrorCode(Int(task.terminationStatus))
}
- guard let authorization = task.authorization else { throw PluginError.unknown }
+ guard let authorization = task.authorization else { throw PluginError.unknown() }
return authorization
}
}
diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj
index fbd57881..5d4ab600 100644
--- a/AltStore.xcodeproj/project.pbxproj
+++ b/AltStore.xcodeproj/project.pbxproj
@@ -127,7 +127,6 @@
BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; };
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; };
BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; };
- BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; };
BF66EE822501AE50007EE018 /* AltStoreCore.h in Headers */ = {isa = PBXBuildFile; fileRef = BF66EE802501AE50007EE018 /* AltStoreCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -174,7 +173,6 @@
BF66EEE92501AED0007EE018 /* JSONDecoder+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE52501AED0007EE018 /* JSONDecoder+Properties.swift */; };
BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */; };
BF66EEEB2501AED0007EE018 /* UIApplication+AppExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF66EEE72501AED0007EE018 /* UIApplication+AppExtension.swift */; };
- BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; };
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */; };
@@ -214,7 +212,7 @@
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
BFAD678E25E0649500D4C4D1 /* ALTDebugConnection.mm in Sources */ = {isa = PBXBuildFile; fileRef = BFAD678D25E0649500D4C4D1 /* ALTDebugConnection.mm */; };
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */; };
- BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; };
+ BFAECC522501B0A400528F27 /* CodableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableError.swift */; };
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; };
BFAECC552501B0A400528F27 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; };
@@ -309,7 +307,7 @@
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = BFE972E2260A8B2700D0BDAC /* NSError+libimobiledevice.mm */; };
- BFECAC7F24FD950B0077C41F /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; };
+ BFECAC7F24FD950B0077C41F /* CodableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableError.swift */; };
BFECAC8024FD950B0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; };
BFECAC8124FD950B0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.swift */; };
BFECAC8224FD950B0077C41F /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
@@ -318,7 +316,7 @@
BFECAC8524FD950B0077C41F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; };
BFECAC8624FD950B0077C41F /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAC8852295C90300587369 /* Result+Conveniences.swift */; };
BFECAC8724FD950B0077C41F /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
- BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; };
+ BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableError.swift */; };
BFECAC8924FD950E0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; };
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.swift */; };
BFECAC8B24FD950E0077C41F /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
@@ -348,10 +346,14 @@
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 */; };
+ D51AD27E29356B7B00967AAA /* ALTWrappedError.h in Headers */ = {isa = PBXBuildFile; fileRef = D51AD27C29356B7B00967AAA /* ALTWrappedError.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
+ D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; };
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; };
+ D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */; };
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; };
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; };
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; };
@@ -366,8 +368,12 @@
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; };
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
+ D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
+ D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */; };
+ D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
+ D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; };
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
/* End PBXBuildFile section */
@@ -740,7 +746,7 @@
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = ""; };
BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; };
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; };
- BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = ""; };
+ BFD44605241188C300EAB90A /* CodableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableError.swift; sourceTree = ""; };
BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = ""; };
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = ""; };
BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; };
@@ -818,12 +824,15 @@
BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = ""; };
BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = ""; };
C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D51AD27C29356B7B00967AAA /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = ""; };
+ D51AD27D29356B7B00967AAA /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = ""; };
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; };
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = ""; };
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 = ""; };
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; };
+ D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = ""; };
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = ""; };
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = ""; };
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = ""; };
@@ -839,8 +848,10 @@
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = ""; };
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = ""; };
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = ""; };
+ D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = ""; };
D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = ""; };
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = ""; };
+ D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = ""; };
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = ""; };
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = ""; };
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 = ""; };
@@ -981,7 +992,7 @@
isa = PBXGroup;
children = (
BF1E3128229F474900370A3C /* ServerProtocol.swift */,
- BFD44605241188C300EAB90A /* CodableServerError.swift */,
+ BFD44605241188C300EAB90A /* CodableError.swift */,
);
path = "Server Protocol";
sourceTree = "";
@@ -994,6 +1005,7 @@
BF18BFFF2485A75F00DD5981 /* Server Protocol */,
BFF767CF2489AC240097E58C /* Connections */,
BFF7C92D2578464D00E55F36 /* XPC */,
+ D5DB145728F9DC0300A8F606 /* Errors */,
BFF767C32489A6800097E58C /* Extensions */,
BFF767C42489A6980097E58C /* Categories */,
);
@@ -1030,6 +1042,7 @@
BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */,
BFF0394A25F0551600BE607D /* MenuController.swift */,
BF904DE9265DAE9A00E86C2A /* InstalledApp.swift */,
+ D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */,
BFC15ADB27BC3AD100ED2FB4 /* Plugin */,
BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */,
@@ -1804,10 +1817,21 @@
children = (
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
+ D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */,
);
path = "Error Log";
sourceTree = "";
};
+ D5DB145728F9DC0300A8F606 /* Errors */ = {
+ isa = PBXGroup;
+ children = (
+ D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */,
+ D51AD27C29356B7B00967AAA /* ALTWrappedError.h */,
+ D51AD27D29356B7B00967AAA /* ALTWrappedError.m */,
+ );
+ path = Errors;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -1867,6 +1891,7 @@
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */,
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
+ D51AD27E29356B7B00967AAA /* ALTWrappedError.h in Headers */,
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2366,7 +2391,7 @@
BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */,
BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */,
- BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */,
+ BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */,
BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */,
BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */,
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */,
@@ -2398,7 +2423,8 @@
BFC712BB2512B9CF00AB5EBE /* PluginManager.swift in Sources */,
BFECAC8224FD950B0077C41F /* ServerProtocol.swift in Sources */,
BFECAC8124FD950B0077C41F /* ALTServerError+Conveniences.swift in Sources */,
- BFECAC7F24FD950B0077C41F /* CodableServerError.swift in Sources */,
+ BFECAC7F24FD950B0077C41F /* CodableError.swift in Sources */,
+ D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */,
BFECAC8624FD950B0077C41F /* Result+Conveniences.swift in Sources */,
BF265D1925F843A000080DC9 /* NSError+AltStore.swift in Sources */,
BF904DEA265DAE9A00E86C2A /* InstalledApp.swift in Sources */,
@@ -2414,8 +2440,10 @@
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
BFECAC9424FD98BA0077C41F /* NSError+ALTServerError.m in Sources */,
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */,
+ D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */,
BFECAC9324FD98BA0077C41F /* CFNotificationName+AltStore.m in Sources */,
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
+ D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */,
BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2488,7 +2516,6 @@
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */,
BF580482246A28F7008AE704 /* ViewController.swift in Sources */,
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */,
- BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */,
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2513,7 +2540,7 @@
BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */,
BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */,
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
- BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */,
+ BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */,
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */,
@@ -2535,6 +2562,7 @@
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */,
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */,
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
+ D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */,
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
@@ -2552,12 +2580,14 @@
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
+ D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */,
BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */,
BF66EECC2501AECA007EE018 /* Source.swift in Sources */,
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
+ D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */,
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */,
@@ -2592,6 +2622,7 @@
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
+ D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
@@ -2613,7 +2644,6 @@
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
- BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */,
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift
index c910021b..c6136cce 100644
--- a/AltStore/Authentication/AuthenticationViewController.swift
+++ b/AltStore/Authentication/AuthenticationViewController.swift
@@ -108,7 +108,7 @@ private extension AuthenticationViewController
case .failure(let error as NSError):
DispatchQueue.main.async {
- let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
+ let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift
index b58c5dfa..f2ea222c 100644
--- a/AltStore/Components/ToastView.swift
+++ b/AltStore/Components/ToastView.swift
@@ -51,45 +51,32 @@ class ToastView: RSTToastView
var error = error as NSError
var underlyingError = error.underlyingError
- var preferredDuration: TimeInterval?
-
if
let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{
- // Treat underlyingError as the primary error.
+ // Treat underlyingError as the primary error, but keep localized title + failure.
+
+ let nsError = (error as NSError)
+ error = (unwrappedUnderlyingError as NSError)
+
+ if let localizedTitle = nsError.localizedTitle
+ {
+ error = error.withLocalizedTitle(localizedTitle)
+ }
+
+ if let localizedFailure = nsError.localizedFailure
+ {
+ error = error.withLocalizedFailure(localizedFailure)
+ }
- error = unwrappedUnderlyingError as NSError
underlyingError = nil
-
- preferredDuration = .longToastViewDuration
}
- let text: String
- let detailText: String?
-
- if let failure = error.localizedFailure
- {
- text = failure
- detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
- }
- else if let reason = error.localizedFailureReason
- {
- text = reason
- detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
- }
- else
- {
- text = error.localizedDescription
- detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
- }
+ let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
+ let detailText = error.localizedDescription
self.init(text: text, detailText: detailText)
-
- if let preferredDuration = preferredDuration
- {
- self.preferredDuration = preferredDuration
- }
}
required init(coder aDecoder: NSCoder) {
diff --git a/AltStore/Intents/IntentHandler.swift b/AltStore/Intents/IntentHandler.swift
index 4a11e640..0aec1c00 100644
--- a/AltStore/Intents/IntentHandler.swift
+++ b/AltStore/Intents/IntentHandler.swift
@@ -127,7 +127,7 @@ private extension IntentHandler
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
}
- catch RefreshError.noInstalledApps
+ catch ~RefreshErrorCode.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
}
diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift
index 6b9448f6..a56e95a7 100644
--- a/AltStore/Managing Apps/AppManager.swift
+++ b/AltStore/Managing Apps/AppManager.swift
@@ -379,7 +379,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source)
case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source
- source.error = (error as NSError).sanitizedForCoreData()
+ source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error
}
@@ -466,7 +466,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw context.error ?? OperationError.unknown }
+ guard let result = results.values.first else { throw context.error ?? OperationError.unknown() }
completionHandler(result)
}
catch
@@ -485,7 +485,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result) -> Void) -> Progress
{
guard let storeApp = app.storeApp else {
- completionHandler(.failure(OperationError.appNotFound))
+ completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1)
}
@@ -493,7 +493,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw OperationError.unknown }
+ guard let result = results.values.first else { throw OperationError.unknown() }
completionHandler(result)
}
catch
@@ -529,7 +529,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw OperationError.unknown }
+ guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -571,7 +571,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw OperationError.unknown }
+ guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -597,7 +597,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw OperationError.unknown }
+ guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -622,7 +622,7 @@ extension AppManager
group.completionHandler = { (results) in
do
{
- guard let result = results.values.first else { throw OperationError.unknown }
+ guard let result = results.values.first else { throw OperationError.unknown() }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
@@ -1270,7 +1270,7 @@ private extension AppManager
case .success(let installedApp):
completionHandler(.success(installedApp))
- case .failure(ALTServerError.unknownRequest), .failure(OperationError.appNotFound):
+ case .failure(ALTServerError.unknownRequest), .failure(~OperationError.Code.appNotFound):
// Fall back to installation if AltServer doesn't support newer provisioning profile requests,
// OR if the cached app could not be found and we may need to redownload it.
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
@@ -1544,7 +1544,7 @@ private extension AppManager
}
guard let application = ALTApplication(fileURL: app.fileURL) else {
- completionHandler(.failure(OperationError.appNotFound))
+ completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress
}
@@ -1556,7 +1556,7 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
- guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
+ guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound(name: "AltBackup") }
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1663,7 +1663,7 @@ private extension AppManager
else
{
// Not preferred server, so ignore these specific errors and throw serverNotFound instead.
- return ConnectionError.serverNotFound
+ return OperationError(.serverNotFound)
}
default: return error
@@ -1716,8 +1716,40 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
- catch
+ catch let nsError as NSError
{
+ var appName: String!
+ if let app = operation.app as? (NSManagedObject & AppProtocol)
+ {
+ if let context = app.managedObjectContext
+ {
+ context.performAndWait {
+ appName = app.name
+ }
+ }
+ else
+ {
+ appName = NSLocalizedString("App", comment: "")
+ }
+ }
+ else
+ {
+ appName = operation.app.name
+ }
+
+ let localizedTitle: String
+ switch operation
+ {
+ case .install: localizedTitle = String(format: NSLocalizedString("Failed to Install %@", comment: ""), appName)
+ case .refresh: localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@", comment: ""), appName)
+ case .update: localizedTitle = String(format: NSLocalizedString("Failed to Update %@", comment: ""), appName)
+ case .activate: localizedTitle = String(format: NSLocalizedString("Failed to Activate %@", comment: ""), appName)
+ case .deactivate: localizedTitle = String(format: NSLocalizedString("Failed to Deactivate %@", comment: ""), appName)
+ case .backup: localizedTitle = String(format: NSLocalizedString("Failed to Back Up %@", comment: ""), appName)
+ case .restore: localizedTitle = String(format: NSLocalizedString("Failed to Restore %@ Backup", comment: ""), appName)
+ }
+
+ let error = nsError.withLocalizedTitle(localizedTitle)
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, for: operation)
@@ -1748,7 +1780,7 @@ private extension AppManager
func log(_ error: Error, for operation: AppOperation)
{
// Sanitize NSError on same thread before performing background task.
- let sanitizedError = (error as NSError).sanitizedForCoreData()
+ let sanitizedError = (error as NSError).sanitizedForSerialization()
let loggedErrorOperation: LoggedError.Operation = {
switch operation
diff --git a/AltStore/Managing Apps/AppManagerErrors.swift b/AltStore/Managing Apps/AppManagerErrors.swift
index 60fa34b9..3745b0ca 100644
--- a/AltStore/Managing Apps/AppManagerErrors.swift
+++ b/AltStore/Managing Apps/AppManagerErrors.swift
@@ -22,43 +22,35 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext?
+ var localizedTitle: String? {
+ var localizedTitle: String?
+ self.managedObjectContext?.performAndWait {
+ if self.sources?.count == 1
+ {
+ localizedTitle = NSLocalizedString("Failed to Refresh Store", comment: "")
+ }
+ else if self.errors.count == 1
+ {
+ guard let source = self.errors.keys.first else { return }
+ localizedTitle = String(format: NSLocalizedString("Failed to Refresh Source “%@”", comment: ""), source.name)
+ }
+ else
+ {
+ localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count))
+ }
+ }
+
+ return localizedTitle
+ }
+
var errorDescription: String? {
if let error = self.primaryError
{
return error.localizedDescription
}
- else
+ else if let error = self.errors.values.first, self.errors.count == 1
{
- var localizedDescription: String?
-
- self.managedObjectContext?.performAndWait {
- if self.sources?.count == 1
- {
- localizedDescription = NSLocalizedString("Could not refresh store.", comment: "")
- }
- else if self.errors.count == 1
- {
- guard let source = self.errors.keys.first else { return }
- localizedDescription = String(format: NSLocalizedString("Could not refresh source “%@”.", comment: ""), source.name)
- }
- else
- {
- localizedDescription = String(format: NSLocalizedString("Could not refresh %@ sources.", comment: ""), NSNumber(value: self.errors.count))
- }
- }
-
- return localizedDescription
- }
- }
-
- var recoverySuggestion: String? {
- if let error = self.primaryError as NSError?
- {
- return error.localizedRecoverySuggestion
- }
- else if self.errors.count == 1
- {
- return nil
+ return error.localizedDescription
}
else
{
@@ -67,8 +59,18 @@ extension AppManager
}
var errorUserInfo: [String : Any] {
- guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
- return [NSUnderlyingErrorKey: error]
+ let errors = Array(self.errors.values)
+
+ var userInfo = [String: Any]()
+ userInfo[ALTLocalizedTitleErrorKey] = self.localizedTitle
+ userInfo[NSUnderlyingErrorKey] = self.primaryError
+
+ if #available(iOS 14.5, *), !errors.isEmpty
+ {
+ userInfo[NSMultipleUnderlyingErrorsKey] = errors
+ }
+
+ return userInfo
}
init(_ error: Error)
diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift
index 1aaf81bd..3dbdf35f 100644
--- a/AltStore/Operations/AuthenticationOperation.swift
+++ b/AltStore/Operations/AuthenticationOperation.swift
@@ -13,7 +13,8 @@ import Network
import AltStoreCore
import AltSign
-enum AuthenticationError: LocalizedError
+typealias AuthenticationError = AuthenticationErrorCode.Error
+enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{
case noTeam
case noCertificate
@@ -21,10 +22,10 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey
case missingCertificate
- var errorDescription: String? {
+ var errorFailureReason: String {
switch self {
- case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
- case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
+ case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams.", comment: "")
+ case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
@@ -210,7 +211,7 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
- else { throw AuthenticationError.noTeam }
+ else { throw AuthenticationError(.noTeam) }
// Account
account.isActiveAccount = true
@@ -429,7 +430,7 @@ private extension AuthenticationOperation
}
else
{
- completionHandler(.failure(error ?? OperationError.unknown))
+ completionHandler(.failure(error ?? OperationError.unknown()))
}
}
}
@@ -456,7 +457,7 @@ private extension AuthenticationOperation
}
else
{
- return completionHandler(.failure(AuthenticationError.noTeam))
+ return completionHandler(.failure(AuthenticationError(.noTeam)))
}
}
@@ -488,7 +489,7 @@ private extension AuthenticationOperation
do
{
let certificate = try Result(certificate, error).get()
- guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
+ guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
@@ -496,7 +497,7 @@ private extension AuthenticationOperation
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
- throw AuthenticationError.missingCertificate
+ throw AuthenticationError(.missingCertificate)
}
certificate.privateKey = privateKey
@@ -517,7 +518,7 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate])
{
- guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
+ guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError(.noCertificate))) }
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
diff --git a/AltStore/Operations/BackgroundRefreshAppsOperation.swift b/AltStore/Operations/BackgroundRefreshAppsOperation.swift
index ee8eb0de..25e51bb6 100644
--- a/AltStore/Operations/BackgroundRefreshAppsOperation.swift
+++ b/AltStore/Operations/BackgroundRefreshAppsOperation.swift
@@ -11,11 +11,12 @@ import CoreData
import AltStoreCore
-enum RefreshError: LocalizedError
+typealias RefreshError = RefreshErrorCode.Error
+enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
{
case noInstalledApps
- var errorDescription: String? {
+ var errorFailureReason: String {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -91,7 +92,7 @@ class BackgroundRefreshAppsOperation: ResultOperation<[String: Result
let appName = installedApp.name
self.appName = appName
- guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
+ guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound(name: appName) }
let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
diff --git a/AltStore/Operations/DownloadAppOperation.swift b/AltStore/Operations/DownloadAppOperation.swift
index c386bb35..9866442b 100644
--- a/AltStore/Operations/DownloadAppOperation.swift
+++ b/AltStore/Operations/DownloadAppOperation.swift
@@ -12,29 +12,13 @@ import Roxas
import AltStoreCore
import AltSign
-private extension DownloadAppOperation
-{
- struct DependencyError: ALTLocalizedError
- {
- let dependency: Dependency
- let error: Error
-
- var failure: String? {
- return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
- }
-
- var underlyingError: Error? {
- return self.error
- }
- }
-}
-
@objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation
{
let app: AppProtocol
let context: AppOperationContext
+ private let appName: String
private let bundleIdentifier: String
private var sourceURL: URL?
private let destinationURL: URL
@@ -47,6 +31,7 @@ class DownloadAppOperation: ResultOperation
self.app = app
self.context = context
+ self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = destinationURL
@@ -69,7 +54,7 @@ class DownloadAppOperation: ResultOperation
print("Downloading App:", self.bundleIdentifier)
- guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
+ guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
self.downloadApp(from: sourceURL) { result in
do
@@ -138,7 +123,7 @@ private extension DownloadAppOperation
let fileURL = try result.get()
var isDirectory: ObjCBool = false
- guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
+ guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
@@ -252,7 +237,7 @@ private extension DownloadAppOperation
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set()
- var dependencyError: DependencyError?
+ var dependencyError: Error?
let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
@@ -285,7 +270,7 @@ private extension DownloadAppOperation
}
catch let error as DecodingError
{
- let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
+ let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("The dependencies for %@ could not be determined.", comment: ""), application.name))
completionHandler(.failure(nsError))
}
catch
@@ -294,7 +279,7 @@ private extension DownloadAppOperation
}
}
- func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result) -> Void)
+ func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result) -> Void)
{
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do
@@ -315,9 +300,10 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL))
}
- catch
+ catch let error as NSError
{
- completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
+ let localizedFailure = String(format: NSLocalizedString("The dependency “%@” could not be downloaded.", comment: ""), dependency.preferredFilename)
+ completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
}
}
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift
index d4f9bd8e..c3c015f5 100644
--- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift
+++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift
@@ -45,7 +45,7 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
- guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
+ guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
@@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
- throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
+ throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
else
{
@@ -281,7 +281,7 @@ extension FetchProvisioningProfilesOperation
{
if let expirationDate = sortedExpirationDates.first
{
- throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
+ throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
else
{
diff --git a/AltStore/Operations/FindServerOperation.swift b/AltStore/Operations/FindServerOperation.swift
index 35d8727d..78e2ac4e 100644
--- a/AltStore/Operations/FindServerOperation.swift
+++ b/AltStore/Operations/FindServerOperation.swift
@@ -85,7 +85,7 @@ class FindServerOperation: ResultOperation
else
{
// No servers.
- self.finish(.failure(ConnectionError.serverNotFound))
+ self.finish(.failure(OperationError.serverNotFound))
}
}
}
diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift
index ded928f5..b7c60b98 100644
--- a/AltStore/Operations/OperationError.swift
+++ b/AltStore/Operations/OperationError.swift
@@ -8,55 +8,135 @@
import Foundation
import AltSign
+import AltStoreCore
-enum OperationError: LocalizedError
+extension OperationError
{
- static let domain = OperationError.unknown._domain
+ enum Code: Int, ALTErrorCode, CaseIterable
+ {
+ typealias Error = OperationError
+
+ /* General */
+ case unknown = 1000
+ case unknownResult
+ case cancelled
+ case timedOut
+ case notAuthenticated
+ case appNotFound
+ case unknownUDID
+ case invalidApp
+ case invalidParameters
+ case maximumAppIDLimitReached
+ case noSources
+ case openAppFailed
+ case missingAppGroup
+
+ /* Connection */
+ case serverNotFound = 1200
+ case connectionFailed
+ case connectionDropped
+ }
- case unknown
- case unknownResult
- case cancelled
- case timedOut
+ static let unknownResult: OperationError = .init(code: .unknownResult)
+ static let cancelled: OperationError = .init(code: .cancelled)
+ static let timedOut: OperationError = .init(code: .timedOut)
+ static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
+ static let unknownUDID: OperationError = .init(code: .unknownUDID)
+ static let invalidApp: OperationError = .init(code: .invalidApp)
+ static let invalidParameters: OperationError = .init(code: .invalidParameters)
+ static let noSources: OperationError = .init(code: .noSources)
+ static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
- case notAuthenticated
- case appNotFound
+ static let serverNotFound: OperationError = .init(code: .serverNotFound)
+ static let connectionFailed: OperationError = .init(code: .connectionFailed)
+ static let connectionDropped: OperationError = .init(code: .connectionDropped)
- case unknownUDID
+ static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
+ OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
+ }
- case invalidApp
- case invalidParameters
+ static func appNotFound(name: String?) -> OperationError { OperationError(code: .appNotFound, appName: name) }
+ static func openAppFailed(name: String) -> OperationError { OperationError(code: .openAppFailed, appName: name) }
- case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
+ static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
+ OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
+ }
+}
+
+struct OperationError: ALTLocalizedError
+{
+ let code: Code
- case noSources
+ var errorTitle: String?
+ var errorFailure: String?
- case openAppFailed(name: String)
- case missingAppGroup
+ var appName: String?
+ var requiredAppIDs: Int?
+ var availableAppIDs: Int?
+ var expirationDate: Date?
- var failureReason: String? {
- switch self {
- case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
+ var sourceFile: String?
+ var sourceLine: UInt?
+
+ private init(code: Code, failureReason: String? = nil, appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil, expirationDate: Date? = nil,
+ sourceFile: String? = nil, sourceLine: UInt? = nil)
+ {
+ self.code = code
+ self._failureReason = failureReason
+
+ self.appName = appName
+ self.requiredAppIDs = requiredAppIDs
+ self.availableAppIDs = availableAppIDs
+ self.expirationDate = expirationDate
+ self.sourceFile = sourceFile
+ self.sourceLine = sourceLine
+ }
+
+ var errorFailureReason: String {
+ switch self.code
+ {
+ case .unknown:
+ var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occured.", comment: "")
+ guard let sourceFile, let sourceLine else { return failureReason }
+
+ failureReason += " (\(sourceFile) line \(sourceLine))"
+ return failureReason
+
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
- case .appNotFound: return NSLocalizedString("App not found.", comment: "")
- case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
- case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
+ case .unknownUDID: return NSLocalizedString("AltStore could not determine this device's UDID.", comment: "")
+ case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
+ case .maximumAppIDLimitReached: return NSLocalizedString("You cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
- case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name)
- case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "")
- case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
+ case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be accessed.", comment: "")
+
+ case .appNotFound:
+ let appName = self.appName ?? NSLocalizedString("The app", comment: "")
+ return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
+
+ case .openAppFailed:
+ let appName = self.appName ?? NSLocalizedString("the app", comment: "")
+ return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), appName)
+
+ case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
+ case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
+ case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
}
}
+ private var _failureReason: String?
var recoverySuggestion: String? {
- switch self
+ switch self.code
{
- case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
+ case .serverNotFound: return NSLocalizedString("Make sure you're on the same WiFi network as a computer running AltServer, or try connecting this device to your computer via USB.", comment: "")
+ case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
- let message: String
+ guard let appName = self.appName, let requiredAppIDs = self.requiredAppIDs, let availableAppIDs = self.availableAppIDs, let date = self.expirationDate else { return baseMessage }
+
+ var message: String = ""
if requiredAppIDs > 1
{
@@ -69,23 +149,25 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
- let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
- message = prefixMessage + " " + baseMessage
+ let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
+ message = prefixMessage + " " + baseMessage + "\n\n"
}
else
{
- let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
-
- let dateComponentsFormatter = DateComponentsFormatter()
- dateComponentsFormatter.maximumUnitCount = 1
- dateComponentsFormatter.unitsStyle = .full
-
- let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
-
- let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
- message = baseMessage + " " + remainingTimeMessage
+ message = baseMessage + " "
}
+ let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
+
+ let dateComponentsFormatter = DateComponentsFormatter()
+ dateComponentsFormatter.maximumUnitCount = 1
+ dateComponentsFormatter.unitsStyle = .full
+
+ let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
+
+ let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
+ message += remainingTimeMessage
+
return message
default: return nil
diff --git a/AltStore/Operations/Patch App/PatchAppOperation.swift b/AltStore/Operations/Patch App/PatchAppOperation.swift
index af724e1e..c19723e4 100644
--- a/AltStore/Operations/Patch App/PatchAppOperation.swift
+++ b/AltStore/Operations/Patch App/PatchAppOperation.swift
@@ -25,21 +25,41 @@ protocol PatchAppContext
var error: Error? { get }
}
-enum PatchAppError: LocalizedError
+extension PatchAppError
{
- case unsupportedOperatingSystemVersion(OperatingSystemVersion)
+ enum Code: Int, ALTErrorCode, CaseIterable
+ {
+ typealias Error = PatchAppError
+
+ case unsupportedOperatingSystemVersion
+ }
- var errorDescription: String? {
- switch self
+ static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError { PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion) }
+}
+
+struct PatchAppError: ALTLocalizedError
+{
+ let code: Code
+ var errorFailure: String?
+ var errorTitle: String?
+
+ var osVersion: OperatingSystemVersion?
+
+ var errorFailureReason: String {
+ switch self.code
{
- case .unsupportedOperatingSystemVersion(let osVersion):
- var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
- if osVersion.patchVersion != 0
+ case .unsupportedOperatingSystemVersion:
+ let osVersionString: String
+ if let osVersion = self.osVersion?.stringValue
{
- osVersionString += ".\(osVersion.patchVersion)"
+ osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
+ }
+ else
+ {
+ osVersionString = NSLocalizedString("your device's iOS version", comment: "")
}
- let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
+ let errorDescription = String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
}
}
diff --git a/AltStore/Operations/Patch App/PatchViewController.swift b/AltStore/Operations/Patch App/PatchViewController.swift
index 3b7b0cfb..1c41f2ec 100644
--- a/AltStore/Operations/Patch App/PatchViewController.swift
+++ b/AltStore/Operations/Patch App/PatchViewController.swift
@@ -439,7 +439,7 @@ private extension PatchViewController
do
{
- guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
+ 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)
diff --git a/AltStore/Operations/RefreshAppOperation.swift b/AltStore/Operations/RefreshAppOperation.swift
index 354f02f9..00306097 100644
--- a/AltStore/Operations/RefreshAppOperation.swift
+++ b/AltStore/Operations/RefreshAppOperation.swift
@@ -41,7 +41,7 @@ class RefreshAppOperation: ResultOperation
guard let server = self.context.server, let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
- guard let app = self.context.app else { throw OperationError.appNotFound }
+ guard let app = self.context.app else { throw OperationError.appNotFound(name: nil) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
ServerManager.shared.connect(to: server) { (result) in
@@ -84,7 +84,7 @@ class RefreshAppOperation: ResultOperation
self.managedObjectContext.perform {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
- return self.finish(.failure(OperationError.appNotFound))
+ return self.finish(.failure(OperationError.appNotFound(name: app.name)))
}
self.progress.completedUnitCount += 1
diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift
index 1daa92b0..59ccd73b 100644
--- a/AltStore/Operations/VerifyAppOperation.swift
+++ b/AltStore/Operations/VerifyAppOperation.swift
@@ -8,48 +8,74 @@
import Foundation
+import AltStoreCore
import AltSign
import Roxas
-enum VerificationError: ALTLocalizedError
+extension VerificationError
{
- case privateEntitlements(ALTApplication, entitlements: [String: Any])
- case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
- case iOSVersionNotSupported(ALTApplication)
-
- var app: ALTApplication {
- switch self
- {
- case .privateEntitlements(let app, _): return app
- case .mismatchedBundleIdentifiers(let app, _): return app
- case .iOSVersionNotSupported(let app): return app
- }
+ enum Code: Int, ALTErrorCode, CaseIterable
+ {
+ typealias Error = VerificationError
+
+ case privateEntitlements
+ case mismatchedBundleIdentifiers
+ case iOSVersionNotSupported
}
- var failure: String? {
- return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
- }
+ static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError { VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements) }
+ static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError { VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID) }
+ static func iOSVersionNotSupported(app: ALTApplication) -> VerificationError { VerificationError(code: .iOSVersionNotSupported, app: app) }
+}
+
+struct VerificationError: ALTLocalizedError
+{
+ let code: Code
- var failureReason: String? {
- switch self
+ var errorTitle: String?
+ var errorFailure: String?
+
+ var app: ALTApplication?
+ var entitlements: [String: Any]?
+ var sourceBundleID: String?
+
+
+ var errorFailureReason: String {
+ switch self.code
{
- case .privateEntitlements(let app, _):
- return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
+ case .privateEntitlements:
+ let appName = (self.app?.name as String?).map { String(format: NSLocalizedString("“%@”", comment: ""), $0) } ?? NSLocalizedString("The app", comment: "")
+ return String(format: NSLocalizedString("%@ requires private permissions.", comment: ""), appName)
- case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
- return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
-
- case .iOSVersionNotSupported(let app):
- let name = app.name
-
- var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
- if app.minimumiOSVersion.patchVersion > 0
+ case .mismatchedBundleIdentifiers:
+ if let app = self.app, let bundleID = self.sourceBundleID
{
- version += ".\(app.minimumiOSVersion.patchVersion)"
+ return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, bundleID)
+ }
+ else
+ {
+ return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
}
- let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
- return localizedDescription
+ case .iOSVersionNotSupported:
+ if let app = self.app
+ {
+ var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
+ if app.minimumiOSVersion.patchVersion > 0
+ {
+ version += ".\(app.minimumiOSVersion.patchVersion)"
+ }
+
+ let failureReason = String(format: NSLocalizedString("%@ requires %@.", comment: ""), app.name, version)
+ return failureReason
+ }
+ else
+ {
+ let version = ProcessInfo.processInfo.operatingSystemVersion.stringValue
+
+ let failureReason = String(format: NSLocalizedString("This app does not support iOS %@.", comment: ""), version)
+ return failureReason
+ }
}
}
}
@@ -78,14 +104,17 @@ class VerifyAppOperation: ResultOperation
throw error
}
+ let appName = self.context.app?.name ?? NSLocalizedString("The app", comment: "")
+ self.localizedFailure = String(format: NSLocalizedString("%@ could not be installed.", comment: ""), appName)
+
guard let app = self.context.app else { throw OperationError.invalidParameters }
guard app.bundleIdentifier == self.context.bundleIdentifier else {
- throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
+ throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
- throw VerificationError.iOSVersionNotSupported(app)
+ throw VerificationError.iOSVersionNotSupported(app: app)
}
if #available(iOS 13.5, *)
@@ -116,7 +145,7 @@ class VerifyAppOperation: ResultOperation
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true
- let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
+ let error = VerificationError.privateEntitlements(entitlements, app: app)
self.process(error) { (result) in
self.finish(result.mapError { $0 as Error })
}
@@ -145,15 +174,16 @@ private extension VerifyAppOperation
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async {
- switch error
+ switch error.code
{
- case .privateEntitlements(_, let entitlements):
+ case .privateEntitlements:
+ guard let entitlements = error.entitlements else { return completion(.failure(error)) }
let permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing:
-
+
%@
-
+
Private permissions allow apps to do more than normally allowed by iOS, including potentially accessing sensitive private data. Make sure to only install apps from sources you trust.
""", comment: ""), permissions)
diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift
index 4314c1df..f2a55633 100644
--- a/AltStore/Server/Server.swift
+++ b/AltStore/Server/Server.swift
@@ -8,22 +8,6 @@
import Network
-enum ConnectionError: LocalizedError
-{
- case serverNotFound
- case connectionFailed
- case connectionDropped
-
- var failureReason: String? {
- switch self
- {
- case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
- case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
- case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
- }
- }
-}
-
extension Server
{
enum ConnectionType
diff --git a/AltStore/Server/ServerManager.swift b/AltStore/Server/ServerManager.swift
index abc88c43..04eef659 100644
--- a/AltStore/Server/ServerManager.swift
+++ b/AltStore/Server/ServerManager.swift
@@ -171,7 +171,7 @@ private extension ServerManager
{
case .failed(let error):
print("Failed to connect to service \(server.service?.name ?? "").", error)
- completion(.failure(ConnectionError.connectionFailed))
+ completion(.failure(OperationError.connectionFailed))
case .cancelled:
completion(.failure(OperationError.cancelled))
@@ -192,7 +192,7 @@ private extension ServerManager
func connectToLocalServer(_ server: Server, completion: @escaping (Result) -> Void)
{
- guard let machServiceName = server.machServiceName else { return completion(.failure(ConnectionError.connectionFailed)) }
+ guard let machServiceName = server.machServiceName else { return completion(.failure(OperationError.connectionFailed)) }
let xpcConnection = NSXPCConnection.makeConnection(machServiceName: machServiceName)
diff --git a/AltStore/Settings/Error Log/ErrorDetailsViewController.swift b/AltStore/Settings/Error Log/ErrorDetailsViewController.swift
new file mode 100644
index 00000000..9c805aef
--- /dev/null
+++ b/AltStore/Settings/Error Log/ErrorDetailsViewController.swift
@@ -0,0 +1,53 @@
+//
+// ErrorDetailsViewController.swift
+// AltStore
+//
+// Created by Riley Testut on 10/5/22.
+// Copyright © 2022 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+import AltStoreCore
+
+class ErrorDetailsViewController: UIViewController
+{
+ var loggedError: LoggedError?
+
+ @IBOutlet private var textView: UITextView!
+
+ override func viewDidLoad()
+ {
+ super.viewDidLoad()
+
+ if let error = self.loggedError?.error
+ {
+ self.title = error.localizedErrorCode
+
+ let font = self.textView.font ?? UIFont.preferredFont(forTextStyle: .body)
+ let detailedDescription = error.formattedDetailedDescription(with: font)
+ self.textView.attributedText = detailedDescription
+ }
+ else
+ {
+ self.title = NSLocalizedString("Error Details", comment: "")
+ }
+
+ self.navigationController?.navigationBar.tintColor = .altPrimary
+
+ if #available(iOS 15, *), let sheetController = self.navigationController?.sheetPresentationController
+ {
+ sheetController.detents = [.medium(), .large()]
+ sheetController.selectedDetentIdentifier = .medium
+ sheetController.prefersGrabberVisible = true
+ }
+ }
+
+ override func viewDidLayoutSubviews()
+ {
+ super.viewDidLayoutSubviews()
+
+ self.textView.textContainerInset.left = self.view.layoutMargins.left
+ self.textView.textContainerInset.right = self.view.layoutMargins.right
+ }
+}
diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift
index c0f7fa27..ed7acf70 100644
--- a/AltStore/Settings/Error Log/ErrorLogViewController.swift
+++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift
@@ -37,6 +37,20 @@ class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
}
+
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?)
+ {
+ guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
+
+ let navigationController = segue.destination as! UINavigationController
+
+ let errorDetailsViewController = navigationController.viewControllers.first as! ErrorDetailsViewController
+ errorDetailsViewController.loggedError = loggedError
+ }
+
+ @IBAction private func unwindFromErrorDetails(_ segue: UIStoryboardSegue)
+ {
+ }
}
private extension ErrorLogViewController
@@ -58,13 +72,7 @@ private extension ErrorLogViewController
let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
-
- switch loggedError.domain
- {
- case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code))
- case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code))
- default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode
- }
+ cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
@@ -91,7 +99,10 @@ private extension ErrorLogViewController
},
UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError)
- }
+ },
+ UIAction(title: NSLocalizedString("View More Details", comment: ""), image: UIImage(systemName: "ellipsis.circle")) { [weak self] _ in
+ self?.viewMoreDetails(for: loggedError)
+ },
])
cell.menuButton.menu = menu
@@ -224,13 +235,18 @@ private extension ErrorLogViewController
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
- let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+")
+ let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true)
}
+
+ func viewMoreDetails(for loggedError: LoggedError)
+ {
+ self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
+ }
}
extension ErrorLogViewController
diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard
index 7a6f51bb..7e54e313 100644
--- a/AltStore/Settings/Settings.storyboard
+++ b/AltStore/Settings/Settings.storyboard
@@ -991,11 +991,73 @@ Settings by i cons from the Noun Project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1009,5 +1071,8 @@ Settings by i cons from the Noun Project
+
+
+
diff --git a/AltStore/Sources/SourcesViewController.swift b/AltStore/Sources/SourcesViewController.swift
index 38bcefd1..34fb8745 100644
--- a/AltStore/Sources/SourcesViewController.swift
+++ b/AltStore/Sources/SourcesViewController.swift
@@ -12,17 +12,22 @@ import CoreData
import AltStoreCore
import Roxas
-struct SourceError: LocalizedError
+struct SourceError: ALTLocalizedError
{
- enum Code
+ enum Code: Int, ALTErrorCode
{
+ typealias Error = SourceError
+
case unsupported
}
var code: Code
+ var errorTitle: String?
+ var errorFailure: String?
+
@Managed var source: Source
- var errorDescription: String? {
+ var errorFailureReason: String {
switch self.code
{
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of AltStore.", comment: ""), self.$source.name)
diff --git a/AltStoreCore/AltStoreCore.h b/AltStoreCore/AltStoreCore.h
index 73d95a9c..02ddf3b1 100644
--- a/AltStoreCore/AltStoreCore.h
+++ b/AltStoreCore/AltStoreCore.h
@@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// Shared
#import
#import
+#import
#import
#import
diff --git a/AltStoreCore/Patreon/PatreonAPI.swift b/AltStoreCore/Patreon/PatreonAPI.swift
index 92db9608..31da8653 100644
--- a/AltStoreCore/Patreon/PatreonAPI.swift
+++ b/AltStoreCore/Patreon/PatreonAPI.swift
@@ -15,24 +15,25 @@ private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswux
private let campaignID = "2863968"
-extension PatreonAPI
+typealias PatreonAPIError = PatreonAPIErrorCode.Error
+enum PatreonAPIErrorCode: Int, ALTErrorEnum, CaseIterable
{
- enum Error: LocalizedError
- {
- case unknown
- case notAuthenticated
- case invalidAccessToken
-
- var errorDescription: String? {
- switch self
- {
- case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
- case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
- case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
- }
+ case unknown
+ case notAuthenticated
+ case invalidAccessToken
+
+ var errorFailureReason: String {
+ switch self
+ {
+ case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
+ case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
+ case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
}
}
-
+}
+
+extension PatreonAPI
+{
enum AuthorizationType
{
case none
@@ -110,7 +111,7 @@ public extension PatreonAPI
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value
- else { throw Error.unknown }
+ else { throw PatreonAPIError(.unknown) }
self.fetchAccessToken(oauthCode: code) { (result) in
switch result
@@ -151,9 +152,9 @@ public extension PatreonAPI
self.send(request, authorizationType: .user) { (result: Result) in
switch result
{
- case .failure(Error.notAuthenticated):
+ case .failure(~PatreonAPIErrorCode.notAuthenticated):
self.signOut() { (result) in
- completion(.failure(Error.notAuthenticated))
+ completion(.failure(PatreonAPIError(.notAuthenticated)))
}
case .failure(let error): completion(.failure(error))
@@ -357,11 +358,11 @@ private extension PatreonAPI
{
case .none: break
case .creator:
- guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
+ guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(PatreonAPIError(.invalidAccessToken))) }
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user:
- guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
+ guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(PatreonAPIError(.notAuthenticated))) }
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
@@ -374,8 +375,8 @@ private extension PatreonAPI
{
switch authorizationType
{
- case .creator: completion(.failure(Error.invalidAccessToken))
- case .none: completion(.failure(Error.notAuthenticated))
+ case .creator: completion(.failure(PatreonAPIError(.invalidAccessToken)))
+ case .none: completion(.failure(PatreonAPIError(.notAuthenticated)))
case .user:
self.refreshAccessToken() { (result) in
switch result
diff --git a/Dependencies/AltSign b/Dependencies/AltSign
index 3d6fbeac..88aac399 160000
--- a/Dependencies/AltSign
+++ b/Dependencies/AltSign
@@ -1 +1 @@
-Subproject commit 3d6fbeac25cf3cc3302ede5cee2888733eded399
+Subproject commit 88aac399cff2001c5f00b0306a4d7d1507777a29
diff --git a/Shared/Categories/NSError+ALTServerError.m b/Shared/Categories/NSError+ALTServerError.m
index 534bca30..205a5dc7 100644
--- a/Shared/Categories/NSError+ALTServerError.m
+++ b/Shared/Categories/NSError+ALTServerError.m
@@ -8,9 +8,15 @@
#import "NSError+ALTServerError.h"
-NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
-NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
-NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection";
+#if TARGET_OS_OSX
+#import "AltServer-Swift.h"
+#else
+#import
+#endif
+
+NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
+NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
+NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
@@ -24,8 +30,16 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
+ (void)load
{
- [NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
- if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
+ [NSError alt_setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
+ if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
+ {
+ return [error altserver_localizedDescription];
+ }
+ else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey])
+ {
+ return [error altserver_localizedFailure];
+ }
+ else if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{
return [error altserver_localizedFailureReason];
}
@@ -41,10 +55,10 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return nil;
}];
- [NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
- if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
+ [NSError alt_setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
+ if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{
- return [error altserver_connection_localizedDescription];
+ return [error altserver_connection_localizedFailureReason];
}
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{
@@ -55,6 +69,53 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
}];
}
+- (nullable NSString *)altserver_localizedDescription
+{
+ switch ((ALTServerError)self.code)
+ {
+ case ALTServerErrorUnderlyingError:
+ {
+ // We're wrapping another error, so return the wrapped error's localized description.
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ return underlyingError.localizedDescription;
+ }
+
+ default:
+ return nil;
+ }
+}
+
+- (nullable NSString *)altserver_localizedFailure
+{
+ switch ((ALTServerError)self.code)
+ {
+ case ALTServerErrorUnderlyingError:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ return underlyingError.alt_localizedFailure;
+ }
+
+ case ALTServerErrorConnectionFailed:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ if (underlyingError.localizedFailureReason != nil)
+ {
+ // Only return localized failure if there is an underlying error with failure reason.
+#if TARGET_OS_OSX
+ return NSLocalizedString(@"There was an error connecting to the device.", @"");
+#else
+ return NSLocalizedString(@"AltServer could not establish a connection to AltStore.", @"");
+#endif
+ }
+
+ return nil;
+ }
+
+ default:
+ return nil;
+ }
+}
+
- (nullable NSString *)altserver_localizedFailureReason
{
switch ((ALTServerError)self.code)
@@ -73,6 +134,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode];
}
+ // Return nil because this is a "pass-through" error, so if underlyingError doesn't have failure reason, this doesn't either.
return nil;
}
@@ -80,20 +142,30 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"An unknown error occured.", @"");
case ALTServerErrorConnectionFailed:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ if (underlyingError.localizedFailureReason != nil)
+ {
+ return underlyingError.localizedFailureReason;
+ }
+
+ // Return fallback failure reason if there isn't an underlying error with failure reason.
+
#if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else
- return NSLocalizedString(@"Could not connect to AltServer.", @"");
+ return NSLocalizedString(@"AltServer could not establish a connection to AltStore.", @"");
#endif
+ }
case ALTServerErrorLostConnection:
- return NSLocalizedString(@"Lost connection to AltServer.", @"");
+ return NSLocalizedString(@"The connection to AltServer was lost.", @"");
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"AltServer could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed:
- return NSLocalizedString(@"Failed to write app data to device.", @"");
+ return NSLocalizedString(@"AltServer could not write data to this device.", @"");
case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"AltServer received an invalid request.", @"");
@@ -102,13 +174,21 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"AltServer sent an invalid response.", @"");
case ALTServerErrorInvalidApp:
- return NSLocalizedString(@"The app is invalid.", @"");
+ return NSLocalizedString(@"The app is in an invalid format.", @"");
case ALTServerErrorInstallationFailed:
- return NSLocalizedString(@"An error occured while installing the app.", @"");
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ if (underlyingError != nil)
+ {
+ return underlyingError.localizedFailureReason ?: underlyingError.localizedDescription;
+ }
+
+ return NSLocalizedString(@"An error occurred while installing the app.", @"");
+ }
case ALTServerErrorMaximumFreeAppLimitReached:
- return NSLocalizedString(@"Cannot activate more than 3 apps with a non-developer Apple ID.", @"");
+ return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
@@ -117,7 +197,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"AltServer does not support this request.", @"");
case ALTServerErrorUnknownResponse:
- return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
+ return NSLocalizedString(@"AltStore received an unknown response from AltServer.", @"");
case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"The provided anisette data is invalid.", @"");
@@ -141,7 +221,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
case ALTServerErrorIncompatibleDeveloperDisk:
{
NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @"");
- NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"The disk is incompatible with %@.", @""), osVersion];
+ NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"The disk is incompatible with %@.", @""), osVersion]; // "Developer" disk is included in localizedFailure
return failureReason;
}
}
@@ -153,7 +233,24 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{
switch ((ALTServerError)self.code)
{
+ case ALTServerErrorUnderlyingError:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ return underlyingError.localizedRecoverySuggestion;
+ }
+
case ALTServerErrorConnectionFailed:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ if (underlyingError.localizedRecoverySuggestion != nil)
+ {
+ return underlyingError.localizedRecoverySuggestion;
+ }
+
+ // If there is no underlying error, fall through to ALTServerErrorDeviceNotFound.
+ // return nil;
+ }
+
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @"");
@@ -182,6 +279,12 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{
switch ((ALTServerError)self.code)
{
+ case ALTServerErrorUnderlyingError:
+ {
+ NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
+ return underlyingError.alt_localizedDebugDescription;
+ }
+
case ALTServerErrorIncompatibleDeveloperDisk:
{
NSString *path = self.userInfo[NSFilePathErrorKey];
@@ -191,7 +294,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
}
NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @"");
- NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at\n\n%@\n\nis incompatible with %@.", @""), path, osVersion];
+ NSString *debugDescription = [NSString stringWithFormat:NSLocalizedString(@"The Developer disk located at %@ is incompatible with %@.", @""), path, osVersion];
return debugDescription;
}
@@ -232,7 +335,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
#pragma mark - AltServerConnectionErrorDomain -
-- (nullable NSString *)altserver_connection_localizedDescription
+- (nullable NSString *)altserver_connection_localizedFailureReason
{
switch ((ALTServerConnectionError)self.code)
{
diff --git a/Shared/Errors/ALTLocalizedError.swift b/Shared/Errors/ALTLocalizedError.swift
new file mode 100644
index 00000000..1a50541d
--- /dev/null
+++ b/Shared/Errors/ALTLocalizedError.swift
@@ -0,0 +1,182 @@
+//
+// ALTLocalizedError.swift
+// AltStore
+//
+// Created by Riley Testut on 10/14/22.
+// Copyright © 2022 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import AltSign
+
+public let ALTLocalizedTitleErrorKey = "ALTLocalizedTitle"
+public let ALTLocalizedDescriptionKey = "ALTLocalizedDescription"
+
+public protocol ALTLocalizedError: LocalizedError, CustomNSError, CustomStringConvertible
+{
+ associatedtype Code: ALTErrorCode
+
+ var code: Code { get }
+ var errorFailureReason: String { get }
+
+ var errorTitle: String? { get set }
+ var errorFailure: String? { get set }
+
+ var sourceFile: String? { get set }
+ var sourceLine: UInt? { get set }
+}
+
+public extension ALTLocalizedError
+{
+ var sourceFile: String? {
+ get { nil }
+ set {}
+ }
+
+ var sourceLine: UInt? {
+ get { nil }
+ set {}
+ }
+}
+
+public protocol ALTErrorCode: RawRepresentable where RawValue == Int
+{
+ associatedtype Error: ALTLocalizedError where Error.Code == Self
+
+ static var errorDomain: String { get } // Optional
+}
+
+public protocol ALTErrorEnum: ALTErrorCode
+{
+ associatedtype Error = DefaultLocalizedError
+
+ var errorFailureReason: String { get }
+}
+
+/// LocalizedError & CustomNSError & CustomStringConvertible
+public extension ALTLocalizedError
+{
+ var errorCode: Int { self.code.rawValue }
+
+ var errorDescription: String? {
+ guard (self as NSError).localizedFailure == nil else {
+ // Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason.
+ return nil
+ }
+
+ // Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message.
+ return self.failureReason
+ }
+
+ var failureReason: String? {
+ return self.errorFailureReason
+ }
+
+ var errorUserInfo: [String : Any] {
+ let userInfo: [String: Any?] = [
+ NSLocalizedFailureErrorKey: self.errorFailure,
+ ALTLocalizedTitleErrorKey: self.errorTitle,
+ ALTSourceFileErrorKey: self.sourceFile,
+ ALTSourceLineErrorKey: self.sourceLine,
+ ]
+
+ return userInfo.compactMapValues { $0 }
+ }
+
+ var description: String {
+ let description = "\(self.localizedErrorCode) “\(self.localizedDescription)”"
+ return description
+ }
+}
+
+/// Default Implementations
+public extension ALTLocalizedError where Code: ALTErrorEnum
+{
+ static var errorDomain: String {
+ return Code.errorDomain
+ }
+
+ // ALTErrorEnum Codes provide their failure reason directly.
+ var errorFailureReason: String {
+ return self.code.errorFailureReason
+ }
+}
+
+/// Default Implementations
+public extension ALTErrorCode
+{
+ static var errorDomain: String {
+ let typeName = String(reflecting: Self.self) // "\(Self.self)" doesn't include module name, but String(reflecting:) does.
+ let errorDomain = typeName.replacingOccurrences(of: "ErrorCode", with: "Error")
+ return errorDomain
+ }
+}
+
+public extension ALTLocalizedError
+{
+ // Allows us to initialize errors with localizedTitle + localizedFailure
+ // while still using the error's custom initializer at callsite.
+ init(_ error: Self, localizedTitle: String? = nil, localizedFailure: String? = nil)
+ {
+ self = error
+
+ if let localizedTitle
+ {
+ self.errorTitle = localizedTitle
+ }
+
+ if let localizedFailure
+ {
+ self.errorFailure = localizedFailure
+ }
+ }
+}
+
+public struct DefaultLocalizedError: ALTLocalizedError
+{
+ public let code: Code
+
+ public var errorTitle: String?
+ public var errorFailure: String?
+ public var sourceFile: String?
+ public var sourceLine: UInt?
+
+ public init(_ code: Code, localizedTitle: String? = nil, localizedFailure: String? = nil, sourceFile: String? = #fileID, sourceLine: UInt? = #line)
+ {
+ self.code = code
+ self.errorTitle = localizedTitle
+ self.errorFailure = localizedFailure
+ self.sourceFile = sourceFile
+ self.sourceLine = sourceLine
+ }
+}
+
+/// Custom Operators
+/// These allow us to pattern match ALTErrorCodes against arbitrary errors via ~ prefix.
+prefix operator ~
+public prefix func ~(expression: Code) -> NSError
+{
+ let nsError = NSError(domain: Code.errorDomain, code: expression.rawValue)
+ return nsError
+}
+
+public func ~=(pattern: any Swift.Error, value: any Swift.Error) -> Bool
+{
+ let isMatch = pattern._domain == value._domain && pattern._code == value._code
+ return isMatch
+}
+
+// These operators *should* allow us to match ALTErrorCodes against arbitrary errors,
+// but they don't work as of iOS 16.1 and Swift 5.7.
+//
+//public func ~=(pattern: Error, value: Swift.Error) -> Bool
+//{
+// let isMatch = pattern._domain == value._domain && pattern._code == value._code
+// return isMatch
+//}
+//
+//public func ~=(pattern: Code, value: Swift.Error) -> Bool
+//{
+// let isMatch = Code.errorDomain == value._domain && pattern.rawValue == value._code
+// return isMatch
+//}
diff --git a/Shared/Errors/ALTWrappedError.h b/Shared/Errors/ALTWrappedError.h
new file mode 100644
index 00000000..cc44ed37
--- /dev/null
+++ b/Shared/Errors/ALTWrappedError.h
@@ -0,0 +1,25 @@
+//
+// ALTWrappedError.h
+// AltStoreCore
+//
+// Created by Riley Testut on 11/28/22.
+// Copyright © 2022 Riley Testut. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Overrides localizedDescription to check userInfoValueProvider for failure reason
+// instead of default behavior which just returns NSLocalizedFailureErrorKey if present.
+//
+// Must be written in Objective-C for Swift.Error <-> NSError bridging to work correctly.
+@interface ALTWrappedError : NSError
+
+@property (copy, nonatomic) NSError *wrappedError;
+
+- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary *)userInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Shared/Errors/ALTWrappedError.m b/Shared/Errors/ALTWrappedError.m
new file mode 100644
index 00000000..5b58ec9c
--- /dev/null
+++ b/Shared/Errors/ALTWrappedError.m
@@ -0,0 +1,73 @@
+//
+// ALTWrappedError.m
+// AltStoreCore
+//
+// Created by Riley Testut on 11/28/22.
+// Copyright © 2022 Riley Testut. All rights reserved.
+//
+
+#import "ALTWrappedError.h"
+
+@implementation ALTWrappedError
+
++ (BOOL)supportsSecureCoding
+{
+ // Required in order to serialize errors for legacy AltServer communication.
+ return YES;
+}
+
+- (instancetype)initWithError:(NSError *)error userInfo:(NSDictionary *)userInfo
+{
+ self = [super initWithDomain:error.domain code:error.code userInfo:userInfo];
+ if (self)
+ {
+ if ([error isKindOfClass:[ALTWrappedError class]])
+ {
+ _wrappedError = [(ALTWrappedError *)error wrappedError];
+ }
+ else
+ {
+ _wrappedError = [error copy];
+ }
+ }
+
+ return self;
+}
+
+- (NSString *)localizedDescription
+{
+ NSString *localizedFailure = self.userInfo[NSLocalizedFailureErrorKey];
+ if (localizedFailure != nil)
+ {
+ NSString *wrappedLocalizedDescription = self.wrappedError.userInfo[NSLocalizedDescriptionKey];
+ NSString *localizedFailureReason = wrappedLocalizedDescription ?: self.wrappedError.localizedFailureReason ?: self.wrappedError.localizedDescription;
+
+ NSString *localizedDescription = [NSString stringWithFormat:@"%@ %@", localizedFailure, localizedFailureReason];
+ return localizedDescription;
+ }
+
+ // localizedFailure is nil, so return wrappedError's localizedDescription.
+ return self.wrappedError.localizedDescription;
+}
+
+- (NSString *)localizedFailureReason
+{
+ return self.wrappedError.localizedFailureReason;
+}
+
+- (NSString *)localizedRecoverySuggestion
+{
+ return self.wrappedError.localizedRecoverySuggestion;
+}
+
+- (NSString *)debugDescription
+{
+ return self.wrappedError.debugDescription;
+}
+
+- (NSString *)helpAnchor
+{
+ return self.wrappedError.helpAnchor;
+}
+
+@end
diff --git a/Shared/Extensions/ALTServerError+Conveniences.swift b/Shared/Extensions/ALTServerError+Conveniences.swift
index 8ca47903..2078b25e 100644
--- a/Shared/Extensions/ALTServerError+Conveniences.swift
+++ b/Shared/Extensions/ALTServerError+Conveniences.swift
@@ -21,12 +21,10 @@ public extension ALTServerError
case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error)
case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
case let error as NSError:
+ // Assign error as underlying error, even if there already is one,
+ // because it'll still be accessible via error.underlyingError.underlyingError.
var userInfo = error.userInfo
- if !userInfo.keys.contains(NSUnderlyingErrorKey)
- {
- // Assign underlying error (if there isn't already one).
- userInfo[NSUnderlyingErrorKey] = error
- }
+ userInfo[NSUnderlyingErrorKey] = error
self = ALTServerError(.underlyingError, userInfo: userInfo)
}
diff --git a/Shared/Extensions/NSError+AltStore.swift b/Shared/Extensions/NSError+AltStore.swift
index 55288d49..d0ae9562 100644
--- a/Shared/Extensions/NSError+AltStore.swift
+++ b/Shared/Extensions/NSError+AltStore.swift
@@ -8,7 +8,17 @@
import Foundation
-extension NSError
+#if canImport(UIKit)
+import UIKit
+public typealias ALTFont = UIFont
+#elseif canImport(AppKit)
+import AppKit
+public typealias ALTFont = NSFont
+#endif
+
+import AltSign
+
+public extension NSError
{
@objc(alt_localizedFailure)
var localizedFailure: String? {
@@ -22,50 +32,56 @@ extension NSError
return debugDescription
}
+ @objc(alt_localizedTitle)
+ var localizedTitle: String? {
+ let localizedTitle = self.userInfo[ALTLocalizedTitleErrorKey] as? String
+ return localizedTitle
+ }
+
@objc(alt_errorWithLocalizedFailure:)
func withLocalizedFailure(_ failure: String) -> NSError
{
- var userInfo = self.userInfo
- userInfo[NSLocalizedFailureErrorKey] = failure
-
- if let failureReason = self.localizedFailureReason
+ switch self
{
- userInfo[NSLocalizedFailureReasonErrorKey] = failureReason
+ case var error as any ALTLocalizedError:
+ error.errorFailure = failure
+ return error as NSError
+
+ default:
+ var userInfo = self.userInfo
+ userInfo[NSLocalizedFailureErrorKey] = failure
+
+ let error = ALTWrappedError(error: self, userInfo: userInfo)
+ return error
}
- else if self.localizedFailure == nil && self.localizedFailureReason == nil && self.localizedDescription.contains(self.localizedErrorCode)
- {
- // Default localizedDescription, so replace with just the localized error code portion.
- userInfo[NSLocalizedFailureReasonErrorKey] = "(\(self.localizedErrorCode).)"
- }
- else
- {
- userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedDescription
- }
-
- if let localizedDescription = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
- {
- userInfo[NSLocalizedDescriptionKey] = localizedDescription
- }
-
- // Don't accidentally remove localizedDescription from dictionary
- // userInfo[NSLocalizedDescriptionKey] = NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedDescriptionKey) as? String
-
- if let recoverySuggestion = self.localizedRecoverySuggestion
- {
- userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion
- }
-
- let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
- return error
}
- func sanitizedForCoreData() -> NSError
+ @objc(alt_errorWithLocalizedTitle:)
+ func withLocalizedTitle(_ title: String) -> NSError
+ {
+ switch self
+ {
+ case var error as any ALTLocalizedError:
+ error.errorTitle = title
+ return error as NSError
+
+ default:
+ var userInfo = self.userInfo
+ userInfo[ALTLocalizedTitleErrorKey] = title
+
+ let error = ALTWrappedError(error: self, userInfo: userInfo)
+ return error
+ }
+ }
+
+ func sanitizedForSerialization() -> NSError
{
var userInfo = self.userInfo
- userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
+ userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
+ userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
// Remove userInfo values that don't conform to NSSecureEncoding.
userInfo = userInfo.filter { (key, value) in
@@ -75,22 +91,160 @@ extension NSError
// Sanitize underlying errors.
if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error
{
- let sanitizedError = (underlyingError as NSError).sanitizedForCoreData()
+ let sanitizedError = (underlyingError as NSError).sanitizedForSerialization()
userInfo[NSUnderlyingErrorKey] = sanitizedError
}
if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error]
{
- let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForCoreData() }
+ let sanitizedErrors = underlyingErrors.map { ($0 as NSError).sanitizedForSerialization() }
userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors
}
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
+
+ func formattedDetailedDescription(with font: ALTFont) -> NSAttributedString
+ {
+ #if canImport(UIKit)
+ let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor
+ let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize)
+ #else
+ let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
+ let boldFont = ALTFont(descriptor: boldFontDescriptor, size: font.pointSize) ?? font
+ #endif
+
+ var preferredKeyOrder = [
+ NSDebugDescriptionErrorKey,
+ NSLocalizedDescriptionKey,
+ NSLocalizedFailureErrorKey,
+ NSLocalizedFailureReasonErrorKey,
+ NSLocalizedRecoverySuggestionErrorKey,
+ ALTLocalizedTitleErrorKey,
+ ALTSourceFileErrorKey,
+ ALTSourceLineErrorKey,
+ NSUnderlyingErrorKey
+ ]
+
+ if #available(iOS 14.5, macOS 11.3, *)
+ {
+ preferredKeyOrder.append(NSMultipleUnderlyingErrorsKey)
+ }
+
+ var userInfo = self.userInfo
+ userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
+ userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
+ userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
+ userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
+ userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
+
+ let sortedUserInfo = userInfo.sorted { (a, b) in
+ let indexA = preferredKeyOrder.firstIndex(of: a.key)
+ let indexB = preferredKeyOrder.firstIndex(of: b.key)
+
+ switch (indexA, indexB)
+ {
+ case (let indexA?, let indexB?): return indexA < indexB
+ case (_?, nil): return true // indexA exists, indexB is nil, so A should come first.
+ case (nil, _?): return false // indexA is nil, indexB exists, so B should come first.
+ case (nil, nil): return a.key < b.key // both indexes are nil, so sort alphabetically.
+ }
+ }
+
+ let detailedDescription = NSMutableAttributedString()
+
+ for (key, value) in sortedUserInfo
+ {
+ let keyName: String
+ switch key
+ {
+ case NSDebugDescriptionErrorKey: keyName = NSLocalizedString("Debug Description", comment: "")
+ case NSLocalizedDescriptionKey: keyName = NSLocalizedString("Error Description", comment: "")
+ case NSLocalizedFailureErrorKey: keyName = NSLocalizedString("Failure", comment: "")
+ case NSLocalizedFailureReasonErrorKey: keyName = NSLocalizedString("Failure Reason", comment: "")
+ case NSLocalizedRecoverySuggestionErrorKey: keyName = NSLocalizedString("Recovery Suggestion", comment: "")
+ case ALTLocalizedTitleErrorKey: keyName = NSLocalizedString("Title", comment: "")
+ case ALTSourceFileErrorKey: keyName = NSLocalizedString("Source File", comment: "")
+ case ALTSourceLineErrorKey: keyName = NSLocalizedString("Source Line", comment: "")
+ case NSUnderlyingErrorKey: keyName = NSLocalizedString("Underlying Error", comment: "")
+ default:
+ if #available(iOS 14.5, macOS 11.3, *), key == NSMultipleUnderlyingErrorsKey
+ {
+ keyName = NSLocalizedString("Underlying Errors", comment: "")
+ }
+ else
+ {
+ keyName = key
+ }
+ }
+
+ let attributedKey = NSAttributedString(string: keyName, attributes: [.font: boldFont])
+ let attributedValue = NSAttributedString(string: String(describing: value), attributes: [.font: font])
+
+ let attributedString = NSMutableAttributedString(attributedString: attributedKey)
+ attributedString.mutableString.append("\n")
+ attributedString.append(attributedValue)
+
+ if !detailedDescription.string.isEmpty
+ {
+ detailedDescription.mutableString.append("\n\n")
+ }
+
+ detailedDescription.append(attributedString)
+ }
+
+ // Support dark mode
+ #if canImport(UIKit)
+ if #available(iOS 13, *)
+ {
+ detailedDescription.addAttribute(.foregroundColor, value: UIColor.label, range: NSMakeRange(0, detailedDescription.length))
+ }
+ #else
+ detailedDescription.addAttribute(.foregroundColor, value: NSColor.labelColor, range: NSMakeRange(0, detailedDescription.length))
+ #endif
+
+ return detailedDescription
+ }
}
-extension Error
+public extension NSError
+{
+ typealias UserInfoProvider = (Error, String) -> Any?
+
+ @objc
+ class func alt_setUserInfoValueProvider(forDomain domain: String, provider: UserInfoProvider?)
+ {
+ NSError.setUserInfoValueProvider(forDomain: domain) { (error, key) in
+ let nsError = error as NSError
+
+ switch key
+ {
+ case NSLocalizedDescriptionKey:
+ if nsError.localizedFailure != nil
+ {
+ // Error has localizedFailure, so return nil to construct localizedDescription from it + localizedFailureReason.
+ return nil
+ }
+ else if let localizedDescription = provider?(error, NSLocalizedDescriptionKey) as? String
+ {
+ // Only call provider() if there is no localizedFailure.
+ return localizedDescription
+ }
+
+ // Otherwise, return failureReason for localizedDescription to avoid system prepending "Operation Failed" message.
+ // Do NOT return provider(NSLocalizedFailureReason), which might be unexpectedly nil if unrecognized error code.
+ return nsError.localizedFailureReason
+
+ default:
+ let value = provider?(error, key)
+ return value
+ }
+ }
+ }
+}
+
+public extension Error
{
var underlyingError: Error? {
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
@@ -98,46 +252,22 @@ extension Error
}
var localizedErrorCode: String {
- let localizedErrorCode = String(format: NSLocalizedString("%@ error %@", comment: ""), (self as NSError).domain, (self as NSError).code as NSNumber)
+ let nsError = self as NSError
+ let localizedErrorCode = String(format: NSLocalizedString("%@ %@", comment: ""), nsError.domain, self.displayCode as NSNumber)
return localizedErrorCode
}
-}
-
-protocol ALTLocalizedError: LocalizedError, CustomNSError
-{
- var failure: String? { get }
- var underlyingError: Error? { get }
-}
-
-extension ALTLocalizedError
-{
- var errorUserInfo: [String : Any] {
- let userInfo = ([
- NSLocalizedDescriptionKey: self.errorDescription,
- NSLocalizedFailureReasonErrorKey: self.failureReason,
- NSLocalizedFailureErrorKey: self.failure,
- NSUnderlyingErrorKey: self.underlyingError
- ] as [String: Any?]).compactMapValues { $0 }
- return userInfo
- }
-
- var underlyingError: Error? {
- // Error's default implementation calls errorUserInfo,
- // but ALTLocalizedError.errorUserInfo calls underlyingError.
- // Return nil to prevent infinite recursion.
- return nil
- }
-
- var errorDescription: String? {
- guard let errorFailure = self.failure else { return (self.underlyingError as NSError?)?.localizedDescription }
- guard let failureReason = self.failureReason else { return errorFailure }
+ var displayCode: Int {
+ guard let serverError = self as? ALTServerError else {
+ // Not ALTServerError, so display regular code.
+ return (self as NSError).code
+ }
- let errorDescription = errorFailure + " " + failureReason
- return errorDescription
+ // We want ALTServerError codes to start at 2000,
+ // but we can't change them without breaking AltServer compatibility.
+ // Instead, we just add 2000 when displaying code to user
+ // to make it appear as if codes start at 2000 normally.
+ let code = 2000 + serverError.code.rawValue
+ return code
}
-
- var failureReason: String? { (self.underlyingError as NSError?)?.localizedDescription }
- var recoverySuggestion: String? { (self.underlyingError as NSError?)?.localizedRecoverySuggestion }
- var helpAnchor: String? { (self.underlyingError as NSError?)?.helpAnchor }
}
diff --git a/Shared/Server Protocol/CodableError.swift b/Shared/Server Protocol/CodableError.swift
new file mode 100644
index 00000000..4cca686c
--- /dev/null
+++ b/Shared/Server Protocol/CodableError.swift
@@ -0,0 +1,249 @@
+//
+// CodableError.swift
+// AltKit
+//
+// Created by Riley Testut on 3/5/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
+extension ALTServerError.Code: Codable {}
+
+private extension ErrorUserInfoKey
+{
+ static let altLocalizedDescription: String = "ALTLocalizedDescription"
+ static let altLocalizedFailureReason: String = "ALTLocalizedFailureReason"
+ static let altLocalizedRecoverySuggestion: String = "ALTLocalizedRecoverySuggestion"
+ static let altDebugDescription: String = "ALTDebugDescription"
+}
+
+extension CodableError
+{
+ enum UserInfoValue: Codable
+ {
+ case unknown
+ case string(String)
+ case number(Int)
+ case error(NSError)
+ case codableError(CodableError)
+ indirect case array([UserInfoValue])
+ indirect case dictionary([String: UserInfoValue])
+
+ var value: Any? {
+ switch self
+ {
+ case .unknown: return nil
+ case .string(let string): return string
+ case .number(let number): return number
+ case .error(let error): return error
+ case .codableError(let error): return error.error
+ case .array(let array): return array.compactMap { $0.value } // .compactMap instead of .map to ensure nil values are removed.
+ case .dictionary(let dictionary): return dictionary.compactMapValues { $0.value } // .compactMapValues instead of .mapValues to ensure nil values are removed.
+ }
+ }
+
+ var codableValue: Codable? {
+ switch self
+ {
+ case .unknown, .string, .number: return self.value as? Codable
+ case .codableError(let error): return error
+ case .error(let nsError):
+ // Ignore error because we don't want to fail completely if error contains invalid user info value.
+ let sanitizedError = nsError.sanitizedForSerialization()
+ let data = try? NSKeyedArchiver.archivedData(withRootObject: sanitizedError, requiringSecureCoding: true)
+ return data
+
+ case .array(let array): return array
+ case .dictionary(let dictionary): return dictionary
+ }
+ }
+
+ init(_ rawValue: Any?)
+ {
+ switch rawValue
+ {
+ case let string as String: self = .string(string)
+ case let number as Int: self = .number(number)
+ case let error as NSError: self = .codableError(CodableError(error: error))
+ case let array as [Any]: self = .array(array.compactMap(UserInfoValue.init))
+ case let dictionary as [String: Any]: self = .dictionary(dictionary.compactMapValues(UserInfoValue.init))
+ default: self = .unknown
+ }
+ }
+
+ init(from decoder: Decoder) throws
+ {
+ let container = try decoder.singleValueContainer()
+
+ if
+ let data = try? container.decode(Data.self),
+ let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
+ {
+ self = .error(error)
+ }
+ else if let codableError = try? container.decode(CodableError.self)
+ {
+ self = .codableError(codableError)
+ }
+ else if let string = try? container.decode(String.self)
+ {
+ self = .string(string)
+ }
+ else if let number = try? container.decode(Int.self)
+ {
+ self = .number(number)
+ }
+ else if let array = try? container.decode([UserInfoValue].self)
+ {
+ self = .array(array)
+ }
+ else if let dictionary = try? container.decode([String: UserInfoValue].self)
+ {
+ self = .dictionary(dictionary)
+ }
+ else
+ {
+ self = .unknown
+ }
+ }
+
+ func encode(to encoder: Encoder) throws
+ {
+ var container = encoder.singleValueContainer()
+
+ if let value = self.codableValue
+ {
+ try container.encode(value)
+ }
+ else
+ {
+ try container.encodeNil()
+ }
+ }
+ }
+}
+
+struct CodableError: Codable
+{
+ var error: Error {
+ return self.rawError ?? NSError(domain: self.errorDomain, code: self.errorCode, userInfo: self.userInfo ?? [:])
+ }
+ private var rawError: Error?
+
+ private var errorDomain: String
+ private var errorCode: Int
+ private var userInfo: [String: Any]?
+
+ private enum CodingKeys: String, CodingKey
+ {
+ case errorDomain
+ case errorCode
+ case legacyUserInfo = "userInfo"
+ case errorUserInfo
+ }
+
+ init(error: Error)
+ {
+ self.rawError = error
+
+ let nsError = error as NSError
+ self.errorDomain = nsError.domain
+ self.errorCode = nsError.code
+
+ if !nsError.userInfo.isEmpty
+ {
+ self.userInfo = nsError.userInfo
+ }
+ }
+
+ init(from decoder: Decoder) throws
+ {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ // Assume ALTServerError.errorDomain if no explicit domain provided.
+ self.errorDomain = try container.decodeIfPresent(String.self, forKey: .errorDomain) ?? ALTServerError.errorDomain
+ self.errorCode = try container.decode(Int.self, forKey: .errorCode)
+
+ if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .errorUserInfo)
+ {
+ // Attempt decoding from .errorUserInfo first, because it will gracefully handle unknown user info values.
+
+ // Copy ALTLocalized... values to NSLocalized... if provider is nil or if error is unrecognized.
+ // This ensures we preserve error messages if receiving an unknown error.
+ var userInfo = rawUserInfo.compactMapValues { $0.value }
+
+ // Recognized == the provider returns value for NSLocalizedFailureReasonErrorKey, or error is ALTServerError.underlyingError.
+ let provider = NSError.userInfoValueProvider(forDomain: self.errorDomain)
+ let isRecognizedError = (
+ provider?(self.error, NSLocalizedFailureReasonErrorKey) != nil ||
+ (self.error._domain == ALTServerError.errorDomain && self.error._code == ALTServerError.underlyingError.rawValue)
+ )
+
+ if !isRecognizedError
+ {
+ // Error not recognized, so copy over NSLocalizedDescriptionKey and NSLocalizedFailureReasonErrorKey.
+ userInfo[NSLocalizedDescriptionKey] = userInfo[ErrorUserInfoKey.altLocalizedDescription]
+ userInfo[NSLocalizedFailureReasonErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedFailureReason]
+ }
+
+ // Copy over NSLocalizedRecoverySuggestionErrorKey and NSDebugDescriptionErrorKey if provider returns nil.
+ if provider?(self.error, NSLocalizedRecoverySuggestionErrorKey) == nil
+ {
+ userInfo[NSLocalizedRecoverySuggestionErrorKey] = userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion]
+ }
+
+ if provider?(self.error, NSDebugDescriptionErrorKey) == nil
+ {
+ userInfo[NSDebugDescriptionErrorKey] = userInfo[ErrorUserInfoKey.altDebugDescription]
+ }
+
+ userInfo[ErrorUserInfoKey.altLocalizedDescription] = nil
+ userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nil
+ userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nil
+ userInfo[ErrorUserInfoKey.altDebugDescription] = nil
+
+ self.userInfo = userInfo
+ }
+ else if let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .legacyUserInfo)
+ {
+ // Fall back to decoding .legacyUserInfo, which only supports String and NSError values.
+ let userInfo = rawUserInfo.compactMapValues { $0.value }
+ self.userInfo = userInfo
+ }
+ }
+
+ func encode(to encoder: Encoder) throws
+ {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(self.errorDomain, forKey: .errorDomain)
+ try container.encode(self.errorCode, forKey: .errorCode)
+
+ let rawLegacyUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
+ // .legacyUserInfo only supports String and NSError values.
+ switch value
+ {
+ case let string as String: return .string(string)
+ case let error as NSError: return .error(error) // Must use .error, not .codableError for backwards compatibility.
+ default: return nil
+ }
+ }
+ try container.encodeIfPresent(rawLegacyUserInfo, forKey: .legacyUserInfo)
+
+ let nsError = self.error as NSError
+
+ var userInfo = self.userInfo ?? [:]
+ userInfo[ErrorUserInfoKey.altLocalizedDescription] = nsError.localizedDescription
+ userInfo[ErrorUserInfoKey.altLocalizedFailureReason] = nsError.localizedFailureReason
+ userInfo[ErrorUserInfoKey.altLocalizedRecoverySuggestion] = nsError.localizedRecoverySuggestion
+ userInfo[ErrorUserInfoKey.altDebugDescription] = nsError.localizedDebugDescription
+
+ // No need to use alternate key. This is a no-op if userInfo already contains localizedFailure,
+ // but it caches the UserInfoProvider value if one exists.
+ userInfo[NSLocalizedFailureErrorKey] = nsError.localizedFailure
+
+ let rawUserInfo = userInfo.compactMapValues { UserInfoValue($0) }
+ try container.encodeIfPresent(rawUserInfo, forKey: .errorUserInfo)
+ }
+}
diff --git a/Shared/Server Protocol/CodableServerError.swift b/Shared/Server Protocol/CodableServerError.swift
deleted file mode 100644
index a9980dcb..00000000
--- a/Shared/Server Protocol/CodableServerError.swift
+++ /dev/null
@@ -1,126 +0,0 @@
-//
-// CodableServerError.swift
-// AltKit
-//
-// Created by Riley Testut on 3/5/20.
-// Copyright © 2020 Riley Testut. All rights reserved.
-//
-
-import Foundation
-
-// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
-extension ALTServerError.Code: Codable {}
-
-extension CodableServerError
-{
- enum UserInfoValue: Codable
- {
- case string(String)
- case error(NSError)
-
- public init(from decoder: Decoder) throws
- {
- let container = try decoder.singleValueContainer()
-
- if
- let data = try? container.decode(Data.self),
- let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
- {
- self = .error(error)
- }
- else if let string = try? container.decode(String.self)
- {
- self = .string(string)
- }
- else
- {
- throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded")
- }
- }
-
- func encode(to encoder: Encoder) throws
- {
- var container = encoder.singleValueContainer()
-
- switch self
- {
- case .string(let string): try container.encode(string)
- case .error(let error):
- guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else {
- let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded")
- throw EncodingError.invalidValue(self, context)
- }
-
- try container.encode(data)
- }
- }
- }
-}
-
-struct CodableServerError: Codable
-{
- var error: ALTServerError {
- return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
- }
-
- private var errorCode: ALTServerError.Code
- private var userInfo: [String: Any]?
-
- private enum CodingKeys: String, CodingKey
- {
- case errorCode
- case userInfo
- }
-
- init(error: ALTServerError)
- {
- self.errorCode = error.code
-
- var userInfo = error.userInfo
- if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
- {
- userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
- }
-
- if !userInfo.isEmpty
- {
- self.userInfo = userInfo
- }
- }
-
- init(from decoder: Decoder) throws
- {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- let errorCode = try container.decode(Int.self, forKey: .errorCode)
- self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
-
- let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo)
-
- let userInfo = rawUserInfo?.mapValues { (value) -> Any in
- switch value
- {
- case .string(let string): return string
- case .error(let error): return error
- }
- }
- self.userInfo = userInfo
- }
-
- func encode(to encoder: Encoder) throws
- {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(self.error.code.rawValue, forKey: .errorCode)
-
- let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
- switch value
- {
- case let string as String: return .string(string)
- case let error as NSError: return .error(error)
- default: return nil
- }
- }
- try container.encodeIfPresent(rawUserInfo, forKey: .userInfo)
- }
-}
-
diff --git a/Shared/Server Protocol/ServerProtocol.swift b/Shared/Server Protocol/ServerProtocol.swift
index 2b0041c9..8b543801 100644
--- a/Shared/Server Protocol/ServerProtocol.swift
+++ b/Shared/Server Protocol/ServerProtocol.swift
@@ -197,20 +197,21 @@ public enum ServerResponse: Decodable
// from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol
{
- public var version = 2
+ public var version = 3
public var identifier = "ErrorResponse"
public var error: ALTServerError {
- return self.serverError?.error ?? ALTServerError(self.errorCode)
+ // Must be ALTServerError
+ return self.serverError.map { ALTServerError($0.error) } ?? ALTServerError(self.errorCode)
}
- private var serverError: CodableServerError?
+ private var serverError: CodableError?
// Legacy (v1)
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
- self.serverError = CodableServerError(error: error)
+ self.serverError = CodableError(error: error)
self.errorCode = error.code
}
}