// 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;
}
}
}