// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers.Binary; using System.ComponentModel; using System.Globalization; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using EpicGames.Core; namespace EpicGames.Horde { /// /// Normalized string identifier for a resource /// [JsonSchemaString] [JsonConverter(typeof(BinaryIdJsonConverter))] [TypeConverter(typeof(BinaryIdTypeConverter))] public readonly struct BinaryId : IEquatable, IComparable { readonly int _a; readonly int _b; readonly int _c; /// /// Number of bytes in the identifier /// public const int NumBytes = 12; /// /// Number of characters when formatted as a string /// public const int NumChars = NumBytes * 2; /// /// Constructor /// /// Bytes to parse public BinaryId(ReadOnlySpan bytes) { _a = BinaryPrimitives.ReadInt32LittleEndian(bytes); _b = BinaryPrimitives.ReadInt32LittleEndian(bytes[4..]); _c = BinaryPrimitives.ReadInt32LittleEndian(bytes[8..]); } /// /// Parse a binary id from a string /// public static BinaryId Parse(string text) { BinaryId id; if (!TryParse(text, out id)) { throw new FormatException($"Invalid BinaryId: {text}"); } return id; } /// /// Parse a binary id from a string /// public static BinaryId Parse(Utf8String text) => Parse(text.Span); /// /// Parse a binary id from a string /// public static BinaryId Parse(ReadOnlySpan text) { BinaryId id; if (!TryParse(text, out id)) { throw new FormatException($"Invalid BinaryId: {Encoding.UTF8.GetString(text)}"); } return id; } /// /// Attempt to parse a binary id from a string /// public static bool TryParse(string text, out BinaryId result) { Span bytes = stackalloc byte[NumBytes]; if (StringUtils.TryParseHexString(text, bytes)) { result = new BinaryId(bytes); return true; } else { result = default; return false; } } /// /// Attempt to parse a binary id from a string /// public static bool TryParse(Utf8String text, out BinaryId result) => TryParse(text.Span, out result); /// /// Attempt to parse a binary id from a string /// public static bool TryParse(ReadOnlySpan text, out BinaryId result) { Span bytes = stackalloc byte[NumBytes]; if (StringUtils.TryParseHexString(text, bytes)) { result = new BinaryId(bytes); return true; } else { result = default; return false; } } /// /// Attempt to parse a binary id from a string /// public static bool TryParse(ReadOnlySpan text, out BinaryId result) { Span bytes = stackalloc byte[NumBytes]; if (StringUtils.TryParseHexString(text, bytes)) { result = new BinaryId(bytes); return true; } else { result = default; return false; } } /// /// Checks whether this BinaryId is set /// public bool IsEmpty => (_a | _b | _c) == 0; /// public override bool Equals(object? obj) => obj is BinaryId id && Equals(id); /// public override int GetHashCode() => HashCode.Combine(_a, _b, _c); /// public bool Equals(BinaryId other) => _a == other._a && _b == other._b && _c == other._c; /// public override string ToString() { Span bytes = stackalloc byte[NumBytes]; ToByteArray(bytes); return StringUtils.FormatHexString(bytes); } /// /// Format this id as a sequence of UTF8 characters /// public Utf8String ToUtf8String() { byte[] data = new byte[NumChars]; ToUtf8String(data.AsSpan()); return new Utf8String(data); } /// /// Format this id as a sequence of UTF8 characters /// public void ToUtf8String(Span chars) { StringUtils.FormatLittleEndianUtf8HexString((uint)_a, chars); StringUtils.FormatLittleEndianUtf8HexString((uint)_b, chars[8..]); StringUtils.FormatLittleEndianUtf8HexString((uint)_c, chars[16..]); } /// /// Format this id as a sequence of UTF8 characters /// public byte[] ToByteArray() { byte[] bytes = new byte[NumBytes]; ToByteArray(bytes); return bytes; } /// /// Format this id as a sequence of UTF8 characters /// public void ToByteArray(Span bytes) { BinaryPrimitives.WriteInt32LittleEndian(bytes, _a); BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], _b); BinaryPrimitives.WriteInt32LittleEndian(bytes[8..], _c); } /// public int CompareTo(BinaryId other) { int result = _a.CompareTo(other._a); if (result == 0) { result = _b.CompareTo(other._b); if (result == 0) { result = _c.CompareTo(other._c); } } return result; } /// /// Compares two binary ids for equality /// /// The first string id /// Second string id /// True if the two string ids are equal public static bool operator ==(BinaryId left, BinaryId right) => left.Equals(right); /// /// Compares two binary ids for inequality /// /// The first string id /// Second string id /// True if the two string ids are not equal public static bool operator !=(BinaryId left, BinaryId right) => !left.Equals(right); #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public static bool operator <(BinaryId left, BinaryId right) => left.CompareTo(right) < 0; public static bool operator <=(BinaryId left, BinaryId right) => left.CompareTo(right) <= 0; public static bool operator >(BinaryId left, BinaryId right) => left.CompareTo(right) > 0; public static bool operator >=(BinaryId left, BinaryId right) => left.CompareTo(right) >= 0; #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } /// /// Class which serializes types /// sealed class BinaryIdJsonConverter : JsonConverter { /// public override BinaryId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => BinaryId.Parse(reader.GetUtf8String()); /// public override void Write(Utf8JsonWriter writer, BinaryId value, JsonSerializerOptions options) { Span bytes = stackalloc byte[BinaryId.NumChars]; value.ToUtf8String(bytes); writer.WriteStringValue(bytes); } } /// /// Class which serializes types /// sealed class BinaryIdTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); /// public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => BinaryId.Parse((string)value); /// public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string); /// public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => ((BinaryId)value!).ToString(); } }