2019-07-24 12:23:54 -07:00
|
|
|
//
|
|
|
|
|
// MergePolicy.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 7/23/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import CoreData
|
|
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
import AltSign
|
2019-07-24 12:23:54 -07:00
|
|
|
import Roxas
|
|
|
|
|
|
2022-11-23 19:08:31 -06:00
|
|
|
extension MergeError
|
|
|
|
|
{
|
2023-01-13 14:46:42 -06:00
|
|
|
public enum Code: Int, ALTErrorCode
|
2022-11-23 19:08:31 -06:00
|
|
|
{
|
2023-01-13 14:46:42 -06:00
|
|
|
public typealias Error = MergeError
|
2022-11-23 19:08:31 -06:00
|
|
|
|
|
|
|
|
case noVersions
|
2023-01-13 14:38:26 -06:00
|
|
|
case incorrectVersionOrder
|
2022-11-23 19:08:31 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-13 14:38:26 -06:00
|
|
|
static func noVersions(for app: StoreApp) -> MergeError { .init(code: .noVersions, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
|
|
|
|
static func incorrectVersionOrder(for app: StoreApp) -> MergeError { .init(code: .incorrectVersionOrder, appName: app.name, appBundleID: app.bundleIdentifier, sourceID: app.sourceIdentifier) }
|
2022-11-23 19:08:31 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
public struct MergeError: ALTLocalizedError
|
2022-11-23 19:08:31 -06:00
|
|
|
{
|
2023-01-13 14:46:42 -06:00
|
|
|
public static var errorDomain: String { "AltStore.MergeError" }
|
2022-11-23 19:08:31 -06:00
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
public let code: Code
|
|
|
|
|
public var errorTitle: String?
|
|
|
|
|
public var errorFailure: String?
|
2022-11-23 19:08:31 -06:00
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
public var appName: String?
|
|
|
|
|
public var appBundleID: String?
|
|
|
|
|
public var sourceID: String?
|
2022-11-23 19:08:31 -06:00
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
public var errorFailureReason: String {
|
2022-11-23 19:08:31 -06:00
|
|
|
switch self.code
|
|
|
|
|
{
|
|
|
|
|
case .noVersions:
|
|
|
|
|
var appName = NSLocalizedString("At least one app", comment: "")
|
|
|
|
|
if let name = self.appName, let bundleID = self.appBundleID
|
|
|
|
|
{
|
|
|
|
|
appName = name + " (\(bundleID))"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(format: NSLocalizedString("%@ does not have any app versions.", comment: ""), appName)
|
2023-01-13 14:38:26 -06:00
|
|
|
|
|
|
|
|
case .incorrectVersionOrder:
|
|
|
|
|
var appName = NSLocalizedString("one or more apps", comment: "")
|
|
|
|
|
if let name = self.appName, let bundleID = self.appBundleID
|
|
|
|
|
{
|
|
|
|
|
appName = name + " (\(bundleID))"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(format: NSLocalizedString("The cached versions for %@ do not match the source.", comment: ""), appName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
public var recoverySuggestion: String? {
|
2023-01-13 14:38:26 -06:00
|
|
|
switch self.code
|
|
|
|
|
{
|
|
|
|
|
case .incorrectVersionOrder: return NSLocalizedString("Please try again later.", comment: "")
|
|
|
|
|
default: return nil
|
2022-11-23 19:08:31 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-13 14:46:42 -06:00
|
|
|
// Necessary to cast back to MergeError from NSError when thrown from NSMergePolicy.
|
|
|
|
|
extension MergeError: _ObjectiveCBridgeableError
|
|
|
|
|
{
|
|
|
|
|
public var errorUserInfo: [String : Any] {
|
|
|
|
|
// Copied from ALTLocalizedError
|
|
|
|
|
var userInfo: [String: Any?] = [
|
|
|
|
|
NSLocalizedFailureErrorKey: self.errorFailure,
|
|
|
|
|
ALTLocalizedTitleErrorKey: self.errorTitle,
|
|
|
|
|
ALTSourceFileErrorKey: self.sourceFile,
|
|
|
|
|
ALTSourceLineErrorKey: self.sourceLine,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
userInfo["appName"] = self.appName
|
|
|
|
|
userInfo["appBundleID"] = self.appBundleID
|
|
|
|
|
userInfo["sourceID"] = self.sourceID
|
|
|
|
|
|
|
|
|
|
return userInfo.compactMapValues { $0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public init?(_bridgedNSError error: NSError)
|
|
|
|
|
{
|
|
|
|
|
guard error.domain == MergeError.errorDomain, let code = Code(rawValue: error.code) else { return nil }
|
|
|
|
|
|
|
|
|
|
self.code = code
|
|
|
|
|
self.errorTitle = error.localizedTitle
|
|
|
|
|
self.errorFailure = error.localizedFailure
|
|
|
|
|
|
|
|
|
|
self.appName = error.userInfo["appName"] as? String
|
|
|
|
|
self.appBundleID = error.userInfo["appBundleID"] as? String
|
|
|
|
|
self.sourceID = error.userInfo["sourceID"] as? String
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-23 19:08:31 -06:00
|
|
|
private extension Error
|
|
|
|
|
{
|
|
|
|
|
func serialized(withFailure failure: String) -> NSError
|
|
|
|
|
{
|
|
|
|
|
// We need to serialize Swift errors thrown during merge conflict to preserve error messages.
|
|
|
|
|
|
|
|
|
|
let serializedError = (self as NSError).withLocalizedFailure(failure).sanitizedForSerialization()
|
|
|
|
|
|
|
|
|
|
var userInfo = serializedError.userInfo
|
|
|
|
|
userInfo[NSLocalizedDescriptionKey] = nil // Remove NSLocalizedDescriptionKey value to prevent duplicating localized failure in localized description.
|
|
|
|
|
|
|
|
|
|
let error = NSError(domain: serializedError.domain, code: serializedError.code, userInfo: userInfo)
|
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-24 12:23:54 -07:00
|
|
|
open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|
|
|
|
{
|
|
|
|
|
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
|
|
|
|
{
|
|
|
|
|
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
2020-03-24 13:27:44 -07:00
|
|
|
for conflict in conflicts
|
|
|
|
|
{
|
|
|
|
|
switch conflict.conflictingObjects.first
|
|
|
|
|
{
|
|
|
|
|
case is StoreApp where conflict.conflictingObjects.count == 2:
|
|
|
|
|
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
|
|
|
|
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
|
|
|
|
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
|
|
|
|
|
|
|
|
|
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
|
|
|
|
{
|
|
|
|
|
// Delete previous permissions (same as below).
|
|
|
|
|
for permission in previousApp.permissions
|
|
|
|
|
{
|
|
|
|
|
permission.managedObjectContext?.delete(permission)
|
|
|
|
|
}
|
2022-09-12 17:05:55 -07:00
|
|
|
|
|
|
|
|
// Delete previous versions (different than below).
|
|
|
|
|
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
|
|
|
|
|
{
|
|
|
|
|
appVersion.managedObjectContext?.delete(appVersion)
|
|
|
|
|
}
|
2020-03-24 13:27:44 -07:00
|
|
|
}
|
|
|
|
|
|
2022-09-13 15:31:14 -07:00
|
|
|
case is AppVersion where conflict.conflictingObjects.count == 2:
|
|
|
|
|
// Occurs first time fetching sources after migrating from pre-AppVersion database model.
|
|
|
|
|
let conflictingAppVersions = conflict.conflictingObjects.lazy.compactMap { $0 as? AppVersion }
|
|
|
|
|
|
|
|
|
|
// Primary AppVersion == AppVersion whose latestVersionApp.latestVersion points back to itself.
|
2024-08-06 10:43:52 +09:00
|
|
|
if let primaryAppVersion = conflictingAppVersions.first(where: { $0.latestSupportedVersionApp?.latestSupportedVersion == $0 }),
|
2022-09-13 15:31:14 -07:00
|
|
|
let secondaryAppVersion = conflictingAppVersions.first(where: { $0 != primaryAppVersion })
|
|
|
|
|
{
|
|
|
|
|
secondaryAppVersion.managedObjectContext?.delete(secondaryAppVersion)
|
|
|
|
|
print("[ALTLog] Resolving AppVersion context-level conflict. Most likely due to migrating from pre-AppVersion model version.", primaryAppVersion)
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-24 13:27:44 -07:00
|
|
|
default:
|
|
|
|
|
// Unknown context-level conflict.
|
|
|
|
|
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try super.resolve(constraintConflicts: conflicts)
|
|
|
|
|
|
|
|
|
|
return
|
2019-07-24 12:23:54 -07:00
|
|
|
}
|
|
|
|
|
|
2023-04-04 17:14:52 -05:00
|
|
|
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]()
|
2023-04-04 13:59:19 -05:00
|
|
|
var featuredAppIDsBySourceID = [String: [String]]()
|
2023-01-13 14:38:26 -06:00
|
|
|
|
2019-07-24 12:23:54 -07:00
|
|
|
for conflict in conflicts
|
|
|
|
|
{
|
|
|
|
|
switch conflict.databaseObject
|
|
|
|
|
{
|
2019-07-31 14:07:00 -07:00
|
|
|
case let databaseObject as StoreApp:
|
2019-07-24 12:23:54 -07:00
|
|
|
// Delete previous permissions
|
|
|
|
|
for permission in databaseObject.permissions
|
|
|
|
|
{
|
|
|
|
|
permission.managedObjectContext?.delete(permission)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-12 17:05:55 -07:00
|
|
|
if let contextApp = conflict.conflictingObjects.first as? StoreApp
|
|
|
|
|
{
|
2023-01-13 14:38:26 -06:00
|
|
|
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
|
|
|
|
|
|
|
|
|
|
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
|
2022-09-12 17:05:55 -07:00
|
|
|
{
|
2023-01-13 14:38:26 -06:00
|
|
|
// Version # does NOT exist in context, so delete existing appVersion.
|
|
|
|
|
appVersion.managedObjectContext?.delete(appVersion)
|
2022-09-12 17:05:55 -07:00
|
|
|
}
|
2022-11-16 17:31:44 -06:00
|
|
|
|
2023-04-04 17:14:52 -05:00
|
|
|
if let globallyUniqueID = contextApp.globallyUniqueID
|
|
|
|
|
{
|
|
|
|
|
// Core Data _normally_ preserves the correct ordering of versions when merging,
|
|
|
|
|
// but just in case we cache the order and reorder the versions post-merge if needed.
|
|
|
|
|
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions
|
|
|
|
|
}
|
2022-09-12 17:05:55 -07:00
|
|
|
}
|
|
|
|
|
|
2019-07-30 17:00:04 -07:00
|
|
|
case let databaseObject as Source:
|
|
|
|
|
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
2023-04-04 13:59:19 -05:00
|
|
|
|
2019-07-30 17:00:04 -07:00
|
|
|
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
2019-09-03 21:58:07 -07:00
|
|
|
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
2023-04-04 13:59:19 -05:00
|
|
|
|
2019-07-30 17:00:04 -07:00
|
|
|
for app in databaseObject.apps
|
|
|
|
|
{
|
|
|
|
|
if !bundleIdentifiers.contains(app.bundleIdentifier)
|
|
|
|
|
{
|
|
|
|
|
// No longer listed in Source, so remove it from database.
|
|
|
|
|
app.managedObjectContext?.delete(app)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-03 21:58:07 -07:00
|
|
|
for newsItem in databaseObject.newsItems
|
|
|
|
|
{
|
|
|
|
|
if !newsItemIdentifiers.contains(newsItem.identifier)
|
|
|
|
|
{
|
|
|
|
|
// No longer listed in Source, so remove it from database.
|
|
|
|
|
newsItem.managedObjectContext?.delete(newsItem)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 13:59:19 -05:00
|
|
|
if let contextSource = conflict.conflictingObjects.first as? Source
|
|
|
|
|
{
|
|
|
|
|
featuredAppIDsBySourceID[databaseObject.identifier] = contextSource.featuredApps?.map { $0.bundleIdentifier }
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-24 12:23:54 -07:00
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try super.resolve(constraintConflicts: conflicts)
|
2022-11-16 17:31:44 -06:00
|
|
|
|
|
|
|
|
for conflict in conflicts
|
|
|
|
|
{
|
|
|
|
|
switch conflict.databaseObject
|
|
|
|
|
{
|
|
|
|
|
case let databaseObject as StoreApp:
|
2022-11-23 19:08:31 -06:00
|
|
|
do
|
|
|
|
|
{
|
2023-01-13 14:38:26 -06:00
|
|
|
let appVersions: [AppVersion]
|
|
|
|
|
|
2023-04-04 17:14:52 -05:00
|
|
|
if let globallyUniqueID = databaseObject.globallyUniqueID,
|
|
|
|
|
let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID],
|
2023-01-13 14:38:26 -06:00
|
|
|
let sortedAppVersionsArray = sortedAppVersions.array as? [String],
|
|
|
|
|
case let databaseVersions = databaseObject.versions.map({ $0.version }),
|
|
|
|
|
databaseVersions != sortedAppVersionsArray
|
|
|
|
|
{
|
|
|
|
|
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
|
|
|
|
|
|
|
|
|
|
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
|
|
|
|
|
let indexA = sortedAppVersions.index(of: versionA.version)
|
|
|
|
|
let indexB = sortedAppVersions.index(of: versionB.version)
|
|
|
|
|
return indexA < indexB
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let appVersionValues = fixedAppVersions.map { $0.version }
|
|
|
|
|
guard appVersionValues == sortedAppVersionsArray else {
|
|
|
|
|
// fixedAppVersions still doesn't match source's versions, so throw MergeError.
|
|
|
|
|
throw MergeError.incorrectVersionOrder(for: databaseObject)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appVersions = fixedAppVersions
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
appVersions = databaseObject.versions
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-23 19:08:31 -06:00
|
|
|
// Update versions post-merging to make sure latestSupportedVersion is correct.
|
2023-01-13 14:38:26 -06:00
|
|
|
try databaseObject.setVersions(appVersions)
|
2022-11-23 19:08:31 -06:00
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
let nsError = error.serialized(withFailure: NSLocalizedString("AltStore's database could not be saved.", comment: ""))
|
|
|
|
|
throw nsError
|
|
|
|
|
}
|
2022-11-16 17:31:44 -06:00
|
|
|
|
2023-04-04 13:59:19 -05:00
|
|
|
case let databaseObject as Source:
|
|
|
|
|
guard let featuredAppIDs = featuredAppIDsBySourceID[databaseObject.identifier] else {
|
|
|
|
|
databaseObject.setFeaturedApps(nil)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let featuredApps: [StoreApp]?
|
|
|
|
|
|
|
|
|
|
let databaseFeaturedAppIDs = databaseObject.featuredApps?.map { $0.bundleIdentifier }
|
|
|
|
|
if databaseFeaturedAppIDs != featuredAppIDs
|
|
|
|
|
{
|
|
|
|
|
let fixedFeaturedApps = databaseObject.apps.lazy.filter { featuredAppIDs.contains($0.bundleIdentifier) }.sorted { (appA, appB) in
|
|
|
|
|
let indexA = featuredAppIDs.firstIndex(of: appA.bundleIdentifier)!
|
|
|
|
|
let indexB = featuredAppIDs.firstIndex(of: appB.bundleIdentifier)!
|
|
|
|
|
return indexA < indexB
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
featuredApps = fixedFeaturedApps
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
featuredApps = databaseObject.featuredApps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update featuredApps post-merging to make sure relationships are correct,
|
|
|
|
|
// even if the ordering is correct.
|
|
|
|
|
databaseObject.setFeaturedApps(featuredApps)
|
|
|
|
|
|
2022-11-16 17:31:44 -06:00
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-24 12:23:54 -07:00
|
|
|
}
|
|
|
|
|
}
|