diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/zencore/include/zencore/string.h | 64 | ||||
| -rw-r--r-- | src/zencore/string.cpp | 90 |
2 files changed, 150 insertions, 4 deletions
diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h index b4926070c..a1c7a3914 100644 --- a/src/zencore/include/zencore/string.h +++ b/src/zencore/include/zencore/string.h @@ -1349,6 +1349,70 @@ std::string HideSensitiveString(std::string_view String); ////////////////////////////////////////////////////////////////////////// +/// Owns a heap-allocated, null-terminated string buffer. +/// Lightweight alternative to std::string when only immutable +/// storage and string_view access are needed. +/// +/// The buffer layout is [length-prefix][chars...][NUL]. The first +/// byte stores the string length for strings shorter than 255 bytes, +/// avoiding strlen() on every access. A sentinel value of 0xFF +/// indicates a longer string; Size() then returns 255 + strlen() +/// starting at the 256th character, skipping the known prefix. +class CompactString +{ +public: + CompactString() = default; + + explicit CompactString(std::string_view Str) : m_Data(new char[1 + Str.size() + 1]) + { + m_Data[0] = static_cast<char>(Str.size() < LengthFallbackSentinel ? Str.size() : LengthFallbackSentinel); + memcpy(m_Data + 1, Str.data(), Str.size()); + m_Data[1 + Str.size()] = '\0'; + } + + ~CompactString() { delete[] m_Data; } + + CompactString(const CompactString&) = delete; + CompactString& operator=(const CompactString&) = delete; + + CompactString(CompactString&& Other) noexcept : m_Data(Other.m_Data) { Other.m_Data = nullptr; } + + CompactString& operator=(CompactString&& Other) noexcept + { + if (this != &Other) + { + delete[] m_Data; + m_Data = Other.m_Data; + Other.m_Data = nullptr; + } + return *this; + } + + const char* c_str() const { return m_Data ? m_Data + 1 : ""; } + + size_t Size() const + { + if (!m_Data) + { + return 0; + } + uint8_t Prefix = static_cast<uint8_t>(m_Data[0]); + return Prefix < LengthFallbackSentinel ? Prefix : LengthFallbackSentinel + strlen(m_Data + 1 + LengthFallbackSentinel); + } + + std::string_view ToView() const { return {c_str(), Size()}; } + bool IsEmpty() const { return m_Data == nullptr || m_Data[1] == '\0'; } + + operator std::string_view() const { return ToView(); } + +private: + static constexpr uint8_t LengthFallbackSentinel = 0xFF; + + char* m_Data = nullptr; +}; + +////////////////////////////////////////////////////////////////////////// + void string_forcelink(); // internal } // namespace zen diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp index 44f78aa75..2691d14b8 100644 --- a/src/zencore/string.cpp +++ b/src/zencore/string.cpp @@ -9,6 +9,7 @@ #include <inttypes.h> #include <math.h> #include <stdio.h> +#include <charconv> #include <exception> #include <ostream> #include <stdexcept> @@ -54,15 +55,21 @@ namespace zen { bool ToString(std::span<char> Buffer, uint64_t Num) { - snprintf(Buffer.data(), Buffer.size(), "%" PRIu64, Num); - + auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num); + if (Ec == std::errc{}) + { + *Ptr = '\0'; + } return true; } bool ToString(std::span<char> Buffer, int64_t Num) { - snprintf(Buffer.data(), Buffer.size(), "%" PRId64, Num); - + auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num); + if (Ec == std::errc{}) + { + *Ptr = '\0'; + } return true; } @@ -1341,6 +1348,81 @@ TEST_CASE("hidesensitivestring") CHECK_EQ(HideSensitiveString("1234567890123456789"sv), "1234XXXX..."sv); } +TEST_CASE("CompactString.default") +{ + CompactString S; + CHECK(S.IsEmpty()); + CHECK(S.Size() == 0); + CHECK(S.ToView() == std::string_view()); + CHECK(S.c_str()[0] == '\0'); +} + +TEST_CASE("CompactString.empty") +{ + CompactString S(std::string_view("")); + CHECK(S.IsEmpty()); + CHECK(S.Size() == 0); +} + +TEST_CASE("CompactString.short") +{ + CompactString S(std::string_view("hello")); + CHECK(!S.IsEmpty()); + CHECK(S.Size() == 5); + CHECK(S.ToView() == std::string_view("hello")); +} + +TEST_CASE("CompactString.sentinel_boundary") +{ + // 254 chars — largest value that fits in the prefix byte + std::string Str254(254, 'x'); + std::string_view View254(Str254); + CompactString S(View254); + CHECK(S.Size() == 254); + CHECK(S.ToView() == View254); +} + +TEST_CASE("CompactString.sentinel_exact") +{ + // 255 chars — hits the 0xFF sentinel, falls back to strlen + std::string Str255(255, 'y'); + std::string_view View255(Str255); + CompactString S(View255); + CHECK(S.Size() == 255); + CHECK(S.ToView() == View255); +} + +TEST_CASE("CompactString.long") +{ + // Well beyond the sentinel + std::string Str512(512, 'z'); + std::string_view View512(Str512); + CompactString S(View512); + CHECK(S.Size() == 512); + CHECK(S.ToView() == View512); +} + +TEST_CASE("CompactString.move") +{ + CompactString A(std::string_view("test")); + CompactString B(std::move(A)); + CHECK(A.IsEmpty()); + CHECK(B.ToView() == std::string_view("test")); + + CompactString C(std::string_view("first")); + CompactString D(std::string_view("second")); + D = std::move(C); + CHECK(C.IsEmpty()); + CHECK(D.ToView() == std::string_view("first")); +} + +TEST_CASE("CompactString.implicit_conversion") +{ + CompactString S(std::string_view("view")); + std::string_view V = S; + CHECK(V == std::string_view("view")); +} + TEST_SUITE_END(); void |