Skip to content

Commit 572f96b

Browse files
jhassehorenmar
authored andcommitted
Fix counting of UTF-8 codepoints in TextFlow
For line-wrapping bytes were counted instead of codepoints. This resulted in line-breaks being inserted at the wrong position and also breaking UTF-8 characters. Fixes #1022.
1 parent 8492fd4 commit 572f96b

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

src/catch2/internal/catch_textflow.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ namespace {
2626
return std::memchr( chars, c, sizeof( chars ) - 1 ) != nullptr;
2727
}
2828

29+
bool isUtf8ContinuationByte( char c ) {
30+
return ( static_cast<unsigned char>( c ) & 0xC0 ) == 0x80;
31+
}
32+
2933
} // namespace
3034

3135
namespace Catch {
@@ -52,6 +56,11 @@ namespace Catch {
5256
if ( it != m_string.end() ) {
5357
++m_size;
5458
++it;
59+
// Skip UTF-8 continuation bytes
60+
while ( it != m_string.end() &&
61+
isUtf8ContinuationByte( *it ) ) {
62+
++it;
63+
}
5564
}
5665
}
5766
}
@@ -114,6 +123,11 @@ namespace Catch {
114123
void AnsiSkippingString::const_iterator::advance() {
115124
assert( m_it != m_string->end() );
116125
m_it++;
126+
// Skip UTF-8 continuation bytes
127+
while ( m_it != m_string->end() &&
128+
isUtf8ContinuationByte( *m_it ) ) {
129+
m_it++;
130+
}
117131
tryParseAnsiEscapes();
118132
}
119133

@@ -133,6 +147,11 @@ namespace Catch {
133147
assert( *m_it == '\033' );
134148
m_it--;
135149
}
150+
// Skip back over UTF-8 continuation bytes to the leading byte
151+
while ( isUtf8ContinuationByte( *m_it ) ) {
152+
assert( m_it != m_string->begin() );
153+
m_it--;
154+
}
136155
}
137156

138157
static bool isBoundary( AnsiSkippingString const& line,

tests/SelfTest/IntrospectiveTests/TextFlow.tests.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,81 @@ TEST_CASE( "TextFlow::AnsiSkippingString substrings properly",
381381
}
382382
}
383383

384+
TEST_CASE( "TextFlow::AnsiSkippingString counts UTF-8 codepoints",
385+
"[TextFlow][ansiskippingstring][approvals]" ) {
386+
SECTION( "2-byte codepoints" ) {
387+
AnsiSkippingString str( "\xC3\xA4\xC3\xB6\xC3\xBC" ); // äöü
388+
CHECK( str.size() == 3 );
389+
}
390+
SECTION( "3-byte codepoints" ) {
391+
AnsiSkippingString str( "\xE4\xB8\xAD\xE6\x96\x87" ); // 中文
392+
CHECK( str.size() == 2 );
393+
}
394+
SECTION( "4-byte codepoints" ) {
395+
// U+1F600 U+1F60E
396+
AnsiSkippingString str( "\xF0\x9F\x98\x80\xF0\x9F\x98\x8E" );
397+
CHECK( str.size() == 2 );
398+
}
399+
SECTION( "mixed ASCII and UTF-8" ) {
400+
AnsiSkippingString str( "a\xC3\xA4" "b" ); // aäb
401+
CHECK( str.size() == 3 );
402+
}
403+
SECTION( "UTF-8 with ANSI escapes" ) {
404+
AnsiSkippingString str( "\033[31m\xC3\xA4\xC3\xB6\xC3\xBC\033[0m" );
405+
CHECK( str.size() == 3 );
406+
}
407+
}
408+
409+
TEST_CASE( "TextFlow::AnsiSkippingString iterates UTF-8 codepoints",
410+
"[TextFlow][ansiskippingstring][approvals]" ) {
411+
// "aäb" = 'a' (0x61), 'ä' (0xC3 0xA4), 'b' (0x62)
412+
std::string text = "a\xC3\xA4" "b";
413+
AnsiSkippingString str( text );
414+
415+
SECTION( "forward iteration has correct count" ) {
416+
int count = 0;
417+
for ( auto it = str.begin(); it != str.end(); ++it ) {
418+
++count;
419+
}
420+
CHECK( count == 3 );
421+
}
422+
SECTION( "backward iteration has correct count" ) {
423+
auto it = str.end();
424+
int count = 0;
425+
while ( it != str.begin() ) {
426+
--it;
427+
++count;
428+
}
429+
CHECK( count == 3 );
430+
}
431+
SECTION( "substring preserves full UTF-8 bytes" ) {
432+
auto a = str.begin();
433+
auto b = str.begin();
434+
++b; // past 'a'
435+
++b; // past 'ä'
436+
CHECK( str.substring( a, b ) == "a\xC3\xA4" );
437+
}
438+
}
439+
440+
TEST_CASE( "TextFlow::Column wraps UTF-8 text correctly",
441+
"[TextFlow][column][approvals]" ) {
442+
// "äöü äöü äöü" = 11 codepoints, 17 bytes
443+
Column col( "\xC3\xA4\xC3\xB6\xC3\xBC \xC3\xA4\xC3\xB6\xC3\xBC \xC3\xA4\xC3\xB6\xC3\xBC" );
444+
445+
SECTION( "width=8" ) {
446+
col.width( 8 );
447+
// 7 visible codepoints "äöü äöü" fit, then wrap
448+
REQUIRE( as_written( col ) ==
449+
"\xC3\xA4\xC3\xB6\xC3\xBC \xC3\xA4\xC3\xB6\xC3\xBC\n"
450+
"\xC3\xA4\xC3\xB6\xC3\xBC" );
451+
}
452+
SECTION( "width=80" ) {
453+
col.width( 80 );
454+
REQUIRE( as_written( col ) ==
455+
"\xC3\xA4\xC3\xB6\xC3\xBC \xC3\xA4\xC3\xB6\xC3\xBC \xC3\xA4\xC3\xB6\xC3\xBC" );
456+
}
457+
}
458+
384459
TEST_CASE( "TextFlow::Column skips ansi escape sequences",
385460
"[TextFlow][column][approvals]" ) {
386461
std::string text = "\033[38;2;98;174;239m\033[38;2;198;120;221mThe quick brown \033[38;2;198;120;221mfox jumped over the lazy dog\033[0m";

0 commit comments

Comments
 (0)