// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once #include "types.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "fast_float/fast_float.h" // Older versions of libstdc++ are missing support for from_chars() with floats, and was only recently // merged in libc++. So, just fall back to stringstream (yuck!) on everywhere except MSVC. #if !defined(_MSC_VER) #include #include #endif namespace StringUtil { /// Checks if a wildcard matches a search string. bool WildcardMatch(const char* subject, const char* mask, bool case_sensitive = true); /// Safe version of strlcpy. std::size_t Strlcpy(char* dst, const char* src, std::size_t size); /// Strlcpy from string_view. std::size_t Strlcpy(char* dst, const std::string_view src, std::size_t size); /// Bounds checked version of strlen. std::size_t Strnlen(const char* str, std::size_t max_size); /// Platform-independent strcasecmp static inline int Strcasecmp(const char* s1, const char* s2) { #ifdef _MSC_VER return _stricmp(s1, s2); #else return strcasecmp(s1, s2); #endif } /// Platform-independent strcasecmp static inline int Strncasecmp(const char* s1, const char* s2, std::size_t n) { #ifdef _MSC_VER return _strnicmp(s1, s2, n); #else return strncasecmp(s1, s2, n); #endif } // Case-insensitive equality of string views. static inline bool EqualNoCase(std::string_view s1, std::string_view s2) { const size_t s1_len = s1.length(); const size_t s2_len = s2.length(); if (s1_len != s2_len) return false; else if (s1_len == 0) return true; return (Strncasecmp(s1.data(), s2.data(), s1_len) == 0); } static inline int CompareNoCase(std::string_view s1, std::string_view s2) { const size_t s1_len = s1.length(); const size_t s2_len = s2.length(); const size_t compare_len = std::min(s1_len, s2_len); const int compare_res = (compare_len > 0) ? Strncasecmp(s1.data(), s2.data(), compare_len) : 0; return (compare_len != 0) ? compare_res : ((s1_len < s2_len) ? -1 : ((s1_len > s2_len) ? 1 : 0)); } /// Wrapper around std::from_chars template::value, bool> = true> inline std::optional FromChars(const std::string_view str, int base = 10) { T value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.length(), value, base); if (result.ec != std::errc()) return std::nullopt; return value; } template::value, bool> = true> inline std::optional FromChars(const std::string_view str, int base, std::string_view* endptr) { T value; const char* ptr = str.data(); const char* end = ptr + str.length(); const std::from_chars_result result = std::from_chars(ptr, end, value, base); if (result.ec != std::errc()) return std::nullopt; if (endptr) { const size_t remaining_len = end - result.ptr; *endptr = (remaining_len > 0) ? std::string_view(result.ptr, remaining_len) : std::string_view(); } return value; } template::value, bool> = true> inline std::optional FromCharsWithOptionalBase(std::string_view str, std::string_view* endptr = nullptr) { int base = 10; if (str.starts_with("0x")) { base = 16; str = str.substr(2); } else if (str.starts_with("0b")) { base = 2; str = str.substr(1); } else if (str.starts_with("0") && str.length() > 1) { base = 8; str = str.substr(1); } if (endptr) return FromChars(str, base, endptr); else return FromChars(str, base); } template::value, bool> = true> inline std::optional FromChars(const std::string_view str) { T value; const fast_float::from_chars_result result = fast_float::from_chars(str.data(), str.data() + str.length(), value); if (result.ec != std::errc()) return std::nullopt; return value; } template::value, bool> = true> inline std::optional FromChars(const std::string_view str, std::string_view* endptr) { T value; const char* ptr = str.data(); const char* end = ptr + str.length(); const fast_float::from_chars_result result = fast_float::from_chars(ptr, end, value); if (result.ec != std::errc()) return std::nullopt; if (endptr) { const size_t remaining_len = end - result.ptr; *endptr = (remaining_len > 0) ? std::string_view(result.ptr, remaining_len) : std::string_view(); } return value; } /// Wrapper around std::to_chars template::value, bool> = true> inline std::string ToChars(T value, int base = 10) { constexpr size_t MAX_SIZE = 32; char buf[MAX_SIZE]; std::string ret; const std::to_chars_result result = std::to_chars(buf, buf + MAX_SIZE, value, base); if (result.ec == std::errc()) ret.append(buf, result.ptr - buf); return ret; } template::value, bool> = true> inline std::string ToChars(T value) { // No to_chars() in older versions of libstdc++/libc++. #ifdef _MSC_VER constexpr size_t MAX_SIZE = 64; char buf[MAX_SIZE]; std::string ret; const std::to_chars_result result = std::to_chars(buf, buf + MAX_SIZE, value); if (result.ec == std::errc()) ret.append(buf, result.ptr - buf); return ret; #else std::ostringstream ss; ss.imbue(std::locale::classic()); ss << value; return std::move(ss).str(); #endif } /// Explicit override for booleans template<> inline std::optional FromChars(const std::string_view str, int base) { if (Strncasecmp("true", str.data(), str.length()) == 0 || Strncasecmp("yes", str.data(), str.length()) == 0 || Strncasecmp("on", str.data(), str.length()) == 0 || Strncasecmp("1", str.data(), str.length()) == 0 || Strncasecmp("enabled", str.data(), str.length()) == 0) { return true; } if (Strncasecmp("false", str.data(), str.length()) == 0 || Strncasecmp("no", str.data(), str.length()) == 0 || Strncasecmp("off", str.data(), str.length()) == 0 || Strncasecmp("0", str.data(), str.length()) == 0 || Strncasecmp("disabled", str.data(), str.length()) == 0) { return false; } return std::nullopt; } template<> inline std::string ToChars(bool value, int base) { return std::string(value ? "true" : "false"); } /// Returns true if the given character is whitespace. static inline bool IsWhitespace(char ch) { return ((ch >= 0x09 && ch <= 0x0D) || // horizontal tab, line feed, vertical tab, form feed, carriage return ch == 0x20); // space } /// Encode/decode hexadecimal byte buffers u8 DecodeHexDigit(char ch); size_t DecodeHex(std::span dest, const std::string_view str); std::optional> DecodeHex(const std::string_view str); std::string EncodeHex(const void* data, size_t length); template ALWAYS_INLINE static std::string EncodeHex(const std::span data) { return EncodeHex(data.data(), data.size_bytes()); } /// Returns true if the character is a hexadecimal digit. template ALWAYS_INLINE static bool IsHexDigit(T ch) { return ((ch >= static_cast('a') && ch <= static_cast('f')) || (ch >= static_cast('A') && ch <= static_cast('F')) || (ch >= static_cast('0') && ch <= static_cast('9'))); } /// Returns a byte array from the provided hex string, computed at compile-time. template static constexpr std::array ParseFixedHexString(const char str[]) { std::array h{}; for (int i = 0; str[i] != '\0'; i++) { u8 nibble = 0; char ch = str[i]; if (ch >= '0' && ch <= '9') nibble = str[i] - '0'; else if (ch >= 'a' && ch <= 'z') nibble = 0xA + (str[i] - 'a'); else if (ch >= 'A' && ch <= 'Z') nibble = 0xA + (str[i] - 'A'); h[i / 2] |= nibble << (((i & 1) ^ 1) * 4); } return h; } /// Encode/decode Base64 buffers. static constexpr size_t DecodedBase64Length(const std::string_view str) { // Should be a multiple of 4. const size_t str_length = str.length(); if ((str_length % 4) != 0) return 0; // Reverse padding. size_t padding = 0; if (str.length() >= 2) { padding += static_cast(str[str_length - 1] == '='); padding += static_cast(str[str_length - 2] == '='); } return (str_length / 4) * 3 - padding; } static constexpr size_t EncodedBase64Length(const std::span data) { return ((data.size() + 2) / 3) * 4; } size_t DecodeBase64(const std::span data, const std::string_view str); size_t EncodeBase64(const std::span dest, const std::span data); std::string EncodeBase64(const std::span data); std::optional> DecodeBase64(const std::string_view str); /// StartsWith/EndsWith variants which aren't case sensitive. ALWAYS_INLINE static bool StartsWithNoCase(const std::string_view str, const std::string_view prefix) { return (!str.empty() && Strncasecmp(str.data(), prefix.data(), prefix.length()) == 0); } ALWAYS_INLINE static bool EndsWithNoCase(const std::string_view str, const std::string_view suffix) { const std::size_t suffix_length = suffix.length(); return (str.length() >= suffix_length && Strncasecmp(str.data() + (str.length() - suffix_length), suffix.data(), suffix_length) == 0); } /// Strip whitespace from the start/end of the string. std::string_view StripWhitespace(const std::string_view str); void StripWhitespace(std::string* str); /// Splits a string based on a single character delimiter. [[nodiscard]] std::vector SplitString(const std::string_view str, char delimiter, bool skip_empty = true); [[nodiscard]] std::vector SplitNewString(const std::string_view str, char delimiter, bool skip_empty = true); /// Returns true if the given string is found in the string list container. template static inline bool IsInStringList(const T& list, const std::string_view str) { return std::any_of(std::begin(list), std::end(list), [&str](const auto& it) { return (str == it); }); } /// Adds a string to a string list container. No append is performed if the string already exists. template static inline bool AddToStringList(T& list, const std::string_view str) { if (IsInStringList(list, str)) return false; list.emplace_back(str); return true; } /// Removes a string from a string list container. template static inline bool RemoveFromStringList(T& list, const std::string_view str) { bool removed = false; for (auto iter = std::begin(list); iter != std::end(list);) { if (str == *iter) { iter = list.erase(iter); removed = true; } else { ++iter; } } return removed; } /// Joins a string together using the specified delimiter. template static inline std::string JoinString(const T& start, const T& end, char delimiter) { std::string ret; for (auto it = start; it != end; ++it) { if (it != start) ret += delimiter; ret.append(*it); } return ret; } template static inline std::string JoinString(const T& list, char delimiter) { return JoinString(std::begin(list), std::end(list), delimiter); } template static inline std::string JoinString(const T& start, const T& end, const std::string_view delimiter) { std::string ret; for (auto it = start; it != end; ++it) { if (it != start) ret.append(delimiter); ret.append(*it); } return ret; } template static inline std::string JoinString(const T& list, const std::string_view delimiter) { return JoinString(std::begin(list), std::end(list), delimiter); } /// Replaces all instances of search in subject with replacement. [[nodiscard]] std::string ReplaceAll(const std::string_view subject, const std::string_view search, const std::string_view replacement); void ReplaceAll(std::string* subject, const std::string_view search, const std::string_view replacement); [[nodiscard]] std::string ReplaceAll(const std::string_view subject, const char search, const char replacement); void ReplaceAll(std::string* subject, const char search, const char replacement); /// Parses an assignment string (Key = Value) into its two components. bool ParseAssignmentString(const std::string_view str, std::string_view* key, std::string_view* value); /// Unicode replacement character. static constexpr char32_t UNICODE_REPLACEMENT_CHARACTER = 0xFFFD; /// Appends a UTF-16/UTF-32 codepoint to a UTF-8 string. void EncodeAndAppendUTF8(std::string& s, char32_t ch); size_t EncodeAndAppendUTF8(void* utf8, size_t pos, size_t size, char32_t ch); size_t GetEncodedUTF8Length(char32_t ch); /// Decodes UTF-8 to a single unicode codepoint. /// Returns the number of bytes the codepoint took in the original string. size_t DecodeUTF8(const void* bytes, size_t length, char32_t* ch); size_t DecodeUTF8(const std::string_view str, size_t offset, char32_t* ch); size_t DecodeUTF8(const std::string& str, size_t offset, char32_t* ch); /// Appends a unicode codepoint to a UTF-16 string. size_t EncodeAndAppendUTF16(void* utf16, size_t pos, size_t size, char32_t codepoint); /// Decodes UTF-16 to a single unicode codepoint. /// Returns the number of bytes the codepoint took in the original string. size_t DecodeUTF16(const void* bytes, size_t pos, size_t size, char32_t* codepoint); // Replaces the end of a string with ellipsis if it exceeds the specified length. std::string Ellipsise(const std::string_view str, u32 max_length, const char* ellipsis = "..."); void EllipsiseInPlace(std::string& str, u32 max_length, const char* ellipsis = "..."); /// Searches for the specified byte pattern in the given memory span. Wildcards (i.e. ??) are supported. std::optional BytePatternSearch(const std::span bytes, const std::string_view pattern); /// Strided memcpy/memcmp. ALWAYS_INLINE static void StrideMemCpy(void* dst, std::size_t dst_stride, const void* src, std::size_t src_stride, std::size_t copy_size, std::size_t count) { if (src_stride == dst_stride && src_stride == copy_size) { std::memcpy(dst, src, src_stride * count); return; } const u8* src_ptr = static_cast(src); u8* dst_ptr = static_cast(dst); for (std::size_t i = 0; i < count; i++) { std::memcpy(dst_ptr, src_ptr, copy_size); src_ptr += src_stride; dst_ptr += dst_stride; } } ALWAYS_INLINE static int StrideMemCmp(const void* p1, std::size_t p1_stride, const void* p2, std::size_t p2_stride, std::size_t copy_size, std::size_t count) { if (p1_stride == p2_stride && p1_stride == copy_size) return std::memcmp(p1, p2, p1_stride * count); const u8* p1_ptr = static_cast(p1); const u8* p2_ptr = static_cast(p2); for (std::size_t i = 0; i < count; i++) { int result = std::memcmp(p1_ptr, p2_ptr, copy_size); if (result != 0) return result; p2_ptr += p2_stride; p1_ptr += p1_stride; } return 0; } #ifdef _WIN32 /// Converts the specified UTF-8 string to a wide string. std::wstring UTF8StringToWideString(const std::string_view str); bool UTF8StringToWideString(std::wstring& dest, const std::string_view str); /// Converts the specified wide string to a UTF-8 string. std::string WideStringToUTF8String(const std::wstring_view str); bool WideStringToUTF8String(std::string& dest, const std::wstring_view str); #endif } // namespace StringUtil