Skip to content

Commit

Permalink
Refactor calculating file hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
SadPencil committed Oct 4, 2024
1 parent b77fbb4 commit 7c2709d
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e)
public void OnJoined()
{
FileHashCalculator fhc = new FileHashCalculator();
fhc.CalculateHashes(gameModes);
fhc.CalculateHashes();

if (IsHost)
{
Expand Down
4 changes: 2 additions & 2 deletions DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit,
public void OnJoined()
{
FileHashCalculator fhc = new FileHashCalculator();
fhc.CalculateHashes(GameModeMaps.GameModes);
fhc.CalculateHashes();

gameFilesHash = fhc.GetCompleteHash();

Expand Down Expand Up @@ -1299,7 +1299,7 @@ protected override void StartGame()
AddNotice("Starting game...".L10N("Client:Main:StartingGame"));

FileHashCalculator fhc = new FileHashCalculator();
fhc.CalculateHashes(GameModeMaps.GameModes);
fhc.CalculateHashes();

if (gameFilesHash != fhc.GetCompleteHash())
{
Expand Down
4 changes: 2 additions & 2 deletions DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public void SetUp(bool isHost,
this.client.GetStream().Flush();

var fhc = new FileHashCalculator();
fhc.CalculateHashes(GameModeMaps.GameModes);
fhc.CalculateHashes();
localFileHash = fhc.GetCompleteHash();

RefreshMapSelectionUI();
Expand All @@ -171,7 +171,7 @@ public void SetUp(bool isHost,
public void PostJoin()
{
var fhc = new FileHashCalculator();
fhc.CalculateHashes(GameModeMaps.GameModes);
fhc.CalculateHashes();
SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash());
ResetAutoReadyCheckbox();
}
Expand Down
4 changes: 2 additions & 2 deletions DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public void SetUp(bool isHost,
this.client.GetStream().Flush();

var fhc = new FileHashCalculator();
fhc.CalculateHashes(gameModes);
fhc.CalculateHashes();
localFileHash = fhc.GetCompleteHash();
}
else
Expand All @@ -145,7 +145,7 @@ public void SetUp(bool isHost,
public void PostJoin()
{
var fhc = new FileHashCalculator();
fhc.CalculateHashes(gameModes);
fhc.CalculateHashes();
SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash());
UpdateDiscordPresence(true);
}
Expand Down
205 changes: 131 additions & 74 deletions DXMainClient/Online/FileHashCalculator.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

using ClientCore;
using ClientCore.I18N;
using DTAClient.Domain.Multiplayer;

using Rampastring.Tools;

using Utilities = Rampastring.Tools.Utilities;

namespace DTAClient.Online
{
public class FileHashCalculator
{
private FileHashes fh;
private const string CONFIGNAME = "FHCConfig.ini";
private bool calculateGameExeHash = true;

string[] fileNamesToCheck = new string[]
private static readonly IReadOnlyList<string> knownTextFileExtensions = [".txt", ".ini", ".json", ".xml"];

private string[] fileNamesToCheck = new string[]
{
#if ARES
"Ares.dll",
Expand Down Expand Up @@ -72,9 +78,11 @@ public class FileHashCalculator

public FileHashCalculator() => ParseConfigFile();

public void CalculateHashes(List<GameMode> gameModes)
private string finalHash = string.Empty;

public void CalculateHashes()
{
fh = new FileHashes
FileHashes fh = new()
{
GameOptionsHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, "GameOptions.ini")),
ClientDXHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "clientdx.exe")),
Expand All @@ -89,7 +97,6 @@ public void CalculateHashes(List<GameMode> gameModes)
LauncherExeHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.GameLauncherExecutableName)),
MPMapsHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)),
FHCConfigHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.BASE_RESOURCE_PATH, CONFIGNAME)),
INIHashes = string.Empty
};

// .NET 8 hashes are optional
Expand All @@ -106,7 +113,7 @@ public void CalculateHashes(List<GameMode> gameModes)
fh.ClientOGLNET8Hash = Utilities.CalculateSHA1ForFile(fileOGL8.FullName);

