// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.Buffers.Binary; using System.ComponentModel; using System.Globalization; using System.IO; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; namespace EpicGames.Core { /// /// Struct representing a strongly typed IoHash value (a 20-byte Blake3 hash). /// [JsonConverter(typeof(IoHashJsonConverter))] [TypeConverter(typeof(IoHashTypeConverter))] public readonly struct IoHash(ulong a, ulong b, uint c) : IEquatable, IComparable { /// /// Length of an IoHash /// public const int NumBytes = 20; /// /// Length of the hash in bits /// public const int NumBits = NumBytes * 8; /// /// Threshold size at which to use multiple threads for hashing /// const int MultiThreadedSize = 1_000_000; readonly ulong _a = a; readonly ulong _b = b; readonly uint _c = c; /// /// Hash consisting of zeroes /// public static IoHash Zero { get; } = new IoHash(0, 0, 0); /// /// Constructor /// /// Memory to construct from public IoHash(ReadOnlySpan span) : this(BinaryPrimitives.ReadUInt64BigEndian(span), BinaryPrimitives.ReadUInt64BigEndian(span.Slice(8)), BinaryPrimitives.ReadUInt32BigEndian(span.Slice(16))) { } /// /// Construct /// /// The hasher to construct from public static IoHash FromBlake3(Blake3.Hasher hasher) { Span output = stackalloc byte[32]; hasher.Finalize(output); return new IoHash(output); } /// /// Creates the IoHash for a block of data. /// /// Data to compute the hash for /// New hash instance containing the hash of the data public static IoHash Compute(ReadOnlySpan data) { Span output = stackalloc byte[32]; using (Blake3.Hasher hasher = Blake3.Hasher.New()) { if (data.Length < MultiThreadedSize) { hasher.Update(data); } else { hasher.UpdateWithJoin(data); } hasher.Finalize(output); } return new IoHash(output); } /// /// Creates the IoHash for a block of data. /// /// Data to compute the hash for /// New hash instance containing the hash of the data public static IoHash Compute(ReadOnlySequence sequence) { if (sequence.IsSingleSegment) { return Compute(sequence.FirstSpan); } using (Blake3.Hasher hasher = Blake3.Hasher.New()) { foreach (ReadOnlyMemory segment in sequence) { if (segment.Length < MultiThreadedSize) { hasher.Update(segment.Span); } else { hasher.UpdateWithJoin(segment.Span); } } return FromBlake3(hasher); } } /// /// Creates the IoHash for a string with a given encoding /// /// Data to compute the hash for /// The character encoding of the data /// New hash instance containing the hash of the data public static IoHash Compute(string data, Encoding encoding) => Compute(encoding.GetBytes(data)); /// /// Creates the IoHash for a string with the default encoding /// /// Data to compute the hash for /// New hash instance containing the hash of the data public static IoHash Compute(string data) => Compute(data, Encoding.Default); /// /// Creates the IoHash for a stream. /// /// Data to compute the hash for /// New content hash instance containing the hash of the data public static IoHash Compute(Stream stream) { using (Blake3.Hasher hasher = Blake3.Hasher.New()) { Span buffer = stackalloc byte[16384]; int length; while ((length = stream.Read(buffer)) > 0) { hasher.Update(buffer.Slice(0, length)); } return FromBlake3(hasher); } } /// /// Creates the IoHash for a stream asynchronously. /// /// Data to compute the hash for /// Cancellation token for the operation /// New content hash instance containing the hash of the data public static async Task ComputeAsync(Stream stream, CancellationToken cancellationToken = default) => await ComputeAsync(stream, -1, cancellationToken); /// /// Creates the IoHash for a stream asynchronously. /// /// Data to compute the hash for /// If available, the file size so an appropriate buffer size can be used /// Cancellation token used to terminate processing /// New content hash instance containing the hash of the data public static async Task ComputeAsync(Stream stream, long fileSizeHint, CancellationToken cancellationToken = default) { const int MaxBufferSize = 1 * 1024 * 1024; const int MinBufferSize = 16 * 1024; using (Blake3.Hasher hasher = Blake3.Hasher.New()) { Task Callback(ReadOnlyMemory data) { hasher.UpdateWithJoin(data.Span); return Task.CompletedTask; } await stream.ReadAllBytesAsync(fileSizeHint, MinBufferSize, MaxBufferSize, Callback, cancellationToken); return FromBlake3(hasher); } } /// /// Parses a digest from the given hex string /// /// /// public static IoHash Parse(string text) { return new IoHash(StringUtils.ParseHexString(text)); } /// /// Parses a digest from the given hex string /// /// /// public static IoHash Parse(ReadOnlySpan text) { return new IoHash(StringUtils.ParseHexString(text)); } /// /// Parses a digest from the given hex string /// /// /// public static IoHash Parse(ReadOnlySpan text) { return new IoHash(StringUtils.ParseHexString(text)); } /// /// Parses a digest from the given hex string /// /// /// Receives the hash on success /// public static bool TryParse(ReadOnlySpan text, out IoHash hash) { byte[]? bytes; if (StringUtils.TryParseHexString(text, out bytes) && bytes.Length == IoHash.NumBytes) { hash = new IoHash(bytes); return true; } else { hash = default; return false; } } /// /// Parses a digest from the given hex string /// /// /// Receives the hash on success /// public static bool TryParse(ReadOnlySpan text, out IoHash hash) { byte[]? bytes; if (StringUtils.TryParseHexString(text, out bytes) && bytes.Length == IoHash.NumBytes) { hash = new IoHash(bytes); return true; } else { hash = default; return false; } } /// public int CompareTo(IoHash other) { if (_a != other._a) { return (_a < other._a) ? -1 : +1; } else if (_b != other._b) { return (_b < other._b) ? -1 : +1; } else { return (_c < other._c) ? -1 : +1; } } /// public bool Equals(IoHash other) => _a == other._a && _b == other._b && _c == other._c; /// public override bool Equals(object? obj) => (obj is IoHash hash) && Equals(hash); /// public override int GetHashCode() => (int)_a; /// /// Format the hash as a utf8 string /// public Utf8String ToUtf8String() { Span buffer = stackalloc byte[IoHash.NumBytes]; CopyTo(buffer); return StringUtils.FormatUtf8HexString(buffer); } /// /// Formats the hash as a utf8 string /// /// Output buffer for the converted string public void ToUtf8String(Span chars) { Span buffer = stackalloc byte[IoHash.NumBytes]; CopyTo(buffer); StringUtils.FormatUtf8HexString(buffer, chars); } /// public override string ToString() { Span buffer = stackalloc byte[IoHash.NumBytes]; CopyTo(buffer); return StringUtils.FormatHexString(buffer); } /// /// Convert this hash to a byte array /// /// Data for the hash public byte[] ToByteArray() { byte[] data = new byte[NumBytes]; CopyTo(data); return data; } /// /// Copies this hash into a span /// /// public void CopyTo(Span span) { BinaryPrimitives.WriteUInt64BigEndian(span, _a); BinaryPrimitives.WriteUInt64BigEndian(span[8..], _b); BinaryPrimitives.WriteUInt32BigEndian(span[16..], _c); } /// /// Test two hash values for equality /// public static bool operator ==(IoHash a, IoHash b) => a.Equals(b); /// /// Test two hash values for equality /// public static bool operator !=(IoHash a, IoHash b) => !(a == b); /// /// Tests whether A > B /// public static bool operator >(IoHash a, IoHash b) => a.CompareTo(b) > 0; /// /// Tests whether A is less than B /// public static bool operator <(IoHash a, IoHash b) => a.CompareTo(b) < 0; /// /// Tests whether A is greater than or equal to B /// public static bool operator >=(IoHash a, IoHash b) => a.CompareTo(b) >= 0; /// /// Tests whether A is less than or equal to B /// public static bool operator <=(IoHash a, IoHash b) => a.CompareTo(b) <= 0; /// /// Convert a Blake3Hash to an IoHash /// /// public static implicit operator IoHash(Blake3Hash hash) { return new IoHash(hash.Span.Slice(0, NumBytes)); } } /// /// Extension methods for dealing with IoHash values /// public static class IoHashExtensions { /// /// Read an from a binary archive /// /// Reader to serialize data from /// New IoHash instance public static IoHash? ReadIoHash(this BinaryArchiveReader reader) { byte[]? data = reader.ReadByteArray(); return data == null ? null : new IoHash(data); } /// /// Read an from a memory reader /// /// Reader to serialize data from /// New IoHash instance public static IoHash ReadIoHash(this IMemoryReader reader) { return new IoHash(reader.ReadFixedLengthBytes(IoHash.NumBytes).Span); } /// /// Write an to a binary archive /// /// The writer to output data to /// The IoHash to write public static void WriteIoHash(this BinaryArchiveWriter writer, IoHash? hash) { writer.WriteByteArray(hash?.ToByteArray()); } /// /// Write an to a memory writer /// /// The writer to output data to /// The IoHash to write public static void WriteIoHash(this IMemoryWriter writer, IoHash hash) { hash.CopyTo(writer.GetSpan(IoHash.NumBytes)); writer.Advance(IoHash.NumBytes); } } /// /// Type converter for IoHash to and from JSON /// sealed class IoHashJsonConverter : JsonConverter { /// public override IoHash Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => IoHash.Parse(reader.ValueSpan); /// public override void Write(Utf8JsonWriter writer, IoHash value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToUtf8String().Span); } /// /// Type converter from strings to IoHash objects /// sealed class IoHashTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } /// public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return IoHash.Parse((string)value); } } }