diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 548e3a47..3c814fe9 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -199,8 +199,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 44e8009a..c3f37399 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 3a679935..ce00ec50 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -162,14 +162,12 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions) { - //TODO: Throw error if there isn't at least one version. - for version in versions { version.appBundleID = self.bundleIdentifier } - self.setVersions(versions) + try self.setVersions(versions) } else { @@ -187,7 +185,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable size: Int64(size), appBundleID: self.bundleIdentifier, in: context) - self.setVersions([appVersion]) + try self.setVersions([appVersion]) } } catch @@ -204,8 +202,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 }) @@ -225,7 +227,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 @@ -263,7 +264,7 @@ public extension StoreApp appBundleID: app.bundleIdentifier, sourceID: Source.altStoreIdentifier, in: context) - app.setVersions([appVersion]) + try? app.setVersions([appVersion]) #if BETA app.isBeta = true