mirror of
https://github.com/kwsch/NHSE.git
synced 2026-04-24 23:27:14 -05:00
Check hash integrity on sav open
Clean up the Murmur3 implementation to be more C#-like
This commit is contained in:
parent
f085748530
commit
ac79cb06f9
|
|
@ -5,27 +5,8 @@ namespace NHSE.Core;
|
|||
/// <summary>
|
||||
/// Contains the <see cref="HashRegions"/> for a <see cref="FileName"/>.
|
||||
/// </summary>
|
||||
public sealed class FileHashDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the File that these <see cref="HashRegions"/> apply to.
|
||||
/// </summary>
|
||||
public readonly string FileName;
|
||||
|
||||
/// <summary>
|
||||
/// Expected file size of the <see cref="FileName"/>.
|
||||
/// </summary>
|
||||
public readonly uint FileSize;
|
||||
|
||||
/// <summary>
|
||||
/// Hash specs that are done in this file.
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<FileHashRegion> HashRegions;
|
||||
|
||||
public FileHashDetails(string fileName, uint fileSize, IReadOnlyList<FileHashRegion> regions)
|
||||
{
|
||||
FileName = fileName;
|
||||
FileSize = fileSize;
|
||||
HashRegions = regions;
|
||||
}
|
||||
}
|
||||
/// <param name="FileName">Name of the File that these <see cref="HashRegions"/> apply to.</param>
|
||||
/// <param name="FileSize">Expected file size of the <see cref="FileName"/>.</param>
|
||||
/// <param name="HashRegions">Hash specs that are done in this file.</param>
|
||||
/// <remarks>Checking equality is fine to do the regular shallow check; length essentially governs hash regions.</remarks>
|
||||
public sealed record FileHashDetails(string FileName, uint FileSize, IReadOnlyList<FileHashRegion> HashRegions);
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
namespace NHSE.Core;
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the region that a validation hash is calculated over.
|
||||
/// </summary>
|
||||
/// <param name="HashOffset">Offset of the calculated hash.</param>
|
||||
/// <param name="Size">Length of the hashed data.</param>
|
||||
public readonly record struct FileHashRegion(int HashOffset, int Size)
|
||||
/// <param name="Length">Length of the hashed data.</param>
|
||||
public readonly record struct FileHashRegion(int HashOffset, int Length)
|
||||
{
|
||||
/// <summary>
|
||||
/// Offset where the data to be hashed starts at (calculated).
|
||||
|
|
@ -13,9 +15,11 @@
|
|||
public int BeginOffset => HashOffset + 4;
|
||||
|
||||
/// <summary>
|
||||
/// Offset where the data to be hashed ends at (calculated).
|
||||
/// Offset where the data to be hashed ends at (calculated), exclusive.
|
||||
/// </summary>
|
||||
public int EndOffset => BeginOffset + Size;
|
||||
public int EndOffset => BeginOffset + Length;
|
||||
|
||||
public Range HashedRange => BeginOffset..EndOffset;
|
||||
|
||||
public override string ToString() => $"0x{HashOffset:X}: (0x{BeginOffset:X}-0x{EndOffset:X})";
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
|
@ -6,54 +8,29 @@ namespace NHSE.Core;
|
|||
/// <summary>
|
||||
/// MurmurHash implementation used by Animal Crossing New Horizons
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Never is any remainder in inputs, so we don't need to handle that case.
|
||||
/// </remarks>
|
||||
public static class Murmur3
|
||||
{
|
||||
private static uint Murmur32_Scramble(uint k)
|
||||
private static uint Scramble(uint value)
|
||||
{
|
||||
k = (k * 0x16A88000) | ((k * 0xCC9E2D51) >> 17);
|
||||
k *= 0x1B873593;
|
||||
return k;
|
||||
value = (value * 0x16A88000) | ((value * 0xCC9E2D51) >> 17);
|
||||
value *= 0x1B873593;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the hash at the specified offset, using the input parameters.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to hash</param>
|
||||
/// <param name="seed">Initial Murmur seed (optional)</param>
|
||||
/// <returns>Calculated hash.</returns>
|
||||
public static uint GetMurmur3Hash(ReadOnlySpan<byte> data, uint seed = 0)
|
||||
private static uint Advance(uint checksum, uint value)
|
||||
{
|
||||
var checksum = seed;
|
||||
var remaining = data;
|
||||
while (remaining.Length >= sizeof(uint))
|
||||
{
|
||||
var val = ReadUInt32LittleEndian(remaining);
|
||||
checksum ^= Murmur32_Scramble(val);
|
||||
checksum = (checksum >> 19) | (checksum << 13);
|
||||
checksum = (checksum * 5) + 0xE6546B64;
|
||||
remaining = remaining[sizeof(uint)..];
|
||||
}
|
||||
checksum ^= Scramble(value);
|
||||
checksum = (checksum >> 19) | (checksum << 13);
|
||||
checksum = (checksum * 5) + 0xE6546B64;
|
||||
return checksum;
|
||||
}
|
||||
|
||||
if (!remaining.IsEmpty)
|
||||
{
|
||||
uint val = 0;
|
||||
switch (remaining.Length)
|
||||
{
|
||||
case 3:
|
||||
val |= (uint)remaining[2] << 16;
|
||||
goto case 2;
|
||||
case 2:
|
||||
val |= (uint)remaining[1] << 8;
|
||||
goto case 1;
|
||||
case 1:
|
||||
val |= remaining[0];
|
||||
break;
|
||||
}
|
||||
|
||||
checksum ^= Murmur32_Scramble(val);
|
||||
}
|
||||
|
||||
checksum ^= (uint)data.Length;
|
||||
private static uint Finalize(uint checksum, uint length)
|
||||
{
|
||||
checksum ^= length;
|
||||
checksum ^= checksum >> 16;
|
||||
checksum *= 0x85EBCA6B;
|
||||
checksum ^= checksum >> 13;
|
||||
|
|
@ -66,12 +43,47 @@ public static uint GetMurmur3Hash(ReadOnlySpan<byte> data, uint seed = 0)
|
|||
/// Updates the hash at the specified offset, using the input parameters.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to hash</param>
|
||||
/// <param name="hashDestination">Location two write the hash</param>
|
||||
/// <returns>Calculated hash that was written back to the data.</returns>
|
||||
public static uint UpdateMurmur32(ReadOnlySpan<byte> data, Span<byte> hashDestination)
|
||||
/// <param name="seed">Initial Murmur seed (optional)</param>
|
||||
/// <returns>Calculated hash.</returns>
|
||||
public static uint Hash(ReadOnlySpan<byte> data, uint seed = 0)
|
||||
{
|
||||
var newHash = GetMurmur3Hash(data);
|
||||
WriteUInt32LittleEndian(hashDestination, newHash);
|
||||
return newHash;
|
||||
Debug.Assert(data.Length % 4 == 0); // no irregular sizes (no remainder if processing as u32*)
|
||||
var checksum = seed;
|
||||
var u32 = MemoryMarshal.Cast<byte, uint>(data);
|
||||
foreach (var x in u32)
|
||||
{
|
||||
var value = x;
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
value = ReverseEndianness(value);
|
||||
checksum = Advance(checksum, value);
|
||||
}
|
||||
|
||||
return Finalize(checksum, (uint)data.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to determine the length of data that produces the expected hash.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to hash</param>
|
||||
/// <param name="expect">Expected checksum</param>
|
||||
/// <param name="seed">Initial Murmur seed (optional)</param>
|
||||
/// <returns>Length of data (in bytes) that produces the expected hash, or -1 if not found.</returns>
|
||||
public static int GetLength(ReadOnlySpan<byte> data, uint expect, uint seed = 0)
|
||||
{
|
||||
var checksum = seed;
|
||||
var u32 = MemoryMarshal.Cast<byte, uint>(data);
|
||||
for (int i = 0; i < u32.Length; i++)
|
||||
{
|
||||
var value = u32[i];
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
value = ReverseEndianness(value);
|
||||
|
||||
checksum = Advance(checksum, value);
|
||||
var length = ((i + 1) * sizeof(uint));
|
||||
var actual = Finalize(checksum, (uint)length);
|
||||
if (actual == expect)
|
||||
return length;
|
||||
}
|
||||
return -1; // not found
|
||||
}
|
||||
}
|
||||
|
|
@ -67,10 +67,9 @@ public void Hash()
|
|||
var ver = Info.GetKnownRevisionIndex();
|
||||
var hash = RevisionChecker.HashInfo[ver];
|
||||
var details = hash.GetFile(NameData);
|
||||
if (details == null)
|
||||
throw new ArgumentNullException(nameof(NameData));
|
||||
ArgumentNullException.ThrowIfNull(details, nameof(NameData));
|
||||
foreach (var h in details.HashRegions)
|
||||
Murmur3.UpdateMurmur32(Data.Slice(h.BeginOffset, h.Size), Data[h.HashOffset..]);
|
||||
WriteUInt32LittleEndian(Data[h.HashOffset..], Murmur3.Hash(Data[h.HashedRange]));
|
||||
}
|
||||
|
||||
public IEnumerable<FileHashRegion> InvalidHashes()
|
||||
|
|
@ -78,11 +77,10 @@ public IEnumerable<FileHashRegion> InvalidHashes()
|
|||
var ver = Info.GetKnownRevisionIndex();
|
||||
var hash = RevisionChecker.HashInfo[ver];
|
||||
var details = hash.GetFile(NameData);
|
||||
if (details == null)
|
||||
throw new ArgumentNullException(nameof(NameData));
|
||||
ArgumentNullException.ThrowIfNull(details, nameof(NameData));
|
||||
foreach (var h in details.HashRegions)
|
||||
{
|
||||
var current = Murmur3.GetMurmur3Hash(Data.Slice(h.BeginOffset, h.Size));
|
||||
var current = Murmur3.Hash(Data[h.HashedRange]);
|
||||
var saved = ReadUInt32LittleEndian(Data[h.HashOffset..]);
|
||||
if (current != saved)
|
||||
yield return h;
|
||||
|
|
|
|||
|
|
@ -3,22 +3,10 @@
|
|||
/// <summary>
|
||||
/// Stores file sizes for various savedata files at different patch revisions.
|
||||
/// </summary>
|
||||
public class SaveFileSizes
|
||||
{
|
||||
public readonly uint Main;
|
||||
public readonly uint Personal;
|
||||
public readonly uint PhotoStudioIsland;
|
||||
public readonly uint PostBox;
|
||||
public readonly uint Profile;
|
||||
public readonly uint WhereAreN;
|
||||
|
||||
public SaveFileSizes(uint main, uint personal, uint photo, uint postbox, uint profile, uint wherearen = 0)
|
||||
{
|
||||
Main = main;
|
||||
Personal = personal;
|
||||
PhotoStudioIsland = photo;
|
||||
PostBox = postbox;
|
||||
Profile = profile;
|
||||
WhereAreN = wherearen;
|
||||
}
|
||||
}
|
||||
public sealed record SaveFileSizes(
|
||||
uint Main,
|
||||
uint Personal,
|
||||
uint PhotoStudioIsland,
|
||||
uint PostBox,
|
||||
uint Profile,
|
||||
uint WhereAreN = 0);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using NHSE.Core;
|
||||
using NHSE.Injection;
|
||||
|
|
@ -46,6 +47,13 @@ private static void Open(HorizonSave file)
|
|||
if (prompt != DialogResult.Yes)
|
||||
return;
|
||||
}
|
||||
var isAnyHashBad = file.GetInvalidHashes().Any();
|
||||
if (isAnyHashBad)
|
||||
{
|
||||
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MessageStrings.MsgSaveDataHashMismatch, MessageStrings.MsgAskContinue);
|
||||
if (prompt != DialogResult.Yes)
|
||||
return;
|
||||
}
|
||||
|
||||
new Editor(file).Show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public static class MessageStrings
|
|||
public static string MsgSaveDataExportFail { get; set; } = "Unable to save files to their original location.";
|
||||
public static string MsgSaveDataHashesValid { get; set; } = "Hashes are valid.";
|
||||
public static string MsgSaveDataSizeMismatch { get; set; } = "Save file sizes appear to be incorrect.";
|
||||
public static string MsgSaveDataHashMismatch { get; set; } = "Save file hashes appear to be incorrect.";
|
||||
|
||||
public static string MsgMoveOut { get; set; } = "Are you trying to make the Villager move out?";
|
||||
public static string MsgMoveOutSuggest { get; set; } = "If so, set the Event Flag (024 - ForceMoveOut) to 1 so that the Villager is removed by the game.";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user