using System.Buffers; using System.Diagnostics; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; using System.Security.Cryptography; using Blake3; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Helper; public static class FileHash { public static async Task GetHashAsync( HashAlgorithm hashAlgorithm, Stream stream, byte[] buffer, Action? progress = default ) { ulong totalBytesRead = 0; using (hashAlgorithm) { int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false)) != 0) { totalBytesRead += (ulong)bytesRead; hashAlgorithm.TransformBlock(buffer, 0, bytesRead, null, 0); progress?.Invoke(totalBytesRead); } hashAlgorithm.TransformFinalBlock(buffer, 0, 0); var hash = hashAlgorithm.Hash; if (hash == null || hash.Length == 0) { throw new InvalidOperationException("Hash algorithm did not produce a hash."); } return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); } } public static async Task GetSha256Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Could not find file: {filePath}"); } var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length); var shared = ArrayPool.Shared; var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes)); try { await using var stream = File.OpenRead(filePath); var hash = await GetHashAsync( SHA256.Create(), stream, buffer, totalBytesRead => { progress?.Report(new ProgressReport(totalBytesRead, totalBytes, type: ProgressType.Hashing)); } ) .ConfigureAwait(false); return hash; } finally { shared.Return(buffer); } } public static async Task GetBlake3Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Could not find file: {filePath}"); } var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length); var readBytes = 0ul; var shared = ArrayPool.Shared; var buffer = shared.Rent(GetBufferSize(totalBytes)); try { await using var stream = File.OpenRead(filePath); using var hasher = Hasher.New(); while (true) { var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); if (bytesRead == 0) { break; } readBytes += (ulong)bytesRead; hasher.Update(buffer.AsSpan(0, bytesRead)); progress?.Report(new ProgressReport(readBytes, totalBytes)); } return hasher.Finalize().ToString(); } finally { shared.Return(buffer); } } public static async Task GetBlake3Async(Stream stream, IProgress? progress = default) { var totalBytes = Convert.ToUInt64(stream.Length); var readBytes = 0ul; var shared = ArrayPool.Shared; var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes)); try { using var hasher = Hasher.New(); while (true) { var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); if (bytesRead == 0) { break; } readBytes += (ulong)bytesRead; hasher.Update(buffer.AsSpan(0, bytesRead)); progress?.Report(new ProgressReport(readBytes, totalBytes)); } return hasher.Finalize(); } finally { shared.Return(buffer); } } /// /// Get the Blake3 hash of a span of data with multi-threading. /// public static Hash GetBlake3Parallel(ReadOnlySpan data) { using var hasher = Hasher.New(); hasher.UpdateWithJoin(data); return hasher.Finalize(); } /// /// Task.Run wrapped /// public static Task GetBlake3ParallelAsync(ReadOnlyMemory data) { return Task.Run(() => GetBlake3Parallel(data.Span)); } /// /// Get the Blake3 hash of a file as memory-mapped with multi-threading. /// public static Hash GetBlake3MemoryMappedParallel(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } var totalBytes = Convert.ToInt64(new FileInfo(filePath).Length); using var hasher = Hasher.New(); // Memory map using var fileStream = File.OpenRead(filePath); using var memoryMappedFile = MemoryMappedFile.CreateFromFile( fileStream, null, totalBytes, MemoryMappedFileAccess.Read, HandleInheritability.None, false ); using var accessor = memoryMappedFile.CreateViewAccessor(0, totalBytes, MemoryMappedFileAccess.Read); Debug.Assert(accessor.Capacity == fileStream.Length); var buffer = new byte[accessor.Capacity]; accessor.ReadArray(0, buffer, 0, buffer.Length); hasher.UpdateWithJoin(buffer); return hasher.Finalize(); } /// /// Task.Run wrapped /// public static Task GetBlake3MemoryMappedParallelAsync(string filePath) { return Task.Run(() => GetBlake3MemoryMappedParallel(filePath)); } /// /// Determines suitable buffer size for hashing based on stream length. /// private static int GetBufferSize(ulong totalBytes) => totalBytes switch { < Size.MiB => 8 * (int)Size.KiB, < 500 * Size.MiB => 16 * (int)Size.KiB, < Size.GiB => 32 * (int)Size.KiB, _ => 64 * (int)Size.KiB }; }