From 2017584da470c54abcb48a353aed64d4e216b6b1 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 ++- .../Operations/FetchSourceOperation.swift | 57 +++++++++++++++++++ AltStore/Sources/SourcesViewController.swift | 18 +++--- 3 files changed, 73 insertions(+), 9 deletions(-) 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) }