FileInfo fileUGL8 = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), "BinariesNET8", "UniversalGL", "clientogl.dll");
if (fileUGL8.Exists)
if (fileUGL8.Exists)
fh.ClientUGLNET8Hash = Utilities.CalculateSHA1ForFile(fileUGL8.FullName);

Logger.Log("Hash for " + ProgramConstants.BASE_RESOURCE_PATH + CONFIGNAME + ": " + fh.FHCConfigHash);
Expand All @@ -126,11 +133,12 @@ public void CalculateHashes(List<GameMode> gameModes)
if (!string.IsNullOrEmpty(ClientConfiguration.Instance.GameLauncherExecutableName))
Logger.Log("Hash for " + ClientConfiguration.Instance.GameLauncherExecutableName + ": " + fh.LauncherExeHash);

foreach (string filePath in fileNamesToCheck)
foreach (string relativePath in fileNamesToCheck)
{
fh.INIHashes = AddToStringIfFileExists(fh.INIHashes, filePath);
Logger.Log("Hash for " + filePath + ": " +
Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, filePath)));
string fullPath = SafePath.CombineFilePath(ProgramConstants.GamePath, relativePath);
string hash = fh.AddHashForFileIfExists(relativePath, fullPath);
if (!string.IsNullOrEmpty(hash))
Logger.Log("Hash for " + relativePath + ": " + hash);
}

DirectoryInfo[] iniPaths =
Expand All @@ -145,15 +153,14 @@ public void CalculateHashes(List<GameMode> gameModes)
{
if (path.Exists)
{
List<string> files = path.EnumerateFiles("*", SearchOption.AllDirectories).Select(s => s.Name).ToList();

files.Sort(StringComparer.Ordinal);

foreach (string filename in files)
foreach (string relativePath in path.EnumerateFiles("*", SearchOption.AllDirectories).Select(s => s.Name))
{
string sha1 = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, filename));
fh.INIHashes += sha1;
Logger.Log("Hash for " + filename + ": " + sha1);
string fullPath = SafePath.CombineFilePath(path.FullName, relativePath);
Debug.Assert(File.Exists(fullPath), $"File {fullPath} is supposed to but does not exist.");

string hash = fh.AddHashForFileIfExists(relativePath, fullPath);
if (!string.IsNullOrEmpty(hash))
Logger.Log("Hash for " + relativePath + ": " + hash);
}
}
}
Expand All @@ -171,53 +178,21 @@ public void CalculateHashes(List<GameMode> gameModes)
{
foreach (TranslationGameFile tgf in translationGameFiles)
{
string filePath = SafePath.CombineFilePath(translationFolder.FullName, tgf.Source);
if (File.Exists(filePath))
{
string sha1 = Utilities.CalculateSHA1ForFile(filePath);
fh.INIHashes += sha1;

string fileRelativePath = filePath;
if (filePath.StartsWith(ProgramConstants.GamePath))
fileRelativePath = fileRelativePath.Substring(ProgramConstants.GamePath.Length).TrimStart(Path.DirectorySeparatorChar);

Logger.Log("Hash for " + fileRelativePath + ": " + sha1);
}
string fileRelativePath = SafePath.CombineFilePath(translationFolder.Name, tgf.Source);
string fileFullPath = SafePath.CombineFilePath(translationFolder.FullName, tgf.Source);

string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath);
if (!string.IsNullOrEmpty(hash))
Logger.Log("Hash for " + fileRelativePath + ": " + hash);
}
}
}

fh.INIHashes = Utilities.CalculateSHA1ForString(fh.INIHashes);
finalHash = fh.GetFinalHash();
Logger.Log("Complete hash: " + finalHash);
}

string AddToStringIfFileExists(string str, string path)
{
if (File.Exists(path))
return str + Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, path));

return str;
}

public string GetCompleteHash()
{
string str = fh.GameOptionsHash;
str += fh.ClientDXHash;
str += fh.ClientXNAHash;
str += fh.ClientOGLHash;
str += fh.ClientDXNET8Hash;
str += fh.ClientXNANET8Hash;
str += fh.ClientOGLNET8Hash;
str += fh.ClientUGLNET8Hash;
str += fh.GameExeHash;
str += fh.LauncherExeHash;
str += fh.INIHashes;
str += fh.MPMapsHash;
str += fh.FHCConfigHash;

Logger.Log("Complete hash: " + Utilities.CalculateSHA1ForString(str));

return Utilities.CalculateSHA1ForString(str);
}
public string GetCompleteHash() => finalHash;

