Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor calculating file hashes #567

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
211 changes: 136 additions & 75 deletions DXMainClient/Online/FileHashCalculator.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
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 +76,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 +95,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 +111,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 +131,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 +151,15 @@ 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 filename 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 fileRelativePath = SafePath.CombineFilePath(path.Name, filename);
string fileFullPath = SafePath.CombineFilePath(path.FullName, filename);
Debug.Assert(File.Exists(fileFullPath), $"File {fileFullPath} is supposed to but does not exist.");

string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath);
if (!string.IsNullOrEmpty(hash))
Logger.Log("Hash for " + fileRelativePath + ": " + hash);
}
}
}
Expand All @@ -171,53 +177,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 +212,106 @@ 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").Trim();

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 fileHash in AdditionalFileHashes.Values)
sb.Append(fileHash);

// Merge hashes
string finalHash = sb.ToString();
byte[] buffer = Encoding.ASCII.GetBytes(finalHash);
using SHA1 sha1 = SHA1.Create();
byte[] hash = sha1.ComputeHash(buffer);
return BytesToString(hash);
}
}
}
}
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
Loading