#pragma once /// std::string compatible string class. // fl::string has inlined memory and copy on write semantics. #include "fl/int.h" #include #ifdef __EMSCRIPTEN__ #include #endif #include "fl/geometry.h" #include "fl/math_macros.h" #include "fl/namespace.h" #include "fl/memory.h" #include "fl/optional.h" #include "fl/type_traits.h" #include "fl/vector.h" #include "fl/span.h" #include "fl/force_inline.h" #ifndef FASTLED_STR_INLINED_SIZE #define FASTLED_STR_INLINED_SIZE 64 #endif FASTLED_NAMESPACE_BEGIN struct CRGB; FASTLED_NAMESPACE_END; namespace fl { // Mandatory namespace for this class since it has name // collisions. class string; using Str = fl::string; // backwards compatibility class Tile2x2_u8_wrap; class JsonUiInternal; // Forward declarations for JSON types struct JsonValue; class Json; template struct rect; template struct vec2; template struct vec3; template class Slice; template class HeapVector; template class InlinedVector; template class FixedVector; template class StrN; template class WeakPtr; template class Ptr; template struct Hash; template struct EqualTo; template class HashSet; template class BitsetFixed; class bitset_dynamic; template class BitsetInlined; class XYMap; struct FFTBins; // A copy on write string class. Fast to copy from another // Str object as read only pointers are shared. If the size // of the string is below FASTLED_STR_INLINED_SIZE then the // the entire string fits in the object and no heap allocation occurs. // When the string is large enough it will overflow into heap // allocated memory. Copy on write means that if the Str has // a heap buffer that is shared with another Str object, then // a copy is made and then modified in place. // If write() or append() is called then the internal data structure // will grow to accomodate the new data with extra space for future, // like a vector. /////////////////////////////////////////////////////// // Implementation details. FASTLED_SMART_PTR(StringHolder); class StringFormatter { public: static void append(i32 val, StrN *dst); static void append(u32 val, StrN *dst); static void append(uint64_t val, StrN *dst); static void append(i16 val, StrN *dst); static void append(u16 val, StrN *dst); static bool isSpace(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } static float parseFloat(const char *str, fl::size len); static int parseInt(const char *str, fl::size len); static int parseInt(const char *str); static bool isDigit(char c) { return c >= '0' && c <= '9'; } static void appendFloat(const float &val, StrN *dst); static void appendFloat(const float &val, StrN *dst, int precision); }; class StringHolder { public: StringHolder(const char *str); StringHolder(fl::size length); StringHolder(const char *str, fl::size length); StringHolder(const StringHolder &other) = delete; StringHolder &operator=(const StringHolder &other) = delete; ~StringHolder(); void grow(fl::size newLength); bool hasCapacity(fl::size newLength) const { return newLength <= mCapacity; } const char *data() const { return mData; } char *data() { return mData; } fl::size length() const { return mLength; } fl::size capacity() const { return mCapacity; } bool copy(const char *str, fl::size len) { if ((len + 1) > mCapacity) { return false; } memcpy(mData, str, len); mData[len] = '\0'; mLength = len; return true; } private: char *mData = nullptr; fl::size mLength = 0; fl::size mCapacity = 0; }; template class StrN { protected: fl::size mLength = 0; char mInlineData[SIZE] = {0}; StringHolderPtr mHeapData; public: // Static constants (like std::string) static constexpr fl::size npos = static_cast(-1); // Constructors StrN() = default; // cppcheck-suppress-begin [operatorEqVarError] template StrN(const StrN &other) { copy(other); } StrN(const char *str) { fl::size len = strlen(str); mLength = len; // Length is without null terminator if (len + 1 <= SIZE) { // Check capacity including null memcpy(mInlineData, str, len + 1); // Copy including null mHeapData.reset(); } else { mHeapData = fl::make_shared(str); } } StrN(const StrN &other) { copy(other); } void copy(const char *str) { fl::size len = strlen(str); mLength = len; if (len + 1 <= SIZE) { memcpy(mInlineData, str, len + 1); mHeapData.reset(); } else { if (mHeapData && mHeapData.use_count() <= 1) { // We are the sole owners of this data so we can modify it mHeapData->copy(str, len); return; } mHeapData.reset(); mHeapData = fl::make_shared(str); } } template StrN(const char (&str)[N]) { copy(str, N - 1); // Subtract 1 to not count null terminator } template StrN &operator=(const char (&str)[N]) { assign(str, N); return *this; } StrN &operator=(const StrN &other) { copy(other); return *this; } template StrN &operator=(const StrN &other) { copy(other); return *this; } // cppcheck-suppress-end void assign(const char* str, fl::size len) { mLength = len; if (len + 1 <= SIZE) { memcpy(mInlineData, str, len + 1); mHeapData.reset(); } else { mHeapData = fl::make_shared(str, len); mHeapData->copy(str, len); mHeapData->data()[len] = '\0'; } } bool operator==(const StrN &other) const { return strcmp(c_str(), other.c_str()) == 0; } bool operator!=(const StrN &other) const { return strcmp(c_str(), other.c_str()) != 0; } void copy(const char *str, fl::size len) { mLength = len; if (len + 1 <= SIZE) { fl::memcopy(mInlineData, str, len); // Copy only len characters, not len+1 mInlineData[len] = '\0'; // Add null terminator manually mHeapData.reset(); } else { mHeapData = fl::make_shared(str, len); } } template void copy(const StrN &other) { fl::size len = other.size(); if (len + 1 <= SIZE) { memcpy(mInlineData, other.c_str(), len + 1); mHeapData.reset(); } else { if (other.mHeapData) { mHeapData = other.mHeapData; } else { mHeapData = fl::make_shared(other.c_str()); } } mLength = len; } fl::size capacity() const { return mHeapData ? mHeapData->capacity() : SIZE; } fl::size write(const u8 *data, fl::size n) { const char *str = fl::bit_cast_ptr(static_cast(data)); return write(str, n); } fl::size write(const char *str, fl::size n) { fl::size newLen = mLength + n; if (mHeapData && mHeapData.use_count() <= 1) { if (!mHeapData->hasCapacity(newLen)) { fl::size grow_length = MAX(3, newLen * 3 / 2); mHeapData->grow(grow_length); // Grow by 50% } memcpy(mHeapData->data() + mLength, str, n); mLength = newLen; mHeapData->data()[mLength] = '\0'; return mLength; } if (newLen + 1 <= SIZE) { memcpy(mInlineData + mLength, str, n); mLength = newLen; mInlineData[mLength] = '\0'; return mLength; } mHeapData.reset(); StringHolderPtr newData = fl::make_shared(newLen); if (newData) { memcpy(newData->data(), c_str(), mLength); memcpy(newData->data() + mLength, str, n); newData->data()[newLen] = '\0'; mHeapData = newData; mLength = newLen; } mHeapData = newData; return mLength; } fl::size write(char c) { return write(&c, 1); } fl::size write(u8 c) { const char *str = fl::bit_cast_ptr(static_cast(&c)); return write(str, 1); } fl::size write(const u16 &n) { StrN dst; StringFormatter::append(n, &dst); // Inlined size should suffice return write(dst.c_str(), dst.size()); } fl::size write(const u32 &val) { StrN dst; StringFormatter::append(val, &dst); // Inlined size should suffice return write(dst.c_str(), dst.size()); } fl::size write(const uint64_t &val) { StrN dst; StringFormatter::append(val, &dst); // Inlined size should suffice return write(dst.c_str(), dst.size()); } fl::size write(const i32 &val) { StrN dst; StringFormatter::append(val, &dst); // Inlined size should suffice return write(dst.c_str(), dst.size()); } fl::size write(const i8 val) { StrN dst; StringFormatter::append(i16(val), &dst); // Inlined size should suffice return write(dst.c_str(), dst.size()); } // Destructor ~StrN() {} // Accessors fl::size size() const { return mLength; } fl::size length() const { return size(); } const char *c_str() const { return mHeapData ? mHeapData->data() : mInlineData; } char *c_str_mutable() { return mHeapData ? mHeapData->data() : mInlineData; } char &operator[](fl::size index) { if (index >= mLength) { static char dummy = '\0'; return dummy; } return c_str_mutable()[index]; } char operator[](fl::size index) const { if (index >= mLength) { static char dummy = '\0'; return dummy; } return c_str()[index]; } bool empty() const { return mLength == 0; } // Iterator support for range-based for loops char* begin() { return c_str_mutable(); } char* end() { return c_str_mutable() + mLength; } const char* begin() const { return c_str(); } const char* end() const { return c_str() + mLength; } const char* cbegin() const { return c_str(); } const char* cend() const { return c_str() + mLength; } // Append method bool operator<(const StrN &other) const { return strcmp(c_str(), other.c_str()) < 0; } template bool operator<(const StrN &other) const { return strcmp(c_str(), other.c_str()) < 0; } void reserve(fl::size newCapacity) { // If capacity is less than current length, do nothing if (newCapacity <= mLength) { return; } // If new capacity fits in inline buffer, no need to allocate if (newCapacity + 1 <= SIZE) { return; } // If we already have unshared heap data with sufficient capacity, do // nothing if (mHeapData && mHeapData.use_count() <= 1 && mHeapData->hasCapacity(newCapacity)) { return; } // Need to allocate new storage StringHolderPtr newData = fl::make_shared(newCapacity); if (newData) { // Copy existing content memcpy(newData->data(), c_str(), mLength); newData->data()[mLength] = '\0'; mHeapData = newData; } } void clear(bool freeMemory = false) { mLength = 0; if (freeMemory && mHeapData) { mHeapData.reset(); } } // Find single character fl::size find(const char &value) const { for (fl::size i = 0; i < mLength; ++i) { if (c_str()[i] == value) { return i; } } return npos; } // Find substring (string literal support) fl::size find(const char* substr) const { if (!substr) { return npos; } auto begin = c_str(); const char* found = strstr(begin, substr); if (found) { return found - begin; } return npos; } // Find another string template fl::size find(const StrN& other) const { return find(other.c_str()); } // Find single character starting from position (like std::string) fl::size find(const char &value, fl::size start_pos) const { if (start_pos >= mLength) { return npos; } for (fl::size i = start_pos; i < mLength; ++i) { if (c_str()[i] == value) { return i; } } return npos; } // Find substring starting from position (like std::string) fl::size find(const char* substr, fl::size start_pos) const { if (!substr || start_pos >= mLength) { return npos; } auto begin = c_str() + start_pos; const char* found = strstr(begin, substr); if (found) { return found - c_str(); } return npos; } // Find another string starting from position (like std::string) template fl::size find(const StrN& other, fl::size start_pos) const { return find(other.c_str(), start_pos); } // Contains methods for C++23 compatibility bool contains(const char* substr) const { return find(substr) != npos; } bool contains(char c) const { return find(c) != npos; } template bool contains(const StrN& other) const { return find(other.c_str()) != npos; } // Starts with methods for C++20 compatibility bool starts_with(const char* prefix) const { if (!prefix) { return true; // Empty prefix matches any string } fl::size prefix_len = strlen(prefix); if (prefix_len > mLength) { return false; } return strncmp(c_str(), prefix, prefix_len) == 0; } bool starts_with(char c) const { return mLength > 0 && c_str()[0] == c; } template bool starts_with(const StrN& prefix) const { return starts_with(prefix.c_str()); } // Ends with methods for C++20 compatibility bool ends_with(const char* suffix) const { if (!suffix) { return true; // Empty suffix matches any string } fl::size suffix_len = strlen(suffix); if (suffix_len > mLength) { return false; } return strncmp(c_str() + mLength - suffix_len, suffix, suffix_len) == 0; } bool ends_with(char c) const { return mLength > 0 && c_str()[mLength - 1] == c; } template bool ends_with(const StrN& suffix) const { return ends_with(suffix.c_str()); } StrN substring(fl::size start, fl::size end) const { // short cut, it's the same string if (start == 0 && end == mLength) { return *this; } if (start >= mLength) { return StrN(); } if (end > mLength) { end = mLength; } if (start >= end) { return StrN(); } StrN out; out.copy(c_str() + start, end - start); return out; } StrN substr(fl::size start, fl::size length) const { // Standard substr(pos, length) behavior - convert to substring(start, end) fl::size end = start + length; if (end > mLength) { end = mLength; } return substring(start, end); } StrN substr(fl::size start) const { auto end = mLength; return substring(start, end); } void push_back(char c) { write(c); } // Push ASCII character without numeric conversion for display void push_ascii(char c) { write(c); } void pop_back() { if (mLength > 0) { mLength--; c_str_mutable()[mLength] = '\0'; } } StrN trim() const { StrN out; fl::size start = 0; fl::size end = mLength; while (start < mLength && StringFormatter::isSpace(c_str()[start])) { start++; } while (end > start && StringFormatter::isSpace(c_str()[end - 1])) { end--; } return substring(start, end); } float toFloat() const { return StringFormatter::parseFloat(c_str(), mLength); } private: StringHolderPtr mData; }; class string : public StrN { public: // Standard string npos constant for compatibility static const fl::size npos = static_cast(-1); static int strcmp(const string& a, const string& b); string() : StrN() {} string(const char *str) : StrN(str) {} string(const char *str, fl::size len) : StrN() { copy(str, len); } string(fl::size len, char c) : StrN() { resize(len, c); } string(const string &other) : StrN(other) {} template string(const StrN &other) : StrN(other) {} string &operator=(const string &other) { copy(other); return *this; } string &operator=(const char *str) { copy(str, strlen(str)); return *this; } #ifdef __EMSCRIPTEN__ string(const std::string &str) { copy(str.c_str(), str.size()); } string &operator=(const std::string &str) { copy(str.c_str(), str.size()); return *this; } // append string &append(const std::string &str) { write(str.c_str(), str.size()); return *this; } #endif bool operator>(const string &other) const { return strcmp(c_str(), other.c_str()) > 0; } bool operator>=(const string &other) const { return strcmp(c_str(), other.c_str()) >= 0; } bool operator<(const string &other) const { return strcmp(c_str(), other.c_str()) < 0; } bool operator<=(const string &other) const { return strcmp(c_str(), other.c_str()) <= 0; } bool operator==(const string &other) const { return strcmp(c_str(), other.c_str()) == 0; } bool operator!=(const string &other) const { return strcmp(c_str(), other.c_str()) != 0; } string &operator+=(const string &other) { append(other.c_str(), other.size()); return *this; } template string &operator+=(const T &val) { append(val); return *this; } template string &append(const BitsetFixed &bs) { bs.to_string(this); return *this; } string &append(const bitset_dynamic &bs) { bs.to_string(this); return *this; } template string &append(const BitsetInlined &bs) { bs.to_string(this); return *this; } char front() const { if (empty()) { return '\0'; } return c_str()[0]; } char back() const { if (empty()) { return '\0'; } return c_str()[size() - 1]; } // Push ASCII character without numeric conversion for display void push_ascii(char c) { write(c); } // Generic integral append: only enabled if T is an integral type. This is // needed because on some platforms type(int) is not one of the integral // types like i8, i16, i32, int64_t etc. In such a has just case // the value to i32 and then append it. template ::value>> string &append(const T &val) { write(i32(val)); return *this; } template string &append(const fl::span &slice) { append("["); for (fl::size i = 0; i < slice.size(); ++i) { if (i > 0) { append(", "); } append(slice[i]); } append("]"); return *this; } template string &append(const fl::HeapVector &vec) { fl::span slice(vec.data(), vec.size()); append(slice); return *this; } template string &append(const fl::InlinedVector &vec) { fl::span slice(vec.data(), vec.size()); append(slice); return *this; } string &append(const char *str) { write(str, strlen(str)); return *this; } string &append(const char *str, fl::size len) { write(str, len); return *this; } string &append(long long val) { write(i32(val)); return *this; } // string& append(char c) { write(&c, 1); return *this; } string &append(const i8 &c) { const char *str = fl::bit_cast_ptr(static_cast(&c)); write(str, 1); return *this; } string &append(const u8 &c) { write(static_cast(c)); return *this; } string &append(const u16 &val) { write(val); return *this; } string &append(const i16 &val) { write(i32(val)); return *this; } string &append(const u32 &val) { write(val); return *this; } string &append(const uint64_t &val) { write(val); return *this; } string &append(const i32 &c) { write(c); return *this; } string &append(const bool &val) { if (val) { return append("true"); } else { return append("false"); } } template string &append(const rect &rect) { append(rect.mMin.x); append(","); append(rect.mMin.y); append(","); append(rect.mMax.x); append(","); append(rect.mMax.y); return *this; } template string &append(const vec2 &pt) { append("("); append(pt.x); append(","); append(pt.y); append(")"); return *this; } template string &append(const vec3 &pt) { append("("); append(pt.x); append(","); append(pt.y); append(","); append(pt.z); append(")"); return *this; } template string &append(const WeakPtr &val) { fl::shared_ptr ptr = val.lock(); append(ptr); return *this; } template string &append(const fl::shared_ptr& val) { // append(val->toString()); if (!val) { append("shared_ptr(null)"); } else { T* ptr = val.get(); append("shared_ptr("); append(*ptr); append(")"); } return *this; } string &append(const JsonUiInternal& val); // JSON type append methods - implementations in str.cpp string &append(const JsonValue& val); string &append(const Json& val); template string &append(const fl::FixedVector &vec) { fl::span slice(vec.data(), vec.size()); append(slice); return *this; } string &append(const CRGB &c); string &append(const float &_val) { // round to nearest hundredth StringFormatter::appendFloat(_val, this); return *this; } string &append(const float &_val, int precision) { StringFormatter::appendFloat(_val, this, precision); return *this; } string &append(const double &val) { return append(float(val)); } string &append(const StrN &str) { write(str.c_str(), str.size()); return *this; } string &append(const FFTBins &str); string &append(const XYMap &map); string &append(const Tile2x2_u8_wrap &tile); template string &append(const HashSet &set) { append("{"); for (auto it = set.begin(); it != set.end(); ++it) { if (it != set.begin()) { append(", "); } auto p = *it; append(p.first); } append("}"); return *this; } // Support for fl::optional types template string &append(const fl::optional &opt) { if (opt.has_value()) { append(*opt); } else { append("nullopt"); } return *this; } const char *data() const { return c_str(); } void swap(string &other); // Resize methods to match std::string interface void resize(fl::size count) { resize(count, char()); } void resize(fl::size count, char ch) { if (count < mLength) { // Truncate the string mLength = count; c_str_mutable()[mLength] = '\0'; } else if (count > mLength) { // Extend the string with the specified character fl::size additional_chars = count - mLength; reserve(count); // Ensure enough capacity char* data_ptr = c_str_mutable(); for (fl::size i = 0; i < additional_chars; ++i) { data_ptr[mLength + i] = ch; } mLength = count; data_ptr[mLength] = '\0'; } // If count == mLength, do nothing } private: enum { // Bake the size into the string class so we can issue a compile time // check // to make sure the user doesn't do something funny like try to change // the // size of the inlined string via an included defined instead of a build // define. kStrInlineSize = FASTLED_STR_INLINED_SIZE, }; static void compileTimeAssertions(); }; // to_string template function for converting values to fl::string // This provides std::to_string equivalent functionality using fl::string // Delegates to fl::string::append which handles all type conversions template inline string to_string(T value) { string result; result.append(value); return result; } // Specialized to_string for float with precision inline string to_string(float value, int precision) { string result; result.append(value, precision); return result; } // Free operator+ functions for string concatenation // These allow expressions like: fl::string val = "string" + fl::to_string(5) // String literal + fl::string inline string operator+(const char* lhs, const string& rhs) { string result(lhs); result += rhs; return result; } // fl::string + string literal inline string operator+(const string& lhs, const char* rhs) { string result(lhs); result += rhs; return result; } // fl::string + fl::string inline string operator+(const string& lhs, const string& rhs) { string result(lhs); result += rhs; return result; } // String literal + any type that can be converted to string template inline string operator+(const char* lhs, const T& rhs) { string result(lhs); result += rhs; return result; } // Any type that can be converted to string + string literal template inline string operator+(const T& lhs, const char* rhs) { string result; result.append(lhs); result += rhs; return result; } // fl::string + any type that can be converted to string template inline string operator+(const string& lhs, const T& rhs) { string result(lhs); result += rhs; return result; } // Any type that can be converted to string + fl::string template inline string operator+(const T& lhs, const string& rhs) { string result; result.append(lhs); result += rhs; return result; } } // namespace fl