// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.ComponentModel; using System.Globalization; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using EpicGames.Core; using EpicGames.Serialization; namespace EpicGames.Horde.Storage { /// /// Identifier for a ref in the storage system. Refs serve as GC roots, and are persistent entry points to expanding data structures within the store. /// [JsonSchemaString] [JsonConverter(typeof(RefNameJsonConverter))] [TypeConverter(typeof(RefNameTypeConverter))] [CbConverter(typeof(RefNameCbConverter))] public readonly struct RefName : IEquatable, IComparable { /// /// Empty ref name /// public static RefName Empty { get; } = default; /// /// String for the ref name /// public Utf8String Text { get; } /// /// Constructor /// /// public RefName(string text) : this(new Utf8String(text)) { } /// /// Constructor /// /// public RefName(Utf8String text) { Text = text; ValidatePathArgument(text.Span, nameof(text)); } /// /// Validates a given string as a blob id /// /// String to validate /// Name of the argument public static void ValidatePathArgument(ReadOnlySpan text, string argumentName) { if (text.Length == 0) { throw new ArgumentException("Ref names cannot be empty", argumentName); } if (text[^1] == '/') { throw new ArgumentException($"{Encoding.UTF8.GetString(text)} is not a valid ref name (cannot start or end with a slash)", argumentName); } int lastSlashIdx = -1; for (int idx = 0; idx < text.Length; idx++) { if (text[idx] == '/') { if (lastSlashIdx == idx - 1) { throw new ArgumentException($"{Encoding.UTF8.GetString(text)} is not a valid ref name (leading and consecutive slashes are not permitted)", argumentName); } else { lastSlashIdx = idx; } } else if (text[idx] == '.') { if (idx == 0 || text[idx - 1] == '/') { throw new ArgumentException($"{Encoding.UTF8.GetString(text)} is not a valid ref name (path fragment cannot start with a period)", argumentName); } if (idx + 1 == text.Length || text[idx + 1] == '/') { throw new ArgumentException($"{Encoding.UTF8.GetString(text)} is not a valid ref name (path fragment cannot end with a period)", argumentName); } } else { if (!IsValidChar(text[idx])) { throw new ArgumentException($"{Encoding.UTF8.GetString(text)} is not a valid ref name ('{(char)text[idx]}' is an invalid character)", argumentName); } } } } static readonly uint[] s_validChars = CreateValidCharsArray(); static uint[] CreateValidCharsArray() { const string ValidChars = "0123456789abcdefghijklmnopqrstuvwxyz_/-+."; uint[] validChars = new uint[256 / 8]; for (int idx = 0; idx < ValidChars.Length; idx++) { int index = ValidChars[idx]; validChars[index / 32] |= 1U << (index & 31); } return validChars; } static bool IsValidChar(byte character) { return (s_validChars[character / 32] & (1U << (character & 31))) != 0; } /// public override bool Equals(object? obj) => obj is RefName refId && Equals(refId); /// public override int GetHashCode() => Text.GetHashCode(); /// public bool Equals(RefName refName) => Text == refName.Text; /// public int CompareTo(RefName other) => Text.CompareTo(other.Text); /// public override string ToString() => Text.ToString(); /// public static bool operator ==(RefName lhs, RefName rhs) => lhs.Equals(rhs); /// public static bool operator !=(RefName lhs, RefName rhs) => !lhs.Equals(rhs); /// public static bool operator <(RefName lhs, RefName rhs) => lhs.CompareTo(rhs) < 0; /// public static bool operator <=(RefName lhs, RefName rhs) => lhs.CompareTo(rhs) <= 0; /// public static bool operator >(RefName lhs, RefName rhs) => lhs.CompareTo(rhs) > 0; /// public static bool operator >=(RefName lhs, RefName rhs) => lhs.CompareTo(rhs) >= 0; /// /// Construct a ref from a string /// /// Name of the ref public static implicit operator RefName(string name) => new RefName(name); } /// /// Type converter for IoHash to and from JSON /// sealed class RefNameJsonConverter : JsonConverter { /// public override RefName Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new RefName(new Utf8String(reader.ValueSpan.ToArray())); /// public override void Write(Utf8JsonWriter writer, RefName value, JsonSerializerOptions options) => writer.WriteStringValue(value.Text.Span); } /// /// Type converter from strings to IoHash objects /// sealed class RefNameTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } /// public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) { return new RefName((string)value!); } } /// /// Type converter to compact binary /// sealed class RefNameCbConverter : CbConverter { /// public override RefName Read(CbField field) => new RefName(field.AsUtf8String()); /// public override void Write(CbWriter writer, RefName value) => writer.WriteUtf8StringValue(value.Text); /// public override void WriteNamed(CbWriter writer, CbFieldName name, RefName value) => writer.WriteUtf8String(name, value.Text); } }