From 75201e827648f09883f10d3d17fbecfc59e9dace Mon Sep 17 00:00:00 2001 From: AzureGem Date: Mon, 17 Nov 2025 01:02:51 -0500 Subject: [PATCH] Add IntegrityUtils --- Dalamud/Utility/IntegrityUtils.cs | 133 ++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 Dalamud/Utility/IntegrityUtils.cs 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; } = []; + } +}