diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 191857d8..7c66b4a7 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -415,10 +415,13 @@ extension AppManager switch result { case .success(let source): fetchedSources.insert(source) - case .failure(let error): + case .failure(let nsError as NSError): let source = managedObjectContext.object(with: source.objectID) as! Source - source.error = (error as NSError).sanitizedForSerialization() + let title = String(format: NSLocalizedString("Unable to Refresh “%@” Source", comment: ""), source.name) + + let error = nsError.withLocalizedTitle(title) errors[source] = error + source.error = error.sanitizedForSerialization() } dispatchGroup.leave() diff --git a/AltStore/Operations/FetchSourceOperation.swift b/AltStore/Operations/FetchSourceOperation.swift index 443cd037..49611ac1 100644 --- a/AltStore/Operations/FetchSourceOperation.swift +++ b/AltStore/Operations/FetchSourceOperation.swift @@ -12,6 +12,41 @@ import CoreData import AltStoreCore import Roxas +extension SourceError +{ + enum Code: Int, ALTErrorCode + { + typealias Error = SourceError + + case unsupported + case duplicateBundleID + } + + static func unsupported(_ source: Source) -> SourceError { SourceError(code: .unsupported, source: source) } + static func duplicateBundleID(_ bundleID: String, source: Source) -> SourceError { SourceError(code: .duplicateBundleID, source: source, duplicateBundleID: bundleID) } +} + +struct SourceError: ALTLocalizedError +{ + var code: Code + var errorTitle: String? + var errorFailure: String? + + @Managed var source: Source + var duplicateBundleID: String? + + 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.duplicateBundleID.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 + } + } +} + @objc(FetchSourceOperation) final class FetchSourceOperation: ResultOperation { @@ -78,6 +113,8 @@ final class FetchSourceOperation: ResultOperation let source = try decoder.decode(Source.self, from: data) let identifier = source.identifier + try self.verify(source) + try childContext.save() self.managedObjectContext.perform { @@ -105,3 +142,23 @@ final class FetchSourceOperation: ResultOperation dataTask.resume() } } + +private extension FetchSourceOperation +{ + func verify(_ source: Source) throws + { + #if !BETA + if let trustedSourceIDs = UserDefaults.shared.trustedSourceIDs + { + guard trustedSourceIDs.contains(source.identifier) || source.identifier == Source.altStoreIdentifier else { throw SourceError(code: .unsupported, source: source) } + } + #endif + + var bundleIDs = Set() + for app in source.apps + { + guard !bundleIDs.contains(app.bundleIdentifier) else { throw SourceError.duplicateBundleID(app.bundleIdentifier, source: source) } + bundleIDs.insert(app.bundleIdentifier) + } + } +} diff --git a/AltStore/Sources/SourcesViewController.swift b/AltStore/Sources/SourcesViewController.swift index 0034f53c..96ff8798 100644 --- a/AltStore/Sources/SourcesViewController.swift +++ b/AltStore/Sources/SourcesViewController.swift @@ -239,7 +239,14 @@ private extension SourcesViewController { case .success: break case .failure(OperationError.cancelled): break - case .failure(let error): self.present(error) + + case .failure(var error as SourceError): + let title = String(format: NSLocalizedString("“%@” could not be added to AltStore.", comment: ""), error.$source.name) + error.errorTitle = title + self.present(error) + + case .failure(let error as NSError): + self.present(error.withLocalizedTitle(NSLocalizedString("Unable to Add Source", comment: ""))) } self.collectionView.reloadSections([Section.trusted.rawValue]) @@ -263,10 +270,6 @@ private extension SourcesViewController let sourceName = source.name let managedObjectContext = source.managedObjectContext - #if !BETA - guard let trustedSourceIDs = UserDefaults.shared.trustedSourceIDs, trustedSourceIDs.contains(source.identifier) else { throw SourceError(code: .unsupported, source: source) } - #endif - // Hide warning when adding a featured trusted source. let message = isTrusted ? nil : NSLocalizedString("Make sure to only add sources that you trust.", comment: "") @@ -311,9 +314,10 @@ private extension SourcesViewController } let nsError = error as NSError - let message = nsError.userInfo[NSDebugDescriptionErrorKey] as? String ?? nsError.localizedRecoverySuggestion + let title = nsError.localizedTitle // OK if nil. + let message = [nsError.localizedDescription, nsError.localizedDebugDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") - let alertController = UIAlertController(title: error.localizedDescription, message: message, preferredStyle: .alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(.ok) self.present(alertController, animated: true, completion: nil) }