Refactors app version comparison logic to always include buildVersion

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.
This commit is contained in:
Riley Testut
2023-05-26 19:12:13 -05:00
committed by Magesh K
parent 641e7d5f2e
commit 7f9ee81150
4 changed files with 35 additions and 20 deletions

View File

@@ -47,6 +47,9 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil"))) 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() let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform { backgroundContext.perform {
@@ -61,10 +64,14 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
} }
else 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 installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext) if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)

View File

@@ -76,6 +76,7 @@
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/> <attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/> <attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
<attribute name="version" attributeType="String"/> <attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/> <relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/> <relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>

View File

@@ -229,7 +229,7 @@ private extension DatabaseManager
} }
else 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 storeApp.source = altStoreSource
} }
@@ -242,7 +242,10 @@ private extension DatabaseManager
} }
else 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 installedApp.storeApp = storeApp
} }
@@ -322,7 +325,7 @@ private extension DatabaseManager
let cachedExpirationDate = installedApp.expirationDate let cachedExpirationDate = installedApp.expirationDate
// Must go after comparing versions to see if we need to update our cached AltStore app bundle. // 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 if installedApp.refreshedDate < cachedRefreshedDate
{ {

View File

@@ -60,6 +60,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@NSManaged public var hasAlternateIcon: Bool @NSManaged public var hasAlternateIcon: Bool
@NSManaged public var certificateSerialNumber: String? @NSManaged public var certificateSerialNumber: String?
@NSManaged public var storeBuildVersion: String?
/* Transient */ /* Transient */
@NSManaged public var isRefreshing: Bool @NSManaged public var isRefreshing: Bool
@@ -104,7 +105,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
super.init(entity: entity, insertInto: context) 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) 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.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 public extension InstalledApp
{ {
var localizedVersion: String { var localizedVersion: String {
let localizedVersion = "\(self.version) (\(self.buildVersion))" guard let storeBuildVersion else { return self.version }
let localizedVersion = "\(self.version) (\(storeBuildVersion))"
return localizedVersion return localizedVersion
} }
func update(resignedApp: ALTApplication, certificateSerialNumber: String?) func update(resignedApp: ALTApplication, certificateSerialNumber: String?, storeBuildVersion: String?)
{ {
self.name = resignedApp.name self.name = resignedApp.name
self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.version = resignedApp.version self.version = resignedApp.version
self.buildVersion = resignedApp.buildVersion self.buildVersion = resignedApp.buildVersion
self.storeBuildVersion = storeBuildVersion
self.certificateSerialNumber = certificateSerialNumber self.certificateSerialNumber = certificateSerialNumber
@@ -178,14 +185,7 @@ public extension InstalledApp
func matches(_ appVersion: AppVersion) -> Bool func matches(_ appVersion: AppVersion) -> Bool
{ {
// Versions MUST match exactly. let matchesAppVersion = (self.version == appVersion.version && self.storeBuildVersion == appVersion.buildVersion)
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)
return matchesAppVersion return matchesAppVersion
} }
} }
@@ -207,14 +207,18 @@ public extension InstalledApp
"AND", "AND",
// (latestSupportedVersion.version != installedApp.version || (latestSupportedVersion.buildVersion != nil && latestSupportedVersion.buildVersion != installedApp.buildVersion)) // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion
"(%K != %K OR (%K != '' AND %K != %K))", //
// 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: " ") ].joined(separator: " ")
fetchRequest.predicate = NSPredicate(format: predicateFormat, fetchRequest.predicate = NSPredicate(format: predicateFormat,
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion), #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
#keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version), #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 return fetchRequest
} }