Moves SourceError to its own source file

This commit is contained in:
Riley Testut
2023-05-15 15:38:54 -05:00
parent 631109b54b
commit 0255bd7b1c
5 changed files with 136 additions and 115 deletions

View File

@@ -0,0 +1,184 @@
//
// OperationError.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltStoreCore
extension OperationError
{
enum Code: Int, ALTErrorCode, CaseIterable
{
typealias Error = OperationError
/* General */
case unknown = 1000
case unknownResult = 1001
// case cancelled = 1002
case timedOut = 1003
case notAuthenticated = 1004
case appNotFound = 1005
case unknownUDID = 1006
case invalidApp = 1007
case invalidParameters = 1008
case maximumAppIDLimitReached = 1009
case noSources = 1010
case openAppFailed = 1011
case missingAppGroup = 1012
case forbidden = 1013
/* Connection */
case serverNotFound = 1200
case connectionFailed = 1201
case connectionDropped = 1202
}
static var cancelled: CancellationError { CancellationError() }
static let unknownResult: OperationError = .init(code: .unknownResult)
static let timedOut: OperationError = .init(code: .timedOut)
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
static let unknownUDID: OperationError = .init(code: .unknownUDID)
static let invalidApp: OperationError = .init(code: .invalidApp)
static let invalidParameters: OperationError = .init(code: .invalidParameters)
static let noSources: OperationError = .init(code: .noSources)
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
static let serverNotFound: OperationError = .init(code: .serverNotFound)
static let connectionFailed: OperationError = .init(code: .connectionFailed)
static let connectionDropped: OperationError = .init(code: .connectionDropped)
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
static func appNotFound(name: String?) -> OperationError { OperationError(code: .appNotFound, appName: name) }
static func openAppFailed(name: String) -> OperationError { OperationError(code: .openAppFailed, appName: name) }
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
}
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
}
struct OperationError: ALTLocalizedError
{
let code: Code
var errorTitle: String?
var errorFailure: String?
var appName: String?
var requiredAppIDs: Int?
var availableAppIDs: Int?
var expirationDate: Date?
var sourceFile: String?
var sourceLine: UInt?
private init(code: Code, failureReason: String? = nil, appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil, expirationDate: Date? = nil,
sourceFile: String? = nil, sourceLine: UInt? = nil)
{
self.code = code
self._failureReason = failureReason
self.appName = appName
self.requiredAppIDs = requiredAppIDs
self.availableAppIDs = availableAppIDs
self.expirationDate = expirationDate
self.sourceFile = sourceFile
self.sourceLine = sourceLine
}
var errorFailureReason: String {
switch self.code
{
case .unknown:
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occured.", comment: "")
guard let sourceFile, let sourceLine else { return failureReason }
failureReason += " (\(sourceFile) line \(sourceLine))"
return failureReason
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .unknownUDID: return NSLocalizedString("AltStore could not determine this device's UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .maximumAppIDLimitReached: return NSLocalizedString("You cannot register more than 10 App IDs within a 7 day period.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be accessed.", comment: "")
case .forbidden:
guard let failureReason = self._failureReason else { return NSLocalizedString("The operation is forbidden.", comment: "") }
return failureReason
case .appNotFound:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
case .openAppFailed:
let appName = self.appName ?? NSLocalizedString("the app", comment: "")
return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), appName)
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
}
}
private var _failureReason: String?
var recoverySuggestion: String? {
switch self.code
{
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: "")
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
{
let availableText: String
switch availableAppIDs
{
case 0: availableText = NSLocalizedString("none are available", comment: "")
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage + "\n\n"
}
else
{
message = baseMessage + " "
}
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message += remainingTimeMessage
return message
default: return nil
}
}
}

View File

