// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; namespace EpicGames.Core { /// /// Represents a memory region which can be treated as a utf-8 string. /// public readonly 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 => Memory.Span; /// /// Determines if this string is empty /// public bool IsEmpty => Memory.IsEmpty; /// /// Returns the length of this string /// public int Length => Memory.Length; /// /// Allows indexing individual bytes of the data /// /// Byte index /// Byte at the given index public byte this[int index] => Span[index]; /// /// Constructor /// /// Text to construct from public Utf8String(string text) { Memory = Encoding.UTF8.GetBytes(text); } /// /// Constructor /// /// Text to construct from public Utf8String(ReadOnlySpan text) { int length = Encoding.UTF8.GetByteCount(text); byte[] buffer = new byte[length]; Encoding.UTF8.GetBytes(text, buffer); Memory = buffer; } /// /// Constructor /// /// The data to construct from public Utf8String(ReadOnlyMemory memory) { 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) { Memory = new ReadOnlyMemory(buffer, offset, length); } /// /// Duplicate this string /// /// public Utf8String Clone() { if (Memory.Length == 0) { return default; } 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 static bool operator <(Utf8String left, Utf8String right) => left.CompareTo(right) < 0; /// public static bool operator <=(Utf8String left, Utf8String right) => left.CompareTo(right) <= 0; /// public static bool operator >(Utf8String left, Utf8String right) => left.CompareTo(right) > 0; /// public static bool operator >=(Utf8String left, Utf8String right) => left.CompareTo(right) >= 0; /// public bool Contains(Utf8String str) => IndexOf(str) != -1; /// public bool Contains(Utf8String str, Utf8StringComparer comparer) => IndexOf(str, comparer) != -1; /// public int IndexOf(byte character) { return Span.IndexOf(character); } /// public int IndexOf(char character) { if (character < 0x80) { return Span.IndexOf((byte)character); } else { return Span.IndexOf(Encoding.UTF8.GetBytes(new[] { character })); } } /// public int IndexOf(char character, int index) => IndexOf(character, index, Length - index); /// public int IndexOf(char character, int index, int count) { int result; if (character < 0x80) { result = Span.Slice(index, count).IndexOf((byte)character); } else { result = Span.Slice(index, count).IndexOf(Encoding.UTF8.GetBytes(new[] { character })); } return (result == -1) ? -1 : result + index; } /// public int IndexOf(Utf8String str) { return Span.IndexOf(str.Span); } /// public int IndexOf(Utf8String str, Utf8StringComparer comparer) { for (int idx = 0; idx < Length - str.Length; idx++) { if (comparer.Equals(str.Slice(idx, str.Length), str)) { return idx; } } return -1; } /// public int LastIndexOf(byte character) { return Span.LastIndexOf(character); } /// public int LastIndexOf(char character) { if (character < 0x80) { return Span.LastIndexOf((byte)character); } else { return Span.LastIndexOf(Encoding.UTF8.GetBytes(new[] { character })); } } /// /// 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); } /// /// Implict conversion to a span of bytes /// /// public static implicit operator ReadOnlySpan(Utf8String str) => str.Span; } /// /// Comparison classes for utf8 strings /// public abstract class Utf8StringComparer : IEqualityComparer, IComparer { /// /// Ordinal comparer for utf8 strings /// sealed class OrdinalComparer : Utf8StringComparer { /// public override bool Equals(ReadOnlySpan strA, ReadOnlySpan strB) { return strA.SequenceEqual(strB); } /// public override int GetHashCode(ReadOnlySpan str) { int hash = 5381; for (int idx = 0; idx < str.Length; idx++) { hash += (hash << 5) + str[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 /// 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 str) { HashCode hashCode = new HashCode(); for (int idx = 0; idx < str.Length; idx++) { hashCode.Add(ToUpper(str[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 str) => GetHashCode(str.Span); /// public abstract int GetHashCode(ReadOnlySpan str); /// 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 Utf8StringExtensions { /// /// Reads a null-terminated utf8 string from the buffer, without copying it /// /// The string data public static Utf8String ReadUtf8String(this IMemoryReader reader) { return ReadUtf8StringWithoutCopy(reader).Clone(); } /// /// Reads a null-terminated utf8 string from the buffer, without copying it /// /// The string data public static Utf8String ReadUtf8StringWithoutCopy(this IMemoryReader reader) { return new Utf8String(reader.ReadVariableLengthBytes()); } /// /// Reads a null-terminated utf8 string from the buffer, without copying it /// /// Writer to serialize to /// String to write public static void WriteUtf8String(this IMemoryWriter writer, Utf8String str) { writer.WriteVariableLengthBytes(str.Span); } /// /// Reads a null-terminated utf8 string from the buffer, without copying it /// /// The string data public static Utf8String ReadNullTerminatedUtf8String(this IMemoryReader reader) { int minSize = 1; for (; ; ) { ReadOnlyMemory memory = reader.GetMemory(minSize); int length = memory.Span.IndexOf((byte)0); if (length != -1) { Utf8String str = new Utf8String(memory.Slice(0, length)); reader.Advance(length + 1); return str; } minSize = memory.Length + 1; } } /// /// Writes a null-terminated utf8 string to the buffer /// /// Writer for the output data /// String to write public static void WriteNullTerminatedUtf8String(this IMemoryWriter writer, Utf8String str) { Span span = writer.GetSpan(str.Length + 1); str.Span.CopyTo(span); span[str.Length] = 0; writer.Advance(str.Length + 1); } /// /// Determines the size of a serialized utf-8 string /// /// The string to measure /// Size of the serialized string public static int GetNullTerminatedSize(this Utf8String str) { return str.Length + 1; } } }