mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Moves SourceError to its own source file
This commit is contained in:
319
AltStore/Operations/Errors/OperationError.swift
Normal file
319
AltStore/Operations/Errors/OperationError.swift
Normal file
@@ -0,0 +1,319 @@
|
||||
//
|
||||
// OperationError.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
extension OperationError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = OperationError
|
||||
|
||||
// General
|
||||
case unknown = 1000
|
||||
case unknownResult = 1001
|
||||
// case cancelled = 1002
|
||||
case timedOut = 1003
|
||||
case unableToConnectSideJIT
|
||||
case unableToRespondSideJITDevice
|
||||
case wrongSideJITIP
|
||||
case SideJITIssue // (error: String)
|
||||
case refreshsidejit
|
||||
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 refreshAppFailed
|
||||
|
||||
// Connection
|
||||
case noWiFi = 1200
|
||||
case tooNewError
|
||||
case anisetteV1Error//(message: String)
|
||||
case provisioningError//(result: String, message: String?)
|
||||
case anisetteV3Error//(message: String)
|
||||
|
||||
case cacheClearError//(errors: [String])
|
||||
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 unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
|
||||
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
|
||||
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
|
||||
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
|
||||
static let unknownUDID: OperationError = .init(code: .unknownUDID)
|
||||
static let invalidApp: OperationError = .init(code: .invalidApp)
|
||||
static let noSources: OperationError = .init(code: .noSources)
|
||||
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
||||
|
||||
static let noWiFi: OperationError = .init(code: .noWiFi)
|
||||
static let tooNewError: OperationError = .init(code: .tooNewError)
|
||||
static let provisioningError: OperationError = .init(code: .provisioningError)
|
||||
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
||||
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
||||
|
||||
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
||||
|
||||
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 let domain = OperationError(code: .unknown)._domain
|
||||
|
||||
static func SideJITIssue(error: String?) -> OperationError {
|
||||
var o = OperationError(code: .SideJITIssue)
|
||||
o.errorFailure = error
|
||||
return o
|
||||
}
|
||||
|
||||
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 provisioningError(result: String, message: String?) -> OperationError {
|
||||
var o = OperationError(code: .provisioningError, failureReason: result)
|
||||
o.errorTitle = message
|
||||
return o
|
||||
}
|
||||
|
||||
static func cacheClearError(errors: [String]) -> OperationError {
|
||||
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
static func anisetteV1Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV1Error, failureReason: message)
|
||||
}
|
||||
|
||||
static func anisetteV3Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV3Error, failureReason: message)
|
||||
}
|
||||
|
||||
static func refreshAppFailed(message: String) -> OperationError {
|
||||
OperationError(code: .refreshAppFailed, failureReason: message)
|
||||
}
|
||||
|
||||
static func invalidParameters(_ message: String? = nil) -> OperationError {
|
||||
OperationError(code: .invalidParameters, failureReason: message)
|
||||
|
||||
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 var _failureReason: String?
|
||||
|
||||
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 occurred.", 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("SideStore could not determine this device's UDID.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
|
||||
case .invalidParameters: return NSLocalizedString("Invalid parameters.", 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("SideStore was denied permission to launch %@.", comment: ""), appName)
|
||||
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
|
||||
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
||||
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
||||
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
|
||||
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
||||
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
||||
|
||||
case .refreshAppFailed:
|
||||
let message = self._failureReason ?? ""
|
||||
return String(format: NSLocalizedString("Unable to refresh App\n%@", comment: ""), message)
|
||||
|
||||
case .invalidParameters:
|
||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
||||
}
|
||||
|
||||
}
|
||||
private var _failureReason: String?
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self.code
|
||||
{
|
||||
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
|
||||
case .maximumAppIDLimitReached:
|
||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||
guard let appName, let requiredAppIDs, let availableAppIDs, let 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: expirationDate)
|
||||
let dateFormatter = DateComponentsFormatter()
|
||||
dateFormatter.maximumUnitCount = 1
|
||||
dateFormatter.unitsStyle = .full
|
||||
|
||||
let remainingTime = dateFormatter.string(from: dateComponents)!
|
||||
|
||||
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||
|
||||
return message
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MinimuxerError: LocalizedError {
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .NoDevice:
|
||||
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
|
||||
case .NoConnection:
|
||||
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
|
||||
case .PairingFile:
|
||||
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||
|
||||
case .CreateDebug:
|
||||
return self.createService(name: "debug")
|
||||
case .LookupApps:
|
||||
return self.getFromDevice(name: "installed apps")
|
||||
case .FindApp:
|
||||
return self.getFromDevice(name: "path to the app")
|
||||
case .BundlePath:
|
||||
return self.getFromDevice(name: "bundle path")
|
||||
case .MaxPacket:
|
||||
return self.setArgument(name: "max packet")
|
||||
case .WorkingDirectory:
|
||||
return self.setArgument(name: "working directory")
|
||||
case .Argv:
|
||||
return self.setArgument(name: "argv")
|
||||
case .LaunchSuccess:
|
||||
return self.getFromDevice(name: "launch success")
|
||||
case .Detach:
|
||||
return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .Attach:
|
||||
return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||
|
||||
case .CreateInstproxy:
|
||||
return self.createService(name: "instproxy")
|
||||
case .CreateAfc:
|
||||
return self.createService(name: "AFC")
|
||||
case .RwAfc:
|
||||
return NSLocalizedString("AFC was unable to manage files on the device. This usually means an invalid pairing.", comment: "")
|
||||
case .InstallApp(let message):
|
||||
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "")
|
||||
case .UninstallApp:
|
||||
return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
|
||||
case .CreateMisagent:
|
||||
return self.createService(name: "misagent")
|
||||
case .ProfileInstall:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .ProfileRemove:
|
||||
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createService(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func getFromDevice(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
|
||||
}
|
||||
|
||||
fileprivate func setArgument(name: String) -> String {
|
||||
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
|
||||
}
|
||||
}
|
||||
122
AltStore/Operations/Errors/SourceError.swift
Normal file
122
AltStore/Operations/Errors/SourceError.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
198
AltStore/Operations/Errors/VerificationError.swift
Normal file
198
AltStore/Operations/Errors/VerificationError.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user