diff --git a/Dalamud/Utility/IntegrityUtils.cs b/Dalamud/Utility/IntegrityUtils.cs
new file mode 100644
index 000000000..5e2d054e6
--- /dev/null
+++ b/Dalamud/Utility/IntegrityUtils.cs
@@ -0,0 +1,133 @@
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Dalamud.Utility;
+
+/// Integrity utilities.
+public static class IntegrityUtils
+{
+ private const int BufferSize = 1048576; // 1 MB
+ private const char NormalizationPathSeparator = '/';
+
+ /// Computes the hash of a recursively using with .
+ /// Directory to check integrity for.
+ /// Key for .
+ /// Cancellation Token.
+ /// result with .
+ public static Task ComputeDirectoryHmacSha256Async(string directory, byte[] key, CancellationToken cancellationToken = default) => ComputeDirectoryHmacSha256Async(new DirectoryInfo(directory), key, cancellationToken);
+
+ /// Computes the hash of a recursively using with .
+ /// Directory to check integrity for.
+ /// Key for .
+ /// Cancellation Token.
+ /// result with .
+ public static async Task ComputeDirectoryHmacSha256Async(DirectoryInfo directory, byte[] key, CancellationToken cancellationToken = default)
+ {
+ // Read the entire directory structure recursively
+ var structure = ReadDirectoryStructure(directory, cancellationToken);
+
+ // Sort directories and files
+ structure.Directories.Sort(Comparer.Create((left, right) => StringComparer.Ordinal.Compare(GetNormalizedPath(Path.GetRelativePath(directory.FullName, left.FullName)), GetNormalizedPath(Path.GetRelativePath(directory.FullName, right.FullName)))));
+ structure.Files.Sort(Comparer.Create((left, right) => StringComparer.Ordinal.Compare(GetNormalizedPath(Path.GetRelativePath(directory.FullName, left.FullName)), GetNormalizedPath(Path.GetRelativePath(directory.FullName, right.FullName)))));
+
+ // Create HMACSHA256
+ using var hmac = new HMACSHA256(key);
+
+ // Hash directory paths
+ foreach (var innerDirectory in structure.Directories)
+ {
+ var path = GetNormalizedPath(Path.GetRelativePath(directory.FullName, innerDirectory.FullName));
+ var bytes = Encoding.UTF8.GetBytes(path);
+ hmac.TransformBlock(bytes, 0, bytes.Length, null, 0);
+ }
+
+ // Rent a buffer
+ var buffer = ArrayPool.Shared.Rent(BufferSize);
+ try
+ {
+ // Hash file paths, sizes and contents
+ foreach (var file in structure.Files)
+ {
+ var path = GetNormalizedPath(Path.GetRelativePath(directory.FullName, file.FullName));
+ var bytes = (byte[])[.. Encoding.UTF8.GetBytes(path), .. BitConverter.GetBytes(file.Length)];
+ hmac.TransformBlock(bytes, 0, bytes.Length, null, 0);
+
+ await using var stream = file.OpenRead();
+ while (true)
+ {
+ var result = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+ if (result is 0) break;
+ hmac.TransformBlock(buffer, 0, result, null, 0);
+ }
+ }
+ }
+ finally
+ {
+ // Return the buffer
+ ArrayPool.Shared.Return(buffer);
+ }
+
+ // Compute HMACSHA256 and returns the hash
+ _ = hmac.TransformFinalBlock([], 0, 0);
+ var hash = hmac.Hash;
+ Debug.Assert(hash is not null, "Result hash should not be null");
+ return hash;
+ }
+
+ /// Reads 's structure.
+ /// Directory to read the structure from.
+ /// Cancellation token.
+ /// .
+ private static DirectoryStructure ReadDirectoryStructure(DirectoryInfo directory, CancellationToken cancellationToken)
+ {
+ // This method doesn't do anything by itself, it simply creates a new empty
+ // structure then call ReadDirectoryStructureCore to do all the work recursively.
+ var result = new DirectoryStructure();
+ ReadDirectoryStructureCore(directory, result, cancellationToken);
+ return result;
+ }
+
+ /// Inner functionality for .
+ /// Directory to read the structure from.
+ /// Directory structure to add into.
+ /// Cancellation token.
+ private static void ReadDirectoryStructureCore(DirectoryInfo directory, DirectoryStructure structure, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Add all files into the structure
+ structure.Files.AddRange(directory.GetFiles());
+
+ // Add all directories into the structure
+ var directories = directory.GetDirectories();
+ structure.Directories.AddRange(directories);
+
+ // Recurse into all directories
+ foreach (var innerDirectory in directories)
+ {
+ ReadDirectoryStructureCore(innerDirectory, structure, cancellationToken);
+ }
+ }
+
+ /// Normalize for hashing purposes.
+ /// Path to normalize.
+ /// Normalized .
+ private static string GetNormalizedPath(string path) => path.Replace(Path.DirectorySeparatorChar, NormalizationPathSeparator);
+
+ /// Contains the directory structure.
+ /// Used internally only.
+ private sealed class DirectoryStructure
+ {
+ /// Gets all directories found recursively.
+ public List Directories { get; } = [];
+
+ /// Gets all files found recursively.
+ public List Files { get; } = [];
+ }
+}