From 7f9ee81150a85f48df2a771946291dbf84cb2e3b Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 26 May 2023 19:12:13 -0500 Subject: [PATCH] Refactors app version comparison logic to always include buildVersion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before, whether or not the source included the buildVersion affected the comparison. If present, the buildVersion was used in comparison, if not, only the version itself was used for comparsion. This meant it was impossible to update from a version with a buildVersion to the same version without one (e.g. going from betas to final releases). Now we _always_ consider the buildVersion in the comparsion, so an earlier entry in versions array without buildVersion can be considered “newer” even if versions match. --- AltStore/Operations/InstallAppOperation.swift | 11 ++++-- .../AltStore 12.xcdatamodel/contents | 1 + AltStoreCore/Model/DatabaseManager.swift | 9 +++-- AltStoreCore/Model/InstalledApp.swift | 34 +++++++++++-------- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 59e452c8..d2832847 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -47,6 +47,9 @@ final class InstallAppOperation: ResultOperation return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil"))) } + @Managed var appVersion = self.context.appVersion + let storeBuildVersion = $appVersion.buildVersion + let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() backgroundContext.perform { @@ -61,10 +64,14 @@ final class InstallAppOperation: ResultOperation } else { - installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext) + installedApp = InstalledApp(resignedApp: resignedApp, + originalBundleIdentifier: self.context.bundleIdentifier, + certificateSerialNumber: certificate.serialNumber, + storeBuildVersion: storeBuildVersion, + context: backgroundContext) } - installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber) + installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion) installedApp.needsResign = false if let team = DatabaseManager.shared.activeTeam(in: backgroundContext) diff --git a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents index 37c2632f..c80ebb34 100644 --- a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents +++ b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents @@ -76,6 +76,7 @@ + diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift index 26c18c14..6ec9a7f2 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -229,7 +229,7 @@ private extension DatabaseManager } else { - storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: localApp.buildVersion, in: context) + storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: nil, in: context) storeApp.source = altStoreSource } @@ -242,7 +242,10 @@ private extension DatabaseManager } else { - installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context) + //TODO: Support build versions. + // For backwards compatibility reasons, we cannot use localApp's buildVersion as storeBuildVersion, + // or else the latest update will _always_ be considered new because we don't use buildVersions in our source (yet). + installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, storeBuildVersion: nil, context: context) installedApp.storeApp = storeApp } @@ -322,7 +325,7 @@ private extension DatabaseManager let cachedExpirationDate = installedApp.expirationDate // Must go after comparing versions to see if we need to update our cached AltStore app bundle. - installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber) + installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber, storeBuildVersion: nil) if installedApp.refreshedDate < cachedRefreshedDate { diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index 9ac627d2..871b4c14 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -60,6 +60,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol @NSManaged public var hasAlternateIcon: Bool @NSManaged public var certificateSerialNumber: String? + @NSManaged public var storeBuildVersion: String? /* Transient */ @NSManaged public var isRefreshing: Bool @@ -104,7 +105,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol super.init(entity: entity, insertInto: context) } - public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext) + public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, storeBuildVersion: String?, context: NSManagedObjectContext) { super.init(entity: InstalledApp.entity(), insertInto: context) @@ -117,24 +118,30 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. - self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber) + // In practice this update() is redundant because we always call update() again after init from callers, + // but better to have an init that is guaranteed to successfully initialize an object + // than one that has a hidden assumption a second method will be called. + self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber, storeBuildVersion: storeBuildVersion) } } public extension InstalledApp { var localizedVersion: String { - let localizedVersion = "\(self.version) (\(self.buildVersion))" + guard let storeBuildVersion else { return self.version } + + let localizedVersion = "\(self.version) (\(storeBuildVersion))" return localizedVersion } - func update(resignedApp: ALTApplication, certificateSerialNumber: String?) + func update(resignedApp: ALTApplication, certificateSerialNumber: String?, storeBuildVersion: String?) { self.name = resignedApp.name self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.version = resignedApp.version self.buildVersion = resignedApp.buildVersion + self.storeBuildVersion = storeBuildVersion self.certificateSerialNumber = certificateSerialNumber @@ -178,14 +185,7 @@ public extension InstalledApp func matches(_ appVersion: AppVersion) -> Bool { - // Versions MUST match exactly. - guard self.version == appVersion.version else { return false } - - // If buildVersion is nil, return true because versions match. - guard let buildVersion = appVersion.buildVersion else { return true } - - // If buildVersion != nil, compare buildVersions and return result. - let matchesAppVersion = (self.buildVersion == buildVersion) + let matchesAppVersion = (self.version == appVersion.version && self.storeBuildVersion == appVersion.buildVersion) return matchesAppVersion } } @@ -207,14 +207,18 @@ public extension InstalledApp "AND", - // (latestSupportedVersion.version != installedApp.version || (latestSupportedVersion.buildVersion != nil && latestSupportedVersion.buildVersion != installedApp.buildVersion)) - "(%K != %K OR (%K != '' AND %K != %K))", + // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion + // + // We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil) + // because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL. + "(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))", ].joined(separator: " ") fetchRequest.predicate = NSPredicate(format: predicateFormat, #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version), - #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.buildVersion)) + #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion), + #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion)) return fetchRequest }