diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 1addee55..b308a9ee 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -193,8 +193,13 @@ private extension BrowseViewController throw error } } - catch + catch var error as NSError { + if error.localizedTitle == nil + { + error = error.withLocalizedTitle(NSLocalizedString("Unable to Refresh Store", comment: "")) + } + DispatchQueue.main.async { if self.dataSource.itemCount > 0 { diff --git a/AltStore/News/NewsViewController.swift b/AltStore/News/NewsViewController.swift index c6b55cb6..82af2d8c 100644 --- a/AltStore/News/NewsViewController.swift +++ b/AltStore/News/NewsViewController.swift @@ -210,8 +210,13 @@ private extension NewsViewController throw error } } - catch + catch var error as NSError { + if error.localizedTitle == nil + { + error = error.withLocalizedTitle(NSLocalizedString("Unable to Refresh Store", comment: "")) + } + DispatchQueue.main.async { if self.dataSource.itemCount > 0 { diff --git a/AltStoreCore/Model/MergePolicy.swift b/AltStoreCore/Model/MergePolicy.swift index 56265e7e..679285a0 100644 --- a/AltStoreCore/Model/MergePolicy.swift +++ b/AltStoreCore/Model/MergePolicy.swift @@ -10,6 +10,60 @@ import CoreData import Roxas +extension MergeError +{ + enum Code: Int, ALTErrorCode + { + typealias Error = MergeError + + case noVersions + } + + static func noVersions(for app: AppProtocol) -> MergeError { .init(code: .noVersions, appName: app.name, appBundleID: app.bundleIdentifier) } +} + +struct MergeError: ALTLocalizedError +{ + static var errorDomain: String { "AltStore.MergeError" } + + let code: Code + var errorTitle: String? + var errorFailure: String? + + var appName: String? + var appBundleID: String? + + var errorFailureReason: String { + switch self.code + { + case .noVersions: + var appName = NSLocalizedString("At least one app", comment: "") + if let name = self.appName, let bundleID = self.appBundleID + { + appName = name + " (\(bundleID))" + } + + return String(format: NSLocalizedString("%@ does not have any app versions.", comment: ""), appName) + } + } +} + +private extension Error +{ + func serialized(withFailure failure: String) -> NSError + { + // We need to serialize Swift errors thrown during merge conflict to preserve error messages. + + let serializedError = (self as NSError).withLocalizedFailure(failure).sanitizedForSerialization() + + var userInfo = serializedError.userInfo + userInfo[NSLocalizedDescriptionKey] = nil // Remove NSLocalizedDescriptionKey value to prevent duplicating localized failure in localized description. + + let error = NSError(domain: serializedError.domain, code: serializedError.code, userInfo: userInfo) + return error + } +} + open class MergePolicy: RSTRelationshipPreservingMergePolicy { open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws @@ -108,7 +162,15 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy return indexA < indexB } - databaseObject.setVersions(sortedVersions) + do + { + try databaseObject.setVersions(sortedVersions) + } + catch + { + let nsError = error.serialized(withFailure: NSLocalizedString("AltStore's database could not be saved.", comment: "")) + throw nsError + } } case let databaseObject as Source: @@ -146,8 +208,16 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy switch conflict.databaseObject { case let databaseObject as StoreApp: - // Update versions post-merging to make sure latestSupportedVersion is correct. - databaseObject.setVersions(databaseObject.versions) + do + { + // Update versions post-merging to make sure latestSupportedVersion is correct. + try databaseObject.setVersions(databaseObject.versions) + } + catch + { + let nsError = error.serialized(withFailure: NSLocalizedString("AltStore's database could not be saved.", comment: "")) + throw nsError + } default: break } diff --git a/AltStoreCore/Model/StoreApp.swift b/AltStoreCore/Model/StoreApp.swift index 6934cb13..0116a338 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -267,12 +267,12 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable } else { throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.") } - + // else { // throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.") // } } - + if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor) { guard let tintColor = UIColor(hexString: tintColorHex) else { @@ -293,13 +293,13 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable if (versions.count == 0){ throw DecodingError.dataCorruptedError(forKey: .versions, in: container, debugDescription: "At least one version is required in key: versions") } - + for version in versions { version.appBundleID = self.bundleIdentifier } - self.setVersions(versions) + try self.setVersions(versions) } else { @@ -317,7 +317,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable size: Int64(size), appBundleID: self.bundleIdentifier, in: context) - self.setVersions([appVersion]) + try self.setVersions([appVersion]) } } catch @@ -334,8 +334,12 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable internal extension StoreApp { - func setVersions(_ versions: [AppVersion]) + func setVersions(_ versions: [AppVersion]) throws { + guard let latestVersion = versions.first else { + throw MergeError.noVersions(for: self) + } + self._versions = NSOrderedSet(array: versions) let latestSupportedVersion = versions.first(where: { $0.isSupported }) @@ -355,7 +359,6 @@ internal extension StoreApp } // Preserve backwards compatibility by assigning legacy property values. - guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") } self.latestVersionString = latestVersion.version self._versionDate = latestVersion.date self._versionDescription = latestVersion.localizedDescription @@ -393,7 +396,7 @@ public extension StoreApp appBundleID: app.bundleIdentifier, sourceID: Source.altStoreIdentifier, in: context) - app.setVersions([appVersion]) + try? app.setVersions([appVersion]) print("makeAltStoreApp StoreApp: \(String(describing: app))")