@@ -0,0 +1,122 @@
//
// SourceError.swift
// AltStoreCore
//
// Created by Riley Testut on 5/3/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import AltStoreCore
extension SourceError
{
enum Code: Int, ALTErrorCode
{
typealias Error = SourceError
case unsupported
case duplicateBundleID
case duplicateVersion
case changedID
case duplicate
case missingPermissionUsageDescription
}
static func unsupported(_ source: Source) -> SourceError { SourceError(code: .unsupported, source: source) }
static func duplicateBundleID(_ bundleID: String, source: Source) -> SourceError { SourceError(code: .duplicateBundleID, source: source, bundleID: bundleID) }
static func duplicateVersion(_ version: String, for app: StoreApp, source: Source) -> SourceError { SourceError(code: .duplicateVersion, source: source, app: app, version: version) }
static func changedID(_ identifier: String, previousID: String, source: Source) -> SourceError { SourceError(code: .changedID, source: source, sourceID: identifier, previousSourceID: previousID) }
static func duplicate(_ source: Source, previousSourceName: String?) -> SourceError { SourceError(code: .duplicate, source: source, previousSourceName: previousSourceName) }
static func missingPermissionUsageDescription(for permission: any ALTAppPermission, app: StoreApp, source: Source) -> SourceError {
SourceError(code: .missingPermissionUsageDescription, source: source, app: app, permission: permission)
}
}
struct SourceError: ALTLocalizedError
{
let code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var source: Source
@Managed var app: StoreApp?
var bundleID: String?
var version: String?
@UserInfoValue var previousSourceName: String?
// Store in userInfo so they can be viewed from Error Log.
@UserInfoValue var sourceID: String?
@UserInfoValue var previousSourceID: String?
@UserInfoValue
var permission: (any ALTAppPermission)?
var errorFailureReason: String {
switch self.code
{
case .unsupported: return String(format: NSLocalizedString("The source “%@” is not supported by this version of AltStore.", comment: ""), self.$source.name)
case .duplicateBundleID:
let bundleIDFragment = self.bundleID.map { String(format: NSLocalizedString("the bundle identifier %@", comment: ""), $0) } ?? NSLocalizedString("the same bundle identifier", comment: "")
let failureReason = String(format: NSLocalizedString("The source “%@” contains multiple apps with %@.", comment: ""), self.$source.name, bundleIDFragment)
return failureReason
case .duplicateVersion:
var versionFragment = NSLocalizedString("duplicate versions", comment: "")
if let version
{
versionFragment += " (\(version))"
}
let appFragment: String
if let name = self.$app.name, let bundleID = self.$app.bundleIdentifier
{
appFragment = name + " (\(bundleID))"
}
else
{
appFragment = NSLocalizedString("one or more apps", comment: "")
}
let failureReason = String(format: NSLocalizedString("The source “%@” contains %@ for %@.", comment: ""), self.$source.name, versionFragment, appFragment)
return failureReason
case .changedID:
let failureReason = String(format: NSLocalizedString("The identifier of the source “%@” has changed.", comment: ""), self.$source.name)
return failureReason
case .duplicate:
let baseMessage = String(format: NSLocalizedString("A source with the identifier '%@' already exists", comment: ""), self.$source.identifier)
guard let previousSourceName else { return baseMessage + "." }
let failureReason = baseMessage + " (“\(previousSourceName)”)."
return failureReason
case .missingPermissionUsageDescription:
let appName = self.$app.name ?? String(format: NSLocalizedString("an app in source “%@”", comment: ""), self.$source.name)
guard let permission else {
return String(format: NSLocalizedString("A permission for %@ is missing a usage description.", comment: ""), appName)
}
let permissionType = permission.type.localizedName ?? NSLocalizedString("Permission", comment: "")
let failureReason = String(format: NSLocalizedString("The %@ '%@' for %@ is missing a usage description.", comment: ""), permissionType.lowercased(), permission.rawValue, appName)
return failureReason
}
}
var recoverySuggestion: String? {
switch self.code
{
case .changedID: return NSLocalizedString("A source cannot change its identifier once added. This source can no longer be updated.", comment: "")
case .duplicate:
let failureReason = NSLocalizedString("Please remove the existing source in order to add this one.", comment: "")
return failureReason
default: return nil
}
}
}

View File

