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 } }