[Shared] Refactors error handling based on ALTLocalizedError protocol (#1115)

* [Shared] Revises ALTLocalizedError protocol

* Refactors errors to conform to revised ALTLocalizedError protocol

* [Missing Commit] Remaining changes for ALTLocalizedError

* [AltServer] Refactors errors to conform to revised ALTLocalizedError protocol

* [Missing Commit] Declares ALTLocalizedTitleErrorKey + ALTLocalizedDescriptionKey

* Updates Objective-C errors to match revised ALTLocalizedError

* [Missing Commit] Unnecessary ALTLocalizedDescription logic

* [Shared] Refactors NSError.withLocalizedFailure to properly support ALTLocalizedError

* [Shared] Supports adding localized titles to errors via NSError.withLocalizedTitle()

* Revises ErrorResponse logic to support arbitrary errors and user info values

* [Missed Commit] Renames CodableServerError to CodableError

* Merges ConnectionError into OperationError

* [Missed Commit] Doesn’t check ALTWrappedError’s userInfo for localizedDescription

* [Missed] Fixes incorrect errorDomain for ALTErrorEnums

* [Missed] Removes nonexistent ALTWrappedError.h

* Includes source file and line number in OperationError.unknown failureReason

* Adds localizedTitle to AppManager operation errors

* Fixes adding localizedTitle + localizedFailure to ALTWrappedError

* Updates ToastView to use error’s localizedTitle as title

* [Shared] Adds NSError.formattedDetailedDescription(with:)

Returns formatted NSAttributedString containing all user info values intended for displaying to the user.

* [Shared] Updates Error.localizedErrorCode to say “code” instead of “error”

* Conforms ALTLocalizedError to CustomStringConvertible

* Adds “View More Details” option to Error Log context menu to view detailed error description

* [Shared] Fixes NSError.formattedDetailedDescription appearing black in dark mode

* [AltServer] Updates error alert to match revised error logic

Uses error’s localizedTitle as alert title.

* [AltServer] Adds “View More Details” button to error alert to view detailed error info

* [AltServer] Renames InstallError to OperationError and conforms to ALTErrorEnum

* [Shared] Removes CodableError support for Date user info values

Not currently used, and we don’t want to accidentally parse a non-Date as a Date in the meantime.

* [Shared] Includes dynamic UserInfoValueProvider values in NSError.formattedDetailedDescription()

* [Shared] Includes source file + line in NSError.formattedDetailedDescription()

Automatically captures source file + line when throwing ALTErrorEnums.

* [Shared] Captures source file + line for unknown errors

* Removes sourceFunction from OperationError

* Adds localizedTitle to AuthenticationViewController errors

* [Shared] Moves nested ALTWrappedError logic to ALTWrappedError initializer

* [AltServer] Removes now-redundant localized failure from JIT errors

All JIT errors now have a localizedTitle which effectively says the same thing.

* Makes OperationError.Code start at 1000

“Connection errors” subsection starts at 1200.

* [Shared] Updates Error domains to revised [Source].[ErrorType] format

* Updates ALTWrappedError.localizedDescription to prioritize using wrapped NSLocalizedDescription as failure reason

* Makes ALTAppleAPIError codes start at 3000

* [AltServer] Adds relevant localizedFailures to ALTDeviceManager.installApplication() errors

* Revises OperationError failureReasons and recovery suggestions

All failure reasons now read correctly when preceded by a failure reason and “because”.

* Revises ALTServerError error messages
All failure reasons now read correctly when preceded by a failure reason and “because”.

* Most failure reasons now read correctly when preceded by a failure reason and “because”.
* ALTServerErrorUnderlyingError forwards all user info provider calls to underlying error.

* Revises error messages for ALTAppleAPIErrorIncorrectCredentials

* [Missed] Removes NSError+AltStore.swift from AltStore target

* [Shared] Updates AltServerErrorDomain to revised [Source].[ErrorType] format

* [Shared] Removes “code” from Error.localizedErrorCode

* [Shared] Makes ALTServerError codes (appear to) start at 2000

We can’t change the actual error codes without breaking backwards compatibility, so instead we just add 2000 whenever we display ALTServerError codes to the user.

* Moves VerificationError.errorFailure to VerifyAppOperation

* Supports custom failure reason for OperationError.unknown

* [Shared] Changes AltServerErrorDomain to “AltServer.ServerError”

* [Shared] Converts ALTWrappedError to Objective-C class

NSError subclasses must be written in ObjC for Swift.Error <-> NSError bridging to work correctly.

# Conflicts:
#	AltStore.xcodeproj/project.pbxproj

* Fixes decoding CodableError nested user info values
This commit is contained in:
Riley Testut
2023-01-24 13:56:41 -06:00
committed by GitHub
parent 0c4fe98370
commit 3b38d725d7
44 changed files with 1707 additions and 644 deletions

View File

@@ -11,5 +11,6 @@
#import "ALTConstants.h" #import "ALTConstants.h"
#import "ALTConnection.h" #import "ALTConnection.h"
#import "AltXPCProtocol.h" #import "AltXPCProtocol.h"
#import "ALTWrappedError.h"
#import "NSError+ALTServerError.h" #import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h" #import "CFNotificationName+AltStore.h"

View File

@@ -56,6 +56,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var isAltPluginUpdateAvailable = false private var isAltPluginUpdateAvailable = false
private var popoverController: NSPopover?
private var popoverError: NSError?
private var errorAlert: NSAlert?
func applicationDidFinishLaunching(_ aNotification: Notification) func applicationDidFinishLaunching(_ aNotification: Notification)
{ {
UserDefaults.standard.registerDefaults() UserDefaults.standard.registerDefaults()
@@ -159,8 +163,9 @@ private extension AppDelegate
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
case .failure(let error): case .failure(let error as NSError):
self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("JIT compilation could not be enabled for %@.", comment: ""), app.name)) let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
case .success: case .success:
let alert = NSAlert() let alert = NSAlert()
@@ -250,13 +255,13 @@ private extension AppDelegate
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication): case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore // Ignore
break break
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { 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 self.pluginManager.isUpdateAvailable { result in
switch result 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): case .success(let isUpdateAvailable):
self.isAltPluginUpdateAvailable = 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 nsError = error as NSError
let alert = NSAlert() var messageComponents = [error.localizedDescription]
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)
}
}
if let recoverySuggestion = nsError.localizedRecoverySuggestion if let recoverySuggestion = nsError.localizedRecoverySuggestion
{ {
messageComponents.append(recoverySuggestion) messageComponents.append(recoverySuggestion)
} }
let informativeText = messageComponents.joined(separator: separator) let title = nsError.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
alert.informativeText = informativeText 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) NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
alert.runModal() 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) @objc func toggleLaunchAtLogin(_ item: NSMenuItem)

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@@ -384,5 +384,59 @@
</objects> </objects>
<point key="canvasLocation" x="75" y="0.0"/> <point key="canvasLocation" x="75" y="0.0"/>
</scene> </scene>
<!--Error Details View Controller-->
<scene sceneID="IpC-sd-lPu">
<objects>
<viewController storyboardIdentifier="errorDetailsViewController" id="vPa-1q-slD" customClass="ErrorDetailsViewController" customModule="AltServer" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="umL-zC-zP8">
<rect key="frame" x="0.0" y="0.0" width="450" height="88"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="16" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="aUr-m2-nXm">
<rect key="frame" x="20" y="20" width="410" height="48"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8GZ-nV-XXA">
<rect key="frame" x="-2" y="32" width="40" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Label" id="V5D-v5-MVX">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j2o-2b-63k">
<rect key="frame" x="-2" y="0.0" width="92" height="16"/>
<textFieldCell key="cell" selectable="YES" title="Multiline Label" id="3jf-Z7-88l">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="aUr-m2-nXm" secondAttribute="trailing" constant="20" id="RnW-JC-q92"/>
<constraint firstAttribute="bottom" secondItem="aUr-m2-nXm" secondAttribute="bottom" constant="20" id="eGe-BS-TFJ"/>
<constraint firstItem="aUr-m2-nXm" firstAttribute="top" secondItem="umL-zC-zP8" secondAttribute="top" constant="20" id="fel-H8-WFO"/>
<constraint firstItem="aUr-m2-nXm" firstAttribute="leading" secondItem="umL-zC-zP8" secondAttribute="leading" constant="20" id="tcH-p8-4QH"/>
</constraints>
</view>
<connections>
<outlet property="detailedDescriptionLabel" destination="j2o-2b-63k" id="de3-yf-oTt"/>
<outlet property="errorCodeLabel" destination="8GZ-nV-XXA" id="cXZ-11-wrJ"/>
</connections>
</viewController>
<customObject id="wrv-m0-8zg" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="537" y="17"/>
</scene>
</scenes> </scenes>
</document> </document>

