// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; namespace EpicGames.Core { /// /// Represents a memory region which can be treated as a utf-8 string. /// public struct Utf8String : IEquatable, IComparable { /// /// An empty string /// public static readonly Utf8String Empty = new Utf8String(); /// /// The data represented by this string /// public ReadOnlyMemory Memory { get; } /// /// Returns read only span for this string /// public ReadOnlySpan Span { get { return Memory.Span; } } /// /// Determines if this string is empty /// public bool IsEmpty { get { return Memory.IsEmpty; } } /// /// Returns the length of this string /// public int Length { get { return Memory.Length; } } /// /// Allows indexing individual bytes of the data /// /// Byte index /// Byte at the given index public byte this[int Index] { get { return Span[Index]; } } /// /// Constructor /// /// Text to construct from public Utf8String(string Text) { this.Memory = Encoding.UTF8.GetBytes(Text); } /// /// Constructor /// /// The data to construct from public Utf8String(ReadOnlyMemory Memory) { this.Memory = Memory; } /// /// Constructor /// /// The buffer to construct from /// Offset within the buffer /// Length of the string within the buffer public Utf8String(byte[] Buffer, int Offset, int Length) { this.Memory = new ReadOnlyMemory(Buffer, Offset, Length); } /// /// Duplicate this string /// /// public Utf8String Clone() { byte[] NewBuffer = new byte[Memory.Length]; Memory.CopyTo(NewBuffer); return new Utf8String(NewBuffer); } /// /// Tests two strings for equality /// /// The first string to compare /// The second string to compare /// True if the strings are equal public static bool operator ==(Utf8String A, Utf8String B) { return A.Equals(B); } /// /// Tests two strings for inequality /// /// The first string to compare /// The second string to compare /// True if the strings are not equal public static bool operator !=(Utf8String A, Utf8String B) { return !A.Equals(B); } /// public bool Equals(Utf8String Other) => Utf8StringComparer.Ordinal.Equals(Span, Other.Span); /// public int CompareTo(Utf8String Other) => Utf8StringComparer.Ordinal.Compare(Span, Other.Span); /// public bool Contains(Utf8String String) => IndexOf(String) != -1; /// public bool Contains(Utf8String String, Utf8StringComparer Comparer) => IndexOf(String, Comparer) != -1; /// public int IndexOf(byte Char) { return Span.IndexOf(Char); } /// public int IndexOf(char Char) { if (Char < 0x80) { return Span.IndexOf((byte)Char); } else { return Span.IndexOf(Encoding.UTF8.GetBytes(new[] { Char })); } } /// public int IndexOf(char Char, int Index) => IndexOf(Char, Index, Length - Index); /// public int IndexOf(char Char, int Index, int Count) { int Result; if (Char < 0x80) { Result = Span.Slice(Index, Count).IndexOf((byte)Char); } else { Result = Span.Slice(Index, Count).IndexOf(Encoding.UTF8.GetBytes(new[] { Char })); } return (Result == -1) ? -1 : Result + Index; } /// public int IndexOf(Utf8String String) { return Span.IndexOf(String.Span); } /// public int IndexOf(Utf8String String, Utf8StringComparer Comparer) { for (int Idx = 0; Idx < Length - String.Length; Idx++) { if (Comparer.Equals(String.Slice(Idx, String.Length), String)) { return Idx; } } return -1; } /// public int LastIndexOf(byte Char) { return Span.IndexOf(Char); } /// public int LastIndexOf(char Char) { if (Char < 0x80) { return Span.IndexOf((byte)Char); } else { return Span.IndexOf(Encoding.UTF8.GetBytes(new[] { Char })); } } /// /// Tests if this string starts with another string /// /// The string to check against /// True if this string starts with the other string public bool StartsWith(Utf8String Other) { return Span.StartsWith(Other.Span); } /// /// Tests if this string ends with another string /// /// The string to check against /// The string comparer /// True if this string ends with the other string public bool StartsWith(Utf8String Other, Utf8StringComparer Comparer) { return Length >= Other.Length && Comparer.Equals(Slice(0, Other.Length), Other); } /// /// Tests if this string ends with another string /// /// The string to check against /// True if this string ends with the other string public bool EndsWith(Utf8String Other) { return Span.EndsWith(Other.Span); } /// /// Tests if this string ends with another string /// /// The string to check against /// The string comparer /// True if this string ends with the other string public bool EndsWith(Utf8String Other, Utf8StringComparer Comparer) { return Length >= Other.Length && Comparer.Equals(Slice(Length - Other.Length), Other); } /// public Utf8String Slice(int Start) => Substring(Start); /// public Utf8String Slice(int Start, int Count) => Substring(Start, Count); /// public Utf8String Substring(int Start) { return new Utf8String(Memory.Slice(Start)); } /// public Utf8String Substring(int Start, int Count) { return new Utf8String(Memory.Slice(Start, Count)); } /// /// Tests if this string is equal to the other object /// /// Object to compare to /// True if the objects are equivalent public override bool Equals(object? obj) { Utf8String? Other = obj as Utf8String?; return Other != null && Equals(Other.Value); } /// /// Returns the hash code of this string /// /// Hash code for the string public override int GetHashCode() => Utf8StringComparer.Ordinal.GetHashCode(Span); /// /// Gets the string represented by this data /// /// The utf-8 string public override string ToString() { return Encoding.UTF8.GetString(Span); } /// /// Parse a string as an unsigned integer /// /// /// public static uint ParseUnsignedInt(Utf8String Text) { ReadOnlySpan Bytes = Text.Span; if (Bytes.Length == 0) { throw new Exception("Cannot parse empty string as an integer"); } uint Value = 0; for (int Idx = 0; Idx < Bytes.Length; Idx++) { uint Digit = (uint)(Bytes[Idx] - '0'); if (Digit > 9) { throw new Exception($"Cannot parse '{Text}' as an integer"); } Value = (Value * 10) + Digit; } return Value; } /// /// Appends two strings /// /// /// /// public static Utf8String operator +(Utf8String A, Utf8String B) { if (A.Length == 0) { return B; } if (B.Length == 0) { return A; } byte[] Buffer = new byte[A.Length + B.Length]; A.Span.CopyTo(Buffer); B.Span.CopyTo(Buffer.AsSpan(A.Length)); return new Utf8String(Buffer); } /// /// Converts a string to a utf-8 string /// /// Text to convert public static implicit operator Utf8String(string Text) { return new Utf8String(new ReadOnlyMemory(Encoding.UTF8.GetBytes(Text))); } } /// /// Comparison classes for utf8 strings /// public abstract class Utf8StringComparer : IEqualityComparer, IComparer { /// /// Ordinal comparer for utf8 strings /// public sealed class OrdinalComparer : Utf8StringComparer { /// public override bool Equals(ReadOnlySpan StrA, ReadOnlySpan StrB) { return StrA.SequenceEqual(StrB); } /// public override int GetHashCode(ReadOnlySpan String) { int Hash = 5381; for (int Idx = 0; Idx < String.Length; Idx++) { Hash += (Hash << 5) + String[Idx]; } return Hash; } public override int Compare(ReadOnlySpan StrA, ReadOnlySpan StrB) { return StrA.SequenceCompareTo(StrB); } } /// /// Comparison between ReadOnlyUtf8String objects that ignores case for ASCII characters /// public sealed class OrdinalIgnoreCaseComparer : Utf8StringComparer { /// public override bool Equals(ReadOnlySpan StrA, ReadOnlySpan StrB) { if (StrA.Length != StrB.Length) { return false; } for (int Idx = 0; Idx < StrA.Length; Idx++) { if (StrA[Idx] != StrB[Idx] && ToUpper(StrA[Idx]) != ToUpper(StrB[Idx])) { return false; } } return true; } /// public override int GetHashCode(ReadOnlySpan String) { HashCode HashCode = new HashCode(); for (int Idx = 0; Idx < String.Length; Idx++) { HashCode.Add(ToUpper(String[Idx])); } return HashCode.ToHashCode(); } /// public override int Compare(ReadOnlySpan SpanA, ReadOnlySpan SpanB) { int Length = Math.Min(SpanA.Length, SpanB.Length); for (int Idx = 0; Idx < Length; Idx++) { if (SpanA[Idx] != SpanB[Idx]) { int UpperA = ToUpper(SpanA[Idx]); int UpperB = ToUpper(SpanB[Idx]); if (UpperA != UpperB) { return UpperA - UpperB; } } } return SpanA.Length - SpanB.Length; } /// /// Convert a character to uppercase /// /// Character to convert /// The uppercase version of the character static byte ToUpper(byte Character) { return (Character >= 'a' && Character <= 'z') ? (byte)(Character - 'a' + 'A') : Character; } } /// /// Static instance of the ordinal utf8 ordinal comparer /// public static Utf8StringComparer Ordinal { get; } = new OrdinalComparer(); /// /// Static instance of the case-insensitive utf8 ordinal string comparer /// public static Utf8StringComparer OrdinalIgnoreCase { get; } = new OrdinalIgnoreCaseComparer(); /// public bool Equals(Utf8String StrA, Utf8String StrB) => Equals(StrA.Span, StrB.Span); /// public abstract bool Equals(ReadOnlySpan StrA, ReadOnlySpan StrB); /// public int GetHashCode(Utf8String String) => GetHashCode(String.Span); /// public abstract int GetHashCode(ReadOnlySpan String); /// public int Compare(Utf8String StrA, Utf8String StrB) => Compare(StrA.Span, StrB.Span); /// public abstract int Compare(ReadOnlySpan StrA, ReadOnlySpan StrB); } /// /// Extension methods for ReadOnlyUtf8String objects /// public static class MemoryWriterExtensions { /// /// Reads a null-terminated utf8 string from the buffer /// /// The string data public static Utf8String ReadString(this MemoryReader Reader) { ReadOnlySpan Span = Reader.Span; int Length = Span.IndexOf((byte)0); Utf8String Value = new Utf8String(Reader.ReadFixedLengthBytes(Length)); Reader.ReadInt8(); return Value; } /// /// Writes a UTF8 string into memory with a null terminator /// /// The memory writer to serialize to /// String to write public static void WriteString(this MemoryWriter Writer, Utf8String String) { Writer.WriteFixedLengthBytes(String.Span); Writer.WriteInt8(0); } /// /// Determines the size of a serialized utf-8 string /// /// The string to measure /// Size of the serialized string public static int GetSerializedSize(this Utf8String String) { return String.Length + 1; } } }