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