View File

@@ -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 - (void)_enableUnsignedCodeExecutionForProcessWithName:(nullable NSString *)processName pid:(int32_t)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
{ {
dispatch_async(self.connectionQueue, ^{ 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; NSString *attachCommand = nil;
if (processName) if (processName)
@@ -133,7 +130,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy]; NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName; userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name; userInfo[ALTDeviceNameErrorKey] = self.device.name;
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError); return completionHandler(NO, returnError);
@@ -145,7 +141,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy]; NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName; userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name; userInfo[ALTDeviceNameErrorKey] = self.device.name;
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError); return completionHandler(NO, returnError);
@@ -249,7 +244,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
{ {
if (error) 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; return NO;
@@ -292,7 +291,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
break; break;
default: 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; break;
} }
} }

View File

@@ -10,13 +10,14 @@ import Foundation
import AltSign import AltSign
enum DeveloperDiskError: LocalizedError typealias DeveloperDiskError = DeveloperDiskErrorCode.Error
enum DeveloperDiskErrorCode: Int, ALTErrorEnum
{ {
case unknownDownloadURL case unknownDownloadURL
case unsupportedOperatingSystem case unsupportedOperatingSystem
case downloadedDiskNotFound case downloadedDiskNotFound
var errorDescription: String? { var errorFailureReason: String {
switch self switch self
{ {
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "") case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
@@ -88,14 +89,14 @@ class DeveloperDiskManager
{ {
do do
{ {
guard let osName = device.type.osName else { throw DeveloperDiskError.unsupportedOperatingSystem } guard let osName = device.type.osName else { throw DeveloperDiskError(.unsupportedOperatingSystem) }
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?> let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
switch device.type switch device.type
{ {
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
default: throw DeveloperDiskError.unsupportedOperatingSystem default: throw DeveloperDiskError(.unsupportedOperatingSystem)
} }
var osVersion = device.osVersion var osVersion = device.osVersion
@@ -146,7 +147,7 @@ class DeveloperDiskManager
do do
{ {
let developerDiskURLs = try result.get() 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 switch diskURL
{ {
@@ -201,7 +202,7 @@ private extension DeveloperDiskManager
{ {
guard let data = data else { throw error! } 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)) completionHandler(.success(response.disks))
} }
catch 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))) completionHandler(.success((diskFileURL, signatureFileURL)))
} }
@@ -318,7 +319,7 @@ private extension DeveloperDiskManager
} }
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else { guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
return completionHandler(.failure(downloadError ?? DeveloperDiskError.downloadedDiskNotFound)) return completionHandler(.failure(downloadError ?? DeveloperDiskError(.downloadedDiskNotFound)))
} }
completionHandler(.success((diskFileURL, signatureFileURL))) completionHandler(.success((diskFileURL, signatureFileURL)))

View File

@@ -14,34 +14,21 @@ private let appGroupsSemaphore = DispatchSemaphore(value: 1)
private let developerDiskManager = DeveloperDiskManager() private let developerDiskManager = DeveloperDiskManager()
enum InstallError: Int, LocalizedError, _ObjectiveCBridgeableError typealias OperationError = OperationErrorCode.Error
enum OperationErrorCode: Int, ALTErrorEnum
{ {
case cancelled case cancelled
case noTeam case noTeam
case missingPrivateKey case missingPrivateKey
case missingCertificate case missingCertificate
var errorDescription: String? { var errorFailureReason: String {
switch self switch self
{ {
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .noTeam: return "You are not a member of any developer teams." case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "")
case .missingPrivateKey: return "The developer certificate's private key could not be found." case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "")
case .missingCertificate: return "The developer certificate could not be found." case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
}
}
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
} }
} }
} }
@@ -54,16 +41,18 @@ extension ALTDeviceManager
var appName = (url.isFileURL) ? url.deletingPathExtension().lastPathComponent : NSLocalizedString("AltStore", comment: "") var appName = (url.isFileURL) ? url.deletingPathExtension().lastPathComponent : NSLocalizedString("AltStore", comment: "")
func finish(_ result: Result<ALTApplication, Error>, title: String = "") func finish(_ result: Result<ALTApplication, Error>, failure: String? = nil)
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
switch result switch result
{ {
case .success(let app): completion(.success(app)) case .success(let app): completion(.success(app))
case .failure(var error as NSError): 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)) completion(.failure(error))
@@ -144,24 +133,25 @@ extension ALTDeviceManager
let profiles = try result.get() let profiles = try result.get()
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in 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 catch
{ {
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles") finish(.failure(error), failure: NSLocalizedString("AltServer could not fetch new provisioning profiles.", comment: ""))
} }
} }
} }
catch catch
{ {
finish(.failure(error), title: "Failed to Refresh Anisette Data") finish(.failure(error))
} }
} }
} }
catch 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 catch
{ {
finish(.failure(error), title: "Failed to Fetch Certificate") finish(.failure(error), failure: NSLocalizedString("A valid signing certificate could not be created.", comment: ""))
} }
} }
} }
catch 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 catch
{ {
finish(.failure(error), title: "Failed to Fetch Team") finish(.failure(error))
} }
} }
} }
catch catch
{ {
finish(.failure(error), title: "Failed to Authenticate") finish(.failure(error), failure: NSLocalizedString("AltServer could not sign in with your Apple ID.", comment: ""))
} }
} }
} }
catch catch
{ {
finish(.failure(error), title: "Failed to Fetch Anisette Data") finish(.failure(error))
} }
} }
} }
@@ -306,7 +296,7 @@ private extension ALTDeviceManager
} }
else else
{ {
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown))) completionHandler(.failure(error ?? ALTAppleAPIError.unknown()))
} }
} }
} }
@@ -332,7 +322,7 @@ private extension ALTDeviceManager
} }
else else
{ {
throw InstallError.noTeam throw OperationError(.noTeam)
} }
} }
catch 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() func addCertificate()
@@ -393,7 +383,7 @@ private extension ALTDeviceManager
do do
{ {
let certificate = try Result(certificate, error).get() 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 ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
@@ -401,7 +391,7 @@ private extension ALTDeviceManager
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw InstallError.missingCertificate throw OperationError(.missingCertificate)
} }
certificate.privateKey = privateKey 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 ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in

