You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
2) Enhancements to StringView and a few other helper classes 3) New UE style ignore case comparison 4) Moved shared UHT definitions into a common C# file. #rb jonathan.adamczewski #rnx #preflight 620e3cdc8b6428e701a24666 [CL 19033446 by Tim Smith in ue5-main branch]
639 lines
17 KiB
C#
639 lines
17 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace EpicGames.Core
|
|
{
|
|
public static class StringUtils
|
|
{
|
|
/// <summary>
|
|
/// Array mapping from ascii index to hexadecimal digits.
|
|
/// </summary>
|
|
static sbyte[] HexDigits;
|
|
|
|
/// <summary>
|
|
/// Hex digits to utf8 byte
|
|
/// </summary>
|
|
static byte[] HexDigitToUtf8Byte = Encoding.UTF8.GetBytes("0123456789abcdef");
|
|
|
|
/// <summary>
|
|
/// Array mapping human readable size of bytes, 1024^x. long max is within the range of Exabytes.
|
|
/// </summary>
|
|
static string[] ByteSizes = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
|
|
|
|
/// <summary>
|
|
/// Static constructor. Initializes the HexDigits array.
|
|
/// </summary>
|
|
static StringUtils()
|
|
{
|
|
HexDigits = new sbyte[256];
|
|
for (int Idx = 0; Idx < 256; Idx++)
|
|
{
|
|
HexDigits[Idx] = -1;
|
|
}
|
|
for (int Idx = '0'; Idx <= '9'; Idx++)
|
|
{
|
|
HexDigits[Idx] = (sbyte)(Idx - '0');
|
|
}
|
|
for (int Idx = 'a'; Idx <= 'f'; Idx++)
|
|
{
|
|
HexDigits[Idx] = (sbyte)(10 + Idx - 'a');
|
|
}
|
|
for (int Idx = 'A'; Idx <= 'F'; Idx++)
|
|
{
|
|
HexDigits[Idx] = (sbyte)(10 + Idx - 'A');
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indents a string by a given indent
|
|
/// </summary>
|
|
/// <param name="Text">The text to indent</param>
|
|
/// <param name="Indent">The indent to add to each line</param>
|
|
/// <returns>The indented string</returns>
|
|
public static string Indent(string Text, string Indent)
|
|
{
|
|
string Result = "";
|
|
if(Text.Length > 0)
|
|
{
|
|
Result = Indent + Text.Replace("\n", "\n" + Indent);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand all the property references (of the form $(PropertyName)) in a string.
|
|
/// </summary>
|
|
/// <param name="Text">The input string to expand properties in</param>
|
|
/// <param name="Properties">Dictionary of properties to expand</param>
|
|
/// <returns>The expanded string</returns>
|
|
public static string ExpandProperties(string Text, Dictionary<string, string> Properties)
|
|
{
|
|
return ExpandProperties(Text, Name => { Properties.TryGetValue(Name, out string? Value); return Value; });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand all the property references (of the form $(PropertyName)) in a string.
|
|
/// </summary>
|
|
/// <param name="Text">The input string to expand properties in</param>
|
|
/// <param name="GetPropertyValue">Delegate to retrieve a property value</param>
|
|
/// <returns>The expanded string</returns>
|
|
public static string ExpandProperties(string Text, Func<string, string?> GetPropertyValue)
|
|
{
|
|
string Result = Text;
|
|
for (int Idx = Result.IndexOf("$("); Idx != -1; Idx = Result.IndexOf("$(", Idx))
|
|
{
|
|
// Find the end of the variable name
|
|
int EndIdx = Result.IndexOf(')', Idx + 2);
|
|
if (EndIdx == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Extract the variable name from the string
|
|
string Name = Result.Substring(Idx + 2, EndIdx - (Idx + 2));
|
|
|
|
// Check if we've got a value for this variable
|
|
string? Value = GetPropertyValue(Name);
|
|
if (Value == null)
|
|
{
|
|
// Do not expand it; must be preprocessing the script.
|
|
Idx = EndIdx;
|
|
}
|
|
else
|
|
{
|
|
// Replace the variable, or skip past it
|
|
Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1);
|
|
|
|
// Make sure we skip over the expanded variable; we don't want to recurse on it.
|
|
Idx += Value.Length;
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/// <inheritdoc cref="WordWrap(string, int, int, int)">
|
|
public static IEnumerable<string> WordWrap(string Text, int MaxWidth)
|
|
{
|
|
return WordWrap(Text, 0, 0, MaxWidth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes a given sentence and wraps it on a word by word basis so that no line exceeds the set maximum line length. Words longer than a line
|
|
/// are broken up. Returns the sentence as a list of individual lines.
|
|
/// </summary>
|
|
/// <param name="Text">The text to be wrapped</param>
|
|
/// <param name="InitialIndent">Indent for the first line</param>
|
|
/// <param name="HangingIndent">Indent for subsequent lines</param>
|
|
/// <param name="MaxWidth">The maximum (non negative) length of the returned sentences</param>
|
|
public static IEnumerable<string> WordWrap(string Text, int InitialIndent, int HangingIndent, int MaxWidth)
|
|
{
|
|
StringBuilder Builder = new StringBuilder();
|
|
|
|
int MinIdx = 0;
|
|
for (int LineIdx = 0; MinIdx < Text.Length; LineIdx++)
|
|
{
|
|
int Indent = (LineIdx == 0) ? InitialIndent : HangingIndent;
|
|
int MaxWidthForLine = MaxWidth - Indent;
|
|
int MaxIdx = GetWordWrapLineEnd(Text, MinIdx, MaxWidthForLine);
|
|
|
|
int PrintMaxIdx = MaxIdx;
|
|
while (PrintMaxIdx > MinIdx && Char.IsWhiteSpace(Text[PrintMaxIdx - 1]))
|
|
{
|
|
PrintMaxIdx--;
|
|
}
|
|
|
|
Builder.Clear();
|
|
Builder.Append(' ', Indent);
|
|
Builder.Append(Text, MinIdx, PrintMaxIdx - MinIdx);
|
|
yield return Builder.ToString();
|
|
|
|
MinIdx = MaxIdx;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next character index to end a word-wrapped line on
|
|
/// </summary>
|
|
static int GetWordWrapLineEnd(string Text, int MinIdx, int MaxWidth)
|
|
{
|
|
MaxWidth = Math.Min(MaxWidth, Text.Length - MinIdx);
|
|
|
|
int MaxIdx = Text.IndexOf('\n', MinIdx, MaxWidth);
|
|
if (MaxIdx == -1)
|
|
{
|
|
MaxIdx = MinIdx + MaxWidth;
|
|
}
|
|
else
|
|
{
|
|
return MaxIdx + 1;
|
|
}
|
|
|
|
if (MaxIdx == Text.Length)
|
|
{
|
|
return MaxIdx;
|
|
}
|
|
else if (Char.IsWhiteSpace(Text[MaxIdx]))
|
|
{
|
|
for (; ; MaxIdx++)
|
|
{
|
|
if (MaxIdx == Text.Length)
|
|
{
|
|
return MaxIdx;
|
|
}
|
|
if (Text[MaxIdx] != ' ')
|
|
{
|
|
return MaxIdx;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int TryMaxIdx = MaxIdx; ; TryMaxIdx--)
|
|
{
|
|
if(TryMaxIdx == MinIdx)
|
|
{
|
|
return MaxIdx;
|
|
}
|
|
if (Text[TryMaxIdx - 1] == ' ')
|
|
{
|
|
return TryMaxIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extension method to allow formatting a string to a stringbuilder and appending a newline
|
|
/// </summary>
|
|
/// <param name="Builder">The string builder</param>
|
|
/// <param name="Format">Format string, as used for StringBuilder.AppendFormat</param>
|
|
/// <param name="Args">Arguments for the format string</param>
|
|
public static void AppendLine(this StringBuilder Builder, string Format, params object[] Args)
|
|
{
|
|
Builder.AppendFormat(Format, Args);
|
|
Builder.AppendLine();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a list of strings in the style "1, 2, 3 and 4"
|
|
/// </summary>
|
|
/// <param name="Arguments">List of strings to format</param>
|
|
/// <param name="Conjunction">Conjunction to use between the last two items in the list (eg. "and" or "or")</param>
|
|
/// <returns>Formatted list of strings</returns>
|
|
public static string FormatList(string[] Arguments, string Conjunction = "and")
|
|
{
|
|
StringBuilder Result = new StringBuilder();
|
|
if (Arguments.Length > 0)
|
|
{
|
|
Result.Append(Arguments[0]);
|
|
for (int Idx = 1; Idx < Arguments.Length; Idx++)
|
|
{
|
|
if (Idx == Arguments.Length - 1)
|
|
{
|
|
Result.AppendFormat(" {0} ", Conjunction);
|
|
}
|
|
else
|
|
{
|
|
Result.Append(", ");
|
|
}
|
|
Result.Append(Arguments[Idx]);
|
|
}
|
|
}
|
|
return Result.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a list of strings in the style "1, 2, 3 and 4"
|
|
/// </summary>
|
|
/// <param name="Arguments">List of strings to format</param>
|
|
/// <param name="Conjunction">Conjunction to use between the last two items in the list (eg. "and" or "or")</param>
|
|
/// <returns>Formatted list of strings</returns>
|
|
public static string FormatList(IEnumerable<string> Arguments, string Conjunction = "and")
|
|
{
|
|
return FormatList(Arguments.ToArray(), Conjunction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a list of items
|
|
/// </summary>
|
|
/// <param name="Items">Array of items</param>
|
|
/// <param name="MaxCount">Maximum number of items to include in the list</param>
|
|
/// <returns>Formatted list of items</returns>
|
|
public static string FormatList(string[] Items, int MaxCount)
|
|
{
|
|
if (Items.Length == 0)
|
|
{
|
|
return "unknown";
|
|
}
|
|
else if (Items.Length == 1)
|
|
{
|
|
return Items[0];
|
|
}
|
|
else if (Items.Length <= MaxCount)
|
|
{
|
|
return $"{String.Join(", ", Items.Take(Items.Length - 1))} and {Items.Last()}";
|
|
}
|
|
else
|
|
{
|
|
return $"{String.Join(", ", Items.Take(MaxCount - 1))} and {Items.Length - (MaxCount - 1)} others";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal digit
|
|
/// </summary>
|
|
/// <param name="Character">Character to parse</param>
|
|
/// <returns>Value of this digit, or -1 if invalid</returns>
|
|
public static int GetHexDigit(byte Character)
|
|
{
|
|
return HexDigits[Character];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal digit
|
|
/// </summary>
|
|
/// <param name="Character">Character to parse</param>
|
|
/// <returns>Value of this digit, or -1 if invalid</returns>
|
|
public static int GetHexDigit(char Character)
|
|
{
|
|
return HexDigits[Math.Min((uint)Character, 127)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal string into an array of bytes
|
|
/// </summary>
|
|
/// <returns>Array of bytes</returns>
|
|
public static byte[] ParseHexString(string Text)
|
|
{
|
|
byte[]? Bytes;
|
|
if(!TryParseHexString(Text, out Bytes))
|
|
{
|
|
throw new FormatException(String.Format("Invalid hex string: '{0}'", Text));
|
|
}
|
|
return Bytes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal string into an array of bytes
|
|
/// </summary>
|
|
/// <returns>Array of bytes</returns>
|
|
public static byte[] ParseHexString(ReadOnlySpan<byte> Text)
|
|
{
|
|
byte[]? Bytes;
|
|
if (!TryParseHexString(Text, out Bytes))
|
|
{
|
|
throw new FormatException($"Invalid hex string: '{Encoding.UTF8.GetString(Text)}'");
|
|
}
|
|
return Bytes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal string into an array of bytes
|
|
/// </summary>
|
|
/// <param name="Text">Text to parse</param>
|
|
/// <returns></returns>
|
|
public static bool TryParseHexString(string Text, [NotNullWhen(true)] out byte[]? OutBytes)
|
|
{
|
|
if((Text.Length & 1) != 0)
|
|
{
|
|
throw new FormatException("Length of hex string must be a multiple of two characters");
|
|
}
|
|
|
|
byte[] Bytes = new byte[Text.Length / 2];
|
|
for(int Idx = 0; Idx < Text.Length; Idx += 2)
|
|
{
|
|
int Value = (GetHexDigit(Text[Idx]) << 4) | GetHexDigit(Text[Idx + 1]);
|
|
if(Value < 0)
|
|
{
|
|
OutBytes = null;
|
|
return false;
|
|
}
|
|
Bytes[Idx / 2] = (byte)Value;
|
|
}
|
|
OutBytes = Bytes;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a hexadecimal string into an array of bytes
|
|
/// </summary>
|
|
/// <param name="Text">Text to parse</param>
|
|
/// <returns></returns>
|
|
public static bool TryParseHexString(ReadOnlySpan<byte> Text, [NotNullWhen(true)] out byte[]? OutBytes)
|
|
{
|
|
if ((Text.Length & 1) != 0)
|
|
{
|
|
throw new FormatException("Length of hex string must be a multiple of two characters");
|
|
}
|
|
|
|
byte[] Bytes = new byte[Text.Length / 2];
|
|
for (int Idx = 0; Idx < Text.Length; Idx += 2)
|
|
{
|
|
int Value = ParseHexByte(Text, Idx);
|
|
if (Value < 0)
|
|
{
|
|
OutBytes = null;
|
|
return false;
|
|
}
|
|
Bytes[Idx / 2] = (byte)Value;
|
|
}
|
|
OutBytes = Bytes;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a hex byte from the given offset into a span of utf8 characters
|
|
/// </summary>
|
|
/// <param name="Text">The text to parse</param>
|
|
/// <param name="Idx">Index within the text to parse</param>
|
|
/// <returns>The parsed value, or a negative value on error</returns>
|
|
public static int ParseHexByte(ReadOnlySpan<byte> Text, int Idx)
|
|
{
|
|
return ((int)HexDigits[Text[Idx]] << 4) | ((int)HexDigits[Text[Idx + 1]]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats an array of bytes as a hexadecimal string
|
|
/// </summary>
|
|
/// <param name="Bytes">An array of bytes</param>
|
|
/// <returns>String representation of the array</returns>
|
|
public static string FormatHexString(byte[] Bytes)
|
|
{
|
|
return FormatHexString(Bytes.AsSpan());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats an array of bytes as a hexadecimal string
|
|
/// </summary>
|
|
/// <param name="Bytes">An array of bytes</param>
|
|
/// <returns>String representation of the array</returns>
|
|
public static string FormatHexString(ReadOnlySpan<byte> Bytes)
|
|
{
|
|
const string HexDigits = "0123456789abcdef";
|
|
|
|
char[] Characters = new char[Bytes.Length * 2];
|
|
for (int Idx = 0; Idx < Bytes.Length; Idx++)
|
|
{
|
|
Characters[Idx * 2 + 0] = HexDigits[Bytes[Idx] >> 4];
|
|
Characters[Idx * 2 + 1] = HexDigits[Bytes[Idx] & 15];
|
|
}
|
|
return new string(Characters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats an array of bytes as a hexadecimal string
|
|
/// </summary>
|
|
/// <param name="Bytes">An array of bytes</param>
|
|
/// <returns>String representation of the array</returns>
|
|
public static Utf8String FormatUtf8HexString(ReadOnlySpan<byte> Bytes)
|
|
{
|
|
byte[] Characters = new byte[Bytes.Length * 2];
|
|
for (int Idx = 0; Idx < Bytes.Length; Idx++)
|
|
{
|
|
Characters[Idx * 2 + 0] = HexDigitToUtf8Byte[Bytes[Idx] >> 4];
|
|
Characters[Idx * 2 + 1] = HexDigitToUtf8Byte[Bytes[Idx] & 15];
|
|
}
|
|
return new Utf8String(Characters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Quotes a string as a command line argument
|
|
/// </summary>
|
|
/// <param name="String">The string to quote</param>
|
|
/// <returns>The quoted argument if it contains any spaces, otherwise the original string</returns>
|
|
public static string QuoteArgument(this string String)
|
|
{
|
|
if (String.Contains(' '))
|
|
{
|
|
return $"\"{String}\"";
|
|
}
|
|
else
|
|
{
|
|
return String;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats bytes into a human readable string
|
|
/// </summary>
|
|
/// <param name="Bytes">The total number of bytes</param>
|
|
/// <param name="DecimalPlaces">The number of decimal places to round the resulting value</param>
|
|
/// <returns>Human readable string based on the value of Bytes</returns>
|
|
public static string FormatBytesString(long Bytes, int DecimalPlaces = 2)
|
|
{
|
|
if (Bytes == 0)
|
|
{
|
|
return $"0 {ByteSizes[0]}";
|
|
}
|
|
long BytesAbs = Math.Abs(Bytes);
|
|
int Power = Convert.ToInt32(Math.Floor(Math.Log(BytesAbs, 1024)));
|
|
double Value = Math.Round(BytesAbs / Math.Pow(1024, Power), DecimalPlaces);
|
|
return $"{(Math.Sign(Bytes) * Value)} {ByteSizes[Power]}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a bytes string into bytes. E.g 1.2KB -> 1229
|
|
/// </summary>
|
|
/// <param name="BytesString"></param>
|
|
/// <returns></returns>
|
|
public static long ParseBytesString( string BytesString )
|
|
{
|
|
BytesString = BytesString.Trim();
|
|
|
|
int Power = ByteSizes.FindIndex( s => (s != ByteSizes[0]) && BytesString.EndsWith(s, StringComparison.InvariantCultureIgnoreCase ) ); // need to handle 'B' suffix separately
|
|
if (Power == -1 && BytesString.EndsWith(ByteSizes[0]))
|
|
{
|
|
Power = 0;
|
|
}
|
|
if (Power != -1)
|
|
{
|
|
BytesString = BytesString.Substring(0, BytesString.Length - ByteSizes[Power].Length );
|
|
}
|
|
|
|
double Value = double.Parse(BytesString);
|
|
if (Power > 0 )
|
|
{
|
|
Value *= Math.Pow(1024, Power);
|
|
}
|
|
|
|
return (long)Math.Round(Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a bytes string into bytes. E.g 1.5KB -> 1536
|
|
/// </summary>
|
|
/// <param name="BytesString"></param>
|
|
/// <returns></returns>
|
|
public static bool TryParseBytesString( string BytesString, out long? Bytes )
|
|
{
|
|
try
|
|
{
|
|
Bytes = ParseBytesString(BytesString);
|
|
return true;
|
|
}
|
|
catch(Exception)
|
|
{
|
|
}
|
|
|
|
Bytes = null;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a string to remove VT100 escape codes
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string ParseEscapeCodes(string Line)
|
|
{
|
|
char EscapeChar = '\u001b';
|
|
|
|
int Index = Line.IndexOf(EscapeChar);
|
|
if (Index != -1)
|
|
{
|
|
int LastIndex = 0;
|
|
|
|
StringBuilder Result = new StringBuilder();
|
|
for (; ; )
|
|
{
|
|
Result.Append(Line, LastIndex, Index - LastIndex);
|
|
|
|
while (Index < Line.Length)
|
|
{
|
|
char Char = Line[Index];
|
|
if ((Char >= 'a' && Char <= 'z') || (Char >= 'A' && Char <= 'Z'))
|
|
{
|
|
Index++;
|
|
break;
|
|
}
|
|
Index++;
|
|
}
|
|
|
|
LastIndex = Index;
|
|
|
|
Index = Line.IndexOf(EscapeChar, Index);
|
|
if (Index == -1)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
Result.Append(Line, LastIndex, Line.Length - LastIndex);
|
|
|
|
Line = Result.ToString();
|
|
}
|
|
|
|
return Line;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Truncates the given string to the maximum length, appending an elipsis if it is longer than allowed.
|
|
/// </summary>
|
|
/// <param name="Text"></param>
|
|
/// <param name="MaxLength"></param>
|
|
/// <returns></returns>
|
|
public static string Truncate(string Text, int MaxLength)
|
|
{
|
|
if (Text.Length > MaxLength)
|
|
{
|
|
Text = Text.Substring(0, MaxLength - 3) + "...";
|
|
}
|
|
return Text;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compare two strings using UnrealEngine's ignore case algorithm
|
|
/// </summary>
|
|
/// <param name="X">First string to compare</param>
|
|
/// <param name="Y">Second string to compare</param>
|
|
/// <returns>Less than zero if X < Y, zero if X == Y, and greater than zero if X > y</returns>
|
|
public static int CompareIgnoreCaseUE(ReadOnlySpan<char> X, ReadOnlySpan<char> Y)
|
|
{
|
|
int Length = X.Length < Y.Length ? X.Length : Y.Length;
|
|
|
|
for (int Index = 0; Index < Length; ++Index)
|
|
{
|
|
char XC = X[Index];
|
|
char YC = Y[Index];
|
|
if (XC == YC)
|
|
{
|
|
continue;
|
|
}
|
|
else if (((XC | YC) & 0xffffff80) == 0) // if (BothAscii)
|
|
{
|
|
if (XC >= 'A' && XC <= 'Z')
|
|
{
|
|
XC += (char)32;
|
|
}
|
|
if (YC >= 'A' && YC <= 'Z')
|
|
{
|
|
YC += (char)32;
|
|
}
|
|
int Diff = XC - YC;
|
|
if (Diff != 0)
|
|
{
|
|
return Diff;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return XC - YC;
|
|
}
|
|
}
|
|
|
|
if (X.Length == Length)
|
|
{
|
|
return Y.Length == Length ? 0 : /* X[Length] */ -Y[Length];
|
|
}
|
|
else
|
|
{
|
|
return X[Length] /* - Y[Length] */;
|
|
}
|
|
}
|
|
}
|
|
}
|