diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index d87910a..5bbdf2d 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -152,6 +152,7 @@ BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */; }; BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; }; BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; }; + BFFBD3D9224A0756002EFC79 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */; }; BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */; }; BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */; }; BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */; }; @@ -319,6 +320,7 @@ BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = ""; }; BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesPresentationController.swift; sourceTree = ""; }; BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesStoryboardSegue.swift; sourceTree = ""; }; BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialGamesStoryboardSegue.swift; sourceTree = ""; }; @@ -370,6 +372,7 @@ BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */, BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */, BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */, + BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */, ); path = Extensions; sourceTree = ""; @@ -1073,6 +1076,7 @@ BF4828861F9028F500028B97 /* System.swift in Sources */, BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */, BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */, + BFFBD3D9224A0756002EFC79 /* URL+ExtendedAttributes.swift in Sources */, BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */, BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */, BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */, diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 548bfa0..18c3aba 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -527,22 +527,32 @@ private extension GameViewController guard let game = self.game as? Game else { return } DatabaseManager.shared.performBackgroundTask { (context) in - let game = context.object(with: game.objectID) as! Game - - if let gameSave = game.gameSave - { - gameSave.modifiedDate = Date() - } - else - { - let gameSave = GameSave(context: context) - gameSave.identifier = game.identifier - game.gameSave = gameSave - } - do { + let game = context.object(with: game.objectID) as! Game + + let hash = try RSTHasher.sha1HashOfFile(at: game.gameSaveURL) + let previousHash = game.gameSaveURL.extendedAttribute(name: "com.rileytestut.delta.sha1Hash") + + guard hash != previousHash else { return } + + if let gameSave = game.gameSave + { + gameSave.modifiedDate = Date() + } + else + { + let gameSave = GameSave(context: context) + gameSave.identifier = game.identifier + game.gameSave = gameSave + } + try context.save() + try game.gameSaveURL.setExtendedAttribute(name: "com.rileytestut.delta.sha1Hash", value: hash) + } + catch CocoaError.fileNoSuchFile + { + // Ignore } catch { diff --git a/Delta/Extensions/URL+ExtendedAttributes.swift b/Delta/Extensions/URL+ExtendedAttributes.swift new file mode 100644 index 0000000..596ef1c --- /dev/null +++ b/Delta/Extensions/URL+ExtendedAttributes.swift @@ -0,0 +1,45 @@ +// +// URL+ExtendedAttributes.swift +// Delta +// +// Created by Riley Testut on 3/26/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +extension URL +{ + func setExtendedAttribute(name: String, value: String) throws + { + try self.withUnsafeFileSystemRepresentation { (path) in + let data = value.data(using: .utf8) + let result = data?.withUnsafeBytes { (buffer) in + setxattr(path, name, buffer.baseAddress, buffer.count, 0, 0) + } + + if let result = result, result < 0 + { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .ENOENT) + } + } + } + + func extendedAttribute(name: String) -> String? + { + let value = self.withUnsafeFileSystemRepresentation { (path) -> String? in + let size = getxattr(path, name, nil, 0, 0, 0) + guard size >= 0 else { return nil } + + var data = Data(count: size) + let result = data.withUnsafeMutableBytes { getxattr(path, name, $0.baseAddress, $0.count, 0, 0) } + + guard result >= 0 else { return nil } + + let value = String(data: data, encoding: .utf8)! + return value + } + + return value + } +}