From 96dec5329d8938111115d42e95108c41620ff252 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 22 Nov 2022 13:02:19 -0600 Subject: [PATCH] =?UTF-8?q?Verifies=20Sources=20don=E2=80=99t=20contain=20?= =?UTF-8?q?duplicate=20bundle=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AltStore assumes all apps have unique bundle IDs per source. Weird bugs can occur when this is not the case (such as merging multiple store listings into one), so we now verify upfront whether source contains duplicate bundle IDs before saving. --- AltStore/Managing Apps/AppManager.swift | 7 ++- AltStore/Managing Apps/AppManagerErrors.swift | 6 +- .../Operations/FetchSourceOperation.swift | 57 +++++++++++++++++++ AltStore/Sources/SourcesViewController.swift | 41 ++++--------- 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 18f13fcf..f1417aae 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -394,10 +394,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/Managing Apps/AppManagerErrors.swift b/AltStore/Managing Apps/AppManagerErrors.swift index 3745b0ca..0d085ce4 100644 --- a/AltStore/Managing Apps/AppManagerErrors.swift +++ b/AltStore/Managing Apps/AppManagerErrors.swift @@ -27,16 +27,16 @@ extension AppManager self.managedObjectContext?.performAndWait { if self.sources?.count == 1 { - localizedTitle = NSLocalizedString("Failed to Refresh Store", comment: "") + localizedTitle = NSLocalizedString("Unable to Refresh Store", comment: "") } else if self.errors.count == 1 { guard let source = self.errors.keys.first else { return } - localizedTitle = String(format: NSLocalizedString("Failed to Refresh Source “%@”", comment: ""), source.name) + localizedTitle = String(format: NSLocalizedString("Unable to Refresh “%@” Source", comment: ""), source.name) } else { - localizedTitle = String(format: NSLocalizedString("Failed to Refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count)) + localizedTitle = String(format: NSLocalizedString("Unable to Refresh %@ Sources", comment: ""), NSNumber(value: self.errors.count)) } } diff --git a/AltStore/Operations/FetchSourceOperation.swift b/AltStore/Operations/FetchSourceOperation.swift index 9492503d..ccbeb953 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) class FetchSourceOperation: ResultOperation { @@ -78,6 +113,8 @@ 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 @@ 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 34fb8745..584995c5 100644 --- a/AltStore/Sources/SourcesViewController.swift +++ b/AltStore/Sources/SourcesViewController.swift @@ -12,29 +12,6 @@ import CoreData import AltStoreCore import Roxas -struct SourceError: ALTLocalizedError -{ - enum Code: Int, ALTErrorCode - { - typealias Error = SourceError - - case unsupported - } - - var code: Code - var errorTitle: String? - var errorFailure: String? - - @Managed var source: Source - - 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) - } - } -} - @objc(SourcesFooterView) private class SourcesFooterView: TextCollectionReusableView { @@ -241,7 +218,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]) @@ -265,10 +249,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: "") @@ -313,9 +293,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) }