You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.6 KiB
209 lines
6.6 KiB
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<string> GetHashAsync( |
|
HashAlgorithm hashAlgorithm, |
|
Stream stream, |
|
byte[] buffer, |
|
Action<ulong>? 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<string> GetSha256Async(string filePath, IProgress<ProgressReport>? 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<byte>.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<string> GetBlake3Async(string filePath, IProgress<ProgressReport>? 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<byte>.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<Hash> GetBlake3Async(Stream stream, IProgress<ProgressReport>? progress = default) |
|
{ |
|
var totalBytes = Convert.ToUInt64(stream.Length); |
|
var readBytes = 0ul; |
|
var shared = ArrayPool<byte>.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); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Get the Blake3 hash of a span of data with multi-threading. |
|
/// </summary> |
|
public static Hash GetBlake3Parallel(ReadOnlySpan<byte> data) |
|
{ |
|
using var hasher = Hasher.New(); |
|
hasher.UpdateWithJoin(data); |
|
return hasher.Finalize(); |
|
} |
|
|
|
/// <summary> |
|
/// Task.Run wrapped <see cref="GetBlake3Parallel"/> |
|
/// </summary> |
|
public static Task<Hash> GetBlake3ParallelAsync(ReadOnlyMemory<byte> data) |
|
{ |
|
return Task.Run(() => GetBlake3Parallel(data.Span)); |
|
} |
|
|
|
/// <summary> |
|
/// Get the Blake3 hash of a file as memory-mapped with multi-threading. |
|
/// </summary> |
|
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(); |
|
} |
|
|
|
/// <summary> |
|
/// Task.Run wrapped <see cref="GetBlake3MemoryMappedParallel"/> |
|
/// </summary> |
|
public static Task<Hash> GetBlake3MemoryMappedParallelAsync(string filePath) |
|
{ |
|
return Task.Run(() => GetBlake3MemoryMappedParallel(filePath)); |
|
} |
|
|
|
/// <summary> |
|
/// Determines suitable buffer size for hashing based on stream length. |
|
/// </summary> |
|
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 |
|
}; |
|
}
|
|
|