private void ParseConfigFile()
{
Expand All @@ -238,19 +213,101 @@ private void ParseConfigFile()
fileNamesToCheck = filenames.ToArray();
}

private record struct FileHashes(
string GameOptionsHash,
string ClientDXHash,
string ClientXNAHash,
string ClientOGLHash,
string ClientDXNET8Hash,
string ClientXNANET8Hash,
string ClientOGLNET8Hash,
string ClientUGLNET8Hash,
string INIHashes,
string MPMapsHash,
string GameExeHash,
string LauncherExeHash,
string FHCConfigHash);
private static string NormalizePath(string path) => path.Replace('\\', '/');

private static string CalculateSHA1ForFile(string path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;

FileInfo file = SafePath.GetFile(path);
if (!file.Exists)
return string.Empty;

using Stream inputStream = file.OpenRead();

if (knownTextFileExtensions.Contains(file.Extension, StringComparer.InvariantCultureIgnoreCase))
{
// Normalize line endings to LF
UTF8Encoding utf8Encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false);

using StreamReader reader = new(inputStream, utf8Encoding, detectEncodingFromByteOrderMarks: false);
string text = reader.ReadToEnd();
text = text.Replace("\r\n", "\n");

byte[] bytes = utf8Encoding.GetBytes(text);

using SHA1 sha1 = SHA1.Create();
return BytesToString(sha1.ComputeHash(bytes));
}
else
{
using SHA1 sha1 = SHA1.Create();
return BytesToString(sha1.ComputeHash(inputStream));
}
}

private static string BytesToString(byte[] bytes) =>
BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();

private class FileHashes()
{
public string GameOptionsHash;
public string ClientDXHash;
public string ClientXNAHash;
public string ClientOGLHash;
public string ClientDXNET8Hash;
public string ClientXNANET8Hash;
public string ClientOGLNET8Hash;
public string ClientUGLNET8Hash;
public string MPMapsHash;
public string GameExeHash;
public string LauncherExeHash;
public string FHCConfigHash;

public readonly SortedDictionary<string, string> AdditionalFileHashes = new(StringComparer.InvariantCultureIgnoreCase);

public string AddHashForFileIfExists(string relativePath) =>
AddHashForFileIfExists(relativePath, relativePath);

public string AddHashForFileIfExists(string relativePath, string filePath)
{
Debug.Assert(!relativePath.StartsWith(ProgramConstants.GamePath), $"File path {relativePath} should be a relative path.");

string hash = CalculateSHA1ForFile(filePath);
if (!string.IsNullOrEmpty(hash))
{
AdditionalFileHashes[NormalizePath(relativePath)] = hash;
return hash;
}
else
{
return string.Empty;
}
}

public string GetFinalHash()
{
var sb = new StringBuilder();
sb.Append(GameOptionsHash);
sb.Append(ClientDXHash);
sb.Append(ClientXNAHash);
sb.Append(ClientOGLHash);
sb.Append(ClientDXNET8Hash);
sb.Append(ClientXNANET8Hash);
sb.Append(ClientOGLNET8Hash);
sb.Append(ClientUGLNET8Hash);
sb.Append(GameExeHash);
sb.Append(LauncherExeHash);
sb.Append(MPMapsHash);
sb.Append(FHCConfigHash);

// Append additional file hashes, ordered by key
foreach (string hash in AdditionalFileHashes.Values)
sb.Append(hash);

return Utilities.CalculateSHA1ForString(sb.ToString());
}
}
}
}
8 changes: 8 additions & 0 deletions DXMainClient/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ public void Execute()
UserINISettings.Instance.ClientResolutionY = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, "ClientResolutionY", resolution.Height);
}

#if DEBUG
// Calculate hashes
{
FileHashCalculator fhc = new();
fhc.CalculateHashes();
}
#endif

gameClass.Run();
}

Expand Down

0 comments on commit 7c2709d

Please sign in to comment.