Files
UnrealEngineUWP/Engine/Source/Programs/Shared/EpicGames.Core/StringUtils.cs
Ben Marsh 5bf92bfe33 Horde: Hardening for storage bundles.
- CommitService now writes bundles rather than tree pack objects.
- Legacy TreePack classes removed.
- Added commands for creating, extracting, diffing bundles to Horde Agent and Horde Build (implemented in both places using shared code, in order to account for convenience in configuring the matching storage backend).
- Some bug fixes.

#preflight none

[CL 19176653 by Ben Marsh in ue5-main branch]
2022-02-28 12:55:47 -05:00

641 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)
{
OutBytes = null;
return false;
}
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)
{
OutBytes = null;
return false;
}
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] */;
}
}
}
}