From 56ac05f9a342a884dec463ea225c3e2134dd4edb Mon Sep 17 00:00:00 2001
From: Moritz Bender <35152647+Morilli@users.noreply.github.com>
Date: Sun, 13 Aug 2023 14:11:48 -0700
Subject: [PATCH] Allow migrating non-SHA1 hashes on movie import (#3733)

* Allow migrating non-SHA1 hashes on movie import

* Show more descriptive message on missing hash

* actually return null here

* Simplify LsmvImport

* add more hash HeaderKeys

* remove unnecessary IEmulator argument

this could potentially fix bugs even

* explicitly cast to ReadOnlySpan
---
 src/BizHawk.Client.Common/movie/HeaderKeys.cs |  3 +
 .../movie/import/FcmImport.cs                 |  2 +-
 .../movie/import/Fm2Import.cs                 |  2 +-
 .../movie/import/IMovieImport.cs              | 71 ++++++++++++++-----
 .../movie/import/LsmvImport.cs                |  8 +--
 .../movie/import/MmvImport.cs                 |  2 +-
 .../movie/import/MovieImport.cs               |  4 +-
 .../movie/import/SmvImport.cs                 |  2 +-
 src/BizHawk.Client.EmuHawk/MainForm.Movie.cs  |  6 +-
 src/BizHawk.Client.EmuHawk/MainForm.cs        |  2 +-
 10 files changed, 73 insertions(+), 29 deletions(-)

diff --git a/src/BizHawk.Client.Common/movie/HeaderKeys.cs b/src/BizHawk.Client.Common/movie/HeaderKeys.cs
index 1f6c6905df..2ec87ba6cb 100644
--- a/src/BizHawk.Client.Common/movie/HeaderKeys.cs
+++ b/src/BizHawk.Client.Common/movie/HeaderKeys.cs
@@ -15,6 +15,9 @@ namespace BizHawk.Client.Common
 		public const string StartsFromSaveram = "StartsFromSaveRam";
 		public const string SavestateBinaryBase64Blob = "SavestateBinaryBase64Blob"; // this string will not contain base64: ; it's implicit (this is to avoid another big string op to dice off the base64: substring)
 		public const string Sha1 = "SHA1"; // misleading name; either CRC32, MD5, or SHA1, hex-encoded, unprefixed
+		public const string Sha256 = "SHA256";
+		public const string Md5 = "MD5";
+		public const string Crc32 = "CRC32";
 		public const string FirmwareSha1 = "FirmwareSHA1";
 		public const string Pal = "PAL";
 		public const string BoardName = "BoardName";
diff --git a/src/BizHawk.Client.Common/movie/import/FcmImport.cs b/src/BizHawk.Client.Common/movie/import/FcmImport.cs
index 8e1d7a44cd..ef0d919639 100644
--- a/src/BizHawk.Client.Common/movie/import/FcmImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/FcmImport.cs
@@ -107,7 +107,7 @@ namespace BizHawk.Client.Common.movie.import
 
 			// 020 16-byte md5sum of the ROM used
 			byte[] md5 = r.ReadBytes(16);
-			Result.Movie.HeaderEntries[Md5] = md5.BytesToHexString().ToLower();
+			Result.Movie.HeaderEntries[HeaderKeys.Md5] = md5.BytesToHexString().ToLower();
 
 			// 030 4-byte little-endian unsigned int: version of the emulator used
 			uint emuVersion = r.ReadUInt32();
diff --git a/src/BizHawk.Client.Common/movie/import/Fm2Import.cs b/src/BizHawk.Client.Common/movie/import/Fm2Import.cs
index b7562df451..df81b59dcf 100644
--- a/src/BizHawk.Client.Common/movie/import/Fm2Import.cs
+++ b/src/BizHawk.Client.Common/movie/import/Fm2Import.cs
@@ -84,7 +84,7 @@ namespace BizHawk.Client.Common
 					byte[] md5 = DecodeBlob(blob);
 					if (md5 != null && md5.Length == 16)
 					{
-						Result.Movie.HeaderEntries[Md5] = md5.BytesToHexString().ToLower();
+						Result.Movie.HeaderEntries[HeaderKeys.Md5] = md5.BytesToHexString().ToLower();
 					}
 					else
 					{
diff --git a/src/BizHawk.Client.Common/movie/import/IMovieImport.cs b/src/BizHawk.Client.Common/movie/import/IMovieImport.cs
index a8247df728..dcfaa7e286 100644
--- a/src/BizHawk.Client.Common/movie/import/IMovieImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/IMovieImport.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 
-using BizHawk.Emulation.Common;
+using BizHawk.Common;
 
 namespace BizHawk.Client.Common
 {
@@ -12,7 +12,6 @@ namespace BizHawk.Client.Common
 		ImportResult Import(
 			IDialogParent dialogParent,
 			IMovieSession session,
-			IEmulator emulator,
 			string path,
 			Config config);
 	}
@@ -20,26 +19,14 @@ namespace BizHawk.Client.Common
 	internal abstract class MovieImporter : IMovieImport
 	{
 		protected const string EmulationOrigin = "emuOrigin";
-		protected const string Md5 = "MD5";
 		protected const string MovieOrigin = "MovieOrigin";
 
 		protected IDialogParent _dialogParent;
-
-		protected void MaybeSetCorePreference(string sysID, string coreName, string fileExt)
-		{
-			if (Config.PreferredCores[sysID] != coreName
-				&& _dialogParent.ModalMessageBox2(
-					$"{fileExt} movies will have a better chance of syncing using the {coreName} core. Change your core preference for {sysID} roms to {coreName} now?",
-					icon: EMsgBoxIcon.Question))
-			{
-				Config.PreferredCores[sysID] = coreName;
-			}
-		}
+		private delegate bool MatchesMovieHash(ReadOnlySpan<byte> romData);
 
 		public ImportResult Import(
 			IDialogParent dialogParent,
 			IMovieSession session,
-			IEmulator emulator,
 			string path,
 			Config config)
 		{
@@ -55,17 +42,69 @@ namespace BizHawk.Client.Common
 
 			var newFileName = $"{SourceFile.FullName}.{Bk2Movie.Extension}";
 			Result.Movie = session.Get(newFileName);
-			Result.Movie.Attach(emulator);
 			RunImport();
 
 			if (!Result.Errors.Any())
 			{
+				if (string.IsNullOrEmpty(Result.Movie.Hash))
+				{
+					string hash = null;
+					// try to generate a matching hash from the original ROM
+					if (Result.Movie.HeaderEntries.TryGetValue(HeaderKeys.Crc32, out string crcHash))
+					{
+						hash = PromptForRom(data => string.Equals(CRC32Checksum.ComputeDigestHex(data), crcHash, StringComparison.OrdinalIgnoreCase));
+					}
+					else if (Result.Movie.HeaderEntries.TryGetValue(HeaderKeys.Md5, out string md5Hash))
+					{
+						hash = PromptForRom(data => string.Equals(MD5Checksum.ComputeDigestHex(data), md5Hash, StringComparison.OrdinalIgnoreCase));
+					}
+					else if (Result.Movie.HeaderEntries.TryGetValue(HeaderKeys.Sha256, out string sha256Hash))
+					{
+						hash = PromptForRom(data => string.Equals(SHA256Checksum.ComputeDigestHex(data), sha256Hash, StringComparison.OrdinalIgnoreCase));
+					}
+
+					if (hash is not null)
+						Result.Movie.Hash = hash;
+				}
+
 				Result.Movie.Save();
 			}
 
 			return Result;
 		}
 
+		/// <summary>
+		/// Prompts the user for a ROM file that matches the original movie file's hash
+		/// and returns a SHA1 hash of that ROM file.
+		/// </summary>
+		/// <param name="matchesMovieHash">Function that checks whether the ROM data matches the original hash</param>
+		/// <returns>SHA1 hash of the selected ROM file</returns>
+		private string PromptForRom(MatchesMovieHash matchesMovieHash)
+		{
+			string messageBoxText = "Please select the original ROM to finalize the import process.";
+			while (true)
+			{
+				if (!_dialogParent.ModalMessageBox2(messageBoxText, "ROM required to populate hash", useOKCancel: true))
+					return null;
+
+				var result = _dialogParent.ShowFileOpenDialog(
+					filter: RomLoader.RomFilter,
+					initDir: Config.PathEntries.RomAbsolutePath(Result.Movie.SystemID));
+				if (result is null)
+					return null; // skip hash migration when the dialog was canceled
+
+				using var rom = new HawkFile(result);
+				if (rom.IsArchive) rom.BindFirst();
+				var romData = (ReadOnlySpan<byte>) rom.ReadAllBytes();
+				if (romData.Length % 1024 == 512)
+					romData = romData.Slice(512, romData.Length - 512);
+				if (matchesMovieHash(romData))
+					return SHA1Checksum.ComputeDigestHex(romData);
+
+				messageBoxText = "The selected ROM does not match the movie's hash. Please try again.";
+			}
+		}
+
 		protected Config Config { get; private set; }
 
 		protected ImportResult Result { get; } = new ImportResult();
diff --git a/src/BizHawk.Client.Common/movie/import/LsmvImport.cs b/src/BizHawk.Client.Common/movie/import/LsmvImport.cs
index d11ff42bcd..564e9f6192 100644
--- a/src/BizHawk.Client.Common/movie/import/LsmvImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/LsmvImport.cs
@@ -4,6 +4,7 @@ using System.IO.Compression;
 using System.Linq;
 using System.Text;
 using BizHawk.Common.IOExtensions;
+using BizHawk.Common.StringExtensions;
 using BizHawk.Emulation.Common;
 using BizHawk.Emulation.Cores;
 using BizHawk.Emulation.Cores.Nintendo.BSNES;
@@ -214,10 +215,9 @@ namespace BizHawk.Client.Common.movie.import
 				else if (item.FullName.EndsWith(".sha256"))
 				{
 					using var stream = item.Open();
-					string rom = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
-					int pos = item.FullName.LastIndexOf(".sha256");
-					string name = item.FullName.Substring(0, pos);
-					Result.Movie.HeaderEntries[$"SHA256_{name}"] = rom;
+					string sha256Hash = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
+					string name = item.FullName.RemoveSuffix(".sha256");
+					Result.Movie.HeaderEntries[name is "rom" ? HeaderKeys.Sha256 : $"SHA256_{name}"] = sha256Hash;
 				}
 				else if (item.FullName == "savestate")
 				{
diff --git a/src/BizHawk.Client.Common/movie/import/MmvImport.cs b/src/BizHawk.Client.Common/movie/import/MmvImport.cs
index 9e97157026..22ec738295 100644
--- a/src/BizHawk.Client.Common/movie/import/MmvImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/MmvImport.cs
@@ -92,7 +92,7 @@ namespace BizHawk.Client.Common.movie.import
 
 			// 00e4-00f3: binary: rom MD5 digest
 			byte[] md5 = r.ReadBytes(16);
-			Result.Movie.HeaderEntries[Md5] = md5.BytesToHexString().ToLower();
+			Result.Movie.HeaderEntries[HeaderKeys.Md5] = md5.BytesToHexString().ToLower();
 
 			var ss = new SMS.SmsSyncSettings();
 			var cd = new SMSControllerDeck(ss.Port1, ss.Port2, isGameGear, ss.UseKeyboard);
diff --git a/src/BizHawk.Client.Common/movie/import/MovieImport.cs b/src/BizHawk.Client.Common/movie/import/MovieImport.cs
index 38e09b98d4..72310e478e 100644
--- a/src/BizHawk.Client.Common/movie/import/MovieImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/MovieImport.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Linq;
 using System.IO;
 using System.Reflection;
-using BizHawk.Emulation.Common;
 
 namespace BizHawk.Client.Common
 {
@@ -36,7 +35,6 @@ namespace BizHawk.Client.Common
 		public static ImportResult ImportFile(
 			IDialogParent dialogParent,
 			IMovieSession session,
-			IEmulator emulator,
 			string path,
 			Config config)
 		{
@@ -51,7 +49,7 @@ namespace BizHawk.Client.Common
 			// Create a new instance of the importer class using the no-argument constructor
 
 			return importerType.GetConstructor(Array.Empty<Type>())?.Invoke(Array.Empty<object>()) is IMovieImport importer
-				? importer.Import(dialogParent, session, emulator, path, config)
+				? importer.Import(dialogParent, session, path, config)
 				: ImportResult.Error($"No importer found for file type {ext}");
 		}
 
diff --git a/src/BizHawk.Client.Common/movie/import/SmvImport.cs b/src/BizHawk.Client.Common/movie/import/SmvImport.cs
index ef7d3cf94e..b6889eb760 100644
--- a/src/BizHawk.Client.Common/movie/import/SmvImport.cs
+++ b/src/BizHawk.Client.Common/movie/import/SmvImport.cs
@@ -183,7 +183,7 @@ namespace BizHawk.Client.Common.movie.import
 				// 000 3 bytes of zero padding: 00 00 00 003 4-byte integer: CRC32 of the ROM 007 23-byte ascii string
 				r.ReadBytes(3);
 				int crc32 = r.ReadInt32();
-				Result.Movie.HeaderEntries["CRC32"] = crc32.ToString("X08");
+				Result.Movie.HeaderEntries[HeaderKeys.Crc32] = crc32.ToString("X08");
 
 				// the game name copied from the ROM, truncated to 23 bytes (the game name in the ROM is 21 bytes)
 				string gameName = NullTerminated(Encoding.UTF8.GetString(r.ReadBytes(23)));
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs b/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
index fa08f5bbe9..6412e72012 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
@@ -54,7 +54,11 @@ namespace BizHawk.Client.EmuHawk
 
 			SetMainformMovieInfo();
 
-			if (MovieSession.Movie.Hash != Game.Hash)
+			if (string.IsNullOrEmpty(MovieSession.Movie.Hash))
+			{
+				AddOnScreenMessage("Movie is missing hash, skipping hash check");
+			}
+			else if (MovieSession.Movie.Hash != Game.Hash)
 			{
 				AddOnScreenMessage("Warning: Movie hash does not match the ROM");
 			}
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 2ca287af51..98da8daac5 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -4150,7 +4150,7 @@ namespace BizHawk.Client.EmuHawk
 
 		private void ProcessMovieImport(string fn, bool start)
 		{
-			var result = MovieImport.ImportFile(this, MovieSession, Emulator, fn, Config);
+			var result = MovieImport.ImportFile(this, MovieSession, fn, Config);
 
 			if (result.Errors.Any())
 			{