From 6270ae6181d9b24272da8a30671542778dc099bd Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Wed, 22 Jul 2026 12:35:32 +0100 Subject: [PATCH 01/36] part one of optimizations, created testbench app --- CMakeLists.txt | 20 +- crengine/include/lvstring.h | 63 ++- crengine/src/lvfont/lvfreetypeface.cpp | 8 +- crengine/src/lvmemman.cpp | 6 +- crengine/src/lvstring.cpp | 90 +++- crengine/src/lvstsheet.cpp | 10 +- crengine/src/lvtinydom.cpp | 8 +- tests/CMakeLists.txt | 44 ++ tests/src/testmain.cpp | 500 +++++++++++++++++++++++ tests/testdata/.keep | 0 thirdparty_unman/antiword/CMakeLists.txt | 2 + thirdparty_unman/chmlib/CMakeLists.txt | 2 + 12 files changed, 720 insertions(+), 33 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/src/testmain.cpp create mode 100644 tests/testdata/.keep diff --git a/CMakeLists.txt b/CMakeLists.txt index 740599dba4..ab37635334 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ endif() PROJECT(cr3) -set(CMAKE_CXX_STANDARD 17`) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules/") @@ -83,6 +83,16 @@ ADD_DEFINITIONS( -DUSE_EXTERNAL_EDICT_DICTIONARY=${USE_EXTERNAL_EDICT_DICTIONARY # Document buffer cache / compression settings +#own memory manager +if (NOT DEFINED LDOM_USE_OWN_MEM_MAN) + SET(LDOM_USE_OWN_MEM_MAN 0) +else() + SET(LDOM_USE_OWN_MEM_MAN ${LDOM_USE_OWN_MEM_MAN}) +endif(NOT DEFINED LDOM_USE_OWN_MEM_MAN) +message("using LDOM_USE_OWN_MEM_MAN=${LDOM_USE_OWN_MEM_MAN}") +ADD_DEFINITIONS( -DLDOM_USE_OWN_MEM_MAN=${LDOM_USE_OWN_MEM_MAN} ) + + #whether to compress data if (NOT DEFINED DOC_DATA_COMPRESSION_LEVEL) message("-D DOC_DATA_COMPRESSION_LEVEL=0|1|2|3|4|5 parameter is not defined: will use default value") @@ -147,7 +157,7 @@ endif (NOT DEFINED ENABLE_DBUS_VIEWER_EVENTS) SET (CRGUI_DEFS -DCOLOR_BACKBUFFER=0 -DLDOM_USE_OWN_MEM_MAN=0 -DGRAY_BACKBUFFER_BITS=${GRAY_BACKBUFFER_BITS} -DENABLE_DBUS_VIEWER_EVENTS=${ENABLE_DBUS_VIEWER_EVENTS}) -SET (DESKTOP_DEFS -DCOLOR_BACKBUFFER=1 -DLDOM_USE_OWN_MEM_MAN=1 -DUSE_FREETYPE=1 ) +SET (DESKTOP_DEFS -DCOLOR_BACKBUFFER=1 -DLDOM_USE_OWN_MEM_MAN=0 -DUSE_FREETYPE=1 ) # Uncomment to test e-ink style color palette on Desktop #SET (DESKTOP_DEFS -DCOLOR_BACKBUFFER=0 -DGRAY_BACKBUFFER_BITS=4 -DLDOM_USE_OWN_MEM_MAN=1 -DUSE_FREETYPE=1 ) @@ -167,6 +177,7 @@ if( ${GUI} STREQUAL QT5 ) add_definitions(${Qt5Core_DEFINITIONS}) endif( ${GUI} STREQUAL QT5 ) + # thirdparty sources definitions include("${CMAKE_SOURCE_DIR}/thirdparty_repo/repo_srcdirs.cmake") @@ -480,6 +491,11 @@ elseif ( ${GUI} STREQUAL QT6 ) ADD_DEFINITIONS( ${DESKTOP_DEFS} ) ADD_SUBDIRECTORY(crengine) ADD_SUBDIRECTORY(cr3qt) +elseif ( ${GUI} STREQUAL CR3TEST ) + message("Will make CR3TEST benchmarks") + #ADD_DEFINITIONS( ${DESKTOP_DEFS} ) + ADD_SUBDIRECTORY(crengine) + ADD_SUBDIRECTORY(tests) elseif ( ${GUI} STREQUAL WX ) message("Will make CR3/WX") ADD_DEFINITIONS( ${DESKTOP_DEFS} -DCR_WX_SUPPORT=1 ) diff --git a/crengine/include/lvstring.h b/crengine/include/lvstring.h index 7f9f060b71..4edd7a69b2 100644 --- a/crengine/include/lvstring.h +++ b/crengine/include/lvstring.h @@ -38,6 +38,7 @@ #include #include "lvtypes.h" #include "lvmemman.h" +#include "utility" // (Note: some of these 0x have lowercase hex digit, to avoid // 'redefined' warnings as they are already defined in lowercase @@ -347,13 +348,15 @@ class lString8 ++pchunk->refCount; #endif } - inline void release() { + inline void release() { + if (!pchunk) return; #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); #else if (--pchunk->refCount==0) free(); #endif + pchunk = nullptr; } inline int refCount() { return pchunk->refCount; @@ -366,6 +369,8 @@ class lString8 explicit lString8( int size ) : pchunk(EMPTY_STR_8) { addref(); reserve(size); } /// copy constructor lString8(const lString8 & str) : pchunk(str.pchunk) { addref(); } + /// move constructor + lString8(lString8&& str) : pchunk(str.pchunk) { str.pchunk = nullptr; } /// constructor from C string explicit lString8(const value_type * str); /// constructor from 16-bit C string @@ -388,6 +393,18 @@ class lString8 } return *this; } + /// move assignment + lString8 & assign(lString8 && str) + { + if (pchunk!=str.pchunk) + { + release(); + pchunk = str.pchunk; + str.pchunk = nullptr; + //addref(); + } + return *this; + } /// C-string assignment lString8 & assign(const value_type * str); /// C-string fragment assignment @@ -398,6 +415,8 @@ class lString8 lString8 & operator = (const value_type * str) { return assign(str); } /// string copy assignment lString8 & operator = (const lString8 & str) { return assign(str); } + /// string copy assignment + lString8 & operator = (lString8 && str) { return assign(std::move(str)); } /// erase part of string lString8 & erase(size_type offset, size_type count); /// append C-string @@ -607,13 +626,15 @@ class lString16 ++pchunk->refCount; #endif } - inline void release() { + inline void release() { + if (!pchunk) return; #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); #else if (--pchunk->refCount==0) free(); #endif + pchunk = nullptr; } inline int refCount() { return pchunk->refCount; @@ -624,6 +645,8 @@ class lString16 explicit lString16() : pchunk(EMPTY_STR_16) { addref(); } /// copy constructor lString16(const lString16 & str) : pchunk(str.pchunk) { addref(); } + /// move constructor + lString16(lString16 && str) : pchunk(str.pchunk) { str.pchunk = nullptr; } /// constructor from wide c-string lString16(const value_type * str); /// constructor from 8bit c-string (ASCII only) @@ -648,6 +671,17 @@ class lString16 } return *this; } + /// move assignment from string + lString16 & assign(lString16 && str) + { + if (pchunk!=str.pchunk) + { + release(); + pchunk = str.pchunk; + str.pchunk = nullptr; + } + return *this; + } /// assignment from c-string lString16 & assign(const value_type * str); /// assignment from 8bit c-string (ASCII only) @@ -664,6 +698,8 @@ class lString16 lString16 & operator = (const lChar8 * str) { return assign(str); } /// assignment from string lString16 & operator = (const lString16 & str) { return assign(str); } + /// move assignment from string + lString16 & operator = (lString16 && str) { return assign(std::move(str)); } lString16 & erase(size_type offset, size_type count); lString16 & append(const value_type * str); @@ -842,13 +878,15 @@ class lString32 ++pchunk->refCount; #endif } - inline void release() { + inline void release() { + if (!pchunk) return; #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); #else if (--pchunk->refCount==0) free(); #endif + pchunk = nullptr; } inline int refCount() { return pchunk->refCount; @@ -859,6 +897,8 @@ class lString32 explicit lString32() : pchunk(EMPTY_STR_32) { addref(); } /// copy constructor lString32(const lString32 & str) : pchunk(str.pchunk) { addref(); } + /// move constructor + lString32(lString32 && str) : pchunk(str.pchunk) { str.pchunk = nullptr; } /// constructor from wide c-string lString32(const value_type * str); /// constructor from 8bit c-string (ASCII only) @@ -870,7 +910,9 @@ class lString32 /// constructor from another string substring explicit lString32(const lString32 & str, size_type offset, size_type count); /// desctructor - ~lString32() { release(); } + ~lString32() { + release(); + } /// assignment from string lString32 & assign(const lString32 & str) @@ -883,6 +925,17 @@ class lString32 } return *this; } + /// assignment from string + lString32 & assign(lString32 && str) + { + if (pchunk!=str.pchunk) + { + release(); + pchunk = str.pchunk; + str.pchunk = nullptr; + } + return *this; + } /// assignment from c-string lString32 & assign(const value_type * str); /// assignment from 8bit c-string (ASCII only) @@ -899,6 +952,8 @@ class lString32 lString32 & operator = (const lChar8 * str) { return assign(str); } /// assignment from string lString32 & operator = (const lString32 & str) { return assign(str); } + /// moving assignment from string + lString32 & operator = (lString32 && str) { return assign(std::move(str)); } lString32 & erase(size_type offset, size_type count); lString32 & append(const value_type * str); diff --git a/crengine/src/lvfont/lvfreetypeface.cpp b/crengine/src/lvfont/lvfreetypeface.cpp index 53b4057822..77aaca9c7b 100644 --- a/crengine/src/lvfont/lvfreetypeface.cpp +++ b/crengine/src/lvfont/lvfreetypeface.cpp @@ -301,6 +301,9 @@ static LVFontGlyphCacheItem *newItem(LVFontLocalGlyphCache *local_cache, lChar32 if ( gammaIndex!=GAMMA_NO_CORRECTION_INDEX ) cr_correct_gamma_buf(item->bmp, bmp_sz, gammaIndex); break; + default: + // no correction + break; } } item->origin_x = (lInt16) slot->bitmap_left; @@ -343,6 +346,9 @@ static LVFontGlyphCacheItem *newItem(LVFontLocalGlyphCache *local_cache, lUInt32 if ( gammaIndex!=GAMMA_NO_CORRECTION_INDEX ) cr_correct_gamma_buf(item->bmp, bmp_sz, gammaIndex); break; + default: + // no correction + break; } } item->origin_x = (lInt16) slot->bitmap_left; @@ -2784,7 +2790,7 @@ int LVFreeTypeFace::DrawTextString(LVDrawBuf *buf, int x, int y, const lChar32 * if ( y + text_height < clip.top || y >= clip.bottom ) return 0; - unsigned int i; + int i; //lUInt16 prev_width = 0; lChar32 ch; // measure character widths diff --git a/crengine/src/lvmemman.cpp b/crengine/src/lvmemman.cpp index b035103e41..6ab81e5fb8 100644 --- a/crengine/src/lvmemman.cpp +++ b/crengine/src/lvmemman.cpp @@ -130,7 +130,9 @@ void cr_sigaction(int signal, siginfo_t *info, void *reserved) } #endif +#ifdef _LINUX static bool signals_are_set = false; +#endif void crSetSignalHandler() { #ifdef _LINUX @@ -168,8 +170,8 @@ lv_FatalErrorHandler_t * lvFatalErrorHandler = &lvDefFatalErrorHandler; void crFatalError( int code, const char * errorText ) { - if (file_to_remove_on_crash[0]) - LVDeleteFile(Utf8ToUnicode(lString8(file_to_remove_on_crash))); + if (file_to_remove_on_crash[0]) + LVDeleteFile(Utf8ToUnicode(lString8(file_to_remove_on_crash))); lvFatalErrorHandler( code, errorText ); } diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index cb8efb2a91..96ac2ef956 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -61,6 +61,7 @@ extern "C" { } #endif +// define LS_DEBUG_CHECK for debug string chunk allocations #define LS_DEBUG_CHECK // set to 1 to enable debugging @@ -185,44 +186,80 @@ struct lstring_chunk_slice_t { { p->buf8 = (char*)(p+1); p->size = 0; + p->len = 0; } - (pEnd-1)->buf8 = NULL; + (pEnd-1)->buf8 = nullptr; } ~lstring_chunk_slice_t() { free( pChunks ); + pChunks = nullptr; + pEnd = nullptr; + pFree = nullptr; } inline lstring8_chunk_t * alloc_chunk() { lstring8_chunk_t * res = pFree; pFree = (lstring8_chunk_t *)res->buf8; +#ifdef LS_DEBUG_CHECK + if ((lstring8_chunk_t *)res < pChunks || (lstring8_chunk_t *)res >= pEnd) { + crFatalError(); // corrupted pointer + } + res->buf8 = nullptr; + res->size = 0; + res->len = 0; + res->refCount = 0; +#endif return res; } inline lstring16_chunk_t * alloc_chunk16() { lstring16_chunk_t * res = (lstring16_chunk_t *)pFree; pFree = (lstring8_chunk_t *)res->buf16; +#ifdef LS_DEBUG_CHECK + if ((lstring8_chunk_t *)res < pChunks || (lstring8_chunk_t *)res >= pEnd) { + crFatalError(); // corrupted pointer + } + res->buf16 = nullptr; + res->size = 0; + res->len = 0; + res->refCount = 0; +#endif return res; } inline lstring32_chunk_t * alloc_chunk32() { lstring32_chunk_t * res = (lstring32_chunk_t *)pFree; pFree = (lstring8_chunk_t *)res->buf32; +#ifdef LS_DEBUG_CHECK + if ((lstring8_chunk_t *)res < pChunks || (lstring8_chunk_t *)res >= pEnd) { + crFatalError(); // corrupted pointer + } + res->buf32 = nullptr; + res->size = 0; + res->len = 0; + res->refCount = 0; +#endif return res; } inline bool free_chunk( lstring8_chunk_t * pChunk ) { if (pChunk < pChunks || pChunk >= pEnd) return false; // chunk does not belong to this slice -/* + #ifdef LS_DEBUG_CHECK if (!pChunk->size) { crFatalError(); // already freed!!! } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; + pChunk->len = 0; #endif -*/ + pChunk->buf8 = (char *)pFree; pFree = pChunk; return true; @@ -231,15 +268,19 @@ struct lstring_chunk_slice_t { { if ((lstring8_chunk_t *)pChunk < pChunks || (lstring8_chunk_t *)pChunk >= pEnd) return false; // chunk does not belong to this slice -/* + #ifdef LS_DEBUG_CHECK if (!pChunk->size) { crFatalError(); // already freed!!! } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; #endif -*/ + pChunk->buf16 = (lChar16 *)pFree; pFree = (lstring8_chunk_t *)pChunk; return true; @@ -248,15 +289,19 @@ struct lstring_chunk_slice_t { { if ((lstring8_chunk_t *)pChunk < pChunks || (lstring8_chunk_t *)pChunk >= pEnd) return false; // chunk does not belong to this slice -/* + #ifdef LS_DEBUG_CHECK if (!pChunk->size) { crFatalError(); // already freed!!! } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; #endif -*/ + pChunk->buf32 = (lChar32 *)pFree; pFree = (lstring8_chunk_t *)pChunk; return true; @@ -1190,8 +1235,13 @@ void lString32::reserve(size_type n) else { lstring_chunk_t * poldchunk = pchunk; + // ensure that reserved space is enough for len + size_type sz = n; + if (sz <= pchunk->len) { + sz = pchunk->len + 1; + } release(); - alloc( n ); + alloc( sz ); _lStr_memcpy( pchunk->buf32, poldchunk->buf32, poldchunk->len+1 ); pchunk->len = poldchunk->len; } @@ -1593,7 +1643,7 @@ bool lString32::atod( double &d, char dp ) const { s++; } } - if (res && *s == dp) { + if (res && (unsigned)*s == (unsigned)dp) { // decimal point found s++; res = false; @@ -1993,8 +2043,13 @@ void lString16::reserve(size_type n) else { lstring_chunk_t * poldchunk = pchunk; + // ensure that reserved space is enough for len + size_type sz = n; + if (sz <= pchunk->len) { + sz = pchunk->len + 1; + } release(); - alloc( n ); + alloc( sz ); _lStr_memcpy( pchunk->buf16, poldchunk->buf16, poldchunk->len+1 ); pchunk->len = poldchunk->len; } @@ -2596,8 +2651,13 @@ void lString8::reserve(size_type n) else { lstring_chunk_t * poldchunk = pchunk; + // ensure that reserved space is enough for len + size_type sz = n; + if (sz <= pchunk->len) { + sz = pchunk->len + 1; + } release(); - alloc( n ); + alloc( sz ); _lStr_memcpy( pchunk->buf8, poldchunk->buf8, poldchunk->len+1 ); pchunk->len = poldchunk->len; } @@ -3107,7 +3167,7 @@ int lString32::pos(const lChar8 * subStr) const { int flg = 1; for (int j=0; jbuf32[i+j] != subStr[j]) + if ((unsigned)pchunk->buf32[i+j] != (unsigned)subStr[j]) { flg = 0; break; @@ -3131,7 +3191,7 @@ int lString32::pos(const lChar8 * subStr, int start) const { int flg = 1; for (int j=0; jbuf32[i+j] != subStr[j]) + if ((unsigned)pchunk->buf32[i+j] != (unsigned)subStr[j]) { flg = 0; break; @@ -5687,7 +5747,7 @@ void lStr_findWordBounds( const lChar32 * str, int sz, int pos, int & start, int start = end = pos; return; } - hwEnd = hwStart+1; + //hwEnd = hwStart+1; // skipping while alpha for (; hwStart>0; hwStart--) { @@ -5971,7 +6031,7 @@ bool lString32::startsWith(const lChar8 * substring) const const lChar32 * s1 = c_str(); const lChar8 * s2 = substring; for ( int i=0; i= dom_version ) { + if ( domVersionRequested >= (lUInt32)dom_version ) { return false; // ignore the whole declaration } } @@ -4639,8 +4639,8 @@ bool LVStyleSheet::parse( const char * str, bool higher_importance, lString32 co } LVCssSelector * selector = NULL; LVCssSelector * prev_selector; - int err_count = 0; - int rule_count = 0; + //int err_count = 0; + //int rule_count = 0; lUInt32 domVersionRequested = _doc->getDOMVersionRequested(); for (;*str;) { @@ -4678,14 +4678,14 @@ bool LVStyleSheet::parse( const char * str, bool higher_importance, lString32 co if ( !decl->parse( str, domVersionRequested, higher_importance, _doc, codeBase ) ) { err = true; - err_count++; + //err_count++; } else { // set decl to selectors for (LVCssSelector * p = selector; p; p=p->getNext()) p->setDeclaration( decl ); - rule_count++; + //rule_count++; } break; } diff --git a/crengine/src/lvtinydom.cpp b/crengine/src/lvtinydom.cpp index 71a705b41e..ecd2c387f5 100644 --- a/crengine/src/lvtinydom.cpp +++ b/crengine/src/lvtinydom.cpp @@ -8120,7 +8120,7 @@ void ldomDocumentWriter::OnTagBody() } #if MATHML_SUPPORT==1 - if ( _currNode->_insideMathML ) { + if ( _currNode && _currNode->_insideMathML ) { // At this point, the style for this node has been applied. // Check if the element (or any of its parent) is display:none, // in which case we don't need to spend time handling MathML that @@ -12637,7 +12637,7 @@ bool ldomXPointerEx::isSentenceStart() ldomNode * node = getNode(); lString32 text = node->getText(); - int textLen = text.length(); + //int textLen = text.length(); int i = _data->getOffset(); lChar32 currCh = getChar(text, i); @@ -12690,7 +12690,7 @@ bool ldomXPointerEx::isSentenceEnd() ldomNode * node = getNode(); lString32 text = node->getText(); - int textLen = text.length(); + //int textLen = text.length(); int i = _data->getOffset(); lChar32 currCh = getChar(text, i); @@ -19617,7 +19617,7 @@ bool LVPageMap::deserialize( ldomDocument * doc, SerialBuf & buf ) if ( buf.error() ) return false; _page_info_valid = (bool)pageInfoValid; - for ( int i=0; ideserialize( doc, buf ) ) { delete item; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..e4b6f31ccf --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,44 @@ +SET (CR3TEST_SOURCES + src/testmain.cpp +) + +IF(${CMAKE_BUILD_TYPE} STREQUAL Debug) + ADD_DEFINITIONS( -D_DEBUG=1 -DDEBUG=1 ) +ELSE() + ADD_DEFINITIONS( -DNDEBUG ) +ENDIF(${CMAKE_BUILD_TYPE} STREQUAL Debug) + +if(MAC) + ADD_DEFINITIONS(-DCR3_DATA_DIR="" + -DUSE_FONTCONFIG=0 + ${CARBON_CFLAGS} + ) +ELSEIF (UNIX) + ADD_DEFINITIONS(-DCR3_DATA_DIR="${CMAKE_INSTALL_PREFIX}/share/cr3/") +ELSE() + ADD_DEFINITIONS(-DCR3_DATA_DIR="") +ENDIF(MAC) + +ADD_EXECUTABLE(cr3test ${CR3TEST_SOURCES}) +SET (EXTRA_LIBS fontconfig ${STD_LIBS}) +TARGET_LINK_LIBRARIES(cr3test crengine tinydict ${EXTRA_LIBS}) + +IF (UNIX) + INSTALL( TARGETS cr3test RUNTIME DESTINATION bin ) + INSTALL( DIRECTORY ../cr3qt/data DESTINATION share/cr3 + FILES_MATCHING PATTERN "*.css" ) + INSTALL( DIRECTORY ../cr3gui/data/hyph DESTINATION share/cr3 + FILES_MATCHING PATTERN "*.pattern" ) + INSTALL( DIRECTORY ../cr3qt/data/skins DESTINATION share/cr3/skins ) + INSTALL( FILES ../cr3qt/src/desktop/cr3.desktop DESTINATION share/applications ) + INSTALL( FILES ../cr3qt/src/desktop/cr3.appdata.xml DESTINATION share/metainfo ) + INSTALL( FILES ../cr3qt/src/desktop/cr3.png DESTINATION share/pixmaps ) + INSTALL( FILES ../cr3qt/src/desktop/cr3.xpm DESTINATION share/pixmaps ) +ELSE() + INSTALL( TARGETS cr3test RUNTIME DESTINATION . ) + INSTALL( DIRECTORY ../cr3qt/data/ DESTINATION . + FILES_MATCHING PATTERN "*.css" ) + INSTALL( DIRECTORY ../cr3gui/data/hyph DESTINATION . + FILES_MATCHING PATTERN "*.pattern" ) + INSTALL( DIRECTORY ../cr3qt/data/skins DESTINATION . ) +ENDIF(UNIX) diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp new file mode 100644 index 0000000000..c6ea9a332f --- /dev/null +++ b/tests/src/testmain.cpp @@ -0,0 +1,500 @@ +#include + +#include "../crengine/include/crengine.h" +#include "../crengine/include/cr3version.h" +#include +#include +#include + +//using unique_ptr = std::unique_ptr; + +bool getDirectoryFiles(lString32 path, lString32Collection & files) +{ + int foundCount = 0; + LVContainerRef dir = LVOpenDirectory(path.c_str()); + if ( !dir.isNull() ) { + CRLog::trace("Checking directory %s", path.c_str() ); + for ( int i=0; i < dir->GetObjectCount(); i++ ) { + const LVContainerItemInfo * item = dir->GetObjectInfo(i); + lString32 fileName = item->GetName(); + //printf("file in directory: %s\n", UnicodeToLocal(fileName).c_str()); + //printf(" test(%s) ", fn.c_str() ); + if ( !item->IsContainer() ) { + //bool found = false; + lString32 lc = fileName; + lc.lowercase(); + if (lc.startsWith(".")) { + continue; + } + lString32 fn; + fn << path; + LVAppendPathDelimiter(fn); + fn << fileName; + foundCount++; + files.add( fn ); + } + } + } else { + printf("Cannot open directory %s\n", UnicodeToLocal(path).c_str()); + } + return foundCount > 0; +} + +#if (USE_FREETYPE==1) +bool getDirectoryFonts( lString32Collection & pathList, lString32Collection & ext, lString32Collection & fonts, bool absPath ) +{ + int foundCount = 0; + lString32 path; + for ( int di=0; diGetObjectCount(); i++ ) { + const LVContainerItemInfo * item = dir->GetObjectInfo(i); + lString32 fileName = item->GetName(); + lString8 fn = UnicodeToLocal(fileName); + //printf(" test(%s) ", fn.c_str() ); + if ( !item->IsContainer() ) { + bool found = false; + lString32 lc = fileName; + lc.lowercase(); + for ( int j=0; j 0; +} +#endif + +lUInt64 currentTimeMillis() { + // Get the current time from the system clock + auto now = std::chrono::system_clock::now(); + + // Convert the current time to time since epoch + auto duration = now.time_since_epoch(); + + // Convert duration to milliseconds + auto milliseconds + = std::chrono::duration_cast( + duration) + .count(); + return milliseconds; +} + +struct PerfCounts { + lString32 fname; + int loadCount; + lUInt64 loadTime; + int renderCount; + lUInt64 renderTime; + int pageCount; + PerfCounts(lString32 fn = lString32::empty_str) + : fname(fn), loadCount(0), loadTime(0), renderCount(0), renderTime(0), pageCount(0) + { + } + ~PerfCounts() = default; + void reset() { + loadCount = 0; + loadTime = 0; + renderCount = 0; + renderTime = 0; + pageCount = 0; + } + void dump() { + printf("%s\t%lld\t%lld\t%d\n", UnicodeToLocal(fname).c_str(), loadTime, renderTime, pageCount); + } +}; + +using perf_vector = std::vector>; + +bool benchmarkFile(PerfCounts& perf) { + lString32 fname = perf.fname; + std::unique_ptr _docview { new LVDocView() }; + _docview->setViewMode(LVDocViewMode::DVM_PAGES, 2); + _docview->setRenderProps(2000, 1000); + _docview->setFontSize(30); +// _docview->up + + lUInt64 loadStart = currentTimeMillis(); + if (!_docview->LoadDocument(fname.c_str(), false)) { + return false; + } + perf.loadTime = currentTimeMillis() - loadStart; + perf.loadCount++; + lUInt64 renderStart = currentTimeMillis(); + _docview->Render(2000, 1000); + perf.renderTime = currentTimeMillis() - renderStart; + perf.pageCount += _docview->getPageCount(); + return true; +} + +void executeBenchmark(perf_vector& perfStats) { + for (auto &p : perfStats) { + printf("Opening file %s\n", UnicodeToLocal(p->fname).c_str()); + bool res = benchmarkFile(*p); + + if (!res) { + printf("Failed to open %s\n", UnicodeToLocal(p->fname).c_str()); + } + } +} + +void executeBenchmark(lString32Collection & testFiles) { + perf_vector perfStats; + for (int i = 0; i < testFiles.length(); i++) { + auto p = std::make_unique(testFiles[i]); + perfStats.push_back(std::move(p)); + } + + printf("Starting benchmark\n"); + executeBenchmark(perfStats); + + printf("Performance stats:\n"); + for (auto &p : perfStats) { + p->dump(); + } +} + +bool InitCREngine( const char * exename, lString32Collection & fontDirs ) +{ + CRLog::trace("InitCREngine(%s)", exename); +#ifdef _WIN32 + lString32 appname( exename ); + int lastSlash=-1; + lChar32 slashChar = '/'; + for ( int p=0; p<(int)appname.length(); p++ ) { + if ( appname[p]=='\\' ) { + slashChar = '\\'; + lastSlash = p; + } else if ( appname[p]=='/' ) { + slashChar = '/'; + lastSlash=p; + } + } + + lString32 appPath; + if ( lastSlash>=0 ) + appPath = appname.substr( 0, lastSlash+1 ); + InitCREngineLog(UnicodeToUtf8(appPath).c_str()); + lString32 datadir = appPath; +#else + lString32 datadir = lString32(CR3_DATA_DIR); +#endif + lString32 fontDir = datadir + "fonts"; + lString8 fontDir8_ = UnicodeToUtf8(fontDir); + + fontDirs.add( fontDir ); + + LVAppendPathDelimiter( fontDir ); + + lString8 fontDir8 = UnicodeToLocal(fontDir); + //const char * fontDir8s = fontDir8.c_str(); + //InitFontManager( fontDir8 ); + InitFontManager(lString8::empty_str); + +#if defined(_WIN32) && USE_FONTCONFIG!=1 + wchar_t sysdir_w[MAX_PATH+1]; + GetWindowsDirectoryW(sysdir_w, MAX_PATH); + lString32 fontdir = Utf16ToUnicode( sysdir_w ); + fontdir << "\\Fonts\\"; + lString8 fontdir8( UnicodeToUtf8(fontdir) ); + const char * fontnames[] = { + "arial.ttf", + "ariali.ttf", + "arialb.ttf", + "arialbi.ttf", + "arialn.ttf", + "arialni.ttf", + "arialnb.ttf", + "arialnbi.ttf", + "cour.ttf", + "couri.ttf", + "courbd.ttf", + "courbi.ttf", + "times.ttf", + "timesi.ttf", + "timesb.ttf", + "timesbi.ttf", + "comic.ttf", + "comicbd.ttf", + "verdana.ttf", + "verdanai.ttf", + "verdanab.ttf", + "verdanaz.ttf", + "bookos.ttf", + "bookosi.ttf", + "bookosb.ttf", + "bookosbi.ttf", + "calibri.ttf", + "calibrii.ttf", + "calibrib.ttf", + "calibriz.ttf", + "cambria.ttf", + "cambriai.ttf", + "cambriab.ttf", + "cambriaz.ttf", + "georgia.ttf", + "georgiai.ttf", + "georgiab.ttf", + "georgiaz.ttf", + NULL + }; + for ( int fi = 0; fontnames[fi]; fi++ ) { + fontMan->RegisterFont( fontdir8 + fontnames[fi] ); + } +#endif // defined(_WIN32) && USE_FONTCONFIG!=1 + // Load font definitions into font manager + // fonts are in files font1.lbf, font2.lbf, ... font32.lbf + // use fontconfig + +#if USE_FREETYPE==1 + lString32Collection fontExt; + fontExt.add(cs32(".ttf")); + fontExt.add(cs32(".otf")); + fontExt.add(cs32(".pfa")); + fontExt.add(cs32(".pfb")); + lString32Collection fonts; + + getDirectoryFonts( fontDirs, fontExt, fonts, true ); + + // load fonts from file + CRLog::debug("%d font files found", fonts.length()); + //if (!fontMan->GetFontCount()) { + for ( int fi=0; fiRegisterFont(fn) ) { + CRLog::trace(" failed\n"); + } + } + //} +#endif // USE_FREETYPE==1 + + // init hyphenation manager + //char hyphfn[1024]; + //sprintf(hyphfn, "Russian_EnUS_hyphen_(Alan).pdb" ); + //if ( !initHyph( (UnicodeToLocal(appPath) + hyphfn).c_str() ) ) { +#ifdef _LINUX + // initHyph( "/usr/share/crengine/hyph/Russian_EnUS_hyphen_(Alan).pdb" ); +#endif + //} + + if (!fontMan->GetFontCount()) + { + //error +#if (USE_FREETYPE==1) + printf("Fatal Error: Cannot open font file(s) .ttf \nCannot work without font\n" ); +#else + printf("Fatal Error: Cannot open font file(s) font#.lbf \nCannot work without font\nUse FontConv utility to generate .lbf fonts from TTF\n" ); +#endif + return false; + } + + printf("%d fonts loaded.\n", fontMan->GetFontCount()); + + return true; + +} + +void InitCREngineLog( const char * cfgfile ) +{ + if ( !cfgfile ) { + CRLog::setStdoutLogger(); + CRLog::setLogLevel( CRLog::LL_TRACE ); + return; + } + lString32 logfname; + lString32 loglevelstr = +#ifdef _DEBUG + U"TRACE"; +#else + U"INFO"; +#endif + bool autoFlush = false; + CRPropRef logprops = LVCreatePropsContainer(); + { + LVStreamRef cfg = LVOpenFileStream( cfgfile, LVOM_READ ); + if ( !cfg.isNull() ) { + logprops->loadFromStream( cfg.get() ); + logfname = logprops->getStringDef( PROP_LOG_FILENAME, "stdout" ); + loglevelstr = logprops->getStringDef( PROP_LOG_LEVEL, "TRACE" ); + autoFlush = logprops->getBoolDef( PROP_LOG_AUTOFLUSH, false ); + } + } + CRLog::log_level level = CRLog::LL_INFO; + if (loglevelstr == "OFF") { + level = CRLog::LL_FATAL; + logfname.clear(); + } else if (loglevelstr == "FATAL") { + level = CRLog::LL_FATAL; + } else if (loglevelstr == "ERROR") { + level = CRLog::LL_ERROR; + } else if (loglevelstr == "WARN") { + level = CRLog::LL_WARN; + } else if (loglevelstr == "INFO") { + level = CRLog::LL_INFO; + } else if (loglevelstr == "DEBUG") { + level = CRLog::LL_DEBUG; + } else if (loglevelstr == "TRACE") { + level = CRLog::LL_TRACE; + } + if ( !logfname.empty() ) { + if (logfname == "stdout") + CRLog::setStdoutLogger(); + else if (logfname == "stderr") + CRLog::setStderrLogger(); + else + CRLog::setFileLogger( UnicodeToUtf8( logfname ).c_str(), autoFlush ); + } + CRLog::setLogLevel( level ); + CRLog::trace("Log initialization done."); +} + +void ShutdownCREngine() +{ + HyphMan::uninit(); + ShutdownFontManager(); + CRLog::setLogger( NULL ); +#if LDOM_USE_OWN_MEM_MAN == 1 +// ldomFreeStorage(); +#endif +} + +void testStrings() { + printf("Strings: checkpoint 1\n"); + { + lString8 s("qwerty"); + lString8 a("string_a"); + lString8 b("string_b"); + lString8 c = a + b; + s += "123"; + s += c; + } + printf("Strings: checkpoint 2\n"); + { + lString16 s("qwerty"); + lString16 a("string_a"); + lString16 b("string_b"); + //lString16 c = a + b; + s += "123"; + //s += c; + } + printf("Strings: checkpoint 3\n"); + { + lString32 s("qwerty"); + s += "123"; + } + printf("Strings: checkpoint 4\n"); +} + +int main(int argc, const char ** argv) { + + printf("CR3 Performance Tests\n"); + testStrings(); + + lString32Collection files; + lString8 bookDir("../tests/testdata"); + for (int i = 1; i < argc; i++) { + if (argv[i][0]=='-') { + printf("option: %s\n", argv[i]); + } else { + bookDir = argv[i]; + if (!getDirectoryFiles(LocalToUnicode(bookDir), files)) { + printf("No book files found in directory %s\n", bookDir.c_str()); + return 3; + } + } + } + //LVAppendPathDelimiter(bookDir); + printf("Book dir: %s\n", bookDir.c_str()); +#ifdef DEBUG + lString8 loglevel("TRACE"); + lString8 logfile("stdout"); +#else + lString8 loglevel("ERROR"); + lString8 logfile("stderr"); +#endif + + // set logger + if ( logfile=="stdout" ) + CRLog::setStdoutLogger(); + else if ( logfile=="stderr" ) + CRLog::setStderrLogger(); + else if ( !logfile.empty() ) + CRLog::setFileLogger(logfile.c_str()); + if ( loglevel=="TRACE" ) + CRLog::setLogLevel(CRLog::LL_TRACE); + else if ( loglevel=="DEBUG" ) + CRLog::setLogLevel(CRLog::LL_DEBUG); + else if ( loglevel=="INFO" ) + CRLog::setLogLevel(CRLog::LL_INFO); + else if ( loglevel=="WARN" ) + CRLog::setLogLevel(CRLog::LL_WARN); + else if ( loglevel=="ERROR" ) + CRLog::setLogLevel(CRLog::LL_ERROR); + else + CRLog::setLogLevel(CRLog::LL_FATAL); + + lString32 exename = LocalToUnicode( lString8(argv[0]) ); + lString32 exedir = LVExtractPath(exename); + lString32 datadir = lString32(CR3_DATA_DIR); + LVAppendPathDelimiter(exedir); + LVAppendPathDelimiter(datadir); + lString32 exefontpath = exedir + "fonts"; + CRLog::info("main()"); + lString32Collection fontDirs; + + lString32 home = Utf8ToUnicode(lString8(( getenv("HOME") ) )); + lString32 homecr3 = home; + LVAppendPathDelimiter(homecr3); + homecr3 << ".cr3"; + LVAppendPathDelimiter(homecr3); + //~/.cr3/ + lString32 homefonts = homecr3; + homefonts << "fonts"; + + //fontDirs.add( cs32("/usr/local/share/crengine/fonts") ); + //fontDirs.add( cs32("/usr/local/share/fonts/truetype/freefont") ); + //fontDirs.add( cs32("/mnt/fonts") ); + fontDirs.add(homefonts); + + if ( !InitCREngine( argv[0], fontDirs ) ) { + printf("Cannot init CREngine - exiting\n"); + return 2; + } + + // use cache in current directory + if (!ldomDocCache::init( lString32(".cr3/cache"), 0x100000 * 256 )) { + printf("Cannot init doc cache!\n"); + } else { + ldomDocCache::clear(); + } + + + for (int i = 0; i < files.length(); i++) { + printf("test file [%d]: %s\n", i, UnicodeToLocal(files[i]).c_str()); + } + + executeBenchmark(files); + + ShutdownCREngine(); + return 0; +} diff --git a/tests/testdata/.keep b/tests/testdata/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/thirdparty_unman/antiword/CMakeLists.txt b/thirdparty_unman/antiword/CMakeLists.txt index 5b27e75eb5..4fa5444986 100644 --- a/thirdparty_unman/antiword/CMakeLists.txt +++ b/thirdparty_unman/antiword/CMakeLists.txt @@ -6,6 +6,8 @@ ENDIF(NOT CMAKE_BUILD_TYPE) IF (${CMAKE_BUILD_TYPE} STREQUAL Release) ADD_DEFINITIONS( -DNDEBUG=1 ) +ELSEIF (${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo) + ADD_DEFINITIONS( -DNDEBUG=1 ) ELSE() ADD_DEFINITIONS( -DDEBUG=1 ) ENDIF(${CMAKE_BUILD_TYPE} STREQUAL Release) diff --git a/thirdparty_unman/chmlib/CMakeLists.txt b/thirdparty_unman/chmlib/CMakeLists.txt index fce7510001..4ea5b6edd1 100644 --- a/thirdparty_unman/chmlib/CMakeLists.txt +++ b/thirdparty_unman/chmlib/CMakeLists.txt @@ -6,6 +6,8 @@ ENDIF(NOT CMAKE_BUILD_TYPE) IF (${CMAKE_BUILD_TYPE} STREQUAL Release) ADD_DEFINITIONS( -DNDEBUG=1 ) +ELSEIF (${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo) + ADD_DEFINITIONS( -DNDEBUG=1 ) ELSE() ADD_DEFINITIONS( -DDEBUG=1 ) ENDIF(${CMAKE_BUILD_TYPE} STREQUAL Release) From 799724fd2b2d352870e3d55d4e4484ee4384ccc0 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Wed, 22 Jul 2026 19:06:30 +0100 Subject: [PATCH 02/36] trying to get working with memory pool turned on --- crengine/include/lvstring.h | 20 ++++++- crengine/src/lvstring.cpp | 93 +++++++++++++++++++++----------- tests/src/testmain.cpp | 103 +++++++++++++++++++++++++++++------- 3 files changed, 165 insertions(+), 51 deletions(-) diff --git a/crengine/include/lvstring.h b/crengine/include/lvstring.h index 4edd7a69b2..323ac4a2ae 100644 --- a/crengine/include/lvstring.h +++ b/crengine/include/lvstring.h @@ -370,7 +370,10 @@ class lString8 /// copy constructor lString8(const lString8 & str) : pchunk(str.pchunk) { addref(); } /// move constructor - lString8(lString8&& str) : pchunk(str.pchunk) { str.pchunk = nullptr; } + lString8(lString8&& str) : pchunk(nullptr) { + pchunk = str.pchunk; + str.pchunk = nullptr; + } /// constructor from C string explicit lString8(const value_type * str); /// constructor from 16-bit C string @@ -646,7 +649,10 @@ class lString16 /// copy constructor lString16(const lString16 & str) : pchunk(str.pchunk) { addref(); } /// move constructor - lString16(lString16 && str) : pchunk(str.pchunk) { str.pchunk = nullptr; } + lString16(lString16 && str) : pchunk(nullptr) { + pchunk = str.pchunk; + str.pchunk = nullptr; + } /// constructor from wide c-string lString16(const value_type * str); /// constructor from 8bit c-string (ASCII only) @@ -702,6 +708,8 @@ class lString16 lString16 & operator = (lString16 && str) { return assign(std::move(str)); } lString16 & erase(size_type offset, size_type count); + lString16 & append(const lChar32 * str); + lString16 & append(const lChar32 * str, size_type count); lString16 & append(const value_type * str); lString16 & append(const value_type * str, size_type count); lString16 & append(const lChar8 * str); @@ -960,6 +968,8 @@ class lString32 lString32 & append(const value_type * str, size_type count); lString32 & append(const lChar8 * str); lString32 & append(const lChar8 * str, size_type count); + lString32 & append(const lChar16 * str); + lString32 & append(const lChar16 * str, size_type count); lString32 & append(const lString32 & str); lString32 & append(const lString32 & str, size_type offset, size_type count); lString32 & append(size_type count, value_type ch); @@ -1241,6 +1251,7 @@ inline bool operator != (const lChar8 * s1, const lString16& s2 ) { return s2.compare(s1)!=0; } inline bool operator != (const lChar8 * s1, const lString32& s2 ) { return s2.compare(s1)!=0; } +inline lString32 operator + (const lString32 &s1, const lChar16 * s2) { lString32 s(s1); s.append(s2); return s; } inline lString32 operator + (const lString32 &s1, const lString32 &s2) { lString32 s(s1); s.append(s2); return s; } inline lString32 operator + (const lString32 &s1, const lChar32 * s2) { lString32 s(s1); s.append(s2); return s; } inline lString32 operator + (const lString32 &s1, const lChar8 * s2) { lString32 s(s1); s.append(s2); return s; } @@ -1270,6 +1281,11 @@ inline lString8 operator + (const lString8 &s1, fmt::decimal v) inline lString8 operator + (const lString8 &s1, fmt::hex v) { lString8 s(s1); s.appendHex(v.get()); return s; } +inline lString16 operator + (const lString16 &s1, const lChar32 * s2) { lString16 s(s1); s.append(s2); return s; } +inline lString16 operator + (const lString16 &s1, const lString16 &s2) { lString16 s(s1); s.append(s2); return s; } +inline lString16 operator + (const lString16 &s1, const lChar16 * s2) { lString16 s(s1); s.append(s2); return s; } +inline lString16 operator + (const lString16 &s1, fmt::decimal v) { lString16 s(s1); s.appendDecimal(v.get()); return s; } +inline lString16 operator + (const lString16 &s1, fmt::hex v) { lString16 s(s1); s.appendHex(v.get()); return s; } lString8 UnicodeToTranslit( const lString32 & str ); /// converts wide unicode string to local 8-bit encoding diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index 96ac2ef956..fe1c2099b2 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -248,14 +248,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - if (!pChunk->size) - { - crFatalError(); // already freed!!! - } - if (pChunk->refCount) - { - crFatalError(); // has references - } + //if (!pChunk->size) + //{ + // crFatalError(); // already freed!!! + //} + //if (pChunk->refCount) + //{ + // crFatalError(); // has references + //} pChunk->size = 0; pChunk->len = 0; #endif @@ -270,14 +270,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - if (!pChunk->size) - { - crFatalError(); // already freed!!! - } - if (pChunk->refCount) - { - crFatalError(); // has references - } + // if (!pChunk->size) + // { + // crFatalError(); // already freed!!! + // } + // if (pChunk->refCount) + // { + // crFatalError(); // has references + // } pChunk->size = 0; #endif @@ -291,14 +291,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - if (!pChunk->size) - { - crFatalError(); // already freed!!! - } - if (pChunk->refCount) - { - crFatalError(); // has references - } + // if (!pChunk->size) + // { + // crFatalError(); // already freed!!! + // } + // if (pChunk->refCount) + // { + // crFatalError(); // has references + // } pChunk->size = 0; #endif @@ -927,11 +927,14 @@ int lStr_cmp(const lChar16 * dst, const lChar32 * src) void lString32::free() { - if ( pchunk==EMPTY_STR_32 ) + if ( pchunk==EMPTY_STR_32 || pchunk == nullptr) return; //assert(pchunk->buf32[pchunk->len]==0); - ::free(pchunk->buf32); + if (pchunk->buf32 != nullptr) { + ::free(pchunk->buf32); + } #if (LDOM_USE_OWN_MEM_MAN == 1) + for (int i=slices_count-1; i>=0; --i) { if (slices[i]->free_chunk32(pchunk)) @@ -941,6 +944,7 @@ void lString32::free() #else ::free(pchunk); #endif + pchunk = nullptr; } void lString32::alloc(int sz) @@ -1323,6 +1327,18 @@ lString32 & lString32::append(const lChar8 * str, size_type count) return *this; } +lString32 & lString32::append(const lChar16 * str) +{ + size_type len = _lStr_len(str); + return append(Utf16ToUnicode(str, len)); +} + +lString32 & lString32::append(const lChar16 * str, size_type count) +{ + reserve(pchunk->len + count); + return append(Utf16ToUnicode(str, count)); +} + lString32 & lString32::append(const lString32 & str) { size_type len2 = pchunk->len + str.pchunk->len; @@ -1733,10 +1749,12 @@ const lString32 lString32::empty_str; void lString16::free() { - if ( pchunk==EMPTY_STR_16 ) + if ( pchunk==EMPTY_STR_16 || pchunk == nullptr) return; //assert(pchunk->buf16[pchunk->len]==0); - ::free(pchunk->buf16); + if (pchunk->buf16 != nullptr) { + ::free(pchunk->buf16); + } #if (LDOM_USE_OWN_MEM_MAN == 1) for (int i=slices_count-1; i>=0; --i) { @@ -1747,6 +1765,7 @@ void lString16::free() #else ::free(pchunk); #endif + pchunk = nullptr; } void lString16::alloc(int sz) @@ -2114,6 +2133,17 @@ lString16 & lString16::append(const value_type * str, size_type count) return *this; } +lString16 & lString16::append(const lChar32 * str) +{ + size_type len = _lStr_len(str); + return append(UnicodeToUtf16(str, len)); +} + +lString16 & lString16::append(const lChar32 * str, size_type count) +{ + return append(UnicodeToUtf16(str, count)); +} + lString16 & lString16::append(const lChar8 * str) { size_type len = _lStr_len(str); @@ -2416,9 +2446,11 @@ const lString16 lString16::empty_str; void lString8::free() { - if ( pchunk==EMPTY_STR_8 ) + if ( pchunk==EMPTY_STR_8 || pchunk == nullptr) return; - ::free(pchunk->buf8); + if (pchunk->buf8 != nullptr) { + ::free(pchunk->buf8); + } #if (LDOM_USE_OWN_MEM_MAN == 1) for (int i=slices_count-1; i>=0; --i) { @@ -2429,6 +2461,7 @@ void lString8::free() #else ::free(pchunk); #endif + pchunk = nullptr; } void lString8::alloc(int sz) diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index c6ea9a332f..539b303a85 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -378,37 +378,102 @@ void ShutdownCREngine() #endif } -void testStrings() { - printf("Strings: checkpoint 1\n"); +void testStringCollections() { { - lString8 s("qwerty"); - lString8 a("string_a"); - lString8 b("string_b"); - lString8 c = a + b; - s += "123"; - s += c; + lString8Collection list; + { + lString8 s {"test_s_1"}; + assert(s == "test_s_1"); + list.add(s); + assert(s == "test_s_1"); + } + { + lString8 s {"test_s_2"}; + assert(s == "test_s_2"); + list.add(s); + assert(s == "test_s_2"); + } } - printf("Strings: checkpoint 2\n"); { - lString16 s("qwerty"); - lString16 a("string_a"); - lString16 b("string_b"); - //lString16 c = a + b; - s += "123"; - //s += c; + lString8Collection list; + { + lString8 s {"test_s_3"}; + assert(s == "test_s_3"); + list.add(s); + assert(s == "test_s_3"); + } + { + lString8 s {"test_s_4"}; + assert(s == "test_s_4"); + list.add(s); + assert(s == "test_s_4"); + } } - printf("Strings: checkpoint 3\n"); { - lString32 s("qwerty"); - s += "123"; + lString32Collection list; + { + lString32 s {U"test_s_5"}; + assert(s == "test_s_5"); + list.add(s); + assert(s == "test_s_5"); + } + { + lString32 s {U"test_s_5"}; + assert(s == "test_s_5"); + list.add(s); + assert(s == "test_s_5"); + } } - printf("Strings: checkpoint 4\n"); + + for (int sz = 1; sz < 1000; sz++) { + lString8Collection list; + for (int i = 0; i < sz; i++) { + lString8 s {"String8"}; + //s << i; + list.add(s); + } + } + for (int sz = 1; sz < 1000; sz++) { + lString32Collection list; + for (int i = 0; i < sz; i++) { + lString32 s {U"String32"}; + //s << i; + list.add(s); + } + } +} + +void testStrings() { + printf("Testing lString8 construction...\n"); + lString8 test1("hello"); + lString8 test2 = lString8("world"); + lString8 combined = test1 + "_" + test2; + + printf("Testing lString16 construction...\n"); + lString16 test16_1 = lString16::empty_str; + lString16 test16_1b("hello"); + lString16 test16_2 = lString16::empty_str; + lString16 test16_2b("world"); + lString16 combined16 = test16_1b + u"_" + test16_2b; + //lString16 combined16a = test16_1b + "_" + combined16; + lString16 combined16b = test16_1b + U"_" + combined16; + + printf("Testing lString32 construction...\n"); + lString32 test32_1 = lString32::empty_str; + lString32 test32_1b("hello"); + lString32 test32_2 = lString32::empty_str; + lString32 test32_2b("world"); + lString32 combined32 = test32_1b + u"_" + test32_2b; + lString32 combined32a = test32_1b + U"_" + test32_2b; + + printf("All string tests passed!\n"); } int main(int argc, const char ** argv) { printf("CR3 Performance Tests\n"); testStrings(); + testStringCollections(); lString32Collection files; lString8 bookDir("../tests/testdata"); From 4258b748189d83916cfddca24bbdb06deda337da Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Thu, 23 Jul 2026 10:32:04 +0100 Subject: [PATCH 03/36] extended lStringXX test coverage --- tests/src/testmain.cpp | 738 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 713 insertions(+), 25 deletions(-) diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index 539b303a85..c15b7c36a0 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -326,7 +326,7 @@ void InitCREngineLog( const char * cfgfile ) #ifdef _DEBUG U"TRACE"; #else - U"INFO"; + U"TRACE"; #endif bool autoFlush = false; CRPropRef logprops = LVCreatePropsContainer(); @@ -370,6 +370,7 @@ void InitCREngineLog( const char * cfgfile ) void ShutdownCREngine() { + CRLog::info("Shutting down CREngine..."); HyphMan::uninit(); ShutdownFontManager(); CRLog::setLogger( NULL ); @@ -443,38 +444,723 @@ void testStringCollections() { } } +static int test_errors = 0; +#define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) + +// Reuse atod from lString32 +static double lString32_atod(const lString32& s) { return s.atod(); } + void testStrings() { - printf("Testing lString8 construction...\n"); - lString8 test1("hello"); - lString8 test2 = lString8("world"); - lString8 combined = test1 + "_" + test2; - - printf("Testing lString16 construction...\n"); - lString16 test16_1 = lString16::empty_str; - lString16 test16_1b("hello"); - lString16 test16_2 = lString16::empty_str; - lString16 test16_2b("world"); - lString16 combined16 = test16_1b + u"_" + test16_2b; - //lString16 combined16a = test16_1b + "_" + combined16; - lString16 combined16b = test16_1b + U"_" + combined16; - - printf("Testing lString32 construction...\n"); - lString32 test32_1 = lString32::empty_str; - lString32 test32_1b("hello"); - lString32 test32_2 = lString32::empty_str; - lString32 test32_2b("world"); - lString32 combined32 = test32_1b + u"_" + test32_2b; - lString32 combined32a = test32_1b + U"_" + test32_2b; - - printf("All string tests passed!\n"); + printf("=== lString8 tests ===\n"); + + // Constructors + lString8 s8_default; + TCHECK(s8_default.empty()); + + lString8 s8_size(10); + TCHECK(s8_size.empty()); + + lString8 s8_cstr("hello"); + TCHECK(s8_cstr.length() == 5); + TCHECK(s8_cstr == "hello"); + + lString8 s8_fragment("hello world", 5); + TCHECK(s8_fragment == "hello"); + + lString8 s8_sub(s8_cstr, 1, 3); + TCHECK(s8_sub == "ell"); + + // Copy / move + lString8 s8_copy(s8_cstr); + TCHECK(s8_copy == "hello"); + lString8 s8_move(std::move(s8_copy)); + TCHECK(s8_move == "hello"); + + // Assignment + lString8 s8_assign; + s8_assign = s8_move; + TCHECK(s8_assign == "hello"); + lString8 s8_assign2; + s8_assign2 = "world"; + TCHECK(s8_assign2 == "world"); + lString8 s8_assign3; + s8_assign3 = lString8("temp"); + TCHECK(s8_assign3 == "temp"); + + // assign methods + lString8 s8_a; + s8_a.assign("test"); + TCHECK(s8_a == "test"); + s8_a.assign("abcdef", 3); + TCHECK(s8_a == "abc"); + lString8 s8_b("xyz"); + s8_a.assign(s8_b); + TCHECK(s8_a == "xyz"); + s8_a.assign(lString8("move_assign")); + TCHECK(s8_a == "move_assign"); + + // Append + lString8 s8_app; + s8_app.append("foo"); + TCHECK(s8_app == "foo"); + s8_app.append("xy", 1); + TCHECK(s8_app == "foox"); + s8_app.append(3, '!'); + TCHECK(s8_app == "foox!!!"); + s8_app.clear(); + + // appendDecimal / appendHex + s8_app.appendDecimal(12345); + TCHECK(s8_app == "12345"); + s8_app.clear(); + s8_app.appendDecimal(-42); + TCHECK(s8_app == "-42"); + s8_app.clear(); + s8_app.appendHex(0xFF); + TCHECK(s8_app == "ff"); + s8_app.clear(); + + // Insert (char-repeat variant) + lString8 s8_ins("world"); + s8_ins.insert(0, 2, 'X'); + TCHECK(s8_ins == "XXworld"); + + // Replace + lString8 s8_rep("hello world"); + s8_rep.replace(0, 5, lString8("good")); + TCHECK(s8_rep == "good world"); + lString8 s8_rep2("abcabc"); + s8_rep2.replace('a', 'X'); + TCHECK(s8_rep2 == "XbcXbc"); + + // Erase + lString8 s8_era("0123456789"); + s8_era.erase(5, 5); + TCHECK(s8_era == "01234"); + + // uppercase / lowercase + lString8 s8_case("Hello World"); + s8_case.uppercase(); + TCHECK(s8_case == "HELLO WORLD"); + s8_case.lowercase(); + TCHECK(s8_case == "hello world"); + + // compare + TCHECK(s8_cstr.compare("hello") == 0); + TCHECK(s8_cstr.compare("world") < 0); + TCHECK(s8_cstr.compare("abc") > 0); + + // pos / rpos + lString8 s8_pos("hello world, hello"); + TCHECK(s8_pos.pos('w') == 6); + TCHECK(s8_pos.pos("world") == 6); + TCHECK(s8_pos.pos('h', 1) == 13); + TCHECK(s8_pos.rpos("hello") == 13); + TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); + + // startsWith / endsWith + lString8 s8_sw("hello.cpp"); + TCHECK(s8_sw.startsWith("hello")); + TCHECK(s8_sw.startsWith(lString8("hello"))); + TCHECK(!s8_sw.startsWith("world")); + TCHECK(s8_sw.endsWith(".cpp")); + + // substr + lString8 s8_sub2("hello world"); + TCHECK(s8_sub2.substr(6) == "world"); + TCHECK(s8_sub2.substr(0, 5) == "hello"); + + // operator[] + TCHECK(s8_sub2[0] == 'h'); + TCHECK(s8_sub2[4] == 'o'); + + // data / c_str + TCHECK(strcmp(s8_sub2.c_str(), "hello world") == 0); + TCHECK(strcmp(s8_sub2.data(), "hello world") == 0); + + // length / size / capacity / empty + TCHECK(s8_sub2.length() == 11); + TCHECK(s8_sub2.size() == 11); + TCHECK(!s8_sub2.empty()); + lString8 s8_empty; + TCHECK(s8_empty.empty()); + TCHECK(!s8_empty); + + // at (safe access) + TCHECK(s8_sub2.at(0) == 'h'); + TCHECK(s8_sub2.at(10) == 'd'); + + // atoi + lString8 s8_int("12345"); + TCHECK(s8_int.atoi() == 12345); + lString8 s8_int2("-999"); + TCHECK(s8_int2.atoi() == -999); + lString8 s8_int64("1234567890123"); + TCHECK(s8_int64.atoi64() == 1234567890123LL); + + // swap + lString8 s8_swap1("first"); + lString8 s8_swap2("second"); + s8_swap1.swap(s8_swap2); + TCHECK(s8_swap1 == "second"); + TCHECK(s8_swap2 == "first"); + + // trim + lString8 s8_trim(" spaced "); + s8_trim.trim(); + TCHECK(s8_trim == "spaced"); + + // pack + lString8 s8_pack("hello"); + s8_pack.reserve(1000); + s8_pack.pack(); + + // clear / reset + lString8 s8_clr("something"); + s8_clr.clear(); + TCHECK(s8_clr.empty()); + lString8 s8_rst("something"); + s8_rst.reset(50); + TCHECK(s8_rst.empty()); + TCHECK(s8_rst.capacity() >= 49); + + // resize (fills extra space but doesn't update length) + lString8 s8_rsz("hi"); + s8_rsz.resize(5, 'x'); + + // lastChar / firstChar + lString8 s8_lf("abc"); + TCHECK(s8_lf.firstChar() == 'a'); + TCHECK(s8_lf.lastChar() == 'c'); + + // getHash + lString8 s8_h1("hello"); + lString8 s8_h2("hello"); + TCHECK(s8_h1.getHash() == s8_h2.getHash()); + + // operator << + lString8 s8_shift; + s8_shift << 'A'; + TCHECK(s8_shift == "A"); + s8_shift << "BC"; + TCHECK(s8_shift == "ABC"); + s8_shift << lString8("DE"); + TCHECK(s8_shift == "ABCDE"); + s8_shift.clear(); + s8_shift << fmt::decimal(42); + TCHECK(s8_shift == "42"); + s8_shift.clear(); + s8_shift << fmt::hex(255); + TCHECK(s8_shift == "ff"); + + // operator += + lString8 s8_plus; + s8_plus += "hello"; + TCHECK(s8_plus == "hello"); + s8_plus += lString8(" world"); + TCHECK(s8_plus == "hello world"); + s8_plus += '!'; + TCHECK(s8_plus == "hello world!"); + s8_plus += fmt::decimal(99); + TCHECK(s8_plus == "hello world!99"); + + // external operator == / != + lString8 s8_eq("equal"); + lString8 s8_eq2("equal"); + lString8 s8_neq("not_equal"); + TCHECK(s8_eq == s8_eq2); + TCHECK(!(s8_eq != s8_eq2)); + TCHECK(s8_eq != s8_neq); + TCHECK(s8_eq == "equal"); + TCHECK("equal" == s8_eq); + TCHECK(s8_eq != "other"); + TCHECK("other" != s8_eq); + + // external operator + + lString8 s8_add("abc"); + lString8 s8_added = s8_add + "def"; + TCHECK(s8_added == "abcdef"); + s8_added = s8_add + lString8("ghi"); + TCHECK(s8_added == "abcghi"); + s8_added = s8_add + fmt::decimal(123); + TCHECK(s8_added == "abc123"); + s8_added = s8_add + fmt::hex(0xFF); + TCHECK(s8_added == "abcff"); + + // itoa static + lString8 s8_i = lString8::itoa(42); + TCHECK(s8_i == "42"); + s8_i = lString8::itoa(0U); + TCHECK(s8_i == "0"); + s8_i = lString8::itoa(-123LL); + TCHECK(s8_i == "-123"); + + // cs8 + const lString8& cs8_test = cs8("static_string"); + TCHECK(cs8_test == "static_string"); + // calling cs8 with same string returns same reference + const lString8& cs8_test2 = cs8("static_string"); + TCHECK(&cs8_test == &cs8_test2); + + printf("=== lString16 tests ===\n"); + + // lString16 constructors + lString16 s16_default; + TCHECK(s16_default.empty()); + lString16 s16_cstr(u"hello"); + TCHECK(s16_cstr.length() == 5); + lString16 s16_from8("hello"); // from lChar8 + TCHECK(s16_from8.length() == 5); + lString16 s16_frag(u"hello world", 5); + TCHECK(s16_frag == u"hello"); + lString16 s16_sub(s16_cstr, 1, 3); + TCHECK(s16_sub == u"ell"); + + // Copy / move + lString16 s16_copy(s16_cstr); + TCHECK(s16_copy == u"hello"); + lString16 s16_move(std::move(s16_copy)); + TCHECK(s16_move == u"hello"); + + // Assignment + lString16 s16_assign; + s16_assign = u"world"; + TCHECK(s16_assign == u"world"); + s16_assign = "ascii"; + TCHECK(s16_assign == u"ascii"); + + // Append + lString16 s16_app; + s16_app.append(u"foo"); + TCHECK(s16_app == u"foo"); + s16_app.append((const lChar8*)"bar"); + TCHECK(s16_app == u"foobar"); + s16_app.append(3, u'!'); + TCHECK(s16_app == u"foobar!!!"); + s16_app.clear(); + + // appendDecimal / appendHex + s16_app.appendDecimal(999); + TCHECK(s16_app == u"999"); + s16_app.clear(); + s16_app.appendHex(0xABC); + TCHECK(s16_app == u"abc"); + + // Insert (only string and char-repeat variants exist) + lString16 s16_ins(u"world"); + s16_ins.insert(0, 2, u'X'); + s16_ins.clear(); + s16_ins = u"world"; + s16_ins.insert(0, lString16(u"hello ")); + + // compare + TCHECK(s16_cstr.compare(u"hello") == 0); + TCHECK(s16_cstr.compare((const lChar8*)"hello") == 0); + TCHECK(s16_cstr.compare(u"world") < 0); + + // Replace (not implemented for lString16 -- skip) + + // substr + lString16 s16_sub3(u"hello world"); + TCHECK(s16_sub3.substr(6) == u"world"); + TCHECK(s16_sub3.substr(0, 5) == u"hello"); + + // startsWith / endsWith + TCHECK(s16_sub3.startsWith(u"hello")); + TCHECK(s16_sub3.startsWith((const lChar8*)"hello")); + TCHECK(s16_sub3.endsWith(u"world")); + TCHECK(s16_sub3.endsWith((const lChar8*)"world")); + TCHECK(s16_sub3.endsWith(lString16(u"world"))); + // startsWithNoCase not implemented for lString16 -- skip + + // operator[] + TCHECK(s16_sub3[0] == u'h'); + + // atoi + lString16 s16_int(u"6789"); + int s16_ival = 0; + lInt64 s16_ival64 = 0; + TCHECK(s16_int.atoi() == 6789); + TCHECK(s16_int.atoi(s16_ival) && s16_ival == 6789); + TCHECK(s16_int.atoi(s16_ival64) && s16_ival64 == 6789); + + // getHash + lString16 s16_h1(u"test"); + lString16 s16_h2(u"test"); + TCHECK(s16_h1.getHash() == s16_h2.getHash()); + + // trim / trimNonAlpha + lString16 s16_tr(u" spaced "); + s16_tr.trim(); + TCHECK(s16_tr == u"spaced"); + lString16 s16_tna(u"!!!hello!!!"); + s16_tna.trimNonAlpha(); + TCHECK(s16_tna == u"hello"); + + // swap + lString16 s16_sw1(u"first"); + lString16 s16_sw2(u"second"); + s16_sw1.swap(s16_sw2); + TCHECK(s16_sw1 == u"second"); + TCHECK(s16_sw2 == u"first"); + + // clear / reset + lString16 s16_clr(u"something"); + s16_clr.clear(); + TCHECK(s16_clr.empty()); + lString16 s16_rst2(u"something"); + s16_rst2.reset(50); + TCHECK(s16_rst2.empty()); + + // limit + lString16 s16_lim(u"hello world"); + s16_lim.limit(5); + TCHECK(s16_lim == u"hello"); + + // pack + lString16 s16_pk(u"test"); + s16_pk.reserve(500); + + // operator << + lString16 s16_sh; + s16_sh << u'A'; + TCHECK(s16_sh == u"A"); + s16_sh << u"BC"; + TCHECK(s16_sh == u"ABC"); + s16_sh << fmt::decimal(42); + TCHECK(s16_sh == u"ABC42"); + s16_sh.clear(); + s16_sh << fmt::hex(255); + TCHECK(s16_sh == u"ff"); + + // operator += + lString16 s16_pe; + s16_pe += u"hello"; + TCHECK(s16_pe == u"hello"); + s16_pe += lString16(u" world"); + TCHECK(s16_pe == u"hello world"); + + // itoa + lString16 s16_i16 = lString16::itoa(42); + TCHECK(s16_i16 == u"42"); + s16_i16 = lString16::itoa(0U); + TCHECK(s16_i16 == u"0"); + + // external operator == / != / + + lString16 s16_eq1(u"equal"); + lString16 s16_eq2(u"equal"); + TCHECK(s16_eq1 == s16_eq2); + TCHECK(s16_eq1 == u"equal"); + TCHECK(u"equal" == s16_eq1); + TCHECK(s16_eq1 != u"other"); + lString16 s16_added16 = s16_eq1 + u"_suffix"; + TCHECK(s16_added16 == u"equal_suffix"); + + printf("=== lString32 tests ===\n"); + + // lString32 constructors + lString32 s32_default; + TCHECK(s32_default.empty()); + lString32 s32_cstr(U"hello"); + TCHECK(s32_cstr.length() == 5); + lString32 s32_from8("hello"); // from lChar8 + TCHECK(s32_from8.length() == 5); + lString32 s32_frag(U"hello world", 5); + TCHECK(s32_frag == U"hello"); + lString32 s32_sub(s32_cstr, 1, 3); + TCHECK(s32_sub == U"ell"); + + // Copy / move + lString32 s32_copy(s32_cstr); + TCHECK(s32_copy == U"hello"); + lString32 s32_move(std::move(s32_copy)); + TCHECK(s32_move == U"hello"); + + // Assignment + lString32 s32_assign; + s32_assign = U"world"; + TCHECK(s32_assign == U"world"); + s32_assign = "ascii"; + TCHECK(s32_assign == U"ascii"); + + // Append + lString32 s32_app; + s32_app.append(U"foo"); + TCHECK(s32_app == U"foo"); + s32_app.append((const lChar8*)"bar"); + TCHECK(s32_app == U"foobar"); + s32_app.append(3, U'!'); + TCHECK(s32_app == U"foobar!!!"); + s32_app.clear(); + s32_app.append(lString32(U"test")); + TCHECK(s32_app == U"test"); + + // appendDecimal / appendHex + s32_app.appendDecimal(12345); + TCHECK(s32_app == U"test12345"); + s32_app.clear(); + s32_app.appendHex(0xFF); + TCHECK(s32_app == U"ff"); + + // Insert (only lString32 and char-repeat variants exist) + lString32 s32_ins(U"world"); + s32_ins.insert(0, lString32(U"hello ")); + TCHECK(s32_ins == U"hello world"); + + // Replace (only lString32 variant exists) + lString32 s32_rp(U"ab_ab_ab"); + s32_rp.replace(U"ab", U"X"); + + // Replace (range, lString32 variant) + lString32 s32_rpr(U"hello world"); + s32_rpr.replace(0, 5, lString32(U"good")); + + // Erase + lString32 s32_era(U"0123456789"); + s32_era.erase(5, 5); + TCHECK(s32_era == U"01234"); + + // uppercase / lowercase / capitalize + lString32 s32_case(U"hello world"); + s32_case.uppercase(); + TCHECK(s32_case == U"HELLO WORLD"); + s32_case.lowercase(); + TCHECK(s32_case == U"hello world"); + s32_case.capitalize(); + TCHECK(s32_case == U"Hello World"); + + // fullWidthChars + lString32 s32_fw(U"abc"); + s32_fw.fullWidthChars(); + TCHECK(s32_fw.length() == 3); + TCHECK(s32_fw[0] > 0xFF00); // is fullwidth + + // compare + TCHECK(s32_cstr.compare(U"hello") == 0); + TCHECK(s32_cstr.compare((const lChar8*)"hello") == 0); + TCHECK(s32_cstr.compare(U"world") < 0); + + // split2 (check return value and basic operation) + lString32 s32_sp(U"key=value"); + lString32 k, v; + bool split_ok = s32_sp.split2(U"=", k, v); + + // pos / rpos + lString32 s32_pos(U"hello world, hello"); + TCHECK(s32_pos.pos(U'w') == 6); + TCHECK(s32_pos.pos(U"world") == 6); + TCHECK(s32_pos.pos(U'h', 1) == 13); + TCHECK(s32_pos.rpos(U"hello") == 13); + + // startsWith / endsWith / startsWithNoCase + lString32 s32_sw(U"Hello.cpp"); + TCHECK(s32_sw.startsWith(U"Hello")); + TCHECK(s32_sw.startsWith((const lChar8*)"Hello")); + TCHECK(s32_sw.startsWith(lString32(U"Hello"))); + TCHECK(s32_sw.endsWith(U".cpp")); + TCHECK(s32_sw.endsWith((const lChar8*)".cpp")); + TCHECK(s32_sw.endsWith(lString32(U".cpp"))); + lString32 s32_nc(U"Hello World"); + TCHECK(s32_nc.startsWithNoCase(lString32(U"hello"))); + + // substr + lString32 s32_sub2(U"hello world"); + TCHECK(s32_sub2.substr(6) == U"world"); + TCHECK(s32_sub2.substr(0, 5) == U"hello"); + + // atoi / atod + lString32 s32_int(U"12345"); + TCHECK(s32_int.atoi() == 12345); + int _i; + lInt64 _i64; + double _d; + TCHECK(s32_int.atoi(_i) && _i == 12345); + TCHECK(s32_int.atoi(_i64) && _i64 == 12345); + lString32 s32_dbl(U"3.14"); + TCHECK(s32_dbl.atod() > 3.13 && s32_dbl.atod() < 3.15); + TCHECK(s32_dbl.atod(_d) && _d > 3.13 && _d < 3.15); + + // operator[] + TCHECK(s32_sub2[0] == U'h'); + + // data / c_str + lString32 s32_dc(U"test_data"); + TCHECK(s32_dc.c_str() != nullptr); + TCHECK(s32_dc.data() != nullptr); + + // getHash + lString32 s32_h1(U"hash"); + lString32 s32_h2(U"hash"); + TCHECK(s32_h1.getHash() == s32_h2.getHash()); + + // swap + lString32 s32_sw1(U"first"); + lString32 s32_sw2(U"second"); + s32_sw1.swap(s32_sw2); + TCHECK(s32_sw1 == U"second"); + TCHECK(s32_sw2 == U"first"); + + // clear / reset + lString32 s32_clr(U"something"); + s32_clr.clear(); + TCHECK(s32_clr.empty()); + lString32 s32_rst3(U"something"); + s32_rst3.reset(50); + TCHECK(s32_rst3.empty()); + + // resize / limit + lString32 s32_rsz(U"hi"); + s32_rsz.resize(5, U'x'); + TCHECK(s32_rsz == U"hixxx"); + lString32 s32_lim(U"hello world"); + s32_lim.limit(5); + TCHECK(s32_lim == U"hello"); + + // trim / trimNonAlpha / trimDoubleSpaces + lString32 s32_tr(U" spaced "); + s32_tr.trim(); + TCHECK(s32_tr == U"spaced"); + lString32 s32_tna(U"!!!hello!!!"); + s32_tna.trimNonAlpha(); + TCHECK(s32_tna == U"hello"); + lString32 s32_tds(U"a b c"); + s32_tds.trimDoubleSpaces(false, false); + TCHECK(s32_tds == U"a b c"); + + // pack + lString32 s32_pk2(U"test"); + s32_pk2.reserve(500); + TCHECK(s32_pk2.capacity() >= 500); + s32_pk2.pack(); + TCHECK(s32_pk2.capacity() < 100); + + // operator << + lString32 s32_sh; + s32_sh << U'A'; + TCHECK(s32_sh == U"A"); + s32_sh << U"BC"; + TCHECK(s32_sh == U"ABC"); + s32_sh << fmt::decimal(42); + TCHECK(s32_sh == U"ABC42"); + s32_sh.clear(); + s32_sh << fmt::hex(255); + TCHECK(s32_sh == U"ff"); + + // operator += + lString32 s32_pe; + s32_pe += U"hello"; + TCHECK(s32_pe == U"hello"); + s32_pe += lString32(U" world"); + TCHECK(s32_pe == U"hello world"); + + // replaceParam / replaceIntParam + lString32 s32_rp2(U"Hello $1, you have $2 messages"); + TCHECK(s32_rp2.replaceParam(1, U"Alice")); + TCHECK(s32_rp2.replaceIntParam(2, 5)); + TCHECK(s32_rp2 == U"Hello Alice, you have 5 messages"); + + // itoa + lString32 s32_i32 = lString32::itoa(42); + TCHECK(s32_i32 == U"42"); + s32_i32 = lString32::itoa(0U); + TCHECK(s32_i32 == U"0"); + s32_i32 = lString32::itoa(-99LL); + TCHECK(s32_i32 == U"-99"); + s32_i32 = lString32::itoa(0xFFFFFFFFFFFFFFFFULL); + TCHECK(!s32_i32.empty()); + + // cs32 + const lString32& cs32_test = cs32("static_str32"); + TCHECK(cs32_test == U"static_str32"); + const lString32& cs32_test2 = cs32(U"wide_static"); + TCHECK(cs32_test2 == U"wide_static"); + + // external operator == / != / + + lString32 s32_eq1(U"equal"); + lString32 s32_eq2(U"equal"); + TCHECK(s32_eq1 == s32_eq2); + TCHECK(s32_eq1 == U"equal"); + TCHECK(U"equal" == s32_eq1); + TCHECK(s32_eq1 != U"other"); + lString32 s32_added32 = s32_eq1 + U"_suffix"; + TCHECK(s32_added32 == U"equal_suffix"); + s32_added32 = s32_eq1 + lString32(U"_extra"); + TCHECK(s32_added32 == U"equal_extra"); + s32_added32 = s32_eq1 + fmt::decimal(123); + TCHECK(s32_added32 == U"equal123"); + s32_added32 = s32_eq1 + fmt::hex(0xFF); + TCHECK(s32_added32 == U"equalff"); + + // Mixed-type comparisons + lString32 s32_mix(U"test"); + lString16 s16_mix(u"test"); + lString8 s8_mix("test"); + TCHECK(s32_mix == s16_mix); + TCHECK(s32_mix == s8_mix); + TCHECK(s16_mix == s32_mix); + + // External conversion functions + lString32 s32_conv(U"Hello World"); + lString8 s8_utf8 = UnicodeToUtf8(s32_conv); + TCHECK(s8_utf8 == "Hello World"); + lString16 s16_utf16 = UnicodeToUtf16(s32_conv); + TCHECK(s16_utf16 == u"Hello World"); + lString8 s8_local = UnicodeToLocal(s32_conv); + TCHECK(!s8_local.empty()); + lString8 s8_wtf8 = UnicodeToWtf8(s32_conv); + TCHECK(s8_wtf8 == "Hello World"); + + // Reverse conversion + lString32 s32_back = Utf8ToUnicode(s8_utf8); + TCHECK(s32_back == U"Hello World"); + s32_back = Utf8ToUnicode("direct_cstr"); + TCHECK(s32_back == U"direct_cstr"); + s32_back = Utf8ToUnicode("fragment", 4); + TCHECK(s32_back == U"frag"); + s32_back = Utf16ToUnicode(s16_utf16); + TCHECK(s32_back == U"Hello World"); + s32_back = Utf16ToUnicode(u"direct16"); + TCHECK(s32_back == U"direct16"); + lString32 s32_wtf = Wtf8ToUnicode(s8_wtf8); + TCHECK(s32_wtf == U"Hello World"); + s32_wtf = Wtf8ToUnicode("wtf_direct"); + TCHECK(s32_wtf == U"wtf_direct"); + + // external getHash + lString32 s32_gh(U"hash_me"); + lString16 s16_gh(u"hash_me"); + lString8 s8_gh("hash_me"); + TCHECK(getHash(s32_gh) == s32_gh.getHash()); + TCHECK(getHash(s16_gh) == s16_gh.getHash()); + TCHECK(getHash(s8_gh) == s8_gh.getHash()); + + if (test_errors == 0) + printf("All string tests passed!\n"); + else + printf("FAILED: %d test(s) failed!\n", test_errors); } +bool testsOnly = false; + int main(int argc, const char ** argv) { - printf("CR3 Performance Tests\n"); +#if (LDOM_USE_OWN_MEM_MAN == 1) + printf("LDOM_USE_OWN_MEM_MAN is turned ON\n"); +#endif + printf("CR3 Library Tests\n"); testStrings(); testStringCollections(); + if (testsOnly) { +#if (LDOM_USE_OWN_MEM_MAN == 1) + free_ls_storage(); +#endif + printf("CR3 Library Tests done. Exiting.\n"); + return 0; + } + + printf("CR3 Performance Tests\n"); + lString32Collection files; lString8 bookDir("../tests/testdata"); for (int i = 1; i < argc; i++) { @@ -558,8 +1244,10 @@ int main(int argc, const char ** argv) { printf("test file [%d]: %s\n", i, UnicodeToLocal(files[i]).c_str()); } + printf("Starting benchmark...\n"); executeBenchmark(files); ShutdownCREngine(); + printf("Exiting...\n"); return 0; } From a24e2272aac9f7d5edc9cf24532fa78623c05c27 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Thu, 23 Jul 2026 10:43:55 +0100 Subject: [PATCH 04/36] fix memory corruption - replace memcpy with memmove --- crengine/src/lvstring.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index fe1c2099b2..a8a11023dd 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -613,7 +613,7 @@ inline void _lStr_memcpy(lChar32 * dst, const lChar32 * src, int count) inline void _lStr_memcpy(lChar8 * dst, const lChar8 * src, int count) { - memcpy(dst, (const lChar8 *) src, count); + memmove(dst, (const lChar8 *) src, count); } inline void _lStr_memset(lChar16 * dst, lChar16 value, int count) From cfb9f1768d6c990c26df589c14874d3e7038be7f Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Thu, 23 Jul 2026 14:19:02 +0100 Subject: [PATCH 05/36] separate string library tests to stringtest.* files --- tests/CMakeLists.txt | 3 +- tests/src/stringtest.cpp | 758 ++++++++++++++++++++++++++++++++++++++ tests/src/stringtest.h | 7 + tests/src/testmain.cpp | 764 +-------------------------------------- 4 files changed, 770 insertions(+), 762 deletions(-) create mode 100644 tests/src/stringtest.cpp create mode 100644 tests/src/stringtest.h diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e4b6f31ccf..bc2cc5fd83 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,6 @@ SET (CR3TEST_SOURCES - src/testmain.cpp + src/testmain.cpp + src/stringtest.cpp ) IF(${CMAKE_BUILD_TYPE} STREQUAL Debug) diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp new file mode 100644 index 0000000000..9138331587 --- /dev/null +++ b/tests/src/stringtest.cpp @@ -0,0 +1,758 @@ +#include +#include +#include "../crengine/include/crengine.h" +#include "../crengine/include/lvstring8collection.h" +#include "../crengine/include/lvstring32collection.h" +#include "stringtest.h" + +static int test_errors = 0; +#define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) + +void testStrings() { + printf("=== lString8 tests ===\n"); + + // Constructors + lString8 s8_default; + TCHECK(s8_default.empty()); + + lString8 s8_size(10); + TCHECK(s8_size.empty()); + + lString8 s8_cstr("hello"); + TCHECK(s8_cstr.length() == 5); + TCHECK(s8_cstr == "hello"); + + lString8 s8_fragment("hello world", 5); + TCHECK(s8_fragment == "hello"); + + lString8 s8_sub(s8_cstr, 1, 3); + TCHECK(s8_sub == "ell"); + + // Copy / move + lString8 s8_copy(s8_cstr); + TCHECK(s8_copy == "hello"); + lString8 s8_move(std::move(s8_copy)); + TCHECK(s8_move == "hello"); + + // Assignment + lString8 s8_assign; + s8_assign = s8_move; + TCHECK(s8_assign == "hello"); + lString8 s8_assign2; + s8_assign2 = "world"; + TCHECK(s8_assign2 == "world"); + lString8 s8_assign3; + s8_assign3 = lString8("temp"); + TCHECK(s8_assign3 == "temp"); + + // assign methods + lString8 s8_a; + s8_a.assign("test"); + TCHECK(s8_a == "test"); + s8_a.assign("abcdef", 3); + TCHECK(s8_a == "abc"); + lString8 s8_b("xyz"); + s8_a.assign(s8_b); + TCHECK(s8_a == "xyz"); + s8_a.assign(lString8("move_assign")); + TCHECK(s8_a == "move_assign"); + + // Append + lString8 s8_app; + s8_app.append("foo"); + TCHECK(s8_app == "foo"); + s8_app.append("xy", 1); + TCHECK(s8_app == "foox"); + s8_app.append(3, '!'); + TCHECK(s8_app == "foox!!!"); + s8_app.clear(); + + // appendDecimal / appendHex + s8_app.appendDecimal(12345); + TCHECK(s8_app == "12345"); + s8_app.clear(); + s8_app.appendDecimal(-42); + TCHECK(s8_app == "-42"); + s8_app.clear(); + s8_app.appendHex(0xFF); + TCHECK(s8_app == "ff"); + s8_app.clear(); + + // Insert + lString8 s8_ins("world"); + s8_ins.insert(0, 2, 'X'); + TCHECK(s8_ins == "XXworld"); + + // Replace + lString8 s8_rep("hello world"); + s8_rep.replace(0, 5, lString8("good")); + TCHECK(s8_rep == "good world"); + lString8 s8_rep2("abcabc"); + s8_rep2.replace('a', 'X'); + TCHECK(s8_rep2 == "XbcXbc"); + + // Erase + lString8 s8_era("0123456789"); + s8_era.erase(5, 5); + TCHECK(s8_era == "01234"); + + // uppercase / lowercase + lString8 s8_case("Hello World"); + s8_case.uppercase(); + TCHECK(s8_case == "HELLO WORLD"); + s8_case.lowercase(); + TCHECK(s8_case == "hello world"); + + // compare + TCHECK(s8_cstr.compare("hello") == 0); + TCHECK(s8_cstr.compare("world") < 0); + TCHECK(s8_cstr.compare("abc") > 0); + + // pos / rpos + lString8 s8_pos("hello world, hello"); + TCHECK(s8_pos.pos('w') == 6); + TCHECK(s8_pos.pos("world") == 6); + TCHECK(s8_pos.pos('h', 1) == 13); + TCHECK(s8_pos.rpos("hello") == 13); + TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); + + // startsWith / endsWith + lString8 s8_sw("hello.cpp"); + TCHECK(s8_sw.startsWith("hello")); + TCHECK(s8_sw.startsWith(lString8("hello"))); + TCHECK(!s8_sw.startsWith("world")); + TCHECK(s8_sw.endsWith(".cpp")); + + // substr + lString8 s8_sub2("hello world"); + TCHECK(s8_sub2.substr(6) == "world"); + TCHECK(s8_sub2.substr(0, 5) == "hello"); + + // operator[] + TCHECK(s8_sub2[0] == 'h'); + TCHECK(s8_sub2[4] == 'o'); + + // data / c_str + TCHECK(strcmp(s8_sub2.c_str(), "hello world") == 0); + TCHECK(strcmp(s8_sub2.data(), "hello world") == 0); + + // length / size / capacity / empty + TCHECK(s8_sub2.length() == 11); + TCHECK(s8_sub2.size() == 11); + TCHECK(!s8_sub2.empty()); + lString8 s8_empty; + TCHECK(s8_empty.empty()); + TCHECK(!s8_empty); + + // at + TCHECK(s8_sub2.at(0) == 'h'); + TCHECK(s8_sub2.at(10) == 'd'); + + // atoi + lString8 s8_int("12345"); + TCHECK(s8_int.atoi() == 12345); + lString8 s8_int2("-999"); + TCHECK(s8_int2.atoi() == -999); + lString8 s8_int64("1234567890123"); + TCHECK(s8_int64.atoi64() == 1234567890123LL); + + // swap + lString8 s8_swap1("first"); + lString8 s8_swap2("second"); + s8_swap1.swap(s8_swap2); + TCHECK(s8_swap1 == "second"); + TCHECK(s8_swap2 == "first"); + + // trim + lString8 s8_trim(" spaced "); + s8_trim.trim(); + TCHECK(s8_trim == "spaced"); + + // pack + lString8 s8_pack("hello"); + s8_pack.reserve(1000); + s8_pack.pack(); + + // clear / reset + lString8 s8_clr("something"); + s8_clr.clear(); + TCHECK(s8_clr.empty()); + lString8 s8_rst("something"); + s8_rst.reset(50); + TCHECK(s8_rst.empty()); + TCHECK(s8_rst.capacity() >= 49); + + // resize + lString8 s8_rsz("hi"); + s8_rsz.resize(5, 'x'); + + // lastChar / firstChar + lString8 s8_lf("abc"); + TCHECK(s8_lf.firstChar() == 'a'); + TCHECK(s8_lf.lastChar() == 'c'); + + // getHash + lString8 s8_h1("hello"); + lString8 s8_h2("hello"); + TCHECK(s8_h1.getHash() == s8_h2.getHash()); + + // operator << + lString8 s8_shift; + s8_shift << 'A'; + TCHECK(s8_shift == "A"); + s8_shift << "BC"; + TCHECK(s8_shift == "ABC"); + s8_shift << lString8("DE"); + TCHECK(s8_shift == "ABCDE"); + s8_shift.clear(); + s8_shift << fmt::decimal(42); + TCHECK(s8_shift == "42"); + s8_shift.clear(); + s8_shift << fmt::hex(255); + TCHECK(s8_shift == "ff"); + + // operator += + lString8 s8_plus; + s8_plus += "hello"; + TCHECK(s8_plus == "hello"); + s8_plus += lString8(" world"); + TCHECK(s8_plus == "hello world"); + s8_plus += '!'; + TCHECK(s8_plus == "hello world!"); + s8_plus += fmt::decimal(99); + TCHECK(s8_plus == "hello world!99"); + + // external operator == / != + lString8 s8_eq("equal"); + lString8 s8_eq2("equal"); + lString8 s8_neq("not_equal"); + TCHECK(s8_eq == s8_eq2); + TCHECK(!(s8_eq != s8_eq2)); + TCHECK(s8_eq != s8_neq); + TCHECK(s8_eq == "equal"); + TCHECK("equal" == s8_eq); + TCHECK(s8_eq != "other"); + TCHECK("other" != s8_eq); + + // external operator + + lString8 s8_add("abc"); + lString8 s8_added = s8_add + "def"; + TCHECK(s8_added == "abcdef"); + s8_added = s8_add + lString8("ghi"); + TCHECK(s8_added == "abcghi"); + s8_added = s8_add + fmt::decimal(123); + TCHECK(s8_added == "abc123"); + s8_added = s8_add + fmt::hex(0xFF); + TCHECK(s8_added == "abcff"); + + // itoa static + lString8 s8_i = lString8::itoa(42); + TCHECK(s8_i == "42"); + s8_i = lString8::itoa(0U); + TCHECK(s8_i == "0"); + s8_i = lString8::itoa(-123LL); + TCHECK(s8_i == "-123"); + + // cs8 + const lString8& cs8_test = cs8("static_string"); + TCHECK(cs8_test == "static_string"); + const lString8& cs8_test2 = cs8("static_string"); + TCHECK(&cs8_test == &cs8_test2); + + printf("=== lString16 tests ===\n"); + + // lString16 constructors + lString16 s16_default; + TCHECK(s16_default.empty()); + lString16 s16_cstr(u"hello"); + TCHECK(s16_cstr.length() == 5); + lString16 s16_from8("hello"); + TCHECK(s16_from8.length() == 5); + lString16 s16_frag(u"hello world", 5); + TCHECK(s16_frag == u"hello"); + lString16 s16_sub(s16_cstr, 1, 3); + TCHECK(s16_sub == u"ell"); + + // Copy / move + lString16 s16_copy(s16_cstr); + TCHECK(s16_copy == u"hello"); + lString16 s16_move(std::move(s16_copy)); + TCHECK(s16_move == u"hello"); + + // Assignment + lString16 s16_assign; + s16_assign = u"world"; + TCHECK(s16_assign == u"world"); + s16_assign = "ascii"; + TCHECK(s16_assign == u"ascii"); + + // Append + lString16 s16_app; + s16_app.append(u"foo"); + TCHECK(s16_app == u"foo"); + s16_app.append((const lChar8*)"bar"); + TCHECK(s16_app == u"foobar"); + s16_app.append(3, u'!'); + TCHECK(s16_app == u"foobar!!!"); + s16_app.clear(); + + // appendDecimal / appendHex + s16_app.appendDecimal(999); + TCHECK(s16_app == u"999"); + s16_app.clear(); + s16_app.appendHex(0xABC); + TCHECK(s16_app == u"abc"); + + // Insert + lString16 s16_ins(u"world"); + s16_ins.insert(0, 2, u'X'); + s16_ins.clear(); + s16_ins = u"world"; + s16_ins.insert(0, lString16(u"hello ")); + + // compare + TCHECK(s16_cstr.compare(u"hello") == 0); + TCHECK(s16_cstr.compare((const lChar8*)"hello") == 0); + TCHECK(s16_cstr.compare(u"world") < 0); + + // substr + lString16 s16_sub3(u"hello world"); + TCHECK(s16_sub3.substr(6) == u"world"); + TCHECK(s16_sub3.substr(0, 5) == u"hello"); + + // startsWith / endsWith + TCHECK(s16_sub3.startsWith(u"hello")); + TCHECK(s16_sub3.startsWith((const lChar8*)"hello")); + TCHECK(s16_sub3.endsWith(u"world")); + TCHECK(s16_sub3.endsWith((const lChar8*)"world")); + TCHECK(s16_sub3.endsWith(lString16(u"world"))); + + // operator[] + TCHECK(s16_sub3[0] == u'h'); + + // atoi + lString16 s16_int(u"6789"); + int s16_ival = 0; + lInt64 s16_ival64 = 0; + TCHECK(s16_int.atoi() == 6789); + TCHECK(s16_int.atoi(s16_ival) && s16_ival == 6789); + TCHECK(s16_int.atoi(s16_ival64) && s16_ival64 == 6789); + + // getHash + lString16 s16_h1(u"test"); + lString16 s16_h2(u"test"); + TCHECK(s16_h1.getHash() == s16_h2.getHash()); + + // trim / trimNonAlpha + lString16 s16_tr(u" spaced "); + s16_tr.trim(); + TCHECK(s16_tr == u"spaced"); + lString16 s16_tna(u"!!!hello!!!"); + s16_tna.trimNonAlpha(); + TCHECK(s16_tna == u"hello"); + + // swap + lString16 s16_sw1(u"first"); + lString16 s16_sw2(u"second"); + s16_sw1.swap(s16_sw2); + TCHECK(s16_sw1 == u"second"); + TCHECK(s16_sw2 == u"first"); + + // clear / reset + lString16 s16_clr(u"something"); + s16_clr.clear(); + TCHECK(s16_clr.empty()); + lString16 s16_rst2(u"something"); + s16_rst2.reset(50); + TCHECK(s16_rst2.empty()); + + // limit + lString16 s16_lim(u"hello world"); + s16_lim.limit(5); + TCHECK(s16_lim == u"hello"); + + // pack + lString16 s16_pk(u"test"); + s16_pk.reserve(500); + + // operator << + lString16 s16_sh; + s16_sh << u'A'; + TCHECK(s16_sh == u"A"); + s16_sh << u"BC"; + TCHECK(s16_sh == u"ABC"); + s16_sh << fmt::decimal(42); + TCHECK(s16_sh == u"ABC42"); + s16_sh.clear(); + s16_sh << fmt::hex(255); + TCHECK(s16_sh == u"ff"); + + // operator += + lString16 s16_pe; + s16_pe += u"hello"; + TCHECK(s16_pe == u"hello"); + s16_pe += lString16(u" world"); + TCHECK(s16_pe == u"hello world"); + + // itoa + lString16 s16_i16 = lString16::itoa(42); + TCHECK(s16_i16 == u"42"); + s16_i16 = lString16::itoa(0U); + TCHECK(s16_i16 == u"0"); + + // external operator == / != / + + lString16 s16_eq1(u"equal"); + lString16 s16_eq2(u"equal"); + TCHECK(s16_eq1 == s16_eq2); + TCHECK(s16_eq1 == u"equal"); + TCHECK(u"equal" == s16_eq1); + TCHECK(s16_eq1 != u"other"); + lString16 s16_added16 = s16_eq1 + u"_suffix"; + TCHECK(s16_added16 == u"equal_suffix"); + + printf("=== lString32 tests ===\n"); + + // lString32 constructors + lString32 s32_default; + TCHECK(s32_default.empty()); + lString32 s32_cstr(U"hello"); + TCHECK(s32_cstr.length() == 5); + lString32 s32_from8("hello"); + TCHECK(s32_from8.length() == 5); + lString32 s32_frag(U"hello world", 5); + TCHECK(s32_frag == U"hello"); + lString32 s32_sub(s32_cstr, 1, 3); + TCHECK(s32_sub == U"ell"); + + // Copy / move + lString32 s32_copy(s32_cstr); + TCHECK(s32_copy == U"hello"); + lString32 s32_move(std::move(s32_copy)); + TCHECK(s32_move == U"hello"); + + // Assignment + lString32 s32_assign; + s32_assign = U"world"; + TCHECK(s32_assign == U"world"); + s32_assign = "ascii"; + TCHECK(s32_assign == U"ascii"); + + // Append + lString32 s32_app; + s32_app.append(U"foo"); + TCHECK(s32_app == U"foo"); + s32_app.append((const lChar8*)"bar"); + TCHECK(s32_app == U"foobar"); + s32_app.append(3, U'!'); + TCHECK(s32_app == U"foobar!!!"); + s32_app.clear(); + s32_app.append(lString32(U"test")); + TCHECK(s32_app == U"test"); + + // appendDecimal / appendHex + s32_app.appendDecimal(12345); + TCHECK(s32_app == U"test12345"); + s32_app.clear(); + s32_app.appendHex(0xFF); + TCHECK(s32_app == U"ff"); + + // Insert + lString32 s32_ins(U"world"); + s32_ins.insert(0, lString32(U"hello ")); + TCHECK(s32_ins == U"hello world"); + + // Replace + lString32 s32_rp(U"ab_ab_ab"); + s32_rp.replace(U"ab", U"X"); + + // Replace (range) + lString32 s32_rpr(U"hello world"); + s32_rpr.replace(0, 5, lString32(U"good")); + + // Erase + lString32 s32_era(U"0123456789"); + s32_era.erase(5, 5); + TCHECK(s32_era == U"01234"); + + // uppercase / lowercase / capitalize + lString32 s32_case(U"hello world"); + s32_case.uppercase(); + TCHECK(s32_case == U"HELLO WORLD"); + s32_case.lowercase(); + TCHECK(s32_case == U"hello world"); + s32_case.capitalize(); + TCHECK(s32_case == U"Hello World"); + + // fullWidthChars + lString32 s32_fw(U"abc"); + s32_fw.fullWidthChars(); + TCHECK(s32_fw.length() == 3); + TCHECK(s32_fw[0] > 0xFF00); + + // compare + TCHECK(s32_cstr.compare(U"hello") == 0); + TCHECK(s32_cstr.compare((const lChar8*)"hello") == 0); + TCHECK(s32_cstr.compare(U"world") < 0); + + // split2 + lString32 s32_sp(U"key=value"); + lString32 k, v; + s32_sp.split2(U"=", k, v); + + // pos / rpos + lString32 s32_pos(U"hello world, hello"); + TCHECK(s32_pos.pos(U'w') == 6); + TCHECK(s32_pos.pos(U"world") == 6); + TCHECK(s32_pos.pos(U'h', 1) == 13); + TCHECK(s32_pos.rpos(U"hello") == 13); + + // startsWith / endsWith / startsWithNoCase + lString32 s32_sw(U"Hello.cpp"); + TCHECK(s32_sw.startsWith(U"Hello")); + TCHECK(s32_sw.startsWith((const lChar8*)"Hello")); + TCHECK(s32_sw.startsWith(lString32(U"Hello"))); + TCHECK(s32_sw.endsWith(U".cpp")); + TCHECK(s32_sw.endsWith((const lChar8*)".cpp")); + TCHECK(s32_sw.endsWith(lString32(U".cpp"))); + lString32 s32_nc(U"Hello World"); + TCHECK(s32_nc.startsWithNoCase(lString32(U"hello"))); + + // substr + lString32 s32_sub2(U"hello world"); + TCHECK(s32_sub2.substr(6) == U"world"); + TCHECK(s32_sub2.substr(0, 5) == U"hello"); + + // atoi / atod + lString32 s32_int(U"12345"); + TCHECK(s32_int.atoi() == 12345); + int _i; + lInt64 _i64; + double _d; + TCHECK(s32_int.atoi(_i) && _i == 12345); + TCHECK(s32_int.atoi(_i64) && _i64 == 12345); + lString32 s32_dbl(U"3.14"); + TCHECK(s32_dbl.atod() > 3.13 && s32_dbl.atod() < 3.15); + TCHECK(s32_dbl.atod(_d) && _d > 3.13 && _d < 3.15); + + // operator[] + TCHECK(s32_sub2[0] == U'h'); + + // data / c_str + lString32 s32_dc(U"test_data"); + TCHECK(s32_dc.c_str() != nullptr); + TCHECK(s32_dc.data() != nullptr); + + // getHash + lString32 s32_h1(U"hash"); + lString32 s32_h2(U"hash"); + TCHECK(s32_h1.getHash() == s32_h2.getHash()); + + // swap + lString32 s32_sw1(U"first"); + lString32 s32_sw2(U"second"); + s32_sw1.swap(s32_sw2); + TCHECK(s32_sw1 == U"second"); + TCHECK(s32_sw2 == U"first"); + + // clear / reset + lString32 s32_clr(U"something"); + s32_clr.clear(); + TCHECK(s32_clr.empty()); + lString32 s32_rst3(U"something"); + s32_rst3.reset(50); + TCHECK(s32_rst3.empty()); + + // resize / limit + lString32 s32_rsz(U"hi"); + s32_rsz.resize(5, U'x'); + TCHECK(s32_rsz == U"hixxx"); + lString32 s32_lim(U"hello world"); + s32_lim.limit(5); + TCHECK(s32_lim == U"hello"); + + // trim / trimNonAlpha / trimDoubleSpaces + lString32 s32_tr(U" spaced "); + s32_tr.trim(); + TCHECK(s32_tr == U"spaced"); + lString32 s32_tna(U"!!!hello!!!"); + s32_tna.trimNonAlpha(); + TCHECK(s32_tna == U"hello"); + lString32 s32_tds(U"a b c"); + s32_tds.trimDoubleSpaces(false, false); + TCHECK(s32_tds == U"a b c"); + + // pack + lString32 s32_pk2(U"test"); + s32_pk2.reserve(500); + TCHECK(s32_pk2.capacity() >= 500); + s32_pk2.pack(); + TCHECK(s32_pk2.capacity() < 100); + + // operator << + lString32 s32_sh; + s32_sh << U'A'; + TCHECK(s32_sh == U"A"); + s32_sh << U"BC"; + TCHECK(s32_sh == U"ABC"); + s32_sh << fmt::decimal(42); + TCHECK(s32_sh == U"ABC42"); + s32_sh.clear(); + s32_sh << fmt::hex(255); + TCHECK(s32_sh == U"ff"); + + // operator += + lString32 s32_pe; + s32_pe += U"hello"; + TCHECK(s32_pe == U"hello"); + s32_pe += lString32(U" world"); + TCHECK(s32_pe == U"hello world"); + + // replaceParam / replaceIntParam + lString32 s32_rp2(U"Hello $1, you have $2 messages"); + TCHECK(s32_rp2.replaceParam(1, U"Alice")); + TCHECK(s32_rp2.replaceIntParam(2, 5)); + TCHECK(s32_rp2 == U"Hello Alice, you have 5 messages"); + + // itoa + lString32 s32_i32 = lString32::itoa(42); + TCHECK(s32_i32 == U"42"); + s32_i32 = lString32::itoa(0U); + TCHECK(s32_i32 == U"0"); + s32_i32 = lString32::itoa(-99LL); + TCHECK(s32_i32 == U"-99"); + s32_i32 = lString32::itoa(0xFFFFFFFFFFFFFFFFULL); + TCHECK(!s32_i32.empty()); + + // cs32 + const lString32& cs32_test = cs32("static_str32"); + TCHECK(cs32_test == U"static_str32"); + const lString32& cs32_test2 = cs32(U"wide_static"); + TCHECK(cs32_test2 == U"wide_static"); + + // external operator == / != / + + lString32 s32_eq1(U"equal"); + lString32 s32_eq2(U"equal"); + TCHECK(s32_eq1 == s32_eq2); + TCHECK(s32_eq1 == U"equal"); + TCHECK(U"equal" == s32_eq1); + TCHECK(s32_eq1 != U"other"); + lString32 s32_added32 = s32_eq1 + U"_suffix"; + TCHECK(s32_added32 == U"equal_suffix"); + s32_added32 = s32_eq1 + lString32(U"_extra"); + TCHECK(s32_added32 == U"equal_extra"); + s32_added32 = s32_eq1 + fmt::decimal(123); + TCHECK(s32_added32 == U"equal123"); + s32_added32 = s32_eq1 + fmt::hex(0xFF); + TCHECK(s32_added32 == U"equalff"); + + // Mixed-type comparisons + lString32 s32_mix(U"test"); + lString16 s16_mix(u"test"); + lString8 s8_mix("test"); + TCHECK(s32_mix == s16_mix); + TCHECK(s32_mix == s8_mix); + TCHECK(s16_mix == s32_mix); + + // External conversion functions + lString32 s32_conv(U"Hello World"); + lString8 s8_utf8 = UnicodeToUtf8(s32_conv); + TCHECK(s8_utf8 == "Hello World"); + lString16 s16_utf16 = UnicodeToUtf16(s32_conv); + TCHECK(s16_utf16 == u"Hello World"); + lString8 s8_local = UnicodeToLocal(s32_conv); + TCHECK(!s8_local.empty()); + lString8 s8_wtf8 = UnicodeToWtf8(s32_conv); + TCHECK(s8_wtf8 == "Hello World"); + + // Reverse conversion + lString32 s32_back = Utf8ToUnicode(s8_utf8); + TCHECK(s32_back == U"Hello World"); + s32_back = Utf8ToUnicode("direct_cstr"); + TCHECK(s32_back == U"direct_cstr"); + s32_back = Utf8ToUnicode("fragment", 4); + TCHECK(s32_back == U"frag"); + s32_back = Utf16ToUnicode(s16_utf16); + TCHECK(s32_back == U"Hello World"); + s32_back = Utf16ToUnicode(u"direct16"); + TCHECK(s32_back == U"direct16"); + lString32 s32_wtf = Wtf8ToUnicode(s8_wtf8); + TCHECK(s32_wtf == U"Hello World"); + s32_wtf = Wtf8ToUnicode("wtf_direct"); + TCHECK(s32_wtf == U"wtf_direct"); + + // external getHash + lString32 s32_gh(U"hash_me"); + lString16 s16_gh(u"hash_me"); + lString8 s8_gh("hash_me"); + TCHECK(getHash(s32_gh) == s32_gh.getHash()); + TCHECK(getHash(s16_gh) == s16_gh.getHash()); + TCHECK(getHash(s8_gh) == s8_gh.getHash()); + + if (test_errors == 0) + printf("All string tests passed!\n"); + else + printf("FAILED: %d test(s) failed!\n", test_errors); +} + +void testStringCollections() { + { + lString8Collection list; + { + lString8 s {"test_s_1"}; + assert(s == "test_s_1"); + list.add(s); + assert(s == "test_s_1"); + } + { + lString8 s {"test_s_2"}; + assert(s == "test_s_2"); + list.add(s); + assert(s == "test_s_2"); + } + } + { + lString8Collection list; + { + lString8 s {"test_s_3"}; + assert(s == "test_s_3"); + list.add(s); + assert(s == "test_s_3"); + } + { + lString8 s {"test_s_4"}; + assert(s == "test_s_4"); + list.add(s); + assert(s == "test_s_4"); + } + } + { + lString32Collection list; + { + lString32 s {U"test_s_5"}; + assert(s == "test_s_5"); + list.add(s); + assert(s == "test_s_5"); + } + { + lString32 s {U"test_s_5"}; + assert(s == "test_s_5"); + list.add(s); + assert(s == "test_s_5"); + } + } + + for (int sz = 1; sz < 1000; sz++) { + lString8Collection list; + for (int i = 0; i < sz; i++) { + lString8 s {"String8"}; + list.add(s); + } + } + for (int sz = 1; sz < 1000; sz++) { + lString32Collection list; + for (int i = 0; i < sz; i++) { + lString32 s {U"String32"}; + list.add(s); + } + } +} diff --git a/tests/src/stringtest.h b/tests/src/stringtest.h new file mode 100644 index 0000000000..a220161e52 --- /dev/null +++ b/tests/src/stringtest.h @@ -0,0 +1,7 @@ +#ifndef __STRINGTEST_H_INCLUDED__ +#define __STRINGTEST_H_INCLUDED__ + +void testStrings(); +void testStringCollections(); + +#endif diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index c15b7c36a0..6a400bf4e7 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -2,6 +2,9 @@ #include "../crengine/include/crengine.h" #include "../crengine/include/cr3version.h" +#include "../crengine/include/lvstring8collection.h" +#include "../crengine/include/lvstring32collection.h" +#include "stringtest.h" #include #include #include @@ -379,767 +382,6 @@ void ShutdownCREngine() #endif } -void testStringCollections() { - { - lString8Collection list; - { - lString8 s {"test_s_1"}; - assert(s == "test_s_1"); - list.add(s); - assert(s == "test_s_1"); - } - { - lString8 s {"test_s_2"}; - assert(s == "test_s_2"); - list.add(s); - assert(s == "test_s_2"); - } - } - { - lString8Collection list; - { - lString8 s {"test_s_3"}; - assert(s == "test_s_3"); - list.add(s); - assert(s == "test_s_3"); - } - { - lString8 s {"test_s_4"}; - assert(s == "test_s_4"); - list.add(s); - assert(s == "test_s_4"); - } - } - { - lString32Collection list; - { - lString32 s {U"test_s_5"}; - assert(s == "test_s_5"); - list.add(s); - assert(s == "test_s_5"); - } - { - lString32 s {U"test_s_5"}; - assert(s == "test_s_5"); - list.add(s); - assert(s == "test_s_5"); - } - } - - for (int sz = 1; sz < 1000; sz++) { - lString8Collection list; - for (int i = 0; i < sz; i++) { - lString8 s {"String8"}; - //s << i; - list.add(s); - } - } - for (int sz = 1; sz < 1000; sz++) { - lString32Collection list; - for (int i = 0; i < sz; i++) { - lString32 s {U"String32"}; - //s << i; - list.add(s); - } - } -} - -static int test_errors = 0; -#define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) - -// Reuse atod from lString32 -static double lString32_atod(const lString32& s) { return s.atod(); } - -void testStrings() { - printf("=== lString8 tests ===\n"); - - // Constructors - lString8 s8_default; - TCHECK(s8_default.empty()); - - lString8 s8_size(10); - TCHECK(s8_size.empty()); - - lString8 s8_cstr("hello"); - TCHECK(s8_cstr.length() == 5); - TCHECK(s8_cstr == "hello"); - - lString8 s8_fragment("hello world", 5); - TCHECK(s8_fragment == "hello"); - - lString8 s8_sub(s8_cstr, 1, 3); - TCHECK(s8_sub == "ell"); - - // Copy / move - lString8 s8_copy(s8_cstr); - TCHECK(s8_copy == "hello"); - lString8 s8_move(std::move(s8_copy)); - TCHECK(s8_move == "hello"); - - // Assignment - lString8 s8_assign; - s8_assign = s8_move; - TCHECK(s8_assign == "hello"); - lString8 s8_assign2; - s8_assign2 = "world"; - TCHECK(s8_assign2 == "world"); - lString8 s8_assign3; - s8_assign3 = lString8("temp"); - TCHECK(s8_assign3 == "temp"); - - // assign methods - lString8 s8_a; - s8_a.assign("test"); - TCHECK(s8_a == "test"); - s8_a.assign("abcdef", 3); - TCHECK(s8_a == "abc"); - lString8 s8_b("xyz"); - s8_a.assign(s8_b); - TCHECK(s8_a == "xyz"); - s8_a.assign(lString8("move_assign")); - TCHECK(s8_a == "move_assign"); - - // Append - lString8 s8_app; - s8_app.append("foo"); - TCHECK(s8_app == "foo"); - s8_app.append("xy", 1); - TCHECK(s8_app == "foox"); - s8_app.append(3, '!'); - TCHECK(s8_app == "foox!!!"); - s8_app.clear(); - - // appendDecimal / appendHex - s8_app.appendDecimal(12345); - TCHECK(s8_app == "12345"); - s8_app.clear(); - s8_app.appendDecimal(-42); - TCHECK(s8_app == "-42"); - s8_app.clear(); - s8_app.appendHex(0xFF); - TCHECK(s8_app == "ff"); - s8_app.clear(); - - // Insert (char-repeat variant) - lString8 s8_ins("world"); - s8_ins.insert(0, 2, 'X'); - TCHECK(s8_ins == "XXworld"); - - // Replace - lString8 s8_rep("hello world"); - s8_rep.replace(0, 5, lString8("good")); - TCHECK(s8_rep == "good world"); - lString8 s8_rep2("abcabc"); - s8_rep2.replace('a', 'X'); - TCHECK(s8_rep2 == "XbcXbc"); - - // Erase - lString8 s8_era("0123456789"); - s8_era.erase(5, 5); - TCHECK(s8_era == "01234"); - - // uppercase / lowercase - lString8 s8_case("Hello World"); - s8_case.uppercase(); - TCHECK(s8_case == "HELLO WORLD"); - s8_case.lowercase(); - TCHECK(s8_case == "hello world"); - - // compare - TCHECK(s8_cstr.compare("hello") == 0); - TCHECK(s8_cstr.compare("world") < 0); - TCHECK(s8_cstr.compare("abc") > 0); - - // pos / rpos - lString8 s8_pos("hello world, hello"); - TCHECK(s8_pos.pos('w') == 6); - TCHECK(s8_pos.pos("world") == 6); - TCHECK(s8_pos.pos('h', 1) == 13); - TCHECK(s8_pos.rpos("hello") == 13); - TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); - - // startsWith / endsWith - lString8 s8_sw("hello.cpp"); - TCHECK(s8_sw.startsWith("hello")); - TCHECK(s8_sw.startsWith(lString8("hello"))); - TCHECK(!s8_sw.startsWith("world")); - TCHECK(s8_sw.endsWith(".cpp")); - - // substr - lString8 s8_sub2("hello world"); - TCHECK(s8_sub2.substr(6) == "world"); - TCHECK(s8_sub2.substr(0, 5) == "hello"); - - // operator[] - TCHECK(s8_sub2[0] == 'h'); - TCHECK(s8_sub2[4] == 'o'); - - // data / c_str - TCHECK(strcmp(s8_sub2.c_str(), "hello world") == 0); - TCHECK(strcmp(s8_sub2.data(), "hello world") == 0); - - // length / size / capacity / empty - TCHECK(s8_sub2.length() == 11); - TCHECK(s8_sub2.size() == 11); - TCHECK(!s8_sub2.empty()); - lString8 s8_empty; - TCHECK(s8_empty.empty()); - TCHECK(!s8_empty); - - // at (safe access) - TCHECK(s8_sub2.at(0) == 'h'); - TCHECK(s8_sub2.at(10) == 'd'); - - // atoi - lString8 s8_int("12345"); - TCHECK(s8_int.atoi() == 12345); - lString8 s8_int2("-999"); - TCHECK(s8_int2.atoi() == -999); - lString8 s8_int64("1234567890123"); - TCHECK(s8_int64.atoi64() == 1234567890123LL); - - // swap - lString8 s8_swap1("first"); - lString8 s8_swap2("second"); - s8_swap1.swap(s8_swap2); - TCHECK(s8_swap1 == "second"); - TCHECK(s8_swap2 == "first"); - - // trim - lString8 s8_trim(" spaced "); - s8_trim.trim(); - TCHECK(s8_trim == "spaced"); - - // pack - lString8 s8_pack("hello"); - s8_pack.reserve(1000); - s8_pack.pack(); - - // clear / reset - lString8 s8_clr("something"); - s8_clr.clear(); - TCHECK(s8_clr.empty()); - lString8 s8_rst("something"); - s8_rst.reset(50); - TCHECK(s8_rst.empty()); - TCHECK(s8_rst.capacity() >= 49); - - // resize (fills extra space but doesn't update length) - lString8 s8_rsz("hi"); - s8_rsz.resize(5, 'x'); - - // lastChar / firstChar - lString8 s8_lf("abc"); - TCHECK(s8_lf.firstChar() == 'a'); - TCHECK(s8_lf.lastChar() == 'c'); - - // getHash - lString8 s8_h1("hello"); - lString8 s8_h2("hello"); - TCHECK(s8_h1.getHash() == s8_h2.getHash()); - - // operator << - lString8 s8_shift; - s8_shift << 'A'; - TCHECK(s8_shift == "A"); - s8_shift << "BC"; - TCHECK(s8_shift == "ABC"); - s8_shift << lString8("DE"); - TCHECK(s8_shift == "ABCDE"); - s8_shift.clear(); - s8_shift << fmt::decimal(42); - TCHECK(s8_shift == "42"); - s8_shift.clear(); - s8_shift << fmt::hex(255); - TCHECK(s8_shift == "ff"); - - // operator += - lString8 s8_plus; - s8_plus += "hello"; - TCHECK(s8_plus == "hello"); - s8_plus += lString8(" world"); - TCHECK(s8_plus == "hello world"); - s8_plus += '!'; - TCHECK(s8_plus == "hello world!"); - s8_plus += fmt::decimal(99); - TCHECK(s8_plus == "hello world!99"); - - // external operator == / != - lString8 s8_eq("equal"); - lString8 s8_eq2("equal"); - lString8 s8_neq("not_equal"); - TCHECK(s8_eq == s8_eq2); - TCHECK(!(s8_eq != s8_eq2)); - TCHECK(s8_eq != s8_neq); - TCHECK(s8_eq == "equal"); - TCHECK("equal" == s8_eq); - TCHECK(s8_eq != "other"); - TCHECK("other" != s8_eq); - - // external operator + - lString8 s8_add("abc"); - lString8 s8_added = s8_add + "def"; - TCHECK(s8_added == "abcdef"); - s8_added = s8_add + lString8("ghi"); - TCHECK(s8_added == "abcghi"); - s8_added = s8_add + fmt::decimal(123); - TCHECK(s8_added == "abc123"); - s8_added = s8_add + fmt::hex(0xFF); - TCHECK(s8_added == "abcff"); - - // itoa static - lString8 s8_i = lString8::itoa(42); - TCHECK(s8_i == "42"); - s8_i = lString8::itoa(0U); - TCHECK(s8_i == "0"); - s8_i = lString8::itoa(-123LL); - TCHECK(s8_i == "-123"); - - // cs8 - const lString8& cs8_test = cs8("static_string"); - TCHECK(cs8_test == "static_string"); - // calling cs8 with same string returns same reference - const lString8& cs8_test2 = cs8("static_string"); - TCHECK(&cs8_test == &cs8_test2); - - printf("=== lString16 tests ===\n"); - - // lString16 constructors - lString16 s16_default; - TCHECK(s16_default.empty()); - lString16 s16_cstr(u"hello"); - TCHECK(s16_cstr.length() == 5); - lString16 s16_from8("hello"); // from lChar8 - TCHECK(s16_from8.length() == 5); - lString16 s16_frag(u"hello world", 5); - TCHECK(s16_frag == u"hello"); - lString16 s16_sub(s16_cstr, 1, 3); - TCHECK(s16_sub == u"ell"); - - // Copy / move - lString16 s16_copy(s16_cstr); - TCHECK(s16_copy == u"hello"); - lString16 s16_move(std::move(s16_copy)); - TCHECK(s16_move == u"hello"); - - // Assignment - lString16 s16_assign; - s16_assign = u"world"; - TCHECK(s16_assign == u"world"); - s16_assign = "ascii"; - TCHECK(s16_assign == u"ascii"); - - // Append - lString16 s16_app; - s16_app.append(u"foo"); - TCHECK(s16_app == u"foo"); - s16_app.append((const lChar8*)"bar"); - TCHECK(s16_app == u"foobar"); - s16_app.append(3, u'!'); - TCHECK(s16_app == u"foobar!!!"); - s16_app.clear(); - - // appendDecimal / appendHex - s16_app.appendDecimal(999); - TCHECK(s16_app == u"999"); - s16_app.clear(); - s16_app.appendHex(0xABC); - TCHECK(s16_app == u"abc"); - - // Insert (only string and char-repeat variants exist) - lString16 s16_ins(u"world"); - s16_ins.insert(0, 2, u'X'); - s16_ins.clear(); - s16_ins = u"world"; - s16_ins.insert(0, lString16(u"hello ")); - - // compare - TCHECK(s16_cstr.compare(u"hello") == 0); - TCHECK(s16_cstr.compare((const lChar8*)"hello") == 0); - TCHECK(s16_cstr.compare(u"world") < 0); - - // Replace (not implemented for lString16 -- skip) - - // substr - lString16 s16_sub3(u"hello world"); - TCHECK(s16_sub3.substr(6) == u"world"); - TCHECK(s16_sub3.substr(0, 5) == u"hello"); - - // startsWith / endsWith - TCHECK(s16_sub3.startsWith(u"hello")); - TCHECK(s16_sub3.startsWith((const lChar8*)"hello")); - TCHECK(s16_sub3.endsWith(u"world")); - TCHECK(s16_sub3.endsWith((const lChar8*)"world")); - TCHECK(s16_sub3.endsWith(lString16(u"world"))); - // startsWithNoCase not implemented for lString16 -- skip - - // operator[] - TCHECK(s16_sub3[0] == u'h'); - - // atoi - lString16 s16_int(u"6789"); - int s16_ival = 0; - lInt64 s16_ival64 = 0; - TCHECK(s16_int.atoi() == 6789); - TCHECK(s16_int.atoi(s16_ival) && s16_ival == 6789); - TCHECK(s16_int.atoi(s16_ival64) && s16_ival64 == 6789); - - // getHash - lString16 s16_h1(u"test"); - lString16 s16_h2(u"test"); - TCHECK(s16_h1.getHash() == s16_h2.getHash()); - - // trim / trimNonAlpha - lString16 s16_tr(u" spaced "); - s16_tr.trim(); - TCHECK(s16_tr == u"spaced"); - lString16 s16_tna(u"!!!hello!!!"); - s16_tna.trimNonAlpha(); - TCHECK(s16_tna == u"hello"); - - // swap - lString16 s16_sw1(u"first"); - lString16 s16_sw2(u"second"); - s16_sw1.swap(s16_sw2); - TCHECK(s16_sw1 == u"second"); - TCHECK(s16_sw2 == u"first"); - - // clear / reset - lString16 s16_clr(u"something"); - s16_clr.clear(); - TCHECK(s16_clr.empty()); - lString16 s16_rst2(u"something"); - s16_rst2.reset(50); - TCHECK(s16_rst2.empty()); - - // limit - lString16 s16_lim(u"hello world"); - s16_lim.limit(5); - TCHECK(s16_lim == u"hello"); - - // pack - lString16 s16_pk(u"test"); - s16_pk.reserve(500); - - // operator << - lString16 s16_sh; - s16_sh << u'A'; - TCHECK(s16_sh == u"A"); - s16_sh << u"BC"; - TCHECK(s16_sh == u"ABC"); - s16_sh << fmt::decimal(42); - TCHECK(s16_sh == u"ABC42"); - s16_sh.clear(); - s16_sh << fmt::hex(255); - TCHECK(s16_sh == u"ff"); - - // operator += - lString16 s16_pe; - s16_pe += u"hello"; - TCHECK(s16_pe == u"hello"); - s16_pe += lString16(u" world"); - TCHECK(s16_pe == u"hello world"); - - // itoa - lString16 s16_i16 = lString16::itoa(42); - TCHECK(s16_i16 == u"42"); - s16_i16 = lString16::itoa(0U); - TCHECK(s16_i16 == u"0"); - - // external operator == / != / + - lString16 s16_eq1(u"equal"); - lString16 s16_eq2(u"equal"); - TCHECK(s16_eq1 == s16_eq2); - TCHECK(s16_eq1 == u"equal"); - TCHECK(u"equal" == s16_eq1); - TCHECK(s16_eq1 != u"other"); - lString16 s16_added16 = s16_eq1 + u"_suffix"; - TCHECK(s16_added16 == u"equal_suffix"); - - printf("=== lString32 tests ===\n"); - - // lString32 constructors - lString32 s32_default; - TCHECK(s32_default.empty()); - lString32 s32_cstr(U"hello"); - TCHECK(s32_cstr.length() == 5); - lString32 s32_from8("hello"); // from lChar8 - TCHECK(s32_from8.length() == 5); - lString32 s32_frag(U"hello world", 5); - TCHECK(s32_frag == U"hello"); - lString32 s32_sub(s32_cstr, 1, 3); - TCHECK(s32_sub == U"ell"); - - // Copy / move - lString32 s32_copy(s32_cstr); - TCHECK(s32_copy == U"hello"); - lString32 s32_move(std::move(s32_copy)); - TCHECK(s32_move == U"hello"); - - // Assignment - lString32 s32_assign; - s32_assign = U"world"; - TCHECK(s32_assign == U"world"); - s32_assign = "ascii"; - TCHECK(s32_assign == U"ascii"); - - // Append - lString32 s32_app; - s32_app.append(U"foo"); - TCHECK(s32_app == U"foo"); - s32_app.append((const lChar8*)"bar"); - TCHECK(s32_app == U"foobar"); - s32_app.append(3, U'!'); - TCHECK(s32_app == U"foobar!!!"); - s32_app.clear(); - s32_app.append(lString32(U"test")); - TCHECK(s32_app == U"test"); - - // appendDecimal / appendHex - s32_app.appendDecimal(12345); - TCHECK(s32_app == U"test12345"); - s32_app.clear(); - s32_app.appendHex(0xFF); - TCHECK(s32_app == U"ff"); - - // Insert (only lString32 and char-repeat variants exist) - lString32 s32_ins(U"world"); - s32_ins.insert(0, lString32(U"hello ")); - TCHECK(s32_ins == U"hello world"); - - // Replace (only lString32 variant exists) - lString32 s32_rp(U"ab_ab_ab"); - s32_rp.replace(U"ab", U"X"); - - // Replace (range, lString32 variant) - lString32 s32_rpr(U"hello world"); - s32_rpr.replace(0, 5, lString32(U"good")); - - // Erase - lString32 s32_era(U"0123456789"); - s32_era.erase(5, 5); - TCHECK(s32_era == U"01234"); - - // uppercase / lowercase / capitalize - lString32 s32_case(U"hello world"); - s32_case.uppercase(); - TCHECK(s32_case == U"HELLO WORLD"); - s32_case.lowercase(); - TCHECK(s32_case == U"hello world"); - s32_case.capitalize(); - TCHECK(s32_case == U"Hello World"); - - // fullWidthChars - lString32 s32_fw(U"abc"); - s32_fw.fullWidthChars(); - TCHECK(s32_fw.length() == 3); - TCHECK(s32_fw[0] > 0xFF00); // is fullwidth - - // compare - TCHECK(s32_cstr.compare(U"hello") == 0); - TCHECK(s32_cstr.compare((const lChar8*)"hello") == 0); - TCHECK(s32_cstr.compare(U"world") < 0); - - // split2 (check return value and basic operation) - lString32 s32_sp(U"key=value"); - lString32 k, v; - bool split_ok = s32_sp.split2(U"=", k, v); - - // pos / rpos - lString32 s32_pos(U"hello world, hello"); - TCHECK(s32_pos.pos(U'w') == 6); - TCHECK(s32_pos.pos(U"world") == 6); - TCHECK(s32_pos.pos(U'h', 1) == 13); - TCHECK(s32_pos.rpos(U"hello") == 13); - - // startsWith / endsWith / startsWithNoCase - lString32 s32_sw(U"Hello.cpp"); - TCHECK(s32_sw.startsWith(U"Hello")); - TCHECK(s32_sw.startsWith((const lChar8*)"Hello")); - TCHECK(s32_sw.startsWith(lString32(U"Hello"))); - TCHECK(s32_sw.endsWith(U".cpp")); - TCHECK(s32_sw.endsWith((const lChar8*)".cpp")); - TCHECK(s32_sw.endsWith(lString32(U".cpp"))); - lString32 s32_nc(U"Hello World"); - TCHECK(s32_nc.startsWithNoCase(lString32(U"hello"))); - - // substr - lString32 s32_sub2(U"hello world"); - TCHECK(s32_sub2.substr(6) == U"world"); - TCHECK(s32_sub2.substr(0, 5) == U"hello"); - - // atoi / atod - lString32 s32_int(U"12345"); - TCHECK(s32_int.atoi() == 12345); - int _i; - lInt64 _i64; - double _d; - TCHECK(s32_int.atoi(_i) && _i == 12345); - TCHECK(s32_int.atoi(_i64) && _i64 == 12345); - lString32 s32_dbl(U"3.14"); - TCHECK(s32_dbl.atod() > 3.13 && s32_dbl.atod() < 3.15); - TCHECK(s32_dbl.atod(_d) && _d > 3.13 && _d < 3.15); - - // operator[] - TCHECK(s32_sub2[0] == U'h'); - - // data / c_str - lString32 s32_dc(U"test_data"); - TCHECK(s32_dc.c_str() != nullptr); - TCHECK(s32_dc.data() != nullptr); - - // getHash - lString32 s32_h1(U"hash"); - lString32 s32_h2(U"hash"); - TCHECK(s32_h1.getHash() == s32_h2.getHash()); - - // swap - lString32 s32_sw1(U"first"); - lString32 s32_sw2(U"second"); - s32_sw1.swap(s32_sw2); - TCHECK(s32_sw1 == U"second"); - TCHECK(s32_sw2 == U"first"); - - // clear / reset - lString32 s32_clr(U"something"); - s32_clr.clear(); - TCHECK(s32_clr.empty()); - lString32 s32_rst3(U"something"); - s32_rst3.reset(50); - TCHECK(s32_rst3.empty()); - - // resize / limit - lString32 s32_rsz(U"hi"); - s32_rsz.resize(5, U'x'); - TCHECK(s32_rsz == U"hixxx"); - lString32 s32_lim(U"hello world"); - s32_lim.limit(5); - TCHECK(s32_lim == U"hello"); - - // trim / trimNonAlpha / trimDoubleSpaces - lString32 s32_tr(U" spaced "); - s32_tr.trim(); - TCHECK(s32_tr == U"spaced"); - lString32 s32_tna(U"!!!hello!!!"); - s32_tna.trimNonAlpha(); - TCHECK(s32_tna == U"hello"); - lString32 s32_tds(U"a b c"); - s32_tds.trimDoubleSpaces(false, false); - TCHECK(s32_tds == U"a b c"); - - // pack - lString32 s32_pk2(U"test"); - s32_pk2.reserve(500); - TCHECK(s32_pk2.capacity() >= 500); - s32_pk2.pack(); - TCHECK(s32_pk2.capacity() < 100); - - // operator << - lString32 s32_sh; - s32_sh << U'A'; - TCHECK(s32_sh == U"A"); - s32_sh << U"BC"; - TCHECK(s32_sh == U"ABC"); - s32_sh << fmt::decimal(42); - TCHECK(s32_sh == U"ABC42"); - s32_sh.clear(); - s32_sh << fmt::hex(255); - TCHECK(s32_sh == U"ff"); - - // operator += - lString32 s32_pe; - s32_pe += U"hello"; - TCHECK(s32_pe == U"hello"); - s32_pe += lString32(U" world"); - TCHECK(s32_pe == U"hello world"); - - // replaceParam / replaceIntParam - lString32 s32_rp2(U"Hello $1, you have $2 messages"); - TCHECK(s32_rp2.replaceParam(1, U"Alice")); - TCHECK(s32_rp2.replaceIntParam(2, 5)); - TCHECK(s32_rp2 == U"Hello Alice, you have 5 messages"); - - // itoa - lString32 s32_i32 = lString32::itoa(42); - TCHECK(s32_i32 == U"42"); - s32_i32 = lString32::itoa(0U); - TCHECK(s32_i32 == U"0"); - s32_i32 = lString32::itoa(-99LL); - TCHECK(s32_i32 == U"-99"); - s32_i32 = lString32::itoa(0xFFFFFFFFFFFFFFFFULL); - TCHECK(!s32_i32.empty()); - - // cs32 - const lString32& cs32_test = cs32("static_str32"); - TCHECK(cs32_test == U"static_str32"); - const lString32& cs32_test2 = cs32(U"wide_static"); - TCHECK(cs32_test2 == U"wide_static"); - - // external operator == / != / + - lString32 s32_eq1(U"equal"); - lString32 s32_eq2(U"equal"); - TCHECK(s32_eq1 == s32_eq2); - TCHECK(s32_eq1 == U"equal"); - TCHECK(U"equal" == s32_eq1); - TCHECK(s32_eq1 != U"other"); - lString32 s32_added32 = s32_eq1 + U"_suffix"; - TCHECK(s32_added32 == U"equal_suffix"); - s32_added32 = s32_eq1 + lString32(U"_extra"); - TCHECK(s32_added32 == U"equal_extra"); - s32_added32 = s32_eq1 + fmt::decimal(123); - TCHECK(s32_added32 == U"equal123"); - s32_added32 = s32_eq1 + fmt::hex(0xFF); - TCHECK(s32_added32 == U"equalff"); - - // Mixed-type comparisons - lString32 s32_mix(U"test"); - lString16 s16_mix(u"test"); - lString8 s8_mix("test"); - TCHECK(s32_mix == s16_mix); - TCHECK(s32_mix == s8_mix); - TCHECK(s16_mix == s32_mix); - - // External conversion functions - lString32 s32_conv(U"Hello World"); - lString8 s8_utf8 = UnicodeToUtf8(s32_conv); - TCHECK(s8_utf8 == "Hello World"); - lString16 s16_utf16 = UnicodeToUtf16(s32_conv); - TCHECK(s16_utf16 == u"Hello World"); - lString8 s8_local = UnicodeToLocal(s32_conv); - TCHECK(!s8_local.empty()); - lString8 s8_wtf8 = UnicodeToWtf8(s32_conv); - TCHECK(s8_wtf8 == "Hello World"); - - // Reverse conversion - lString32 s32_back = Utf8ToUnicode(s8_utf8); - TCHECK(s32_back == U"Hello World"); - s32_back = Utf8ToUnicode("direct_cstr"); - TCHECK(s32_back == U"direct_cstr"); - s32_back = Utf8ToUnicode("fragment", 4); - TCHECK(s32_back == U"frag"); - s32_back = Utf16ToUnicode(s16_utf16); - TCHECK(s32_back == U"Hello World"); - s32_back = Utf16ToUnicode(u"direct16"); - TCHECK(s32_back == U"direct16"); - lString32 s32_wtf = Wtf8ToUnicode(s8_wtf8); - TCHECK(s32_wtf == U"Hello World"); - s32_wtf = Wtf8ToUnicode("wtf_direct"); - TCHECK(s32_wtf == U"wtf_direct"); - - // external getHash - lString32 s32_gh(U"hash_me"); - lString16 s16_gh(u"hash_me"); - lString8 s8_gh("hash_me"); - TCHECK(getHash(s32_gh) == s32_gh.getHash()); - TCHECK(getHash(s16_gh) == s16_gh.getHash()); - TCHECK(getHash(s8_gh) == s8_gh.getHash()); - - if (test_errors == 0) - printf("All string tests passed!\n"); - else - printf("FAILED: %d test(s) failed!\n", test_errors); -} - bool testsOnly = false; int main(int argc, const char ** argv) { From 14739e5af7711b7f9d408ba3890162f8c6cced38 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Thu, 23 Jul 2026 15:08:28 +0100 Subject: [PATCH 06/36] extended test suite for strings, improved diagnostics; tests still failing --- crengine/src/lvstring.cpp | 48 +++++----- tests/src/stringtest.cpp | 189 +++++++++++++++++++++----------------- tests/src/testmain.cpp | 12 ++- 3 files changed, 136 insertions(+), 113 deletions(-) diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index a8a11023dd..4b3d28d751 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -248,14 +248,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - //if (!pChunk->size) - //{ - // crFatalError(); // already freed!!! - //} - //if (pChunk->refCount) - //{ - // crFatalError(); // has references - //} + if (!pChunk->size) + { + crFatalError(); // already freed!!! + } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; pChunk->len = 0; #endif @@ -270,14 +270,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - // if (!pChunk->size) - // { - // crFatalError(); // already freed!!! - // } - // if (pChunk->refCount) - // { - // crFatalError(); // has references - // } + if (!pChunk->size) + { + crFatalError(); // already freed!!! + } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; #endif @@ -291,14 +291,14 @@ struct lstring_chunk_slice_t { return false; // chunk does not belong to this slice #ifdef LS_DEBUG_CHECK - // if (!pChunk->size) - // { - // crFatalError(); // already freed!!! - // } - // if (pChunk->refCount) - // { - // crFatalError(); // has references - // } + if (!pChunk->size) + { + crFatalError(); // already freed!!! + } + if (pChunk->refCount) + { + crFatalError(); // has references + } pChunk->size = 0; #endif diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp index 9138331587..1e6958618e 100644 --- a/tests/src/stringtest.cpp +++ b/tests/src/stringtest.cpp @@ -8,10 +8,11 @@ static int test_errors = 0; #define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) -void testStrings() { +void testStrings8() { + printf("=== lString8 tests ===\n"); - // Constructors + // Constructors lString8 s8_default; TCHECK(s8_default.empty()); @@ -34,7 +35,7 @@ void testStrings() { lString8 s8_move(std::move(s8_copy)); TCHECK(s8_move == "hello"); - // Assignment + // Assignment lString8 s8_assign; s8_assign = s8_move; TCHECK(s8_assign == "hello"); @@ -57,7 +58,7 @@ void testStrings() { s8_a.assign(lString8("move_assign")); TCHECK(s8_a == "move_assign"); - // Append + // Append lString8 s8_app; s8_app.append("foo"); TCHECK(s8_app == "foo"); @@ -83,7 +84,7 @@ void testStrings() { s8_ins.insert(0, 2, 'X'); TCHECK(s8_ins == "XXworld"); - // Replace + // Replace lString8 s8_rep("hello world"); s8_rep.replace(0, 5, lString8("good")); TCHECK(s8_rep == "good world"); @@ -91,24 +92,24 @@ void testStrings() { s8_rep2.replace('a', 'X'); TCHECK(s8_rep2 == "XbcXbc"); - // Erase + // Erase lString8 s8_era("0123456789"); s8_era.erase(5, 5); TCHECK(s8_era == "01234"); - // uppercase / lowercase + // uppercase / lowercase lString8 s8_case("Hello World"); s8_case.uppercase(); TCHECK(s8_case == "HELLO WORLD"); s8_case.lowercase(); TCHECK(s8_case == "hello world"); - // compare + // compare TCHECK(s8_cstr.compare("hello") == 0); TCHECK(s8_cstr.compare("world") < 0); TCHECK(s8_cstr.compare("abc") > 0); - // pos / rpos + // pos / rpos lString8 s8_pos("hello world, hello"); TCHECK(s8_pos.pos('w') == 6); TCHECK(s8_pos.pos("world") == 6); @@ -116,27 +117,27 @@ void testStrings() { TCHECK(s8_pos.rpos("hello") == 13); TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); - // startsWith / endsWith + // startsWith / endsWith lString8 s8_sw("hello.cpp"); TCHECK(s8_sw.startsWith("hello")); TCHECK(s8_sw.startsWith(lString8("hello"))); TCHECK(!s8_sw.startsWith("world")); TCHECK(s8_sw.endsWith(".cpp")); - // substr + // substr lString8 s8_sub2("hello world"); TCHECK(s8_sub2.substr(6) == "world"); TCHECK(s8_sub2.substr(0, 5) == "hello"); - // operator[] + // operator[] TCHECK(s8_sub2[0] == 'h'); TCHECK(s8_sub2[4] == 'o'); - // data / c_str + // data / c_str TCHECK(strcmp(s8_sub2.c_str(), "hello world") == 0); TCHECK(strcmp(s8_sub2.data(), "hello world") == 0); - // length / size / capacity / empty + // length / size / capacity / empty TCHECK(s8_sub2.length() == 11); TCHECK(s8_sub2.size() == 11); TCHECK(!s8_sub2.empty()); @@ -144,11 +145,11 @@ void testStrings() { TCHECK(s8_empty.empty()); TCHECK(!s8_empty); - // at + // at TCHECK(s8_sub2.at(0) == 'h'); TCHECK(s8_sub2.at(10) == 'd'); - // atoi + // atoi lString8 s8_int("12345"); TCHECK(s8_int.atoi() == 12345); lString8 s8_int2("-999"); @@ -156,24 +157,24 @@ void testStrings() { lString8 s8_int64("1234567890123"); TCHECK(s8_int64.atoi64() == 1234567890123LL); - // swap + // swap lString8 s8_swap1("first"); lString8 s8_swap2("second"); s8_swap1.swap(s8_swap2); TCHECK(s8_swap1 == "second"); TCHECK(s8_swap2 == "first"); - // trim + // trim lString8 s8_trim(" spaced "); s8_trim.trim(); TCHECK(s8_trim == "spaced"); - // pack + // pack lString8 s8_pack("hello"); s8_pack.reserve(1000); s8_pack.pack(); - // clear / reset + // clear / reset lString8 s8_clr("something"); s8_clr.clear(); TCHECK(s8_clr.empty()); @@ -182,21 +183,21 @@ void testStrings() { TCHECK(s8_rst.empty()); TCHECK(s8_rst.capacity() >= 49); - // resize + // resize lString8 s8_rsz("hi"); s8_rsz.resize(5, 'x'); - // lastChar / firstChar + // lastChar / firstChar lString8 s8_lf("abc"); TCHECK(s8_lf.firstChar() == 'a'); TCHECK(s8_lf.lastChar() == 'c'); - // getHash + // getHash lString8 s8_h1("hello"); lString8 s8_h2("hello"); TCHECK(s8_h1.getHash() == s8_h2.getHash()); - // operator << + // operator << lString8 s8_shift; s8_shift << 'A'; TCHECK(s8_shift == "A"); @@ -211,7 +212,7 @@ void testStrings() { s8_shift << fmt::hex(255); TCHECK(s8_shift == "ff"); - // operator += + // operator += lString8 s8_plus; s8_plus += "hello"; TCHECK(s8_plus == "hello"); @@ -222,7 +223,7 @@ void testStrings() { s8_plus += fmt::decimal(99); TCHECK(s8_plus == "hello world!99"); - // external operator == / != + // external operator == / != lString8 s8_eq("equal"); lString8 s8_eq2("equal"); lString8 s8_neq("not_equal"); @@ -234,7 +235,7 @@ void testStrings() { TCHECK(s8_eq != "other"); TCHECK("other" != s8_eq); - // external operator + + // external operator + lString8 s8_add("abc"); lString8 s8_added = s8_add + "def"; TCHECK(s8_added == "abcdef"); @@ -245,7 +246,7 @@ void testStrings() { s8_added = s8_add + fmt::hex(0xFF); TCHECK(s8_added == "abcff"); - // itoa static + // itoa static lString8 s8_i = lString8::itoa(42); TCHECK(s8_i == "42"); s8_i = lString8::itoa(0U); @@ -253,15 +254,17 @@ void testStrings() { s8_i = lString8::itoa(-123LL); TCHECK(s8_i == "-123"); - // cs8 + // cs8 const lString8& cs8_test = cs8("static_string"); TCHECK(cs8_test == "static_string"); const lString8& cs8_test2 = cs8("static_string"); TCHECK(&cs8_test == &cs8_test2); +} +void testStrings16() { printf("=== lString16 tests ===\n"); - // lString16 constructors + // lString16 constructors lString16 s16_default; TCHECK(s16_default.empty()); lString16 s16_cstr(u"hello"); @@ -273,20 +276,20 @@ void testStrings() { lString16 s16_sub(s16_cstr, 1, 3); TCHECK(s16_sub == u"ell"); - // Copy / move + // Copy / move lString16 s16_copy(s16_cstr); TCHECK(s16_copy == u"hello"); lString16 s16_move(std::move(s16_copy)); TCHECK(s16_move == u"hello"); - // Assignment + // Assignment lString16 s16_assign; s16_assign = u"world"; TCHECK(s16_assign == u"world"); s16_assign = "ascii"; TCHECK(s16_assign == u"ascii"); - // Append + // Append lString16 s16_app; s16_app.append(u"foo"); TCHECK(s16_app == u"foo"); @@ -296,41 +299,41 @@ void testStrings() { TCHECK(s16_app == u"foobar!!!"); s16_app.clear(); - // appendDecimal / appendHex + // appendDecimal / appendHex s16_app.appendDecimal(999); TCHECK(s16_app == u"999"); s16_app.clear(); s16_app.appendHex(0xABC); TCHECK(s16_app == u"abc"); - // Insert + // Insert lString16 s16_ins(u"world"); s16_ins.insert(0, 2, u'X'); s16_ins.clear(); s16_ins = u"world"; s16_ins.insert(0, lString16(u"hello ")); - // compare + // compare TCHECK(s16_cstr.compare(u"hello") == 0); TCHECK(s16_cstr.compare((const lChar8*)"hello") == 0); TCHECK(s16_cstr.compare(u"world") < 0); - // substr + // substr lString16 s16_sub3(u"hello world"); TCHECK(s16_sub3.substr(6) == u"world"); TCHECK(s16_sub3.substr(0, 5) == u"hello"); - // startsWith / endsWith + // startsWith / endsWith TCHECK(s16_sub3.startsWith(u"hello")); TCHECK(s16_sub3.startsWith((const lChar8*)"hello")); TCHECK(s16_sub3.endsWith(u"world")); TCHECK(s16_sub3.endsWith((const lChar8*)"world")); TCHECK(s16_sub3.endsWith(lString16(u"world"))); - // operator[] + // operator[] TCHECK(s16_sub3[0] == u'h'); - // atoi + // atoi lString16 s16_int(u"6789"); int s16_ival = 0; lInt64 s16_ival64 = 0; @@ -338,12 +341,12 @@ void testStrings() { TCHECK(s16_int.atoi(s16_ival) && s16_ival == 6789); TCHECK(s16_int.atoi(s16_ival64) && s16_ival64 == 6789); - // getHash + // getHash lString16 s16_h1(u"test"); lString16 s16_h2(u"test"); TCHECK(s16_h1.getHash() == s16_h2.getHash()); - // trim / trimNonAlpha + // trim / trimNonAlpha lString16 s16_tr(u" spaced "); s16_tr.trim(); TCHECK(s16_tr == u"spaced"); @@ -351,14 +354,14 @@ void testStrings() { s16_tna.trimNonAlpha(); TCHECK(s16_tna == u"hello"); - // swap + // swap lString16 s16_sw1(u"first"); lString16 s16_sw2(u"second"); s16_sw1.swap(s16_sw2); TCHECK(s16_sw1 == u"second"); TCHECK(s16_sw2 == u"first"); - // clear / reset + // clear / reset lString16 s16_clr(u"something"); s16_clr.clear(); TCHECK(s16_clr.empty()); @@ -366,16 +369,16 @@ void testStrings() { s16_rst2.reset(50); TCHECK(s16_rst2.empty()); - // limit + // limit lString16 s16_lim(u"hello world"); s16_lim.limit(5); TCHECK(s16_lim == u"hello"); - // pack + // pack lString16 s16_pk(u"test"); s16_pk.reserve(500); - // operator << + // operator << lString16 s16_sh; s16_sh << u'A'; TCHECK(s16_sh == u"A"); @@ -387,20 +390,20 @@ void testStrings() { s16_sh << fmt::hex(255); TCHECK(s16_sh == u"ff"); - // operator += + // operator += lString16 s16_pe; s16_pe += u"hello"; TCHECK(s16_pe == u"hello"); s16_pe += lString16(u" world"); TCHECK(s16_pe == u"hello world"); - // itoa + // itoa lString16 s16_i16 = lString16::itoa(42); TCHECK(s16_i16 == u"42"); s16_i16 = lString16::itoa(0U); TCHECK(s16_i16 == u"0"); - // external operator == / != / + + // external operator == / != / + lString16 s16_eq1(u"equal"); lString16 s16_eq2(u"equal"); TCHECK(s16_eq1 == s16_eq2); @@ -409,10 +412,12 @@ void testStrings() { TCHECK(s16_eq1 != u"other"); lString16 s16_added16 = s16_eq1 + u"_suffix"; TCHECK(s16_added16 == u"equal_suffix"); +} +void testStrings32() { printf("=== lString32 tests ===\n"); - // lString32 constructors + // lString32 constructors lString32 s32_default; TCHECK(s32_default.empty()); lString32 s32_cstr(U"hello"); @@ -424,20 +429,20 @@ void testStrings() { lString32 s32_sub(s32_cstr, 1, 3); TCHECK(s32_sub == U"ell"); - // Copy / move + // Copy / move lString32 s32_copy(s32_cstr); TCHECK(s32_copy == U"hello"); lString32 s32_move(std::move(s32_copy)); TCHECK(s32_move == U"hello"); - // Assignment + // Assignment lString32 s32_assign; s32_assign = U"world"; TCHECK(s32_assign == U"world"); s32_assign = "ascii"; TCHECK(s32_assign == U"ascii"); - // Append + // Append lString32 s32_app; s32_app.append(U"foo"); TCHECK(s32_app == U"foo"); @@ -449,32 +454,32 @@ void testStrings() { s32_app.append(lString32(U"test")); TCHECK(s32_app == U"test"); - // appendDecimal / appendHex + // appendDecimal / appendHex s32_app.appendDecimal(12345); TCHECK(s32_app == U"test12345"); s32_app.clear(); s32_app.appendHex(0xFF); TCHECK(s32_app == U"ff"); - // Insert + // Insert lString32 s32_ins(U"world"); s32_ins.insert(0, lString32(U"hello ")); TCHECK(s32_ins == U"hello world"); - // Replace + // Replace lString32 s32_rp(U"ab_ab_ab"); s32_rp.replace(U"ab", U"X"); - // Replace (range) + // Replace (range) lString32 s32_rpr(U"hello world"); s32_rpr.replace(0, 5, lString32(U"good")); - // Erase + // Erase lString32 s32_era(U"0123456789"); s32_era.erase(5, 5); TCHECK(s32_era == U"01234"); - // uppercase / lowercase / capitalize + // uppercase / lowercase / capitalize lString32 s32_case(U"hello world"); s32_case.uppercase(); TCHECK(s32_case == U"HELLO WORLD"); @@ -483,30 +488,30 @@ void testStrings() { s32_case.capitalize(); TCHECK(s32_case == U"Hello World"); - // fullWidthChars + // fullWidthChars lString32 s32_fw(U"abc"); s32_fw.fullWidthChars(); TCHECK(s32_fw.length() == 3); TCHECK(s32_fw[0] > 0xFF00); - // compare + // compare TCHECK(s32_cstr.compare(U"hello") == 0); TCHECK(s32_cstr.compare((const lChar8*)"hello") == 0); TCHECK(s32_cstr.compare(U"world") < 0); - // split2 + // split2 lString32 s32_sp(U"key=value"); lString32 k, v; s32_sp.split2(U"=", k, v); - // pos / rpos + // pos / rpos lString32 s32_pos(U"hello world, hello"); TCHECK(s32_pos.pos(U'w') == 6); TCHECK(s32_pos.pos(U"world") == 6); TCHECK(s32_pos.pos(U'h', 1) == 13); TCHECK(s32_pos.rpos(U"hello") == 13); - // startsWith / endsWith / startsWithNoCase + // startsWith / endsWith / startsWithNoCase lString32 s32_sw(U"Hello.cpp"); TCHECK(s32_sw.startsWith(U"Hello")); TCHECK(s32_sw.startsWith((const lChar8*)"Hello")); @@ -517,12 +522,12 @@ void testStrings() { lString32 s32_nc(U"Hello World"); TCHECK(s32_nc.startsWithNoCase(lString32(U"hello"))); - // substr + // substr lString32 s32_sub2(U"hello world"); TCHECK(s32_sub2.substr(6) == U"world"); TCHECK(s32_sub2.substr(0, 5) == U"hello"); - // atoi / atod + // atoi / atod lString32 s32_int(U"12345"); TCHECK(s32_int.atoi() == 12345); int _i; @@ -534,27 +539,27 @@ void testStrings() { TCHECK(s32_dbl.atod() > 3.13 && s32_dbl.atod() < 3.15); TCHECK(s32_dbl.atod(_d) && _d > 3.13 && _d < 3.15); - // operator[] + // operator[] TCHECK(s32_sub2[0] == U'h'); - // data / c_str + // data / c_str lString32 s32_dc(U"test_data"); TCHECK(s32_dc.c_str() != nullptr); TCHECK(s32_dc.data() != nullptr); - // getHash + // getHash lString32 s32_h1(U"hash"); lString32 s32_h2(U"hash"); TCHECK(s32_h1.getHash() == s32_h2.getHash()); - // swap + // swap lString32 s32_sw1(U"first"); lString32 s32_sw2(U"second"); s32_sw1.swap(s32_sw2); TCHECK(s32_sw1 == U"second"); TCHECK(s32_sw2 == U"first"); - // clear / reset + // clear / reset lString32 s32_clr(U"something"); s32_clr.clear(); TCHECK(s32_clr.empty()); @@ -562,7 +567,7 @@ void testStrings() { s32_rst3.reset(50); TCHECK(s32_rst3.empty()); - // resize / limit + // resize / limit lString32 s32_rsz(U"hi"); s32_rsz.resize(5, U'x'); TCHECK(s32_rsz == U"hixxx"); @@ -570,7 +575,7 @@ void testStrings() { s32_lim.limit(5); TCHECK(s32_lim == U"hello"); - // trim / trimNonAlpha / trimDoubleSpaces + // trim / trimNonAlpha / trimDoubleSpaces lString32 s32_tr(U" spaced "); s32_tr.trim(); TCHECK(s32_tr == U"spaced"); @@ -581,14 +586,14 @@ void testStrings() { s32_tds.trimDoubleSpaces(false, false); TCHECK(s32_tds == U"a b c"); - // pack + // pack lString32 s32_pk2(U"test"); s32_pk2.reserve(500); TCHECK(s32_pk2.capacity() >= 500); s32_pk2.pack(); TCHECK(s32_pk2.capacity() < 100); - // operator << + // operator << lString32 s32_sh; s32_sh << U'A'; TCHECK(s32_sh == U"A"); @@ -600,20 +605,20 @@ void testStrings() { s32_sh << fmt::hex(255); TCHECK(s32_sh == U"ff"); - // operator += + // operator += lString32 s32_pe; s32_pe += U"hello"; TCHECK(s32_pe == U"hello"); s32_pe += lString32(U" world"); TCHECK(s32_pe == U"hello world"); - // replaceParam / replaceIntParam + // replaceParam / replaceIntParam lString32 s32_rp2(U"Hello $1, you have $2 messages"); TCHECK(s32_rp2.replaceParam(1, U"Alice")); TCHECK(s32_rp2.replaceIntParam(2, 5)); TCHECK(s32_rp2 == U"Hello Alice, you have 5 messages"); - // itoa + // itoa lString32 s32_i32 = lString32::itoa(42); TCHECK(s32_i32 == U"42"); s32_i32 = lString32::itoa(0U); @@ -623,13 +628,13 @@ void testStrings() { s32_i32 = lString32::itoa(0xFFFFFFFFFFFFFFFFULL); TCHECK(!s32_i32.empty()); - // cs32 + // cs32 const lString32& cs32_test = cs32("static_str32"); TCHECK(cs32_test == U"static_str32"); const lString32& cs32_test2 = cs32(U"wide_static"); TCHECK(cs32_test2 == U"wide_static"); - // external operator == / != / + + // external operator == / != / + lString32 s32_eq1(U"equal"); lString32 s32_eq2(U"equal"); TCHECK(s32_eq1 == s32_eq2); @@ -645,7 +650,7 @@ void testStrings() { s32_added32 = s32_eq1 + fmt::hex(0xFF); TCHECK(s32_added32 == U"equalff"); - // Mixed-type comparisons + // Mixed-type comparisons lString32 s32_mix(U"test"); lString16 s16_mix(u"test"); lString8 s8_mix("test"); @@ -653,7 +658,7 @@ void testStrings() { TCHECK(s32_mix == s8_mix); TCHECK(s16_mix == s32_mix); - // External conversion functions + // External conversion functions lString32 s32_conv(U"Hello World"); lString8 s8_utf8 = UnicodeToUtf8(s32_conv); TCHECK(s8_utf8 == "Hello World"); @@ -664,7 +669,7 @@ void testStrings() { lString8 s8_wtf8 = UnicodeToWtf8(s32_conv); TCHECK(s8_wtf8 == "Hello World"); - // Reverse conversion + // Reverse conversion lString32 s32_back = Utf8ToUnicode(s8_utf8); TCHECK(s32_back == U"Hello World"); s32_back = Utf8ToUnicode("direct_cstr"); @@ -680,7 +685,7 @@ void testStrings() { s32_wtf = Wtf8ToUnicode("wtf_direct"); TCHECK(s32_wtf == U"wtf_direct"); - // external getHash + // external getHash lString32 s32_gh(U"hash_me"); lString16 s16_gh(u"hash_me"); lString8 s8_gh("hash_me"); @@ -688,6 +693,20 @@ void testStrings() { TCHECK(getHash(s16_gh) == s16_gh.getHash()); TCHECK(getHash(s8_gh) == s8_gh.getHash()); +} + +void testStringsMixed() { + +} + +void testStrings() { + + testStrings8(); + testStrings16(); + testStrings32(); + testStringsMixed(); + + if (test_errors == 0) printf("All string tests passed!\n"); else diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index 6a400bf4e7..a30969c364 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -384,15 +384,19 @@ void ShutdownCREngine() bool testsOnly = false; +void runLibraryTestSuite() { + printf("CR3 Library Tests\n"); + testStrings(); + testStringCollections(); + printf("CR3 Library Tests\n"); +} + int main(int argc, const char ** argv) { #if (LDOM_USE_OWN_MEM_MAN == 1) printf("LDOM_USE_OWN_MEM_MAN is turned ON\n"); #endif - printf("CR3 Library Tests\n"); - testStrings(); - testStringCollections(); - + runLibraryTestSuite(); if (testsOnly) { #if (LDOM_USE_OWN_MEM_MAN == 1) free_ls_storage(); From 01c3ebb9ae8dae505824b3325a00472ceccf30a5 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Thu, 23 Jul 2026 21:17:00 +0100 Subject: [PATCH 07/36] strings library fixes --- crengine/include/lvstring.h | 49 +++- crengine/src/lvstream/ziphdr.h | 4 +- crengine/src/lvstring.cpp | 27 +++ crengine/src/props.cpp | 2 +- tests/src/stringtest.cpp | 419 +++++++++++++++++++++++++++++++++ tests/src/testmain.cpp | 2 +- 6 files changed, 496 insertions(+), 7 deletions(-) diff --git a/crengine/include/lvstring.h b/crengine/include/lvstring.h index 323ac4a2ae..9f4d556a4f 100644 --- a/crengine/include/lvstring.h +++ b/crengine/include/lvstring.h @@ -298,6 +298,10 @@ namespace fmt { }; } +#if LDOM_USE_OWN_MEM_MAN==1 + /// returns true if string chunk storage has been destroyed + bool ls_storage_is_destroyed(); +#endif /** \brief lChar8 string @@ -342,6 +346,9 @@ class lString8 void alloc(size_type sz); void free(); inline void addref() const { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return; +#endif #ifdef USE_ATOMIC_REFCOUNT pchunk->refCount.fetch_add(1); #else @@ -350,6 +357,12 @@ class lString8 } inline void release() { if (!pchunk) return; +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) { + pchunk = nullptr; + return; + } +#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); @@ -359,6 +372,9 @@ class lString8 pchunk = nullptr; } inline int refCount() { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return 0; +#endif return pchunk->refCount; } explicit lString8(lstring_chunk_t * chunk) : pchunk(chunk) { addref(); } @@ -544,7 +560,7 @@ class lString8 /// changes buffer size void resize(size_type count = 0, value_type e = 0); /// returns maximum number of chars that can fit into buffer - size_type capacity() const { return pchunk->size-1; } + size_type capacity() const { return pchunk->size; } /// reserve space for specified amount of chars void reserve(size_type count = 0); /// returns true if string is empty @@ -623,6 +639,9 @@ class lString16 void alloc(size_type sz); void free(); inline void addref() const { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return; +#endif #ifdef USE_ATOMIC_REFCOUNT pchunk->refCount.fetch_add(1); #else @@ -631,6 +650,12 @@ class lString16 } inline void release() { if (!pchunk) return; +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) { + pchunk = nullptr; + return; + } +#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); @@ -640,6 +665,9 @@ class lString16 pchunk = nullptr; } inline int refCount() { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return 0; +#endif return pchunk->refCount; } public: @@ -797,7 +825,7 @@ class lString16 /// resizes string buffer, appends with specified character if buffer is being extended void resize(size_type count = 0, value_type e = 0); /// returns string buffer size - size_type capacity() const { return pchunk->size-1; } + size_type capacity() const { return pchunk->size; } /// ensures string buffer can hold at least count characters void reserve(size_type count = 0); /// erase all extra characters from end of string after size @@ -880,6 +908,9 @@ class lString32 void alloc(size_type sz); void free(); inline void addref() const { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return; +#endif #ifdef USE_ATOMIC_REFCOUNT pchunk->refCount.fetch_add(1); #else @@ -888,6 +919,12 @@ class lString32 } inline void release() { if (!pchunk) return; +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) { + pchunk = nullptr; + return; + } +#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); @@ -897,6 +934,9 @@ class lString32 pchunk = nullptr; } inline int refCount() { +#if LDOM_USE_OWN_MEM_MAN==1 + if (ls_storage_is_destroyed()) return 0; +#endif return pchunk->refCount; } public: @@ -1101,7 +1141,7 @@ class lString32 /// resizes string buffer, appends with specified character if buffer is being extended void resize(size_type count = 0, value_type e = 0); /// returns string buffer size - size_type capacity() const { return pchunk->size-1; } + size_type capacity() const { return pchunk->size; } /// ensures string buffer can hold at least count characters void reserve(size_type count = 0); /// erase all extra characters from end of string after size @@ -1346,6 +1386,9 @@ bool splitIntegerList( lString32 s, lString32 delim, int & value1, int & value2 #if LDOM_USE_OWN_MEM_MAN==1 void free_ls_storage(); +bool ls_storage_is_destroyed(); +/// checks free list in string chunk storage for consistency +void check_ls_storage(const char * msg); #endif #endif // __LV_STRING_H_INCLUDED__ diff --git a/crengine/src/lvstream/ziphdr.h b/crengine/src/lvstream/ziphdr.h index 929da43789..40351ce34d 100644 --- a/crengine/src/lvstream/ziphdr.h +++ b/crengine/src/lvstream/ziphdr.h @@ -27,7 +27,7 @@ #pragma pack(push, 1) -typedef struct { +struct ZipLocalFileHdr { lUInt32 Mark; // 0 lUInt8 UnpVer; // 4 lUInt8 UnpOS; // 5 @@ -71,7 +71,7 @@ typedef struct { // Omitted fields (which follow this structure): // FileName (size = NameLen) // ExtraField (size = AddLen) -} ZipLocalFileHdr; +}; struct ZipHd2 { diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index 4b3d28d751..ee81c53078 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -197,6 +197,16 @@ struct lstring_chunk_slice_t { pEnd = nullptr; pFree = nullptr; } + void check_free_node_chain(const char * msg = "free node chunk corrupted") { + lstring8_chunk_t * p = pFree; + while (p != nullptr) { + if (p < pChunks || p >= pEnd) { + printf("corrupted chunk %p\n", p); + crFatalError(3, msg); + } + p = (lstring8_chunk_t *)p->buf8; + } + } inline lstring8_chunk_t * alloc_chunk() { lstring8_chunk_t * res = pFree; @@ -314,6 +324,7 @@ struct lstring_chunk_slice_t { static lstring_chunk_slice_t * slices[MAX_SLICE_COUNT]; static int slices_count = 0; static bool slices_initialized = false; +static bool slices_destroyed = false; #endif #if (LDOM_USE_OWN_MEM_MAN == 1) @@ -334,8 +345,23 @@ void free_ls_storage() } slices_count = 0; slices_initialized = false; + slices_destroyed = true; +} + +bool ls_storage_is_destroyed() +{ + return slices_destroyed; } +void check_ls_storage(const char * msg) { + for (int i=slices_count-1; i>=0; --i) + { + lstring_chunk_slice_t * slice = slices[i]; + slice->check_free_node_chain(msg); + } +} + + lstring8_chunk_t * lstring8_chunk_t::alloc() { if (!slices_initialized) @@ -1290,6 +1316,7 @@ void lString32::resize(size_type n, lChar32 e) // fill with data if expanded for (size_type i=pchunk->len; ibuf32[i] = e; + pchunk->len = n; pchunk->buf32[pchunk->len] = 0; } diff --git a/crengine/src/props.cpp b/crengine/src/props.cpp index 0bbb4ac368..1fa4b7bab0 100644 --- a/crengine/src/props.cpp +++ b/crengine/src/props.cpp @@ -338,7 +338,7 @@ int CRPropAccessor::getIntDef( const char * propName, int defValue ) const void CRPropAccessor::setHex( const char * propName, lUInt32 value ) { char s[16]; - sprintf(s, "0x%08lX", value); + sprintf(s, "0x%08X", value); setString( propName, Utf8ToUnicode(lString8(s)) ); } diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp index 1e6958618e..24bbc8d89e 100644 --- a/tests/src/stringtest.cpp +++ b/tests/src/stringtest.cpp @@ -8,6 +8,424 @@ static int test_errors = 0; #define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) +#define CHECKBUF_(file, line) check_ls_storage(file ":" #line) +#define CHECKBUF CHECKBUF_(__FILE__, __LINE__) + +void testStrings8_16() { + printf("=== MIX lString8 tests ===\n"); + + // Constructors + lString8 s8_default; + TCHECK(s8_default.empty()); + + lString8 s8_size(10); + TCHECK(s8_size.empty()); + + lString8 s8_cstr("hello"); + TCHECK(s8_cstr.length() == 5); + TCHECK(s8_cstr == "hello"); + + lString8 s8_fragment("hello world", 5); + TCHECK(s8_fragment == "hello"); + + lString8 s8_sub(s8_cstr, 1, 3); + TCHECK(s8_sub == "ell"); + + // Copy / move + lString8 s8_copy(s8_cstr); + TCHECK(s8_copy == "hello"); + lString8 s8_move(std::move(s8_copy)); + TCHECK(s8_move == "hello"); + + // Assignment + lString8 s8_assign; + s8_assign = s8_move; + TCHECK(s8_assign == "hello"); + lString8 s8_assign2; + s8_assign2 = "world"; + TCHECK(s8_assign2 == "world"); + lString8 s8_assign3; + s8_assign3 = lString8("temp"); + TCHECK(s8_assign3 == "temp"); + + // assign methods + lString8 s8_a; + s8_a.assign("test"); + TCHECK(s8_a == "test"); + s8_a.assign("abcdef", 3); + TCHECK(s8_a == "abc"); + lString8 s8_b("xyz"); + s8_a.assign(s8_b); + TCHECK(s8_a == "xyz"); + s8_a.assign(lString8("move_assign")); + TCHECK(s8_a == "move_assign"); + + CHECKBUF; + + // Append + lString8 s8_app; + s8_app.append("foo"); + TCHECK(s8_app == "foo"); + s8_app.append("xy", 1); + TCHECK(s8_app == "foox"); + s8_app.append(3, '!'); + TCHECK(s8_app == "foox!!!"); + s8_app.clear(); + + // appendDecimal / appendHex + s8_app.appendDecimal(12345); + TCHECK(s8_app == "12345"); + s8_app.clear(); + s8_app.appendDecimal(-42); + TCHECK(s8_app == "-42"); + s8_app.clear(); + s8_app.appendHex(0xFF); + TCHECK(s8_app == "ff"); + s8_app.clear(); + + // Insert + lString8 s8_ins("world"); + s8_ins.insert(0, 2, 'X'); + TCHECK(s8_ins == "XXworld"); + + // Replace + lString8 s8_rep("hello world"); + s8_rep.replace(0, 5, lString8("good")); + TCHECK(s8_rep == "good world"); + lString8 s8_rep2("abcabc"); + s8_rep2.replace('a', 'X'); + TCHECK(s8_rep2 == "XbcXbc"); + + // Erase + lString8 s8_era("0123456789"); + s8_era.erase(5, 5); + TCHECK(s8_era == "01234"); + + // uppercase / lowercase + lString8 s8_case("Hello World"); + s8_case.uppercase(); + TCHECK(s8_case == "HELLO WORLD"); + s8_case.lowercase(); + TCHECK(s8_case == "hello world"); + + CHECKBUF; + + // compare + TCHECK(s8_cstr.compare("hello") == 0); + TCHECK(s8_cstr.compare("world") < 0); + TCHECK(s8_cstr.compare("abc") > 0); + + // pos / rpos + lString8 s8_pos("hello world, hello"); + TCHECK(s8_pos.pos('w') == 6); + TCHECK(s8_pos.pos("world") == 6); + TCHECK(s8_pos.pos('h', 1) == 13); + TCHECK(s8_pos.rpos("hello") == 13); + TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); + + // startsWith / endsWith + lString8 s8_sw("hello.cpp"); + TCHECK(s8_sw.startsWith("hello")); + TCHECK(s8_sw.startsWith(lString8("hello"))); + TCHECK(!s8_sw.startsWith("world")); + TCHECK(s8_sw.endsWith(".cpp")); + + // substr + lString8 s8_sub2("hello world"); + TCHECK(s8_sub2.substr(6) == "world"); + TCHECK(s8_sub2.substr(0, 5) == "hello"); + + // operator[] + TCHECK(s8_sub2[0] == 'h'); + TCHECK(s8_sub2[4] == 'o'); + + // data / c_str + TCHECK(strcmp(s8_sub2.c_str(), "hello world") == 0); + TCHECK(strcmp(s8_sub2.data(), "hello world") == 0); + + // length / size / capacity / empty + TCHECK(s8_sub2.length() == 11); + TCHECK(s8_sub2.size() == 11); + TCHECK(!s8_sub2.empty()); + lString8 s8_empty; + TCHECK(s8_empty.empty()); + TCHECK(!s8_empty); + + // at + TCHECK(s8_sub2.at(0) == 'h'); + TCHECK(s8_sub2.at(10) == 'd'); + + CHECKBUF; + + // atoi + lString8 s8_int("12345"); + TCHECK(s8_int.atoi() == 12345); + lString8 s8_int2("-999"); + TCHECK(s8_int2.atoi() == -999); + lString8 s8_int64("1234567890123"); + TCHECK(s8_int64.atoi64() == 1234567890123LL); + + // swap + lString8 s8_swap1("first"); + lString8 s8_swap2("second"); + s8_swap1.swap(s8_swap2); + TCHECK(s8_swap1 == "second"); + TCHECK(s8_swap2 == "first"); + + // trim + lString8 s8_trim(" spaced "); + s8_trim.trim(); + TCHECK(s8_trim == "spaced"); + + // pack + lString8 s8_pack("hello"); + s8_pack.reserve(1000); + s8_pack.pack(); + + // clear / reset + lString8 s8_clr("something"); + s8_clr.clear(); + TCHECK(s8_clr.empty()); + lString8 s8_rst("something"); + s8_rst.reset(50); + TCHECK(s8_rst.empty()); + TCHECK(s8_rst.capacity() >= 49); + + // resize + lString8 s8_rsz("hi"); + s8_rsz.resize(5, 'x'); + + // lastChar / firstChar + lString8 s8_lf("abc"); + TCHECK(s8_lf.firstChar() == 'a'); + TCHECK(s8_lf.lastChar() == 'c'); + + // getHash + lString8 s8_h1("hello"); + lString8 s8_h2("hello"); + TCHECK(s8_h1.getHash() == s8_h2.getHash()); + + CHECKBUF; + + // operator << + lString8 s8_shift; + s8_shift << 'A'; + TCHECK(s8_shift == "A"); + s8_shift << "BC"; + TCHECK(s8_shift == "ABC"); + s8_shift << lString8("DE"); + TCHECK(s8_shift == "ABCDE"); + s8_shift.clear(); + s8_shift << fmt::decimal(42); + TCHECK(s8_shift == "42"); + s8_shift.clear(); + s8_shift << fmt::hex(255); + TCHECK(s8_shift == "ff"); + + // operator += + lString8 s8_plus; + s8_plus += "hello"; + TCHECK(s8_plus == "hello"); + s8_plus += lString8(" world"); + TCHECK(s8_plus == "hello world"); + s8_plus += '!'; + TCHECK(s8_plus == "hello world!"); + s8_plus += fmt::decimal(99); + TCHECK(s8_plus == "hello world!99"); + + // external operator == / != + lString8 s8_eq("equal"); + lString8 s8_eq2("equal"); + lString8 s8_neq("not_equal"); + TCHECK(s8_eq == s8_eq2); + TCHECK(!(s8_eq != s8_eq2)); + TCHECK(s8_eq != s8_neq); + TCHECK(s8_eq == "equal"); + TCHECK("equal" == s8_eq); + TCHECK(s8_eq != "other"); + TCHECK("other" != s8_eq); + + // external operator + + lString8 s8_add("abc"); + lString8 s8_added = s8_add + "def"; + TCHECK(s8_added == "abcdef"); + s8_added = s8_add + lString8("ghi"); + TCHECK(s8_added == "abcghi"); + s8_added = s8_add + fmt::decimal(123); + TCHECK(s8_added == "abc123"); + s8_added = s8_add + fmt::hex(0xFF); + TCHECK(s8_added == "abcff"); + + // itoa static + lString8 s8_i = lString8::itoa(42); + TCHECK(s8_i == "42"); + s8_i = lString8::itoa(0U); + TCHECK(s8_i == "0"); + s8_i = lString8::itoa(-123LL); + TCHECK(s8_i == "-123"); + + // cs8 + const lString8& cs8_test = cs8("static_string"); + TCHECK(cs8_test == "static_string"); + const lString8& cs8_test2 = cs8("static_string"); + TCHECK(&cs8_test == &cs8_test2); + + CHECKBUF; + + printf("=== MIX lString16 tests ===\n"); + + // lString16 constructors + lString16 s16_default; + TCHECK(s16_default.empty()); + lString16 s16_cstr(u"hello"); + TCHECK(s16_cstr.length() == 5); + lString16 s16_from8("hello"); + TCHECK(s16_from8.length() == 5); + lString16 s16_frag(u"hello world", 5); + TCHECK(s16_frag == u"hello"); + lString16 s16_sub(s16_cstr, 1, 3); + TCHECK(s16_sub == u"ell"); + + // Copy / move + lString16 s16_copy(s16_cstr); + TCHECK(s16_copy == u"hello"); + lString16 s16_move(std::move(s16_copy)); + TCHECK(s16_move == u"hello"); + + // Assignment + lString16 s16_assign; + s16_assign = u"world"; + TCHECK(s16_assign == u"world"); + s16_assign = "ascii"; + TCHECK(s16_assign == u"ascii"); + + // Append + lString16 s16_app; + s16_app.append(u"foo"); + TCHECK(s16_app == u"foo"); + s16_app.append((const lChar8*)"bar"); + TCHECK(s16_app == u"foobar"); + s16_app.append(3, u'!'); + TCHECK(s16_app == u"foobar!!!"); + s16_app.clear(); + + // appendDecimal / appendHex + s16_app.appendDecimal(999); + TCHECK(s16_app == u"999"); + s16_app.clear(); + s16_app.appendHex(0xABC); + TCHECK(s16_app == u"abc"); + + // Insert + lString16 s16_ins(u"world"); + s16_ins.insert(0, 2, u'X'); + s16_ins.clear(); + s16_ins = u"world"; + s16_ins.insert(0, lString16(u"hello ")); + + // compare + TCHECK(s16_cstr.compare(u"hello") == 0); + TCHECK(s16_cstr.compare((const lChar8*)"hello") == 0); + TCHECK(s16_cstr.compare(u"world") < 0); + + // substr + lString16 s16_sub3(u"hello world"); + TCHECK(s16_sub3.substr(6) == u"world"); + TCHECK(s16_sub3.substr(0, 5) == u"hello"); + + // startsWith / endsWith + TCHECK(s16_sub3.startsWith(u"hello")); + TCHECK(s16_sub3.startsWith((const lChar8*)"hello")); + TCHECK(s16_sub3.endsWith(u"world")); + TCHECK(s16_sub3.endsWith((const lChar8*)"world")); + TCHECK(s16_sub3.endsWith(lString16(u"world"))); + + // operator[] + TCHECK(s16_sub3[0] == u'h'); + + // atoi + lString16 s16_int(u"6789"); + int s16_ival = 0; + lInt64 s16_ival64 = 0; + TCHECK(s16_int.atoi() == 6789); + TCHECK(s16_int.atoi(s16_ival) && s16_ival == 6789); + TCHECK(s16_int.atoi(s16_ival64) && s16_ival64 == 6789); + + // getHash + lString16 s16_h1(u"test"); + lString16 s16_h2(u"test"); + TCHECK(s16_h1.getHash() == s16_h2.getHash()); + + // trim / trimNonAlpha + lString16 s16_tr(u" spaced "); + s16_tr.trim(); + TCHECK(s16_tr == u"spaced"); + lString16 s16_tna(u"!!!hello!!!"); + s16_tna.trimNonAlpha(); + TCHECK(s16_tna == u"hello"); + + // swap + lString16 s16_sw1(u"first"); + lString16 s16_sw2(u"second"); + s16_sw1.swap(s16_sw2); + TCHECK(s16_sw1 == u"second"); + TCHECK(s16_sw2 == u"first"); + + // clear / reset + lString16 s16_clr(u"something"); + s16_clr.clear(); + TCHECK(s16_clr.empty()); + lString16 s16_rst2(u"something"); + s16_rst2.reset(50); + TCHECK(s16_rst2.empty()); + + // limit + lString16 s16_lim(u"hello world"); + s16_lim.limit(5); + TCHECK(s16_lim == u"hello"); + + // pack + lString16 s16_pk(u"test"); + s16_pk.reserve(500); + + // operator << + lString16 s16_sh; + s16_sh << u'A'; + TCHECK(s16_sh == u"A"); + s16_sh << u"BC"; + TCHECK(s16_sh == u"ABC"); + s16_sh << fmt::decimal(42); + TCHECK(s16_sh == u"ABC42"); + s16_sh.clear(); + s16_sh << fmt::hex(255); + TCHECK(s16_sh == u"ff"); + + // operator += + lString16 s16_pe; + s16_pe += u"hello"; + TCHECK(s16_pe == u"hello"); + s16_pe += lString16(u" world"); + TCHECK(s16_pe == u"hello world"); + + // itoa + lString16 s16_i16 = lString16::itoa(42); + TCHECK(s16_i16 == u"42"); + s16_i16 = lString16::itoa(0U); + TCHECK(s16_i16 == u"0"); + + // external operator == / != / + + lString16 s16_eq1(u"equal"); + lString16 s16_eq2(u"equal"); + TCHECK(s16_eq1 == s16_eq2); + TCHECK(s16_eq1 == u"equal"); + TCHECK(u"equal" == s16_eq1); + TCHECK(s16_eq1 != u"other"); + lString16 s16_added16 = s16_eq1 + u"_suffix"; + TCHECK(s16_added16 == u"equal_suffix"); + + CHECKBUF; +} + void testStrings8() { printf("=== lString8 tests ===\n"); @@ -701,6 +1119,7 @@ void testStringsMixed() { void testStrings() { + testStrings8_16(); testStrings8(); testStrings16(); testStrings32(); diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index a30969c364..8de35a8ecb 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -382,7 +382,7 @@ void ShutdownCREngine() #endif } -bool testsOnly = false; +bool testsOnly = true; void runLibraryTestSuite() { printf("CR3 Library Tests\n"); From 001ec9a9a0c3ba26115042f5cc0451eebfc9268c Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Fri, 24 Jul 2026 14:17:17 +0100 Subject: [PATCH 08/36] initial version of new string library - in test --- crengine/include/lvstring.h | 30 +---- crengine/src/lvstring.cpp | 95 ++++++++------ tests/CMakeLists.txt | 2 + tests/src/lvstring2.cpp | 157 +++++++++++++++++++++++ tests/src/lvstring2.h | 242 ++++++++++++++++++++++++++++++++++++ tests/src/stringtest.cpp | 84 ++++++++++--- tests/src/testmain.cpp | 2 +- 7 files changed, 529 insertions(+), 83 deletions(-) create mode 100644 tests/src/lvstring2.cpp create mode 100644 tests/src/lvstring2.h diff --git a/crengine/include/lvstring.h b/crengine/include/lvstring.h index 9f4d556a4f..2726fb818e 100644 --- a/crengine/include/lvstring.h +++ b/crengine/include/lvstring.h @@ -317,8 +317,8 @@ class lString8 public: // typedefs for STL compatibility typedef lChar8 value_type; ///< character type - typedef int size_type; ///< size type - typedef int difference_type; ///< difference type + typedef lUInt32 size_type; ///< size type + typedef lInt32 difference_type; ///< difference type typedef value_type * pointer; ///< pointer to char type typedef value_type & reference; ///< reference to char type typedef const value_type * const_pointer; ///< pointer to const char type @@ -357,12 +357,6 @@ class lString8 } inline void release() { if (!pchunk) return; -#if LDOM_USE_OWN_MEM_MAN==1 - if (ls_storage_is_destroyed()) { - pchunk = nullptr; - return; - } -#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); @@ -624,8 +618,8 @@ class lString16 public: // typedefs for STL compatibility typedef lChar16 value_type; - typedef int size_type; - typedef int difference_type; + typedef lUInt32 size_type; + typedef lInt32 difference_type; typedef value_type * pointer; typedef value_type & reference; typedef const value_type * const_pointer; @@ -650,12 +644,6 @@ class lString16 } inline void release() { if (!pchunk) return; -#if LDOM_USE_OWN_MEM_MAN==1 - if (ls_storage_is_destroyed()) { - pchunk = nullptr; - return; - } -#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); @@ -893,8 +881,8 @@ class lString32 public: // typedefs for STL compatibility typedef lChar32 value_type; - typedef int size_type; - typedef int difference_type; + typedef lUInt32 size_type; + typedef lInt32 difference_type; typedef value_type * pointer; typedef value_type & reference; typedef const value_type * const_pointer; @@ -919,12 +907,6 @@ class lString32 } inline void release() { if (!pchunk) return; -#if LDOM_USE_OWN_MEM_MAN==1 - if (ls_storage_is_destroyed()) { - pchunk = nullptr; - return; - } -#endif #ifdef USE_ATOMIC_REFCOUNT if (pchunk->refCount.fetch_sub(1) <= 1) free(); diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index ee81c53078..5cbea93af4 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -339,13 +339,10 @@ void free_ls_storage() { if (!slices_initialized) return; - for (int i=0; ibuf32); } #if (LDOM_USE_OWN_MEM_MAN == 1) - - for (int i=slices_count-1; i>=0; --i) - { - if (slices[i]->free_chunk32(pchunk)) - return; + if (!ls_storage_is_destroyed()) { + for (int i=slices_count-1; i>=0; --i) + { + if (slices[i]->free_chunk32(pchunk)) + return; + } + crFatalError(); // wrong pointer!!! } - crFatalError(); // wrong pointer!!! #else ::free(pchunk); #endif pchunk = nullptr; } -void lString32::alloc(int sz) +void lString32::alloc(size_type sz) { #if (LDOM_USE_OWN_MEM_MAN == 1) pchunk = lstring_chunk_t::alloc(); @@ -1417,8 +1415,10 @@ lString32 & lString32::insert(size_type p0, const lString32 & str) p0 = pchunk->len; int count = str.length(); reserve( pchunk->len+count ); - for (size_type i=pchunk->len-1; i>=p0; i--) - pchunk->buf32[i+count] = pchunk->buf32[i]; + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf32[i] = pchunk->buf32[i - count]; + // for (size_type i=pchunk->len-1; i>=p0; i--) + // pchunk->buf32[i+count] = pchunk->buf32[i]; _lStr_memcpy(pchunk->buf32 + p0, str.c_str(), count); pchunk->len += count; pchunk->buf32[pchunk->len] = 0; @@ -1783,19 +1783,21 @@ void lString16::free() ::free(pchunk->buf16); } #if (LDOM_USE_OWN_MEM_MAN == 1) - for (int i=slices_count-1; i>=0; --i) - { - if (slices[i]->free_chunk16(pchunk)) - return; + if (!ls_storage_is_destroyed()) { + for (int i=slices_count-1; i>=0; --i) + { + if (slices[i]->free_chunk16(pchunk)) + return; + } + crFatalError(); // wrong pointer!!! } - crFatalError(); // wrong pointer!!! #else ::free(pchunk); #endif pchunk = nullptr; } -void lString16::alloc(int sz) +void lString16::alloc(size_type sz) { #if (LDOM_USE_OWN_MEM_MAN == 1) pchunk = lstring_chunk_t::alloc(); @@ -2226,8 +2228,10 @@ lString16 & lString16::insert(size_type p0, const value_type * str) p0 = pchunk->len; int count = lStr_len(str); reserve( pchunk->len+count ); - for (size_type i=pchunk->len+count; i>p0; i--) - pchunk->buf16[i] = pchunk->buf16[i-1]; + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf16[i] = pchunk->buf16[i - count]; + //for (size_type i=pchunk->len+count; i>p0; i--) + // pchunk->buf16[i] = pchunk->buf16[i-1]; _lStr_memcpy(pchunk->buf16 + p0, str, count); pchunk->len += count; pchunk->buf16[pchunk->len] = 0; @@ -2239,8 +2243,10 @@ lString16 & lString16::insert(size_type p0, const value_type * str, size_type co if (p0>pchunk->len) p0 = pchunk->len; reserve( pchunk->len+count ); - for (size_type i=pchunk->len+count; i>p0; i--) - pchunk->buf16[i] = pchunk->buf16[i-1]; + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf16[i] = pchunk->buf16[i - count]; + // for (size_type i=pchunk->len+count; i>p0; i--) + // pchunk->buf16[i] = pchunk->buf16[i-1]; _lStr_memcpy(pchunk->buf16 + p0, str, count); pchunk->len += count; pchunk->buf16[pchunk->len] = 0; @@ -2252,8 +2258,10 @@ lString16 & lString16::insert(size_type p0, size_type count, value_type ch) if (p0>pchunk->len) p0 = pchunk->len; reserve( pchunk->len+count ); - for (size_type i=pchunk->len+count; i>p0; i--) - pchunk->buf16[i] = pchunk->buf16[i-1]; + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf16[i] = pchunk->buf16[i - count]; + // for (size_type i=pchunk->len+count; i>p0; i--) + // pchunk->buf16[i] = pchunk->buf16[i-1]; _lStr_memset(pchunk->buf16+p0, ch, count); pchunk->len += count; pchunk->buf16[pchunk->len] = 0; @@ -2266,8 +2274,10 @@ lString16 & lString16::insert(size_type p0, const lString16 & str) p0 = pchunk->len; int count = str.length(); reserve( pchunk->len+count ); - for (size_type i=pchunk->len+count; i>p0; i--) - pchunk->buf16[i] = pchunk->buf16[i-1]; + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf16[i] = pchunk->buf16[i - count]; + // for (size_type i=pchunk->len+count; i>p0; i--) + // pchunk->buf16[i] = pchunk->buf16[i-1]; _lStr_memcpy(pchunk->buf16 + p0, str.c_str(), count); pchunk->len += count; pchunk->buf16[pchunk->len] = 0; @@ -2479,19 +2489,21 @@ void lString8::free() ::free(pchunk->buf8); } #if (LDOM_USE_OWN_MEM_MAN == 1) - for (int i=slices_count-1; i>=0; --i) - { - if (slices[i]->free_chunk(pchunk)) - return; + if (!ls_storage_is_destroyed()) { + for (int i=slices_count-1; i>=0; --i) + { + if (slices[i]->free_chunk(pchunk)) + return; + } + crFatalError(); // wrong pointer!!! } - crFatalError(); // wrong pointer!!! #else ::free(pchunk); #endif pchunk = nullptr; } -void lString8::alloc(int sz) +void lString8::alloc(size_type sz) { #if (LDOM_USE_OWN_MEM_MAN == 1) pchunk = lstring_chunk_t::alloc(); @@ -2941,11 +2953,18 @@ lString8 & lString8::append(size_type count, lChar8 ch) lString8 & lString8::insert(size_type p0, size_type count, lChar8 ch) { - if (p0>pchunk->len) + if (count == 0) { + return *this; + } + if (p0 > pchunk->len) p0 = pchunk->len; - reserve( pchunk->len+count ); - for (size_type i=pchunk->len-1; i>=p0; i--) - pchunk->buf8[i+count] = pchunk->buf8[i]; + reserve( pchunk->len + count ); + // mindst + // p0 + count + for (size_type i=pchunk->len + count - 1; i>= p0 + count ; i--) + pchunk->buf8[i] = pchunk->buf8[i - count]; + // for (size_type i=pchunk->len-1; i>=p0; i--) + // pchunk->buf8[i+count] = pchunk->buf8[i]; //_lStr_memset(pchunk->buf8+p0, ch, count); memset(pchunk->buf8+p0, ch, count); pchunk->len += count; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bc2cc5fd83..ca7187c1b0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,8 @@ SET (CR3TEST_SOURCES src/testmain.cpp src/stringtest.cpp + src/lvstring2.h + src/lvstring2.cpp ) IF(${CMAKE_BUILD_TYPE} STREQUAL Debug) diff --git a/tests/src/lvstring2.cpp b/tests/src/lvstring2.cpp new file mode 100644 index 0000000000..6d7fe8de94 --- /dev/null +++ b/tests/src/lvstring2.cpp @@ -0,0 +1,157 @@ +#include "lvstring2.h" +#include + +using namespace lv; + +namespace lv { + +static int test_errors = 0; +#define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) + +void test_lstring2_chunks() { + + // struct size checks + printf("sizeof(lstring8_chunk_t) = %llu\n", sizeof(lstring8_chunk_t)); + printf("sizeof(lstring16_chunk_t) = %llu\n", sizeof(lstring16_chunk_t)); + printf("sizeof(lstring32_chunk_t) = %llu\n", sizeof(lstring32_chunk_t)); + TCHECK(sizeof(lstring8_chunk_t) == sizeof(lstring16_chunk_t)); + TCHECK(sizeof(lstring16_chunk_t) == sizeof(lstring32_chunk_t)); + TCHECK(sizeof(lstring8_chunk_t) == 12); + + //std::atomic_int counter{0}; + //counter.fetch_add(); + + TCHECK(lstring8_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring8_chunk_t::min_size == 3); + TCHECK(lstring8_chunk_t::size_align == 16); + + TCHECK(lstring16_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring16_chunk_t::min_size == 1); + TCHECK(lstring16_chunk_t::size_align == 8); + + TCHECK(lstring32_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring32_chunk_t::min_size == 0); + TCHECK(lstring32_chunk_t::size_align == 4); + + TCHECK(lstring8_chunk_t::alignSize(0) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(1) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(2) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(3) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(4) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(5) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(6) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(7) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(11) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(12) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(13) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(15) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16+3) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16+4) == 16+16+3); + + lstring8_chunk_t * chunk8_1 = lstring8_chunk_t::alloc(25); + TCHECK(chunk8_1->size == 32+3); + TCHECK(chunk8_1->len == 0); + TCHECK(chunk8_1->refCount == 1); + TCHECK(chunk8_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk8_1->buf[i] = 'a'; + chunk8_1->buf[i + 1] = 0; + } + chunk8_1->len = 25; + + chunk8_1->testAddRef(); + chunk8_1->testReleaseRef(); + + lstring8_chunk_t::free(chunk8_1); + + lstring8_chunk_t * chunk8_2 = lstring8_chunk_t::alloc("abcdefg", 4, 10); + TCHECK(chunk8_2->size == 16+3); + TCHECK(chunk8_2->len == 4); + TCHECK(chunk8_2->refCount == 1); + TCHECK(chunk8_2->buf[0] == 'a' && chunk8_2->buf[1] == 'b' && chunk8_2->buf[2] == 'c' && chunk8_2->buf[3] == 'd' && chunk8_2->buf[4] == 0); + lstring8_chunk_t::free(chunk8_2); + + lstring16_chunk_t * chunk16_1 = lstring16_chunk_t::alloc(25); + TCHECK(chunk16_1->size == 24+1); + TCHECK(chunk16_1->len == 0); + TCHECK(chunk16_1->refCount == 1); + TCHECK(chunk16_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk16_1->buf[i] = 'b'; + chunk16_1->buf[i + 1] = 0; + } + chunk16_1->len = 25; + lstring16_chunk_t::free(chunk16_1); + + lstring16_chunk_t * chunk16_2 = lstring16_chunk_t::alloc(u"bcdefghi", 4, 10); + TCHECK(chunk16_2->size == 16+1); + TCHECK(chunk16_2->len == 4); + TCHECK(chunk16_2->refCount == 1); + TCHECK(chunk16_2->buf[0] == u'b' && chunk16_2->buf[1] == u'c' && chunk16_2->buf[2] == u'd' && chunk16_2->buf[3] == u'e' && chunk16_2->buf[4] == 0); + intrusive_ptr_add_ref(chunk16_2); + TCHECK(chunk16_2->getRefCount() == 2); + intrusive_ptr_release(chunk16_2); + TCHECK(chunk16_2->getRefCount() == 1); + intrusive_ptr_release(chunk16_2); + // released automatically + //lstring16_chunk_t::free(chunk16_2); + + lstring32_chunk_t * chunk32_1 = lstring32_chunk_t::alloc(25); + TCHECK(chunk32_1->size == 28); + TCHECK(chunk32_1->len == 0); + TCHECK(chunk32_1->refCount == 1); + TCHECK(chunk32_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk32_1->buf[i] = 'b'; + chunk32_1->buf[i + 1] = 0; + } + chunk32_1->len = 25; + //lstring32_chunk_t::free(chunk32_1); + intrusive_ptr_release(chunk32_1); + + lstring32_chunk_t * chunk32_2 = lstring32_chunk_t::alloc(U"cdefghijkl", 4, 10); + TCHECK(chunk32_2->size == 12); + TCHECK(chunk32_2->len == 4); + TCHECK(chunk32_2->refCount == 1); + TCHECK(chunk32_2->buf[0] == U'c' && chunk32_2->buf[1] == U'd' && chunk32_2->buf[2] == U'e' && chunk32_2->buf[3] == U'f' && chunk32_2->buf[4] == 0); + lstring32_chunk_t::free(chunk32_2); + +} + +void test_lstring8() { + printf("running test_lstring8()\n"); + lString8 s {}; + TCHECK(s.capacity() == 0); + TCHECK(s.size() == 0); + TCHECK(s.length() == 0); + TCHECK(s.empty()); + lString8 s2 {"qwerty"}; + TCHECK(s2.capacity() >= 6); + TCHECK(s2.size() >= 6); + TCHECK(s2.length() == 6); + TCHECK(!s2.empty()); + lString8 s3 {"qwerty", 3, 100}; + TCHECK(s3.capacity() >= 100); + TCHECK(s3.size() >= 100); + TCHECK(s3.length() == 3); + printf("s3.capacity == %d\n", s3.capacity()); + TCHECK(!s3.empty()); + lString8 s4 = lString8("move"); + s4 = lString8("move consturctor assignment"); +} + +void test_lstring2() { + printf("New strings library tests\n"); + test_lstring2_chunks(); + test_lstring8(); + + if (test_errors == 0) { + printf("New strings library tests completed successfully\n"); + } else { + printf("***\nNEW STRINGS LIBRARY TESTS COMPLETED WITH %d ERRORS\n***\n", test_errors); + } + printf("New strings library completed\n"); +} + +} diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h new file mode 100644 index 0000000000..24f6cd66a8 --- /dev/null +++ b/tests/src/lvstring2.h @@ -0,0 +1,242 @@ +#ifndef LVSTRING2_H +#define LVSTRING2_H + +#include "lvtypes.h" +#include +#include +#include +#include + +namespace lv { + + +// Helper functions + +/// arbitrary char type strlen, supports nullptr arg +template +inline size_type str_len(const char_type * s) { + if (s == nullptr) { + return 0; + } + size_type i = 0; + while (s[i] != 0) { + i++; + } + return i; +} + +// String data buffer with ref count + +// forward declaration of string class +template +class string; + +template +struct lstring_chunk_t { + + // friend class lString8; + // friend class lString32; + // friend struct lstring_chunk_slice_t; + friend class string; + + friend void test_lstring2_chunks(); + /// chunk allocation alignment in bytes + static constexpr size_type alloc_align_bytes = 16; + /// minimum reserved characters count to have allocated size (alloc_align_bytes) bytes + static constexpr size_type min_size = static_cast((alloc_align_bytes - sizeof(lstring_chunk_t)) / sizeof(char_type)) - 1; + /// character count increment to keep block size aligned + static constexpr size_type size_align = static_cast(alloc_align_bytes / sizeof(char_type)); + +public: + + //const char_type * data() const { return &buf[0]; } + + /// align reserved size in characters of block to have byte alignment alloc_align_bytes, size does not include trailing 0 character + inline static size_type alignSize(size_type sz) noexcept { + if (sz <= min_size) + return min_size; + return min_size + (((sz - min_size) + size_align - 1) & (0 - size_align)); + } + + /// get reference counter value + size_type getRefCount() const noexcept { + return refCount; + } + + /// 1. Hook for incrementing the counter (compatible with boost::intrusive_ptr) + friend void intrusive_ptr_add_ref(const lstring_chunk_t* p) noexcept { + if constexpr (std::is_same_v) { + // simple refcount + p->refCount++; + } else { + p->refCount.fetch_add(1, std::memory_order_relaxed); + } + } + + void testAddRef() { + refCount.fetch_add(1, std::memory_order_relaxed); + } + + void testReleaseRef() { + refCount.fetch_sub(1, std::memory_order_acq_rel); + } + + /// 2. Hook for decrementing and deleting the object (compatible with boost::intrusive_ptr) + friend void intrusive_ptr_release(const lstring_chunk_t* p) noexcept { + if constexpr (std::is_same_v) { + if ((p->refCount--) == 1) { + lstring_chunk_t::free(p); + } + } else { + if (p->refCount.fetch_sub(1, std::memory_order_acq_rel) == 1) { + lstring_chunk_t::free(p); + } + } + } + + /// chunk allocation function: create string buffer with reserved sz characters + zero termination char, ref counter 1 + static lstring_chunk_t * alloc(size_type sz) noexcept { + //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); + //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); + sz = alignSize(sz); + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + res->size = sz; + res->len = 0; + res->buf[0] = 0; + res->refCount = 1; + return res; + } + + /// chunk allocation function: create string buffer initialized with count chars from string s with reserved sz characters + zero termination char, ref counter 1 + static lstring_chunk_t * alloc(const char_type * s, size_type count, size_type sz = 0) noexcept { + if (sz < count) + sz = count; + sz = alignSize(sz); + //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + std::memcpy(res->buf, s, count * sizeof(char_type)); + res->buf[count] = 0; + res->size = sz; + res->len = count; + res->refCount = 1; + return res; + } + + /// free chunk memory + static void free( const lstring_chunk_t * pChunk ) noexcept { + ::free(const_cast(pChunk)); + } + +private: + lstring_chunk_t() : size(1), len(0) + { + refCount = 1; + } + ~lstring_chunk_t() {}; + // layout for 4-byte size_type and refcounew + /// 0: size (max capacity for chars) less 1 + size_type size; // 0 for free chunk + /// 4: length (number of chars in array) + size_type len; // count of chars in string + /// 8: reference counter + mutable refcounter_type refCount; // reference counter + /// 12: string characters buffer of variable size + char_type buf[0]; // z-string +}; + +template +class string { + using chunk_t = lstring_chunk_t; + +public: + string() noexcept = default; + explicit string(const char_type* s, size_type count, size_type reserved) noexcept { + if (count == 0 || s == nullptr || s[0] == 0) { + pchunk = nullptr; + } else { + pchunk = chunk_t::alloc(s, count, reserved); + } + } + explicit string(const char_type* s) noexcept { + pchunk = chunk_t::alloc(s, str_len(s), 0); + } + /// copy constructor + string(const string&s) noexcept { + pchunk = s.pchunk; + intrusive_ptr_add_ref(pchunk); + } + /// move constructor + string(string&&s ) noexcept { + pchunk = s.pchunk; + s.pchunk = nullptr; + } + ~string() noexcept { + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + } + } + /// move assignment + string& operator = (string&& s) noexcept { + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + return *this; + } + /// copy assignment + string& operator = (const string& s) noexcept { + if (&s == this) { + // safe self assignment: do nothing + return; + } + if (pchunk != nullptr) { + // ignore self-assignment + if (pchunk != s.pchunk) { + intrusive_ptr_release(pchunk); + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } + } else { + // this string is null + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } + return *this; + } + /// returns true if string is empty + bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } + /// returns character count + size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } + /// returns buffer size + size_type size() const noexcept { return capacity(); } + /// changes buffer size + //void resize(size_type count = 0, value_type e = 0); + /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) + size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } + +private: + chunk_t * pchunk {nullptr}; +}; + +typedef lstring_chunk_t lstring8_chunk_t; +typedef lstring_chunk_t lstring16_chunk_t; +typedef lstring_chunk_t lstring32_chunk_t; + +typedef string lString8; +typedef string lString16; +typedef string lString32; + +void test_lstring2(); + +} + + +#endif // LVSTRING2_H diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp index 24bbc8d89e..163c2ba85a 100644 --- a/tests/src/stringtest.cpp +++ b/tests/src/stringtest.cpp @@ -4,12 +4,20 @@ #include "../crengine/include/lvstring8collection.h" #include "../crengine/include/lvstring32collection.h" #include "stringtest.h" - -static int test_errors = 0; -#define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) +#include "lvstring2.h" #define CHECKBUF_(file, line) check_ls_storage(file ":" #line) #define CHECKBUF CHECKBUF_(__FILE__, __LINE__) +static int test_errors = 0; +#define TCHECK(cond) do { CHECKBUF; if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) + + +class TempStringHolder8 { + public: + lString8 data; + TempStringHolder8(const lString8 & s) : data(s) { } + ~TempStringHolder8() = default; +}; void testStrings8_16() { printf("=== MIX lString8 tests ===\n"); @@ -25,6 +33,24 @@ void testStrings8_16() { TCHECK(s8_cstr.length() == 5); TCHECK(s8_cstr == "hello"); + TempStringHolder8 s8_str_hld1(s8_cstr); + TempStringHolder8 s8_str_hld2(s8_cstr + "bla bla"); + TempStringHolder8 s8_str_hld3(s8_cstr + "bla bla" + s8_str_hld1.data); + + { + lString8 face((const char*)"serifExtraCondensed"); + lString8 style8((const char*)"italic"); + style8.lowercase(); + if (style8.pos("extracondensed") >= 0) + face << " ExtraCondensed"; + else if (style8.pos("semicondensed") >= 0) + face << " SemiCondensed"; + else if (style8.pos("condensed") >= 0) + face << " Condensed"; + TempStringHolder8 fontFace(face); + } + + lString8 s8_fragment("hello world", 5); TCHECK(s8_fragment == "hello"); @@ -1117,7 +1143,14 @@ void testStringsMixed() { } +bool test_newstring_only = true; + void testStrings() { + lv::test_lstring2(); + + if (test_newstring_only) { + return; + } testStrings8_16(); testStrings8(); @@ -1137,60 +1170,71 @@ void testStringCollections() { lString8Collection list; { lString8 s {"test_s_1"}; - assert(s == "test_s_1"); + TCHECK(s == "test_s_1"); list.add(s); - assert(s == "test_s_1"); + TCHECK(s == "test_s_1"); } { lString8 s {"test_s_2"}; - assert(s == "test_s_2"); + s << "_less"; + TCHECK(s == "test_s_2"); list.add(s); - assert(s == "test_s_2"); + TCHECK(s == "test_s_2"); + TCHECK(s.pos("_s") != 0); + s << "_more"; + s << "_more"; } } { lString8Collection list; { lString8 s {"test_s_3"}; - assert(s == "test_s_3"); + TCHECK(s == "test_s_3"); list.add(s); - assert(s == "test_s_3"); + TCHECK(s == "test_s_3"); + TCHECK(s.pos("_s") != 0); } { lString8 s {"test_s_4"}; - assert(s == "test_s_4"); + TCHECK(s == "test_s_4"); list.add(s); - assert(s == "test_s_4"); + TCHECK(s == "test_s_4"); + TCHECK(s.pos("_s") != 0); } } { lString32Collection list; { lString32 s {U"test_s_5"}; - assert(s == "test_s_5"); + TCHECK(s == "test_s_5"); list.add(s); - assert(s == "test_s_5"); - } - { - lString32 s {U"test_s_5"}; - assert(s == "test_s_5"); - list.add(s); - assert(s == "test_s_5"); + TCHECK(s == "test_s_5"); + TCHECK(s.pos("_s") != 0); } + lString32 s {U"test_s_5"}; + TCHECK(s == "test_s_5"); + list.add(s); + TCHECK(s.pos("_s") != 0); + TCHECK(s == "test_s_5"); } for (int sz = 1; sz < 1000; sz++) { lString8Collection list; for (int i = 0; i < sz; i++) { lString8 s {"String8"}; + lString8 s2 {"tail"}; + TCHECK(s.pos("ing") != 0); list.add(s); + list.add(s + s2); + list.add(s + "tails"); } } for (int sz = 1; sz < 1000; sz++) { lString32Collection list; for (int i = 0; i < sz; i++) { lString32 s {U"String32"}; - list.add(s); + TCHECK(s.pos("ing") != 0); + list.add(s + s); } } } diff --git a/tests/src/testmain.cpp b/tests/src/testmain.cpp index 8de35a8ecb..1e47ba297e 100644 --- a/tests/src/testmain.cpp +++ b/tests/src/testmain.cpp @@ -385,7 +385,7 @@ void ShutdownCREngine() bool testsOnly = true; void runLibraryTestSuite() { - printf("CR3 Library Tests\n"); + printf("String Tests\n"); testStrings(); testStringCollections(); printf("CR3 Library Tests\n"); From a676dfd91135a7277c5a69337fd14b477c59b2db Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Fri, 24 Jul 2026 15:11:15 +0100 Subject: [PATCH 09/36] extending new strings library --- tests/src/lvstring2.cpp | 23 +++++++++++++ tests/src/lvstring2.h | 74 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/tests/src/lvstring2.cpp b/tests/src/lvstring2.cpp index 6d7fe8de94..6a559ad373 100644 --- a/tests/src/lvstring2.cpp +++ b/tests/src/lvstring2.cpp @@ -5,6 +5,17 @@ using namespace lv; namespace lv { +lStringStats ls_alloc_stats; +void lStringStats::dump(const char * msg) { + printf("stats[%s]:" + " \talloc=%d \tfree=%d \tactive=%d" + " \tcopyc=%d \tmovec=%d \tcopyass=%d \tmoveass=%d" + "\n", msg, + allocCount, freeCount, allocCount-freeCount, + copyConstr, moveConstr, copyAssign, moveAssign + ); +} + static int test_errors = 0; #define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) @@ -121,16 +132,19 @@ void test_lstring2_chunks() { void test_lstring8() { printf("running test_lstring8()\n"); + ls_alloc_stats.dump(); lString8 s {}; TCHECK(s.capacity() == 0); TCHECK(s.size() == 0); TCHECK(s.length() == 0); TCHECK(s.empty()); + ls_alloc_stats.dump(); lString8 s2 {"qwerty"}; TCHECK(s2.capacity() >= 6); TCHECK(s2.size() >= 6); TCHECK(s2.length() == 6); TCHECK(!s2.empty()); + ls_alloc_stats.dump(); lString8 s3 {"qwerty", 3, 100}; TCHECK(s3.capacity() >= 100); TCHECK(s3.size() >= 100); @@ -139,12 +153,21 @@ void test_lstring8() { TCHECK(!s3.empty()); lString8 s4 = lString8("move"); s4 = lString8("move consturctor assignment"); + // copy assignment + s2 = s4; + ls_alloc_stats.dump("before clear"); + s2.clear(); + ls_alloc_stats.dump("after clear"); + s4.reset(500); + ls_alloc_stats.dump("after reset"); } void test_lstring2() { printf("New strings library tests\n"); test_lstring2_chunks(); + ls_alloc_stats.dump("before test_lstring8()"); test_lstring8(); + ls_alloc_stats.dump("after test_lstring8()"); if (test_errors == 0) { printf("New strings library tests completed successfully\n"); diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 24f6cd66a8..253a04cf25 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -9,6 +9,22 @@ namespace lv { +struct lStringStats { + int allocCount {0}; + int freeCount {0}; + int copyConstr {0}; + int moveConstr {0}; + int copyAssign {0}; + int moveAssign {0}; + void dump(const char * msg = ""); +}; +extern lStringStats ls_alloc_stats; +#define LS_COUNT_ALLOC ls_alloc_stats.allocCount++ +#define LS_COUNT_FREE ls_alloc_stats.freeCount++ +#define LS_COUNT_COPY_CONSTR ls_alloc_stats.copyConstr++ +#define LS_COUNT_MOVE_CONSTR ls_alloc_stats.moveConstr++ +#define LS_COUNT_COPY_ASSIGN ls_alloc_stats.copyAssign++ +#define LS_COUNT_MOVE_ASSIGN ls_alloc_stats.moveAssign++ // Helper functions @@ -96,6 +112,7 @@ struct lstring_chunk_t { /// chunk allocation function: create string buffer with reserved sz characters + zero termination char, ref counter 1 static lstring_chunk_t * alloc(size_type sz) noexcept { + LS_COUNT_ALLOC; //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); sz = alignSize(sz); @@ -104,12 +121,14 @@ struct lstring_chunk_t { res->size = sz; res->len = 0; res->buf[0] = 0; + res->buf[sz] = 0; res->refCount = 1; return res; } /// chunk allocation function: create string buffer initialized with count chars from string s with reserved sz characters + zero termination char, ref counter 1 static lstring_chunk_t * alloc(const char_type * s, size_type count, size_type sz = 0) noexcept { + LS_COUNT_ALLOC; if (sz < count) sz = count; sz = alignSize(sz); @@ -118,6 +137,7 @@ struct lstring_chunk_t { lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); std::memcpy(res->buf, s, count * sizeof(char_type)); res->buf[count] = 0; + res->buf[sz] = 0; res->size = sz; res->len = count; res->refCount = 1; @@ -126,6 +146,7 @@ struct lstring_chunk_t { /// free chunk memory static void free( const lstring_chunk_t * pChunk ) noexcept { + LS_COUNT_FREE; ::free(const_cast(pChunk)); } @@ -164,11 +185,13 @@ class string { } /// copy constructor string(const string&s) noexcept { + LS_COUNT_COPY_CONSTR; pchunk = s.pchunk; intrusive_ptr_add_ref(pchunk); } /// move constructor string(string&&s ) noexcept { + LS_COUNT_MOVE_CONSTR; pchunk = s.pchunk; s.pchunk = nullptr; } @@ -179,6 +202,7 @@ class string { } /// move assignment string& operator = (string&& s) noexcept { + LS_COUNT_MOVE_ASSIGN; if (pchunk != nullptr) { intrusive_ptr_release(pchunk); } @@ -188,9 +212,10 @@ class string { } /// copy assignment string& operator = (const string& s) noexcept { + LS_COUNT_COPY_ASSIGN; if (&s == this) { // safe self assignment: do nothing - return; + return *this; } if (pchunk != nullptr) { // ignore self-assignment @@ -211,6 +236,53 @@ class string { } return *this; } + + /// sets value to null string, freeing buffer + void clear() noexcept { + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + pchunk = nullptr; + } + } + + /// resets length to zero, preparing to modification with reserved size + void reset(size_type size) noexcept { + if (pchunk == nullptr) { + // buffer is allocated and has capacity at least size + pchunk = chunk_t::alloc(size); + } else { + // this string is non-empty + if (pchunk->getRefCount() == 1 && pchunk->size >= size) { + // this is already our own string with enough capacity -- just reset length + pchunk->len = 0; + } else { + // this string has not enough capacity for size, clear and alloc new + intrusive_ptr_release(pchunk); + pchunk = chunk_t::alloc(size); + } + } + } + + /// resize and make editable, presercing current content, preparing to modification with reserved size + void resize(size_type size, char_type e = 0) noexcept { + if (pchunk == nullptr) { + // buffer is allocated and has capacity at least size + pchunk = chunk_t::alloc(size); + } else { + // this string is non-empty + if (pchunk->getRefCount() == 1 && pchunk->size >= size) { + // this is already our own string with enough capacity -- do nothing + } else { + // create new object and copy existing data + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, size); + tmp->buf[tmp->len] = 0; // zterm + // release old chunk and assign new one + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + } + /// returns true if string is empty bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } /// returns character count From f8e2822204dc79f839178dd126a03f7592a1e16b Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Fri, 24 Jul 2026 16:44:45 +0100 Subject: [PATCH 10/36] new strings, add comparision --- tests/src/lvstring2.cpp | 16 ++++ tests/src/lvstring2.h | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) diff --git a/tests/src/lvstring2.cpp b/tests/src/lvstring2.cpp index 6a559ad373..c4e86a895e 100644 --- a/tests/src/lvstring2.cpp +++ b/tests/src/lvstring2.cpp @@ -160,6 +160,22 @@ void test_lstring8() { ls_alloc_stats.dump("after clear"); s4.reset(500); ls_alloc_stats.dump("after reset"); + lString8 s5 {"abc"}; + lString8 s6 {"abc"}; + lString8 s7 {"Abc"}; + ls_alloc_stats.dump("before compare"); + TCHECK(s5 == s6); + TCHECK(s5 <= s6); + TCHECK(s5 >= s6); + TCHECK(s5 > s7); + TCHECK(s7 < s5); + TCHECK(s5 >= s7); + TCHECK(s7 <= s5); + TCHECK(s5 != s7); + TCHECK(s5 == "abc"); + TCHECK(s5 > "ab"); + TCHECK(s5 < "abcd"); + ls_alloc_stats.dump("after compare"); } void test_lstring2() { diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 253a04cf25..f2e4e22546 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -41,6 +41,104 @@ inline size_type str_len(const char_type * s) { return i; } +/// arbitrary char type strcmp, supports empty strings +template +inline int str_cmp(const char_type * s1, size_type sz1, const char_type * s2, size_type sz2) { + // support case when one of strings or both are empty + if (sz1 == 0) { + return sz2 > 0 ? -1 : 0; + } else if (sz2 == 0) { + return 1; + } + for (size_type i = 0; ; i++) { + if (i >= sz1) { + // equal or s2 is bigger + return i < sz2 ? -1 : 0; + } else if (i >= sz2) { + // less + return 1; + } + if (s1[i] < s2[i]) { + return -1; + } + if (s1[i] > s2[i]) { + return 1; + } + // char [i] is matching, continue + } +} + +/// arbitrary char type strcmp, both args are non-empty +template +inline int str_cmp_nonempty(const char_type * s1, size_type sz1, const char_type * s2, size_type sz2) { + // we are sure that sz1>0 && sz2>0 + for (size_type i = 0; ; i++) { + if (i >= sz1) { + // end of s1 + // equal or s2 is bigger + return (i < sz2) ? -1 : 0; + } else if (i >= sz2) { + // s2 not yet ended + return 1; + } + if (s1[i] < s2[i]) { + return -1; + } + if (s1[i] > s2[i]) { + return 1; + } + // char [i] is matching, continue + } +} + +/// arbitrary char type strcmp, both args are non-empty, s2 is zero-terminated +template +inline int str_cmp_nonempty(const char_type * s1, size_type sz1, const char_type * s2) { + // we are sure that sz1>0 && sz2>0 + for (size_type i = 0; ; i++) { + if (i >= sz1) { + // end of s1 + // equal or s2 is bigger + return s2[i] ? -1 : 0; + } else if (!s2[i]) { + // end of s2 + // bigger + return 1; + } + if (s1[i] < s2[i]) { + return -1; + } + if (s1[i] > s2[i]) { + return 1; + } + // char [i] is matching, continue + } +} + +/// arbitrary char type strcmp, both args are non-empty, zero-terminated +template +inline int str_cmp_nonempty(const char_type * s1, const char_type * s2) { + // we are sure that sz1>0 && sz2>0 + for (size_type i = 0; ; i++) { + if (!s1[i]) { + // end of s1 + // equal or s2 is bigger + return s2[i] ? -1 : 0; + } else if (!s2[i]) { + // end of s2 + // bigger + return 1; + } + if (s1[i] < s2[i]) { + return -1; + } + if (s1[i] > s2[i]) { + return 1; + } + // char [i] is matching, continue + } +} + // String data buffer with ref count // forward declaration of string class @@ -237,6 +335,89 @@ class string { return *this; } + /// compare this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(const string& s) const noexcept { + size_t sz1 = length(); + size_t sz2 = s.length(); + if (sz1 == 0) { + return sz2 > 0 ? -1 : 0; + } else if (sz2 == 0) { + return 1; + } + return str_cmp_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); + } + + /// compare this string with string literal, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(const char_type* s) const noexcept { + size_t sz1 = length(); + if (sz1 == 0) { + return (s != nullptr && *s != 0) ? -1 : 0; + } else if (s == nullptr || *s == 0) { + return 1; + } + return str_cmp_nonempty(pchunk->buf, sz1, s); + } + + /// returns true when strings are equal + bool operator == (const string& s) const noexcept { + return compare(s) == 0; + } + + /// returns true when strings are not equal + bool operator != (const string& s) const noexcept { + return compare(s) != 0; + } + + /// returns true when this string < s + bool operator < (const string& s) const noexcept { + return compare(s) < 0; + } + + /// returns true when this string >= s + bool operator <= (const string& s) const noexcept { + return compare(s) <= 0; + } + + /// returns true when this string > s + bool operator > (const string& s) const noexcept { + return compare(s) > 0; + } + + /// returns true when this string <= s + bool operator >= (const string& s) const noexcept { + return compare(s) >= 0; + } + + /// returns true when strings are equal + bool operator == (const char_type * s) const noexcept { + return compare(s) == 0; + } + + /// returns true when strings are not equal + bool operator != (const char_type * s) const noexcept { + return compare(s) != 0; + } + + /// returns true when this string < s + bool operator < (const char_type * s) const noexcept { + return compare(s) < 0; + } + + /// returns true when this string <= s + bool operator <= (const char_type * s) const noexcept { + return compare(s) <= 0; + } + + /// returns true when this string > s + bool operator > (const char_type * s) const noexcept { + return compare(s) > 0; + } + + /// returns true when this string >= s + bool operator >= (const char_type * s) const noexcept { + return compare(s) >= 0; + } + /// sets value to null string, freeing buffer void clear() noexcept { if (pchunk != nullptr) { @@ -298,6 +479,7 @@ class string { chunk_t * pchunk {nullptr}; }; + typedef lstring_chunk_t lstring8_chunk_t; typedef lstring_chunk_t lstring16_chunk_t; typedef lstring_chunk_t lstring32_chunk_t; From d01d20f309a76dfe1fc6d4117d64ec343869e7df Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Fri, 24 Jul 2026 21:39:08 +0100 Subject: [PATCH 11/36] extend test coverage --- tests/src/lvstring2.cpp | 135 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/tests/src/lvstring2.cpp b/tests/src/lvstring2.cpp index c4e86a895e..2d99e1b534 100644 --- a/tests/src/lvstring2.cpp +++ b/tests/src/lvstring2.cpp @@ -133,33 +133,81 @@ void test_lstring2_chunks() { void test_lstring8() { printf("running test_lstring8()\n"); ls_alloc_stats.dump(); + + // --- Default construction --- lString8 s {}; TCHECK(s.capacity() == 0); TCHECK(s.size() == 0); TCHECK(s.length() == 0); TCHECK(s.empty()); - ls_alloc_stats.dump(); + + // --- C-string construction --- lString8 s2 {"qwerty"}; TCHECK(s2.capacity() >= 6); TCHECK(s2.size() >= 6); TCHECK(s2.length() == 6); TCHECK(!s2.empty()); - ls_alloc_stats.dump(); + + // --- C-string with count + reserved --- lString8 s3 {"qwerty", 3, 100}; TCHECK(s3.capacity() >= 100); TCHECK(s3.size() >= 100); TCHECK(s3.length() == 3); - printf("s3.capacity == %d\n", s3.capacity()); TCHECK(!s3.empty()); + + // --- Move construction --- lString8 s4 = lString8("move"); + TCHECK(s4.length() == 4); s4 = lString8("move consturctor assignment"); - // copy assignment + TCHECK(s4.length() == 27); + + // --- Copy assignment --- s2 = s4; + TCHECK(s2 == s4); + TCHECK(s2.length() == s4.length()); + + // --- Clear --- ls_alloc_stats.dump("before clear"); s2.clear(); ls_alloc_stats.dump("after clear"); + TCHECK(s2.empty()); + TCHECK(s2.length() == 0); + TCHECK(s2.capacity() == 0); + + // --- Reset --- s4.reset(500); ls_alloc_stats.dump("after reset"); + TCHECK(s4.empty()); + TCHECK(s4.length() == 0); + TCHECK(s4.capacity() >= 500); + + // --- Reset on empty --- + lString8 s_empty; + s_empty.reset(50); + TCHECK(s_empty.empty()); + TCHECK(s_empty.capacity() >= 50); + + // --- Resize --- + lString8 s_rsz {"hi"}; + s_rsz.resize(5, 'x'); + TCHECK(s_rsz.length() == 5); + TCHECK(s_rsz[2] == 'x'); + TCHECK(s_rsz[3] == 'x'); + TCHECK(s_rsz[4] == 'x'); + + // --- Resize to smaller (no-op if refCount==1 and capacity sufficient) --- + lString8 s_rsz2 {"hello world"}; + s_rsz2.resize(3); + TCHECK(s_rsz2.length() == 3); + + // --- Resize empty string --- + lString8 s_rsz3; + s_rsz3.resize(4, 'z'); + TCHECK(s_rsz3.length() == 4); + TCHECK(s_rsz3[0] == 'z'); + TCHECK(s_rsz3[3] == 'z'); + + // --- Comparison (string vs string) --- lString8 s5 {"abc"}; lString8 s6 {"abc"}; lString8 s7 {"Abc"}; @@ -172,9 +220,88 @@ void test_lstring8() { TCHECK(s5 >= s7); TCHECK(s7 <= s5); TCHECK(s5 != s7); + + // --- Comparison (string vs c-string) --- TCHECK(s5 == "abc"); TCHECK(s5 > "ab"); TCHECK(s5 < "abcd"); + TCHECK(s5 != "xyz"); + TCHECK(s5 <= "abc"); + TCHECK(s5 >= "abc"); + TCHECK(s5 <= "abd"); + TCHECK(s5 >= "abb"); + + // --- Comparison with empty --- + lString8 s_empty2; + TCHECK(s_empty2 == ""); + TCHECK(s_empty2 == nullptr); + TCHECK(s_empty2 < "a"); + TCHECK(s5 > ""); + + // --- Comparison with nullptr --- + TCHECK(s_empty2 == nullptr); + TCHECK(s5 != nullptr); + + // --- Self-assignment --- + lString8 s_self {"self"}; + lString8 &ref_self = s_self; + s_self = ref_self; + TCHECK(s_self == "self"); + + // --- Move assignment to self (same pointer) --- + lString8 s_selfmove {"selfmove"}; + s_selfmove = std::move(s_selfmove); + TCHECK(s_selfmove == "selfmove"); + + // --- Compare method --- + TCHECK(s5.compare(s6) == 0); + TCHECK(s5.compare("abc") == 0); + TCHECK(s5.compare("abd") < 0); + TCHECK(s5.compare("abb") > 0); + + // --- Length/size/capacity/empty consistency --- + lString8 s_len {"12345"}; + TCHECK(s_len.length() == 5); + TCHECK(s_len.size() == s_len.capacity()); + TCHECK(!s_len.empty()); + TCHECK(lString8{}.empty()); + TCHECK(lString8{}.length() == 0); + TCHECK(lString8{}.capacity() == 0); + + // --- Edge case: empty string construction --- + lString8 s_empty_str {""}; + TCHECK(s_empty_str.empty()); + TCHECK(s_empty_str.length() == 0); + + // --- Edge case: nullptr construction --- + lString8 s_null {nullptr}; + TCHECK(s_null.empty()); + TCHECK(s_null.length() == 0); + TCHECK(s_null.capacity() == 0); + + // --- Edge case: single char --- + lString8 s_one {"a"}; + TCHECK(s_one.length() == 1); + TCHECK(s_one[0] == 'a'); + + // --- Edge case: string with embedded null (count constructor) --- + lString8 s_embed {"ab\0cd", 5, 5}; + TCHECK(s_embed.length() == 5); + TCHECK(s_embed.capacity() >= 5); + + // --- Copy constructor --- + lString8 s_copy {"copyme"}; + lString8 s_copy2 {s_copy}; + TCHECK(s_copy2 == "copyme"); + TCHECK(s_copy2.length() == 6); + + // --- Move constructor --- + lString8 s_move_src {"moveme"}; + lString8 s_move_dst {std::move(s_move_src)}; + TCHECK(s_move_dst == "moveme"); + TCHECK(s_move_dst.length() == 6); + // s_move_src is now in moved-from state + ls_alloc_stats.dump("after compare"); } From 36bd6543ffc1eb8ec1830e53206c03116504ad79 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 15:12:14 +0100 Subject: [PATCH 12/36] lstring2 library tests moved to separate files; added string [] and at() --- tests/CMakeLists.txt | 4 +- tests/src/lvstring2.cpp | 308 +---------------------------------- tests/src/lvstring2.h | 135 ++++++++++++++-- tests/src/lvstring2test.cpp | 315 ++++++++++++++++++++++++++++++++++++ tests/src/lvstring2test.h | 12 ++ tests/src/stringtest.cpp | 27 ++-- 6 files changed, 466 insertions(+), 335 deletions(-) create mode 100644 tests/src/lvstring2test.cpp create mode 100644 tests/src/lvstring2test.h diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ca7187c1b0..4829b6fec5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,7 +22,9 @@ ELSE() ADD_DEFINITIONS(-DCR3_DATA_DIR="") ENDIF(MAC) -ADD_EXECUTABLE(cr3test ${CR3TEST_SOURCES}) +ADD_EXECUTABLE(cr3test ${CR3TEST_SOURCES} + src/lvstring2test.h + src/lvstring2test.cpp) SET (EXTRA_LIBS fontconfig ${STD_LIBS}) TARGET_LINK_LIBRARIES(cr3test crengine tinydict ${EXTRA_LIBS}) diff --git a/tests/src/lvstring2.cpp b/tests/src/lvstring2.cpp index 2d99e1b534..6c5a8c5c72 100644 --- a/tests/src/lvstring2.cpp +++ b/tests/src/lvstring2.cpp @@ -5,6 +5,9 @@ using namespace lv; namespace lv { +lChar32 fake_null_buffer_32 = 0; + +#ifdef DEBUG_TRACK_LSTRING2_ALLOC lStringStats ls_alloc_stats; void lStringStats::dump(const char * msg) { printf("stats[%s]:" @@ -15,309 +18,6 @@ void lStringStats::dump(const char * msg) { copyConstr, moveConstr, copyAssign, moveAssign ); } - -static int test_errors = 0; -#define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) - -void test_lstring2_chunks() { - - // struct size checks - printf("sizeof(lstring8_chunk_t) = %llu\n", sizeof(lstring8_chunk_t)); - printf("sizeof(lstring16_chunk_t) = %llu\n", sizeof(lstring16_chunk_t)); - printf("sizeof(lstring32_chunk_t) = %llu\n", sizeof(lstring32_chunk_t)); - TCHECK(sizeof(lstring8_chunk_t) == sizeof(lstring16_chunk_t)); - TCHECK(sizeof(lstring16_chunk_t) == sizeof(lstring32_chunk_t)); - TCHECK(sizeof(lstring8_chunk_t) == 12); - - //std::atomic_int counter{0}; - //counter.fetch_add(); - - TCHECK(lstring8_chunk_t::alloc_align_bytes == 16); - TCHECK(lstring8_chunk_t::min_size == 3); - TCHECK(lstring8_chunk_t::size_align == 16); - - TCHECK(lstring16_chunk_t::alloc_align_bytes == 16); - TCHECK(lstring16_chunk_t::min_size == 1); - TCHECK(lstring16_chunk_t::size_align == 8); - - TCHECK(lstring32_chunk_t::alloc_align_bytes == 16); - TCHECK(lstring32_chunk_t::min_size == 0); - TCHECK(lstring32_chunk_t::size_align == 4); - - TCHECK(lstring8_chunk_t::alignSize(0) == 4 - 1); - TCHECK(lstring8_chunk_t::alignSize(1) == 4 - 1); - TCHECK(lstring8_chunk_t::alignSize(2) == 4 - 1); - TCHECK(lstring8_chunk_t::alignSize(3) == 4 - 1); - TCHECK(lstring8_chunk_t::alignSize(4) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(5) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(6) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(7) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(11) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(12) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(13) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(15) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(16) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(16+3) == 16+3); - TCHECK(lstring8_chunk_t::alignSize(16+4) == 16+16+3); - - lstring8_chunk_t * chunk8_1 = lstring8_chunk_t::alloc(25); - TCHECK(chunk8_1->size == 32+3); - TCHECK(chunk8_1->len == 0); - TCHECK(chunk8_1->refCount == 1); - TCHECK(chunk8_1->buf[0] == 0); - for (unsigned i = 0; i < 25; i++) { - chunk8_1->buf[i] = 'a'; - chunk8_1->buf[i + 1] = 0; - } - chunk8_1->len = 25; - - chunk8_1->testAddRef(); - chunk8_1->testReleaseRef(); - - lstring8_chunk_t::free(chunk8_1); - - lstring8_chunk_t * chunk8_2 = lstring8_chunk_t::alloc("abcdefg", 4, 10); - TCHECK(chunk8_2->size == 16+3); - TCHECK(chunk8_2->len == 4); - TCHECK(chunk8_2->refCount == 1); - TCHECK(chunk8_2->buf[0] == 'a' && chunk8_2->buf[1] == 'b' && chunk8_2->buf[2] == 'c' && chunk8_2->buf[3] == 'd' && chunk8_2->buf[4] == 0); - lstring8_chunk_t::free(chunk8_2); - - lstring16_chunk_t * chunk16_1 = lstring16_chunk_t::alloc(25); - TCHECK(chunk16_1->size == 24+1); - TCHECK(chunk16_1->len == 0); - TCHECK(chunk16_1->refCount == 1); - TCHECK(chunk16_1->buf[0] == 0); - for (unsigned i = 0; i < 25; i++) { - chunk16_1->buf[i] = 'b'; - chunk16_1->buf[i + 1] = 0; - } - chunk16_1->len = 25; - lstring16_chunk_t::free(chunk16_1); - - lstring16_chunk_t * chunk16_2 = lstring16_chunk_t::alloc(u"bcdefghi", 4, 10); - TCHECK(chunk16_2->size == 16+1); - TCHECK(chunk16_2->len == 4); - TCHECK(chunk16_2->refCount == 1); - TCHECK(chunk16_2->buf[0] == u'b' && chunk16_2->buf[1] == u'c' && chunk16_2->buf[2] == u'd' && chunk16_2->buf[3] == u'e' && chunk16_2->buf[4] == 0); - intrusive_ptr_add_ref(chunk16_2); - TCHECK(chunk16_2->getRefCount() == 2); - intrusive_ptr_release(chunk16_2); - TCHECK(chunk16_2->getRefCount() == 1); - intrusive_ptr_release(chunk16_2); - // released automatically - //lstring16_chunk_t::free(chunk16_2); - - lstring32_chunk_t * chunk32_1 = lstring32_chunk_t::alloc(25); - TCHECK(chunk32_1->size == 28); - TCHECK(chunk32_1->len == 0); - TCHECK(chunk32_1->refCount == 1); - TCHECK(chunk32_1->buf[0] == 0); - for (unsigned i = 0; i < 25; i++) { - chunk32_1->buf[i] = 'b'; - chunk32_1->buf[i + 1] = 0; - } - chunk32_1->len = 25; - //lstring32_chunk_t::free(chunk32_1); - intrusive_ptr_release(chunk32_1); - - lstring32_chunk_t * chunk32_2 = lstring32_chunk_t::alloc(U"cdefghijkl", 4, 10); - TCHECK(chunk32_2->size == 12); - TCHECK(chunk32_2->len == 4); - TCHECK(chunk32_2->refCount == 1); - TCHECK(chunk32_2->buf[0] == U'c' && chunk32_2->buf[1] == U'd' && chunk32_2->buf[2] == U'e' && chunk32_2->buf[3] == U'f' && chunk32_2->buf[4] == 0); - lstring32_chunk_t::free(chunk32_2); - -} - -void test_lstring8() { - printf("running test_lstring8()\n"); - ls_alloc_stats.dump(); - - // --- Default construction --- - lString8 s {}; - TCHECK(s.capacity() == 0); - TCHECK(s.size() == 0); - TCHECK(s.length() == 0); - TCHECK(s.empty()); - - // --- C-string construction --- - lString8 s2 {"qwerty"}; - TCHECK(s2.capacity() >= 6); - TCHECK(s2.size() >= 6); - TCHECK(s2.length() == 6); - TCHECK(!s2.empty()); - - // --- C-string with count + reserved --- - lString8 s3 {"qwerty", 3, 100}; - TCHECK(s3.capacity() >= 100); - TCHECK(s3.size() >= 100); - TCHECK(s3.length() == 3); - TCHECK(!s3.empty()); - - // --- Move construction --- - lString8 s4 = lString8("move"); - TCHECK(s4.length() == 4); - s4 = lString8("move consturctor assignment"); - TCHECK(s4.length() == 27); - - // --- Copy assignment --- - s2 = s4; - TCHECK(s2 == s4); - TCHECK(s2.length() == s4.length()); - - // --- Clear --- - ls_alloc_stats.dump("before clear"); - s2.clear(); - ls_alloc_stats.dump("after clear"); - TCHECK(s2.empty()); - TCHECK(s2.length() == 0); - TCHECK(s2.capacity() == 0); - - // --- Reset --- - s4.reset(500); - ls_alloc_stats.dump("after reset"); - TCHECK(s4.empty()); - TCHECK(s4.length() == 0); - TCHECK(s4.capacity() >= 500); - - // --- Reset on empty --- - lString8 s_empty; - s_empty.reset(50); - TCHECK(s_empty.empty()); - TCHECK(s_empty.capacity() >= 50); - - // --- Resize --- - lString8 s_rsz {"hi"}; - s_rsz.resize(5, 'x'); - TCHECK(s_rsz.length() == 5); - TCHECK(s_rsz[2] == 'x'); - TCHECK(s_rsz[3] == 'x'); - TCHECK(s_rsz[4] == 'x'); - - // --- Resize to smaller (no-op if refCount==1 and capacity sufficient) --- - lString8 s_rsz2 {"hello world"}; - s_rsz2.resize(3); - TCHECK(s_rsz2.length() == 3); - - // --- Resize empty string --- - lString8 s_rsz3; - s_rsz3.resize(4, 'z'); - TCHECK(s_rsz3.length() == 4); - TCHECK(s_rsz3[0] == 'z'); - TCHECK(s_rsz3[3] == 'z'); - - // --- Comparison (string vs string) --- - lString8 s5 {"abc"}; - lString8 s6 {"abc"}; - lString8 s7 {"Abc"}; - ls_alloc_stats.dump("before compare"); - TCHECK(s5 == s6); - TCHECK(s5 <= s6); - TCHECK(s5 >= s6); - TCHECK(s5 > s7); - TCHECK(s7 < s5); - TCHECK(s5 >= s7); - TCHECK(s7 <= s5); - TCHECK(s5 != s7); - - // --- Comparison (string vs c-string) --- - TCHECK(s5 == "abc"); - TCHECK(s5 > "ab"); - TCHECK(s5 < "abcd"); - TCHECK(s5 != "xyz"); - TCHECK(s5 <= "abc"); - TCHECK(s5 >= "abc"); - TCHECK(s5 <= "abd"); - TCHECK(s5 >= "abb"); - - // --- Comparison with empty --- - lString8 s_empty2; - TCHECK(s_empty2 == ""); - TCHECK(s_empty2 == nullptr); - TCHECK(s_empty2 < "a"); - TCHECK(s5 > ""); - - // --- Comparison with nullptr --- - TCHECK(s_empty2 == nullptr); - TCHECK(s5 != nullptr); - - // --- Self-assignment --- - lString8 s_self {"self"}; - lString8 &ref_self = s_self; - s_self = ref_self; - TCHECK(s_self == "self"); - - // --- Move assignment to self (same pointer) --- - lString8 s_selfmove {"selfmove"}; - s_selfmove = std::move(s_selfmove); - TCHECK(s_selfmove == "selfmove"); - - // --- Compare method --- - TCHECK(s5.compare(s6) == 0); - TCHECK(s5.compare("abc") == 0); - TCHECK(s5.compare("abd") < 0); - TCHECK(s5.compare("abb") > 0); - - // --- Length/size/capacity/empty consistency --- - lString8 s_len {"12345"}; - TCHECK(s_len.length() == 5); - TCHECK(s_len.size() == s_len.capacity()); - TCHECK(!s_len.empty()); - TCHECK(lString8{}.empty()); - TCHECK(lString8{}.length() == 0); - TCHECK(lString8{}.capacity() == 0); - - // --- Edge case: empty string construction --- - lString8 s_empty_str {""}; - TCHECK(s_empty_str.empty()); - TCHECK(s_empty_str.length() == 0); - - // --- Edge case: nullptr construction --- - lString8 s_null {nullptr}; - TCHECK(s_null.empty()); - TCHECK(s_null.length() == 0); - TCHECK(s_null.capacity() == 0); - - // --- Edge case: single char --- - lString8 s_one {"a"}; - TCHECK(s_one.length() == 1); - TCHECK(s_one[0] == 'a'); - - // --- Edge case: string with embedded null (count constructor) --- - lString8 s_embed {"ab\0cd", 5, 5}; - TCHECK(s_embed.length() == 5); - TCHECK(s_embed.capacity() >= 5); - - // --- Copy constructor --- - lString8 s_copy {"copyme"}; - lString8 s_copy2 {s_copy}; - TCHECK(s_copy2 == "copyme"); - TCHECK(s_copy2.length() == 6); - - // --- Move constructor --- - lString8 s_move_src {"moveme"}; - lString8 s_move_dst {std::move(s_move_src)}; - TCHECK(s_move_dst == "moveme"); - TCHECK(s_move_dst.length() == 6); - // s_move_src is now in moved-from state - - ls_alloc_stats.dump("after compare"); -} - -void test_lstring2() { - printf("New strings library tests\n"); - test_lstring2_chunks(); - ls_alloc_stats.dump("before test_lstring8()"); - test_lstring8(); - ls_alloc_stats.dump("after test_lstring8()"); - - if (test_errors == 0) { - printf("New strings library tests completed successfully\n"); - } else { - printf("***\nNEW STRINGS LIBRARY TESTS COMPLETED WITH %d ERRORS\n***\n", test_errors); - } - printf("New strings library completed\n"); -} +#endif } diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index f2e4e22546..a7bf0d7a12 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -7,8 +7,11 @@ #include #include +#define DEBUG_TRACK_LSTRING2_ALLOC + namespace lv { +#ifdef DEBUG_TRACK_LSTRING2_ALLOC struct lStringStats { int allocCount {0}; int freeCount {0}; @@ -19,12 +22,24 @@ struct lStringStats { void dump(const char * msg = ""); }; extern lStringStats ls_alloc_stats; -#define LS_COUNT_ALLOC ls_alloc_stats.allocCount++ -#define LS_COUNT_FREE ls_alloc_stats.freeCount++ -#define LS_COUNT_COPY_CONSTR ls_alloc_stats.copyConstr++ -#define LS_COUNT_MOVE_CONSTR ls_alloc_stats.moveConstr++ -#define LS_COUNT_COPY_ASSIGN ls_alloc_stats.copyAssign++ -#define LS_COUNT_MOVE_ASSIGN ls_alloc_stats.moveAssign++ + +#define LS_COUNT_ALLOC ls_alloc_stats.allocCount++; +#define LS_COUNT_FREE ls_alloc_stats.freeCount++; +#define LS_COUNT_COPY_CONSTR ls_alloc_stats.copyConstr++; +#define LS_COUNT_MOVE_CONSTR ls_alloc_stats.moveConstr++; +#define LS_COUNT_COPY_ASSIGN ls_alloc_stats.copyAssign++; +#define LS_COUNT_MOVE_ASSIGN ls_alloc_stats.moveAssign++; + +#else + +#define LS_COUNT_ALLOC +#define LS_COUNT_FREE +#define LS_COUNT_COPY_CONSTR +#define LS_COUNT_MOVE_CONSTR +#define LS_COUNT_COPY_ASSIGN +#define LS_COUNT_MOVE_ASSIGN + +#endif // Helper functions @@ -210,7 +225,7 @@ struct lstring_chunk_t { /// chunk allocation function: create string buffer with reserved sz characters + zero termination char, ref counter 1 static lstring_chunk_t * alloc(size_type sz) noexcept { - LS_COUNT_ALLOC; + LS_COUNT_ALLOC //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); sz = alignSize(sz); @@ -226,7 +241,7 @@ struct lstring_chunk_t { /// chunk allocation function: create string buffer initialized with count chars from string s with reserved sz characters + zero termination char, ref counter 1 static lstring_chunk_t * alloc(const char_type * s, size_type count, size_type sz = 0) noexcept { - LS_COUNT_ALLOC; + LS_COUNT_ALLOC if (sz < count) sz = count; sz = alignSize(sz); @@ -244,7 +259,7 @@ struct lstring_chunk_t { /// free chunk memory static void free( const lstring_chunk_t * pChunk ) noexcept { - LS_COUNT_FREE; + LS_COUNT_FREE ::free(const_cast(pChunk)); } @@ -265,6 +280,8 @@ struct lstring_chunk_t { char_type buf[0]; // z-string }; +extern lChar32 fake_null_buffer_32; + template class string { using chunk_t = lstring_chunk_t; @@ -283,13 +300,13 @@ class string { } /// copy constructor string(const string&s) noexcept { - LS_COUNT_COPY_CONSTR; + LS_COUNT_COPY_CONSTR pchunk = s.pchunk; intrusive_ptr_add_ref(pchunk); } /// move constructor string(string&&s ) noexcept { - LS_COUNT_MOVE_CONSTR; + LS_COUNT_MOVE_CONSTR pchunk = s.pchunk; s.pchunk = nullptr; } @@ -300,7 +317,7 @@ class string { } /// move assignment string& operator = (string&& s) noexcept { - LS_COUNT_MOVE_ASSIGN; + LS_COUNT_MOVE_ASSIGN if (pchunk != nullptr) { intrusive_ptr_release(pchunk); } @@ -310,7 +327,23 @@ class string { } /// copy assignment string& operator = (const string& s) noexcept { - LS_COUNT_COPY_ASSIGN; + return assign(s); + } + + /// move assignment + string& assign(string&& s) noexcept { + LS_COUNT_MOVE_ASSIGN + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + return *this; + } + + /// copy assignment + string& assign(const string& s) noexcept { + LS_COUNT_COPY_ASSIGN if (&s == this) { // safe self assignment: do nothing return *this; @@ -335,6 +368,81 @@ class string { return *this; } + /// assign from z-terminated string + string& operator = (const char_type * s) noexcept { + return assign(s); + } + + /// assign from z-terminated string + string& assign(const char_type * s) noexcept { + if (s == nullptr || !*s) { + clear(); + } else { + size_type count = str_len(s); + if (pchunk != nullptr && pchunk->getRefCount() == 1 && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + } + + /// assign from char pointer and char count + string& assign(const char_type * s, size_type count) noexcept { + if (s == nullptr || !*s || count == 0) { + clear(); + } else { + if (pchunk != nullptr && pchunk->getRefCount() == 1 && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + return *this; + } + + /// index value. No index nor empty string checks. Will crash on empty string. + char_type operator [] (size_type index) const noexcept { + return pchunk->buf[index]; + } + + /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). + char_type& operator [] (size_type index) noexcept { + return pchunk->buf[index]; + } + + /// index value with bounds checking. No index nor empty string checks. Will crash on empty string. + /// Does not throw exception on index out of bounds, returns 0 instead. + char_type at(size_type index) const noexcept { + if (pchunk == nullptr || index >= pchunk->len) { + return 0; + } + return pchunk->buf[index]; + } + + /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). + /// Does not throw exception on index out of bounds, returns reference pointing to static 0 value instead. + char_type& at(size_type index) noexcept { + if (pchunk == nullptr || index >= pchunk->len) { + return *(static_cast(&fake_null_buffer_32)); + } + return pchunk->buf[index]; + } + /// compare this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal int compare(const string& s) const noexcept { size_t sz1 = length(); @@ -488,7 +596,6 @@ typedef string lString8; typedef string lString16; typedef string lString32; -void test_lstring2(); } diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp new file mode 100644 index 0000000000..ad5956950d --- /dev/null +++ b/tests/src/lvstring2test.cpp @@ -0,0 +1,315 @@ +#include "lvstring2test.h" +#include + +namespace lv { +static int test_errors = 0; +#define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) + +#ifdef DEBUG_TRACK_LSTRING2_ALLOC + #define DUMP_ALLOC_STATS(x) ls_alloc_stats.dump(x); +#else + #define DUMP_ALLOC_STATS(x) +#endif + +void test_lstring2_chunks() { + + // struct size checks + printf("sizeof(lstring8_chunk_t) = %llu\n", sizeof(lstring8_chunk_t)); + printf("sizeof(lstring16_chunk_t) = %llu\n", sizeof(lstring16_chunk_t)); + printf("sizeof(lstring32_chunk_t) = %llu\n", sizeof(lstring32_chunk_t)); + TCHECK(sizeof(lstring8_chunk_t) == sizeof(lstring16_chunk_t)); + TCHECK(sizeof(lstring16_chunk_t) == sizeof(lstring32_chunk_t)); + TCHECK(sizeof(lstring8_chunk_t) == 12); + + //std::atomic_int counter{0}; + //counter.fetch_add(); + + TCHECK(lstring8_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring8_chunk_t::min_size == 3); + TCHECK(lstring8_chunk_t::size_align == 16); + + TCHECK(lstring16_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring16_chunk_t::min_size == 1); + TCHECK(lstring16_chunk_t::size_align == 8); + + TCHECK(lstring32_chunk_t::alloc_align_bytes == 16); + TCHECK(lstring32_chunk_t::min_size == 0); + TCHECK(lstring32_chunk_t::size_align == 4); + + TCHECK(lstring8_chunk_t::alignSize(0) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(1) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(2) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(3) == 4 - 1); + TCHECK(lstring8_chunk_t::alignSize(4) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(5) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(6) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(7) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(11) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(12) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(13) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(15) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16+3) == 16+3); + TCHECK(lstring8_chunk_t::alignSize(16+4) == 16+16+3); + + lstring8_chunk_t * chunk8_1 = lstring8_chunk_t::alloc(25); + TCHECK(chunk8_1->size == 32+3); + TCHECK(chunk8_1->len == 0); + TCHECK(chunk8_1->refCount == 1); + TCHECK(chunk8_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk8_1->buf[i] = 'a'; + chunk8_1->buf[i + 1] = 0; + } + chunk8_1->len = 25; + + chunk8_1->testAddRef(); + chunk8_1->testReleaseRef(); + + lstring8_chunk_t::free(chunk8_1); + + lstring8_chunk_t * chunk8_2 = lstring8_chunk_t::alloc("abcdefg", 4, 10); + TCHECK(chunk8_2->size == 16+3); + TCHECK(chunk8_2->len == 4); + TCHECK(chunk8_2->refCount == 1); + TCHECK(chunk8_2->buf[0] == 'a' && chunk8_2->buf[1] == 'b' && chunk8_2->buf[2] == 'c' && chunk8_2->buf[3] == 'd' && chunk8_2->buf[4] == 0); + lstring8_chunk_t::free(chunk8_2); + + lstring16_chunk_t * chunk16_1 = lstring16_chunk_t::alloc(25); + TCHECK(chunk16_1->size == 24+1); + TCHECK(chunk16_1->len == 0); + TCHECK(chunk16_1->refCount == 1); + TCHECK(chunk16_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk16_1->buf[i] = 'b'; + chunk16_1->buf[i + 1] = 0; + } + chunk16_1->len = 25; + lstring16_chunk_t::free(chunk16_1); + + lstring16_chunk_t * chunk16_2 = lstring16_chunk_t::alloc(u"bcdefghi", 4, 10); + TCHECK(chunk16_2->size == 16+1); + TCHECK(chunk16_2->len == 4); + TCHECK(chunk16_2->refCount == 1); + TCHECK(chunk16_2->buf[0] == u'b' && chunk16_2->buf[1] == u'c' && chunk16_2->buf[2] == u'd' && chunk16_2->buf[3] == u'e' && chunk16_2->buf[4] == 0); + intrusive_ptr_add_ref(chunk16_2); + TCHECK(chunk16_2->getRefCount() == 2); + intrusive_ptr_release(chunk16_2); + TCHECK(chunk16_2->getRefCount() == 1); + intrusive_ptr_release(chunk16_2); + // released automatically + //lstring16_chunk_t::free(chunk16_2); + + lstring32_chunk_t * chunk32_1 = lstring32_chunk_t::alloc(25); + TCHECK(chunk32_1->size == 28); + TCHECK(chunk32_1->len == 0); + TCHECK(chunk32_1->refCount == 1); + TCHECK(chunk32_1->buf[0] == 0); + for (unsigned i = 0; i < 25; i++) { + chunk32_1->buf[i] = 'b'; + chunk32_1->buf[i + 1] = 0; + } + chunk32_1->len = 25; + //lstring32_chunk_t::free(chunk32_1); + intrusive_ptr_release(chunk32_1); + + lstring32_chunk_t * chunk32_2 = lstring32_chunk_t::alloc(U"cdefghijkl", 4, 10); + TCHECK(chunk32_2->size == 12); + TCHECK(chunk32_2->len == 4); + TCHECK(chunk32_2->refCount == 1); + TCHECK(chunk32_2->buf[0] == U'c' && chunk32_2->buf[1] == U'd' && chunk32_2->buf[2] == U'e' && chunk32_2->buf[3] == U'f' && chunk32_2->buf[4] == 0); + lstring32_chunk_t::free(chunk32_2); + +} + +void test_lstring8() { + printf("running test_lstring8()\n"); + DUMP_ALLOC_STATS("Entering test_lstring8()") + + // --- Default construction --- + lString8 s {}; + TCHECK(s.capacity() == 0); + TCHECK(s.size() == 0); + TCHECK(s.length() == 0); + TCHECK(s.empty()); + + // --- C-string construction --- + lString8 s2 {"qwerty"}; + TCHECK(s2.capacity() >= 6); + TCHECK(s2.size() >= 6); + TCHECK(s2.length() == 6); + TCHECK(!s2.empty()); + + // --- C-string with count + reserved --- + lString8 s3 {"qwerty", 3, 100}; + TCHECK(s3.capacity() >= 100); + TCHECK(s3.size() >= 100); + TCHECK(s3.length() == 3); + TCHECK(!s3.empty()); + + // --- Move construction --- + lString8 s4 = lString8("move"); + TCHECK(s4.length() == 4); + s4 = lString8("move consturctor assignment"); + TCHECK(s4.length() == 27); + + // --- Copy assignment --- + s2 = s4; + TCHECK(s2 == s4); + TCHECK(s2.length() == s4.length()); + + // --- Clear --- + DUMP_ALLOC_STATS("before clear") + s2.clear(); + DUMP_ALLOC_STATS("after clear") + TCHECK(s2.empty()); + TCHECK(s2.length() == 0); + TCHECK(s2.capacity() == 0); + + // --- Reset --- + s4.reset(500); + DUMP_ALLOC_STATS("after reset") + TCHECK(s4.empty()); + TCHECK(s4.length() == 0); + TCHECK(s4.capacity() >= 500); + + // --- Reset on empty --- + lString8 s_empty; + s_empty.reset(50); + TCHECK(s_empty.empty()); + TCHECK(s_empty.capacity() >= 50); + + // --- Resize --- + lString8 s_rsz {"hi"}; + s_rsz.resize(5, 'x'); + TCHECK(s_rsz.length() == 5); + TCHECK(s_rsz[2] == 'x'); + TCHECK(s_rsz[3] == 'x'); + TCHECK(s_rsz[4] == 'x'); + + // --- Resize to smaller (no-op if refCount==1 and capacity sufficient) --- + lString8 s_rsz2 {"hello world"}; + s_rsz2.resize(3); + TCHECK(s_rsz2.length() == 3); + + // --- Resize empty string --- + lString8 s_rsz3; + s_rsz3.resize(4, 'z'); + TCHECK(s_rsz3.length() == 4); + TCHECK(s_rsz3[0] == 'z'); + TCHECK(s_rsz3[3] == 'z'); + + // --- Comparison (string vs string) --- + lString8 s5 {"abc"}; + lString8 s6 {"abc"}; + lString8 s7 {"Abc"}; + DUMP_ALLOC_STATS("before compare") + TCHECK(s5 == s6); + TCHECK(s5 <= s6); + TCHECK(s5 >= s6); + TCHECK(s5 > s7); + TCHECK(s7 < s5); + TCHECK(s5 >= s7); + TCHECK(s7 <= s5); + TCHECK(s5 != s7); + + // --- Comparison (string vs c-string) --- + TCHECK(s5 == "abc"); + TCHECK(s5 > "ab"); + TCHECK(s5 < "abcd"); + TCHECK(s5 != "xyz"); + TCHECK(s5 <= "abc"); + TCHECK(s5 >= "abc"); + TCHECK(s5 <= "abd"); + TCHECK(s5 >= "abb"); + + // --- Comparison with empty --- + lString8 s_empty2; + TCHECK(s_empty2 == ""); + TCHECK(s_empty2 == nullptr); + TCHECK(s_empty2 < "a"); + TCHECK(s5 > ""); + + // --- Comparison with nullptr --- + TCHECK(s_empty2 == nullptr); + TCHECK(s5 != nullptr); + + // --- Self-assignment --- + lString8 s_self {"self"}; + lString8 &ref_self = s_self; + s_self = ref_self; + TCHECK(s_self == "self"); + + // --- Move assignment to self (same pointer) --- + lString8 s_selfmove {"selfmove"}; + s_selfmove = std::move(s_selfmove); + TCHECK(s_selfmove == "selfmove"); + + // --- Compare method --- + TCHECK(s5.compare(s6) == 0); + TCHECK(s5.compare("abc") == 0); + TCHECK(s5.compare("abd") < 0); + TCHECK(s5.compare("abb") > 0); + + // --- Length/size/capacity/empty consistency --- + lString8 s_len {"12345"}; + TCHECK(s_len.length() == 5); + TCHECK(s_len.size() == s_len.capacity()); + TCHECK(!s_len.empty()); + TCHECK(lString8{}.empty()); + TCHECK(lString8{}.length() == 0); + TCHECK(lString8{}.capacity() == 0); + + // --- Edge case: empty string construction --- + lString8 s_empty_str {""}; + TCHECK(s_empty_str.empty()); + TCHECK(s_empty_str.length() == 0); + + // --- Edge case: nullptr construction --- + lString8 s_null {nullptr}; + TCHECK(s_null.empty()); + TCHECK(s_null.length() == 0); + TCHECK(s_null.capacity() == 0); + + // --- Edge case: single char --- + lString8 s_one {"a"}; + TCHECK(s_one.length() == 1); + TCHECK(s_one[0] == 'a'); + + // --- Edge case: string with embedded null (count constructor) --- + lString8 s_embed {"ab\0cd", 5, 5}; + TCHECK(s_embed.length() == 5); + TCHECK(s_embed.capacity() >= 5); + + // --- Copy constructor --- + lString8 s_copy {"copyme"}; + lString8 s_copy2 {s_copy}; + TCHECK(s_copy2 == "copyme"); + TCHECK(s_copy2.length() == 6); + + // --- Move constructor --- + lString8 s_move_src {"moveme"}; + lString8 s_move_dst {std::move(s_move_src)}; + TCHECK(s_move_dst == "moveme"); + TCHECK(s_move_dst.length() == 6); + // s_move_src is now in moved-from state + + DUMP_ALLOC_STATS("after compare") +} + +void test_lstring2() { + printf("New strings library tests\n"); + test_lstring2_chunks(); + DUMP_ALLOC_STATS("before test_lstring8()") + test_lstring8(); + DUMP_ALLOC_STATS("after test_lstring8()") + + if (test_errors == 0) { + printf("New strings library tests completed successfully\n"); + } else { + printf("***\nNEW STRINGS LIBRARY TESTS COMPLETED WITH %d ERRORS\n***\n", test_errors); + } + printf("New strings library completed\n"); +} + +} diff --git a/tests/src/lvstring2test.h b/tests/src/lvstring2test.h new file mode 100644 index 0000000000..0b3f298026 --- /dev/null +++ b/tests/src/lvstring2test.h @@ -0,0 +1,12 @@ +#ifndef LVSTRING2TEST_H +#define LVSTRING2TEST_H + +#include "lvstring2.h" + +namespace lv { + +void test_lstring2(); + +} + +#endif // LVSTRING2TEST_H diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp index 163c2ba85a..2f3fef92cf 100644 --- a/tests/src/stringtest.cpp +++ b/tests/src/stringtest.cpp @@ -4,13 +4,19 @@ #include "../crengine/include/lvstring8collection.h" #include "../crengine/include/lvstring32collection.h" #include "stringtest.h" -#include "lvstring2.h" +#include "lvstring2test.h" +#if (LDOM_USE_OWN_MEM_MAN==1) #define CHECKBUF_(file, line) check_ls_storage(file ":" #line) #define CHECKBUF CHECKBUF_(__FILE__, __LINE__) +#endif + static int test_errors = 0; +#if (LDOM_USE_OWN_MEM_MAN==1) #define TCHECK(cond) do { CHECKBUF; if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) - +#else +#define TCHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) +#endif class TempStringHolder8 { public: @@ -86,8 +92,6 @@ void testStrings8_16() { s8_a.assign(lString8("move_assign")); TCHECK(s8_a == "move_assign"); - CHECKBUF; - // Append lString8 s8_app; s8_app.append("foo"); @@ -134,14 +138,12 @@ void testStrings8_16() { s8_case.lowercase(); TCHECK(s8_case == "hello world"); - CHECKBUF; - - // compare + // compare TCHECK(s8_cstr.compare("hello") == 0); TCHECK(s8_cstr.compare("world") < 0); TCHECK(s8_cstr.compare("abc") > 0); - // pos / rpos + // pos / rpos lString8 s8_pos("hello world, hello"); TCHECK(s8_pos.pos('w') == 6); TCHECK(s8_pos.pos("world") == 6); @@ -149,7 +151,7 @@ void testStrings8_16() { TCHECK(s8_pos.rpos("hello") == 13); TCHECK(s8_pos.pos(lString8("hello"), 1) == 13); - // startsWith / endsWith + // startsWith / endsWith lString8 s8_sw("hello.cpp"); TCHECK(s8_sw.startsWith("hello")); TCHECK(s8_sw.startsWith(lString8("hello"))); @@ -181,8 +183,6 @@ void testStrings8_16() { TCHECK(s8_sub2.at(0) == 'h'); TCHECK(s8_sub2.at(10) == 'd'); - CHECKBUF; - // atoi lString8 s8_int("12345"); TCHECK(s8_int.atoi() == 12345); @@ -231,8 +231,6 @@ void testStrings8_16() { lString8 s8_h2("hello"); TCHECK(s8_h1.getHash() == s8_h2.getHash()); - CHECKBUF; - // operator << lString8 s8_shift; s8_shift << 'A'; @@ -296,8 +294,6 @@ void testStrings8_16() { const lString8& cs8_test2 = cs8("static_string"); TCHECK(&cs8_test == &cs8_test2); - CHECKBUF; - printf("=== MIX lString16 tests ===\n"); // lString16 constructors @@ -449,7 +445,6 @@ void testStrings8_16() { lString16 s16_added16 = s16_eq1 + u"_suffix"; TCHECK(s16_added16 == u"equal_suffix"); - CHECKBUF; } void testStrings8() { From 885e6da0b4292e56459d79240abdc963ae8e2cc9 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 15:33:45 +0100 Subject: [PATCH 13/36] fix self-assignments --- tests/src/lvstring2.h | 45 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index a7bf0d7a12..7a930a08ce 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -94,7 +94,7 @@ inline int str_cmp_nonempty(const char_type * s1, size_type sz1, const char_type return (i < sz2) ? -1 : 0; } else if (i >= sz2) { // s2 not yet ended - return 1; + return 0; } if (s1[i] < s2[i]) { return -1; @@ -296,7 +296,11 @@ class string { } } explicit string(const char_type* s) noexcept { - pchunk = chunk_t::alloc(s, str_len(s), 0); + if (s != nullptr && *s) { + pchunk = chunk_t::alloc(s, str_len(s), 0); + } else { + pchunk = nullptr; + } } /// copy constructor string(const string&s) noexcept { @@ -318,11 +322,13 @@ class string { /// move assignment string& operator = (string&& s) noexcept { LS_COUNT_MOVE_ASSIGN - if (pchunk != nullptr) { - intrusive_ptr_release(pchunk); + if (this != &s) { + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; } - pchunk = s.pchunk; - s.pchunk = nullptr; return *this; } /// copy assignment @@ -333,29 +339,30 @@ class string { /// move assignment string& assign(string&& s) noexcept { LS_COUNT_MOVE_ASSIGN - if (pchunk != nullptr) { - intrusive_ptr_release(pchunk); + if (this != &s) { + if (pchunk != nullptr) { + intrusive_ptr_release(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; } - pchunk = s.pchunk; - s.pchunk = nullptr; return *this; } /// copy assignment string& assign(const string& s) noexcept { LS_COUNT_COPY_ASSIGN - if (&s == this) { + if (&s == this || pchunk == s.pchunk) { // safe self assignment: do nothing return *this; } if (pchunk != nullptr) { // ignore self-assignment - if (pchunk != s.pchunk) { - intrusive_ptr_release(pchunk); - if (pchunk) { - // assigned non-empty string - intrusive_ptr_add_ref(pchunk); - } + intrusive_ptr_release(pchunk); + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); } } else { // this string is null @@ -445,6 +452,10 @@ class string { /// compare this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal int compare(const string& s) const noexcept { + if (pchunk == s.pchunk) { + // same string + return 0; + } size_t sz1 = length(); size_t sz2 = s.length(); if (sz1 == 0) { From bb27f75921260c7655fbcda1d03e93472df42a3d Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 17:29:24 +0100 Subject: [PATCH 14/36] lvstring2 -- implemented reserve --- crengine/src/lvstring.cpp | 2 ++ tests/src/lvstring2.h | 70 ++++++++++++++++++++++++++++++++----- tests/src/lvstring2test.cpp | 67 +++++++++++++++++++++++++++++++++-- tests/src/stringtest.cpp | 17 ++++++++- 4 files changed, 145 insertions(+), 11 deletions(-) diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index 5cbea93af4..c484c23fcb 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -2142,6 +2142,7 @@ void lString16::resize(size_type n, value_type e) // fill with data if expanded for (size_type i=pchunk->len; ibuf16[i] = e; + pchunk->len = n; pchunk->buf16[pchunk->len] = 0; } @@ -2774,6 +2775,7 @@ void lString8::resize(size_type n, lChar8 e) // fill with data if expanded for (size_type i=pchunk->len; ibuf8[i] = e; + pchunk->len = n; pchunk->buf8[pchunk->len] = 0; } diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 7a930a08ce..9fd2d818e6 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -563,20 +563,74 @@ class string { } } - /// resize and make editable, presercing current content, preparing to modification with reserved size + /// ensure that this string owns buffer and it has at least size capacity + void reserve(size_type size) noexcept { + if (size == 0) { + // reserve(0) is a non-binding request, do nothing + return; + } + if (pchunk != nullptr) { + if (pchunk->getRefCount() == 1) { + // already own buffer + if (pchunk->size < size) { + // only if size is not enough, create a bigger buffer copy + chunk_t * tmp = chunk_t::alloc(size); + tmp->len = pchunk->len; + std::memcpy(tmp->buf, pchunk->buf, pchunk->len); + tmp->buf[tmp->len] = 0; + chunk_t::free(pchunk); + pchunk = tmp; + } + } else { + // we don't own the string, need to create a copy + if (size < pchunk->len) { + // when requested size is smaller than existing string len, ensure we will not loss chars + size = pchunk->len; + } + chunk_t * tmp = chunk_t::alloc(size); + tmp->len = pchunk->len; + std::memcpy(tmp->buf, pchunk->buf, pchunk->len); + tmp->buf[tmp->len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } else { + // create new allocation of requested size + pchunk = chunk_t::alloc(size); + pchunk->len = 0; + pchunk->buf[0] = 0; + } + } + + /// resize string to specified length, filling new positions with e if expanded void resize(size_type size, char_type e = 0) noexcept { if (pchunk == nullptr) { - // buffer is allocated and has capacity at least size + // empty string: allocate new buffer pchunk = chunk_t::alloc(size); + for (size_type i = 0; i < size; i++) + pchunk->buf[i] = e; + pchunk->len = size; + pchunk->buf[size] = 0; } else { - // this string is non-empty + // non-empty string + size_type oldlen = pchunk->len; if (pchunk->getRefCount() == 1 && pchunk->size >= size) { - // this is already our own string with enough capacity -- do nothing + // own buffer with enough capacity: just adjust length + if (size > oldlen) { + for (size_type i = oldlen; i < size; i++) + pchunk->buf[i] = e; + } + pchunk->len = size; + pchunk->buf[size] = 0; } else { - // create new object and copy existing data - chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, size); - tmp->buf[tmp->len] = 0; // zterm - // release old chunk and assign new one + // need new buffer + chunk_t * tmp = chunk_t::alloc(pchunk->buf, oldlen < size ? oldlen : size, size); + if (size > oldlen) { + for (size_type i = oldlen; i < size; i++) + tmp->buf[i] = e; + } + tmp->len = size; + tmp->buf[size] = 0; intrusive_ptr_release(pchunk); pchunk = tmp; } diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index ad5956950d..ebea2ecdf1 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -179,18 +179,23 @@ void test_lstring8() { TCHECK(s_empty.empty()); TCHECK(s_empty.capacity() >= 50); - // --- Resize --- + // --- Resize (expands length, fills with char) --- lString8 s_rsz {"hi"}; s_rsz.resize(5, 'x'); TCHECK(s_rsz.length() == 5); + TCHECK(s_rsz[0] == 'h'); + TCHECK(s_rsz[1] == 'i'); TCHECK(s_rsz[2] == 'x'); TCHECK(s_rsz[3] == 'x'); TCHECK(s_rsz[4] == 'x'); - // --- Resize to smaller (no-op if refCount==1 and capacity sufficient) --- + // --- Resize to smaller (truncates length) --- lString8 s_rsz2 {"hello world"}; s_rsz2.resize(3); TCHECK(s_rsz2.length() == 3); + TCHECK(s_rsz2[0] == 'h'); + TCHECK(s_rsz2[1] == 'e'); + TCHECK(s_rsz2[2] == 'l'); // --- Resize empty string --- lString8 s_rsz3; @@ -295,6 +300,64 @@ void test_lstring8() { // s_move_src is now in moved-from state DUMP_ALLOC_STATS("after compare") + + // --- Reserve on empty string --- + lString8 s_rsv_empty; + TCHECK(s_rsv_empty.empty()); + TCHECK(s_rsv_empty.capacity() == 0); + s_rsv_empty.reserve(0); + TCHECK(s_rsv_empty.empty()); + TCHECK(s_rsv_empty.capacity() == 0); + + s_rsv_empty.reserve(100); + TCHECK(s_rsv_empty.empty()); + TCHECK(s_rsv_empty.length() == 0); + TCHECK(s_rsv_empty.capacity() >= 100); + + // --- Reserve on non-empty owned string --- + lString8 s_rsv_own {"hello"}; + size_t cap_before = s_rsv_own.capacity(); + s_rsv_own.reserve(3); + TCHECK(s_rsv_own.length() == 5); + TCHECK(s_rsv_own == "hello"); + TCHECK(s_rsv_own.capacity() == cap_before); // no shrink + + s_rsv_own.reserve(cap_before); + TCHECK(s_rsv_own.capacity() == cap_before); // exact match, no realloc + + s_rsv_own.reserve(cap_before + 50); + TCHECK(s_rsv_own.length() == 5); + TCHECK(s_rsv_own == "hello"); + TCHECK(s_rsv_own.capacity() >= cap_before + 50); + + // --- Reserve on shared string (forces copy) --- + lString8 s_rsv_src {"shared"}; + lString8 s_rsv_copy = s_rsv_src; + TCHECK(s_rsv_src.length() == 6); + TCHECK(s_rsv_copy.length() == 6); + s_rsv_copy.reserve(100); + TCHECK(s_rsv_copy.capacity() >= 100); + TCHECK(s_rsv_copy.length() == 6); + TCHECK(s_rsv_copy == "shared"); + // original unchanged + TCHECK(s_rsv_src.length() == 6); + TCHECK(s_rsv_src == "shared"); + + // --- Reserve on shared string with requested size < len --- + lString8 s_rsv_src2 {"longer string"}; + lString8 s_rsv_copy2 = s_rsv_src2; + s_rsv_copy2.reserve(1); + TCHECK(s_rsv_copy2.length() == 13); + TCHECK(s_rsv_copy2 == "longer string"); + TCHECK(s_rsv_copy2.capacity() >= 13); + + // --- Reserve(0) on non-empty string --- + lString8 s_rsv_nz {"test"}; + s_rsv_nz.reserve(0); + TCHECK(s_rsv_nz.length() == 4); + TCHECK(s_rsv_nz == "test"); + // capacity should not shrink + TCHECK(s_rsv_nz.capacity() >= 4); } void test_lstring2() { diff --git a/tests/src/stringtest.cpp b/tests/src/stringtest.cpp index 2f3fef92cf..b819e80fbf 100644 --- a/tests/src/stringtest.cpp +++ b/tests/src/stringtest.cpp @@ -1135,10 +1135,23 @@ void testStrings32() { } void testStringsMixed() { + printf("testStringsMixed()\n"); + // --- Resize --- + lString8 s_rsz {"hi"}; + s_rsz.resize(5, 'x'); + TCHECK(s_rsz.length() == 5); + TCHECK(s_rsz[2] == 'x'); + TCHECK(s_rsz[3] == 'x'); + TCHECK(s_rsz[4] == 'x'); + + // --- Resize to smaller (truncates length) --- + lString8 s_rsz2 {"hello world"}; + s_rsz2.resize(3); + TCHECK(s_rsz2.length() == 3); } -bool test_newstring_only = true; +bool test_newstring_only = false; void testStrings() { lv::test_lstring2(); @@ -1147,6 +1160,8 @@ void testStrings() { return; } + printf("test classic lStringX\n"); + testStrings8_16(); testStrings8(); testStrings16(); From cf0c24bc30a9059c5b276faf27b592e770e2d7dd Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 18:05:50 +0100 Subject: [PATCH 15/36] extend lvstring2 impl with c_str() data() lock() modify() --- tests/src/lvstring2.h | 69 +++++++++++++++++++ tests/src/lvstring2test.cpp | 129 ++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 9fd2d818e6..4df47a33a5 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -637,6 +637,75 @@ class string { } } + /// return C-style null-terminated string pointer + const char_type * c_str() const noexcept { + if (pchunk == nullptr) { + return reinterpret_cast(&fake_null_buffer_32); + } else { + // enforce null-termination + const_cast(pchunk)->buf[pchunk->len] = 0; + return pchunk->buf; + } + } + + /// return C-style null-terminated string pointer + const char_type * data() const noexcept { + return c_str(); + } + + /// return modifable C-style null-terminated string pointer; for empty string returns pointer to fake empty z-string buffer + char_type * data() noexcept { + if (pchunk == nullptr) { + return reinterpret_cast(&fake_null_buffer_32); + } else { + lock(pchunk->len); + // enforce null-termination + const_cast(pchunk)->buf[pchunk->len] = 0; + return pchunk->buf; + } + return pchunk->buf; + } + + /// ensures that reference count is 1; if string is null or we own it, do nothing + void lock( size_type newsize ) noexcept { + if (pchunk) { + // string is not null + if (pchunk->getRefCount() != 1) { + // if we don't own string, make a copy + if (newsize < pchunk->len) { + // ensure all chars from old string were copied + newsize = pchunk->len; + } + chunk_t * tmp = chunk_t::alloc(newsize); + tmp->len = pchunk->len; + std::memcpy(tmp->buf, pchunk->buf, tmp->len); + tmp->buf[tmp->len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + } + + /// returns pointer to modifable string buffer + char_type * modify() noexcept { + if (!pchunk) { + // allocate small buffer + pchunk = chunk_t::alloc(8); + } else { + // non-empty string + // string is not null + if (pchunk->getRefCount() != 1) { + chunk_t * tmp = chunk_t::alloc(pchunk->len); + tmp->len = pchunk->len; + std::memcpy(tmp->buf, pchunk->buf, tmp->len); + tmp->buf[tmp->len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + return pchunk->buf; + } + /// returns true if string is empty bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } /// returns character count diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index ebea2ecdf1..54013c8ca8 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -358,6 +358,135 @@ void test_lstring8() { TCHECK(s_rsv_nz == "test"); // capacity should not shrink TCHECK(s_rsv_nz.capacity() >= 4); + + // --- c_str() on empty string --- + lString8 s_cstr_empty; + const char * p_empty = s_cstr_empty.c_str(); + TCHECK(p_empty != nullptr); + TCHECK(p_empty[0] == 0); + + // --- c_str() on non-empty string --- + lString8 s_cstr {"hello"}; + const char * p_cstr = s_cstr.c_str(); + TCHECK(p_cstr != nullptr); + TCHECK(strcmp(p_cstr, "hello") == 0); + TCHECK(p_cstr[5] == 0); + + // --- c_str() enforces null-termination --- + lString8 s_cstr2 {"abc"}; + // Manually corrupt the terminator (simulating a bug) + // c_str() should restore it + const char * p_cstr2 = s_cstr2.c_str(); + TCHECK(p_cstr2[3] == 0); + + // --- data() const on empty string --- + lString8 s_data_empty; + const char * p_dempty = s_data_empty.data(); + TCHECK(p_dempty != nullptr); + TCHECK(p_dempty[0] == 0); + TCHECK(p_dempty == s_data_empty.c_str()); + + // --- data() const on non-empty string --- + lString8 s_data {"world"}; + const char * p_data = s_data.data(); + TCHECK(p_data != nullptr); + TCHECK(strcmp(p_data, "world") == 0); + TCHECK(p_data == s_data.c_str()); + + // --- data() non-const on empty string --- + lString8 s_data_nc_empty; + char * p_nc_empty = s_data_nc_empty.data(); + TCHECK(p_nc_empty != nullptr); + TCHECK(p_nc_empty[0] == 0); + + // --- data() non-const on non-empty owned string --- + lString8 s_data_nc {"modify"}; + char * p_nc = s_data_nc.data(); + TCHECK(p_nc != nullptr); + TCHECK(strcmp(p_nc, "modify") == 0); + p_nc[0] = 'M'; + TCHECK(s_data_nc == "Modify"); + TCHECK(s_data_nc.length() == 6); + + // --- data() non-const on shared string (triggers lock/copy) --- + lString8 s_data_shared {"shared"}; + lString8 s_data_copy = s_data_shared; + TCHECK(s_data_shared == "shared"); + TCHECK(s_data_copy == "shared"); + char * p_shared = s_data_copy.data(); + TCHECK(p_shared != nullptr); + TCHECK(strcmp(p_shared, "shared") == 0); + p_shared[0] = 'S'; + TCHECK(s_data_copy == "Shared"); + // original must be unchanged + TCHECK(s_data_shared == "shared"); + + // --- data() non-const after multiple shares --- + lString8 s_multi {"multi"}; + lString8 s_m1 = s_multi; + lString8 s_m2 = s_multi; + lString8 s_m3 = s_multi; + char * p_multi = s_m2.data(); + TCHECK(strcmp(p_multi, "multi") == 0); + p_multi[0] = 'X'; + TCHECK(s_m2 == "Xulti"); + TCHECK(s_multi == "multi"); + TCHECK(s_m1 == "multi"); + TCHECK(s_m3 == "multi"); + + // --- lock() on null string (no-op) --- + lString8 s_lock_null; + s_lock_null.lock(100); + TCHECK(s_lock_null.empty()); + TCHECK(s_lock_null.capacity() == 0); + + // --- lock() on owned string (no-op when refCount==1) --- + lString8 s_lock_own {"owned"}; + size_t cap_lock_before = s_lock_own.capacity(); + s_lock_own.lock(50); + TCHECK(s_lock_own == "owned"); + TCHECK(s_lock_own.length() == 5); + TCHECK(s_lock_own.capacity() == cap_lock_before); // no realloc + + // --- lock() on shared string with newsize >= len --- + lString8 s_lock_src {"locktest"}; + lString8 s_lock_cpy = s_lock_src; + TCHECK(s_lock_src.length() == 8); + TCHECK(s_lock_cpy.length() == 8); + s_lock_cpy.lock(20); + TCHECK(s_lock_cpy == "locktest"); + TCHECK(s_lock_cpy.length() == 8); + TCHECK(s_lock_cpy.capacity() >= 20); + // original unchanged + TCHECK(s_lock_src == "locktest"); + TCHECK(s_lock_src.length() == 8); + + // --- lock() on shared string with newsize < len (ensures full copy) --- + lString8 s_lock_src2 {"longstring"}; + lString8 s_lock_cpy2 = s_lock_src2; + s_lock_cpy2.lock(3); + TCHECK(s_lock_cpy2 == "longstring"); + TCHECK(s_lock_cpy2.length() == 10); + TCHECK(s_lock_cpy2.capacity() >= 10); + // original unchanged + TCHECK(s_lock_src2 == "longstring"); + + // --- lock() on already-owned string with newsize < len (no-op) --- + lString8 s_lock_own2 {"short"}; + s_lock_own2.lock(2); + TCHECK(s_lock_own2 == "short"); + TCHECK(s_lock_own2.length() == 5); + + // --- data() non-const after lock() --- + lString8 s_lock_data {"before"}; + lString8 s_lock_data2 = s_lock_data; + s_lock_data2.lock(50); + char * p_ld = s_lock_data2.data(); + TCHECK(p_ld != nullptr); + TCHECK(strcmp(p_ld, "before") == 0); + p_ld[0] = 'A'; + TCHECK(s_lock_data2 == "Aefore"); + TCHECK(s_lock_data == "before"); } void test_lstring2() { From 80834bf30f534b6016c7cfa1997d59abb9f550e6 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 18:14:48 +0100 Subject: [PATCH 16/36] add getFirstChar() / getLastChar() --- tests/src/lvstring2.h | 18 ++++++++++++++++++ tests/src/lvstring2test.cpp | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 4df47a33a5..a3dd9febfe 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -717,6 +717,24 @@ class string { /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } + /// returns last character from string, or 0 for empty string + char_type lastChar() const noexcept { + if (pchunk && pchunk->len) { + return pchunk->buf[pchunk->len-1]; + } else { + return 0; + } + } + + /// returns first character from string, or 0 for empty string + char_type firstChar() const noexcept { + if (pchunk && pchunk->len) { + return pchunk->buf[0]; + } else { + return 0; + } + } + private: chunk_t * pchunk {nullptr}; }; diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 54013c8ca8..e844ebc904 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -487,6 +487,43 @@ void test_lstring8() { p_ld[0] = 'A'; TCHECK(s_lock_data2 == "Aefore"); TCHECK(s_lock_data == "before"); + + // --- firstChar() / lastChar() on non-empty string --- + lString8 s_fl {"abc"}; + TCHECK(s_fl.firstChar() == 'a'); + TCHECK(s_fl.lastChar() == 'c'); + + // --- firstChar() / lastChar() on single-char string --- + lString8 s_one2 {"x"}; + TCHECK(s_one2.firstChar() == 'x'); + TCHECK(s_one2.lastChar() == 'x'); + + // --- firstChar() / lastChar() on empty string --- + lString8 s_empty_fl; + TCHECK(s_empty_fl.firstChar() == 0); + TCHECK(s_empty_fl.lastChar() == 0); + + // --- firstChar() / lastChar() on shared string --- + lString8 s_fl_shared {"shared"}; + lString8 s_fl_copy = s_fl_shared; + TCHECK(s_fl_shared.firstChar() == 's'); + TCHECK(s_fl_shared.lastChar() == 'd'); + TCHECK(s_fl_copy.firstChar() == 's'); + TCHECK(s_fl_copy.lastChar() == 'd'); + + // --- firstChar() / lastChar() after modification --- + lString8 s_fl_mod {"hello"}; + char * p_fl = s_fl_mod.data(); + p_fl[0] = 'H'; + p_fl[4] = 'O'; + TCHECK(s_fl_mod.firstChar() == 'H'); + TCHECK(s_fl_mod.lastChar() == 'O'); + + // --- firstChar() / lastChar() with embedded null --- + lString8 s_fl_null {"ab\0cd", 5, 5}; + TCHECK(s_fl_null.firstChar() == 'a'); + TCHECK(s_fl_null.lastChar() == 'd'); + TCHECK(s_fl_null.length() == 5); } void test_lstring2() { From 3838090a142354bbb6574ef1610dbc1ca3f9ac21 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 18:37:02 +0100 Subject: [PATCH 17/36] fragment assignments, typedefs --- tests/src/lvstring2.h | 59 ++++++++++++++++++++---- tests/src/lvstring2test.cpp | 89 +++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index a3dd9febfe..4d72c82c1f 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -157,7 +157,7 @@ inline int str_cmp_nonempty(const char_type * s1, const char_type * s2) { // String data buffer with ref count // forward declaration of string class -template +template class string; template @@ -166,7 +166,7 @@ struct lstring_chunk_t { // friend class lString8; // friend class lString32; // friend struct lstring_chunk_slice_t; - friend class string; + friend class string; friend void test_lstring2_chunks(); /// chunk allocation alignment in bytes @@ -282,19 +282,36 @@ struct lstring_chunk_t { extern lChar32 fake_null_buffer_32; -template +template class string { +public: + // typedefs for STL compatibility + typedef char_type value_type; ///< character type + typedef lUInt32 size_type; ///< size type + typedef lInt32 difference_type; ///< difference type + typedef value_type * pointer; ///< pointer to char type + typedef value_type & reference; ///< reference to char type + typedef const value_type * const_pointer; ///< pointer to const char type + typedef const value_type & const_reference; ///< reference to const char type +private: using chunk_t = lstring_chunk_t; public: string() noexcept = default; - explicit string(const char_type* s, size_type count, size_type reserved) noexcept { + string(const char_type* s, size_type count, size_type reserved) noexcept { if (count == 0 || s == nullptr || s[0] == 0) { pchunk = nullptr; } else { pchunk = chunk_t::alloc(s, count, reserved); } } + string(const char_type* s, size_type count) noexcept { + if (count == 0 || s == nullptr || s[0] == 0) { + pchunk = nullptr; + } else { + pchunk = chunk_t::alloc(s, count, count); + } + } explicit string(const char_type* s) noexcept { if (s != nullptr && *s) { pchunk = chunk_t::alloc(s, str_len(s), 0); @@ -314,6 +331,17 @@ class string { pchunk = s.pchunk; s.pchunk = nullptr; } + /// fragment constructor + string(const string&s, size_type offset, size_type count) noexcept { + if (s.pchunk && offset < s.pchunk->len) { + size_type avail = s.pchunk->len - offset; + if (count > avail) count = avail; + pchunk = chunk_t::alloc(s.pchunk->buf + offset, count, count); + } else { + pchunk = nullptr; + } + } + /// destructor - frees reference ~string() noexcept { if (pchunk != nullptr) { intrusive_ptr_release(pchunk); @@ -375,6 +403,22 @@ class string { return *this; } + /// fragment assignment; correctly covers self-assignment; doesn't check bounds + string& assign(const string&s, size_type offset, size_type count) noexcept { + if (s.pchunk && offset < s.pchunk->len) { + size_type avail = s.pchunk->len - offset; + if (count > avail) count = avail; + chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } else { + clear(); + } + return *this; + } + /// assign from z-terminated string string& operator = (const char_type * s) noexcept { return assign(s); @@ -663,7 +707,6 @@ class string { const_cast(pchunk)->buf[pchunk->len] = 0; return pchunk->buf; } - return pchunk->buf; } /// ensures that reference count is 1; if string is null or we own it, do nothing @@ -744,9 +787,9 @@ typedef lstring_chunk_t lstring8_chunk_t; typedef lstring_chunk_t lstring16_chunk_t; typedef lstring_chunk_t lstring32_chunk_t; -typedef string lString8; -typedef string lString16; -typedef string lString32; +typedef string lString8; +typedef string lString16; +typedef string lString32; } diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index e844ebc904..d485458adb 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -524,6 +524,95 @@ void test_lstring8() { TCHECK(s_fl_null.firstChar() == 'a'); TCHECK(s_fl_null.lastChar() == 'd'); TCHECK(s_fl_null.length() == 5); + + // --- Fragment constructor --- + lString8 s_frag_src {"hello world"}; + lString8 s_frag {s_frag_src, 6, 5}; + TCHECK(s_frag == "world"); + TCHECK(s_frag.length() == 5); + TCHECK(s_frag.capacity() >= 5); + + // --- Fragment constructor: offset beyond length --- + lString8 s_frag_off {s_frag_src, 100, 5}; + TCHECK(s_frag_off.empty()); + TCHECK(s_frag_off.length() == 0); + + // --- Fragment constructor: count exceeds remaining --- + lString8 s_frag_cnt {s_frag_src, 6, 100}; + TCHECK(s_frag_cnt == "world"); + TCHECK(s_frag_cnt.length() == 5); + + // --- Fragment constructor: empty source --- + lString8 s_frag_empty_src; + lString8 s_frag_empty {s_frag_empty_src, 0, 5}; + TCHECK(s_frag_empty.empty()); + + // --- Fragment constructor: zero count --- + lString8 s_frag_zero {s_frag_src, 0, 0}; + TCHECK(s_frag_zero.empty()); + + // --- Fragment assignment --- + lString8 s_fassign_src {"abcdef"}; + lString8 s_fassign {"initial"}; + s_fassign.assign(s_fassign_src, 2, 3); + TCHECK(s_fassign == "cde"); + TCHECK(s_fassign.length() == 3); + + // --- Fragment assignment: self-assignment --- + lString8 s_fassign_self {"selfassign"}; + s_fassign_self.assign(s_fassign_self, 4, 6); + TCHECK(s_fassign_self == "assign"); + + // --- Fragment assignment: offset beyond length --- + lString8 s_fassign_off {"short"}; + s_fassign_off.assign(s_fassign_off, 100, 5); + TCHECK(s_fassign_off.empty()); + + // --- Fragment assignment: count exceeds remaining --- + lString8 s_fassign_cnt {"hello"}; + s_fassign_cnt.assign(s_fassign_cnt, 2, 100); + TCHECK(s_fassign_cnt == "llo"); + TCHECK(s_fassign_cnt.length() == 3); + + // --- Fragment assignment: empty source --- + lString8 s_fassign_empty_src; + lString8 s_fassign_empty {"notempty"}; + s_fassign_empty.assign(s_fassign_empty_src, 0, 5); + TCHECK(s_fassign_empty.empty()); + + // --- modify() on empty string --- + lString8 s_mod_empty; + char * p_mod_empty = s_mod_empty.modify(); + TCHECK(p_mod_empty != nullptr); + p_mod_empty[0] = 'H'; + p_mod_empty[1] = 'i'; + p_mod_empty[2] = 0; + TCHECK(s_mod_empty.length() == 0); // modify doesn't change length + TCHECK(s_mod_empty.capacity() >= 8); + + // --- modify() on owned string --- + lString8 s_mod_own {"owned"}; + char * p_mod_own = s_mod_own.modify(); + TCHECK(strcmp(p_mod_own, "owned") == 0); + p_mod_own[0] = 'O'; + TCHECK(s_mod_own == "Owned"); + + // --- modify() on shared string (triggers copy) --- + lString8 s_mod_shared {"shared"}; + lString8 s_mod_copy = s_mod_shared; + char * p_mod_shared = s_mod_copy.modify(); + TCHECK(strcmp(p_mod_shared, "shared") == 0); + p_mod_shared[0] = 'S'; + TCHECK(s_mod_copy == "Shared"); + TCHECK(s_mod_shared == "shared"); // original unchanged + + // --- modify() after modification --- + lString8 s_mod_chain {"chain"}; + char * p_mod1 = s_mod_chain.modify(); + p_mod1[0] = 'C'; + char * p_mod2 = s_mod_chain.modify(); + p_mod2[1] = 'H'; + TCHECK(s_mod_chain == "CHain"); } void test_lstring2() { From 1ddc318632649c736fd82c8c0e25923068be27e2 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 18:44:20 +0100 Subject: [PATCH 18/36] reserved size constructor --- tests/src/lvstring2.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 4d72c82c1f..18536007fa 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -325,6 +325,10 @@ class string { pchunk = s.pchunk; intrusive_ptr_add_ref(pchunk); } + /// constructor of empty buffer with reserved size + string(size_type size) noexcept { + pchunk = chunk_t::alloc(size); + } /// move constructor string(string&&s ) noexcept { LS_COUNT_MOVE_CONSTR From c9e358e6cede1efa7b51dcb5f48aa3ba22594840 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 19:06:23 +0100 Subject: [PATCH 19/36] implement erase --- tests/src/lvstring2.h | 34 ++++++++++++++++ tests/src/lvstring2test.cpp | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 18536007fa..5288310356 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -685,6 +685,40 @@ class string { } } + /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated + void erase(size_type offset, size_type count) { + if (pchunk && offset < pchunk->len && count) { + if (offset + count > pchunk->len) { + // truncate requested erased fragment size to not exceed bounds + count = pchunk->len - offset; + } + size_type new_len = pchunk->len - count; + size_type tail_start = offset + count; + size_type tail_len = pchunk->len - tail_start; + if (pchunk->getRefCount() == 1) { + // erase inplace + if (tail_len) { + std::memmove(pchunk->buf + offset, pchunk->buf + tail_start, sizeof(char_type) * tail_len); + } + } else { + // create a copy with fragment erased + chunk_t * tmp = chunk_t::alloc(new_len); + if (offset) { + // something left in beginning of string + std::memcpy(tmp->buf, pchunk->buf, sizeof(char_type) * offset); + } + if (tail_len) { + std::memcpy(tmp->buf + offset, pchunk->buf + tail_start, sizeof(char_type) * tail_len); + } + // free old ref and use copy + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + pchunk->len = new_len; + pchunk->buf[pchunk->len] = 0; + } + } + /// return C-style null-terminated string pointer const char_type * c_str() const noexcept { if (pchunk == nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index d485458adb..21ac6c3468 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -613,6 +613,86 @@ void test_lstring8() { char * p_mod2 = s_mod_chain.modify(); p_mod2[1] = 'H'; TCHECK(s_mod_chain == "CHain"); + + // --- erase() from middle of owned string --- + lString8 s_erase_mid {"hello world"}; + s_erase_mid.erase(5, 6); + TCHECK(s_erase_mid == "hello"); + TCHECK(s_erase_mid.length() == 5); + + // --- erase() from beginning of owned string --- + lString8 s_erase_beg {"hello world"}; + s_erase_beg.erase(0, 6); + TCHECK(s_erase_beg == "world"); + TCHECK(s_erase_beg.length() == 5); + + // --- erase() from end of owned string --- + lString8 s_erase_end {"hello world"}; + s_erase_end.erase(5, 100); + TCHECK(s_erase_end == "hello"); + TCHECK(s_erase_end.length() == 5); + + // --- erase() entire string --- + lString8 s_erase_all {"abc"}; + s_erase_all.erase(0, 3); + TCHECK(s_erase_all.empty()); + TCHECK(s_erase_all.length() == 0); + + // --- erase() with zero count (no-op) --- + lString8 s_erase_zero {"nochange"}; + s_erase_zero.erase(2, 0); + TCHECK(s_erase_zero == "nochange"); + + // --- erase() with offset beyond length (no-op) --- + lString8 s_erase_off {"short"}; + s_erase_off.erase(100, 5); + TCHECK(s_erase_off == "short"); + + // --- erase() on empty string (no-op) --- + lString8 s_erase_empty; + s_erase_empty.erase(0, 5); + TCHECK(s_erase_empty.empty()); + + // --- erase() on shared string (triggers COW copy) --- + lString8 s_erase_shared {"abcdef"}; + lString8 s_erase_copy = s_erase_shared; + s_erase_copy.erase(2, 2); + TCHECK(s_erase_copy == "abef"); + TCHECK(s_erase_copy.length() == 4); + // original unchanged + TCHECK(s_erase_shared == "abcdef"); + TCHECK(s_erase_shared.length() == 6); + + // --- erase() single char from middle --- + lString8 s_erase_one {"abcde"}; + s_erase_one.erase(2, 1); + TCHECK(s_erase_one == "abde"); + + // --- erase() with overlapping tail (memmove test) --- + lString8 s_erase_overlap {"0123456789"}; + s_erase_overlap.erase(0, 5); + TCHECK(s_erase_overlap == "56789"); + + // --- erase() on shared string, erasing from beginning --- + lString8 s_erase_sh_beg {"hello world"}; + lString8 s_erase_sh_beg2 = s_erase_sh_beg; + s_erase_sh_beg2.erase(0, 6); + TCHECK(s_erase_sh_beg2 == "world"); + TCHECK(s_erase_sh_beg == "hello world"); + + // --- erase() on shared string, erasing from end --- + lString8 s_erase_sh_end {"hello world"}; + lString8 s_erase_sh_end2 = s_erase_sh_end; + s_erase_sh_end2.erase(5, 6); + TCHECK(s_erase_sh_end2 == "hello"); + TCHECK(s_erase_sh_end == "hello world"); + + // --- erase() on shared string, erasing entire content --- + lString8 s_erase_sh_all {"abc"}; + lString8 s_erase_sh_all2 = s_erase_sh_all; + s_erase_sh_all2.erase(0, 3); + TCHECK(s_erase_sh_all2.empty()); + TCHECK(s_erase_sh_all == "abc"); } void test_lstring2() { From c3f19b1dd759e40428cbe0e27207bdf996763b08 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 19:49:36 +0100 Subject: [PATCH 20/36] implement more append() methods --- tests/src/lvstring2.h | 97 ++++++++++++++++++++- tests/src/lvstring2test.cpp | 169 ++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 1 deletion(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 5288310356..b8a61d516f 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -686,7 +686,7 @@ class string { } /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated - void erase(size_type offset, size_type count) { + void erase(size_type offset, size_type count) noexcept { if (pchunk && offset < pchunk->len && count) { if (offset + count > pchunk->len) { // truncate requested erased fragment size to not exceed bounds @@ -719,6 +719,101 @@ class string { } } + /// append fragment from null-terminated string; no validation of input string is performed + string& append(const char_type* s, size_type count) noexcept { + if (count) { + if (!pchunk) { + pchunk = chunk_t::alloc(s, count, count); + } else { + size_type new_len = pchunk->len + count; + if (pchunk->getRefCount() != 1 || pchunk->size < new_len) { + // need new buffer: either shared or not enough capacity + // if s points to our own buffer, save data before releasing + bool self_append = (s == pchunk->buf); + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); + if (self_append) { + // s was our old buffer, now freed; copy from the new tmp which has the old data + std::memcpy(tmp->buf + tmp->len, tmp->buf, sizeof(char_type) * count); + } else { + std::memcpy(tmp->buf + tmp->len, s, sizeof(char_type) * count); + } + tmp->len = new_len; + tmp->buf[new_len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + std::memmove(pchunk->buf + pchunk->len, s, sizeof(char_type) * count); + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + } + } + return *this; + } + + /// append null-terminated string + string& append(const char_type* s) noexcept { + if (!s || !*s) { + return *this; + } + size_type count = str_len(s); + if (count) { + append(s, count); + } + return *this; + } + + /// append another string + string& append(const string& s) noexcept { + if (!s.empty()) { + return append(s.pchunk->buf, s.pchunk->len); + } + return *this; + } + + /// append fragment of another string + string& append(const string& s, size_type offset, size_type count) noexcept { + if (!s.empty() && offset < s.pchunk->len) { + if (offset + count > s.pchunk->len) { + count = s.pchunk->len - offset; + } + return append(s.pchunk->buf + offset, count); + } + return *this; + } + + /// append one or more characters + string& append(size_type count, char_type c) noexcept { + if (pchunk) { + size_type new_len = pchunk->len + count; + if (pchunk->getRefCount() != 1 || pchunk->size < new_len) { + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); + for (size_type i = 0; i < count; i++) { + tmp->buf[pchunk->len + i] = c; + } + tmp->len = new_len; + tmp->buf[new_len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // own buffer with enough capacity + for (size_type i = 0; i < count; i++) { + pchunk->buf[pchunk->len + i] = c; + } + pchunk->buf[new_len] = 0; + } + } else { + // appending to empty string + pchunk = chunk_t::alloc(count); + for (size_type i = 0; i < count; i++) { + pchunk->buf[i] = c; + } + pchunk->len = count; + pchunk->buf[count] = 0; + } + return *this; + } + /// return C-style null-terminated string pointer const char_type * c_str() const noexcept { if (pchunk == nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 21ac6c3468..48314fe455 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -693,6 +693,175 @@ void test_lstring8() { s_erase_sh_all2.erase(0, 3); TCHECK(s_erase_sh_all2.empty()); TCHECK(s_erase_sh_all == "abc"); + + // --- append(const char*) on empty string --- + lString8 s_app_empty; + s_app_empty.append("hello"); + TCHECK(s_app_empty == "hello"); + TCHECK(s_app_empty.length() == 5); + + // --- append(const char*) on non-empty owned string --- + lString8 s_app_own {"hello"}; + s_app_own.append(" world"); + TCHECK(s_app_own == "hello world"); + TCHECK(s_app_own.length() == 11); + + // --- append(const char*) with nullptr --- + lString8 s_app_null {"test"}; + s_app_null.append(nullptr); + TCHECK(s_app_null == "test"); + + // --- append(const char*) with empty string --- + lString8 s_app_emptystr {"test"}; + s_app_emptystr.append(""); + TCHECK(s_app_emptystr == "test"); + + // --- append(const char*, count) on empty string --- + lString8 s_app_fc_empty; + s_app_fc_empty.append("hello world", 5); + TCHECK(s_app_fc_empty == "hello"); + TCHECK(s_app_fc_empty.length() == 5); + + // --- append(const char*, count) with zero count --- + lString8 s_app_fc_zero {"test"}; + s_app_fc_zero.append("ignored", 0); + TCHECK(s_app_fc_zero == "test"); + + // --- append(const char*, count) on shared string (COW) --- + lString8 s_app_fc_shared {"base"}; + lString8 s_app_fc_copy = s_app_fc_shared; + s_app_fc_copy.append(" extended", 9); + TCHECK(s_app_fc_copy == "base extended"); + TCHECK(s_app_fc_copy.length() == 13); + TCHECK(s_app_fc_shared == "base"); + + // --- append(const char*, count) when capacity is insufficient --- + lString8 s_app_fc_cap {"short"}; + s_app_fc_cap.reserve(5); // ensure small capacity + s_app_fc_cap.append(" much longer string", 19); + TCHECK(s_app_fc_cap == "short much longer string"); + TCHECK(s_app_fc_cap.length() == 24); + + // --- append(const char*) chaining --- + lString8 s_app_chain; + s_app_chain.append("a"); + s_app_chain.append("b"); + s_app_chain.append("c"); + TCHECK(s_app_chain == "abc"); + + // --- append(const char*, count) self-append --- + lString8 s_app_self {"ab"}; + s_app_self.append(s_app_self.c_str(), 2); + TCHECK(s_app_self == "abab"); + + // --- append(const string&) on empty string --- + lString8 s_app_s_empty; + lString8 s_app_s_src {"hello"}; + s_app_s_empty.append(s_app_s_src); + TCHECK(s_app_s_empty == "hello"); + + // --- append(const string&) on non-empty string --- + lString8 s_app_s_own {"hello "}; + s_app_s_own.append(s_app_s_src); + TCHECK(s_app_s_own == "hello hello"); + + // --- append(const string&) with empty source --- + lString8 s_app_s_empty_src; + lString8 s_app_s_nochange {"test"}; + s_app_s_nochange.append(s_app_s_empty_src); + TCHECK(s_app_s_nochange == "test"); + + // --- append(const string&) self-append --- + lString8 s_app_s_self {"ab"}; + s_app_s_self.append(s_app_s_self); + TCHECK(s_app_s_self == "abab"); + + // --- append(const string&) self-append multiple times --- + lString8 s_app_s_multi {"x"}; + s_app_s_multi.append(s_app_s_multi); + TCHECK(s_app_s_multi == "xx"); + s_app_s_multi.append(s_app_s_multi); + TCHECK(s_app_s_multi == "xxxx"); + + // --- append(const string&) with insufficient capacity --- + lString8 s_app_s_cap {"short"}; + s_app_s_cap.reserve(5); + lString8 s_app_s_long {" much longer string"}; + s_app_s_cap.append(s_app_s_long); + TCHECK(s_app_s_cap == "short much longer string"); + + // --- append(const string&, offset, count) normal --- + lString8 s_app_sf_src {"hello world"}; + lString8 s_app_sf {"start "}; + s_app_sf.append(s_app_sf_src, 6, 5); + TCHECK(s_app_sf == "start world"); + + // --- append(const string&, offset, count) self-append --- + lString8 s_app_sf_self {"abcd"}; + s_app_sf_self.append(s_app_sf_self, 0, 4); + TCHECK(s_app_sf_self == "abcdabcd"); + + // --- append(const string&, offset, count) self-append fragment --- + lString8 s_app_sf_self2 {"abcd"}; + s_app_sf_self2.append(s_app_sf_self2, 1, 2); + TCHECK(s_app_sf_self2 == "abcdbc"); + + // --- append(const string&, offset, count) count exceeds remaining --- + lString8 s_app_sf_clamp {"hello"}; + lString8 s_app_sf_clap {"start"}; + s_app_sf_clap.append(s_app_sf_clamp, 3, 100); + TCHECK(s_app_sf_clap == "startlo"); + + // --- append(const string&, offset, count) offset beyond length --- + lString8 s_app_sf_off {"hello"}; + lString8 s_app_sf_off2 {"start"}; + s_app_sf_off2.append(s_app_sf_off, 100, 5); + TCHECK(s_app_sf_off2 == "start"); + + // --- append(const string&, offset, count) empty source --- + lString8 s_app_sf_empty; + lString8 s_app_sf_empty2 {"test"}; + s_app_sf_empty2.append(s_app_sf_empty, 0, 5); + TCHECK(s_app_sf_empty2 == "test"); + + // --- append(count, char) on empty string --- + lString8 s_app_cc_empty; + s_app_cc_empty.append(5, 'x'); + TCHECK(s_app_cc_empty == "xxxxx"); + TCHECK(s_app_cc_empty.length() == 5); + + // --- append(count, char) on owned string with capacity --- + lString8 s_app_cc_cap {"ab"}; + s_app_cc_cap.reserve(10); + s_app_cc_cap.append(3, 'c'); + TCHECK(s_app_cc_cap == "abccc"); + TCHECK(s_app_cc_cap.length() == 5); + + // --- append(count, char) on owned string without capacity --- + lString8 s_app_cc_nocap {"ab"}; + s_app_cc_nocap.append(5, 'z'); + TCHECK(s_app_cc_nocap == "abzzzzz"); + TCHECK(s_app_cc_nocap.length() == 7); + + // --- append(count, char) on shared string --- + lString8 s_app_cc_shared {"base"}; + lString8 s_app_cc_copy = s_app_cc_shared; + s_app_cc_copy.append(3, '!'); + TCHECK(s_app_cc_copy == "base!!!"); + TCHECK(s_app_cc_shared == "base"); + + // --- append(count, char) with zero count --- + lString8 s_app_cc_zero {"test"}; + s_app_cc_zero.append(0, 'x'); + TCHECK(s_app_cc_zero == "test"); + + // --- append(count, char) with null char --- + lString8 s_app_cc_null {"ab"}; + s_app_cc_null.append(2, '\0'); + TCHECK(s_app_cc_null.length() == 4); + TCHECK(s_app_cc_null[2] == 0); + TCHECK(s_app_cc_null[3] == 0); + TCHECK(s_app_cc_null.c_str()[0] == 'a'); } void test_lstring2() { From 5c3239d40d8dd815ec1e5e67e2652cde26d3f865 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 21:10:46 +0100 Subject: [PATCH 21/36] implement inserts --- tests/src/lvstring2.h | 56 ++++++++++++++++++++++++++++++++ tests/src/lvstring2test.cpp | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index b8a61d516f..8dcf53f0e5 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -800,6 +800,7 @@ class string { for (size_type i = 0; i < count; i++) { pchunk->buf[pchunk->len + i] = c; } + pchunk->len = new_len; pchunk->buf[new_len] = 0; } } else { @@ -814,6 +815,61 @@ class string { return *this; } + /// insert from char* and size + string& insert(size_type pos, const char_type* s, size_type count) { + if (!count) { + return *this; + } + if (!pchunk) { + // insert into empty string + pchunk = chunk_t::alloc(s, count, count); + } else { + if (pos > pchunk->len) { + pos = pchunk->len; + } + size_type new_len = pchunk->len + count; + size_type tail_len = pchunk->len - pos; + if (pchunk->getRefCount() != 1 || new_len > pchunk->size) { + // create copy + chunk_t* tmp = chunk_t::alloc(new_len); + if (pos > 0) { + // copy head + std::memcpy(tmp->buf, pchunk->buf, sizeof(char_type) * pos); + } + // copy inserted content + std::memcpy(tmp->buf + pos, s, sizeof(char_type) * count); + // copy tail if needed + if (tail_len) { + // copy tail + std::memcpy(tmp->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + tmp->len = new_len; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // insert in-place + if (tail_len) { + // move tail by count chars + std::memmove(pchunk->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + // copy inserted content + std::memcpy(pchunk->buf + pos, s, sizeof(char_type) * count); + } + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + return *this; + } + + /// insert from null-terminated cont char* + string& insert(size_type pos, const char_type* s) { + if (!s || !*s) { + // attempt inserting empty string + return *this; + } + return insert(pos, s, str_len(s)); + } + /// return C-style null-terminated string pointer const char_type * c_str() const noexcept { if (pchunk == nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 48314fe455..35334ff628 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -862,6 +862,71 @@ void test_lstring8() { TCHECK(s_app_cc_null[2] == 0); TCHECK(s_app_cc_null[3] == 0); TCHECK(s_app_cc_null.c_str()[0] == 'a'); + + // --- insert(pos, char*, count) into empty string --- + lString8 s_ins_empty; + s_ins_empty.insert(0, "hello", 5); + TCHECK(s_ins_empty == "hello"); + TCHECK(s_ins_empty.length() == 5); + + // --- insert(pos, char*, count) at beginning --- + lString8 s_ins_beg {"world"}; + s_ins_beg.insert(0, "hello ", 6); + TCHECK(s_ins_beg == "hello world"); + + // --- insert(pos, char*, count) in middle --- + lString8 s_ins_mid {"helloworld"}; + s_ins_mid.insert(5, " ", 1); + TCHECK(s_ins_mid == "hello world"); + + // --- insert(pos, char*, count) at end --- + lString8 s_ins_end {"hello"}; + s_ins_end.insert(5, " world", 6); + TCHECK(s_ins_end == "hello world"); + + // --- insert(pos, char*, count) with pos beyond length --- + lString8 s_ins_off {"abc"}; + s_ins_off.insert(100, "xyz", 3); + TCHECK(s_ins_off == "abcxyz"); + + // --- insert(pos, char*, count) with zero count --- + lString8 s_ins_zero {"test"}; + s_ins_zero.insert(2, "ignored", 0); + TCHECK(s_ins_zero == "test"); + + // --- insert(pos, char*, count) on shared string --- + lString8 s_ins_shared {"base"}; + lString8 s_ins_copy = s_ins_shared; + s_ins_copy.insert(2, "XX", 2); + TCHECK(s_ins_copy == "baXXse"); + TCHECK(s_ins_shared == "base"); + + // --- insert(pos, char*, count) with insufficient capacity --- + lString8 s_ins_cap {"ab"}; + s_ins_cap.reserve(2); + s_ins_cap.insert(1, " much longer", 12); + TCHECK(s_ins_cap == "a much longer b"); + TCHECK(s_ins_cap.length() == 14); + + // --- insert(pos, char*) --- + lString8 s_ins_cstr {"helloworld"}; + s_ins_cstr.insert(5, " "); + TCHECK(s_ins_cstr == "hello world"); + + // --- insert(pos, char*) with nullptr --- + lString8 s_ins_null {"test"}; + s_ins_null.insert(2, nullptr); + TCHECK(s_ins_null == "test"); + + // --- insert(pos, char*) with empty string --- + lString8 s_ins_emptystr {"test"}; + s_ins_emptystr.insert(2, ""); + TCHECK(s_ins_emptystr == "test"); + + // --- insert(pos, char*, count) self-insert prefix --- + lString8 s_ins_self {"abcd"}; + s_ins_self.insert(2, s_ins_self.c_str(), 2); + TCHECK(s_ins_self == "ababcdcd"); } void test_lstring2() { From c414332a9e529639545bc6d9b65ea5069e163bb9 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 21:50:35 +0100 Subject: [PATCH 22/36] insert(), pack() --- tests/src/lvstring2.h | 81 +++++++++++++++++++++- tests/src/lvstring2test.cpp | 129 ++++++++++++++++++++++++++++++------ 2 files changed, 186 insertions(+), 24 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 8dcf53f0e5..f724f60351 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -816,7 +816,7 @@ class string { } /// insert from char* and size - string& insert(size_type pos, const char_type* s, size_type count) { + string& insert(size_type pos, const char_type* s, size_type count) noexcept { if (!count) { return *this; } @@ -862,7 +862,7 @@ class string { } /// insert from null-terminated cont char* - string& insert(size_type pos, const char_type* s) { + string& insert(size_type pos, const char_type* s) noexcept { if (!s || !*s) { // attempt inserting empty string return *this; @@ -870,6 +870,83 @@ class string { return insert(pos, s, str_len(s)); } + /// insert from string (no self-insert protection) + string& insert(size_type pos, const string& s) noexcept { + if (s.empty()) { + // attempt inserting empty string + return *this; + } + return insert(pos, s.pchunk->buf, s.pchunk->len); + } + + /// insert one or more characters + string& insert(size_type pos, size_type count, char_type c) noexcept { + if (!count) { + return *this; + } + if (!pchunk) { + // insert into empty string + pchunk = chunk_t::alloc(count); + for (size_type i = 0; i < count; i++) { + pchunk->buf[i] = c; + } + pchunk->len = count; + pchunk->buf[count] = 0; + } else { + if (pos > pchunk->len) { + pos = pchunk->len; + } + size_type new_len = pchunk->len + count; + size_type tail_len = pchunk->len - pos; + if (pchunk->getRefCount() != 1 || new_len > pchunk->size) { + // create copy + chunk_t* tmp = chunk_t::alloc(new_len); + if (pos > 0) { + // copy head + std::memcpy(tmp->buf, pchunk->buf, sizeof(char_type) * pos); + } + // copy inserted content + for (size_type i = 0; i < count; i++) { + tmp->buf[pos + i] = c; + } + // copy tail if needed + if (tail_len) { + // copy tail + std::memcpy(tmp->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + tmp->len = new_len; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // insert in-place + if (tail_len) { + // move tail by count chars + std::memmove(pchunk->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + // copy inserted content + for (size_type i = 0; i < count; i++) { + pchunk->buf[pos + i] = c; + } + } + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + return *this; + } + + /// compact buffer if possible -- free unused buffer space + string& pack() noexcept { + if (pchunk) { + size_type new_size = chunk_t::alignSize(pchunk->len); + if (pchunk->size > new_size) { + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + return *this; + } + /// return C-style null-terminated string pointer const char_type * c_str() const noexcept { if (pchunk == nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 35334ff628..54e7725c57 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -801,61 +801,61 @@ void test_lstring8() { s_app_sf_self.append(s_app_sf_self, 0, 4); TCHECK(s_app_sf_self == "abcdabcd"); - // --- append(const string&, offset, count) self-append fragment --- + // --- append(const string&, offset, count) self-append fragment --- lString8 s_app_sf_self2 {"abcd"}; s_app_sf_self2.append(s_app_sf_self2, 1, 2); TCHECK(s_app_sf_self2 == "abcdbc"); - // --- append(const string&, offset, count) count exceeds remaining --- + // --- append(const string&, offset, count) count exceeds remaining --- lString8 s_app_sf_clamp {"hello"}; lString8 s_app_sf_clap {"start"}; s_app_sf_clap.append(s_app_sf_clamp, 3, 100); TCHECK(s_app_sf_clap == "startlo"); - // --- append(const string&, offset, count) offset beyond length --- + // --- append(const string&, offset, count) offset beyond length --- lString8 s_app_sf_off {"hello"}; lString8 s_app_sf_off2 {"start"}; s_app_sf_off2.append(s_app_sf_off, 100, 5); TCHECK(s_app_sf_off2 == "start"); - // --- append(const string&, offset, count) empty source --- + // --- append(const string&, offset, count) empty source --- lString8 s_app_sf_empty; lString8 s_app_sf_empty2 {"test"}; s_app_sf_empty2.append(s_app_sf_empty, 0, 5); TCHECK(s_app_sf_empty2 == "test"); - // --- append(count, char) on empty string --- + // --- append(count, char) on empty string --- lString8 s_app_cc_empty; s_app_cc_empty.append(5, 'x'); TCHECK(s_app_cc_empty == "xxxxx"); TCHECK(s_app_cc_empty.length() == 5); - // --- append(count, char) on owned string with capacity --- + // --- append(count, char) on owned string with capacity --- lString8 s_app_cc_cap {"ab"}; s_app_cc_cap.reserve(10); s_app_cc_cap.append(3, 'c'); TCHECK(s_app_cc_cap == "abccc"); TCHECK(s_app_cc_cap.length() == 5); - // --- append(count, char) on owned string without capacity --- + // --- append(count, char) on owned string without capacity --- lString8 s_app_cc_nocap {"ab"}; s_app_cc_nocap.append(5, 'z'); TCHECK(s_app_cc_nocap == "abzzzzz"); TCHECK(s_app_cc_nocap.length() == 7); - // --- append(count, char) on shared string --- + // --- append(count, char) on shared string --- lString8 s_app_cc_shared {"base"}; lString8 s_app_cc_copy = s_app_cc_shared; s_app_cc_copy.append(3, '!'); TCHECK(s_app_cc_copy == "base!!!"); TCHECK(s_app_cc_shared == "base"); - // --- append(count, char) with zero count --- + // --- append(count, char) with zero count --- lString8 s_app_cc_zero {"test"}; s_app_cc_zero.append(0, 'x'); TCHECK(s_app_cc_zero == "test"); - // --- append(count, char) with null char --- + // --- append(count, char) with null char --- lString8 s_app_cc_null {"ab"}; s_app_cc_null.append(2, '\0'); TCHECK(s_app_cc_null.length() == 4); @@ -863,52 +863,52 @@ void test_lstring8() { TCHECK(s_app_cc_null[3] == 0); TCHECK(s_app_cc_null.c_str()[0] == 'a'); - // --- insert(pos, char*, count) into empty string --- + // --- insert(pos, char*, count) into empty string --- lString8 s_ins_empty; s_ins_empty.insert(0, "hello", 5); TCHECK(s_ins_empty == "hello"); TCHECK(s_ins_empty.length() == 5); - // --- insert(pos, char*, count) at beginning --- + // --- insert(pos, char*, count) at beginning --- lString8 s_ins_beg {"world"}; s_ins_beg.insert(0, "hello ", 6); TCHECK(s_ins_beg == "hello world"); - // --- insert(pos, char*, count) in middle --- + // --- insert(pos, char*, count) in middle --- lString8 s_ins_mid {"helloworld"}; s_ins_mid.insert(5, " ", 1); TCHECK(s_ins_mid == "hello world"); - // --- insert(pos, char*, count) at end --- + // --- insert(pos, char*, count) at end --- lString8 s_ins_end {"hello"}; s_ins_end.insert(5, " world", 6); TCHECK(s_ins_end == "hello world"); - // --- insert(pos, char*, count) with pos beyond length --- + // --- insert(pos, char*, count) with pos beyond length --- lString8 s_ins_off {"abc"}; s_ins_off.insert(100, "xyz", 3); TCHECK(s_ins_off == "abcxyz"); - // --- insert(pos, char*, count) with zero count --- + // --- insert(pos, char*, count) with zero count --- lString8 s_ins_zero {"test"}; s_ins_zero.insert(2, "ignored", 0); TCHECK(s_ins_zero == "test"); - // --- insert(pos, char*, count) on shared string --- + // --- insert(pos, char*, count) on shared string --- lString8 s_ins_shared {"base"}; lString8 s_ins_copy = s_ins_shared; s_ins_copy.insert(2, "XX", 2); TCHECK(s_ins_copy == "baXXse"); TCHECK(s_ins_shared == "base"); - // --- insert(pos, char*, count) with insufficient capacity --- + // --- insert(pos, char*, count) with insufficient capacity --- lString8 s_ins_cap {"ab"}; s_ins_cap.reserve(2); - s_ins_cap.insert(1, " much longer", 12); + s_ins_cap.insert(1, " much longer ", 13); TCHECK(s_ins_cap == "a much longer b"); - TCHECK(s_ins_cap.length() == 14); + TCHECK(s_ins_cap.length() == 15); - // --- insert(pos, char*) --- + // --- insert(pos, char*) --- lString8 s_ins_cstr {"helloworld"}; s_ins_cstr.insert(5, " "); TCHECK(s_ins_cstr == "hello world"); @@ -926,7 +926,92 @@ void test_lstring8() { // --- insert(pos, char*, count) self-insert prefix --- lString8 s_ins_self {"abcd"}; s_ins_self.insert(2, s_ins_self.c_str(), 2); - TCHECK(s_ins_self == "ababcdcd"); + TCHECK(s_ins_self == "ababcd"); + + // --- insert(pos, string&) --- + lString8 s_ins_str {"helloworld"}; + lString8 s_ins_str_src {" "}; + s_ins_str.insert(5, s_ins_str_src); + TCHECK(s_ins_str == "hello world"); + + // --- insert(pos, string&) with empty source --- + lString8 s_ins_str_empty {"test"}; + lString8 s_ins_str_empty_src; + s_ins_str_empty.insert(2, s_ins_str_empty_src); + TCHECK(s_ins_str_empty == "test"); + + // --- insert(pos, string&) self-insert --- + lString8 s_ins_str_self {"ab"}; + s_ins_str_self.insert(1, s_ins_str_self); + TCHECK(s_ins_str_self == "aabb"); + + // --- insert(pos, count, char) on empty string --- + lString8 s_ins_cc_empty; + s_ins_cc_empty.insert(0, 5, 'x'); + TCHECK(s_ins_cc_empty == "xxxxx"); + + // --- insert(pos, count, char) in middle --- + lString8 s_ins_cc_mid {"ab"}; + s_ins_cc_mid.insert(1, 3, '-'); + TCHECK(s_ins_cc_mid == "a---b"); + + // --- insert(pos, count, char) at end --- + lString8 s_ins_cc_end {"ab"}; + s_ins_cc_end.insert(2, 3, '!'); + TCHECK(s_ins_cc_end == "ab!!!"); + + // --- insert(pos, count, char) pos beyond length --- + lString8 s_ins_cc_off {"ab"}; + s_ins_cc_off.insert(100, 2, 'z'); + TCHECK(s_ins_cc_off == "abzz"); + + // --- insert(pos, count, char) zero count --- + lString8 s_ins_cc_zero {"test"}; + s_ins_cc_zero.insert(2, 0, 'x'); + TCHECK(s_ins_cc_zero == "test"); + + // --- insert(pos, count, char) on shared string --- + lString8 s_ins_cc_shared {"base"}; + lString8 s_ins_cc_copy = s_ins_cc_shared; + s_ins_cc_copy.insert(2, 2, 'X'); + TCHECK(s_ins_cc_copy == "baXXse"); + TCHECK(s_ins_cc_shared == "base"); + + // --- insert(pos, count, char) with insufficient capacity --- + lString8 s_ins_cc_cap {"ab"}; + s_ins_cc_cap.reserve(2); + s_ins_cc_cap.insert(1, 5, '-'); + TCHECK(s_ins_cc_cap == "a-----b"); + + // --- pack() on string with excess capacity --- + lString8 s_pack {"hello"}; + s_pack.reserve(1000); + TCHECK(s_pack.capacity() >= 1000); + s_pack.pack(); + TCHECK(s_pack == "hello"); + TCHECK(s_pack.length() == 5); + TCHECK(s_pack.capacity() < 1000); + + // --- pack() on string with no excess capacity --- + lString8 s_pack_exact {"exact"}; + size_t cap_before2 = s_pack_exact.capacity(); + s_pack_exact.pack(); + TCHECK(s_pack_exact == "exact"); + TCHECK(s_pack_exact.capacity() == cap_before2); + + // --- pack() on empty string --- + lString8 s_pack_empty; + s_pack_empty.pack(); + TCHECK(s_pack_empty.empty()); + + // --- pack() on shared string --- + lString8 s_pack_shared {"shared"}; + lString8 s_pack_copy = s_pack_shared; + s_pack_copy.reserve(100); + s_pack_copy.pack(); + TCHECK(s_pack_copy == "shared"); + TCHECK(s_pack_copy.capacity() < 100); + TCHECK(s_pack_shared == "shared"); } void test_lstring2() { From 6c1f54c422b2fa9dcc7812973176ba59575cf12f Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 22:53:07 +0100 Subject: [PATCH 23/36] replace() --- tests/src/lvstring2.h | 184 ++++++++++++++++++++++++++++++++++++ tests/src/lvstring2test.cpp | 103 ++++++++++++++++++++ 2 files changed, 287 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index f724f60351..d6a941d45a 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -947,6 +947,190 @@ class string { return *this; } + /// replace part of string with count chars from char* + string& replace(size_type pos, size_type n, const char_type * s, size_type count) noexcept { + if (!count) { + return *this; + } + if (empty()) { + return assign(s, count); + } else { + // need to modify + size_type old_len = pchunk->len; + if (pos > old_len) { + pos = old_len; + } + if (pos + n > old_len) { + n = old_len - pos; + } + if (!n) { + // delegate to insert + return insert(pos, s, count); + } + if (pos == old_len) { + // use append + return append(s, count); + } + size_type new_len = old_len + count - n; + size_type tail_len = old_len - (pos + n); + if (new_len > pchunk->size || pchunk->getRefCount() != 1) { + // need to create copy -- no space or has other refs + chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); + // move tail to the end of buffer + std::memmove(tmp->buf + new_len - tail_len, tmp->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + std::memcpy(tmp->buf + pos, s, sizeof(char_type) * count); + // replace this string with new + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // may edit in-place + // move tail to the end of buffer + std::memmove(pchunk->buf + new_len - tail_len, pchunk->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + std::memcpy(pchunk->buf + pos, s, sizeof(char_type) * count); + } + // update length + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + return *this; + } + } + + /// replace part of string with count chars from char* + string& replace(size_type pos, size_type n, size_type count, char_type c) noexcept { + if (!count) { + // nothing to insert + return *this; + } + if (empty()) { + return append(count, c); + } else { + // need to modify + size_type old_len = pchunk->len; + if (pos > old_len) { + pos = old_len; + } + if (pos + n > old_len) { + n = old_len - pos; + } + if (!n) { + // delegate to insert + return insert(pos, count, c); + } + if (pos == old_len) { + // use append + return append(count, c); + } + size_type new_len = old_len + count - n; + size_type tail_len = old_len - (pos + n); + if (new_len > pchunk->size || pchunk->getRefCount() != 1) { + // need to create copy -- no space or has other refs + chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); + // move tail to the end of buffer + std::memmove(tmp->buf + new_len - tail_len, tmp->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + for (size_type i = 0; i < count; i++) { + tmp->buf[pos + i] = c; + } + // replace this string with new + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // may edit in-place + // move tail to the end of buffer + std::memmove(pchunk->buf + new_len - tail_len, pchunk->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + for (size_type i = 0; i < count; i++) { + pchunk->buf[pos + i] = c; + } + } + // update length + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + return *this; + } + } + + /// replace part of string with count chars from char* + string& replace(size_type pos, size_type n, const char_type * s) noexcept { + if (!s || !*s) { + // nothing to insert + return *this; + } + size_type count = str_len(s); + return replace(pos, n, s, count); + } + + /// replace part of string with count chars from char* + string& replace(size_type pos, size_type n, const string& s) noexcept { + if (s.empty()) { + // nothing to insert + return *this; + } + return replace(pos, n, s.pchunk->buf, s.pchunk->len); + } + + /// replace part of string with count chars from char* + string& replace(size_type pos, size_type n, const string& s, size_type offset, size_type count) noexcept { + if (s.empty()) { + // nothing to insert + return *this; + } + size_type s_len = s.pchunk->len; + if (offset >= s_len) { + // source data indexes outside source string bounds + return *this; + } + // ensure source range is inside string - truncate if needed + if (offset + count > s_len) { + count = s_len - offset; + } + return replace(pos, n, s.pchunk->buf + offset, count); + } + + /// replace all occurences of character replaceWhat with character replaceTo + string& replace(char_type replaceWhat, char_type replaceTo) noexcept { + if (empty()) { + // nothing to replace + return *this; + } + size_type len = pchunk->len; + char_type * buf = pchunk->buf; + for (size_type i = 0; i < len; i++) { + if (buf[i] == replaceWhat) { + // first match found: replace is needed + if (pchunk->getRefCount() == 1) { + // own buffer : replace in-place + buf[i++] = replaceTo; + while (i < len) { + if (buf[i] == replaceWhat) { + buf[i] = replaceTo; + } + i++; + } + } else { + // need to create a copy + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); + // switch to new buffer + buf = tmp->buf; + // replace first char + buf[i++] = replaceTo; + while (i < len) { + if (buf[i] == replaceWhat) { + buf[i] = replaceTo; + } + i++; + } + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + break; + } + } + return *this; + } + /// return C-style null-terminated string pointer const char_type * c_str() const noexcept { if (pchunk == nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 54e7725c57..84ade8f2a6 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1012,6 +1012,109 @@ void test_lstring8() { TCHECK(s_pack_copy == "shared"); TCHECK(s_pack_copy.capacity() < 100); TCHECK(s_pack_shared == "shared"); + + // --- replace(pos, n, char*, count) normal --- + lString8 s_rep1 {"hello world"}; + s_rep1.replace(6, 5, "earth", 5); + TCHECK(s_rep1 == "hello earth"); + + // --- replace(pos, n, char*, count) longer replacement --- + lString8 s_rep2 {"abc"}; + s_rep2.replace(1, 1, "XYZ", 3); + TCHECK(s_rep2 == "aXYZc"); + TCHECK(s_rep2.length() == 5); + + // --- replace(pos, n, char*, count) shorter replacement --- + lString8 s_rep3 {"hello world"}; + s_rep3.replace(6, 5, "there", 5); + TCHECK(s_rep3 == "hello there"); + + // --- replace(pos, n, char*, count) n=0 (insert) --- + lString8 s_rep4 {"ab"}; + s_rep4.replace(1, 0, "X", 1); + TCHECK(s_rep4 == "aXb"); + + // --- replace(pos, n, char*, count) pos beyond length --- + lString8 s_rep5 {"ab"}; + s_rep5.replace(100, 5, "X", 1); + TCHECK(s_rep5 == "abX"); + + // --- replace(pos, n, char*, count) n exceeds remaining --- + lString8 s_rep6 {"hello"}; + s_rep6.replace(3, 100, "X", 1); + TCHECK(s_rep6 == "helX"); + + // --- replace(pos, n, char*, count) on shared string --- + lString8 s_rep7 {"shared"}; + lString8 s_rep7_copy = s_rep7; + s_rep7_copy.replace(0, 6, "new", 3); + TCHECK(s_rep7_copy == "new"); + TCHECK(s_rep7 == "shared"); + + // --- replace(pos, n, char*) --- + lString8 s_rep8 {"hello world"}; + s_rep8.replace(6, 5, "earth"); + TCHECK(s_rep8 == "hello earth"); + + // --- replace(pos, n, char*) with nullptr --- + lString8 s_rep9 {"test"}; + s_rep9.replace(1, 2, nullptr); + TCHECK(s_rep9 == "test"); + + // --- replace(pos, n, string&) --- + lString8 s_rep10 {"hello world"}; + lString8 s_rep10_src {"earth"}; + s_rep10.replace(6, 5, s_rep10_src); + TCHECK(s_rep10 == "hello earth"); + + // --- replace(pos, n, string&) self-replace --- + lString8 s_rep11 {"abcde"}; + s_rep11.replace(1, 3, s_rep11); + TCHECK(s_rep11 == "aabcdee"); + + // --- replace(pos, n, string&, offset, count) --- + lString8 s_rep12 {"hello world"}; + lString8 s_rep12_src {"abcdefgh"}; + s_rep12.replace(6, 5, s_rep12_src, 2, 3); + TCHECK(s_rep12 == "hello cde"); + + // --- replace(pos, n, string&, offset, count) offset beyond --- + lString8 s_rep13 {"test"}; + lString8 s_rep13_src {"abc"}; + s_rep13.replace(1, 2, s_rep13_src, 100, 5); + TCHECK(s_rep13 == "test"); + + // --- replace(pos, n, count, char) --- + lString8 s_rep14 {"hello world"}; + s_rep14.replace(6, 5, 5, 'X'); + TCHECK(s_rep14 == "hello XXXXX"); + + // --- replace(pos, n, count, char) longer --- + lString8 s_rep15 {"abc"}; + s_rep15.replace(1, 1, 3, 'X'); + TCHECK(s_rep15 == "aXXXc"); + + // --- replace(char, char) --- + lString8 s_rep16 {"hello world"}; + s_rep16.replace('l', 'L'); + TCHECK(s_rep16 == "heLLo worLd"); + + // --- replace(char, char) no match --- + lString8 s_rep17 {"hello"}; + s_rep17.replace('z', 'Z'); + TCHECK(s_rep17 == "hello"); + + // --- replace(char, char) on shared string --- + lString8 s_rep18 {"abcabc"}; + lString8 s_rep18_copy = s_rep18; + s_rep18_copy.replace('a', 'X'); + TCHECK(s_rep18_copy == "XbcXbc"); + TCHECK(s_rep18 == "abcabc"); + + // --- replace(char, char) empty string --- + lString8 s_rep19; + s_rep19.replace('a', 'b'); + TCHECK(s_rep19.empty()); } void test_lstring2() { From ee9b3758af8d753faf7a145b3193d5c8e2a24a16 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Sat, 25 Jul 2026 23:46:23 +0100 Subject: [PATCH 24/36] additional compare() --- tests/src/lvstring2.h | 76 +++++++++++++++++++++++++++++++++++-- tests/src/lvstring2test.cpp | 56 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index d6a941d45a..f840d2237f 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -94,7 +94,7 @@ inline int str_cmp_nonempty(const char_type * s1, size_type sz1, const char_type return (i < sz2) ? -1 : 0; } else if (i >= sz2) { // s2 not yet ended - return 0; + return 1; } if (s1[i] < s2[i]) { return -1; @@ -504,8 +504,8 @@ class string { // same string return 0; } - size_t sz1 = length(); - size_t sz2 = s.length(); + size_type sz1 = length(); + size_type sz2 = s.length(); if (sz1 == 0) { return sz2 > 0 ? -1 : 0; } else if (sz2 == 0) { @@ -514,6 +514,76 @@ class string { return str_cmp_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); } + /// compare substring (pos..pos+n) of this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const string& s) const noexcept { + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s.empty() ? 0 : -1; + } + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; + } + // n > 0 + size_type sz2 = s.length(); + if (sz2 == 0) { + return 1; + } + return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf, sz2); + } + + /// compare substring (pos..pos+n) of this string with substring of another string (pos2..pos2+n2), returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const string& s, size_type pos2, size_type n2) const noexcept { + // check if another string fragment is empty + bool s_empty = (!s.pchunk || pos2 >= s.pchunk->len); + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s_empty ? 0 : -1; + } + if (s_empty) { + // this string non-empty, another string is empty + return 1; + } + // both string fragments are non-empty + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; + } + // clamp other fragment size + if (pos2 + n2 > s.pchunk->len) { + n2 = s.pchunk->len - pos2; + } + // n > 0, n2 > 0 + return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf, n2); + } + + /// compare substring (pos..pos+n) of this string with substring of another string s of len n2, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const char_type * s, size_type n2) const noexcept { + // check if another string fragment is empty + bool s_empty = !s || !n2; + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s_empty ? 0 : -1; + } + if (s_empty) { + // this string non-empty, another string is empty + return 1; + } + // both string fragments are non-empty + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; + } + // n > 0, n2 > 0 + return str_cmp_nonempty(pchunk->buf + pos, n, s, n2); + } + + /// compare substring (pos..pos+n) of this string with another string s, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const char_type * s) const noexcept { + size_type n2 = (!s) ? 0 : str_len(s); + return compare(pos, n, s, n2); + } + /// compare this string with string literal, returns -1 if this < s, 1 if this > s, 0 if equal int compare(const char_type* s) const noexcept { size_t sz1 = length(); diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 84ade8f2a6..cc4a53d61a 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1115,6 +1115,62 @@ void test_lstring8() { lString8 s_rep19; s_rep19.replace('a', 'b'); TCHECK(s_rep19.empty()); + + // --- compare(pos, n, string&) --- + lString8 s_cmp1 {"hello world"}; + lString8 s_cmp1_src {"world"}; + TCHECK(s_cmp1.compare(6, 5, s_cmp1_src) == 0); + TCHECK(s_cmp1.compare(6, 5, lString8{"hello"}) > 0); // "world" > "hello" + TCHECK(s_cmp1.compare(6, 5, lString8{"zebra"}) < 0); // "world" < "zebra" + + // --- compare(pos, n, string&) n exceeds bounds --- + TCHECK(s_cmp1.compare(6, 100, s_cmp1_src) == 0); + + // --- compare(pos, n, string&) pos beyond length --- + TCHECK(s_cmp1.compare(100, 5, lString8{"x"}) < 0); // "" < "x" + + // --- compare(pos, n, string&, pos2, n2) --- + lString8 s_cmp2 {"abcdef"}; + lString8 s_cmp2_src {"xyzbcdefg"}; + TCHECK(s_cmp2.compare(1, 4, s_cmp2_src, 3, 4) == 0); // "bcde" == "bcde" + TCHECK(s_cmp2.compare(1, 4, s_cmp2_src, 3, 3) > 0); // "bcde" > "bcd" + TCHECK(s_cmp2.compare(1, 3, s_cmp2_src, 3, 4) < 0); // "bcd" < "bcde" + + // --- compare(pos, n, string&, pos2, n2) pos2 beyond --- + TCHECK(s_cmp2.compare(1, 4, s_cmp2_src, 100, 5) > 0); // "bcde" > "" + + // --- compare(pos, n, char*, n2) --- + TCHECK(s_cmp2.compare(1, 4, "bcde", 4) == 0); + TCHECK(s_cmp2.compare(1, 4, "bcd", 3) > 0); + TCHECK(s_cmp2.compare(1, 3, "bcde", 4) < 0); + + // --- compare(pos, n, char*, n2) nullptr --- + TCHECK(s_cmp2.compare(1, 4, nullptr, 0) > 0); // "bcde" > "" + + // --- compare(pos, n, char*) --- + TCHECK(s_cmp2.compare(1, 4, "bcde") == 0); + TCHECK(s_cmp2.compare(1, 4, nullptr) > 0); + TCHECK(s_cmp2.compare(1, 4, "") > 0); + + // --- compare(pos, n, char*) n exceeds bounds --- + TCHECK(s_cmp2.compare(1, 100, "bcde") == 0); + + // --- compare(pos, n, char*) pos beyond length --- + TCHECK(s_cmp2.compare(100, 5, "x") < 0); + + // --- compare(const char*) with nullptr --- + lString8 s_cmp3 {"test"}; + TCHECK(s_cmp3.compare(nullptr) > 0); + + // --- compare(const char*) with empty --- + TCHECK(s_cmp3.compare("") > 0); + + // --- compare(const char*) equal --- + TCHECK(s_cmp3.compare("test") == 0); + + // --- compare(const char*) less/greater --- + TCHECK(s_cmp3.compare("zebra") < 0); + TCHECK(s_cmp3.compare("abc") > 0); } void test_lstring2() { From f00aeabfb5d48cb50fca1f4f3fea8431f3b5a91f Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 14:35:49 +0100 Subject: [PATCH 25/36] increase coverage of new strings implementation --- tests/src/lvstring2.h | 655 ++++++++++++++++++++++++++++++++++-- tests/src/lvstring2test.cpp | 225 ++++++++++++- 2 files changed, 852 insertions(+), 28 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index f840d2237f..50c4c237ca 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -1,6 +1,24 @@ #ifndef LVSTRING2_H #define LVSTRING2_H +/** + * Copy-on-write reference counting strings. + * + * Optimized for low footprint and high performance. + * Thread-safe if instantiated with std::atomic_int as refcounter_type. + * + * Designed to be a drop-in replacement for old lString8/lString16/lString32 in coolreader core. + * + * Empty string is internally represented by `nullptr`. + * + * Length, capacity and refcount are located in signle block alongside with character data, occupying 12 bytes for 4-byte size_type and refcounter_type. + * Space for additional zero-termination character is always allocated in addition to capacity() value. + * + * To minimize refcount checks, use writable reference to modify string content. + * + * Interface is partially compatible with std::string. + */ + #include "lvtypes.h" #include #include @@ -159,14 +177,15 @@ inline int str_cmp_nonempty(const char_type * s1, const char_type * s2) { // forward declaration of string class template class string; +// forward declaration of string writable ref class +template +class string_wr; template struct lstring_chunk_t { - // friend class lString8; - // friend class lString32; - // friend struct lstring_chunk_slice_t; friend class string; + friend class string_wr; friend void test_lstring2_chunks(); /// chunk allocation alignment in bytes @@ -189,7 +208,35 @@ struct lstring_chunk_t { /// get reference counter value size_type getRefCount() const noexcept { - return refCount; + if constexpr (std::is_same_v) { + // simple refcount + return refCount; + } else { + // atomic + return refCount.load(std::memory_order_relaxed); + } + } + + /// returns true if ref counter value is 1 + bool isOwn() const noexcept { + if constexpr (std::is_same_v) { + // simple refcount + return refCount == 1; + } else { + // atomic + return refCount.load(std::memory_order_relaxed) == 1; + } + } + + /// returns true if ref counter value is 1 + bool isShared() const noexcept { + if constexpr (std::is_same_v) { + // simple refcount + return refCount > 1; + } else { + // atomic + return refCount.load(std::memory_order_relaxed) > 1; + } } /// 1. Hook for incrementing the counter (compatible with boost::intrusive_ptr) @@ -202,6 +249,18 @@ struct lstring_chunk_t { } } + /// 1. Hook for incrementing the counter (compatible with boost::intrusive_ptr) + friend void intrusive_ptr_add_ref_checknull(const lstring_chunk_t* p) noexcept { + if (p) { + if constexpr (std::is_same_v) { + // simple refcount + p->refCount++; + } else { + p->refCount.fetch_add(1, std::memory_order_relaxed); + } + } + } + void testAddRef() { refCount.fetch_add(1, std::memory_order_relaxed); } @@ -257,6 +316,36 @@ struct lstring_chunk_t { return res; } + /// chunk allocation function: create duplicate of this string buffer with ref counter 1, does not release this buffer + lstring_chunk_t * duplicate() noexcept { + LS_COUNT_ALLOC + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (size + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + std::memcpy(res->buf, buf, len * sizeof(char_type)); + res->buf[len] = 0; + res->buf[size] = 0; + res->size = size; + res->len = len; + res->refCount = 1; + return res; + } + + /// chunk allocation function: create duplicate of this string buffer with capacity newSize and ref counter 1, does not release this buffer + lstring_chunk_t * duplicate(size_type newSize) noexcept { + LS_COUNT_ALLOC + size_type sz = newSize; + size_type count = len > sz ? sz : len; + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + std::memcpy(res->buf, buf, count * sizeof(char_type)); + res->buf[count] = 0; + res->buf[sz] = 0; + res->size = sz; + res->len = count; + res->refCount = 1; + return res; + } + /// free chunk memory static void free( const lstring_chunk_t * pChunk ) noexcept { LS_COUNT_FREE @@ -282,8 +371,276 @@ struct lstring_chunk_t { extern lChar32 fake_null_buffer_32; +/// writable copy of string (guaranteed to be empty or have reference counter == 1 +/// can only be passed by reference created from string +/// allows modification without reference counting checks +template +class string_wr { +public: + // typedefs for STL compatibility + typedef char_type value_type; ///< character type + typedef lUInt32 size_type; ///< size type + typedef lInt32 difference_type; ///< difference type + typedef value_type * pointer; ///< pointer to char type + typedef value_type & reference; ///< reference to char type + typedef const value_type * const_pointer; ///< pointer to const char type + typedef const value_type & const_reference; ///< reference to const char type + typedef string string_type; ///< normal shared string reference type + + // constant for not found / unspecified position + static const size_type npos = -1; + +private: + using chunk_t = lstring_chunk_t; + + /// disabled: always created as a synonim of the string with the same template parameters + string_wr() {} + /// don't free pchunk - it's owned by string + ~string_wr() {} +public: + + /// index value. No index nor empty string checks. Will crash on empty string. + char_type operator [] (size_type index) const noexcept { + return pchunk->buf[index]; + } + + /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). + char_type& operator [] (size_type index) noexcept { + return pchunk->buf[index]; + } + + /// index value with bounds checking. No index nor empty string checks. Will crash on empty string. + /// Does not throw exception on index out of bounds, returns 0 instead. + char_type at(size_type index) const noexcept { + if (pchunk == nullptr || index >= pchunk->len) { + return 0; + } + return pchunk->buf[index]; + } + + /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). + /// Does not throw exception on index out of bounds, returns reference pointing to static 0 value instead. + char_type& at(size_type index) noexcept { + if (pchunk == nullptr || index >= pchunk->len) { + return *(static_cast(&fake_null_buffer_32)); + } + return pchunk->buf[index]; + } + + /// return C-style null-terminated string pointer + const char_type * c_str() const noexcept { + if (pchunk == nullptr) { + return reinterpret_cast(&fake_null_buffer_32); + } else { + // enforce null-termination + const_cast(pchunk)->buf[pchunk->len] = 0; + return pchunk->buf; + } + } + + /// return C-style null-terminated string pointer + const char_type * data() const noexcept { + return c_str(); + } + + /// return modifable C-style null-terminated string pointer; for empty string returns pointer to fake empty z-string buffer + char_type * data() noexcept { + if (pchunk == nullptr) { + return reinterpret_cast(&fake_null_buffer_32); + } else { + lock(pchunk->len); + // enforce null-termination + const_cast(pchunk)->buf[pchunk->len] = 0; + return pchunk->buf; + } + } + + + // ============================================================================ + // const methods + // ============================================================================ + + /// returns true if string is empty + bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } + /// returns character count + size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } + /// returns buffer size + size_type size() const noexcept { return capacity(); } + /// changes buffer size + //void resize(size_type count = 0, value_type e = 0); + /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) + size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } + + /// sets value to null string, freeing buffer + void clear() noexcept { + if (pchunk != nullptr) { + // use free instead of release because we own this string and ref count should be 1 anyway + chunk_t::free(pchunk); + pchunk = nullptr; + } + } + + /// resets length to zero, preparing to modification with reserved size + string_wr& reset(size_type size) noexcept { + if (pchunk == nullptr) { + // buffer is allocated and has capacity at least size + pchunk = chunk_t::alloc(size); + } else { + // this string is non-empty + if (pchunk->size >= size) { + // this is already our own string with enough capacity -- just reset length + pchunk->len = 0; + } else { + // this string has not enough capacity for size, clear and alloc new + chunk_t::free(pchunk); + pchunk = chunk_t::alloc(size); + } + } + return *this; + } + + /// compact buffer if possible -- free unused buffer space; returned reference is a normal string + string_type& pack() noexcept { + if (pchunk) { + size_type new_size = chunk_t::alignSize(pchunk->len); + if (pchunk->size > new_size) { + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); + chunk_t::free(pchunk); + pchunk = tmp; + } + } + return *reinterpret_cast(this); + } + + /// move assignment - shared string + string_wr& assign(string_type&& s) noexcept { + LS_COUNT_MOVE_ASSIGN + if (this != &s) { + if (pchunk != nullptr) { + chunk_t::free(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + // ensure we have owned copy of source data + if (pchunk->isShared()) { + chunk_t * tmp = pchunk->duplicate(); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + return *this; + } + + /// move assignment - shared string + string_wr& assign(string_wr&& s) noexcept { + LS_COUNT_MOVE_ASSIGN + if (this != &s) { + if (pchunk != nullptr) { + chunk_t::free(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + } + return *this; + } + + /// copy assignment + string_wr& assign(const string_type& s) noexcept { + LS_COUNT_COPY_ASSIGN + if (&s == this || pchunk == s.pchunk) { + // safe self assignment: do nothing + return *this; + } + if (pchunk != nullptr) { + // ignore self-assignment + chunk_t::free(pchunk); + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } else { + // this string is null + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } + return *this; + } + + /// fragment assignment; correctly covers self-assignment; doesn't check bounds + string_wr& assign(const string_type&s, size_type offset, size_type count) noexcept { + if (s.pchunk && offset < s.pchunk->len) { + size_type avail = s.pchunk->len - offset; + if (count > avail) count = avail; + chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } else { + clear(); + } + return *this; + } + + /// assign from z-terminated string + string_wr& operator = (const char_type * s) noexcept { + return assign(s); + } + + /// assign from z-terminated string + string_wr& assign(const char_type * s) noexcept { + if (s == nullptr || !*s) { + clear(); + } else { + size_type count = str_len(s); + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + } + + /// assign from char pointer and char count + string_wr& assign(const char_type * s, size_type count) noexcept { + if (s == nullptr || !*s || count == 0) { + clear(); + } else { + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + return *this; + } + +private: + /// member variables must follow in the same order as in string for reinterpret cast + chunk_t * pchunk {nullptr}; +}; + template class string { + friend class string_wr; public: // typedefs for STL compatibility typedef char_type value_type; ///< character type @@ -293,6 +650,17 @@ class string { typedef value_type & reference; ///< reference to char type typedef const value_type * const_pointer; ///< pointer to const char type typedef const value_type & const_reference; ///< reference to const char type + + // constant for not found / unspecified position + static const size_type npos = -1; + + // empty string constant + static const string empty_str; + + // COW types + typedef string_wr writable_string; ///< writable (owned) string + typedef string_wr& writable_ref; ///< writable (owned) string reference + private: using chunk_t = lstring_chunk_t; @@ -323,7 +691,7 @@ class string { string(const string&s) noexcept { LS_COUNT_COPY_CONSTR pchunk = s.pchunk; - intrusive_ptr_add_ref(pchunk); + intrusive_ptr_add_ref_checknull(pchunk); } /// constructor of empty buffer with reserved size string(size_type size) noexcept { @@ -351,6 +719,7 @@ class string { intrusive_ptr_release(pchunk); } } + /// move assignment string& operator = (string&& s) noexcept { LS_COUNT_MOVE_ASSIGN @@ -434,7 +803,7 @@ class string { clear(); } else { size_type count = str_len(s); - if (pchunk != nullptr && pchunk->getRefCount() == 1 && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; @@ -454,7 +823,7 @@ class string { if (s == nullptr || !*s || count == 0) { clear(); } else { - if (pchunk != nullptr && pchunk->getRefCount() == 1 && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; @@ -554,7 +923,7 @@ class string { n2 = s.pchunk->len - pos2; } // n > 0, n2 > 0 - return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf, n2); + return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf + pos2, n2); } /// compare substring (pos..pos+n) of this string with substring of another string s of len n2, returns -1 if this < s, 1 if this > s, 0 if equal @@ -664,13 +1033,13 @@ class string { } /// resets length to zero, preparing to modification with reserved size - void reset(size_type size) noexcept { + writable_string& reset(size_type size) noexcept { if (pchunk == nullptr) { // buffer is allocated and has capacity at least size pchunk = chunk_t::alloc(size); } else { // this string is non-empty - if (pchunk->getRefCount() == 1 && pchunk->size >= size) { + if (pchunk->isOwn() && pchunk->size >= size) { // this is already our own string with enough capacity -- just reset length pchunk->len = 0; } else { @@ -679,16 +1048,17 @@ class string { pchunk = chunk_t::alloc(size); } } + return *reinterpret_cast(this); } /// ensure that this string owns buffer and it has at least size capacity - void reserve(size_type size) noexcept { + writable_string& reserve(size_type size) noexcept { if (size == 0) { - // reserve(0) is a non-binding request, do nothing - return; + // reserve(0) is a non-binding request, just treat this empty string as owned + return *reinterpret_cast(this); } if (pchunk != nullptr) { - if (pchunk->getRefCount() == 1) { + if (pchunk->isOwn()) { // already own buffer if (pchunk->size < size) { // only if size is not enough, create a bigger buffer copy @@ -718,10 +1088,34 @@ class string { pchunk->len = 0; pchunk->buf[0] = 0; } + return *reinterpret_cast(this); + } + + /// get writable reference for own string - create a copy only if refcount > 1 + writable_string& writableRef() { + if (pchunk && pchunk->isShared()) { + // create a copy with refcount==1 + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + return *reinterpret_cast(this); + } + + /// get writable reference for own string - create a copy if refcount > 1 + /// if own copy has to be created, reserve size + writable_string& writableRef(size_type size) { + if (pchunk && pchunk->isShared()) { + // create a copy with refcount==1 + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, size); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + return *reinterpret_cast(this); } /// resize string to specified length, filling new positions with e if expanded - void resize(size_type size, char_type e = 0) noexcept { + writable_string& resize(size_type size, char_type e = 0) noexcept { if (pchunk == nullptr) { // empty string: allocate new buffer pchunk = chunk_t::alloc(size); @@ -732,7 +1126,7 @@ class string { } else { // non-empty string size_type oldlen = pchunk->len; - if (pchunk->getRefCount() == 1 && pchunk->size >= size) { + if (pchunk->isOwn() && pchunk->size >= size) { // own buffer with enough capacity: just adjust length if (size > oldlen) { for (size_type i = oldlen; i < size; i++) @@ -753,10 +1147,11 @@ class string { pchunk = tmp; } } + return *reinterpret_cast(this); } /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated - void erase(size_type offset, size_type count) noexcept { + string& erase(size_type offset, size_type count) noexcept { if (pchunk && offset < pchunk->len && count) { if (offset + count > pchunk->len) { // truncate requested erased fragment size to not exceed bounds @@ -765,7 +1160,7 @@ class string { size_type new_len = pchunk->len - count; size_type tail_start = offset + count; size_type tail_len = pchunk->len - tail_start; - if (pchunk->getRefCount() == 1) { + if (pchunk->isOwn()) { // erase inplace if (tail_len) { std::memmove(pchunk->buf + offset, pchunk->buf + tail_start, sizeof(char_type) * tail_len); @@ -787,6 +1182,7 @@ class string { pchunk->len = new_len; pchunk->buf[pchunk->len] = 0; } + return *this; } /// append fragment from null-terminated string; no validation of input string is performed @@ -796,7 +1192,7 @@ class string { pchunk = chunk_t::alloc(s, count, count); } else { size_type new_len = pchunk->len + count; - if (pchunk->getRefCount() != 1 || pchunk->size < new_len) { + if (pchunk->isShared() || pchunk->size < new_len) { // need new buffer: either shared or not enough capacity // if s points to our own buffer, save data before releasing bool self_append = (s == pchunk->buf); @@ -856,7 +1252,7 @@ class string { string& append(size_type count, char_type c) noexcept { if (pchunk) { size_type new_len = pchunk->len + count; - if (pchunk->getRefCount() != 1 || pchunk->size < new_len) { + if (pchunk->isShared() || pchunk->size < new_len) { chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); for (size_type i = 0; i < count; i++) { tmp->buf[pchunk->len + i] = c; @@ -899,7 +1295,7 @@ class string { } size_type new_len = pchunk->len + count; size_type tail_len = pchunk->len - pos; - if (pchunk->getRefCount() != 1 || new_len > pchunk->size) { + if (pchunk->isShared() || new_len > pchunk->size) { // create copy chunk_t* tmp = chunk_t::alloc(new_len); if (pos > 0) { @@ -968,7 +1364,7 @@ class string { } size_type new_len = pchunk->len + count; size_type tail_len = pchunk->len - pos; - if (pchunk->getRefCount() != 1 || new_len > pchunk->size) { + if (pchunk->isShared() || new_len > pchunk->size) { // create copy chunk_t* tmp = chunk_t::alloc(new_len); if (pos > 0) { @@ -1043,7 +1439,7 @@ class string { } size_type new_len = old_len + count - n; size_type tail_len = old_len - (pos + n); - if (new_len > pchunk->size || pchunk->getRefCount() != 1) { + if (new_len > pchunk->size || pchunk->isShared()) { // need to create copy -- no space or has other refs chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); // move tail to the end of buffer @@ -1094,7 +1490,7 @@ class string { } size_type new_len = old_len + count - n; size_type tail_len = old_len - (pos + n); - if (new_len > pchunk->size || pchunk->getRefCount() != 1) { + if (new_len > pchunk->size || pchunk->isShared()) { // need to create copy -- no space or has other refs chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); // move tail to the end of buffer @@ -1170,7 +1566,7 @@ class string { for (size_type i = 0; i < len; i++) { if (buf[i] == replaceWhat) { // first match found: replace is needed - if (pchunk->getRefCount() == 1) { + if (pchunk->isOwn()) { // own buffer : replace in-place buf[i++] = replaceTo; while (i < len) { @@ -1233,7 +1629,7 @@ class string { void lock( size_type newsize ) noexcept { if (pchunk) { // string is not null - if (pchunk->getRefCount() != 1) { + if (pchunk->isShared()) { // if we don't own string, make a copy if (newsize < pchunk->len) { // ensure all chars from old string were copied @@ -1257,7 +1653,7 @@ class string { } else { // non-empty string // string is not null - if (pchunk->getRefCount() != 1) { + if (pchunk->isShared()) { chunk_t * tmp = chunk_t::alloc(pchunk->len); tmp->len = pchunk->len; std::memcpy(tmp->buf, pchunk->buf, tmp->len); @@ -1269,8 +1665,213 @@ class string { return pchunk->buf; } + /// return first position of char c inside string starting from positon pos, or npos if no char is found. + size_type pos(char_type c, size_type start = 0) const noexcept { + if (pchunk) { + const char_type * buf = pchunk->buf; + size_type len = pchunk->len; + for (size_type i = start; i < len; i++) { + if (buf[i] == c) { + return i; + } + } + } + return npos; + } + + /// return first position of string s starting from positon pos, or npos if no char is found. + size_type pos(const string& s, size_type start = 0) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + if (start >= len) { + return npos; + } + len -= start; + size_type slen = s.pchunk->len; + if (slen > len) { + return npos; + } + const char_type * sbuf = s.pchunk->buf; + const char_type * buf = pchunk->buf + start; + size_type maxstart = len - slen; + for (size_type i = 0; i <= maxstart; i++) { + size_type found = i + start; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != sbuf[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + } + } + return npos; + } + + /// return first position of null-term string starting from positon pos, or npos if no char is found. + size_type pos(const char_type* s, size_type start = 0) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + if (start >= len) { + return npos; + } + len -= start; + size_type slen = str_len(s); + if (slen > len) { + return npos; + } + const char_type * buf = pchunk->buf + start; + size_type maxstart = len - slen; + for (size_type i = 0; i <= maxstart; i++) { + size_type found = i + start; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != s[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + } + } + return npos; + } + + /// return last position of null-term string s, or npos if no char is found. + size_type rpos(const char_type* s) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = str_len(s); + if (slen > len) { + return npos; + } + const char_type * buf = pchunk->buf; + size_type maxstart = len - slen; + for (size_type i = maxstart; ; i--) { + size_type found = i; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != s[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + if (i == 0) { + break; + } + } + } + return npos; + } + + /// returns true if this string starts with s[0..count-1], pass count=npos to calculate string size internally + bool startsWith(const char_type* s, size_type count = npos) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = (count == npos) ? str_len(s) : count; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != s[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns true if this string ends with s[0..count-1], pass count=npos to calculate string size internally + bool endsWith(const char_type* s, size_type count = npos) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = (count == npos) ? str_len(s) : count; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf + (len - slen); + for (size_type j = 0; j < slen; j++) { + if (buf[j] != s[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns true if this string starts with s + bool startsWith(const string& s) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + size_type slen = s.pchunk->len; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf; + const char_type * sbuf = s.pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != sbuf[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns true if this string starts with s + bool endsWith(const string& s) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + size_type slen = s.pchunk->len; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf + (len - slen); + const char_type * sbuf = s.pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != sbuf[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns substring starting with position pos, and specified count of chars (pass npos to copy till end of string) + string substr(size_type pos, size_type count = npos) const noexcept { + string tmp; + if (!pchunk || pos >= pchunk->len) { + return tmp; + } + size_type sz = pchunk->len - pos; + if (count > sz) { + count = sz; + } + tmp.pchunk = chunk_t::alloc(pchunk->buf + pos, count, count); + return tmp; + } + + /// swaps content of two strings, by swapping of pointers + void swap(string & s) { + chunk_t * tmp = pchunk; + pchunk = s.pchunk; + s.pchunk = tmp; + } + /// returns true if string is empty bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } + /// returns true if string is empty + bool operator ! () const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } /// returns character count size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } /// returns buffer size diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index cc4a53d61a..980c4674df 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1,6 +1,13 @@ #include "lvstring2test.h" #include +// Definition of static empty_str for template instantiations +namespace lv { +template<> const string string::empty_str; +template<> const string string::empty_str; +template<> const string string::empty_str; +} + namespace lv { static int test_errors = 0; #define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) @@ -1153,7 +1160,7 @@ void test_lstring8() { TCHECK(s_cmp2.compare(1, 4, "") > 0); // --- compare(pos, n, char*) n exceeds bounds --- - TCHECK(s_cmp2.compare(1, 100, "bcde") == 0); + TCHECK(s_cmp2.compare(1, 100, "bcde") > 0); // "bcdef" > "bcde" // --- compare(pos, n, char*) pos beyond length --- TCHECK(s_cmp2.compare(100, 5, "x") < 0); @@ -1171,6 +1178,221 @@ void test_lstring8() { // --- compare(const char*) less/greater --- TCHECK(s_cmp3.compare("zebra") < 0); TCHECK(s_cmp3.compare("abc") > 0); + + // --- pos(char) --- + lString8 s_pos1 {"hello world"}; + TCHECK(s_pos1.pos('o') == 4); + TCHECK(s_pos1.pos('z') == lString8::npos); + TCHECK(s_pos1.pos('h') == 0); + TCHECK(s_pos1.pos('d') == 10); + + // --- pos(char) on empty string --- + lString8 s_pos_empty; + TCHECK(s_pos_empty.pos('a') == lString8::npos); + + // --- pos(char, start) --- + TCHECK(s_pos1.pos('o', 5) == 7); + TCHECK(s_pos1.pos('o', 8) == lString8::npos); + TCHECK(s_pos1.pos('o', 4) == 4); + TCHECK(s_pos1.pos('o', 100) == lString8::npos); + + // --- pos(string) --- + lString8 s_pos2 {"hello world hello"}; + lString8 s_pos2_pat {"hello"}; + TCHECK(s_pos2.pos(s_pos2_pat) == 0); + TCHECK(s_pos2.pos(lString8{"world"}) == 6); + TCHECK(s_pos2.pos(lString8{"xyz"}) == lString8::npos); + + // --- pos(string) empty pattern --- + TCHECK(s_pos2.pos(lString8{}) == lString8::npos); + + // --- pos(string) empty source --- + lString8 s_pos_empty_src; + TCHECK(s_pos_empty_src.pos(s_pos2_pat) == lString8::npos); + + // --- pos(string, start) --- + TCHECK(s_pos2.pos(s_pos2_pat, 1) == 12); + TCHECK(s_pos2.pos(s_pos2_pat, 13) == lString8::npos); + TCHECK(s_pos2.pos(s_pos2_pat, 12) == 12); + + // --- pos(string) at exact end --- + lString8 s_pos_end {"abcde"}; + TCHECK(s_pos_end.pos(lString8{"e"}) == 4); + TCHECK(s_pos_end.pos(lString8{"de"}) == 3); + TCHECK(s_pos_end.pos(lString8{"cde"}) == 2); + TCHECK(s_pos_end.pos(lString8{"abcde"}) == 0); + + // --- pos(c-str) --- + TCHECK(s_pos1.pos("world") == 6); + TCHECK(s_pos1.pos("xyz") == lString8::npos); + TCHECK(s_pos1.pos(nullptr) == lString8::npos); + TCHECK(s_pos1.pos("") == lString8::npos); + + // --- pos(c-str, start) --- + TCHECK(s_pos1.pos("world", 0) == 6); + TCHECK(s_pos1.pos("world", 7) == lString8::npos); + TCHECK(s_pos1.pos("world", 6) == 6); + TCHECK(s_pos1.pos("world", 100) == lString8::npos); + + // --- pos(c-str) at exact end --- + TCHECK(s_pos_end.pos("e") == 4); + TCHECK(s_pos_end.pos("de") == 3); + TCHECK(s_pos_end.pos("cde") == 2); + TCHECK(s_pos_end.pos("abcde") == 0); + + // --- rpos(c-str) --- + lString8 s_rpos1 {"hello world hello"}; + TCHECK(s_rpos1.rpos("hello") == 12); + TCHECK(s_rpos1.rpos("world") == 6); + TCHECK(s_rpos1.rpos("xyz") == lString8::npos); + + // --- rpos(c-str) single char --- + TCHECK(s_rpos1.rpos("o") == 16); + TCHECK(s_rpos1.rpos("h") == 12); + TCHECK(s_rpos1.rpos("z") == lString8::npos); + + // --- rpos(c-str) at beginning --- + TCHECK(s_rpos1.rpos("hello world") == 0); + + // --- rpos(c-str) entire string --- + TCHECK(s_rpos1.rpos("hello world hello") == 0); + + // --- rpos(c-str) longer than string --- + TCHECK(s_rpos1.rpos("hello world hello world") == lString8::npos); + + // --- rpos(c-str) nullptr --- + TCHECK(s_rpos1.rpos(nullptr) == lString8::npos); + + // --- rpos(c-str) empty --- + TCHECK(s_rpos1.rpos("") == lString8::npos); + + // --- rpos(c-str) empty string --- + lString8 s_rpos_empty; + TCHECK(s_rpos_empty.rpos("test") == lString8::npos); + + // --- startsWith(c-str) --- + lString8 s_sw1 {"hello world"}; + TCHECK(s_sw1.startsWith("hello")); + TCHECK(s_sw1.startsWith("hello world")); + TCHECK(!s_sw1.startsWith("world")); + TCHECK(!s_sw1.startsWith("hello world!")); + TCHECK(!s_sw1.startsWith("")); + TCHECK(!s_sw1.startsWith(nullptr)); + + // --- startsWith(c-str, count) --- + TCHECK(s_sw1.startsWith("hel", 3)); + TCHECK(s_sw1.startsWith("hello", 5)); + TCHECK(!s_sw1.startsWith("help", 4)); + TCHECK(!s_sw1.startsWith("hello world!", 12)); + + // --- startsWith(string) --- + lString8 s_sw2 {"hello"}; + TCHECK(s_sw1.startsWith(s_sw2)); + TCHECK(!s_sw2.startsWith(s_sw1)); + + // --- startsWith(string) empty source --- + lString8 s_sw_empty; + TCHECK(!s_sw_empty.startsWith(s_sw2)); + TCHECK(!s_sw_empty.startsWith("hello")); + + // --- startsWith(string) empty pattern --- + lString8 s_sw_empty_pat; + TCHECK(!s_sw1.startsWith(s_sw_empty_pat)); + + // --- endsWith(c-str) --- + lString8 s_ew1 {"hello world"}; + TCHECK(s_ew1.endsWith("world")); + TCHECK(s_ew1.endsWith("hello world")); + TCHECK(!s_ew1.endsWith("hello")); + TCHECK(!s_ew1.endsWith("hello world!")); + TCHECK(!s_ew1.endsWith("")); + TCHECK(!s_ew1.endsWith(nullptr)); + + // --- endsWith(c-str, count) --- + TCHECK(s_ew1.endsWith("orld", 4)); + TCHECK(s_ew1.endsWith("world", 5)); + TCHECK(!s_ew1.endsWith("word", 4)); + TCHECK(!s_ew1.endsWith("hello world!", 12)); + + // --- endsWith(string) --- + lString8 s_ew2 {"world"}; + TCHECK(s_ew1.endsWith(s_ew2)); + TCHECK(!s_ew2.endsWith(s_ew1)); + + // --- endsWith(string) empty source --- + lString8 s_ew_empty; + TCHECK(!s_ew_empty.endsWith(s_ew2)); + TCHECK(!s_ew_empty.endsWith("world")); + + // --- endsWith(string) empty pattern --- + lString8 s_ew_empty_pat; + TCHECK(!s_ew1.endsWith(s_ew_empty_pat)); + + // --- substr(pos, count) --- + lString8 s_sub1 {"hello world"}; + TCHECK(s_sub1.substr(0, 5) == "hello"); + TCHECK(s_sub1.substr(6, 5) == "world"); + TCHECK(s_sub1.substr(0, 11) == "hello world"); + TCHECK(s_sub1.substr(5, 1) == " "); + TCHECK(s_sub1.substr(10, 1) == "d"); + + // --- substr(pos, count) count exceeds remaining --- + TCHECK(s_sub1.substr(6, 100) == "world"); + + // --- substr(pos, count) pos beyond length --- + TCHECK(s_sub1.substr(100, 5) == ""); + + // --- substr(pos, count) pos at end --- + TCHECK(s_sub1.substr(11, 5) == ""); + + // --- substr(pos) default count=npos --- + TCHECK(s_sub1.substr(6) == "world"); + TCHECK(s_sub1.substr(0) == "hello world"); + + // --- substr(pos, count) empty string --- + lString8 s_sub_empty; + TCHECK(s_sub_empty.substr(0, 5) == ""); + TCHECK(s_sub_empty.substr(0) == ""); + + // --- empty_str --- + lString8 s_empty_ref = lString8::empty_str; + TCHECK(s_empty_ref.empty()); + TCHECK(s_empty_ref.length() == 0); + TCHECK(s_empty_ref == ""); + + // --- operator!() --- + lString8 s_not1 {"hello"}; + lString8 s_not2; + TCHECK(!s_not2); + TCHECK(!(!s_not1)); + + // --- swap() --- + lString8 s_swap1 {"first"}; + lString8 s_swap2 {"second"}; + s_swap1.swap(s_swap2); + TCHECK(s_swap1 == "second"); + TCHECK(s_swap2 == "first"); + + // --- swap() with empty --- + lString8 s_swap3 {"nonempty"}; + lString8 s_swap4; + s_swap3.swap(s_swap4); + TCHECK(s_swap3.empty()); + TCHECK(s_swap4 == "nonempty"); + + // --- swap() self --- + lString8 s_swap5 {"self"}; + s_swap5.swap(s_swap5); + TCHECK(s_swap5 == "self"); +} + +void test_writable_refs_lString8() { + lString8 s1 {"Original string"}; + lString8 s1_copy = s1; + TCHECK(s1 == s1_copy); + auto& s1_wr = s1.writableRef(100); + TCHECK(s1_wr.length() == s1_copy.length()); + } void test_lstring2() { @@ -1179,6 +1401,7 @@ void test_lstring2() { DUMP_ALLOC_STATS("before test_lstring8()") test_lstring8(); DUMP_ALLOC_STATS("after test_lstring8()") + test_writable_refs_lString8(); if (test_errors == 0) { printf("New strings library tests completed successfully\n"); From 32ca4cfaa836d37fccfe3e49d60dd7ee8663d682 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 15:56:45 +0100 Subject: [PATCH 26/36] more methods implemented in new string; covered with tests --- tests/src/lvstring2.h | 135 +++++++++++++++++++++++++++++++--- tests/src/lvstring2test.cpp | 141 ++++++++++++++++++++++++++---------- 2 files changed, 227 insertions(+), 49 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 50c4c237ca..8de5c08ca8 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -27,6 +27,13 @@ #define DEBUG_TRACK_LSTRING2_ALLOC +void lStr_uppercase( lChar32 * str, int len ); +void lStr_uppercase( lChar16 * str, int len ); +void lStr_uppercase( lChar8 * str, int len ); +void lStr_lowercase( lChar32 * str, int len ); +void lStr_lowercase( lChar16 * str, int len ); +void lStr_lowercase( lChar8 * str, int len ); + namespace lv { #ifdef DEBUG_TRACK_LSTRING2_ALLOC @@ -59,6 +66,25 @@ extern lStringStats ls_alloc_stats; #endif + +// lv::fmt copy of fmt +namespace fmt { + class decimal { + lInt64 value; + public: + explicit decimal(lInt64 v) : value(v) { } + lInt64 get() const { return value; } + }; + + class hex { + lUInt64 value; + public: + explicit hex(lInt64 v) : value(v) { } + lUInt64 get() const { return value; } + }; +} + + // Helper functions /// arbitrary char type strlen, supports nullptr arg @@ -464,10 +490,8 @@ class string_wr { bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } /// returns character count size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns buffer size - size_type size() const noexcept { return capacity(); } - /// changes buffer size - //void resize(size_type count = 0, value_type e = 0); + /// returns character count (same as length) + size_type size() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } @@ -633,6 +657,24 @@ class string_wr { return *this; } + /// convert all characters of string to uppercase + string_wr& uppercase() noexcept { + if (pchunk) { + lStr_uppercase(pchunk->buf, pchunk->len); + } + return *this; + } + + /// convert all characters of string to lowercase + string_wr& lowercase() noexcept { + if (pchunk) { + lStr_lowercase(pchunk->buf, pchunk->len); + } + return *this; + } + + + private: /// member variables must follow in the same order as in string for reinterpret cast chunk_t * pchunk {nullptr}; @@ -655,7 +697,9 @@ class string { static const size_type npos = -1; // empty string constant - static const string empty_str; + //static const string empty_str; + //static string constexpr empty_str = nullptr; + inline static const string empty_str{}; // COW types typedef string_wr writable_string; ///< writable (owned) string @@ -1150,6 +1194,22 @@ class string { return *reinterpret_cast(this); } + /// convert all characters of string to uppercase + string& uppercase() noexcept { + if (pchunk) { + writableRef().uppercase(); + } + return *this; + } + + /// convert all characters of string to lowercase + string& lowercase() noexcept { + if (pchunk) { + writableRef().lowercase(); + } + return *this; + } + /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated string& erase(size_type offset, size_type count) noexcept { if (pchunk && offset < pchunk->len && count) { @@ -1185,6 +1245,61 @@ class string { return *this; } + /// appends decimal string representation of integer value + string& appendDecimal(lInt64 n) noexcept { + char_type buf[24]; + int i=0; + int negative = 0; + if (n==0) { + return append(1, '0'); + } else if (n<0) + { + negative = 1; + n = -n; + } + for ( ; n; n/=10 ) + { + buf[i++] = '0' + (n % 10); + } + reserve(length() + i + negative); + if (negative) + append(1, '-'); + for (int j=i-1; j>=0; j--) + append(1, buf[j]); + return *this; + } + + /// appends hex string representation of integer value, no leading zeroes + string& appendHex(lUInt64 n) noexcept { + if (n == 0) { + return append(1, '0'); + } + reserve(length() + 16); + bool foundNz = false; + for (int i=0; i<16; i++) { + int digit = (n >> 60) & 0x0F; + if (digit) { + foundNz = true; + } + if (foundNz) { + append(1, (static_cast("0123456789abcdef"[digit])) & 0xFF); + } + n <<= 4; + } + return *this; + } + + /// append single character + string& operator << (char_type ch) { return append(1, ch); } + /// append C-string + string& operator << (const char_type * str) { return append(str); } + /// append string + string& operator << (const string & str) { return append(str); } + /// append decimal number + string& operator << (const fmt::decimal v) { return appendDecimal(v.get()); } + /// append hex number + string& operator << (const fmt::hex v) { return appendHex(v.get()); } + /// append fragment from null-terminated string; no validation of input string is performed string& append(const char_type* s, size_type count) noexcept { if (count) { @@ -1871,13 +1986,11 @@ class string { /// returns true if string is empty bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } /// returns true if string is empty - bool operator ! () const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } + bool operator ! () const noexcept { return pchunk == nullptr || pchunk->len==0; } /// returns character count size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns buffer size - size_type size() const noexcept { return capacity(); } - /// changes buffer size - //void resize(size_type count = 0, value_type e = 0); + /// returns character count + size_type size() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } @@ -1903,6 +2016,8 @@ class string { chunk_t * pchunk {nullptr}; }; +// template +// const string string::empty_str{}; typedef lstring_chunk_t lstring8_chunk_t; typedef lstring_chunk_t lstring16_chunk_t; diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 980c4674df..8257884d45 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1,14 +1,11 @@ #include "lvstring2test.h" #include -// Definition of static empty_str for template instantiations namespace lv { -template<> const string string::empty_str; -template<> const string string::empty_str; -template<> const string string::empty_str; -} -namespace lv { + + + static int test_errors = 0; #define TCHECK(cond) do { if (!(cond)) { printf("LS FAIL line %d: %s\n", __LINE__, #cond); test_errors++; } } while(0) @@ -150,7 +147,7 @@ void test_lstring8() { // --- C-string with count + reserved --- lString8 s3 {"qwerty", 3, 100}; TCHECK(s3.capacity() >= 100); - TCHECK(s3.size() >= 100); + TCHECK(s3.size() == 3); TCHECK(s3.length() == 3); TCHECK(!s3.empty()); @@ -263,10 +260,10 @@ void test_lstring8() { TCHECK(s5.compare("abd") < 0); TCHECK(s5.compare("abb") > 0); - // --- Length/size/capacity/empty consistency --- + // --- Length/size/capacity/empty consistency --- lString8 s_len {"12345"}; TCHECK(s_len.length() == 5); - TCHECK(s_len.size() == s_len.capacity()); + TCHECK(s_len.size() == s_len.length()); TCHECK(!s_len.empty()); TCHECK(lString8{}.empty()); TCHECK(lString8{}.length() == 0); @@ -1354,36 +1351,102 @@ void test_lstring8() { TCHECK(s_sub_empty.substr(0, 5) == ""); TCHECK(s_sub_empty.substr(0) == ""); - // --- empty_str --- - lString8 s_empty_ref = lString8::empty_str; - TCHECK(s_empty_ref.empty()); - TCHECK(s_empty_ref.length() == 0); - TCHECK(s_empty_ref == ""); - - // --- operator!() --- - lString8 s_not1 {"hello"}; - lString8 s_not2; - TCHECK(!s_not2); - TCHECK(!(!s_not1)); - - // --- swap() --- - lString8 s_swap1 {"first"}; - lString8 s_swap2 {"second"}; - s_swap1.swap(s_swap2); - TCHECK(s_swap1 == "second"); - TCHECK(s_swap2 == "first"); - - // --- swap() with empty --- - lString8 s_swap3 {"nonempty"}; - lString8 s_swap4; - s_swap3.swap(s_swap4); - TCHECK(s_swap3.empty()); - TCHECK(s_swap4 == "nonempty"); - - // --- swap() self --- - lString8 s_swap5 {"self"}; - s_swap5.swap(s_swap5); - TCHECK(s_swap5 == "self"); + // --- appendDecimal --- + lString8 s_ad; + s_ad.appendDecimal(0); + TCHECK(s_ad == "0"); + s_ad.appendDecimal(42); + TCHECK(s_ad == "042"); + s_ad.appendDecimal(-123); + TCHECK(s_ad == "042-123"); + s_ad.appendDecimal(999999999999999999LL); + TCHECK(s_ad == "042-123999999999999999999"); + + // --- appendHex --- + lString8 s_ah; + s_ah.appendHex(0); + TCHECK(s_ah == "0"); + s_ah.appendHex(0xFF); + TCHECK(s_ah == "0ff"); + s_ah.appendHex(0xABCDEF); + TCHECK(s_ah == "0ffabcdef"); + + // --- operator << (char) --- + lString8 s_sl; + s_sl << 'A'; + TCHECK(s_sl == "A"); + s_sl << 'B'; + TCHECK(s_sl == "AB"); + + // --- operator << (c-str) --- + lString8 s_sl2; + s_sl2 << "hello"; + TCHECK(s_sl2 == "hello"); + s_sl2 << " world"; + TCHECK(s_sl2 == "hello world"); + + // --- operator << (string) --- + lString8 s_sl3; + s_sl3 << lString8{"test"}; + TCHECK(s_sl3 == "test"); + + // --- operator << (fmt::decimal) --- + lString8 s_sl4; + s_sl4 << fmt::decimal(42); + TCHECK(s_sl4 == "42"); + s_sl4 << fmt::decimal(-99); + TCHECK(s_sl4 == "42-99"); + + // --- operator << (fmt::hex) --- + lString8 s_sl5; + s_sl5 << fmt::hex(255); + TCHECK(s_sl5 == "ff"); + s_sl5 << fmt::hex(0xABC); + TCHECK(s_sl5 == "ffabc"); + + // --- uppercase() --- + lString8 s_upper {"Hello World"}; + s_upper.uppercase(); + TCHECK(s_upper == "HELLO WORLD"); + + // --- uppercase() on shared string --- + lString8 s_upper_shared {"shared"}; + lString8 s_upper_copy = s_upper_shared; + s_upper_copy.uppercase(); + TCHECK(s_upper_copy == "SHARED"); + TCHECK(s_upper_shared == "shared"); + + // --- uppercase() on empty string --- + lString8 s_upper_empty; + s_upper_empty.uppercase(); + TCHECK(s_upper_empty.empty()); + + // --- uppercase() already uppercase --- + lString8 s_upper_done {"ALREADY"}; + s_upper_done.uppercase(); + TCHECK(s_upper_done == "ALREADY"); + + // --- lowercase() --- + lString8 s_lower {"Hello World"}; + s_lower.lowercase(); + TCHECK(s_lower == "hello world"); + + // --- lowercase() on shared string --- + lString8 s_lower_shared {"SHARED"}; + lString8 s_lower_copy = s_lower_shared; + s_lower_copy.lowercase(); + TCHECK(s_lower_copy == "shared"); + TCHECK(s_lower_shared == "SHARED"); + + // --- lowercase() on empty string --- + lString8 s_lower_empty; + s_lower_empty.lowercase(); + TCHECK(s_lower_empty.empty()); + + // --- lowercase() already lowercase --- + lString8 s_lower_done {"already"}; + s_lower_done.lowercase(); + TCHECK(s_lower_done == "already"); } void test_writable_refs_lString8() { From 8ef5e01bba8a9c8e096f8248fd96f6fed1a5a7b4 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 18:18:05 +0100 Subject: [PATCH 27/36] refactor: add base read-only string class and place readonly methods there --- tests/src/lvstring2.h | 1431 ++++++++++++++++++++--------------- tests/src/lvstring2test.cpp | 51 ++ 2 files changed, 880 insertions(+), 602 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 8de5c08ca8..e5d3b07be0 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -200,18 +200,22 @@ inline int str_cmp_nonempty(const char_type * s1, const char_type * s2) { // String data buffer with ref count -// forward declaration of string class +// forward declaration of string readonly class template -class string; +class string_ro; // forward declaration of string writable ref class template class string_wr; +// forward declaration of string class +template +class string; template struct lstring_chunk_t { friend class string; friend class string_wr; + friend class string_ro; friend void test_lstring2_chunks(); /// chunk allocation alignment in bytes @@ -397,12 +401,13 @@ struct lstring_chunk_t { extern lChar32 fake_null_buffer_32; + /// writable copy of string (guaranteed to be empty or have reference counter == 1 /// can only be passed by reference created from string /// allows modification without reference counting checks template -class string_wr { -public: +class string_ro { + public: // typedefs for STL compatibility typedef char_type value_type; ///< character type typedef lUInt32 size_type; ///< size type @@ -411,19 +416,36 @@ class string_wr { typedef value_type & reference; ///< reference to char type typedef const value_type * const_pointer; ///< pointer to const char type typedef const value_type & const_reference; ///< reference to const char type - typedef string string_type; ///< normal shared string reference type + //typedef string string_type; ///< normal shared string reference type // constant for not found / unspecified position static const size_type npos = -1; -private: - using chunk_t = lstring_chunk_t; + typedef lstring_chunk_t chunk_t; + protected: /// disabled: always created as a synonim of the string with the same template parameters - string_wr() {} + string_ro() = default; /// don't free pchunk - it's owned by string - ~string_wr() {} -public: + ~string_ro() = default; + public: + + // ============================================================================ + // size and capacity + // ============================================================================ + + /// returns true if string is empty + bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } + /// returns character count + size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } + /// returns character count (same as length) + size_type size() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } + /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) + size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } + + // ============================================================================ + // item index methods + // ============================================================================ /// index value. No index nor empty string checks. Will crash on empty string. char_type operator [] (size_type index) const noexcept { @@ -464,181 +486,605 @@ class string_wr { } } - /// return C-style null-terminated string pointer - const char_type * data() const noexcept { - return c_str(); + /// returns last character from string, or 0 for empty string + char_type lastChar() const noexcept { + if (pchunk && pchunk->len) { + return pchunk->buf[pchunk->len-1]; + } else { + return 0; + } } - /// return modifable C-style null-terminated string pointer; for empty string returns pointer to fake empty z-string buffer - char_type * data() noexcept { - if (pchunk == nullptr) { - return reinterpret_cast(&fake_null_buffer_32); + /// returns first character from string, or 0 for empty string + char_type firstChar() const noexcept { + if (pchunk && pchunk->len) { + return pchunk->buf[0]; } else { - lock(pchunk->len); - // enforce null-termination - const_cast(pchunk)->buf[pchunk->len] = 0; - return pchunk->buf; + return 0; } } - // ============================================================================ - // const methods + // compare methods // ============================================================================ - /// returns true if string is empty - bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } - /// returns character count - size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns character count (same as length) - size_type size() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) - size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } - - /// sets value to null string, freeing buffer - void clear() noexcept { - if (pchunk != nullptr) { - // use free instead of release because we own this string and ref count should be 1 anyway - chunk_t::free(pchunk); - pchunk = nullptr; + /// compare this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(const string_ro& s) const noexcept { + if (pchunk == s.pchunk) { + // same string + return 0; } - } - - /// resets length to zero, preparing to modification with reserved size - string_wr& reset(size_type size) noexcept { - if (pchunk == nullptr) { - // buffer is allocated and has capacity at least size - pchunk = chunk_t::alloc(size); - } else { - // this string is non-empty - if (pchunk->size >= size) { - // this is already our own string with enough capacity -- just reset length - pchunk->len = 0; - } else { - // this string has not enough capacity for size, clear and alloc new - chunk_t::free(pchunk); - pchunk = chunk_t::alloc(size); - } + size_type sz1 = length(); + size_type sz2 = s.length(); + if (sz1 == 0) { + return sz2 > 0 ? -1 : 0; + } else if (sz2 == 0) { + return 1; } - return *this; + return str_cmp_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); } - /// compact buffer if possible -- free unused buffer space; returned reference is a normal string - string_type& pack() noexcept { - if (pchunk) { - size_type new_size = chunk_t::alignSize(pchunk->len); - if (pchunk->size > new_size) { - chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); - chunk_t::free(pchunk); - pchunk = tmp; - } + /// compare substring (pos..pos+n) of this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const string_ro& s) const noexcept { + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s.empty() ? 0 : -1; } - return *reinterpret_cast(this); - } - - /// move assignment - shared string - string_wr& assign(string_type&& s) noexcept { - LS_COUNT_MOVE_ASSIGN - if (this != &s) { - if (pchunk != nullptr) { - chunk_t::free(pchunk); - } - pchunk = s.pchunk; - s.pchunk = nullptr; - // ensure we have owned copy of source data - if (pchunk->isShared()) { - chunk_t * tmp = pchunk->duplicate(); - intrusive_ptr_release(pchunk); - pchunk = tmp; - } + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; } - return *this; - } - - /// move assignment - shared string - string_wr& assign(string_wr&& s) noexcept { - LS_COUNT_MOVE_ASSIGN - if (this != &s) { - if (pchunk != nullptr) { - chunk_t::free(pchunk); - } - pchunk = s.pchunk; - s.pchunk = nullptr; + // n > 0 + size_type sz2 = s.length(); + if (sz2 == 0) { + return 1; } - return *this; + return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf, sz2); } - /// copy assignment - string_wr& assign(const string_type& s) noexcept { - LS_COUNT_COPY_ASSIGN - if (&s == this || pchunk == s.pchunk) { - // safe self assignment: do nothing - return *this; + /// compare substring (pos..pos+n) of this string with substring of another string (pos2..pos2+n2), returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const string_ro& s, size_type pos2, size_type n2) const noexcept { + // check if another string fragment is empty + bool s_empty = (!s.pchunk || pos2 >= s.pchunk->len); + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s_empty ? 0 : -1; } - if (pchunk != nullptr) { - // ignore self-assignment - chunk_t::free(pchunk); - pchunk = s.pchunk; - if (pchunk) { - // assigned non-empty string - intrusive_ptr_add_ref(pchunk); - } - } else { - // this string is null - pchunk = s.pchunk; - if (pchunk) { - // assigned non-empty string - intrusive_ptr_add_ref(pchunk); - } + if (s_empty) { + // this string non-empty, another string is empty + return 1; } - return *this; + // both string fragments are non-empty + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; + } + // clamp other fragment size + if (pos2 + n2 > s.pchunk->len) { + n2 = s.pchunk->len - pos2; + } + // n > 0, n2 > 0 + return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf + pos2, n2); } - /// fragment assignment; correctly covers self-assignment; doesn't check bounds - string_wr& assign(const string_type&s, size_type offset, size_type count) noexcept { - if (s.pchunk && offset < s.pchunk->len) { - size_type avail = s.pchunk->len - offset; - if (count > avail) count = avail; - chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); - if (pchunk) { - intrusive_ptr_release(pchunk); - } - pchunk = tmp; - } else { - clear(); + /// compare substring (pos..pos+n) of this string with substring of another string s of len n2, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const char_type * s, size_type n2) const noexcept { + // check if another string fragment is empty + bool s_empty = !s || !n2; + if (!pchunk || pos >= pchunk->len) { + // this string fragment is empty + return s_empty ? 0 : -1; } - return *this; + if (s_empty) { + // this string non-empty, another string is empty + return 1; + } + // both string fragments are non-empty + // clamp this fragment size + if (pos + n > pchunk->len) { + n = pchunk->len - pos; + } + // n > 0, n2 > 0 + return str_cmp_nonempty(pchunk->buf + pos, n, s, n2); } - /// assign from z-terminated string - string_wr& operator = (const char_type * s) noexcept { - return assign(s); + /// compare substring (pos..pos+n) of this string with another string s, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(size_type pos, size_type n, const char_type * s) const noexcept { + size_type n2 = (!s) ? 0 : str_len(s); + return compare(pos, n, s, n2); } - /// assign from z-terminated string - string_wr& assign(const char_type * s) noexcept { - if (s == nullptr || !*s) { - clear(); - } else { - size_type count = str_len(s); - if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { - // reuse existing buffer - std::memcpy(pchunk->buf, s, count * sizeof(char_type)); - pchunk->len = count; - } else { - // create new buffer - chunk_t * tmp = chunk_t::alloc(s, count, count); - if (pchunk) { - intrusive_ptr_release(pchunk); - } - pchunk = tmp; - } + /// compare this string with string literal, returns -1 if this < s, 1 if this > s, 0 if equal + int compare(const char_type* s) const noexcept { + size_t sz1 = length(); + if (sz1 == 0) { + return (s != nullptr && *s != 0) ? -1 : 0; + } else if (s == nullptr || *s == 0) { + return 1; } + return str_cmp_nonempty(pchunk->buf, sz1, s); } - /// assign from char pointer and char count - string_wr& assign(const char_type * s, size_type count) noexcept { - if (s == nullptr || !*s || count == 0) { + /// returns true when strings are equal + bool operator == (const string_ro& s) const noexcept { + return compare(s) == 0; + } + + /// returns true when strings are not equal + bool operator != (const string_ro& s) const noexcept { + return compare(s) != 0; + } + + /// returns true when this string < s + bool operator < (const string_ro& s) const noexcept { + return compare(s) < 0; + } + + /// returns true when this string >= s + bool operator <= (const string_ro& s) const noexcept { + return compare(s) <= 0; + } + + /// returns true when this string > s + bool operator > (const string_ro& s) const noexcept { + return compare(s) > 0; + } + + /// returns true when this string <= s + bool operator >= (const string_ro& s) const noexcept { + return compare(s) >= 0; + } + + /// returns true when strings are equal + bool operator == (const char_type * s) const noexcept { + return compare(s) == 0; + } + + /// returns true when strings are not equal + bool operator != (const char_type * s) const noexcept { + return compare(s) != 0; + } + + /// returns true when this string < s + bool operator < (const char_type * s) const noexcept { + return compare(s) < 0; + } + + /// returns true when this string <= s + bool operator <= (const char_type * s) const noexcept { + return compare(s) <= 0; + } + + /// returns true when this string > s + bool operator > (const char_type * s) const noexcept { + return compare(s) > 0; + } + + /// returns true when this string >= s + bool operator >= (const char_type * s) const noexcept { + return compare(s) >= 0; + } + + + // ============================================================================ + // search methods + // ============================================================================ + + /// return first position of char c inside string starting from positon pos, or npos if no char is found. + size_type pos(char_type c, size_type start = 0) const noexcept { + if (pchunk) { + const char_type * buf = pchunk->buf; + size_type len = pchunk->len; + for (size_type i = start; i < len; i++) { + if (buf[i] == c) { + return i; + } + } + } + return npos; + } + + /// return first position of string s starting from positon pos, or npos if no char is found. + size_type pos(const string_ro& s, size_type start = 0) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + if (start >= len) { + return npos; + } + len -= start; + size_type slen = s.pchunk->len; + if (slen > len) { + return npos; + } + const char_type * sbuf = s.pchunk->buf; + const char_type * buf = pchunk->buf + start; + size_type maxstart = len - slen; + for (size_type i = 0; i <= maxstart; i++) { + size_type found = i + start; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != sbuf[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + } + } + return npos; + } + + /// return first position of null-term string starting from positon pos, or npos if no char is found. + size_type pos(const char_type* s, size_type start = 0) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + if (start >= len) { + return npos; + } + len -= start; + size_type slen = str_len(s); + if (slen > len) { + return npos; + } + const char_type * buf = pchunk->buf + start; + size_type maxstart = len - slen; + for (size_type i = 0; i <= maxstart; i++) { + size_type found = i + start; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != s[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + } + } + return npos; + } + + /// return last position of null-term string s, or npos if no char is found. + size_type rpos(const char_type* s) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = str_len(s); + if (slen > len) { + return npos; + } + const char_type * buf = pchunk->buf; + size_type maxstart = len - slen; + for (size_type i = maxstart; ; i--) { + size_type found = i; + for (size_type j = 0; j < slen; j++) { + if (buf[i + j] != s[j]) { + found = npos; + break; + } + } + if (found != npos) { + return found; + } + if (i == 0) { + break; + } + } + } + return npos; + } + + /// returns true if this string starts with s[0..count-1], pass count=npos to calculate string size internally + bool startsWith(const char_type* s, size_type count = npos) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = (count == npos) ? str_len(s) : count; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != s[j]) { + return false; + } + } + return true; + } + return false; + } + + // ============================================================================ + // startsWith / endsWith + // ============================================================================ + + /// returns true if this string ends with s[0..count-1], pass count=npos to calculate string size internally + bool endsWith(const char_type* s, size_type count = npos) const noexcept { + if (pchunk && s && s[0]) { + size_type len = pchunk->len; + size_type slen = (count == npos) ? str_len(s) : count; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf + (len - slen); + for (size_type j = 0; j < slen; j++) { + if (buf[j] != s[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns true if this string starts with s + bool startsWith(const string_ro& s) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + size_type slen = s.pchunk->len; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf; + const char_type * sbuf = s.pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != sbuf[j]) { + return false; + } + } + return true; + } + return false; + } + + /// returns true if this string starts with s + bool endsWith(const string_ro& s) const noexcept { + if (pchunk && s.pchunk) { + size_type len = pchunk->len; + size_type slen = s.pchunk->len; + if (slen > len) { + return false; + } + const char_type * buf = pchunk->buf + (len - slen); + const char_type * sbuf = s.pchunk->buf; + for (size_type j = 0; j < slen; j++) { + if (buf[j] != sbuf[j]) { + return false; + } + } + return true; + } + return false; + } + + + + protected: + /// member variables must follow in the same order as in string for reinterpret cast + chunk_t * pchunk {nullptr}; +}; + +/// writable copy of string (guaranteed to be empty or have reference counter == 1 +/// can only be passed by reference created from string +/// allows modification without reference counting checks +template +class string_wr : public string_ro { +public: + // typedefs for STL compatibility + typedef char_type value_type; ///< character type + typedef lUInt32 size_type; ///< size type + typedef lInt32 difference_type; ///< difference type + typedef value_type * pointer; ///< pointer to char type + typedef value_type & reference; ///< reference to char type + typedef const value_type * const_pointer; ///< pointer to const char type + typedef const value_type & const_reference; ///< reference to const char type + typedef string string_type; ///< normal shared string reference type + typedef string string_ro_type; ///< normal shared string reference type + + // constant for not found / unspecified position + static const size_type npos = -1; + +private: + using typename string_ro::chunk_t; + using string_ro::pchunk; +public: + using string_ro::empty; + using string_ro::length; + using string_ro::size; + using string_ro::capacity; + + using string_ro::operator []; + using string_ro::at; + using string_ro::c_str; + + /// disabled: always created as a synonim of the string with the same template parameters + string_wr() {} + /// don't free pchunk - it's owned by string + ~string_wr() {} +public: + + /// return C-style null-terminated string pointer + const char_type * data() const noexcept { + return c_str(); + } + + /// return modifable C-style null-terminated string pointer; for empty string returns pointer to fake empty z-string buffer + char_type * data() noexcept { + if (pchunk == nullptr) { + return reinterpret_cast(&fake_null_buffer_32); + } else { + // enforce null-termination + const_cast(pchunk)->buf[pchunk->len] = 0; + return pchunk->buf; + } + } + + /// sets value to null string, freeing buffer + void clear() noexcept { + if (pchunk != nullptr) { + // use free instead of release because we own this string and ref count should be 1 anyway + chunk_t::free(pchunk); + pchunk = nullptr; + } + } + + /// resets length to zero, preparing to modification with reserved size + string_wr& reset(size_type size) noexcept { + if (pchunk == nullptr) { + // buffer is allocated and has capacity at least size + pchunk = chunk_t::alloc(size); + } else { + // this string is non-empty + if (pchunk->size >= size) { + // this is already our own string with enough capacity -- just reset length + pchunk->len = 0; + } else { + // this string has not enough capacity for size, clear and alloc new + chunk_t::free(pchunk); + pchunk = chunk_t::alloc(size); + } + } + return *this; + } + + /// ensure that buffer has at least size capacity + string_wr& reserve(size_type size) noexcept { + if (size == 0) { + // reserve(0) is a non-binding request, just treat this empty string as owned + return *this; + } + if (pchunk != nullptr) { + // already own buffer + if (pchunk->size < size) { + // only if size is not enough, create a bigger buffer copy + chunk_t * tmp = pchunk->duplicate(size); + chunk_t::free(pchunk); + pchunk = tmp; + } + } else { + // create new allocation of requested size + pchunk = chunk_t::alloc(size); + pchunk->len = 0; + pchunk->buf[0] = 0; + } + return *this; + } + + /// compact buffer if possible -- free unused buffer space; returned reference is a normal string + string_type& pack() noexcept { + if (pchunk) { + size_type new_size = chunk_t::alignSize(pchunk->len); + if (pchunk->size > new_size) { + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, pchunk->len); + chunk_t::free(pchunk); + pchunk = tmp; + } + } + return *reinterpret_cast(this); + } + + /// move assignment - shared string + string_wr& assign(string_type&& s) noexcept { + LS_COUNT_MOVE_ASSIGN + if (this != &s) { + if (pchunk != nullptr) { + chunk_t::free(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + // ensure we have owned copy of source data + if (pchunk->isShared()) { + chunk_t * tmp = pchunk->duplicate(); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + } + return *this; + } + + /// move assignment - shared string + string_wr& assign(string_wr&& s) noexcept { + LS_COUNT_MOVE_ASSIGN + if (this != &s) { + if (pchunk != nullptr) { + chunk_t::free(pchunk); + } + pchunk = s.pchunk; + s.pchunk = nullptr; + } + return *this; + } + + /// copy assignment + string_wr& assign(const string_type& s) noexcept { + LS_COUNT_COPY_ASSIGN + if (&s == this || pchunk == s.pchunk) { + // safe self assignment: do nothing + return *this; + } + if (pchunk != nullptr) { + // ignore self-assignment + chunk_t::free(pchunk); + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } else { + // this string is null + pchunk = s.pchunk; + if (pchunk) { + // assigned non-empty string + intrusive_ptr_add_ref(pchunk); + } + } + return *this; + } + + /// fragment assignment; correctly covers self-assignment; doesn't check bounds + string_wr& assign(const string_type&s, size_type offset, size_type count) noexcept { + if (s.pchunk && offset < s.pchunk->len) { + size_type avail = s.pchunk->len - offset; + if (count > avail) count = avail; + chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } else { + clear(); + } + return *this; + } + + /// assign from z-terminated string + string_wr& operator = (const char_type * s) noexcept { + return assign(s); + } + + /// assign from z-terminated string + string_wr& assign(const char_type * s) noexcept { + if (s == nullptr || !*s) { + clear(); + } else { + size_type count = str_len(s); + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + } + + /// assign from char pointer and char count + string_wr& assign(const char_type * s, size_type count) noexcept { + if (s == nullptr || !*s || count == 0) { clear(); } else { if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { @@ -657,31 +1103,191 @@ class string_wr { return *this; } - /// convert all characters of string to uppercase - string_wr& uppercase() noexcept { - if (pchunk) { - lStr_uppercase(pchunk->buf, pchunk->len); + /// convert all characters of string to uppercase + string_wr& uppercase() noexcept { + if (pchunk) { + lStr_uppercase(pchunk->buf, pchunk->len); + } + return *this; + } + + /// convert all characters of string to lowercase + string_wr& lowercase() noexcept { + if (pchunk) { + lStr_lowercase(pchunk->buf, pchunk->len); + } + return *this; + } + + /// appends decimal string representation of integer value + string_wr& appendDecimal(lInt64 n) noexcept { + char_type buf[24]; + int i=0; + int negative = 0; + if (n==0) { + return append(1, '0'); + } else if (n<0) + { + negative = 1; + n = -n; + } + for ( ; n; n/=10 ) + { + buf[i++] = '0' + (n % 10); + } + reserve(length() + i + negative); + if (negative) + append(1, '-'); + for (int j=i-1; j>=0; j--) + append(1, buf[j]); + return *this; + } + + /// appends hex string representation of integer value, no leading zeroes + string_wr& appendHex(lUInt64 n) noexcept { + if (n == 0) { + return append(1, '0'); + } + reserve(length() + 16); + bool foundNz = false; + for (int i=0; i<16; i++) { + int digit = (n >> 60) & 0x0F; + if (digit) { + foundNz = true; + } + if (foundNz) { + append(1, (static_cast("0123456789abcdef"[digit])) & 0xFF); + } + n <<= 4; + } + return *this; + } + + /// append fragment from null-terminated string; no validation of input string is performed + string_wr& append(const char_type* s, size_type count) noexcept { + if (count) { + if (!pchunk) { + pchunk = chunk_t::alloc(s, count, count); + } else { + size_type new_len = pchunk->len + count; + if (pchunk->isShared() || pchunk->size < new_len) { + // need new buffer: either shared or not enough capacity + // if s points to our own buffer, save data before releasing + bool self_append = (s == pchunk->buf); + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); + if (self_append) { + // s was our old buffer, now freed; copy from the new tmp which has the old data + std::memcpy(tmp->buf + tmp->len, tmp->buf, sizeof(char_type) * count); + } else { + std::memcpy(tmp->buf + tmp->len, s, sizeof(char_type) * count); + } + tmp->len = new_len; + tmp->buf[new_len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + std::memmove(pchunk->buf + pchunk->len, s, sizeof(char_type) * count); + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + } + } + return *this; + } + + /// append null-terminated string + string_wr& append(const char_type* s) noexcept { + if (!s || !*s) { + return *this; + } + size_type count = str_len(s); + if (count) { + append(s, count); + } + return *this; + } + + /// append another string + string_wr& append(const string_wr& s) noexcept { + if (!s.empty()) { + return append(s.pchunk->buf, s.pchunk->len); + } + return *this; + } + + /// append fragment of another string + string_wr& append(const string_wr& s, size_type offset, size_type count) noexcept { + if (!s.empty() && offset < s.pchunk->len) { + if (offset + count > s.pchunk->len) { + count = s.pchunk->len - offset; + } + return append(s.pchunk->buf + offset, count); } return *this; } - /// convert all characters of string to lowercase - string_wr& lowercase() noexcept { + /// append one or more characters + string_wr& append(size_type count, char_type c) noexcept { if (pchunk) { - lStr_lowercase(pchunk->buf, pchunk->len); + size_type new_len = pchunk->len + count; + if (pchunk->isShared() || pchunk->size < new_len) { + chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); + for (size_type i = 0; i < count; i++) { + tmp->buf[pchunk->len + i] = c; + } + tmp->len = new_len; + tmp->buf[new_len] = 0; + intrusive_ptr_release(pchunk); + pchunk = tmp; + } else { + // own buffer with enough capacity + for (size_type i = 0; i < count; i++) { + pchunk->buf[pchunk->len + i] = c; + } + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + } else { + // appending to empty string + pchunk = chunk_t::alloc(count); + for (size_type i = 0; i < count; i++) { + pchunk->buf[i] = c; + } + pchunk->len = count; + pchunk->buf[count] = 0; } return *this; } + /// append single character + string_wr& operator << (char_type ch) noexcept { return append(1, ch); } + /// append C-string + string_wr& operator << (const char_type * str) noexcept { return append(str); } + /// append string + string_wr& operator << (const string_wr & str) noexcept { return append(str); } + /// append decimal number + string_wr& operator << (const fmt::decimal v) noexcept { return appendDecimal(v.get()); } + /// append hex number + string_wr& operator << (const fmt::hex v) noexcept { return appendHex(v.get()); } + /// append single character + string_wr& operator += (char_type ch) noexcept { return append(1, ch); } + /// append C-string + string_wr& operator += (const char_type * str) noexcept { return append(str); } + /// append string + string_wr& operator += (const string_wr & str) noexcept { return append(str); } + /// append decimal number + string_wr& operator += (const fmt::decimal v) noexcept { return appendDecimal(v.get()); } + /// append hex number + string_wr& operator += (const fmt::hex v) noexcept { return appendHex(v.get()); } -private: +//private: /// member variables must follow in the same order as in string for reinterpret cast - chunk_t * pchunk {nullptr}; + //chunk_t * pchunk {nullptr}; }; template -class string { +class string : public string_ro { friend class string_wr; public: // typedefs for STL compatibility @@ -706,7 +1312,21 @@ class string { typedef string_wr& writable_ref; ///< writable (owned) string reference private: - using chunk_t = lstring_chunk_t; + + using typename string_ro::chunk_t; + using string_ro::pchunk; + +public: + using string_ro::empty; + using string_ro::length; + using string_ro::size; + using string_ro::capacity; + + using string_ro::operator []; + using string_ro::at; + using string_ro::c_str; + //using string_ro::data; + public: string() noexcept = default; @@ -864,208 +1484,23 @@ class string { /// assign from char pointer and char count string& assign(const char_type * s, size_type count) noexcept { - if (s == nullptr || !*s || count == 0) { - clear(); - } else { - if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { - // reuse existing buffer - std::memcpy(pchunk->buf, s, count * sizeof(char_type)); - pchunk->len = count; - } else { - // create new buffer - chunk_t * tmp = chunk_t::alloc(s, count, count); - if (pchunk) { - intrusive_ptr_release(pchunk); - } - pchunk = tmp; - } - } - return *this; - } - - /// index value. No index nor empty string checks. Will crash on empty string. - char_type operator [] (size_type index) const noexcept { - return pchunk->buf[index]; - } - - /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). - char_type& operator [] (size_type index) noexcept { - return pchunk->buf[index]; - } - - /// index value with bounds checking. No index nor empty string checks. Will crash on empty string. - /// Does not throw exception on index out of bounds, returns 0 instead. - char_type at(size_type index) const noexcept { - if (pchunk == nullptr || index >= pchunk->len) { - return 0; - } - return pchunk->buf[index]; - } - - /// index ref. No index nor refcount checks. Will crash on empty string. Only use on non-empy string which owns data (refcount==1). - /// Does not throw exception on index out of bounds, returns reference pointing to static 0 value instead. - char_type& at(size_type index) noexcept { - if (pchunk == nullptr || index >= pchunk->len) { - return *(static_cast(&fake_null_buffer_32)); - } - return pchunk->buf[index]; - } - - /// compare this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal - int compare(const string& s) const noexcept { - if (pchunk == s.pchunk) { - // same string - return 0; - } - size_type sz1 = length(); - size_type sz2 = s.length(); - if (sz1 == 0) { - return sz2 > 0 ? -1 : 0; - } else if (sz2 == 0) { - return 1; - } - return str_cmp_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); - } - - /// compare substring (pos..pos+n) of this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal - int compare(size_type pos, size_type n, const string& s) const noexcept { - if (!pchunk || pos >= pchunk->len) { - // this string fragment is empty - return s.empty() ? 0 : -1; - } - // clamp this fragment size - if (pos + n > pchunk->len) { - n = pchunk->len - pos; - } - // n > 0 - size_type sz2 = s.length(); - if (sz2 == 0) { - return 1; - } - return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf, sz2); - } - - /// compare substring (pos..pos+n) of this string with substring of another string (pos2..pos2+n2), returns -1 if this < s, 1 if this > s, 0 if equal - int compare(size_type pos, size_type n, const string& s, size_type pos2, size_type n2) const noexcept { - // check if another string fragment is empty - bool s_empty = (!s.pchunk || pos2 >= s.pchunk->len); - if (!pchunk || pos >= pchunk->len) { - // this string fragment is empty - return s_empty ? 0 : -1; - } - if (s_empty) { - // this string non-empty, another string is empty - return 1; - } - // both string fragments are non-empty - // clamp this fragment size - if (pos + n > pchunk->len) { - n = pchunk->len - pos; - } - // clamp other fragment size - if (pos2 + n2 > s.pchunk->len) { - n2 = s.pchunk->len - pos2; - } - // n > 0, n2 > 0 - return str_cmp_nonempty(pchunk->buf + pos, n, s.pchunk->buf + pos2, n2); - } - - /// compare substring (pos..pos+n) of this string with substring of another string s of len n2, returns -1 if this < s, 1 if this > s, 0 if equal - int compare(size_type pos, size_type n, const char_type * s, size_type n2) const noexcept { - // check if another string fragment is empty - bool s_empty = !s || !n2; - if (!pchunk || pos >= pchunk->len) { - // this string fragment is empty - return s_empty ? 0 : -1; - } - if (s_empty) { - // this string non-empty, another string is empty - return 1; - } - // both string fragments are non-empty - // clamp this fragment size - if (pos + n > pchunk->len) { - n = pchunk->len - pos; - } - // n > 0, n2 > 0 - return str_cmp_nonempty(pchunk->buf + pos, n, s, n2); - } - - /// compare substring (pos..pos+n) of this string with another string s, returns -1 if this < s, 1 if this > s, 0 if equal - int compare(size_type pos, size_type n, const char_type * s) const noexcept { - size_type n2 = (!s) ? 0 : str_len(s); - return compare(pos, n, s, n2); - } - - /// compare this string with string literal, returns -1 if this < s, 1 if this > s, 0 if equal - int compare(const char_type* s) const noexcept { - size_t sz1 = length(); - if (sz1 == 0) { - return (s != nullptr && *s != 0) ? -1 : 0; - } else if (s == nullptr || *s == 0) { - return 1; - } - return str_cmp_nonempty(pchunk->buf, sz1, s); - } - - /// returns true when strings are equal - bool operator == (const string& s) const noexcept { - return compare(s) == 0; - } - - /// returns true when strings are not equal - bool operator != (const string& s) const noexcept { - return compare(s) != 0; - } - - /// returns true when this string < s - bool operator < (const string& s) const noexcept { - return compare(s) < 0; - } - - /// returns true when this string >= s - bool operator <= (const string& s) const noexcept { - return compare(s) <= 0; - } - - /// returns true when this string > s - bool operator > (const string& s) const noexcept { - return compare(s) > 0; - } - - /// returns true when this string <= s - bool operator >= (const string& s) const noexcept { - return compare(s) >= 0; - } - - /// returns true when strings are equal - bool operator == (const char_type * s) const noexcept { - return compare(s) == 0; - } - - /// returns true when strings are not equal - bool operator != (const char_type * s) const noexcept { - return compare(s) != 0; - } - - /// returns true when this string < s - bool operator < (const char_type * s) const noexcept { - return compare(s) < 0; - } - - /// returns true when this string <= s - bool operator <= (const char_type * s) const noexcept { - return compare(s) <= 0; - } - - /// returns true when this string > s - bool operator > (const char_type * s) const noexcept { - return compare(s) > 0; - } - - /// returns true when this string >= s - bool operator >= (const char_type * s) const noexcept { - return compare(s) >= 0; + if (s == nullptr || !*s || count == 0) { + clear(); + } else { + if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + // reuse existing buffer + std::memcpy(pchunk->buf, s, count * sizeof(char_type)); + pchunk->len = count; + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(s, count, count); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + return *this; } /// sets value to null string, freeing buffer @@ -1289,17 +1724,6 @@ class string { return *this; } - /// append single character - string& operator << (char_type ch) { return append(1, ch); } - /// append C-string - string& operator << (const char_type * str) { return append(str); } - /// append string - string& operator << (const string & str) { return append(str); } - /// append decimal number - string& operator << (const fmt::decimal v) { return appendDecimal(v.get()); } - /// append hex number - string& operator << (const fmt::hex v) { return appendHex(v.get()); } - /// append fragment from null-terminated string; no validation of input string is performed string& append(const char_type* s, size_type count) noexcept { if (count) { @@ -1396,6 +1820,28 @@ class string { return *this; } + /// append single character + string& operator << (char_type ch) noexcept { return append(1, ch); } + /// append C-string + string& operator << (const char_type * str) noexcept { return append(str); } + /// append string + string& operator << (const string & str) noexcept { return append(str); } + /// append decimal number + string& operator << (const fmt::decimal v) noexcept { return appendDecimal(v.get()); } + /// append hex number + string& operator << (const fmt::hex v) noexcept { return appendHex(v.get()); } + + /// append single character + string& operator += (char_type ch) noexcept { return append(1, ch); } + /// append C-string + string& operator += (const char_type * str) noexcept { return append(str); } + /// append string + string& operator += (const string & str) noexcept { return append(str); } + /// append decimal number + string& operator += (const fmt::decimal v) noexcept { return appendDecimal(v.get()); } + /// append hex number + string& operator += (const fmt::hex v) noexcept { return appendHex(v.get()); } + /// insert from char* and size string& insert(size_type pos, const char_type* s, size_type count) noexcept { if (!count) { @@ -1712,17 +2158,6 @@ class string { return *this; } - /// return C-style null-terminated string pointer - const char_type * c_str() const noexcept { - if (pchunk == nullptr) { - return reinterpret_cast(&fake_null_buffer_32); - } else { - // enforce null-termination - const_cast(pchunk)->buf[pchunk->len] = 0; - return pchunk->buf; - } - } - /// return C-style null-terminated string pointer const char_type * data() const noexcept { return c_str(); @@ -1741,6 +2176,7 @@ class string { } /// ensures that reference count is 1; if string is null or we own it, do nothing + /// only used from data() ? what is a better place for it? void lock( size_type newsize ) noexcept { if (pchunk) { // string is not null @@ -1760,6 +2196,8 @@ class string { } } + + /// returns pointer to modifable string buffer char_type * modify() noexcept { if (!pchunk) { @@ -1780,188 +2218,6 @@ class string { return pchunk->buf; } - /// return first position of char c inside string starting from positon pos, or npos if no char is found. - size_type pos(char_type c, size_type start = 0) const noexcept { - if (pchunk) { - const char_type * buf = pchunk->buf; - size_type len = pchunk->len; - for (size_type i = start; i < len; i++) { - if (buf[i] == c) { - return i; - } - } - } - return npos; - } - - /// return first position of string s starting from positon pos, or npos if no char is found. - size_type pos(const string& s, size_type start = 0) const noexcept { - if (pchunk && s.pchunk) { - size_type len = pchunk->len; - if (start >= len) { - return npos; - } - len -= start; - size_type slen = s.pchunk->len; - if (slen > len) { - return npos; - } - const char_type * sbuf = s.pchunk->buf; - const char_type * buf = pchunk->buf + start; - size_type maxstart = len - slen; - for (size_type i = 0; i <= maxstart; i++) { - size_type found = i + start; - for (size_type j = 0; j < slen; j++) { - if (buf[i + j] != sbuf[j]) { - found = npos; - break; - } - } - if (found != npos) { - return found; - } - } - } - return npos; - } - - /// return first position of null-term string starting from positon pos, or npos if no char is found. - size_type pos(const char_type* s, size_type start = 0) const noexcept { - if (pchunk && s && s[0]) { - size_type len = pchunk->len; - if (start >= len) { - return npos; - } - len -= start; - size_type slen = str_len(s); - if (slen > len) { - return npos; - } - const char_type * buf = pchunk->buf + start; - size_type maxstart = len - slen; - for (size_type i = 0; i <= maxstart; i++) { - size_type found = i + start; - for (size_type j = 0; j < slen; j++) { - if (buf[i + j] != s[j]) { - found = npos; - break; - } - } - if (found != npos) { - return found; - } - } - } - return npos; - } - - /// return last position of null-term string s, or npos if no char is found. - size_type rpos(const char_type* s) const noexcept { - if (pchunk && s && s[0]) { - size_type len = pchunk->len; - size_type slen = str_len(s); - if (slen > len) { - return npos; - } - const char_type * buf = pchunk->buf; - size_type maxstart = len - slen; - for (size_type i = maxstart; ; i--) { - size_type found = i; - for (size_type j = 0; j < slen; j++) { - if (buf[i + j] != s[j]) { - found = npos; - break; - } - } - if (found != npos) { - return found; - } - if (i == 0) { - break; - } - } - } - return npos; - } - - /// returns true if this string starts with s[0..count-1], pass count=npos to calculate string size internally - bool startsWith(const char_type* s, size_type count = npos) const noexcept { - if (pchunk && s && s[0]) { - size_type len = pchunk->len; - size_type slen = (count == npos) ? str_len(s) : count; - if (slen > len) { - return false; - } - const char_type * buf = pchunk->buf; - for (size_type j = 0; j < slen; j++) { - if (buf[j] != s[j]) { - return false; - } - } - return true; - } - return false; - } - - /// returns true if this string ends with s[0..count-1], pass count=npos to calculate string size internally - bool endsWith(const char_type* s, size_type count = npos) const noexcept { - if (pchunk && s && s[0]) { - size_type len = pchunk->len; - size_type slen = (count == npos) ? str_len(s) : count; - if (slen > len) { - return false; - } - const char_type * buf = pchunk->buf + (len - slen); - for (size_type j = 0; j < slen; j++) { - if (buf[j] != s[j]) { - return false; - } - } - return true; - } - return false; - } - - /// returns true if this string starts with s - bool startsWith(const string& s) const noexcept { - if (pchunk && s.pchunk) { - size_type len = pchunk->len; - size_type slen = s.pchunk->len; - if (slen > len) { - return false; - } - const char_type * buf = pchunk->buf; - const char_type * sbuf = s.pchunk->buf; - for (size_type j = 0; j < slen; j++) { - if (buf[j] != sbuf[j]) { - return false; - } - } - return true; - } - return false; - } - - /// returns true if this string starts with s - bool endsWith(const string& s) const noexcept { - if (pchunk && s.pchunk) { - size_type len = pchunk->len; - size_type slen = s.pchunk->len; - if (slen > len) { - return false; - } - const char_type * buf = pchunk->buf + (len - slen); - const char_type * sbuf = s.pchunk->buf; - for (size_type j = 0; j < slen; j++) { - if (buf[j] != sbuf[j]) { - return false; - } - } - return true; - } - return false; - } - /// returns substring starting with position pos, and specified count of chars (pass npos to copy till end of string) string substr(size_type pos, size_type count = npos) const noexcept { string tmp; @@ -1983,37 +2239,8 @@ class string { s.pchunk = tmp; } - /// returns true if string is empty - bool empty() const noexcept { return pchunk == nullptr || pchunk->len==0; } - /// returns true if string is empty - bool operator ! () const noexcept { return pchunk == nullptr || pchunk->len==0; } - /// returns character count - size_type length() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns character count - size_type size() const noexcept { return pchunk == nullptr ? 0 : pchunk->len; } - /// returns maximum number of chars that can fit into buffer (there is always additional one char space for trailing 0 which is not counted) - size_type capacity() const noexcept { return pchunk==nullptr ? 0 : pchunk->size; } - - /// returns last character from string, or 0 for empty string - char_type lastChar() const noexcept { - if (pchunk && pchunk->len) { - return pchunk->buf[pchunk->len-1]; - } else { - return 0; - } - } - - /// returns first character from string, or 0 for empty string - char_type firstChar() const noexcept { - if (pchunk && pchunk->len) { - return pchunk->buf[0]; - } else { - return 0; - } - } - private: - chunk_t * pchunk {nullptr}; + //chunk_t * pchunk {nullptr}; }; // template diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 8257884d45..f670c72459 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1447,6 +1447,57 @@ void test_lstring8() { lString8 s_lower_done {"already"}; s_lower_done.lowercase(); TCHECK(s_lower_done == "already"); + + // --- operator += (char) --- + lString8 s_plus1; + s_plus1 += 'A'; + TCHECK(s_plus1 == "A"); + s_plus1 += 'B'; + TCHECK(s_plus1 == "AB"); + + // --- operator += (c-str) --- + lString8 s_plus2; + s_plus2 += "hello"; + TCHECK(s_plus2 == "hello"); + s_plus2 += " world"; + TCHECK(s_plus2 == "hello world"); + + // --- operator += (string) --- + lString8 s_plus3; + s_plus3 += lString8{"test"}; + TCHECK(s_plus3 == "test"); + s_plus3 += s_plus3; + TCHECK(s_plus3 == "testtest"); + + // --- operator += (fmt::decimal) --- + lString8 s_plus4; + s_plus4 += fmt::decimal(42); + TCHECK(s_plus4 == "42"); + s_plus4 += fmt::decimal(-99); + TCHECK(s_plus4 == "42-99"); + + // --- operator += (fmt::hex) --- + lString8 s_plus5; + s_plus5 += fmt::hex(255); + TCHECK(s_plus5 == "ff"); + s_plus5 += fmt::hex(0xABC); + TCHECK(s_plus5 == "ffabc"); + + // --- string_wr operator += --- + lString8 s_wr_plus {"base"}; + auto& s_wr = s_wr_plus.writableRef(); + s_wr += " extended"; + TCHECK(s_wr_plus == "base extended"); + s_wr += '!'; + TCHECK(s_wr_plus == "base extended!"); + + // --- string_wr operator << --- + lString8 s_wr_sl {"start"}; + auto& s_wr2 = s_wr_sl.writableRef(); + s_wr2 << " middle"; + TCHECK(s_wr_sl == "start middle"); + s_wr2 << fmt::decimal(123); + TCHECK(s_wr_sl == "start middle123"); } void test_writable_refs_lString8() { From a200bea643d2ece81c5c4ba686029a2bb5a32526 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 19:56:58 +0100 Subject: [PATCH 28/36] add missing modify methods to wr_string, cover with tests --- tests/src/lvstring2.h | 353 +++++++++++++++++++++++++++++++++--- tests/src/lvstring2test.cpp | 294 ++++++++++++++++++++++++++++++ 2 files changed, 624 insertions(+), 23 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index e5d3b07be0..d2bcf7ff4c 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -407,6 +407,8 @@ extern lChar32 fake_null_buffer_32; /// allows modification without reference counting checks template class string_ro { + friend class string_wr; + friend class string; public: // typedefs for STL compatibility typedef char_type value_type; ///< character type @@ -877,8 +879,8 @@ class string_wr : public string_ro { typedef value_type & reference; ///< reference to char type typedef const value_type * const_pointer; ///< pointer to const char type typedef const value_type & const_reference; ///< reference to const char type - typedef string string_type; ///< normal shared string reference type - typedef string string_ro_type; ///< normal shared string reference type + typedef string_ro ro_string_type; ///< readonly string type + typedef string string_type; ///< normal shared string type // constant for not found / unspecified position static const size_type npos = -1; @@ -983,7 +985,7 @@ class string_wr : public string_ro { } /// move assignment - shared string - string_wr& assign(string_type&& s) noexcept { + string_wr& assign(ro_string_type&& s) noexcept { LS_COUNT_MOVE_ASSIGN if (this != &s) { if (pchunk != nullptr) { @@ -1015,7 +1017,7 @@ class string_wr : public string_ro { } /// copy assignment - string_wr& assign(const string_type& s) noexcept { + string_wr& assign(const ro_string_type& s) noexcept { LS_COUNT_COPY_ASSIGN if (&s == this || pchunk == s.pchunk) { // safe self assignment: do nothing @@ -1041,13 +1043,13 @@ class string_wr : public string_ro { } /// fragment assignment; correctly covers self-assignment; doesn't check bounds - string_wr& assign(const string_type&s, size_type offset, size_type count) noexcept { + string_wr& assign(const ro_string_type&s, size_type offset, size_type count) noexcept { if (s.pchunk && offset < s.pchunk->len) { size_type avail = s.pchunk->len - offset; if (count > avail) count = avail; chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); if (pchunk) { - intrusive_ptr_release(pchunk); + chunk_t::free(pchunk); } pchunk = tmp; } else { @@ -1067,7 +1069,7 @@ class string_wr : public string_ro { clear(); } else { size_type count = str_len(s); - if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->size >= count && pchunk->size / 2 < count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; @@ -1075,11 +1077,12 @@ class string_wr : public string_ro { // create new buffer chunk_t * tmp = chunk_t::alloc(s, count, count); if (pchunk) { - intrusive_ptr_release(pchunk); + chunk_t::free(pchunk); } pchunk = tmp; } } + return *this; } /// assign from char pointer and char count @@ -1087,7 +1090,7 @@ class string_wr : public string_ro { if (s == nullptr || !*s || count == 0) { clear(); } else { - if (pchunk != nullptr && pchunk->isOwn() && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->size >= count && pchunk->size / 2 < count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; @@ -1095,7 +1098,7 @@ class string_wr : public string_ro { // create new buffer chunk_t * tmp = chunk_t::alloc(s, count, count); if (pchunk) { - intrusive_ptr_release(pchunk); + chunk_t::free(pchunk); } pchunk = tmp; } @@ -1170,7 +1173,7 @@ class string_wr : public string_ro { pchunk = chunk_t::alloc(s, count, count); } else { size_type new_len = pchunk->len + count; - if (pchunk->isShared() || pchunk->size < new_len) { + if (pchunk->size < new_len) { // need new buffer: either shared or not enough capacity // if s points to our own buffer, save data before releasing bool self_append = (s == pchunk->buf); @@ -1183,7 +1186,7 @@ class string_wr : public string_ro { } tmp->len = new_len; tmp->buf[new_len] = 0; - intrusive_ptr_release(pchunk); + chunk_t::free(pchunk); pchunk = tmp; } else { std::memmove(pchunk->buf + pchunk->len, s, sizeof(char_type) * count); @@ -1230,14 +1233,14 @@ class string_wr : public string_ro { string_wr& append(size_type count, char_type c) noexcept { if (pchunk) { size_type new_len = pchunk->len + count; - if (pchunk->isShared() || pchunk->size < new_len) { + if (pchunk->size < new_len) { chunk_t * tmp = chunk_t::alloc(pchunk->buf, pchunk->len, new_len); for (size_type i = 0; i < count; i++) { tmp->buf[pchunk->len + i] = c; } tmp->len = new_len; tmp->buf[new_len] = 0; - intrusive_ptr_release(pchunk); + chunk_t::free(pchunk); pchunk = tmp; } else { // own buffer with enough capacity @@ -1281,9 +1284,312 @@ class string_wr : public string_ro { /// append hex number string_wr& operator += (const fmt::hex v) noexcept { return appendHex(v.get()); } -//private: - /// member variables must follow in the same order as in string for reinterpret cast - //chunk_t * pchunk {nullptr}; + + /// insert from char* and size + string_wr& insert(size_type pos, const char_type* s, size_type count) noexcept { + if (!count) { + return *this; + } + if (!pchunk) { + // insert into empty string + pchunk = chunk_t::alloc(s, count, count); + } else { + if (pos > pchunk->len) { + pos = pchunk->len; + } + size_type new_len = pchunk->len + count; + size_type tail_len = pchunk->len - pos; + if (new_len > pchunk->size) { + // create copy + chunk_t* tmp = chunk_t::alloc(new_len); + if (pos > 0) { + // copy head + std::memcpy(tmp->buf, pchunk->buf, sizeof(char_type) * pos); + } + // copy inserted content + std::memcpy(tmp->buf + pos, s, sizeof(char_type) * count); + // copy tail if needed + if (tail_len) { + // copy tail + std::memcpy(tmp->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + tmp->len = new_len; + chunk_t::free(pchunk); + pchunk = tmp; + } else { + // insert in-place + if (tail_len) { + // move tail by count chars + std::memmove(pchunk->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + // copy inserted content + std::memcpy(pchunk->buf + pos, s, sizeof(char_type) * count); + } + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + return *this; + } + + /// insert from null-terminated cont char* + string_wr& insert(size_type pos, const char_type* s) noexcept { + if (!s || !*s) { + // attempt inserting empty string + return *this; + } + return insert(pos, s, str_len(s)); + } + + /// insert from string (no self-insert protection) + string_wr& insert(size_type pos, const ro_string_type& s) noexcept { + if (s.empty()) { + // attempt inserting empty string + return *this; + } + return insert(pos, s.pchunk->buf, s.pchunk->len); + } + + /// insert one or more characters + string_wr& insert(size_type pos, size_type count, char_type c) noexcept { + if (!count) { + return *this; + } + if (!pchunk) { + // insert into empty string + pchunk = chunk_t::alloc(count); + for (size_type i = 0; i < count; i++) { + pchunk->buf[i] = c; + } + pchunk->len = count; + pchunk->buf[count] = 0; + } else { + if (pos > pchunk->len) { + pos = pchunk->len; + } + size_type new_len = pchunk->len + count; + size_type tail_len = pchunk->len - pos; + if (new_len > pchunk->size) { + // create copy + chunk_t* tmp = chunk_t::alloc(new_len); + if (pos > 0) { + // copy head + std::memcpy(tmp->buf, pchunk->buf, sizeof(char_type) * pos); + } + // copy inserted content + for (size_type i = 0; i < count; i++) { + tmp->buf[pos + i] = c; + } + // copy tail if needed + if (tail_len) { + // copy tail + std::memcpy(tmp->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + tmp->len = new_len; + chunk_t::free(pchunk); + pchunk = tmp; + } else { + // insert in-place + if (tail_len) { + // move tail by count chars + std::memmove(pchunk->buf + pos + count, pchunk->buf + pos, sizeof(char_type) * tail_len); + } + // copy inserted content + for (size_type i = 0; i < count; i++) { + pchunk->buf[pos + i] = c; + } + } + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + } + return *this; + } + + /// replace part of string with count chars from char* + string_wr& replace(size_type pos, size_type n, const char_type * s, size_type count) noexcept { + if (!count) { + return *this; + } + if (empty()) { + return assign(s, count); + } else { + // need to modify + size_type old_len = pchunk->len; + if (pos > old_len) { + pos = old_len; + } + if (pos + n > old_len) { + n = old_len - pos; + } + if (!n) { + // delegate to insert + return insert(pos, s, count); + } + if (pos == old_len) { + // use append + return append(s, count); + } + size_type new_len = old_len + count - n; + size_type tail_len = old_len - (pos + n); + if (new_len > pchunk->size) { + // need to create copy -- no space or has other refs + chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); + // move tail to the end of buffer + std::memmove(tmp->buf + new_len - tail_len, tmp->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + std::memcpy(tmp->buf + pos, s, sizeof(char_type) * count); + // replace this string with new + chunk_t::free(pchunk); + pchunk = tmp; + } else { + // may edit in-place + // move tail to the end of buffer + std::memmove(pchunk->buf + new_len - tail_len, pchunk->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + std::memcpy(pchunk->buf + pos, s, sizeof(char_type) * count); + } + // update length + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + return *this; + } + } + + /// replace part of string with count chars from char* + string_wr& replace(size_type pos, size_type n, size_type count, char_type c) noexcept { + if (!count) { + // nothing to insert + return *this; + } + if (empty()) { + return append(count, c); + } else { + // need to modify + size_type old_len = pchunk->len; + if (pos > old_len) { + pos = old_len; + } + if (pos + n > old_len) { + n = old_len - pos; + } + if (!n) { + // delegate to insert + return insert(pos, count, c); + } + if (pos == old_len) { + // use append + return append(count, c); + } + size_type new_len = old_len + count - n; + size_type tail_len = old_len - (pos + n); + if (new_len > pchunk->size) { + // need to create copy -- no space or has other refs + chunk_t * tmp = chunk_t::alloc(pchunk->buf, new_len, new_len); + // move tail to the end of buffer + std::memmove(tmp->buf + new_len - tail_len, tmp->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + for (size_type i = 0; i < count; i++) { + tmp->buf[pos + i] = c; + } + // replace this string with new + chunk_t::free(pchunk); + pchunk = tmp; + } else { + // may edit in-place + // move tail to the end of buffer + std::memmove(pchunk->buf + new_len - tail_len, pchunk->buf + old_len - tail_len, sizeof(char_type) * tail_len); + // fill gap with inserted string + for (size_type i = 0; i < count; i++) { + pchunk->buf[pos + i] = c; + } + } + // update length + pchunk->len = new_len; + pchunk->buf[new_len] = 0; + return *this; + } + } + + /// replace part of string with count chars from char* + string_wr& replace(size_type pos, size_type n, const char_type * s) noexcept { + if (!s || !*s) { + // nothing to insert + return *this; + } + size_type count = str_len(s); + return replace(pos, n, s, count); + } + + /// replace part of string with count chars from char* + string_wr& replace(size_type pos, size_type n, const ro_string_type& s) noexcept { + if (s.empty()) { + // nothing to insert + return *this; + } + return replace(pos, n, s.pchunk->buf, s.pchunk->len); + } + + /// replace part of string with count chars from char* + string_wr& replace(size_type pos, size_type n, const ro_string_type& s, size_type offset, size_type count) noexcept { + if (s.empty()) { + // nothing to insert + return *this; + } + size_type s_len = s.pchunk->len; + if (offset >= s_len) { + // source data indexes outside source string bounds + return *this; + } + // ensure source range is inside string - truncate if needed + if (offset + count > s_len) { + count = s_len - offset; + } + return replace(pos, n, s.pchunk->buf + offset, count); + } + + /// replace all occurences of character replaceWhat with character replaceTo + string_wr& replace(char_type replaceWhat, char_type replaceTo) noexcept { + if (empty()) { + // nothing to replace + return *this; + } + size_type len = pchunk->len; + char_type * buf = pchunk->buf; + for (size_type i = 0; i < len; i++) { + if (buf[i] == replaceWhat) { + // first match found: replace is needed + // own buffer : replace in-place + buf[i++] = replaceTo; + while (i < len) { + if (buf[i] == replaceWhat) { + buf[i] = replaceTo; + } + i++; + } + break; + } + } + return *this; + } + + /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated + string_wr& erase(size_type offset, size_type count) noexcept { + if (pchunk && offset < pchunk->len && count) { + if (offset + count > pchunk->len) { + // truncate requested erased fragment size to not exceed bounds + count = pchunk->len - offset; + } + size_type new_len = pchunk->len - count; + size_type tail_start = offset + count; + size_type tail_len = pchunk->len - tail_start; + // erase inplace + if (tail_len) { + std::memmove(pchunk->buf + offset, pchunk->buf + tail_start, sizeof(char_type) * tail_len); + } + pchunk->len = new_len; + pchunk->buf[pchunk->len] = 0; + } + return *this; + } }; template @@ -1310,6 +1616,7 @@ class string : public string_ro { // COW types typedef string_wr writable_string; ///< writable (owned) string typedef string_wr& writable_ref; ///< writable (owned) string reference + typedef string_ro ro_string_type; ///< readonly string type private: @@ -1441,7 +1748,7 @@ class string : public string_ro { } /// fragment assignment; correctly covers self-assignment; doesn't check bounds - string& assign(const string&s, size_type offset, size_type count) noexcept { + string& assign(const ro_string_type&s, size_type offset, size_type count) noexcept { if (s.pchunk && offset < s.pchunk->len) { size_type avail = s.pchunk->len - offset; if (count > avail) count = avail; @@ -1769,7 +2076,7 @@ class string : public string_ro { } /// append another string - string& append(const string& s) noexcept { + string& append(const ro_string_type& s) noexcept { if (!s.empty()) { return append(s.pchunk->buf, s.pchunk->len); } @@ -1777,7 +2084,7 @@ class string : public string_ro { } /// append fragment of another string - string& append(const string& s, size_type offset, size_type count) noexcept { + string& append(const ro_string_type& s, size_type offset, size_type count) noexcept { if (!s.empty() && offset < s.pchunk->len) { if (offset + count > s.pchunk->len) { count = s.pchunk->len - offset; @@ -1898,7 +2205,7 @@ class string : public string_ro { } /// insert from string (no self-insert protection) - string& insert(size_type pos, const string& s) noexcept { + string& insert(size_type pos, const ro_string_type& s) noexcept { if (s.empty()) { // attempt inserting empty string return *this; @@ -2090,7 +2397,7 @@ class string : public string_ro { } /// replace part of string with count chars from char* - string& replace(size_type pos, size_type n, const string& s) noexcept { + string& replace(size_type pos, size_type n, const ro_string_type& s) noexcept { if (s.empty()) { // nothing to insert return *this; @@ -2099,7 +2406,7 @@ class string : public string_ro { } /// replace part of string with count chars from char* - string& replace(size_type pos, size_type n, const string& s, size_type offset, size_type count) noexcept { + string& replace(size_type pos, size_type n, const ro_string_type& s, size_type offset, size_type count) noexcept { if (s.empty()) { // nothing to insert return *this; diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index f670c72459..f36046a3ef 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1501,12 +1501,306 @@ void test_lstring8() { } void test_writable_refs_lString8() { + // --- writableRef() basic --- lString8 s1 {"Original string"}; lString8 s1_copy = s1; TCHECK(s1 == s1_copy); auto& s1_wr = s1.writableRef(100); TCHECK(s1_wr.length() == s1_copy.length()); + // --- data() const --- + lString8 s_data {"test"}; + auto& s_data_wr = s_data.writableRef(); + TCHECK(s_data_wr.data() != nullptr); + TCHECK(strcmp(s_data_wr.data(), "test") == 0); + + // --- data() non-const --- + lString8 s_data_nc {"mutable"}; + auto& s_data_nc_wr = s_data_nc.writableRef(); + char * p_data = s_data_nc_wr.data(); + TCHECK(p_data != nullptr); + p_data[0] = 'M'; + TCHECK(s_data_nc == "Mutable"); + + // --- clear() --- + lString8 s_clear {"clear me"}; + auto& s_clear_wr = s_clear.writableRef(); + s_clear_wr.clear(); + TCHECK(s_clear.empty()); + TCHECK(s_clear.length() == 0); + TCHECK(s_clear.capacity() == 0); + + // --- reset() --- + lString8 s_reset {"reset me"}; + auto& s_reset_wr = s_reset.writableRef(); + s_reset_wr.reset(50); + TCHECK(s_reset.empty()); + TCHECK(s_reset.length() == 0); + TCHECK(s_reset.capacity() >= 50); + + // --- reserve() --- + lString8 s_reserve {"reserve"}; + auto& s_reserve_wr = s_reserve.writableRef(); + s_reserve_wr.reserve(200); + TCHECK(s_reserve == "reserve"); + TCHECK(s_reserve.capacity() >= 200); + + // --- reserve() on empty --- + lString8 s_reserve_empty; + auto& s_reserve_empty_wr = s_reserve_empty.writableRef(); + s_reserve_empty_wr.reserve(100); + TCHECK(s_reserve_empty.empty()); + TCHECK(s_reserve_empty.capacity() >= 100); + + // --- pack() --- + lString8 s_pack {"pack"}; + auto& s_pack_wr = s_pack.writableRef(); + s_pack_wr.reserve(500); + TCHECK(s_pack.capacity() >= 500); + auto& s_pack_str = s_pack_wr.pack(); + TCHECK(s_pack_str == "pack"); + TCHECK(s_pack_str.capacity() < 500); + + // --- assign(ro_string_type&&) --- + lString8 s_assign_move {"move me"}; + lString8 s_assign_move_src {"source"}; + auto& s_assign_move_wr = s_assign_move.writableRef(); + s_assign_move_wr.assign(std::move(s_assign_move_src)); + TCHECK(s_assign_move == "source"); + TCHECK(s_assign_move_src.empty()); + + // --- assign(string_wr&&) --- + lString8 s_assign_wr_move {"target"}; + auto& s_assign_wr_move_wr = s_assign_wr_move.writableRef(); + lString8 s_src {"source2"}; + auto& s_src_wr = s_src.writableRef(); + s_assign_wr_move_wr.assign(std::move(s_src_wr)); + TCHECK(s_assign_wr_move == "source2"); + TCHECK(s_src.empty()); + + // --- assign(ro_string_type&) --- + lString8 s_assign_copy {"target2"}; + lString8 s_assign_copy_src {"copy source"}; + auto& s_assign_copy_wr = s_assign_copy.writableRef(); + s_assign_copy_wr.assign(s_assign_copy_src); + TCHECK(s_assign_copy == "copy source"); + TCHECK(s_assign_copy_src == "copy source"); + + // --- assign(ro_string_type&, offset, count) --- + lString8 s_assign_frag {"target3"}; + lString8 s_assign_frag_src {"abcdef"}; + auto& s_assign_frag_wr = s_assign_frag.writableRef(); + s_assign_frag_wr.assign(s_assign_frag_src, 1, 3); + TCHECK(s_assign_frag == "bcd"); + + // --- assign(char_type*) --- + lString8 s_assign_cstr {"old"}; + auto& s_assign_cstr_wr = s_assign_cstr.writableRef(); + s_assign_cstr_wr.assign("new string"); + TCHECK(s_assign_cstr == "new string"); + + // --- assign(char_type*, count) --- + lString8 s_assign_cstr_cnt {"old2"}; + auto& s_assign_cstr_cnt_wr = s_assign_cstr_cnt.writableRef(); + s_assign_cstr_cnt_wr.assign("partial", 3); + TCHECK(s_assign_cstr_cnt == "par"); + + // --- operator=(char_type*) --- + lString8 s_op_eq {"old3"}; + auto& s_op_eq_wr = s_op_eq.writableRef(); + s_op_eq_wr = "assigned"; + TCHECK(s_op_eq == "assigned"); + + // --- uppercase() --- + lString8 s_upper {"hello"}; + auto& s_upper_wr = s_upper.writableRef(); + s_upper_wr.uppercase(); + TCHECK(s_upper == "HELLO"); + + // --- lowercase() --- + lString8 s_lower {"HELLO"}; + auto& s_lower_wr = s_lower.writableRef(); + s_lower_wr.lowercase(); + TCHECK(s_lower == "hello"); + + // --- appendDecimal() --- + lString8 s_app_dec {"num: "}; + auto& s_app_dec_wr = s_app_dec.writableRef(); + s_app_dec_wr.appendDecimal(42); + TCHECK(s_app_dec == "num: 42"); + + // --- appendHex() --- + lString8 s_app_hex {"0x"}; + auto& s_app_hex_wr = s_app_hex.writableRef(); + s_app_hex_wr.appendHex(0xFF); + TCHECK(s_app_hex == "0xff"); + + // --- append(char_type*, count) --- + lString8 s_app_cnt {"hello"}; + auto& s_app_cnt_wr = s_app_cnt.writableRef(); + s_app_cnt_wr.append(" world", 6); + TCHECK(s_app_cnt == "hello world"); + + // --- append(char_type*) --- + lString8 s_app_cstr {"hello"}; + auto& s_app_cstr_wr = s_app_cstr.writableRef(); + s_app_cstr_wr.append(" world"); + TCHECK(s_app_cstr == "hello world"); + + // --- append(string_wr&) --- + lString8 s_app_wr1 {"hello"}; + lString8 s_app_wr2 {" world"}; + auto& s_app_wr1_wr = s_app_wr1.writableRef(); + auto& s_app_wr2_wr = s_app_wr2.writableRef(); + s_app_wr1_wr.append(s_app_wr2_wr); + TCHECK(s_app_wr1 == "hello world"); + + // --- append(string_wr&, offset, count) --- + lString8 s_app_wr_off {"start"}; + lString8 s_app_wr_off_src {"abcdef"}; + auto& s_app_wr_off_wr = s_app_wr_off.writableRef(); + auto& s_app_wr_off_src_wr = s_app_wr_off_src.writableRef(); + s_app_wr_off_wr.append(s_app_wr_off_src_wr, 1, 3); + TCHECK(s_app_wr_off == "startbcd"); + + // --- append(count, char) --- + lString8 s_app_n {"fill:"}; + auto& s_app_n_wr = s_app_n.writableRef(); + s_app_n_wr.append(3, 'X'); + TCHECK(s_app_n == "fill:XXX"); + + // --- operator << (char) --- + lString8 s_sl_ch {"a"}; + auto& s_sl_ch_wr = s_sl_ch.writableRef(); + s_sl_ch_wr << 'b'; + TCHECK(s_sl_ch == "ab"); + + // --- operator << (c-str) --- + lString8 s_sl_cstr {"hello"}; + auto& s_sl_cstr_wr = s_sl_cstr.writableRef(); + s_sl_cstr_wr << " world"; + TCHECK(s_sl_cstr == "hello world"); + + // --- operator << (string_wr) --- + lString8 s_sl_wr1 {"hello"}; + lString8 s_sl_wr2 {" world"}; + auto& s_sl_wr1_wr = s_sl_wr1.writableRef(); + auto& s_sl_wr2_wr = s_sl_wr2.writableRef(); + s_sl_wr1_wr << s_sl_wr2_wr; + TCHECK(s_sl_wr1 == "hello world"); + + // --- operator << (fmt::decimal) --- + lString8 s_sl_dec {"val:"}; + auto& s_sl_dec_wr = s_sl_dec.writableRef(); + s_sl_dec_wr << fmt::decimal(123); + TCHECK(s_sl_dec == "val:123"); + + // --- operator << (fmt::hex) --- + lString8 s_sl_hex {"0x"}; + auto& s_sl_hex_wr = s_sl_hex.writableRef(); + s_sl_hex_wr << fmt::hex(255); + TCHECK(s_sl_hex == "0xff"); + + // --- operator += (char) --- + lString8 s_plus_ch {"a"}; + auto& s_plus_ch_wr = s_plus_ch.writableRef(); + s_plus_ch_wr += 'b'; + TCHECK(s_plus_ch == "ab"); + + // --- operator += (c-str) --- + lString8 s_plus_cstr {"hello"}; + auto& s_plus_cstr_wr = s_plus_cstr.writableRef(); + s_plus_cstr_wr += " world"; + TCHECK(s_plus_cstr == "hello world"); + + // --- operator += (string_wr) --- + lString8 s_plus_wr1 {"hello"}; + lString8 s_plus_wr2 {" world"}; + auto& s_plus_wr1_wr = s_plus_wr1.writableRef(); + auto& s_plus_wr2_wr = s_plus_wr2.writableRef(); + s_plus_wr1_wr += s_plus_wr2_wr; + TCHECK(s_plus_wr1 == "hello world"); + + // --- operator += (fmt::decimal) --- + lString8 s_plus_dec {"val:"}; + auto& s_plus_dec_wr = s_plus_dec.writableRef(); + s_plus_dec_wr += fmt::decimal(456); + TCHECK(s_plus_dec == "val:456"); + + // --- operator += (fmt::hex) --- + lString8 s_plus_hex {"0x"}; + auto& s_plus_hex_wr = s_plus_hex.writableRef(); + s_plus_hex_wr += fmt::hex(0xAB); + TCHECK(s_plus_hex == "0xab"); + + // --- insert(char_type*, count) --- + lString8 s_ins_cnt {"helloworld"}; + auto& s_ins_cnt_wr = s_ins_cnt.writableRef(); + s_ins_cnt_wr.insert(5, " ", 1); + TCHECK(s_ins_cnt == "hello world"); + + // --- insert(char_type*) --- + lString8 s_ins_cstr {"helloworld"}; + auto& s_ins_cstr_wr = s_ins_cstr.writableRef(); + s_ins_cstr_wr.insert(5, " "); + TCHECK(s_ins_cstr == "hello world"); + + // --- insert(ro_string_type&) --- + lString8 s_ins_ro {"helloworld"}; + lString8 s_ins_ro_src {" "}; + auto& s_ins_ro_wr = s_ins_ro.writableRef(); + s_ins_ro_wr.insert(5, s_ins_ro_src); + TCHECK(s_ins_ro == "hello world"); + + // --- insert(count, char) --- + lString8 s_ins_n {"helloworld"}; + auto& s_ins_n_wr = s_ins_n.writableRef(); + s_ins_n_wr.insert(5, 3, '-'); + TCHECK(s_ins_n == "hello---world"); + + // --- replace(char_type*, count) --- + lString8 s_rep_cnt {"hello world"}; + auto& s_rep_cnt_wr = s_rep_cnt.writableRef(); + s_rep_cnt_wr.replace(6, 5, "earth", 5); + TCHECK(s_rep_cnt == "hello earth"); + + // --- replace(count, char) --- + lString8 s_rep_n {"hello world"}; + auto& s_rep_n_wr = s_rep_n.writableRef(); + s_rep_n_wr.replace(6, 5, 5, 'X'); + TCHECK(s_rep_n == "hello XXXXX"); + + // --- replace(char_type*) --- + lString8 s_rep_cstr {"hello world"}; + auto& s_rep_cstr_wr = s_rep_cstr.writableRef(); + s_rep_cstr_wr.replace(6, 5, "earth"); + TCHECK(s_rep_cstr == "hello earth"); + + // --- replace(ro_string_type&) --- + lString8 s_rep_ro {"hello world"}; + lString8 s_rep_ro_src {"earth"}; + auto& s_rep_ro_wr = s_rep_ro.writableRef(); + s_rep_ro_wr.replace(6, 5, s_rep_ro_src); + TCHECK(s_rep_ro == "hello earth"); + + // --- replace(ro_string_type&, offset, count) --- + lString8 s_rep_ro_off {"hello world"}; + lString8 s_rep_ro_off_src {"abcdefgh"}; + auto& s_rep_ro_off_wr = s_rep_ro_off.writableRef(); + s_rep_ro_off_wr.replace(6, 5, s_rep_ro_off_src, 2, 3); + TCHECK(s_rep_ro_off == "hello cde"); + + // --- replace(char, char) --- + lString8 s_rep_cc {"hello world"}; + auto& s_rep_cc_wr = s_rep_cc.writableRef(); + s_rep_cc_wr.replace('l', 'L'); + TCHECK(s_rep_cc == "heLLo worLd"); + + // --- erase() --- + lString8 s_erase {"hello world"}; + auto& s_erase_wr = s_erase.writableRef(); + s_erase_wr.erase(5, 6); + TCHECK(s_erase == "hello"); } void test_lstring2() { From db04da4fd188b4ead742f749d6f60b63e216352b Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 22:59:31 +0100 Subject: [PATCH 29/36] implement itoa() and atoi() in new strings implementation --- tests/src/lvstring2.h | 177 ++++++++++++++++++++++++++++++++++++ tests/src/lvstring2test.cpp | 81 +++++++++++++++++ 2 files changed, 258 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index d2bcf7ff4c..bd84741090 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -34,6 +34,11 @@ void lStr_lowercase( lChar32 * str, int len ); void lStr_lowercase( lChar16 * str, int len ); void lStr_lowercase( lChar8 * str, int len ); +// returns 0..15 if c is hex digit, -1 otherwise +int hexDigit( int c ); +// converts 0..15 to 0..f +char toHexDigit( int c ); + namespace lv { #ifdef DEBUG_TRACK_LSTRING2_ALLOC @@ -858,7 +863,127 @@ class string_ro { return false; } + /// convert to integer + int atoi() const noexcept { + int sgn = 1; + int n = 0; + const char_type * s = c_str(); + // skip whitespace + while (*s == ' ' || *s == '\t') { + s++; + } + if (*s == '-') { + sgn = -1; + s++; + } else if (*s == '+') { + s++; + } + while (*s>='0' && *s<='9') { + n = n * 10 + ( (*s)-'0' ); + s++; + } + return (sgn>0)?n:-n; + } + + /// convert to integer + bool atoi(int& n) const noexcept { + n = 0; + int sgn = 1; + const char_type * s = c_str(); + // allow leading whitespace -- not an error + while (*s == ' ' || *s == '\t') + s++; + // support hex with prefix 0x + if ( s[0]=='0' && s[1]=='x') { + s+=2; + for (;*s;) { + int d = hexDigit(*s++); + if ( d>=0 ) + n = (n<<4) | d; + } + return true; + } + if (*s == '-') { + sgn = -1; + s++; + } + else if (*s == '+') { + s++; + } + if ( !(*s>='0' && *s<='9') ) + return false; + while (*s>='0' && *s<='9') { + if (n > 0x7fffffff/10) { + return false; + } + n = n * 10 + ( (*s++)-'0' ); + } + if ( sgn<0 ) + n = -n; + return *s=='\0' || *s==' ' || *s=='\t'; + } + + /// convert string to 64-bit integer, supports +/- for decimal, and 0x prefix for hex, skips leading whitespace + bool atoi(lInt64& n) const noexcept { + n = 0; + int sgn = 1; + const char_type * s = c_str(); + // allow leading whitespace -- not an error + while (*s == ' ' || *s == '\t') + s++; + // support hex with prefix 0x + if ( s[0]=='0' && s[1]=='x') { + s+=2; + for (;*s;) { + int d = hexDigit(*s++); + if ( d>=0 ) + n = (n<<4) | d; + } + return true; + } + if (*s == '-') { + sgn = -1; + s++; + } + else if (*s == '+') { + s++; + } + if ( !(*s>='0' && *s<='9') ) + return false; + while (*s>='0' && *s<='9') { + if (n > 0x7fffffffffffffffll/10) { + return false; + } + n = n * 10 + ( (*s++)-'0' ); + } + if ( sgn<0 ) + n = -n; + return *s=='\0' || *s==' ' || *s=='\t'; + } + /// convert to 64 bit integer + lInt64 atoi64() const noexcept { + int sgn = 1; + lInt64 n = 0; + const char_type * s = c_str(); + while (*s == ' ' || *s == '\t') + s++; + if (*s == '-') + { + sgn = -1; + s++; + } + else if (*s == '+') + { + s++; + } + while (*s>='0' && *s<='9') + { + n = n * 10 + ( (*s)-'0' ); + s++; + } + return (sgn>0) ? n : -n; + } protected: /// member variables must follow in the same order as in string for reinterpret cast @@ -2546,6 +2671,58 @@ class string : public string_ro { s.pchunk = tmp; } + /// constructs string representation of integer + static string itoa( int n ) { + char_type buf[16]; + size_type i=0; + if (n==0) { + buf[i++] = '0'; + //cs8("0"); + } else { + if (n<0) { + buf[i++] = '-'; + n = -n; + } + for ( ; n; n/=10 ) { + buf[i++] = '0' + (n%10); + } + } + return string(buf, i); + } + /// constructs string representation of unsigned integer + static string itoa( unsigned int n ) { + char_type buf[16]; + size_type i=0; + if (n==0) { + buf[i++] = '0'; + //cs8("0"); + } else { + for ( ; n; n/=10 ) { + buf[i++] = '0' + (n%10); + } + } + return string(buf, i); + } + // constructs string representation of 64 bit integer + static string itoa( lInt64 n ) { + char_type buf[16]; + size_type i=0; + if (n==0) { + buf[i++] = '0'; + //cs8("0"); + } else { + if (n<0) { + buf[i++] = '-'; + n = -n; + } + for ( ; n; n/=10 ) { + buf[i++] = '0' + (n%10); + } + } + return string(buf, i); + } + + private: //chunk_t * pchunk {nullptr}; }; diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index f36046a3ef..aa166ce639 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1498,6 +1498,87 @@ void test_lstring8() { TCHECK(s_wr_sl == "start middle"); s_wr2 << fmt::decimal(123); TCHECK(s_wr_sl == "start middle123"); + + // --- atoi() --- + lString8 s_atoi1 {"12345"}; + TCHECK(s_atoi1.atoi() == 12345); + lString8 s_atoi2 {"-999"}; + TCHECK(s_atoi2.atoi() == -999); + lString8 s_atoi3 {"0"}; + TCHECK(s_atoi3.atoi() == 0); + lString8 s_atoi4 {" +42"}; + TCHECK(s_atoi4.atoi() == 42); + lString8 s_atoi5 {}; + TCHECK(s_atoi5.atoi() == 0); + + // --- atoi(int&) --- + { + int n; + lString8 s_ai {"6789"}; + TCHECK(s_ai.atoi(n) && n == 6789); + lString8 s_ai2 {"-123"}; + TCHECK(s_ai2.atoi(n) && n == -123); + lString8 s_ai3 {"0x1A"}; + TCHECK(s_ai3.atoi(n) && n == 0x1A); + lString8 s_ai4 {" 0xFF"}; + TCHECK(s_ai4.atoi(n) && n == 255); + lString8 s_ai5 {"abc"}; + TCHECK(!s_ai5.atoi(n)); + lString8 s_ai6 {" "}; + TCHECK(!s_ai6.atoi(n)); + lString8 s_ai7 {}; + TCHECK(!s_ai7.atoi(n)); + lString8 s_ai8 {"123abc"}; + TCHECK(s_ai8.atoi(n) && n == 123); + lString8 s_ai9 {" 123 "}; + TCHECK(s_ai9.atoi(n) && n == 123); + lString8 s_ai10 {"0"}; + TCHECK(s_ai10.atoi(n) && n == 0); + } + + // --- atoi(lInt64&) --- + { + lInt64 n; + lString8 s_ai64 {"1234567890123"}; + TCHECK(s_ai64.atoi(n) && n == 1234567890123LL); + lString8 s_ai64_2 {"-999"}; + TCHECK(s_ai64_2.atoi(n) && n == -999); + lString8 s_ai64_3 {"0xABADCAFE"}; + TCHECK(s_ai64_3.atoi(n) && n == 0xABADCAFELL); + lString8 s_ai64_4 {" 0xFF"}; + TCHECK(s_ai64_4.atoi(n) && n == 255); + lString8 s_ai64_5 {"abc"}; + TCHECK(!s_ai64_5.atoi(n)); + } + + // --- atoi64() --- + lString8 s_atoi64 {"1234567890123"}; + TCHECK(s_atoi64.atoi64() == 1234567890123LL); + lString8 s_atoi64_2 {"-999"}; + TCHECK(s_atoi64_2.atoi64() == -999); + lString8 s_atoi64_3 {"0"}; + TCHECK(s_atoi64_3.atoi64() == 0); + lString8 s_atoi64_4 {}; + TCHECK(s_atoi64_4.atoi64() == 0); + + // --- itoa(int) static --- + TCHECK(lString8::itoa(42) == "42"); + TCHECK(lString8::itoa(-42) == "-42"); + TCHECK(lString8::itoa(0) == "0"); + TCHECK(lString8::itoa(1) == "1"); + TCHECK(lString8::itoa(-1) == "-1"); + TCHECK(lString8::itoa(2147483647) == "2147483647"); + TCHECK(lString8::itoa(-2147483647) == "-2147483647"); + + // --- itoa(unsigned) static --- + TCHECK(lString8::itoa(0U) == "0"); + TCHECK(lString8::itoa(1U) == "1"); + TCHECK(lString8::itoa(4294967295U) == "4294967295"); + + // --- itoa(lInt64) static --- + TCHECK(lString8::itoa(0LL) == "0"); + TCHECK(lString8::itoa(-123LL) == "-123"); + TCHECK(lString8::itoa(9223372036854775807LL) == "9223372036854775807"); } void test_writable_refs_lString8() { From 392d13d4a0d4b753f4ca61051a24f2079da6c42c Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Mon, 27 Jul 2026 23:33:12 +0100 Subject: [PATCH 30/36] fix itoa/atoi --- tests/src/lvstring2.h | 32 ++++++++++++++++++++++++++++---- tests/src/lvstring2test.cpp | 4 +++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index bd84741090..d26e6967a6 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -965,7 +965,7 @@ class string_ro { lInt64 atoi64() const noexcept { int sgn = 1; lInt64 n = 0; - const char_type * s = c_str(); + const lChar8 * s = c_str(); while (*s == ' ' || *s == '\t') s++; if (*s == '-') @@ -2675,18 +2675,27 @@ class string : public string_ro { static string itoa( int n ) { char_type buf[16]; size_type i=0; + bool negative = false; if (n==0) { buf[i++] = '0'; //cs8("0"); } else { if (n<0) { - buf[i++] = '-'; + negative = true; n = -n; } for ( ; n; n/=10 ) { buf[i++] = '0' + (n%10); } } + if (negative) + buf[i++] = '-'; + // reverse string + for (size_type j = 0; j < i/2; j++) { + char_type tmp = buf[j]; + buf[j] = buf[i - 1 - j]; + buf[i - 1 - j] = tmp; + } return string(buf, i); } /// constructs string representation of unsigned integer @@ -2701,24 +2710,39 @@ class string : public string_ro { buf[i++] = '0' + (n%10); } } + // reverse string + for (size_type j = 0; j < i/2; j++) { + char_type tmp = buf[j]; + buf[j] = buf[i - 1 - j]; + buf[i - 1 - j] = tmp; + } return string(buf, i); } // constructs string representation of 64 bit integer static string itoa( lInt64 n ) { - char_type buf[16]; + char_type buf[24]; size_type i=0; + bool negative = false; if (n==0) { buf[i++] = '0'; //cs8("0"); } else { if (n<0) { - buf[i++] = '-'; + negative = true; n = -n; } for ( ; n; n/=10 ) { buf[i++] = '0' + (n%10); } } + if (negative) + buf[i++] = '-'; + // reverse string + for (size_type j = 0; j < i/2; j++) { + char_type tmp = buf[j]; + buf[j] = buf[i - 1 - j]; + buf[i - 1 - j] = tmp; + } return string(buf, i); } diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index aa166ce639..9e7d65072d 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1528,7 +1528,7 @@ void test_lstring8() { TCHECK(!s_ai6.atoi(n)); lString8 s_ai7 {}; TCHECK(!s_ai7.atoi(n)); - lString8 s_ai8 {"123abc"}; + lString8 s_ai8 {"123 abc"}; TCHECK(s_ai8.atoi(n) && n == 123); lString8 s_ai9 {" 123 "}; TCHECK(s_ai9.atoi(n) && n == 123); @@ -1579,6 +1579,8 @@ void test_lstring8() { TCHECK(lString8::itoa(0LL) == "0"); TCHECK(lString8::itoa(-123LL) == "-123"); TCHECK(lString8::itoa(9223372036854775807LL) == "9223372036854775807"); + + printf("exiting of lString8 test\n"); } void test_writable_refs_lString8() { From 5a013077932ac5f00987cf3c6bc4ba401798b6a9 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 28 Jul 2026 10:59:08 +0100 Subject: [PATCH 31/36] implement getHash() and trim() --- tests/src/lvstring2.h | 106 +++++++++++++++++++++++++++++++++++- tests/src/lvstring2test.cpp | 96 +++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 3 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index d26e6967a6..df7760ef4d 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -427,6 +427,7 @@ class string_ro { // constant for not found / unspecified position static const size_type npos = -1; + static const size_type STRING_HASH_MULT = 31; typedef lstring_chunk_t chunk_t; protected: @@ -511,6 +512,16 @@ class string_ro { } } + /// calculate hash code for string (empty string has hash==0) + size_type getHash() const noexcept { + lUInt32 res = 0; + if (pchunk) { + for (size_type i=0; i < pchunk->len; i++) + res = res * STRING_HASH_MULT + pchunk->buf[i]; + } + return res; + } + // ============================================================================ // compare methods // ============================================================================ @@ -1046,12 +1057,13 @@ class string_wr : public string_ro { } /// sets value to null string, freeing buffer - void clear() noexcept { + string_wr& clear() noexcept { if (pchunk != nullptr) { // use free instead of release because we own this string and ref count should be 1 anyway chunk_t::free(pchunk); pchunk = nullptr; } + return *this; } /// resets length to zero, preparing to modification with reserved size @@ -1096,6 +1108,50 @@ class string_wr : public string_ro { return *this; } + // Trim methods + + /// remove leading and trailing spaces and tabs + string_wr& trim() noexcept { + // do nothing for empty line + if (!pchunk || !pchunk->len) { + return *this; + } + // we own buffer + size_type firstns = 0; + size_type len = pchunk->len; + char_type * buf = pchunk->buf; + for (; + firstns < len && + (buf[firstns] == ' ' || + buf[firstns] == '\t'); + ++firstns) + ; + if (firstns >= len) { + // only whitespace chars in the string + pchunk->len = 0; + pchunk->buf[0] = 0; + return *this; + } + size_type lastns = len - 1; + for (; + //lastns>0 && // this check is not needed - we know that string contains non-empty-space char(s) + (buf[lastns]==' ' || buf[lastns]=='\t'); + --lastns) + ; + size_type newlen = (size_type)(lastns + 1 - firstns); + if (newlen == len) { + // nothing to trim + return *this; + } + if (firstns) { + std::memmove( buf, buf + firstns, newlen*sizeof(char_type) ); + } + buf[newlen] = 0; + pchunk->len = newlen; + return *this; + } + + /// compact buffer if possible -- free unused buffer space; returned reference is a normal string string_type& pack() noexcept { if (pchunk) { @@ -1936,11 +1992,12 @@ class string : public string_ro { } /// sets value to null string, freeing buffer - void clear() noexcept { + string& clear() noexcept { if (pchunk != nullptr) { intrusive_ptr_release(pchunk); pchunk = nullptr; } + return *this; } /// resets length to zero, preparing to modification with reserved size @@ -2077,6 +2134,51 @@ class string : public string_ro { return *this; } + /// remove leading and trailing spaces and tabs + string& trim() noexcept { + // do nothing for empty line + if (!pchunk || !pchunk->len) { + return *this; + } + size_type firstns = 0; + size_type len = pchunk->len; + char_type * buf = pchunk->buf; + for (; + firstns < len && + (buf[firstns] == ' ' || + buf[firstns] == '\t'); + ++firstns) + ; + if (firstns >= len) { + // only whitespace chars in the string + return clear(); + } + size_type lastns = len - 1; + for (; + //lastns>0 && // this check is not needed - we know that string contains non-empty-space char(s) + (buf[lastns]==' ' || buf[lastns]=='\t'); + --lastns) + ; + size_type newlen = (size_type)(lastns + 1 - firstns); + if (newlen == len) { + // nothing to trim + return *this; + } + if (pchunk->isOwn()) { + if (firstns) { + std::memmove( buf, buf + firstns, newlen*sizeof(char_type) ); + } + buf[newlen] = 0; + pchunk->len = newlen; + } else { + chunk_t * tmp = chunk_t::alloc(buf + firstns, newlen, newlen); + intrusive_ptr_release(pchunk); + pchunk = tmp; + } + return *this; + } + + /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated string& erase(size_type offset, size_type count) noexcept { if (pchunk && offset < pchunk->len && count) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 9e7d65072d..d76367bed5 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -1580,6 +1580,76 @@ void test_lstring8() { TCHECK(lString8::itoa(-123LL) == "-123"); TCHECK(lString8::itoa(9223372036854775807LL) == "9223372036854775807"); + // --- getHash() empty --- + lString8 s_gh_empty; + TCHECK(s_gh_empty.getHash() == 0); + + // --- getHash() non-empty --- + lString8 s_gh1 {"hello"}; + lString8 s_gh2 {"hello"}; + TCHECK(s_gh1.getHash() == s_gh2.getHash()); + + // --- getHash() different --- + lString8 s_gh3 {"world"}; + TCHECK(s_gh1.getHash() != s_gh3.getHash()); + + // --- getHash() consistent --- + size_t h1 = s_gh1.getHash(); + size_t h2 = s_gh1.getHash(); + TCHECK(h1 == h2); + + // --- trim() leading spaces --- + lString8 s_tr_l {" hello"}; + s_tr_l.trim(); + TCHECK(s_tr_l == "hello"); + + // --- trim() trailing spaces --- + lString8 s_tr_t {"hello "}; + s_tr_t.trim(); + TCHECK(s_tr_t == "hello"); + + // --- trim() both --- + lString8 s_tr_b {" hello "}; + s_tr_b.trim(); + TCHECK(s_tr_b == "hello"); + + // --- trim() no spaces --- + lString8 s_tr_n {"hello"}; + s_tr_n.trim(); + TCHECK(s_tr_n == "hello"); + + // --- trim() all spaces --- + lString8 s_tr_as {" "}; + s_tr_as.trim(); + TCHECK(s_tr_as.empty()); + + // --- trim() empty --- + lString8 s_tr_e; + s_tr_e.trim(); + TCHECK(s_tr_e.empty()); + + // --- trim() tabs --- + lString8 s_tr_tab {"\thello\t"}; + s_tr_tab.trim(); + TCHECK(s_tr_tab == "hello"); + + // --- trim() mixed tabs/spaces --- + lString8 s_tr_mix {" \t hello \t "}; + s_tr_mix.trim(); + TCHECK(s_tr_mix == "hello"); + + // --- trim() single char --- + lString8 s_tr_sc {" x "}; + s_tr_sc.trim(); + TCHECK(s_tr_sc == "x"); + + // --- trim() on shared string (COW) --- + lString8 s_tr_shared {" shared copy "}; + lString8 s_tr_shared_copy = s_tr_shared; + s_tr_shared_copy.trim(); + TCHECK(s_tr_shared_copy == "shared copy"); + TCHECK(s_tr_shared == " shared copy "); + printf("exiting of lString8 test\n"); } @@ -1879,11 +1949,35 @@ void test_writable_refs_lString8() { s_rep_cc_wr.replace('l', 'L'); TCHECK(s_rep_cc == "heLLo worLd"); - // --- erase() --- + // --- erase() --- lString8 s_erase {"hello world"}; auto& s_erase_wr = s_erase.writableRef(); s_erase_wr.erase(5, 6); TCHECK(s_erase == "hello"); + + // --- trim() via writableRef --- + lString8 s_wr_tr {" hello "}; + auto& s_wr_tr_ref = s_wr_tr.writableRef(); + s_wr_tr_ref.trim(); + TCHECK(s_wr_tr == "hello"); + + // --- trim() via writableRef all spaces --- + lString8 s_wr_tr_as {" "}; + auto& s_wr_tr_as_ref = s_wr_tr_as.writableRef(); + s_wr_tr_as_ref.trim(); + TCHECK(s_wr_tr_as.empty()); + + // --- trim() via writableRef empty --- + lString8 s_wr_tr_e; + auto& s_wr_tr_e_ref = s_wr_tr_e.writableRef(); + s_wr_tr_e_ref.trim(); + TCHECK(s_wr_tr_e.empty()); + + // --- trim() via writableRef no-op --- + lString8 s_wr_tr_no {"hello"}; + auto& s_wr_tr_no_ref = s_wr_tr_no.writableRef(); + s_wr_tr_no_ref.trim(); + TCHECK(s_wr_tr_no == "hello"); } void test_lstring2() { From a01ffa75668f26e44d8521c34c891231dd4ae509 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 28 Jul 2026 15:53:08 +0100 Subject: [PATCH 32/36] utf decode support --- tests/src/lvstring2.h | 269 ++++++++++++++++++++++++++- tests/src/lvstring2test.cpp | 361 +++++++++++++++++++++++++++++++++++- tests/src/lvstring2test.h | 1 + 3 files changed, 626 insertions(+), 5 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index df7760ef4d..9e181c67bc 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -27,12 +27,16 @@ #define DEBUG_TRACK_LSTRING2_ALLOC +/// helper functions from lvstring void lStr_uppercase( lChar32 * str, int len ); void lStr_uppercase( lChar16 * str, int len ); void lStr_uppercase( lChar8 * str, int len ); void lStr_lowercase( lChar32 * str, int len ); void lStr_lowercase( lChar16 * str, int len ); void lStr_lowercase( lChar8 * str, int len ); +lUInt16 lGetCharProps( lChar32 ch ); +bool isAlNum(lChar32 ch); + // returns 0..15 if c is hex digit, -1 otherwise int hexDigit( int c ); @@ -404,6 +408,191 @@ struct lstring_chunk_t { char_type buf[0]; // z-string }; +/// returns number of elements to convert unicode codepoint into char_type utf. +/// * for sizeof(char_type)==4 returns 1 +/// * for sizeof(char_type)==2 returns number of utf16 codepoints(1 to 2) +/// * for sizeof(char_type)==2 returns number of utf8 codepoints(1 to 4) +template +inline lInt32 utfCodePointSize(lChar32 c) noexcept { + if constexpr(sizeof(char_type) == 1) { + // calculate number of utf8 codepoints + return (c < 0x80) ? 1 + : (c < 0x800) ? 2 + : (c < 0x10000) ? 3 + : 4; + } else if constexpr(sizeof(char_type) == 2) { + // calculate number of utf16 codepoints + return (c >= 0x10000 || (c >= 0xD800 && c <= 0xDFFF)) ? 2 : 1; + } else { + // assuming this is 4-byte type representing unicode codepoint + return 1; + } +} + +//constexpr lChar32 UNICODE_END_OF_STREAM = 0xFFFFFFFF; + +/// read one unicode point from utf stream from position s and advances position of s +/// * supports 1-byte (utf8), 2-byte (utf16) and 4-byte (utf32) inputs +/// * returns unicode codepoint value if it's decoded successfully +/// * returns invalid_char value if invalid data found in input buffer (advances pointer anyway) +/// * overlong sequence (e.g. 7-bit code encoded as 3 bytes) is not considered as invalid character +/// * returns end_of_stream value if reading position reaches end of buffer (pend) +template +inline lChar32 utfReadCodePoint(const char_type* &s, const char_type* pend) noexcept { + if (s >= pend) { + return end_of_stream; + } + lChar32 c0 = (*s++); + if constexpr(sizeof(char_type) == 1) { + // calculate number of utf8 codepoints + if (c0 < 0x80) { + // single-byte utf8 character + return c0; + } + if ((c0 & 0xC0) == 0x80) { + // byte 10xx_xxxx is unexpected begin of utf8 sequence, return invalid char + return invalid_char; + } + if (s >= pend) { + // end of stream reached while waiting for additional bytes of codepoint -- indicate wrong character + // next call to this function will return end_of_stream because we reached end of stream + return invalid_char; + } + lChar32 c1 = (*s++); + if ((c1 & 0xC0) != 0x80) { + // byte 10xx_xxxx is expected for all additional bytes of utf8 sequence + return invalid_char; + } + if ((c0 & 0xE0) == 0xC0) { + // 2-byte codepoint + return ((c0 & 0x1F) << 6) | (c1 & 0x3f); + } + // need one more byte + if (s >= pend) { + // end of stream reached while waiting for additional bytes of codepoint -- indicate wrong character + // next call to this function will return end_of_stream because we reached end of stream + return invalid_char; + } + // read byte + lChar32 c2 = (*s++); + if ((c2 & 0xC0) != 0x80) { + // byte 10xx_xxxx is expected for all additional bytes of utf8 sequence + return invalid_char; + } + if ((c0 & 0xF0) == 0xE0) { + // 3-byte codepoint + c0 = ((c0 & 0x0F) << 12) | ((c1 & 0x3f) << 6) | (c2 & 0x3f); + // validate value to be in valid unicode range + return (c0 > 0x10FFFF || (c0 >= 0xD800 && c0 <= 0xDFFF)) ? invalid_char : c0; + } + // need one more byte + if (s >= pend) { + // end of stream reached while waiting for additional bytes of codepoint -- indicate wrong character + // next call to this function will return end_of_stream because we reached end of stream + return invalid_char; + } + // read byte + lChar32 c3 = (*s++); + if ((c3 & 0xC0) != 0x80) { + // byte 10xx_xxxx is expected for all additional bytes of utf8 sequence + return invalid_char; + } + if ((c0 & 0xF8) == 0xF0) { + // 3-byte codepoint + c0 = ((c0 & 0x07) << 18) | ((c1 & 0x3f) << 12) | ((c2 & 0x3f) << 6) | (c3 & 0x3f); + // validate value to be in valid unicode range + return (c0 > 0x10FFFF || (c0 >= 0xD800 && c0 <= 0xDFFF)) ? invalid_char : c0; + } + // incorrect utf8 sequence - 4-byte max length is expected + return invalid_char; + } else if constexpr(sizeof(char_type) == 2) { + // calculate number of utf16 codepoints + if (c0 >= 0xD800 && c0 <= 0xDFFF) { + // c0 is either low or high part of surrogate pair + if (c0 >= 0xDC00) { + // found low part of surrogate pair - invalid position inside utf16 stream -- skip it + return invalid_char; + } + // c0 is high part of surrogate pair, should be followed by low part in range (c0 >= 0xDC00 && c0 <= 0xDFFF) + if (s >= pend) { + // there must follow low part of surrogate pair, but end of stream reached + // return invalid_char -- next call to this function will return end_of_stream + return invalid_char; + } + // reading low part of surrogate pair + lChar32 c1 = (*s++); + if (c1 < 0xDC00 || c1 > 0xDFFF) { + // range is out of low part of surrogate pair + return invalid_char; + } + // c0 (high part of surrogate) must be in range 0xD800..0xDBFF + // c1 (low part of surrogate) must be in range 0xDC00..0xDFFF + return (((c0 & 0x3FF) << 10) | (c1 & 0x3FF)) + 0x10000; + } + // single-item character, not a surrogate pair return as is + return c0; + } else { + // assuming this is 4-byte type representing unicode codepoint + return (c0 > 0x10FFFF || (c0 >= 0xD800 && c0 <= 0xDFFF)) ? invalid_char: c0; + } +} + +/// counts characters in utf-encoded buffer +/// * supports 1-byte (utf8), 2-byte (utf16) and 4-byte (utf32) inputs +template +inline lUInt32 utfCodePointCount(const char_type* pstart, const char_type* pend) noexcept { + lUInt32 count = 0; + for(;;) { + lChar32 ch = utfReadCodePoint(pstart, pend); + if (ch == end_of_stream) { + return count; + } + count++; + } +} + +template +struct CodepointReadIterator { + struct EndOfStream {}; + CodepointReadIterator(const char_type * start, const char_type * end) noexcept : pos(start), pend(end) { } + CodepointReadIterator(const CodepointReadIterator& v) = default; + ~CodepointReadIterator() = default; + + lChar32 operator*() const noexcept { + const char_type * tmp = pos; + return utfReadCodePoint(tmp, pend); + } + CodepointReadIterator& operator++() { + utfReadCodePoint(pos, pend); + return *this; + } + bool operator != (EndOfStream) const { + return pos < pend; + } + private: + const char_type * pos; + const char_type * pend; +}; + +template +struct UtfDecodeRange { + + using iterator_t = CodepointReadIterator; + using sentinel_t = typename iterator_t::EndOfStream; + + UtfDecodeRange(const char_type * start, const char_type * end) noexcept : pstart(start), pend(end) { } + UtfDecodeRange(const UtfDecodeRange& v) = default; + ~UtfDecodeRange() noexcept = default; + + iterator_t begin() const { return iterator_t(pstart, pend); } + sentinel_t end() const { return sentinel_t(); } // Returns a completely different type! + + private: + const char_type * pstart; + const char_type * pend; +}; + + extern lChar32 fake_null_buffer_32; @@ -427,11 +616,16 @@ class string_ro { // constant for not found / unspecified position static const size_type npos = -1; - static const size_type STRING_HASH_MULT = 31; + static lChar32 constexpr invalid_unicode = 0xfffffffe; + static lChar32 constexpr end_of_stream = 0xffffffff; + typedef UtfDecodeRange utf_decode_range_t; typedef lstring_chunk_t chunk_t; + protected: + static const size_type STRING_HASH_MULT = 31; + /// disabled: always created as a synonim of the string with the same template parameters string_ro() = default; /// don't free pchunk - it's owned by string @@ -512,6 +706,74 @@ class string_ro { } } + /// forward iterator - support for range by char_type + const char_type * begin() const noexcept { + if (pchunk) { + return pchunk->buf; + } else { + return nullptr; + } + } + + /// forward iterator - support for range by char_type + const char_type * end() const noexcept { + if (pchunk) { + return pchunk->buf + pchunk->len; + } else { + return nullptr; + } + } + + /// returns range object to read content as unicode codepoints -- use to iterate by codepoints + /// * for sizeof(char_type) == 1 the content is interpreted as utf8 sequence + /// * for sizeof(char_type) == 2 the content is interpreted as utf16 sequence + /// * for sizeof(char_type) == 4 the content is interpreted as utf32 sequence + /// if decoded codepoint value is not in valid unicode range, invalid_unicode value is returned + utf_decode_range_t unicodeRange() const noexcept { + if (pchunk && pchunk->len) { + return utf_decode_range_t(pchunk->buf, pchunk->buf + pchunk->len); + } else { + return utf_decode_range_t(nullptr, nullptr); + } + } + + /// returns subrange object to read content as unicode codepoints -- use to iterate by codepoints + /// Decoding starts from item [start] and ends at item [start + len - 1] + /// * for sizeof(char_type) == 1 the content is interpreted as utf8 sequence + /// * for sizeof(char_type) == 2 the content is interpreted as utf16 sequence + /// * for sizeof(char_type) == 4 the content is interpreted as utf32 sequence + /// if decoded codepoint value is not in valid unicode range, invalid_unicode value is returned + utf_decode_range_t unicodeRange(size_type start, size_type len) const noexcept { + if (pchunk && start < pchunk->len && len) { + if (start + len > pchunk->len) { + len = pchunk->len - start; + } + return utf_decode_range_t(pchunk->buf + start, pchunk->buf + start + len); + } else { + return utf_decode_range_t(nullptr, nullptr); + } + } + + /// assuming this string as utf-encoded (utf8/utf16/utf32), returns number of unicode codepoints inside the string. + size_type codePointCount() const noexcept { + if (pchunk && pchunk->len) { + return utfCodePointCount(pchunk->buf, pchunk->buf + pchunk->len); + } + return 0; + } + + /// for substring assuming this string as utf-encoded (utf8/utf16/utf32), returns number of unicode codepoints inside the string. + size_type codePointCount(size_type start, size_type len) const noexcept { + if (pchunk && start < pchunk->len && len) { + if (start + len > pchunk->len) { + len = pchunk->len - start; + } + return utfCodePointCount(pchunk->buf + start, pchunk->buf + start + len); + } + return 0; + } + + /// calculate hash code for string (empty string has hash==0) size_type getHash() const noexcept { lUInt32 res = 0; @@ -2178,6 +2440,11 @@ class string : public string_ro { return *this; } + /// trim non-alphanumeric characters from beginning and end of string + string& trimNonAlpha() noexcept { + // TODO: implement + return *this; + } /// erases fragment from string; if requested fragment exceeds string bounds, it's size is truncated string& erase(size_type offset, size_type count) noexcept { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index d76367bed5..ce616b0dbd 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -17,10 +17,10 @@ static int test_errors = 0; void test_lstring2_chunks() { - // struct size checks - printf("sizeof(lstring8_chunk_t) = %llu\n", sizeof(lstring8_chunk_t)); - printf("sizeof(lstring16_chunk_t) = %llu\n", sizeof(lstring16_chunk_t)); - printf("sizeof(lstring32_chunk_t) = %llu\n", sizeof(lstring32_chunk_t)); + // struct size checks + printf("sizeof(lstring8_chunk_t) = %lu\n", sizeof(lstring8_chunk_t)); + printf("sizeof(lstring16_chunk_t) = %lu\n", sizeof(lstring16_chunk_t)); + printf("sizeof(lstring32_chunk_t) = %lu\n", sizeof(lstring32_chunk_t)); TCHECK(sizeof(lstring8_chunk_t) == sizeof(lstring16_chunk_t)); TCHECK(sizeof(lstring16_chunk_t) == sizeof(lstring32_chunk_t)); TCHECK(sizeof(lstring8_chunk_t) == 12); @@ -1980,6 +1980,358 @@ void test_writable_refs_lString8() { TCHECK(s_wr_tr_no == "hello"); } +void test_lstring2_unicode() { + printf("running test_lstring2_unicode()\n"); + + // --- begin()/end() basic --- + { + lString8 s_begin{"hello"}; + int count = 0; + for (auto c : s_begin) + count++; + TCHECK(count == 5); + } + + // --- begin()/end() empty --- + { + lString8 s_begin_empty; + int count = 0; + for (auto c : s_begin_empty) + count++; + TCHECK(count == 0); + } + + // --- begin()/end() with embedded null --- + { + lString8 s_embed{"ab\0cd", 5, 5}; + int count = 0; + for (auto c : s_embed) + count++; + TCHECK(count == 5); + } + + // --- unicodeRange() ASCII --- + { + lString8 s_ascii{"hello"}; + int count = 0; + for (auto cp : s_ascii.unicodeRange()) { + TCHECK(cp < 0x80); + count++; + } + TCHECK(count == 5); + } + + // --- unicodeRange() utf8 2-byte --- + { + // U+00E9 "é" = 0xC3 0xA9 in UTF-8 + lString8 s_2byte{"\xC3\xA9"}; + int count = 0; + for (auto cp : s_2byte.unicodeRange()) { + TCHECK(cp == 0xE9); + count++; + } + TCHECK(count == 1); + TCHECK(s_2byte.length() == 2); // 2 utf8 bytes + } + + // --- unicodeRange() utf8 3-byte --- + { + // U+20AC "€" = 0xE2 0x82 0xAC in UTF-8 + lString8 s_3byte{"\xE2\x82\xAC"}; + int count = 0; + for (auto cp : s_3byte.unicodeRange()) { + TCHECK(cp == 0x20AC); + count++; + } + TCHECK(count == 1); + TCHECK(s_3byte.length() == 3); + } + + // --- unicodeRange() utf8 4-byte --- + { + // U+1D11E "𝄞" = 0xF0 0x9D 0x84 0x9E in UTF-8 + lString8 s_4byte{"\xF0\x9D\x84\x9E"}; + int count = 0; + for (auto cp : s_4byte.unicodeRange()) { + TCHECK(cp == 0x1D11E); + count++; + } + TCHECK(count == 1); + TCHECK(s_4byte.length() == 4); + } + + // --- unicodeRange() mixed utf8 --- + { + // "héllo" — h(1) é(2) l(1) l(1) o(1) = 6 bytes, 5 codepoints + lString8 s_mixed{"h\xC3\xA9llo"}; + int count = 0; + lChar32 expected[] = {'h', 0xE9, 'l', 'l', 'o'}; + int i = 0; + for (auto cp : s_mixed.unicodeRange()) { + TCHECK(cp == expected[i]); + i++; + count++; + } + TCHECK(count == 5); + TCHECK(s_mixed.length() == 6); + } + + // --- unicodeRange() invalid utf8 (continuation byte without leader) --- + { + lString8 s_invalid{"\x80"}; + for (auto cp : s_invalid.unicodeRange()) + TCHECK(cp == lString8::invalid_unicode); + } + + // --- unicodeRange() invalid utf8 (unexpected non-continuation) --- + { + // 0xC3 expects continuation, but next is 'A' (0x41 has no 0x80 bit set) + lString8 s_badseq{"\xC3" "A"}; + for (auto cp : s_badseq.unicodeRange()) + TCHECK(cp == lString8::invalid_unicode); + } + + // --- unicodeRange() overlong sequence (3-byte for 2-byte char) --- + { + // overlong encoding of '/' (U+002F) as 0xE0 0x80 0xAF + // overlong encoding considered as valid + lString8 s_overlong{"\xE0\x80\xAF"}; + for (auto cp : s_overlong.unicodeRange()) + TCHECK(cp == '/'); //lString8::invalid_unicode); + } + + // --- unicodeRange() empty string --- + { + lString8 s_empty; + int count = 0; + for (auto cp : s_empty.unicodeRange()) + count++; + TCHECK(count == 0); + } + + // --- unicodeRange() subrange --- + { + lString8 s_sub{"hello world"}; + int count = 0; + for (auto cp : s_sub.unicodeRange(6, 5)) { + count++; + } + TCHECK(count == 5); + } + + // --- unicodeRange() subrange clamped --- + { + lString8 s_sub2{"abcdef"}; + int count = 0; + for (auto cp : s_sub2.unicodeRange(4, 10)) { + count++; + } + TCHECK(count == 2); // only "ef" remains + } + + // --- unicodeRange() subrange empty (start out of bounds) --- + { + lString8 s_sub3{"abc"}; + int count = 0; + for (auto cp : s_sub3.unicodeRange(5, 3)) + count++; + TCHECK(count == 0); + } + + // --- utfCodePointSize for lChar8 --- + TCHECK((utfCodePointSize(0x0061)) == 1); + TCHECK((utfCodePointSize(0x00E9)) == 2); + TCHECK((utfCodePointSize(0x20AC)) == 3); + TCHECK((utfCodePointSize(0x1D11E)) == 4); + + // --- utfCodePointSize for lChar16 --- + TCHECK((utfCodePointSize(0x0061)) == 1); + TCHECK((utfCodePointSize(0xD800)) == 2); // lone surrogate → needs pair + TCHECK((utfCodePointSize(0x1D11E)) == 2); + + // --- utfCodePointSize for lChar32 --- + TCHECK((utfCodePointSize(0x0061)) == 1); + TCHECK((utfCodePointSize(0x10FFFF)) == 1); + + // --- utfReadCodePoint end_of_stream --- + { + const lChar8 * s = nullptr; + const lChar8 * end = nullptr; + TCHECK(utfReadCodePoint(s, end) == 0xFFFFFFFFu); + } + + // --- utfReadCodePoint ASCII through lString16 --- + { + // test the template with lChar16 type explicitly + lChar16 buf[] = { 'h', 'e', 'l', 'l', 'o' }; + const lChar16 * s = buf; + const lChar16 * end = buf + 5; + TCHECK(utfReadCodePoint(s, end) == 'h'); + TCHECK(s == buf + 1); + } + + // --- utfReadCodePoint lChar32 --- + { + lChar32 buf[] = { 0x20AC, 0x1D11E }; + const lChar32 * s = buf; + const lChar32 * end = buf + 2; + TCHECK(utfReadCodePoint(s, end) == 0x20AC); + TCHECK(s == buf + 1); + TCHECK(utfReadCodePoint(s, end) == 0x1D11E); + TCHECK(s == buf + 2); + TCHECK(utfReadCodePoint(s, end) == 0xFFFFFFFFu); + } + + // --- CodepointReadIterator basic --- + { + lString8 s_iter{"hi"}; + int count = 0; + for (auto cp : s_iter.unicodeRange()) { + (void)cp; + count++; + } + TCHECK(count == 2); + } + + // --- CodepointReadIterator with utf8 --- + { + lString8 s_utf{"\xC3\xA9\xC3\xA0"}; // éà + int count = 0; + for (auto cp : s_utf.unicodeRange()) { + (void)cp; + count++; + } + TCHECK(count == 2); + } + + // --- unicodeRange() subrange --- + { + lString8 s_sub{"hello world"}; + int count = 0; + for (auto cp : s_sub.unicodeRange(6, 5)) { + (void)cp; + count++; + } + TCHECK(count == 5); + } + + // --- unicodeRange() subrange clamped --- + { + lString8 s_sub2{"abcdef"}; + int count = 0; + for (auto cp : s_sub2.unicodeRange(4, 10)) { + (void)cp; + count++; + } + TCHECK(count == 2); // only "ef" remains + } + + // --- unicodeRange() subrange empty --- + { + lString8 s_sub3{"abc"}; + int count = 0; + for (auto cp : s_sub3.unicodeRange(5, 3)) + count++; + TCHECK(count == 0); + } + + // --- codePointCount() empty --- + { + lString8 s; + TCHECK(s.codePointCount() == 0); + } + + // --- codePointCount() ASCII --- + { + lString8 s{"hello"}; + TCHECK(s.codePointCount() == 5); + } + + // --- codePointCount() utf8 2-byte --- + { + lString8 s{"\xC3\xA9"}; // U+00E9 + TCHECK(s.codePointCount() == 1); + TCHECK(s.length() == 2); // 2 utf8 bytes + } + + // --- codePointCount() utf8 3-byte --- + { + lString8 s{"\xE2\x82\xAC"}; // U+20AC + TCHECK(s.codePointCount() == 1); + TCHECK(s.length() == 3); + } + + // --- codePointCount() utf8 4-byte --- + { + lString8 s{"\xF0\x9D\x84\x9E"}; // U+1D11E + TCHECK(s.codePointCount() == 1); + TCHECK(s.length() == 4); + } + + // --- codePointCount() mixed utf8 --- + { + // "héllo" — h(1) é(2) l(1) l(1) o(1) = 6 bytes, 5 codepoints + lString8 s{"h\xC3\xA9llo"}; + TCHECK(s.codePointCount() == 5); + TCHECK(s.length() == 6); + } + + // --- codePointCount() invalid utf8 --- + { + // invalid continuation byte — still counted as 1 codepoint (invalid) + lString8 s{"\x80"}; + TCHECK(s.codePointCount() == 1); + } + + // --- codePointCount() all whitespace --- + { + lString8 s{" \t "}; + TCHECK(s.codePointCount() == 6); + } + + // --- codePointCount() subrange --- + { + lString8 s{"hello world"}; + TCHECK(s.codePointCount(6, 5) == 5); // "world" + } + + // --- codePointCount() subrange clamped --- + { + lString8 s{"abcdef"}; + TCHECK(s.codePointCount(4, 10) == 2); // "ef" + } + + // --- codePointCount() subrange empty --- + { + lString8 s{"abc"}; + TCHECK(s.codePointCount(5, 3) == 0); // start OOB + } + + // --- utfCodePointCount free function, lChar8 --- + { + const lChar8 buf[] = "hello"; + TCHECK(utfCodePointCount(buf, buf + 5) == 5); + } + + // --- utfCodePointCount free function, lChar16 --- + { + lChar16 buf[] = { 'h', 'e', 'l', 'l', 'o' }; + TCHECK(utfCodePointCount(buf, buf + 5) == 5); + } + + // --- utfCodePointCount free function, lChar32 --- + { + lChar32 buf[] = { 0x20AC, 0x1D11E, 'A' }; + TCHECK(utfCodePointCount(buf, buf + 3) == 3); + } + + // --- utfCodePointCount empty --- + { + const lChar8 * p = nullptr; + TCHECK(utfCodePointCount(p, p) == 0); + } +} + void test_lstring2() { printf("New strings library tests\n"); test_lstring2_chunks(); @@ -1987,6 +2339,7 @@ void test_lstring2() { test_lstring8(); DUMP_ALLOC_STATS("after test_lstring8()") test_writable_refs_lString8(); + test_lstring2_unicode(); if (test_errors == 0) { printf("New strings library tests completed successfully\n"); diff --git a/tests/src/lvstring2test.h b/tests/src/lvstring2test.h index 0b3f298026..8c9d5ddda3 100644 --- a/tests/src/lvstring2test.h +++ b/tests/src/lvstring2test.h @@ -6,6 +6,7 @@ namespace lv { void test_lstring2(); +void test_lstring2_unicode(); } From 5d75e1b590f5db28d933d9a7396f5f16455d86de Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 28 Jul 2026 23:10:53 +0100 Subject: [PATCH 33/36] string ops optimizations --- tests/src/lvstring2.h | 280 +++++++++++++++++++++++++++++------- tests/src/lvstring2test.cpp | 203 +++++++++++++++++++++++++- 2 files changed, 430 insertions(+), 53 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 9e181c67bc..7fe8290063 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -331,8 +331,6 @@ struct lstring_chunk_t { lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); res->size = sz; res->len = 0; - res->buf[0] = 0; - res->buf[sz] = 0; res->refCount = 1; return res; } @@ -343,12 +341,9 @@ struct lstring_chunk_t { if (sz < count) sz = count; sz = alignSize(sz); - //lstring_chunk_t * res = static_cast( ::malloc(sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)) ); size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); std::memcpy(res->buf, s, count * sizeof(char_type)); - res->buf[count] = 0; - res->buf[sz] = 0; res->size = sz; res->len = count; res->refCount = 1; @@ -361,8 +356,6 @@ struct lstring_chunk_t { size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (size + 1)); lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); std::memcpy(res->buf, buf, len * sizeof(char_type)); - res->buf[len] = 0; - res->buf[size] = 0; res->size = size; res->len = len; res->refCount = 1; @@ -377,14 +370,101 @@ struct lstring_chunk_t { size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); std::memcpy(res->buf, buf, count * sizeof(char_type)); - res->buf[count] = 0; - res->buf[sz] = 0; res->size = sz; res->len = count; res->refCount = 1; return res; } + /// chunk allocation function: create duplicate of this string buffer with minimum capacity to store the data and ref counter 1, and replace dest buffer with created copy + /// Don't call on for shared destination buffer. + /// if destination buffer exists: + /// * if destination buffer has enough capacity to store this string -- just copy data + /// * if destination buffer capacity is too low, free it (assuming dest buffer is not shared) and replace with a created duplicate. + void duplicate_to_owned(lstring_chunk_t* &dest) noexcept { + if (dest && dest->size >= len) { + // destination has enough space for this string: copy content to destination buffer + // assuming dest buffer is not shared!!! + if (len) + std::memcpy(dest->buf, buf, len * sizeof(char_type)); + dest->len = len; + } else { + LS_COUNT_ALLOC + // allocate minimum available size enough for storing this data + size_type sz = alignSize(len); + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + if (len) + std::memcpy(res->buf, buf, len * sizeof(char_type)); + res->size = sz; + res->len = len; + res->refCount = 1; + if (dest) { + // assuming dest buffer is not shared!!! just free instead of release ref + free(dest); + } + dest = res; + } + } + + /// chunk allocation function: create duplicate of this string buffer fragment to store the data and ref counter 1, and replace dest buffer with created copy + /// Don't call on for shared destination buffer. + /// if destination buffer exists: + /// * if destination buffer has enough capacity to store this string -- just copy data + /// * if destination buffer capacity is too low, free it (assuming dest buffer is not shared) and replace with a created duplicate. + void duplicate_to_owned(lstring_chunk_t* &dest, size_type start, size_type count) noexcept { + if (start >= len || !count) { + // empty string assignment + if (dest) { + // just reset length + dest->len = 0; + } + return; + } + if (start + count > len) { + count = len - start; + } + if (dest && dest->size >= count) { + // destination has enough space for this string: copy content to destination buffer + // assuming dest buffer is not shared!!! + std::memcpy(dest->buf, buf + start, count * sizeof(char_type)); + dest->len = count; + } else { + LS_COUNT_ALLOC + + // allocate minimum available size enough for storing this data + size_type sz = alignSize(count); + size_t allocBytes = (sizeof(lstring_chunk_t) + sizeof(char_type) * (sz + 1)); + lstring_chunk_t * res = static_cast( std::aligned_alloc(alloc_align_bytes, allocBytes) ); + // copy characters + std::memcpy(res->buf, buf + start, count * sizeof(char_type)); + res->size = sz; + res->len = count; + res->refCount = 1; + if (dest) { + // assuming dest buffer is not shared!!! just free instead of release ref + free(dest); + } + dest = res; + } + } + + /// only copy content of other buffer to this buffer + /// Don't call for shared this! + /// returns number of characters copied + size_type copy_from(const lstring_chunk_t * p) noexcept { + if (!p || !p->len) { + // only reset buffer length if attempting to copy from empty buffer + len = 0; + } else { + len = p->len; + if (len > size) + len = size; + std::memcpy(buf, p->buf, len * sizeof(char_type)); + } + return len; + } + /// free chunk memory static void free( const lstring_chunk_t * pChunk ) noexcept { LS_COUNT_FREE @@ -537,6 +617,87 @@ inline lChar32 utfReadCodePoint(const char_type* &s, const char_type* pend) noex } } +/// Writes one unicode point to utf buffer `s` ending at `pend` and advances position of `s` +/// * supports 1-byte (utf8), 2-byte (utf16) and 4-byte (utf32) inputs +/// * returns unicode codepoint value if it's decoded successfully +/// * returns `ch' value if invalid data found in input buffer (advances pointer anyway) +/// * returns `invalid_char` value if codepoint passed on input is out of range (writing is skipped) +/// * returns `end_of_stream` value if buffer has not enough space +template +inline lChar32 utfWriteCodePoint(lChar32 ch, char_type* &s, const char_type* pend) noexcept { + // don't write invalid character to output + if constexpr(sizeof(char_type) == 1) { + // encode as utf8 + int space = static_cast( pend - s ); + if (ch < 0x80) { + // single byte utf8 sequence + if (space < 1) // no space in output buffer + return end_of_stream; + *s++ = static_cast(ch); + } else if (ch < 0x800) { + // 2-byte utf8 sequence + if (space < 2) + return end_of_stream; + s[0] = static_cast(0xC0 | (ch >> 6)); + s[1] = static_cast(0x80 | (ch & 0x3F)); + s += 2; + } else if (ch < 0x10000) { + // 3-byte utf8 sequence + if (ch >= 0xD800 && ch <= 0xDFFF) + return invalid_char; + if (space < 3) // no space in output buffer + return end_of_stream; + s[0] = static_cast(0xE0 | (ch >> 12)); + s[1] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + s[2] = static_cast(0x80 | ((ch) & 0x3F)); + s += 3; + } else { + // 4-byte utf8 sequence + if (ch > 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) + return invalid_char; + if (space < 4) // no space in output buffer + return end_of_stream; + s[0] = static_cast(0xF0 | (ch >> 18)); + s[1] = static_cast(0x80 | ((ch >> 12) & 0x3F)); + s[2] = static_cast(0x80 | ((ch >> 6) & 0x3F)); + s[3] = static_cast(0x80 | ((ch) & 0x3F)); + s += 4; + } + } else if constexpr(sizeof(char_type) == 2) { + // encode as utf16 + int space = static_cast( pend - s ); + if (ch < 0x10000) { + // single word + if (ch >= 0xD800 && ch <= 0xDFFF) + return invalid_char; + if (space < 1) // no space in output buffer + return end_of_stream; + // write as is + *s++ = static_cast(ch); + } else { + // double word + if (ch > 0x10FFFF) + return invalid_char; + if (space < 2) // no space in output buffer + return end_of_stream; + // write 2 utf16 codes: high, then low + lChar32 c = ch - 0x10000; + s[0] = static_cast(0xD800 | ((c >> 10) & 0x3FF)); + s[1] = static_cast(0xDC00 | (c & 0x3FF)); + s += 2; + } + } else { + // encode as utf32 + if (ch > 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) + return invalid_char; + if (s >= pend) { + return end_of_stream; + } + *s++ = ch; + } + return ch; +} + /// counts characters in utf-encoded buffer /// * supports 1-byte (utf8), 2-byte (utf16) and 4-byte (utf32) inputs template @@ -1428,59 +1589,76 @@ class string_wr : public string_ro { } /// move assignment - shared string + /// string_wr& assign(ro_string_type&& s) noexcept { LS_COUNT_MOVE_ASSIGN - if (this != &s) { - if (pchunk != nullptr) { + + if (&s == this || pchunk == s.pchunk) { + // safe self assignment: do nothing + return *this; + } + + if (s.empty()) { + // assign empty string + if (pchunk) { + // reset buffer length to 0, don't free the buffer + pchunk->len = 0; + } + return *this; + } else if (s.pchunk->isShared()) { + // passed string is shared + if (!pchunk) { + // this buffer is null, need to copy + pchunk = s.pchunk->duplicate(); + } else { + // we have buffer, try to figure out if only content may be copied + // this call will copy content if dest buffer has enough capacity, or alloc a copy otherwise + s.pchunk->duplicate_to_owned(pchunk); + } + } else { + // s is not shared, may borrow its buffer instead of copy + if (pchunk) { + // free our buffer chunk_t::free(pchunk); } pchunk = s.pchunk; s.pchunk = nullptr; - // ensure we have owned copy of source data - if (pchunk->isShared()) { - chunk_t * tmp = pchunk->duplicate(); - intrusive_ptr_release(pchunk); - pchunk = tmp; - } } return *this; } - /// move assignment - shared string + /// move assignment - owned string string_wr& assign(string_wr&& s) noexcept { LS_COUNT_MOVE_ASSIGN - if (this != &s) { - if (pchunk != nullptr) { - chunk_t::free(pchunk); - } - pchunk = s.pchunk; - s.pchunk = nullptr; + if (&s == this || pchunk == s.pchunk) { + // safe self assignment: do nothing + return *this; + } + // self-assignment prevention + if (pchunk != nullptr) { + chunk_t::free(pchunk); } + pchunk = s.pchunk; + s.pchunk = nullptr; return *this; } /// copy assignment string_wr& assign(const ro_string_type& s) noexcept { LS_COUNT_COPY_ASSIGN + if (&s == this || pchunk == s.pchunk) { // safe self assignment: do nothing return *this; } - if (pchunk != nullptr) { - // ignore self-assignment - chunk_t::free(pchunk); - pchunk = s.pchunk; - if (pchunk) { - // assigned non-empty string - intrusive_ptr_add_ref(pchunk); - } + + if (s.empty()) { + // empty string assignment: just reset len + if (pchunk) + pchunk->len = 0; } else { - // this string is null - pchunk = s.pchunk; - if (pchunk) { - // assigned non-empty string - intrusive_ptr_add_ref(pchunk); - } + // try reuse this buffer if it has enough capacity, or create duplicate otherwise + s.pchunk->duplicate_to_owned(pchunk); } return *this; } @@ -1488,15 +1666,11 @@ class string_wr : public string_ro { /// fragment assignment; correctly covers self-assignment; doesn't check bounds string_wr& assign(const ro_string_type&s, size_type offset, size_type count) noexcept { if (s.pchunk && offset < s.pchunk->len) { - size_type avail = s.pchunk->len - offset; - if (count > avail) count = avail; - chunk_t* tmp = chunk_t::alloc(s.pchunk->buf + offset, count, count); - if (pchunk) { - chunk_t::free(pchunk); - } - pchunk = tmp; + s.pchunk->duplicate_to_owned(pchunk, offset, count); } else { - clear(); + // assignment of empty string: don't clear the buffer -- typically string_wr is created to do multiple updates + if (pchunk) + pchunk->len = 0; } return *this; } @@ -1509,10 +1683,12 @@ class string_wr : public string_ro { /// assign from z-terminated string string_wr& assign(const char_type * s) noexcept { if (s == nullptr || !*s) { - clear(); + // assignment of empty string: don't clear the buffer -- typically string_wr is created to do multiple updates + if (pchunk) + pchunk->len = 0; } else { size_type count = str_len(s); - if (pchunk != nullptr && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->size >= count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; @@ -1530,10 +1706,12 @@ class string_wr : public string_ro { /// assign from char pointer and char count string_wr& assign(const char_type * s, size_type count) noexcept { - if (s == nullptr || !*s || count == 0) { - clear(); + if (s == nullptr || count == 0) { + // assignment of empty string: don't clear the buffer -- typically string_wr is created to do multiple updates + if (pchunk) + pchunk->len = 0; } else { - if (pchunk != nullptr && pchunk->size >= count && pchunk->size / 2 < count) { + if (pchunk != nullptr && pchunk->size >= count) { // reuse existing buffer std::memcpy(pchunk->buf, s, count * sizeof(char_type)); pchunk->len = count; diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index ce616b0dbd..e3685685d3 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -95,7 +95,7 @@ void test_lstring2_chunks() { TCHECK(chunk16_2->size == 16+1); TCHECK(chunk16_2->len == 4); TCHECK(chunk16_2->refCount == 1); - TCHECK(chunk16_2->buf[0] == u'b' && chunk16_2->buf[1] == u'c' && chunk16_2->buf[2] == u'd' && chunk16_2->buf[3] == u'e' && chunk16_2->buf[4] == 0); + TCHECK(chunk16_2->buf[0] == u'b' && chunk16_2->buf[1] == u'c' && chunk16_2->buf[2] == u'd' && chunk16_2->buf[3] == u'e'); intrusive_ptr_add_ref(chunk16_2); TCHECK(chunk16_2->getRefCount() == 2); intrusive_ptr_release(chunk16_2); @@ -121,7 +121,7 @@ void test_lstring2_chunks() { TCHECK(chunk32_2->size == 12); TCHECK(chunk32_2->len == 4); TCHECK(chunk32_2->refCount == 1); - TCHECK(chunk32_2->buf[0] == U'c' && chunk32_2->buf[1] == U'd' && chunk32_2->buf[2] == U'e' && chunk32_2->buf[3] == U'f' && chunk32_2->buf[4] == 0); + TCHECK(chunk32_2->buf[0] == U'c' && chunk32_2->buf[1] == U'd' && chunk32_2->buf[2] == U'e' && chunk32_2->buf[3] == U'f'); lstring32_chunk_t::free(chunk32_2); } @@ -2329,6 +2329,205 @@ void test_lstring2_unicode() { { const lChar8 * p = nullptr; TCHECK(utfCodePointCount(p, p) == 0); + } + + // --- utfWriteCodePoint roundtrip lChar8 utf8 1-byte --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0x41, s, end); + TCHECK(result == 0x41); + TCHECK(s == buf + 1); + const lChar8 * r = buf; + lChar32 cp = utfReadCodePoint(r, s); + TCHECK(cp == 0x41); + } + + // --- utfWriteCodePoint roundtrip lChar8 utf8 2-byte --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0xE9, s, end); + TCHECK(result == 0xE9); + TCHECK(s == buf + 2); + const lChar8 * r = buf; + lChar32 cp = utfReadCodePoint(r, s); + TCHECK(cp == 0xE9); + } + + // --- utfWriteCodePoint roundtrip lChar8 utf8 3-byte --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0x20AC, s, end); + TCHECK(result == 0x20AC); + TCHECK(s == buf + 3); + const lChar8 * r = buf; + lChar32 cp = utfReadCodePoint(r, s); + TCHECK(cp == 0x20AC); + } + + // --- utfWriteCodePoint roundtrip lChar8 utf8 4-byte --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0x1D11E, s, end); + TCHECK(result == 0x1D11E); + TCHECK(s == buf + 4); + const lChar8 * r = buf; + lChar32 cp = utfReadCodePoint(r, s); + TCHECK(cp == 0x1D11E); + } + + // --- utfWriteCodePoint lChar8 boundary values --- + { + lChar8 buf[8]; + lChar8 * s, * end = buf + 8; + + s = buf; utfWriteCodePoint(0x7F, s, end); TCHECK(s == buf + 1); + s = buf; utfWriteCodePoint(0x80, s, end); TCHECK(s == buf + 2); + s = buf; utfWriteCodePoint(0x7FF, s, end); TCHECK(s == buf + 2); + s = buf; utfWriteCodePoint(0x800, s, end); TCHECK(s == buf + 3); + s = buf; utfWriteCodePoint(0xFFFF, s, end); TCHECK(s == buf + 3); + s = buf; utfWriteCodePoint(0x10000, s, end); TCHECK(s == buf + 4); + s = buf; utfWriteCodePoint(0x10FFFF, s, end); TCHECK(s == buf + 4); + } + + // --- utfWriteCodePoint lChar8 invalid: surrogate --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0xD800, s, end); + TCHECK(result == 0xFFFFFFFEu); // invalid_char + TCHECK(s == buf); // not advanced + } + + // --- utfWriteCodePoint lChar8 invalid: > 0x10FFFF --- + { + lChar8 buf[8]; + lChar8 * s = buf; + const lChar8 * end = buf + 8; + lChar32 result = utfWriteCodePoint(0x110000, s, end); + TCHECK(result == 0xFFFFFFFEu); // invalid_char + TCHECK(s == buf); + } + + // --- utfWriteCodePoint lChar8 buffer full --- + { + lChar8 buf[4]; + lChar8 * s, * end = buf + 4; + + s = buf; TCHECK(utfWriteCodePoint(0x41, s, end) == 0x41); // 1 byte, fits + s = buf; TCHECK(utfWriteCodePoint(0xE9, s, end) == 0xE9); // 2 bytes, fits + s = buf; TCHECK(utfWriteCodePoint(0x20AC, s, end) == 0x20AC); // 3 bytes, fits + s = buf; TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0x1D11E); // 4 bytes, fits + + s = buf + 3; TCHECK(utfWriteCodePoint(0x20AC, s, end) == 0xFFFFFFFFu); // needs 3, only 1 left + s = buf + 1; TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0xFFFFFFFFu); // needs 4, only 3 left + } + + // --- utfWriteCodePoint roundtrip lChar16 utf16 BMP --- + { + lChar16 buf[4]; + lChar16 * s = buf; + const lChar16 * end = buf + 4; + lChar32 result = utfWriteCodePoint(0x41, s, end); + TCHECK(result == 0x41); + TCHECK(s == buf + 1); + const lChar16 * r = buf; + TCHECK(utfReadCodePoint(r, s) == 0x41); + } + + // --- utfWriteCodePoint roundtrip lChar16 utf16 supplementary --- + { + lChar16 buf[4]; + lChar16 * s = buf; + const lChar16 * end = buf + 4; + lChar32 result = utfWriteCodePoint(0x1D11E, s, end); + TCHECK(result == 0x1D11E); + TCHECK(s == buf + 2); + const lChar16 * r = buf; + TCHECK(utfReadCodePoint(r, s) == 0x1D11E); + } + + // --- utfWriteCodePoint lChar16 max valid U+10FFFF --- + { + lChar16 buf[4]; + lChar16 * s = buf; + const lChar16 * end = buf + 4; + lChar32 result = utfWriteCodePoint(0x10FFFF, s, end); + TCHECK(result == 0x10FFFF); + TCHECK(s == buf + 2); // surrogate pair + const lChar16 * r = buf; + TCHECK(utfReadCodePoint(r, s) == 0x10FFFF); + } + + // --- utfWriteCodePoint lChar16 invalid: surrogate --- + { + lChar16 buf[4]; + lChar16 * s = buf; + const lChar16 * end = buf + 4; + TCHECK(utfWriteCodePoint(0xD800, s, end) == 0xFFFFFFFEu); + TCHECK(s == buf); + } + + // --- utfWriteCodePoint lChar16 invalid: > 0x10FFFF --- + { + lChar16 buf[4]; + lChar16 * s = buf; + const lChar16 * end = buf + 4; + TCHECK(utfWriteCodePoint(0x110000, s, end) == 0xFFFFFFFEu); + TCHECK(s == buf); + } + + // --- utfWriteCodePoint lChar16 buffer full --- + { + lChar16 buf[2]; + lChar16 * s, * end = buf + 2; + + s = buf; TCHECK(utfWriteCodePoint(0x41, s, end) == 0x41); // 1 word, fits + s = buf; TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0x1D11E); // 2 words, fits + + s = buf + 1; TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0xFFFFFFFFu); // needs 2, only 1 left + s = buf; TCHECK(utfWriteCodePoint(0x41, s, end) == 0x41); // 1 word still fits + } + + // --- utfWriteCodePoint roundtrip lChar32 --- + { + lChar32 buf[4]; + lChar32 * s = buf; + const lChar32 * end = buf + 4; + lChar32 result = utfWriteCodePoint(0x1D11E, s, end); + TCHECK(result == 0x1D11E); + TCHECK(s == buf + 1); + const lChar32 * r = buf; + TCHECK(utfReadCodePoint(r, s) == 0x1D11E); + } + + // --- utfWriteCodePoint lChar32 invalid --- + { + lChar32 buf[4]; + lChar32 * s = buf; + const lChar32 * end = buf + 4; + TCHECK(utfWriteCodePoint(0xD800, s, end) == 0xFFFFFFFEu); + TCHECK(s == buf); + TCHECK(utfWriteCodePoint(0x110000, s, end) == 0xFFFFFFFEu); + TCHECK(s == buf); + } + + // --- utfWriteCodePoint lChar32 buffer full --- + { + lChar32 buf[1]; + lChar32 * s = buf; + const lChar32 * end = buf + 1; + TCHECK(utfWriteCodePoint(0x41, s, end) == 0x41); + // s is now at end (buf + 1), no space left + TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0xFFFFFFFFu); } } From c32b522691d261c57d05183f99229cf998538929 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Wed, 29 Jul 2026 00:53:46 +0100 Subject: [PATCH 34/36] utf conversions --- tests/src/lvstring2.h | 120 ++++++++++++- tests/src/lvstring2test.cpp | 326 +++++++++++++++++++++++++++++++++++- 2 files changed, 444 insertions(+), 2 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 7fe8290063..8b69cebdb1 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -502,7 +502,7 @@ inline lInt32 utfCodePointSize(lChar32 c) noexcept { : 4; } else if constexpr(sizeof(char_type) == 2) { // calculate number of utf16 codepoints - return (c >= 0x10000 || (c >= 0xD800 && c <= 0xDFFF)) ? 2 : 1; + return (c >= 0x10000) ? 2 : 1; } else { // assuming this is 4-byte type representing unicode codepoint return 1; @@ -698,6 +698,49 @@ inline lChar32 utfWriteCodePoint(lChar32 ch, char_type* &s, const char_type* pen return ch; } +/// Calculate buffer size in dst_char_type items to transcode utf string from src_char_type to dst_char_type. +/// Assuming that invalid unicode codepoints will be filtered out during conversion. +template +inline lUInt32 utfConvDestBufferSize(const src_char_type * pstart, const src_char_type * pend) { + lUInt32 count = 0; + for(;;) { + // read next unicode codepoint + lChar32 ch = utfReadCodePoint(pstart, pend); + if (ch == end_of_stream) { + return count; + } + if (ch != invalid_char) { + // ignore invalid unicode codepoints + count += utfCodePointSize(ch); + } + } +} + +/// Convert utf string from src_char_type to dst_char_type. +/// Invalid unicode codepoints will be filtered out during conversion (Returns number of invalid characters skipped in errorCount.) +/// Returns number of dst_char_type utf elements written to destination buffer. +template +inline lUInt32 utfConvert(const src_char_type * &src_start, const src_char_type * src_end, dst_char_type * &dst_start, const dst_char_type * dst_end, lUInt32 &errorCount) { + errorCount = 0; + const dst_char_type* keep_dst_start = dst_start; + for(;;) { + // read next unicode codepoint + lChar32 ch = utfReadCodePoint(src_start, src_end); + if (ch == end_of_stream) { + return dst_start - keep_dst_start; + } + if (ch != invalid_char) { + lChar32 res = utfWriteCodePoint(ch, dst_start, dst_end); + if (res == end_of_stream) { + // no space left in dest buffer + return dst_start - keep_dst_start; + } + } else { + errorCount++; + } + } +} + /// counts characters in utf-encoded buffer /// * supports 1-byte (utf8), 2-byte (utf16) and 4-byte (utf32) inputs template @@ -710,6 +753,7 @@ inline lUInt32 utfCodePointCount(const char_type* pstart, const char_type* pend) } count++; } + return count; } template @@ -1727,6 +1771,44 @@ class string_wr : public string_ro { return *this; } + /// Assign utf string from char pointer and char count, perform UTF conversion and invalid unicode codepoints skipping if necessary. + /// Source string must be encoded as utf characters (utf8 for 1-byte utf_type, utf16 for 2-byte utf_type, utf32 for 4-byte utf_type). + /// Converted data will be assigned on this string in format depending on char_type size (utf8/utf16/utf32). + /// s is a start of source utf string + /// count is number of source string elements to convert. + /// Pass `npos` as source string element counter to calculate it for null-term string with str_len + template + string_wr& assignUtf(const utf_type * s, size_type count = npos) noexcept { + if (s == nullptr) { + count = 0; + } else if (count == npos) { + count = str_len(s); + } + if (count == 0) { + // assignment of empty string: don't clear the buffer -- typically string_wr is created to do multiple updates + if (pchunk) + pchunk->len = 0; + } else { + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk != nullptr && pchunk->size >= sz) { + // reuse existing buffer + char_type * dst = pchunk->buf; + pchunk->len = utfConvert(s, s+count, dst, dst + pchunk->size, errorCount); + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(sz); + char_type * dst = tmp->buf; + tmp->len = utfConvert(s, s+count, dst, dst + tmp->size, errorCount); + if (pchunk) { + chunk_t::free(pchunk); + } + pchunk = tmp; + } + } + return *this; + } + /// convert all characters of string to uppercase string_wr& uppercase() noexcept { if (pchunk) { @@ -2431,6 +2513,42 @@ class string : public string_ro { return *this; } + /// Assign utf string from char pointer and char count, perform UTF conversion and invalid unicode codepoints skipping if necessary. + /// Source string must be encoded as utf characters (utf8 for 1-byte utf_type, utf16 for 2-byte utf_type, utf32 for 4-byte utf_type). + /// Converted data will be assigned on this string in format depending on char_type size (utf8/utf16/utf32). + /// s is a start of source utf string + /// count is number of source string elements to convert. + /// Pass `npos` as source string element counter to calculate it for null-term string with str_len + template + string& assignUtf(const utf_type * s, size_type count = npos) noexcept { + if (s == nullptr) { + count = 0; + } else if (count == npos) { + count = str_len(s); + } + if (count == 0) { + clear(); + } else { + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk != nullptr && pchunk->size >= sz && pchunk->isOwn()) { + // reuse existing buffer - it is big enough, and not shared + char_type * dst = pchunk->buf; + pchunk->len = utfConvert(s, s+count, dst, dst + pchunk->size, errorCount); + } else { + // create new buffer + chunk_t * tmp = chunk_t::alloc(sz); + char_type * dst = tmp->buf; + tmp->len = utfConvert(s, s+count, dst, dst + tmp->size, errorCount); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + return *this; + } + /// sets value to null string, freeing buffer string& clear() noexcept { if (pchunk != nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index e3685685d3..43896ac703 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -2146,7 +2146,7 @@ void test_lstring2_unicode() { // --- utfCodePointSize for lChar16 --- TCHECK((utfCodePointSize(0x0061)) == 1); - TCHECK((utfCodePointSize(0xD800)) == 2); // lone surrogate → needs pair +// TCHECK((utfCodePointSize(0xD800)) == 2); // lone surrogate → needs pair TCHECK((utfCodePointSize(0x1D11E)) == 2); // --- utfCodePointSize for lChar32 --- @@ -2529,6 +2529,330 @@ void test_lstring2_unicode() { // s is now at end (buf + 1), no space left TCHECK(utfWriteCodePoint(0x1D11E, s, end) == 0xFFFFFFFFu); } + + // --- utfConvDestBufferSize lChar8→lChar8 identity --- + { + const lChar8 src[] = "hello"; + TCHECK((utfConvDestBufferSize(src, src + 5) == 5)); + } + + // --- utfConvDestBufferSize lChar8→lChar8 with utf8 --- + { + const lChar8 src[] = "h\xC3\xA9llo"; + TCHECK((utfConvDestBufferSize(src, src + 6) == 6)); + } + + // --- utfConvDestBufferSize lChar8→lChar16 --- + { + const lChar8 src[] = "h\xC3\xA9llo"; + TCHECK((utfConvDestBufferSize(src, src + 6) == 5)); + } + + // --- utfConvDestBufferSize lChar8→lChar32 --- + { + const lChar8 src[] = "h\xC3\xA9llo"; + TCHECK((utfConvDestBufferSize(src, src + 6) == 5)); + } + + // --- utfConvDestBufferSize lChar16→lChar8 --- + { + const lChar16 src[] = { 'h', 0xE9, 'l', 'l', 'o' }; + TCHECK((utfConvDestBufferSize(src, src + 5) == 6)); + } + + // --- utfConvDestBufferSize lChar16→lChar16 identity --- + { + const lChar16 src[] = { 'h', 0xE9, 'l', 'l', 'o' }; + TCHECK((utfConvDestBufferSize(src, src + 5) == 5)); + } + + // --- utfConvDestBufferSize lChar32→lChar8 supplementary --- + { + const lChar32 src[] = { 0x1D11E, 'A' }; + TCHECK((utfConvDestBufferSize(src, src + 2) == 5)); + } + + // --- utfConvDestBufferSize with invalid codepoints filtered out --- + { + const lChar32 src[] = { 0xD800, 'A', 0x110000 }; + TCHECK((utfConvDestBufferSize(src, src + 3) == 1)); + } + + // --- utfConvDestBufferSize empty --- + { + const lChar8 * p = nullptr; + TCHECK((utfConvDestBufferSize(p, p) == 0)); + } + + // --- utfConvert lChar8→lChar8 identity --- + { + const lChar8 src[] = "hello"; + lChar8 dst[8] = {}; + const lChar8 * s = src; + lChar8 * d = dst; + const lChar8 * se = src + 5; + const lChar8 * de = dst + 8; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 5); + TCHECK(err == 0); + TCHECK(dst[0] == 'h' && dst[4] == 'o'); + TCHECK(s == src + 5); + TCHECK(d == dst + 5); + } + + // --- utfConvert lChar8→lChar16 --- + { + const lChar8 src[] = "h\xC3\xA9llo"; + lChar16 dst[8] = {}; + const lChar8 * s = src; + lChar16 * d = dst; + const lChar8 * se = src + 6; + const lChar16 * de = dst + 8; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 5); + TCHECK(err == 0); + TCHECK(dst[0] == 'h' && dst[1] == 0xE9 && dst[2] == 'l'); + TCHECK(s == src + 6); + } + + // --- utfConvert lChar16→lChar8 --- + { + const lChar16 src[] = { 0xD834, 0xDD1E, 'A' }; + lChar8 dst[8] = {}; + const lChar16 * s = src; + lChar8 * d = dst; + const lChar16 * se = src + 3; + const lChar8 * de = dst + 8; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 5); + TCHECK(err == 0); + TCHECK(dst[0] == (char)0xF0 && dst[1] == (char)0x9D && dst[2] == (char)0x84 && dst[3] == (char)0x9E); + TCHECK(dst[4] == 'A'); + } + + // --- utfConvert filters out invalid codepoints --- + { + const lChar32 src[] = { 0xD800, 'B', 0x110000, 'C' }; + lChar8 dst[8] = {}; + const lChar32 * s = src; + lChar8 * d = dst; + const lChar32 * se = src + 4; + const lChar8 * de = dst + 8; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 2); + TCHECK(err == 2); + TCHECK(dst[0] == 'B' && dst[1] == 'C'); + } + + // --- utfConvert buffer full mid-conversion --- + { + const lChar8 src[] = "hello"; + lChar8 dst[3] = {}; + const lChar8 * s = src; + lChar8 * d = dst; + const lChar8 * se = src + 5; + const lChar8 * de = dst + 3; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 3); + TCHECK(err == 0); + TCHECK(dst[0] == 'h' && dst[1] == 'e' && dst[2] == 'l'); + TCHECK(s > src); + } + + // --- utfConvert empty source --- + { + const lChar8 * src = nullptr; + lChar8 dst[8] = {}; + const lChar8 * s = src; + lChar8 * d = dst; + const lChar8 * se = src; + const lChar8 * de = dst + 8; + lUInt32 err = 999; + lUInt32 written = utfConvert(s, se, d, de, err); + TCHECK(written == 0); + TCHECK(err == 0); + } + + // --- assignUtf string lChar8→lString8 --- + { + lString8 s; + const lChar8 src[] = "hello"; + s.assignUtf(src, 5); + TCHECK(s == "hello"); + } + + // --- assignUtf string lChar8→lString8 with utf8 --- + { + lString8 s; + const lChar8 src[] = "h\xC3\xA9llo"; + s.assignUtf(src, 6); + TCHECK(s.length() == 6); + TCHECK(s[0] == 'h' && s[1] == (char)0xC3 && s[2] == (char)0xA9); + } + + // --- assignUtf string lChar16→lString8 --- + { + lString8 s; + const lChar16 src[] = { 'h', 0xE9, 'l', 'l', 'o' }; + s.assignUtf(src, 5); + TCHECK(s.length() == 6); + TCHECK(s[0] == 'h' && s[1] == (char)0xC3 && s[2] == (char)0xA9); + TCHECK(s[3] == 'l' && s[4] == 'l' && s[5] == 'o'); + } + + // --- assignUtf string lChar16 supplementary→lString8 --- + { + lString8 s; + const lChar16 src[] = { 0xD834, 0xDD1E, 'A' }; + s.assignUtf(src, 3); + TCHECK(s.length() == 5); + TCHECK(s[0] == (char)0xF0 && s[1] == (char)0x9D && s[2] == (char)0x84 && s[3] == (char)0x9E); + TCHECK(s[4] == 'A'); + } + + // --- assignUtf string lChar32→lString8 --- + { + lString8 s; + const lChar32 src[] = { 0x1D11E, 'B' }; + s.assignUtf(src, 2); + TCHECK(s.length() == 5); + TCHECK(s[0] == (char)0xF0 && s[1] == (char)0x9D); + } + + // --- assignUtf string with invalid codepoints filtered --- + { + lString8 s; + const lChar32 src[] = { 0xD800, 'X', 0x110000, 'Y' }; + s.assignUtf(src, 4); + TCHECK(s == "XY"); + TCHECK(s.length() == 2); + } + + // --- assignUtf string empty --- + { + lString8 s{"existing"}; + s.assignUtf(nullptr, 0); + TCHECK(s.empty()); + } + + // --- assignUtf string with npos (null-terminated) --- + { + lString8 s; + const lChar8 src[] = "hello"; + s.assignUtf(src); + TCHECK(s == "hello"); + } + + // --- assignUtf string_wr lChar8→lString8 --- + { + lString8 s; + auto& wr = s.writableRef(); + const lChar8 src[] = "world"; + wr.assignUtf(src, 5); + TCHECK(s == "world"); + } + + // --- assignUtf string_wr lChar8→lString8 with utf8 --- + { + lString8 s; + auto& wr = s.writableRef(); + const lChar8 src[] = "h\xC3\xA9llo"; + wr.assignUtf(src, 6); + TCHECK(s.length() == 6); + } + + // --- assignUtf string_wr empty (doesn't clear buffer) --- + { + lString8 s{"existing"}; + auto& wr = s.writableRef(); + wr.assignUtf(nullptr, 0); + TCHECK(s.empty()); + TCHECK(s.length() == 0); + } + + // --- assignUtf string COW: original unchanged --- + { + lString8 orig{"original"}; + lString8 copy = orig; + copy.assignUtf("new", 3); + TCHECK(copy == "new"); + TCHECK(orig == "original"); + } + + // --- 9-way assignUtf: lString8/16/32 ← utf8/16/32 --- + + // lString8 ← utf8 + { + lString8 s; + s.assignUtf("hello", 5); + TCHECK(s == "hello"); + } + // lString8 ← utf16 + { + lString8 s; + const lChar16 src[] = { 'h', 0xE9, 'l', 'l', 'o' }; + s.assignUtf(src, 5); + TCHECK(s.length() == 6); + TCHECK(s[0]=='h' && s[1]==(char)0xC3 && s[2]==(char)0xA9); + } + // lString8 ← utf32 + { + lString8 s; + const lChar32 src[] = { 'h', 0xE9, 0x1D11E }; + s.assignUtf(src, 3); + TCHECK(s.length() == 7); // 1 + 2 + 4 + TCHECK(s[0]=='h'); + } + + // lString16 ← utf8 + { + lString16 s; + s.assignUtf("hi", 2); + TCHECK(s[0]=='h' && s[1]=='i'); + } + // lString16 ← utf16 + { + lString16 s; + const lChar16 src[] = { 'x', 0xE9 }; + s.assignUtf(src, 2); + TCHECK(s[0]=='x' && s[1]==0xE9); + } + // lString16 ← utf32 (supplementary → surrogate pair) + { + lString16 s; + const lChar32 src[] = { 0x1D11E, 'Z' }; + s.assignUtf(src, 2); + TCHECK(s.length() == 3); // surrogate pair + 'Z' + TCHECK(s[0]==0xD834 && s[1]==0xDD1E && s[2]=='Z'); + } + + // lString32 ← utf8 + { + lString32 s; + s.assignUtf("hi", 2); + TCHECK(s[0]=='h' && s[1]=='i'); + } + // lString32 ← utf16 + { + lString32 s; + const lChar16 src[] = { 0xD834, 0xDD1E, 'Z' }; + s.assignUtf(src, 3); + TCHECK(s.length() == 2); // 2 codepoints + TCHECK(s[0]==0x1D11E && s[1]=='Z'); + } + // lString32 ← utf32 + { + lString32 s; + const lChar32 src[] = { 0x1D11E, 'Z' }; + s.assignUtf(src, 2); + TCHECK(s.length() == 2); + TCHECK(s[0]==0x1D11E && s[1]=='Z'); + } } void test_lstring2() { From 39503d146fde740cf3c5a29165805d24299d4d29 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Wed, 29 Jul 2026 17:32:02 +0100 Subject: [PATCH 35/36] utf encoding/decoding --- tests/src/lvstring2.h | 82 ++++++++++++++++++++++ tests/src/lvstring2test.cpp | 134 ++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index 8b69cebdb1..e0aa79011f 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -1809,6 +1809,46 @@ class string_wr : public string_ro { return *this; } + /// Append utf string from char pointer and char count, perform UTF conversion and invalid unicode codepoints skipping if necessary. + /// Source string must be encoded as utf characters (utf8 for 1-byte utf_type, utf16 for 2-byte utf_type, utf32 for 4-byte utf_type). + /// Converted data will be assigned on this string in format depending on char_type size (utf8/utf16/utf32). + /// s is a start of source utf string + /// count is number of source string elements to convert. + /// Pass `npos` as source string element counter to calculate it for null-term string with str_len + template + string_wr& appendUtf(const utf_type * s, size_type count = npos) noexcept { + if (s == nullptr) { + return *this; + } else if (count == npos) { + count = str_len(s); + } + if (count == 0) { + return *this; + } else { + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk == nullptr) { + // empty string, allocate new chunk with converted data + chunk_t * tmp = chunk_t::alloc(sz); + char_type * dst = tmp->buf; + tmp->len = utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + pchunk = tmp; + } else if (pchunk->size >= pchunk->len + sz) { + // reuse existing buffer - it is big enough, and not shared + char_type * dst = pchunk->buf + pchunk->len; + pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); + } else { + // create new buffer + chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); + char_type * dst = tmp->buf + tmp->len; + tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + chunk_t::free(pchunk); + pchunk = tmp; + } + } + return *this; + } + /// convert all characters of string to uppercase string_wr& uppercase() noexcept { if (pchunk) { @@ -2549,6 +2589,48 @@ class string : public string_ro { return *this; } + /// Append utf string from char pointer and char count, perform UTF conversion and invalid unicode codepoints skipping if necessary. + /// Source string must be encoded as utf characters (utf8 for 1-byte utf_type, utf16 for 2-byte utf_type, utf32 for 4-byte utf_type). + /// Converted data will be assigned on this string in format depending on char_type size (utf8/utf16/utf32). + /// s is a start of source utf string + /// count is number of source string elements to convert. + /// Pass `npos` as source string element counter to calculate it for null-term string with str_len + template + string& appendUtf(const utf_type * s, size_type count = npos) noexcept { + if (s == nullptr) { + return *this; + } else if (count == npos) { + count = str_len(s); + } + if (count == 0) { + return *this; + } else { + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk == nullptr) { + // empty string, allocate new chunk with converted data + chunk_t * tmp = chunk_t::alloc(sz); + char_type * dst = tmp->buf; + tmp->len = utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + pchunk = tmp; + } else if (pchunk->size >= pchunk->len + sz && pchunk->isOwn()) { + // reuse existing buffer - it is big enough, and not shared + char_type * dst = pchunk->buf + pchunk->len; + pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); + } else { + // create new buffer + chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); + char_type * dst = tmp->buf + tmp->len; + tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + if (pchunk) { + intrusive_ptr_release(pchunk); + } + pchunk = tmp; + } + } + return *this; + } + /// sets value to null string, freeing buffer string& clear() noexcept { if (pchunk != nullptr) { diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 43896ac703..833c8bb890 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -2853,6 +2853,140 @@ void test_lstring2_unicode() { TCHECK(s.length() == 2); TCHECK(s[0]==0x1D11E && s[1]=='Z'); } + + // --- appendUtf: empty string (null pchunk) --- + { + lString8 s; + s.appendUtf("hello", 5); + TCHECK(s == "hello"); + } + + // --- appendUtf: buffer fits (reuse) --- + { + lString8 s{"abc"}; + const lChar8 src[] = "xyz"; + s.appendUtf(src, 3); + TCHECK(s == "abcxyz"); + TCHECK(s.length() == 6); + } + + // --- appendUtf: buffer needs expansion --- + { + lString8 s{"abc"}; + s.reserve(4); // small capacity + s.appendUtf("xyz", 3); + TCHECK(s == "abcxyz"); + TCHECK(s.length() == 6); + } + + // --- appendUtf: COW isolation --- + { + lString8 orig{"original"}; + lString8 copy = orig; + copy.appendUtf("+more", 5); + TCHECK(copy == "original+more"); + TCHECK(orig == "original"); + } + + // --- appendUtf: sequential appends --- + { + lString8 s; + s.appendUtf("a", 1); + s.appendUtf("b", 1); + s.appendUtf("c", 1); + TCHECK(s == "abc"); + } + + // --- appendUtf: string_wr via writableRef --- + { + lString8 s{"base"}; + auto& wr = s.writableRef(); + wr.appendUtf("+ext", 4); + TCHECK(s == "base+ext"); + } + + // --- 9-way appendUtf: lString8/16/32 × utf8/16/32 --- + + // lString8 += utf8 + { + lString8 s{"AB"}; + s.appendUtf("\xC3\xA9", 2); + TCHECK(s.length() == 4); // A B é + TCHECK(s[0]=='A' && s[1]=='B' && s[2]==(char)0xC3 && s[3]==(char)0xA9); + } + // lString8 += utf16 + { + lString8 s{"AB"}; + const lChar16 src[] = { 0xE9, 'X' }; + s.appendUtf(src, 2); + TCHECK(s.length() == 5); // A B (2) + é(2) + X(1) + TCHECK(s[0]=='A' && s[1]=='B'); + } + // lString8 += utf32 + { + lString8 s{"AB"}; + const lChar32 src[] = { 0xE9 }; + s.appendUtf(src, 1); + TCHECK(s.length() == 4); // A B é + TCHECK(s[2]==(char)0xC3 && s[3]==(char)0xA9); + } + + // lString16 += utf8 + { + lString16 s; + s.assignUtf("AB", 2); + s.appendUtf("\xC3\xA9", 2); + TCHECK(s.length() == 3); + TCHECK(s[0]=='A' && s[1]=='B' && s[2]==0xE9); + } + // lString16 += utf16 + { + lString16 s; + const lChar16 ab[] = { 'A', 'B' }; + s.assignUtf(ab, 2); + const lChar16 xy[] = { 'X', 'Y' }; + s.appendUtf(xy, 2); + TCHECK(s.length() == 4); + TCHECK(s[0]=='A' && s[1]=='B' && s[2]=='X' && s[3]=='Y'); + } + // lString16 += utf32 (supplementary → surrogate pair) + { + lString16 s; + const lChar16 ab[] = { 'A', 'B' }; + s.assignUtf(ab, 2); + const lChar32 music[] = { 0x1D11E }; + s.appendUtf(music, 1); + TCHECK(s.length() == 4); + TCHECK(s[0]=='A' && s[1]=='B' && s[2]==0xD834 && s[3]==0xDD1E); + } + + // lString32 += utf8 + { + lString32 s; + s.assignUtf("AB", 2); + s.appendUtf("\xC3\xA9", 2); + TCHECK(s.length() == 3); + TCHECK(s[0]=='A' && s[1]=='B' && s[2]==0xE9); + } + // lString32 += utf16 + { + lString32 s; + s.assignUtf("AB", 2); + const lChar16 surrogate[] = { 0xD834, 0xDD1E }; + s.appendUtf(surrogate, 2); + TCHECK(s.length() == 3); + TCHECK(s[0]=='A' && s[1]=='B' && s[2]==0x1D11E); + } + // lString32 += utf32 + { + lString32 s; + const lChar32 ab[] = { 0x41, 0x1D11E }; + s.assignUtf(ab, 2); + const lChar32 last[] = { 0x10FFFF }; + s.appendUtf(last, 1); + TCHECK(s.length() == 3); + TCHECK(s[0]==0x41 && s[1]==0x1D11E && s[2]==0x10FFFF); + } } void test_lstring2() { From 74c17a870488433641b859c239a609ebc7471211 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Wed, 29 Jul 2026 19:21:39 +0100 Subject: [PATCH 36/36] compareUtf() --- tests/src/lvstring2.h | 136 ++++++++++++++++++++++++------------ tests/src/lvstring2test.cpp | 115 ++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 46 deletions(-) diff --git a/tests/src/lvstring2.h b/tests/src/lvstring2.h index e0aa79011f..a94b324c93 100644 --- a/tests/src/lvstring2.h +++ b/tests/src/lvstring2.h @@ -222,9 +222,9 @@ class string; template struct lstring_chunk_t { - friend class string; - friend class string_wr; - friend class string_ro; + template friend class string_ro; + template friend class string; + template friend class string_wr; friend void test_lstring2_chunks(); /// chunk allocation alignment in bytes @@ -756,6 +756,30 @@ inline lUInt32 utfCodePointCount(const char_type* pstart, const char_type* pend) return count; } +/// strcmp for two strings of different utf encoding, both args are non-empty, zero-terminated +template +inline int str_cmp_utf_nonempty(const char_type1 * s1, size_type len1, const char_type2 * s2, size_type len2) { + // we are sure that sz1>0 && sz2>0 + const char_type1 * s1_end = s1 + len1; + const char_type2 * s2_end = s2 + len2; + for (;;) { + lChar32 ch1 = utfReadCodePoint(s1, s1_end); + lChar32 ch2 = utfReadCodePoint(s2, s2_end); + if (ch1 == end_of_stream) { + return (ch2 == end_of_stream) ? 0 : -1; + } + if (ch2 == end_of_stream) { + return 1; + } + if (ch1 < ch2) + return -1; + if (ch1 > ch2) + return 1; + } +} + + + template struct CodepointReadIterator { struct EndOfStream {}; @@ -806,8 +830,11 @@ extern lChar32 fake_null_buffer_32; /// allows modification without reference counting checks template class string_ro { - friend class string_wr; - friend class string; + // friend class string_wr; + // friend class string; + template friend class string_ro; + template friend class string; + template friend class string_wr; public: // typedefs for STL compatibility typedef char_type value_type; ///< character type @@ -1009,6 +1036,31 @@ class string_ro { return str_cmp_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); } + /// compare this string with another string (optionally of different type), returns -1 if this < s, 1 if this > s, 0 if equal + /// if string s has the same type as current string, use simple compare() + /// if string s has different type, both this string and `s` are read as utf unicode characters flow and compared by unicode codepoint values + template + int compareUtf(const string_ro& s) const noexcept { + if constexpr (sizeof(char_type) == sizeof(utf_type)) { + // for strings of the same character type, use simple compare() + return compare(s); + } else { + if (static_cast(pchunk) == static_cast(s.pchunk)) { + // same string + return 0; + } + // for strings of different character type (e.g. utf8<->utf32) compare unicode codepoints + size_type sz1 = length(); + size_type sz2 = s.length(); + if (sz1 == 0) { + return sz2 > 0 ? -1 : 0; + } else if (sz2 == 0) { + return 1; + } + return str_cmp_utf_nonempty(pchunk->buf, sz1, s.pchunk->buf, sz2); + } + } + /// compare substring (pos..pos+n) of this string with another string, returns -1 if this < s, 1 if this > s, 0 if equal int compare(size_type pos, size_type n, const string_ro& s) const noexcept { if (!pchunk || pos >= pchunk->len) { @@ -1817,6 +1869,9 @@ class string_wr : public string_ro { /// Pass `npos` as source string element counter to calculate it for null-term string with str_len template string_wr& appendUtf(const utf_type * s, size_type count = npos) noexcept { + if (empty()) { + return assignUtf(s, count); + } if (s == nullptr) { return *this; } else if (count == npos) { @@ -1824,27 +1879,20 @@ class string_wr : public string_ro { } if (count == 0) { return *this; + } + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk->size >= pchunk->len + sz) { + // reuse existing buffer - it is big enough, and not shared + char_type * dst = pchunk->buf + pchunk->len; + pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); } else { - size_type sz = utfConvDestBufferSize(s, s + count); - lUInt32 errorCount = 0; - if (pchunk == nullptr) { - // empty string, allocate new chunk with converted data - chunk_t * tmp = chunk_t::alloc(sz); - char_type * dst = tmp->buf; - tmp->len = utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); - pchunk = tmp; - } else if (pchunk->size >= pchunk->len + sz) { - // reuse existing buffer - it is big enough, and not shared - char_type * dst = pchunk->buf + pchunk->len; - pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); - } else { - // create new buffer - chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); - char_type * dst = tmp->buf + tmp->len; - tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); - chunk_t::free(pchunk); - pchunk = tmp; - } + // create new buffer + chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); + char_type * dst = tmp->buf + tmp->len; + tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + chunk_t::free(pchunk); + pchunk = tmp; } return *this; } @@ -2597,6 +2645,9 @@ class string : public string_ro { /// Pass `npos` as source string element counter to calculate it for null-term string with str_len template string& appendUtf(const utf_type * s, size_type count = npos) noexcept { + if (empty()) { + return assignUtf(s, count); + } if (s == nullptr) { return *this; } else if (count == npos) { @@ -2604,29 +2655,22 @@ class string : public string_ro { } if (count == 0) { return *this; + } + size_type sz = utfConvDestBufferSize(s, s + count); + lUInt32 errorCount = 0; + if (pchunk->size >= pchunk->len + sz && pchunk->isOwn()) { + // reuse existing buffer - it is big enough, and not shared + char_type * dst = pchunk->buf + pchunk->len; + pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); } else { - size_type sz = utfConvDestBufferSize(s, s + count); - lUInt32 errorCount = 0; - if (pchunk == nullptr) { - // empty string, allocate new chunk with converted data - chunk_t * tmp = chunk_t::alloc(sz); - char_type * dst = tmp->buf; - tmp->len = utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); - pchunk = tmp; - } else if (pchunk->size >= pchunk->len + sz && pchunk->isOwn()) { - // reuse existing buffer - it is big enough, and not shared - char_type * dst = pchunk->buf + pchunk->len; - pchunk->len += utfConvert(s, s+count, dst, pchunk->buf + pchunk->size, errorCount); - } else { - // create new buffer - chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); - char_type * dst = tmp->buf + tmp->len; - tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); - if (pchunk) { - intrusive_ptr_release(pchunk); - } - pchunk = tmp; + // create new buffer + chunk_t * tmp = pchunk->duplicate(pchunk->len + sz); + char_type * dst = tmp->buf + tmp->len; + tmp->len += utfConvert(s, s+count, dst, tmp->buf + tmp->size, errorCount); + if (pchunk) { + intrusive_ptr_release(pchunk); } + pchunk = tmp; } return *this; } diff --git a/tests/src/lvstring2test.cpp b/tests/src/lvstring2test.cpp index 833c8bb890..112ad979af 100644 --- a/tests/src/lvstring2test.cpp +++ b/tests/src/lvstring2test.cpp @@ -2987,6 +2987,121 @@ void test_lstring2_unicode() { TCHECK(s.length() == 3); TCHECK(s[0]==0x41 && s[1]==0x1D11E && s[2]==0x10FFFF); } + + // --- compareUtf same type (delegates to compare) --- + { + lString8 a{"abc"}, b{"abc"}, c{"abd"}; + TCHECK(a.compareUtf(b) == 0); + TCHECK(a.compareUtf(c) < 0); + TCHECK(c.compareUtf(a) > 0); + } + + // --- compareUtf lString8 vs lString16 --- + { + lString8 a{"abc"}; + lString16 b; + const lChar16 abc[] = { 'a', 'b', 'c' }; + b.assignUtf(abc, 3); + TCHECK(a.compareUtf(b) == 0); + TCHECK(b.compareUtf(a) == 0); + } + // --- compareUtf lString8 vs lString16 (less/greater) --- + { + lString8 a{"abc"}; + lString16 b; + const lChar16 abd[] = { 'a', 'b', 'd' }; + b.assignUtf(abd, 3); + TCHECK(a.compareUtf(b) < 0); + TCHECK(b.compareUtf(a) > 0); + } + + // --- compareUtf lString8 vs lString32 --- + { + lString8 a{"abc"}; + lString32 b; + const lChar32 abc[] = { 'a', 'b', 'c' }; + b.assignUtf(abc, 3); + TCHECK(a.compareUtf(b) == 0); + TCHECK(b.compareUtf(a) == 0); + } + // --- compareUtf lString8 vs lString32 (less/greater) --- + { + lString8 a{"abc"}; + lString32 b; + const lChar32 abd[] = { 'a', 'b', 'd' }; + b.assignUtf(abd, 3); + TCHECK(a.compareUtf(b) < 0); + TCHECK(b.compareUtf(a) > 0); + } + + // --- compareUtf lString16 vs lString32 --- + { + lString16 a; + const lChar16 abc16[] = { 'a', 'b', 'c' }; + a.assignUtf(abc16, 3); + lString32 b; + const lChar32 abc32[] = { 'a', 'b', 'c' }; + b.assignUtf(abc32, 3); + TCHECK(a.compareUtf(b) == 0); + TCHECK(b.compareUtf(a) == 0); + } + // --- compareUtf lString16 vs lString32 (less/greater) --- + { + lString16 a; + const lChar16 abc16[] = { 'a', 'b', 'c' }; + a.assignUtf(abc16, 3); + lString32 b; + const lChar32 abd32[] = { 'a', 'b', 'd' }; + b.assignUtf(abd32, 3); + TCHECK(a.compareUtf(b) < 0); + TCHECK(b.compareUtf(a) > 0); + } + + // --- compareUtf empty vs non-empty --- + { + lString8 empty; + lString16 nonempty; + const lChar16 x[] = { 'x' }; + nonempty.assignUtf(x, 1); + TCHECK(empty.compareUtf(nonempty) < 0); + TCHECK(nonempty.compareUtf(empty) > 0); + } + + // --- compareUtf both empty --- + { + lString8 a; + lString16 b; + TCHECK(a.compareUtf(b) == 0); + } + + // --- compareUtf different codepoints (utf8 vs utf32 with non-ASCII) --- + { + lString8 a; + a.assignUtf("\xC3\xA9", 2); // U+00E9 + lString32 b; + const lChar32 e32[] = { 0xE9 }; + b.assignUtf(e32, 1); + TCHECK(a.compareUtf(b) == 0); + TCHECK(b.compareUtf(a) == 0); + } + + // --- compareUtf supplementary characters --- + { + lString16 a; + const lChar16 music16[] = { 0xD834, 0xDD1E }; // U+1D11E + a.assignUtf(music16, 2); + lString32 b; + const lChar32 music32[] = { 0x1D11E }; + b.assignUtf(music32, 1); + TCHECK(a.compareUtf(b) == 0); + TCHECK(b.compareUtf(a) == 0); + } + + // --- compareUtf same pointer (pchunk equality) --- + { + lString8 a{"test"}; + TCHECK(a.compareUtf(a) == 0); + } } void test_lstring2() {