Throws SyncValidationError when downloading corrupted Game or GameSave record

This commit is contained in:
Riley Testut 2023-08-10 19:18:16 -05:00
parent ca8c2cb8c5
commit fcdd3c7840
5 changed files with 98 additions and 5 deletions

View File

@ -201,6 +201,7 @@
D5A9C01D29DE058C00A8D610 /* VariableFastForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */; };
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; };
D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ADD12129F33FBF00CE0560 /* Features.swift */; };
D5CDCCEE2A859DC200E22131 /* SyncValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CDCCEC2A859B2B00E22131 /* SyncValidationError.swift */; };
D5CDCCEF2A859E5300E22131 /* OSLog+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */; };
D5CDCCF02A859E5500E22131 /* UserDefaults+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */; };
D5CDCCF12A859E7500E22131 /* ReviewSaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1375A2A7D8F2600AB1372 /* ReviewSaveStatesViewController.swift */; };
@ -478,6 +479,7 @@
D5ADD12129F33FBF00CE0560 /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = "<group>"; };
D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+Delta.swift"; sourceTree = "<group>"; };
D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Delta.swift"; sourceTree = "<group>"; };
D5CDCCEC2A859B2B00E22131 /* SyncValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncValidationError.swift; sourceTree = "<group>"; };
D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSAirPlay.swift; sourceTree = "<group>"; };
D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeatureOption.swift"; sourceTree = "<group>"; };
D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = "<group>"; };
@ -888,6 +890,7 @@
isa = PBXGroup;
children = (
BFAB9F7C219A43380080EC7D /* SyncManager.swift */,
D5CDCCEC2A859B2B00E22131 /* SyncValidationError.swift */,
BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */,
BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */,
);
@ -1660,6 +1663,7 @@
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */,
BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */,
D5CDCCEE2A859DC200E22131 /* SyncValidationError.swift in Sources */,
BF4828881F90290F00028B97 /* Action.swift in Sources */,
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */,
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */,

View File

@ -199,4 +199,14 @@ extension Game: Syncable
public var syncableLocalizedName: String? {
return self.name
}
public func awakeFromSync(_ record: AnyRecord) throws
{
guard let gameCollection = self.gameCollection else { throw SyncValidationError.incorrectGameCollection(nil) }
if gameCollection.identifier != self.type.rawValue
{
throw SyncValidationError.incorrectGameCollection(gameCollection.name)
}
}
}

View File

@ -53,7 +53,9 @@ extension GameSave: Syncable
public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name]
// Use self.identifier to always link with exact matching game.
return [.gameID: self.identifier, .gameName: game.name]
}
public var syncableLocalizedName: String? {
@ -66,4 +68,30 @@ extension GameSave: Syncable
return self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier
}
public func awakeFromSync(_ record: AnyRecord) throws
{
guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) }
if game.identifier != self.identifier
{
let fetchRequest = GameSave.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier)
if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil
{
// Relink game with its correct gameSave, in case we accidentally misplaced it.
// Otherwise, corrupted records might displace already-downloaded GameSaves
// due to automatic Core Data relationship propagation, despite us throwing error.
game.gameSave = misplacedGameSave
}
else
{
// Either there is no misplacedGameSave, or there is but it's linked to another game somehow.
game.gameSave = nil
}
throw SyncValidationError.incorrectGame(game.name)
}
}
}

View File

@ -173,6 +173,15 @@ private extension SyncResultViewController
errorMessage = NSLocalizedString("The game for this item is missing. Please re-import the game to resume syncing its data.", comment: "")
}
case .other(_, let error as SyncValidationError):
var message = error.failureReason ?? error.localizedDescription
if let recoverySuggestion = error.recoverySuggestion
{
message += "\n\n" + recoverySuggestion
}
errorMessage = message
case .other(_, let error as NSError): errorMessage = error.localizedFailureReason ?? error.localizedDescription
default: errorMessage = error.failureReason
}
@ -249,9 +258,7 @@ private extension SyncResultViewController
case .gameControllerInputMapping: group = .gameControllerInputMapping
case .gameSave:
guard let gameID = metadata?[.gameID] else { continue }
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: error.record.recordID.identifier)
group = .game(recordID)
case .saveState:
@ -387,7 +394,11 @@ extension SyncResultViewController
case .game:
guard let error = section.errors.first as? RecordError else { return nil }
return error.record.localizedName
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: error.record.recordID.identifier)
let gameName = self.gameNamesByRecordID[recordID] // In case the remote record is corrupted, rely on local names.
return gameName ?? error.record.localizedName
case .saveState(let gameID):
guard let error = section.errors.first as? RecordError else { return nil }

View File

@ -0,0 +1,40 @@
//
// SyncValidationError.swift
// Delta
//
// Created by Riley Testut on 8/10/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
enum SyncValidationError: LocalizedError
{
case incorrectGame(String?)
case incorrectGameCollection(String?)
var failureReason: String? {
switch self
{
case .incorrectGame(let name?):
return String(format: NSLocalizedString("The downloaded record is associated with the wrong game (%@).", comment: ""), name)
case .incorrectGame(nil):
return NSLocalizedString("The downloaded record is not associated with a game.", comment: "")
case .incorrectGameCollection(let name?):
return String(format: NSLocalizedString("The downloaded record is associated with the wrong game system (%@).", comment: ""), name)
case .incorrectGameCollection(nil):
return NSLocalizedString("The downloaded record is not associated with a game system.", comment: "")
}
}
var recoverySuggestion: String? {
switch self
{
case .incorrectGame: return NSLocalizedString("Try restoring an older version to resolve this issue.", comment: "")
case .incorrectGameCollection: return NSLocalizedString("Try restoring an older version, or manually re-import the game to resolve this issue.", comment: "")
}
}
}