View File

@@ -1217,7 +1217,11 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
// ALTServerErrorUnderlyingError uses its underlying error's failure reason as its error description (if one exists), // 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. // 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}]; returnError = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{NSUnderlyingErrorKey: underlyingError}];
} }

View File

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

View File

@@ -15,24 +15,71 @@ import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true) private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle") private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
enum PluginError: LocalizedError extension PluginError
{ {
case cancelled enum Code: Int, ALTErrorCode
case unknown {
case notFound typealias Error = PluginError
case mismatchedHash(hash: String, expectedHash: String)
case taskError(String) case cancelled
case taskErrorCode(Int) case unknown
case notFound
case mismatchedHash
case taskError
case taskErrorCode
}
var errorDescription: String? { static let cancelled = PluginError(code: .cancelled)
switch self 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 .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", 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 .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 .mismatchedHash:
case .taskError(let output): return output let baseMessage = NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.", comment: "")
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode)) 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) let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization) 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. // Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization) 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 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)) 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 return authorization
} }
} }

View File

@@ -127,7 +127,6 @@
BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; }; BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; };
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; }; BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; };
BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; }; 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 */; }; BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; }; 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, ); }; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; }; BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; };
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.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 */; }; BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
BFAD678E25E0649500D4C4D1 /* ALTDebugConnection.mm in Sources */ = {isa = PBXBuildFile; fileRef = BFAD678D25E0649500D4C4D1 /* ALTDebugConnection.mm */; }; BFAD678E25E0649500D4C4D1 /* ALTDebugConnection.mm in Sources */ = {isa = PBXBuildFile; fileRef = BFAD678D25E0649500D4C4D1 /* ALTDebugConnection.mm */; };
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */; }; 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 */; }; BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; };
BFAECC552501B0A400528F27 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; }; 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 */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = BFE972E2260A8B2700D0BDAC /* NSError+libimobiledevice.mm */; }; 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 */; }; BFECAC8024FD950B0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; };
BFECAC8124FD950B0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.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 */; }; 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 */; }; BFECAC8524FD950B0077C41F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF624858BDE00DD5981 /* Connection.swift */; };
BFECAC8624FD950B0077C41F /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAC8852295C90300587369 /* Result+Conveniences.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 */; }; 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 */; }; BFECAC8924FD950E0077C41F /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18BFF22485828200DD5981 /* ConnectionManager.swift */; };
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.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 */; }; 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, ); }; }; 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 */; }; BFF7C920257844FA00E55F36 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.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 */; }; 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, ); }; }; 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 */; }; D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; };
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.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 */; }; D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; };
D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; }; D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; };
D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; }; 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 */; }; D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */; };
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; }; D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */; };
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.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 */; }; 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 */; }; 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 */; }; D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; }; D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -740,7 +746,7 @@
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = "<group>"; }; BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = "<group>"; };
BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = "<group>"; }; BFD44605241188C300EAB90A /* CodableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableError.swift; sourceTree = "<group>"; };
BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = "<group>"; }; BFD52BD222A06EFB000B7ED1 /* ALTConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTConstants.h; sourceTree = "<group>"; };
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; }; BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; }; 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 = "<group>"; }; BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = "<group>"; };
BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; }; BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = "<group>"; };
C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; };
D51AD27D29356B7B00967AAA /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = "<group>"; };
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; }; D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; }; D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; }; D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; };
D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; }; D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = "<group>"; };
D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; }; 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; }; 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 = "<group>"; };
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; }; D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = "<group>"; };
D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; }; D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = "<group>"; };
D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; }; D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = "<group>"; };
@@ -839,8 +848,10 @@
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = "<group>"; }; D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore9ToAltStore10.xcmappingmodel; sourceTree = "<group>"; };
D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = "<group>"; }; D5DAE0932804B0B80034D8D4 /* ScreenshotProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotProcessor.swift; sourceTree = "<group>"; };
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; }; D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = "<group>"; }; D5E1E7C028077DE90016FC96 /* FetchTrustedSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTrustedSourcesOperation.swift; sourceTree = "<group>"; };
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; }; D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; }; D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; }; D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
@@ -981,7 +992,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF1E3128229F474900370A3C /* ServerProtocol.swift */, BF1E3128229F474900370A3C /* ServerProtocol.swift */,
BFD44605241188C300EAB90A /* CodableServerError.swift */, BFD44605241188C300EAB90A /* CodableError.swift */,
); );
path = "Server Protocol"; path = "Server Protocol";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -994,6 +1005,7 @@
BF18BFFF2485A75F00DD5981 /* Server Protocol */, BF18BFFF2485A75F00DD5981 /* Server Protocol */,
BFF767CF2489AC240097E58C /* Connections */, BFF767CF2489AC240097E58C /* Connections */,
BFF7C92D2578464D00E55F36 /* XPC */, BFF7C92D2578464D00E55F36 /* XPC */,
D5DB145728F9DC0300A8F606 /* Errors */,
BFF767C32489A6800097E58C /* Extensions */, BFF767C32489A6800097E58C /* Extensions */,
BFF767C42489A6980097E58C /* Categories */, BFF767C42489A6980097E58C /* Categories */,
); );
@@ -1030,6 +1042,7 @@
BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */, BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */,
BFF0394A25F0551600BE607D /* MenuController.swift */, BFF0394A25F0551600BE607D /* MenuController.swift */,
BF904DE9265DAE9A00E86C2A /* InstalledApp.swift */, BF904DE9265DAE9A00E86C2A /* InstalledApp.swift */,
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */,
BFC15ADB27BC3AD100ED2FB4 /* Plugin */, BFC15ADB27BC3AD100ED2FB4 /* Plugin */,
BF703195229F36FF006E110F /* Devices */, BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */, BFD52BDC22A0A659000B7ED1 /* Connections */,
@@ -1804,10 +1817,21 @@
children = ( children = (
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */,
); );
path = "Error Log"; path = "Error Log";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D5DB145728F9DC0300A8F606 /* Errors */ = {
isa = PBXGroup;
children = (
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */,
D51AD27C29356B7B00967AAA /* ALTWrappedError.h */,
D51AD27D29356B7B00967AAA /* ALTWrappedError.m */,
);
path = Errors;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@@ -1867,6 +1891,7 @@
BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */, BFAECC5D2501B0BF00528F27 /* ALTConnection.h in Headers */,
BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */, BF66EE942501AEBC007EE018 /* ALTAppPermission.h in Headers */,
BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */, BFAECC602501B0BF00528F27 /* NSError+ALTServerError.h in Headers */,
D51AD27E29356B7B00967AAA /* ALTWrappedError.h in Headers */,
BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */, BFAECC5E2501B0BF00528F27 /* CFNotificationName+AltStore.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2366,7 +2391,7 @@
BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF1FE358251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */, BFECAC8F24FD950E0077C41F /* Result+Conveniences.swift in Sources */,
BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */, BF8CAE472489E772004D6CCE /* DaemonRequestHandler.swift in Sources */,
BFECAC8824FD950E0077C41F /* CodableServerError.swift in Sources */, BFECAC8824FD950E0077C41F /* CodableError.swift in Sources */,
BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */, BFC712C32512D5F100AB5EBE /* XPCConnection.swift in Sources */,
BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */, BFC712C52512D5F100AB5EBE /* XPCConnectionHandler.swift in Sources */,
BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */, BFECAC8A24FD950E0077C41F /* ALTServerError+Conveniences.swift in Sources */,
@@ -2398,7 +2423,8 @@
BFC712BB2512B9CF00AB5EBE /* PluginManager.swift in Sources */, BFC712BB2512B9CF00AB5EBE /* PluginManager.swift in Sources */,
BFECAC8224FD950B0077C41F /* ServerProtocol.swift in Sources */, BFECAC8224FD950B0077C41F /* ServerProtocol.swift in Sources */,
BFECAC8124FD950B0077C41F /* ALTServerError+Conveniences.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 */, BFECAC8624FD950B0077C41F /* Result+Conveniences.swift in Sources */,
BF265D1925F843A000080DC9 /* NSError+AltStore.swift in Sources */, BF265D1925F843A000080DC9 /* NSError+AltStore.swift in Sources */,
BF904DEA265DAE9A00E86C2A /* InstalledApp.swift in Sources */, BF904DEA265DAE9A00E86C2A /* InstalledApp.swift in Sources */,
@@ -2414,8 +2440,10 @@
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */, BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
BFECAC9424FD98BA0077C41F /* NSError+ALTServerError.m in Sources */, BFECAC9424FD98BA0077C41F /* NSError+ALTServerError.m in Sources */,
BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */, BFAD67A325E0854500D4C4D1 /* DeveloperDiskManager.swift in Sources */,
D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */,
BFECAC9324FD98BA0077C41F /* CFNotificationName+AltStore.m in Sources */, BFECAC9324FD98BA0077C41F /* CFNotificationName+AltStore.m in Sources */,
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */, BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */,
BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */, BFE972E3260A8B2700D0BDAC /* NSError+libimobiledevice.mm in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2488,7 +2516,6 @@
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */, BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */,
BF580482246A28F7008AE704 /* ViewController.swift in Sources */, BF580482246A28F7008AE704 /* ViewController.swift in Sources */,
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */, BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */,
BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */,
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */, BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2513,7 +2540,7 @@
BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */, BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */,
BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */, BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */,
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */, BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
BFAECC522501B0A400528F27 /* CodableServerError.swift in Sources */, BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */, BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */, BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */,
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */, BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */,
@@ -2535,6 +2562,7 @@
D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */, D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */,
BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */, BFBF331B2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel in Sources */,
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */, D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */, BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */, BF66EE962501AEBC007EE018 /* ALTPatreonBenefitType.m in Sources */,
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */, BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
@@ -2552,12 +2580,14 @@
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */, D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */,
BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */, BFAECC562501B0A400528F27 /* ALTServerError+Conveniences.swift in Sources */,
BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */, BFAECC592501B0A400528F27 /* Result+Conveniences.swift in Sources */,
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */,
BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */, BFAECC542501B0A400528F27 /* NSError+ALTServerError.m in Sources */,
BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */, BF66EEE12501AECA007EE018 /* DatabaseManager.swift in Sources */,
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */, D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */,
BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */, BF66EEEA2501AED0007EE018 /* UIColor+Hex.swift in Sources */,
BF66EECC2501AECA007EE018 /* Source.swift in Sources */, BF66EECC2501AECA007EE018 /* Source.swift in Sources */,
BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */, BF66EED72501AECA007EE018 /* InstalledApp.swift in Sources */,
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */,
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */, BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */, BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */, BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */,
@@ -2592,6 +2622,7 @@
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
@@ -2613,7 +2644,6 @@
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */, BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */,
BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */,
D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */, D5DAE0942804B0B80034D8D4 /* ScreenshotProcessor.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,

View File

@@ -108,7 +108,7 @@ private extension AuthenticationViewController
case .failure(let error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { 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) let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink toastView.textLabel.textColor = .altPink

View File

@@ -51,45 +51,32 @@ class ToastView: RSTToastView
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
var preferredDuration: TimeInterval?
if if
let unwrappedUnderlyingError = underlyingError, let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue 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 underlyingError = nil
preferredDuration = .longToastViewDuration
} }
let text: String let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText: String? let detailText = error.localizedDescription
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
}
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
} }
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {

View File

@@ -127,7 +127,7 @@ private extension IntentHandler
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} }
catch RefreshError.noInstalledApps catch ~RefreshErrorCode.noInstalledApps
{ {
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} }

View File

@@ -379,7 +379,7 @@ extension AppManager
case .success(let source): fetchedSources.insert(source) case .success(let source): fetchedSources.insert(source)
case .failure(let error): case .failure(let error):
let source = managedObjectContext.object(with: source.objectID) as! Source let source = managedObjectContext.object(with: source.objectID) as! Source
source.error = (error as NSError).sanitizedForCoreData() source.error = (error as NSError).sanitizedForSerialization()
errors[source] = error errors[source] = error
} }
@@ -466,7 +466,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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) completionHandler(result)
} }
catch catch
@@ -485,7 +485,7 @@ extension AppManager
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
guard let storeApp = app.storeApp else { guard let storeApp = app.storeApp else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return Progress.discreteProgress(totalUnitCount: 1) return Progress.discreteProgress(totalUnitCount: 1)
} }
@@ -493,7 +493,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do do
{ {
guard let result = results.values.first else { throw OperationError.unknown } guard let result = results.values.first else { throw OperationError.unknown() }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -529,7 +529,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -571,7 +571,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -597,7 +597,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -622,7 +622,7 @@ extension AppManager
group.completionHandler = { (results) in group.completionHandler = { (results) in
do 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() let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil) assert(installedApp.managedObjectContext != nil)
@@ -1270,7 +1270,7 @@ private extension AppManager
case .success(let installedApp): case .success(let installedApp):
completionHandler(.success(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, // 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. // 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. 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 { guard let application = ALTApplication(fileURL: app.fileURL) else {
completionHandler(.failure(OperationError.appNotFound)) completionHandler(.failure(OperationError.appNotFound(name: app.name)))
return progress return progress
} }
@@ -1556,7 +1556,7 @@ private extension AppManager
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString) let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) 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) let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp } guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
@@ -1663,7 +1663,7 @@ private extension AppManager
else else
{ {
// Not preferred server, so ignore these specific errors and throw serverNotFound instead. // Not preferred server, so ignore these specific errors and throw serverNotFound instead.
return ConnectionError.serverNotFound return OperationError(.serverNotFound)
} }
default: return error default: return error
@@ -1716,8 +1716,40 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) } 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) group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
self.log(error, for: operation) self.log(error, for: operation)
@@ -1748,7 +1780,7 @@ private extension AppManager
func log(_ error: Error, for operation: AppOperation) func log(_ error: Error, for operation: AppOperation)
{ {
// Sanitize NSError on same thread before performing background task. // 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 = { let loggedErrorOperation: LoggedError.Operation = {
switch operation switch operation

View File

@@ -22,43 +22,35 @@ extension AppManager
var managedObjectContext: NSManagedObjectContext? 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? { var errorDescription: String? {
if let error = self.primaryError if let error = self.primaryError
{ {
return error.localizedDescription return error.localizedDescription
} }
else else if let error = self.errors.values.first, self.errors.count == 1
{ {
var localizedDescription: String? return error.localizedDescription
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
} }
else else
{ {
@@ -67,8 +59,18 @@ extension AppManager
} }
var errorUserInfo: [String : Any] { var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] } let errors = Array(self.errors.values)
return [NSUnderlyingErrorKey: error]
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) init(_ error: Error)

View File

@@ -13,7 +13,8 @@ import Network
import AltStoreCore import AltStoreCore
import AltSign import AltSign
enum AuthenticationError: LocalizedError typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noTeam case noTeam
case noCertificate case noCertificate
@@ -21,10 +22,10 @@ enum AuthenticationError: LocalizedError
case missingPrivateKey case missingPrivateKey
case missingCertificate case missingCertificate
var errorDescription: String? { var errorFailureReason: String {
switch self { switch self {
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "") case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", 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 .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate 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 guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), 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) 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
account.isActiveAccount = true account.isActiveAccount = true
@@ -429,7 +430,7 @@ private extension AuthenticationOperation
} }
else else
{ {
completionHandler(.failure(error ?? OperationError.unknown)) completionHandler(.failure(error ?? OperationError.unknown()))
} }
} }
} }
@@ -456,7 +457,7 @@ private extension AuthenticationOperation
} }
else else
{ {
return completionHandler(.failure(AuthenticationError.noTeam)) return completionHandler(.failure(AuthenticationError(.noTeam)))
} }
} }
@@ -488,7 +489,7 @@ private extension AuthenticationOperation
do do
{ {
let certificate = try Result(certificate, error).get() 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 ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do do
@@ -496,7 +497,7 @@ private extension AuthenticationOperation
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw AuthenticationError.missingCertificate throw AuthenticationError(.missingCertificate)
} }
certificate.privateKey = privateKey certificate.privateKey = privateKey
@@ -517,7 +518,7 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate]) 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 ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success if let error = error, !success

View File

@@ -11,11 +11,12 @@ import CoreData
import AltStoreCore import AltStoreCore
enum RefreshError: LocalizedError typealias RefreshError = RefreshErrorCode.Error
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
case noInstalledApps case noInstalledApps
var errorDescription: String? { var errorFailureReason: String {
switch self switch self
{ {
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "") case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
@@ -91,7 +92,7 @@ class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledA
super.main() super.main()
guard !self.installedApps.isEmpty else { guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError.noInstalledApps)) self.finish(.failure(RefreshError(.noInstalledApps)))
return return
} }
@@ -207,11 +208,11 @@ private extension BackgroundRefreshAppsOperation
content.title = NSLocalizedString("Refreshed Apps", comment: "") content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "") content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
} }
catch ConnectionError.serverNotFound catch ~OperationError.Code.serverNotFound
{ {
shouldPresentAlert = false shouldPresentAlert = false
} }
catch RefreshError.noInstalledApps catch ~RefreshErrorCode.noInstalledApps
{ {
shouldPresentAlert = false shouldPresentAlert = false
} }

View File

@@ -55,7 +55,7 @@ class BackupAppOperation: ResultOperation<Void>
let appName = installedApp.name let appName = installedApp.name
self.appName = appName 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 let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false) var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)

View File

@@ -12,29 +12,13 @@ import Roxas
import AltStoreCore import AltStoreCore
import AltSign 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) @objc(DownloadAppOperation)
class DownloadAppOperation: ResultOperation<ALTApplication> class DownloadAppOperation: ResultOperation<ALTApplication>
{ {
let app: AppProtocol let app: AppProtocol
let context: AppOperationContext let context: AppOperationContext
private let appName: String
private let bundleIdentifier: String private let bundleIdentifier: String
private var sourceURL: URL? private var sourceURL: URL?
private let destinationURL: URL private let destinationURL: URL
@@ -47,6 +31,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
self.app = app self.app = app
self.context = context self.context = context
self.appName = app.name
self.bundleIdentifier = app.bundleIdentifier self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url self.sourceURL = app.url
self.destinationURL = destinationURL self.destinationURL = destinationURL
@@ -69,7 +54,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
print("Downloading App:", self.bundleIdentifier) 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 self.downloadApp(from: sourceURL) { result in
do do
@@ -138,7 +123,7 @@ private extension DownloadAppOperation
let fileURL = try result.get() let fileURL = try result.get()
var isDirectory: ObjCBool = false 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) 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) let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
var dependencyURLs = Set<URL>() var dependencyURLs = Set<URL>()
var dependencyError: DependencyError? var dependencyError: Error?
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1) 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 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)) completionHandler(.failure(nsError))
} }
catch catch
@@ -294,7 +279,7 @@ private extension DownloadAppOperation
} }
} }
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void) func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
{ {
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
do do
@@ -315,9 +300,10 @@ private extension DownloadAppOperation
completionHandler(.success(destinationURL)) 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) progress.addChild(downloadTask.progress, withPendingUnitCount: 1)

View File

@@ -45,7 +45,7 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
let session = self.context.session let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) } 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) self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
@@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation
{ {
if let expirationDate = sortedExpirationDates.first 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 else
{ {
@@ -281,7 +281,7 @@ extension FetchProvisioningProfilesOperation
{ {
if let expirationDate = sortedExpirationDates.first 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 else
{ {

View File

@@ -85,7 +85,7 @@ class FindServerOperation: ResultOperation<Server>
else else
{ {
// No servers. // No servers.
self.finish(.failure(ConnectionError.serverNotFound)) self.finish(.failure(OperationError.serverNotFound))
} }
} }
} }

View File

@@ -8,55 +8,135 @@
import Foundation import Foundation
import AltSign 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 static let unknownResult: OperationError = .init(code: .unknownResult)
case unknownResult static let cancelled: OperationError = .init(code: .cancelled)
case cancelled static let timedOut: OperationError = .init(code: .timedOut)
case 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 static let serverNotFound: OperationError = .init(code: .serverNotFound)
case appNotFound 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 static func appNotFound(name: String?) -> OperationError { OperationError(code: .appNotFound, appName: name) }
case invalidParameters 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) var appName: String?
case missingAppGroup var requiredAppIDs: Int?
var availableAppIDs: Int?
var expirationDate: Date?
var failureReason: String? { var sourceFile: String?
switch self { var sourceLine: UInt?
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
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 .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "") case .unknownUDID: return NSLocalizedString("AltStore could not determine this device's UDID.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", 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 .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 accessed.", comment: "")
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 .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? { 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 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 if requiredAppIDs > 1
{ {
@@ -69,23 +149,25 @@ enum OperationError: LocalizedError
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs)) 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) let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage message = prefixMessage + " " + baseMessage + "\n\n"
} }
else else
{ {
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date) message = baseMessage + " "
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
} }
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 return message
default: return nil default: return nil

View File

@@ -25,21 +25,41 @@ protocol PatchAppContext
var error: Error? { get } 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? { static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError { PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion) }
switch self }
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): case .unsupportedOperatingSystemVersion:
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" let osVersionString: String
if osVersion.patchVersion != 0 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 return errorDescription
} }
} }

View File

@@ -439,7 +439,7 @@ private extension PatchViewController
do 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() _ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)

View File

@@ -41,7 +41,7 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
guard let server = self.context.server, let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters } 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 } guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
ServerManager.shared.connect(to: server) { (result) in ServerManager.shared.connect(to: server) { (result) in
@@ -84,7 +84,7 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
self.managedObjectContext.perform { self.managedObjectContext.perform {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier) let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else { 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 self.progress.completedUnitCount += 1

View File

@@ -8,48 +8,74 @@
import Foundation import Foundation
import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
enum VerificationError: ALTLocalizedError extension VerificationError
{ {
case privateEntitlements(ALTApplication, entitlements: [String: Any]) enum Code: Int, ALTErrorCode, CaseIterable
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String) {
case iOSVersionNotSupported(ALTApplication) typealias Error = VerificationError
var app: ALTApplication { case privateEntitlements
switch self case mismatchedBundleIdentifiers
{ case iOSVersionNotSupported
case .privateEntitlements(let app, _): return app
case .mismatchedBundleIdentifiers(let app, _): return app
case .iOSVersionNotSupported(let app): return app
}
} }
var failure: String? { static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError { VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements) }
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name) 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? { var errorTitle: String?
switch self var errorFailure: String?
var app: ALTApplication?
var entitlements: [String: Any]?
var sourceBundleID: String?
var errorFailureReason: String {
switch self.code
{ {
case .privateEntitlements(let app, _): case .privateEntitlements:
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name) 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): case .mismatchedBundleIdentifiers:
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID) if let app = self.app, let bundleID = self.sourceBundleID
case .iOSVersionNotSupported(let app):
let name = app.name
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
if app.minimumiOSVersion.patchVersion > 0
{ {
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) case .iOSVersionNotSupported:
return localizedDescription 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<Void>
throw error 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 let app = self.context.app else { throw OperationError.invalidParameters }
guard app.bundleIdentifier == self.context.bundleIdentifier else { 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 { guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
throw VerificationError.iOSVersionNotSupported(app) throw VerificationError.iOSVersionNotSupported(app: app)
} }
if #available(iOS 13.5, *) if #available(iOS 13.5, *)
@@ -116,7 +145,7 @@ class VerifyAppOperation: ResultOperation<Void>
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any] let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
app.hasPrivateEntitlements = true app.hasPrivateEntitlements = true
let error = VerificationError.privateEntitlements(app, entitlements: entitlements) let error = VerificationError.privateEntitlements(entitlements, app: app)
self.process(error) { (result) in self.process(error) { (result) in
self.finish(result.mapError { $0 as Error }) 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)) } guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
DispatchQueue.main.async { 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 permissions = entitlements.keys.sorted().joined(separator: "\n")
let message = String(format: NSLocalizedString(""" let message = String(format: NSLocalizedString("""
You must allow access to these private permissions before continuing: 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. 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) """, comment: ""), permissions)

View File

@@ -8,22 +8,6 @@
import Network 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 extension Server
{ {
enum ConnectionType enum ConnectionType

View File

@@ -171,7 +171,7 @@ private extension ServerManager
{ {
case .failed(let error): case .failed(let error):
print("Failed to connect to service \(server.service?.name ?? "").", error) print("Failed to connect to service \(server.service?.name ?? "").", error)
completion(.failure(ConnectionError.connectionFailed)) completion(.failure(OperationError.connectionFailed))
case .cancelled: case .cancelled:
completion(.failure(OperationError.cancelled)) completion(.failure(OperationError.cancelled))
@@ -192,7 +192,7 @@ private extension ServerManager
func connectToLocalServer(_ server: Server, completion: @escaping (Result<Connection, Error>) -> Void) func connectToLocalServer(_ server: Server, completion: @escaping (Result<Connection, Error>) -> 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) let xpcConnection = NSXPCConnection.makeConnection(machServiceName: machServiceName)

View File

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

View File

@@ -37,6 +37,20 @@ class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = 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 private extension ErrorLogViewController
@@ -58,13 +72,7 @@ private extension ErrorLogViewController
let cell = cell as! ErrorLogTableViewCell let cell = cell as! ErrorLogTableViewCell
cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date) cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date)
cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "") cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "")
cell.errorCodeLabel.text = loggedError.error.localizedErrorCode
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
}
let nsError = loggedError.error as NSError let nsError = loggedError.error as NSError
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") 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 UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
self?.searchFAQ(for: loggedError) 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 cell.menuButton.menu = menu
@@ -224,13 +235,18 @@ private extension ErrorLogViewController
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")! let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! 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)] components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL) let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
safariViewController.preferredControlTintColor = .altPrimary safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true) self.present(safariViewController, animated: true)
} }
func viewMoreDetails(for loggedError: LoggedError)
{
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
} }
extension ErrorLogViewController extension ErrorLogViewController

View File

@@ -991,11 +991,73 @@ Settings by i cons from the Noun Project</string>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections>
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
</connections>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="rU1-TZ-TD8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1697" y="1774"/> <point key="canvasLocation" x="1697" y="1774"/>
</scene> </scene>
<!--Error Details View Controller-->
<scene sceneID="XNO-Yg-I7t">
<objects>
<viewController id="xB2-Se-VVg" customClass="ErrorDetailsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eBQ-se-VIy">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ctd-NB-4ov">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="Nm8-69-Ngi"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ctd-NB-4ov" firstAttribute="leading" secondItem="eBQ-se-VIy" secondAttribute="leading" id="Cv1-Te-gBH"/>
<constraint firstItem="ctd-NB-4ov" firstAttribute="top" secondItem="eBQ-se-VIy" secondAttribute="top" id="HRY-Rg-iMI"/>
<constraint firstAttribute="trailing" secondItem="ctd-NB-4ov" secondAttribute="trailing" id="Lc1-K7-iuq"/>
<constraint firstAttribute="bottom" secondItem="ctd-NB-4ov" secondAttribute="bottom" id="zCz-Cy-Y5z"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="XpE-V9-EaY">
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="rnr-dX-4Ev">
<connections>
<segue destination="ZSp-1n-UJ9" kind="unwind" unwindAction="unwindFromErrorDetails:" id="TFu-zD-QyF"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="textView" destination="ctd-NB-4ov" id="x2C-9R-Xz1"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8AM-Vx-XTN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="ZSp-1n-UJ9" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3389.5999999999999" y="1772.5637181409297"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="4LJ-Od-dCK">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7gm-d1-zWK" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="xB2-Se-VVg" kind="relationship" relationship="rootViewController" id="RpP-UM-JfJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OXW-bf-HIj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2554" y="1773"/>
</scene>
</scenes> </scenes>
<resources> <resources>
<image name="Next" width="18" height="18"/> <image name="Next" width="18" height="18"/>
@@ -1009,5 +1071,8 @@ Settings by i cons from the Noun Project</string>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@@ -12,17 +12,22 @@ import CoreData
import AltStoreCore import AltStoreCore
import Roxas import Roxas
struct SourceError: LocalizedError struct SourceError: ALTLocalizedError
{ {
enum Code enum Code: Int, ALTErrorCode
{ {
typealias Error = SourceError
case unsupported case unsupported
} }
var code: Code var code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var source: Source @Managed var source: Source
var errorDescription: String? { var errorFailureReason: String {
switch self.code switch self.code
{ {
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of AltStore.", comment: ""), self.$source.name) case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of AltStore.", comment: ""), self.$source.name)

View File

@@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
// Shared // Shared
#import <AltStoreCore/ALTConstants.h> #import <AltStoreCore/ALTConstants.h>
#import <AltStoreCore/ALTConnection.h> #import <AltStoreCore/ALTConnection.h>
#import <AltStoreCore/ALTWrappedError.h>
#import <AltStoreCore/NSError+ALTServerError.h> #import <AltStoreCore/NSError+ALTServerError.h>
#import <AltStoreCore/CFNotificationName+AltStore.h> #import <AltStoreCore/CFNotificationName+AltStore.h>

View File

@@ -15,24 +15,25 @@ private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswux
private let campaignID = "2863968" private let campaignID = "2863968"
extension PatreonAPI typealias PatreonAPIError = PatreonAPIErrorCode.Error
enum PatreonAPIErrorCode: Int, ALTErrorEnum, CaseIterable
{ {
enum Error: LocalizedError case unknown
{ case notAuthenticated
case unknown case invalidAccessToken
case notAuthenticated
case invalidAccessToken var errorFailureReason: String {
switch self
var errorDescription: String? { {
switch self case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
{ case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "") case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
}
} }
} }
}
extension PatreonAPI
{
enum AuthorizationType enum AuthorizationType
{ {
case none case none
@@ -110,7 +111,7 @@ public extension PatreonAPI
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false), let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value let code = codeQueryItem.value
else { throw Error.unknown } else { throw PatreonAPIError(.unknown) }
self.fetchAccessToken(oauthCode: code) { (result) in self.fetchAccessToken(oauthCode: code) { (result) in
switch result switch result
@@ -151,9 +152,9 @@ public extension PatreonAPI
self.send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in self.send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in
switch result switch result
{ {
case .failure(Error.notAuthenticated): case .failure(~PatreonAPIErrorCode.notAuthenticated):
self.signOut() { (result) in self.signOut() { (result) in
completion(.failure(Error.notAuthenticated)) completion(.failure(PatreonAPIError(.notAuthenticated)))
} }
case .failure(let error): completion(.failure(error)) case .failure(let error): completion(.failure(error))
@@ -357,11 +358,11 @@ private extension PatreonAPI
{ {
case .none: break case .none: break
case .creator: 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") request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user: 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") request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
} }
@@ -374,8 +375,8 @@ private extension PatreonAPI
{ {
switch authorizationType switch authorizationType
{ {
case .creator: completion(.failure(Error.invalidAccessToken)) case .creator: completion(.failure(PatreonAPIError(.invalidAccessToken)))
case .none: completion(.failure(Error.notAuthenticated)) case .none: completion(.failure(PatreonAPIError(.notAuthenticated)))
case .user: case .user:
self.refreshAccessToken() { (result) in self.refreshAccessToken() { (result) in
switch result switch result

View File

@@ -8,9 +8,15 @@
#import "NSError+ALTServerError.h" #import "NSError+ALTServerError.h"
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer"; #if TARGET_OS_OSX
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation"; #import "AltServer-Swift.h"
NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection"; #else
#import <AltStoreCore/AltStoreCore-Swift.h>
#endif
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain"; NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode"; NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
@@ -24,8 +30,16 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
+ (void)load + (void)load
{ {
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { [NSError alt_setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey]) 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]; return [error altserver_localizedFailureReason];
} }
@@ -41,10 +55,10 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return nil; return nil;
}]; }];
[NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { [NSError alt_setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{ {
return [error altserver_connection_localizedDescription]; return [error altserver_connection_localizedFailureReason];
} }
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey]) 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 - (nullable NSString *)altserver_localizedFailureReason
{ {
switch ((ALTServerError)self.code) switch ((ALTServerError)self.code)
@@ -73,6 +134,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode]; 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; return nil;
} }
@@ -80,20 +142,30 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"An unknown error occured.", @""); return NSLocalizedString(@"An unknown error occured.", @"");
case ALTServerErrorConnectionFailed: 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 #if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @""); return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else #else
return NSLocalizedString(@"Could not connect to AltServer.", @""); return NSLocalizedString(@"AltServer could not establish a connection to AltStore.", @"");
#endif #endif
}
case ALTServerErrorLostConnection: case ALTServerErrorLostConnection:
return NSLocalizedString(@"Lost connection to AltServer.", @""); return NSLocalizedString(@"The connection to AltServer was lost.", @"");
case ALTServerErrorDeviceNotFound: case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"AltServer could not find this device.", @""); return NSLocalizedString(@"AltServer could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed: case ALTServerErrorDeviceWriteFailed:
return NSLocalizedString(@"Failed to write app data to device.", @""); return NSLocalizedString(@"AltServer could not write data to this device.", @"");
case ALTServerErrorInvalidRequest: case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"AltServer received an invalid request.", @""); return NSLocalizedString(@"AltServer received an invalid request.", @"");
@@ -102,13 +174,21 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
return NSLocalizedString(@"AltServer sent an invalid response.", @""); return NSLocalizedString(@"AltServer sent an invalid response.", @"");
case ALTServerErrorInvalidApp: case ALTServerErrorInvalidApp:
return NSLocalizedString(@"The app is invalid.", @""); return NSLocalizedString(@"The app is in an invalid format.", @"");
case ALTServerErrorInstallationFailed: 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: 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: case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); 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.", @""); return NSLocalizedString(@"AltServer does not support this request.", @"");
case ALTServerErrorUnknownResponse: case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"Received an unknown response from AltServer.", @""); return NSLocalizedString(@"AltStore received an unknown response from AltServer.", @"");
case ALTServerErrorInvalidAnisetteData: case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"The provided anisette data is invalid.", @""); return NSLocalizedString(@"The provided anisette data is invalid.", @"");
@@ -141,7 +221,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
case ALTServerErrorIncompatibleDeveloperDisk: case ALTServerErrorIncompatibleDeveloperDisk:
{ {
NSString *osVersion = [self altserver_osVersion] ?: NSLocalizedString(@"this device's OS version", @""); 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; return failureReason;
} }
} }
@@ -153,7 +233,24 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
{ {
switch ((ALTServerError)self.code) switch ((ALTServerError)self.code)
{ {
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.localizedRecoverySuggestion;
}
case ALTServerErrorConnectionFailed: 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: case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @""); 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) switch ((ALTServerError)self.code)
{ {
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
return underlyingError.alt_localizedDebugDescription;
}
case ALTServerErrorIncompatibleDeveloperDisk: case ALTServerErrorIncompatibleDeveloperDisk:
{ {
NSString *path = self.userInfo[NSFilePathErrorKey]; 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 *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; return debugDescription;
} }
@@ -232,7 +335,7 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
#pragma mark - AltServerConnectionErrorDomain - #pragma mark - AltServerConnectionErrorDomain -
- (nullable NSString *)altserver_connection_localizedDescription - (nullable NSString *)altserver_connection_localizedFailureReason
{ {
switch ((ALTServerConnectionError)self.code) switch ((ALTServerConnectionError)self.code)
{ {

View File

@@ -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<Code>: 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<Self>
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<Code: ALTErrorEnum>: 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 ~<Code: ALTErrorCode>(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 ~=<Error: ALTLocalizedError>(pattern: Error, value: Swift.Error) -> Bool
//{
// let isMatch = pattern._domain == value._domain && pattern._code == value._code
// return isMatch
//}
//
//public func ~=<Code: ALTErrorCode>(pattern: Code, value: Swift.Error) -> Bool
//{
// let isMatch = Code.errorDomain == value._domain && pattern.rawValue == value._code
// return isMatch
//}

View File

@@ -0,0 +1,25 @@
//
// ALTWrappedError.h
// AltStoreCore
//
// Created by Riley Testut on 11/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
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<NSString *, id> *)userInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -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<NSString *,id> *)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

View File

@@ -21,12 +21,10 @@ public extension ALTServerError
case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error) case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error)
case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error) case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
case let error as NSError: 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 var userInfo = error.userInfo
if !userInfo.keys.contains(NSUnderlyingErrorKey) userInfo[NSUnderlyingErrorKey] = error
{
// Assign underlying error (if there isn't already one).
userInfo[NSUnderlyingErrorKey] = error
}
self = ALTServerError(.underlyingError, userInfo: userInfo) self = ALTServerError(.underlyingError, userInfo: userInfo)
} }

View File

@@ -8,7 +8,17 @@
import Foundation 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) @objc(alt_localizedFailure)
var localizedFailure: String? { var localizedFailure: String? {
@@ -22,50 +32,56 @@ extension NSError
return debugDescription return debugDescription
} }
@objc(alt_localizedTitle)
var localizedTitle: String? {
let localizedTitle = self.userInfo[ALTLocalizedTitleErrorKey] as? String
return localizedTitle
}
@objc(alt_errorWithLocalizedFailure:) @objc(alt_errorWithLocalizedFailure:)
func withLocalizedFailure(_ failure: String) -> NSError func withLocalizedFailure(_ failure: String) -> NSError
{ {
var userInfo = self.userInfo switch self
userInfo[NSLocalizedFailureErrorKey] = failure
if let failureReason = self.localizedFailureReason
{ {
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 var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
userInfo[NSDebugDescriptionErrorKey] = self.localizedDebugDescription
// Remove userInfo values that don't conform to NSSecureEncoding. // Remove userInfo values that don't conform to NSSecureEncoding.
userInfo = userInfo.filter { (key, value) in userInfo = userInfo.filter { (key, value) in
@@ -75,22 +91,160 @@ extension NSError
// Sanitize underlying errors. // Sanitize underlying errors.
if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error if let underlyingError = userInfo[NSUnderlyingErrorKey] as? Error
{ {
let sanitizedError = (underlyingError as NSError).sanitizedForCoreData() let sanitizedError = (underlyingError as NSError).sanitizedForSerialization()
userInfo[NSUnderlyingErrorKey] = sanitizedError userInfo[NSUnderlyingErrorKey] = sanitizedError
} }
if #available(iOS 14.5, macOS 11.3, *), let underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey] as? [Error] 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 userInfo[NSMultipleUnderlyingErrorsKey] = sanitizedErrors
} }
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo) let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error 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? { var underlyingError: Error? {
let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error let underlyingError = (self as NSError).userInfo[NSUnderlyingErrorKey] as? Error
@@ -98,46 +252,22 @@ extension Error
} }
var localizedErrorCode: String { 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 return localizedErrorCode
} }
}
protocol ALTLocalizedError: LocalizedError, CustomNSError
{
var failure: String? { get }
var underlyingError: Error? { get } var displayCode: Int {
} guard let serverError = self as? ALTServerError else {
// Not ALTServerError, so display regular code.
extension ALTLocalizedError return (self as NSError).code
{ }
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 }
let errorDescription = errorFailure + " " + failureReason // We want ALTServerError codes to start at 2000,
return errorDescription // 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 }
} }

View File

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

View File

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

View File

@@ -197,20 +197,21 @@ public enum ServerResponse: Decodable
// from easily changing response format for a request in the future. // from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol public struct ErrorResponse: ServerMessageProtocol
{ {
public var version = 2 public var version = 3
public var identifier = "ErrorResponse" public var identifier = "ErrorResponse"
public var error: ALTServerError { 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) // Legacy (v1)
private var errorCode: ALTServerError.Code private var errorCode: ALTServerError.Code
public init(error: ALTServerError) public init(error: ALTServerError)
{ {
self.serverError = CodableServerError(error: error) self.serverError = CodableError(error: error)
self.errorCode = error.code self.errorCode = error.code
} }
} }