@@ -0,0 +1,198 @@
//
// VerificationError.swift
// AltStore
//
// Created by Riley Testut on 5/11/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import AltStoreCore
import AltSign
extension VerificationError
{
enum Code: Int, ALTErrorCode, CaseIterable
{
typealias Error = VerificationError
// Legacy
// case privateEntitlements = 0
case mismatchedBundleIdentifiers = 1
case iOSVersionNotSupported = 2
case mismatchedHash = 3
case mismatchedVersion = 4
case undeclaredPermissions = 6
case addedPermissions = 7
}
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
}
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
VerificationError(code: .iOSVersionNotSupported, app: app, deviceOSVersion: osVersion, requiredOSVersion: requiredOSVersion)
}
static func mismatchedHash(_ hash: String, expectedHash: String, app: AppProtocol) -> VerificationError {
VerificationError(code: .mismatchedHash, app: app, hash: hash, expectedHash: expectedHash)
}
static func mismatchedVersion(_ version: String, expectedVersion: String, app: AppProtocol) -> VerificationError {
VerificationError(code: .mismatchedVersion, app: app, version: version, expectedVersion: expectedVersion)
}
static func undeclaredPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError {
VerificationError(code: .undeclaredPermissions, app: app, permissions: permissions)
}
static func addedPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError {
VerificationError(code: .addedPermissions, app: app, permissions: permissions)
}
}
struct VerificationError: ALTLocalizedError
{
let code: Code
var errorTitle: String?
var errorFailure: String?
@Managed var app: AppProtocol?
var sourceBundleID: String?
var deviceOSVersion: OperatingSystemVersion?
var requiredOSVersion: OperatingSystemVersion?
@UserInfoValue var hash: String?
@UserInfoValue var expectedHash: String?
@UserInfoValue var version: String?
@UserInfoValue var expectedVersion: String?
@UserInfoValue
var permissions: [any ALTAppPermission]?
var errorDescription: String? {
//TODO: Make this automatic somehow with ALTLocalizedError
guard self.errorFailure == nil else { return nil }
switch self.code
{
case .iOSVersionNotSupported:
guard let deviceOSVersion else { break }
var failureReason = self.errorFailureReason
if self.app == nil
{
// failureReason does not start with app name, so make first letter lowercase.
let firstLetter = failureReason.prefix(1).lowercased()
failureReason = firstLetter + failureReason.dropFirst()
}
let localizedDescription = String(format: NSLocalizedString("This device is running iOS %@, but %@", comment: ""), deviceOSVersion.stringValue, failureReason)
return localizedDescription
default: break
}
return self.errorFailureReason
}
var errorFailureReason: String {
switch self.code
{
case .mismatchedBundleIdentifiers:
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID
{
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), appBundleID, bundleID)
}
else
{
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
}
case .iOSVersionNotSupported:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
guard let requiredOSVersion else {
return String(format: NSLocalizedString("%@ does not support iOS %@.", comment: ""), appName, deviceOSVersion.stringValue)
}
if deviceOSVersion > requiredOSVersion
{
// Device OS version is higher than maximum supported OS version.
let failureReason = String(format: NSLocalizedString("%@ requires iOS %@ or earlier.", comment: ""), appName, requiredOSVersion.stringValue)
return failureReason
}
else
{
// Device OS version is lower than minimum supported OS version.
let failureReason = String(format: NSLocalizedString("%@ requires iOS %@ or later.", comment: ""), appName, requiredOSVersion.stringValue)
return failureReason
}
case .mismatchedHash:
let appName = self.$app.name ?? NSLocalizedString("the downloaded app", comment: "")
return String(format: NSLocalizedString("The SHA-256 hash of %@ does not match the hash specified by the source.", comment: ""), appName)
case .mismatchedVersion:
let appName = self.$app.name ?? NSLocalizedString("the app", comment: "")
return String(format: NSLocalizedString("The downloaded version of %@ does not match the version specified by the source.", comment: ""), appName)
case .undeclaredPermissions:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ requires additional permissions not specified by the source.", comment: ""), appName)
case .addedPermissions:
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("%@ requires more permissions than the version that is already installed.", comment: ""), appName)
}
}
var recoverySuggestion: String? {
switch self.code
{
case .undeclaredPermissions:
guard let permissions, !permissions.isEmpty else { return nil }
let baseMessage = NSLocalizedString("These permissions must be declared by the source in order for AltStore to install this app:", comment: "")
let permissionsByType = Dictionary(grouping: permissions) { $0.type }
let permissionSections = [ALTAppPermissionType.entitlement, .privacy, .backgroundMode].compactMap { (type) -> String? in
guard let permissions = permissionsByType[type] else { return nil }
// "Privacy:"
var sectionText = "\(type.localizedName ?? type.rawValue):\n"
// Sort permissions + join into single string.
let sortedList = permissions.map { permission -> String in
if let localizedName = permission.localizedName
{
// "Entitlement Name (com.apple.entitlement.name)"
return "\(localizedName) (\(permission.rawValue))"
}
else
{
// "com.apple.entitlement.name"
return permission.rawValue
}
}
.sorted { $0.localizedStandardCompare($1) == .orderedAscending } // Case-insensitive sorting
.joined(separator: "\n")
sectionText += sortedList
return sectionText
}
let recoverySuggestion = ([baseMessage] + permissionSections).joined(separator: "\n\n")
return recoverySuggestion
default: return nil
}
}
}