// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilStringFunctionLibrary.h" #include "Misc/Base64.h" #include "Misc/Crc.h" #include "Misc/Paths.h" #include "Misc/SecureHash.h" namespace { TArray StringToUtf8Bytes(const FString& String) { const FTCHARToUTF8 Converter(*String, String.Len()); return TArray(reinterpret_cast(Converter.Get()), Converter.Length()); } int32 CalculateLevenshteinDistance( FStringView Left, FStringView Right, TArray& PreviousRow, TArray& CurrentRow) { int32 Start = 0; while (Start < Left.Len() && Start < Right.Len() && Left[Start] == Right[Start]) { ++Start; } int32 LeftEnd = Left.Len(); int32 RightEnd = Right.Len(); while (LeftEnd > Start && RightEnd > Start && Left[LeftEnd - 1] == Right[RightEnd - 1]) { --LeftEnd; --RightEnd; } int32 LeftLength = LeftEnd - Start; int32 RightLength = RightEnd - Start; int32 LeftStart = Start; int32 RightStart = Start; if (RightLength > LeftLength) { Swap(Left, Right); Swap(LeftLength, RightLength); Swap(LeftStart, RightStart); } if (RightLength == 0) { return LeftLength; } PreviousRow.SetNumUninitialized(RightLength + 1); CurrentRow.SetNumUninitialized(RightLength + 1); for (int32 ColumnIndex = 0; ColumnIndex <= RightLength; ++ColumnIndex) { PreviousRow[ColumnIndex] = ColumnIndex; } for (int32 RowIndex = 1; RowIndex <= LeftLength; ++RowIndex) { CurrentRow[0] = RowIndex; for (int32 ColumnIndex = 1; ColumnIndex <= RightLength; ++ColumnIndex) { const int32 SubstitutionCost = Left[LeftStart + RowIndex - 1] == Right[RightStart + ColumnIndex - 1] ? 0 : 1; CurrentRow[ColumnIndex] = FMath::Min3( PreviousRow[ColumnIndex] + 1, CurrentRow[ColumnIndex - 1] + 1, PreviousRow[ColumnIndex - 1] + SubstitutionCost); } Swap(PreviousRow, CurrentRow); } return PreviousRow[RightLength]; } float CalculateStringSimilarity( const FStringView Left, const FStringView Right, const int32 MaxLength, TArray& PreviousRow, TArray& CurrentRow) { if (MaxLength == 0) { return 1.0f; } const int32 Distance = CalculateLevenshteinDistance(Left, Right, PreviousRow, CurrentRow); return 1.0f - static_cast(Distance) / static_cast(MaxLength); } } bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String) { for (const TCHAR& Char : String) { if (FChar::IsAlpha(Char)) { return true; } } return false; } bool UDirectiveUtilStringFunctionLibrary::ContainsNumbers(const FString& String) { for (const TCHAR& Char : String) { if (FChar::IsDigit(Char)) { return true; } } return false; } bool UDirectiveUtilStringFunctionLibrary::ContainsSpaces(const FString& String) { for (const TCHAR& Char : String) { if (FChar::IsWhitespace(Char)) { return true; } } return false; } bool UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(const FString& String) { for (const TCHAR& Char : String) { if (FChar::IsPunct(Char)) { return true; } } return false; } FString UDirectiveUtilStringFunctionLibrary::FilterCharacters( const FString& String, const bool bLetters, const bool bNumbers, const bool bSpecialCharacters, const bool bSpaces) { FString NewString; NewString.Reserve(String.Len()); for (const TCHAR& Char : String) { if (bLetters && FChar::IsAlpha(Char)) continue; if (bNumbers && FChar::IsDigit(Char)) continue; if (bSpecialCharacters && FChar::IsPunct(Char)) continue; if (bSpaces && FChar::IsWhitespace(Char)) continue; NewString.AppendChar(Char); } return NewString; } FString UDirectiveUtilStringFunctionLibrary::TruncateString(const FString& String, const int32 MaxLength, const FString& Suffix) { if (String.Len() <= MaxLength) { return String; } return String.Left(FMath::Max(0, MaxLength - Suffix.Len())) + Suffix; } FString UDirectiveUtilStringFunctionLibrary::ToTitleCase(const FString& String) { FString Result; Result.Reserve(String.Len()); bool bCapitalizeNext = true; for (const TCHAR& Char : String) { if (FChar::IsWhitespace(Char)) { bCapitalizeNext = true; Result.AppendChar(Char); } else if (bCapitalizeNext && FChar::IsAlpha(Char)) { Result.AppendChar(FChar::ToUpper(Char)); bCapitalizeNext = false; } else { Result.AppendChar(FChar::ToLower(Char)); bCapitalizeNext = false; } } return Result; } TArray UDirectiveUtilStringFunctionLibrary::SplitIntoWords(const FString& String) { TArray Words; FString CurrentWord; for (int32 Index = 0; Index < String.Len(); ++Index) { const TCHAR Char = String[Index]; if (!FChar::IsAlnum(Char)) { if (!CurrentWord.IsEmpty()) { Words.Add(MoveTemp(CurrentWord)); CurrentWord.Reset(); } continue; } if (!CurrentWord.IsEmpty()) { const TCHAR Previous = CurrentWord[CurrentWord.Len() - 1]; const bool bNextIsLower = Index + 1 < String.Len() && FChar::IsLower(String[Index + 1]); const bool bBoundary = (FChar::IsLower(Previous) && FChar::IsUpper(Char)) || (FChar::IsUpper(Previous) && FChar::IsUpper(Char) && bNextIsLower) || (FChar::IsAlpha(Previous) && FChar::IsDigit(Char)) || (FChar::IsDigit(Previous) && FChar::IsAlpha(Char)); if (bBoundary) { Words.Add(MoveTemp(CurrentWord)); CurrentWord.Reset(); } } CurrentWord.AppendChar(Char); } if (!CurrentWord.IsEmpty()) { Words.Add(MoveTemp(CurrentWord)); } return Words; } FString UDirectiveUtilStringFunctionLibrary::ToCamelCase(const FString& String) { const TArray Words = SplitIntoWords(String); FString Result; for (int32 Index = 0; Index < Words.Num(); ++Index) { FString Word = Words[Index].ToLower(); if (Index > 0) { Word[0] = FChar::ToUpper(Word[0]); } Result += Word; } return Result; } FString UDirectiveUtilStringFunctionLibrary::ToPascalCase(const FString& String) { FString Result; for (const FString& Word : SplitIntoWords(String)) { FString Cased = Word.ToLower(); Cased[0] = FChar::ToUpper(Cased[0]); Result += Cased; } return Result; } FString UDirectiveUtilStringFunctionLibrary::ToSnakeCase(const FString& String) { return FString::Join(SplitIntoWords(String), TEXT("_")).ToLower(); } FString UDirectiveUtilStringFunctionLibrary::ToKebabCase(const FString& String) { return FString::Join(SplitIntoWords(String), TEXT("-")).ToLower(); } TArray UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray StringArray) { Algo::Sort(StringArray); return StringArray; } TArray UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const TArray StringArray) { TArray SortedArray = StringArray; Algo::Sort(SortedArray); return SortedArray; } int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive) { const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower(); const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower(); const FStringView ViewA = bCaseSensitive ? FStringView(A) : FStringView(NormalizedA); const FStringView ViewB = bCaseSensitive ? FStringView(B) : FStringView(NormalizedB); TArray PreviousRow; TArray CurrentRow; return CalculateLevenshteinDistance(ViewA, ViewB, PreviousRow, CurrentRow); } float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive) { const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower(); const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower(); TArray PreviousRow; TArray CurrentRow; return CalculateStringSimilarity( bCaseSensitive ? FStringView(A) : FStringView(NormalizedA), bCaseSensitive ? FStringView(B) : FStringView(NormalizedB), FMath::Max(A.Len(), B.Len()), PreviousRow, CurrentRow); } bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray& SearchTerms, const bool bCaseSensitive) { const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase; for (const FString& Term : SearchTerms) { if (!Term.IsEmpty() && Source.Contains(Term, SearchCase)) { return true; } } return false; } bool UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(const FString& Source, const TArray& SearchTerms, const bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex) { OutFoundIndex = INDEX_NONE; OutTermIndex = INDEX_NONE; const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase; for (int32 TermIndex = 0; TermIndex < SearchTerms.Num(); ++TermIndex) { const FString& Term = SearchTerms[TermIndex]; if (Term.IsEmpty()) { continue; } const int32 FoundIndex = Source.Find(Term, SearchCase, ESearchDir::FromStart); if (FoundIndex != INDEX_NONE && (OutFoundIndex == INDEX_NONE || FoundIndex < OutFoundIndex)) { OutFoundIndex = FoundIndex; OutTermIndex = TermIndex; } } return OutFoundIndex != INDEX_NONE; } FString UDirectiveUtilStringFunctionLibrary::Base64Encode(const FString& Source) { return FBase64::Encode(Source); } bool UDirectiveUtilStringFunctionLibrary::Base64Decode(const FString& Source, FString& OutDecoded) { OutDecoded.Reset(); return FBase64::Decode(Source, OutDecoded); } FString UDirectiveUtilStringFunctionLibrary::HexEncode(const FString& String) { return HexEncodeBytes(StringToUtf8Bytes(String)); } bool UDirectiveUtilStringFunctionLibrary::HexDecode(const FString& Hex, FString& OutString) { OutString.Reset(); TArray Bytes; if (!HexDecodeBytes(Hex, Bytes)) { return false; } const FUTF8ToTCHAR Converter(reinterpret_cast(Bytes.GetData()), Bytes.Num()); OutString = FString(Converter.Length(), Converter.Get()); return true; } FString UDirectiveUtilStringFunctionLibrary::HexEncodeBytes(const TArray& Bytes) { return BytesToHexLower(Bytes.GetData(), Bytes.Num()); } bool UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(const FString& Hex, TArray& OutBytes) { OutBytes.Reset(); if (Hex.Len() % 2 != 0) { return false; } OutBytes.Reserve(Hex.Len() / 2); for (int32 Index = 0; Index < Hex.Len(); Index += 2) { const TCHAR High = Hex[Index]; const TCHAR Low = Hex[Index + 1]; if (!CheckTCharIsHex(High) || !CheckTCharIsHex(Low)) { OutBytes.Reset(); return false; } OutBytes.Add(static_cast((TCharToNibble(High) << 4) | TCharToNibble(Low))); } return true; } FString UDirectiveUtilStringFunctionLibrary::Md5HashString(const FString& String) { return Md5HashBytes(StringToUtf8Bytes(String)); } FString UDirectiveUtilStringFunctionLibrary::Md5HashBytes(const TArray& Bytes) { return FMD5::HashBytes(Bytes.GetData(), Bytes.Num()); } FString UDirectiveUtilStringFunctionLibrary::Sha1HashString(const FString& String) { return Sha1HashBytes(StringToUtf8Bytes(String)); } FString UDirectiveUtilStringFunctionLibrary::Sha1HashBytes(const TArray& Bytes) { uint8 Digest[20]; FSHA1::HashBuffer(Bytes.GetData(), Bytes.Num(), Digest); return BytesToHexLower(Digest, UE_ARRAY_COUNT(Digest)); } int32 UDirectiveUtilStringFunctionLibrary::Crc32String(const FString& String) { return Crc32Bytes(StringToUtf8Bytes(String)); } int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray& Bytes) { return static_cast(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num())); } namespace { bool IsReservedDeviceFileName(const FString& FileName) { FString Stem = FileName; int32 DotIndex = INDEX_NONE; if (FileName.FindChar(TEXT('.'), DotIndex)) { Stem = FileName.Left(DotIndex); } while (Stem.EndsWith(TEXT(".")) || Stem.EndsWith(TEXT(" "))) { Stem.LeftChopInline(1); } if (Stem.IsEmpty()) { return false; } static const TCHAR* ReservedNames[] = { TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("CLOCK$"), TEXT("NUL"), TEXT("COM1"), TEXT("COM2"), TEXT("COM3"), TEXT("COM4"), TEXT("COM5"), TEXT("COM6"), TEXT("COM7"), TEXT("COM8"), TEXT("COM9"), TEXT("LPT1"), TEXT("LPT2"), TEXT("LPT3"), TEXT("LPT4"), TEXT("LPT5"), TEXT("LPT6"), TEXT("LPT7"), TEXT("LPT8"), TEXT("LPT9") }; for (const TCHAR* ReservedName : ReservedNames) { if (Stem.Equals(ReservedName, ESearchCase::IgnoreCase)) { return true; } } return false; } } bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String) { return !String.IsEmpty() && String != TEXT(".") && String != TEXT("..") && FPaths::GetCleanFilename(String) == String && SanitizeFileName(String) == String; } FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement) { FString Result = FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]); while (Result.EndsWith(TEXT(".")) || Result.EndsWith(TEXT(" "))) { Result.LeftChopInline(1); } if (IsReservedDeviceFileName(Result)) { Result = FString(TEXT("_")) + Result; } return Result; } int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray& Candidates, float& OutSimilarity, const bool bCaseSensitive) { OutSimilarity = 0.0f; int32 BestIndex = INDEX_NONE; const FString NormalizedInput = bCaseSensitive ? FString() : Input.ToLower(); const FStringView InputView = bCaseSensitive ? FStringView(Input) : FStringView(NormalizedInput); TArray PreviousRow; TArray CurrentRow; for (int32 Index = 0; Index < Candidates.Num(); ++Index) { const FString NormalizedCandidate = bCaseSensitive ? FString() : Candidates[Index].ToLower(); const FStringView CandidateView = bCaseSensitive ? FStringView(Candidates[Index]) : FStringView(NormalizedCandidate); const float Similarity = CalculateStringSimilarity( InputView, CandidateView, FMath::Max(Input.Len(), Candidates[Index].Len()), PreviousRow, CurrentRow); if (BestIndex == INDEX_NONE || Similarity > OutSimilarity) { BestIndex = Index; OutSimilarity = Similarity; if (Similarity == 1.0f) { break; } } } return BestIndex; }