From 88c3b7d1185a3fcc70997cd92b52eef846d2913f Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 26 May 2026 16:11:30 +0800 Subject: [PATCH] Common (stringUtil): adds `ffUtf8CharLenWidth` --- src/common/stringUtils.h | 84 +++++++++++++++++++++ src/logo/logo.c | 19 +---- src/modules/separator/separator.c | 76 ++++--------------- tests/strutil.c | 118 ++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 79 deletions(-) create mode 100644 tests/strutil.c diff --git a/src/common/stringUtils.h b/src/common/stringUtils.h index 34e5cc832..1bc77ecae 100644 --- a/src/common/stringUtils.h +++ b/src/common/stringUtils.h @@ -5,6 +5,8 @@ #include #include +#include "common/wcwidth.h" + static inline bool ffStrSet(const char* str) { if (str == NULL) { return false; @@ -71,6 +73,88 @@ static inline bool ffCharIsDigit(char c) { return '0' <= c && c <= '9'; } +// Parse one UTF-8 character, returning consumed byte count and display width. +// Invalid / incomplete sequence falls back to one-byte width=1. +// If the Unicode codepoint is non-printable, width becomes 0. +static inline uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width) { + if (__builtin_expect(length == 0 || *str == '\0', false)) { + if (width) { + *width = 0; + } + return 0; + } + + unsigned char first = (unsigned char) *str; + if (__builtin_expect(first < 0x80, true)) { + if (width) { + *width = 1; + } + return 1; + } + + uint8_t bytes; + if ((first & 0xE0) == 0xC0) { + bytes = 2; + } else if ((first & 0xF0) == 0xE0) { + bytes = 3; + } else if ((first & 0xF8) == 0xF0) { + bytes = 4; + } else { + if (width) { + *width = 1; + } + return 1; + } + + if (length < bytes) { + if (width) { + *width = 1; + } + return 1; + } + + for (uint8_t i = 1; i < bytes; ++i) { + unsigned char continuation = (unsigned char) str[i]; + if (continuation == '\0' || (continuation & 0xC0) != 0x80) { + if (width) { + *width = 1; + } + return 1; + } + } + + uint32_t ucs = (uint32_t) (first & ((1U << (8 - bytes)) - 1)); + for (uint8_t i = 1; i < bytes; ++i) { + ucs <<= 6; + ucs |= (uint32_t) ((unsigned char) str[i] & 0x3F); + } + + int wcWidth = mk_wcwidth(ucs); + if (width) { + *width = (uint8_t) (wcWidth < 0 ? 0 : wcWidth); + } + return bytes; +} + +static inline uint32_t ffUtf8StrWidth(const char* str, uint32_t length) { + uint32_t result = 0; + const char* ptr = str; + + while (length > 0 && *ptr != '\0') { + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth(ptr, length, &width); + if (__builtin_expect(bytes == 0, false)) { + break; + } + + result += width; + ptr += bytes; + length -= bytes; + } + + return result > 0 ? result : (uint32_t) (ptr - str); +} + static inline bool ffCharIsHexDigit(char c) { return ffCharIsDigit(c) || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F'); } diff --git a/src/logo/logo.c b/src/logo/logo.c index 160c4b641..48696b1d3 100644 --- a/src/logo/logo.c +++ b/src/logo/logo.c @@ -139,22 +139,9 @@ static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bo } } - ++lineWidth; - - int codepoint = (unsigned char) *data; - uint8_t bytes; - - if (codepoint <= 127) { - bytes = 1; - } else if ((codepoint & 0xE0) == 0xC0) { - bytes = 2; - } else if ((codepoint & 0xF0) == 0xE0) { - bytes = 3; - } else if ((codepoint & 0xF8) == 0xF0) { - bytes = 4; - } else { - bytes = 1; - } + uint8_t charWidth; + uint8_t bytes = ffUtf8CharLenWidth(data, UINT32_MAX, &charWidth); + lineWidth += charWidth; for (uint8_t i = 0; i < bytes; ++i) { if (*data == '\0') { diff --git a/src/modules/separator/separator.c b/src/modules/separator/separator.c index 7be01919c..402ccb65c 100644 --- a/src/modules/separator/separator.c +++ b/src/modules/separator/separator.c @@ -1,64 +1,10 @@ #include "common/printing.h" #include "common/jsonconfig.h" #include "common/stringUtils.h" -#include "common/mallocHelper.h" -#include "common/wcwidth.h" #include "common/textModifier.h" #include "logo/logo.h" #include "modules/separator/separator.h" -#include - -#if __SIZEOF_WCHAR_T__ == 4 -static inline size_t mbrtoc32(uint32_t* restrict pc32, const char* restrict s, size_t n, mbstate_t* restrict ps) { - return mbrtowc((wchar_t*) pc32, s, n, ps); -} -#else - #include -#endif - -static uint8_t getMbrWidth(const char* mbstr, uint32_t length, const char** next, mbstate_t* state) { - if (__builtin_expect((uint8_t) *mbstr < 0x80, true)) // ASCII fast path - { - if (next) { - *next = mbstr + 1; - } - return 1; - } - - uint32_t c32; - uint32_t len = (uint32_t) mbrtoc32(&c32, mbstr, length, state); - if (len >= (uint32_t) -3) { - // Invalid or incomplete multibyte sequence - if (next) { - *next = mbstr + 1; - } - return 1; - } - - if (next) { - *next = mbstr + len; - } - int width = mk_wcwidth(c32); - return width < 0 ? 0 : (uint8_t) width; -} - -static uint32_t getWcsWidth(const FFstrbuf* mbstr) { - mbstate_t state = {}; - uint32_t remainLength = mbstr->length; - uint32_t result = 0; - - const char* ptr = mbstr->chars; - while (remainLength > 0 && *ptr != '\0') { - const char* lastPtr = NULL; - result += getMbrWidth(ptr, remainLength, &lastPtr, &state); - remainLength -= (uint32_t) (lastPtr - ptr); - ptr = lastPtr; - } - - return result > 0 ? (uint32_t) result : mbstr->length; -} - bool ffPrintSeparator(FFSeparatorOptions* options) { ffLogoPrintLine(); @@ -75,17 +21,16 @@ bool ffPrintSeparator(FFSeparatorOptions* options) { } } } else { - setlocale(LC_CTYPE, ""); const FFPlatform* platform = &instance.state.platform; uint32_t titleLength = 1 // @ - + getWcsWidth(&platform->userName) // user name + + ffUtf8StrWidth(platform->userName.chars, platform->userName.length) // user name + (instance.state.titleFqdn ? platform->hostName.length : ffStrbufFirstIndexC(&platform->hostName, '.')); // host name if (__builtin_expect(options->string.length == 1, 1)) { ffPrintCharTimes(options->string.chars[0], titleLength); } else { - uint32_t wcsLength = getWcsWidth(&options->string); + uint32_t wcsLength = ffUtf8StrWidth(options->string.chars, options->string.length); int remaining = (int) titleLength; // Write the whole separator as often as it fits fully into titleLength @@ -98,11 +43,17 @@ bool ffPrintSeparator(FFSeparatorOptions* options) { if (wcsLength != options->string.length) { // Unicode chars const char* ptr = options->string.chars; - mbstate_t state = {}; - const char* next = NULL; - while (remaining > 0 && *ptr != '\0') { - remaining -= (int) getMbrWidth(ptr, (uint32_t) (options->string.length - (ptr - options->string.chars)), &next, &state); - ptr = next; + uint32_t remainBytes = options->string.length; + while (remaining > 0 && remainBytes > 0 && *ptr != '\0') { + uint8_t charWidth = 0; + uint8_t bytes = ffUtf8CharLenWidth(ptr, remainBytes, &charWidth); + if (__builtin_expect(bytes == 0, false)) { + break; + } + + remaining -= (int) charWidth; + ptr += bytes; + remainBytes -= bytes; } fwrite(options->string.chars, (size_t) (ptr - options->string.chars), 1, stdout); } else { @@ -110,7 +61,6 @@ bool ffPrintSeparator(FFSeparatorOptions* options) { } } } - setlocale(LC_CTYPE, "C"); } if (options->outputColor.length && !instance.config.display.pipe) { diff --git a/tests/strutil.c b/tests/strutil.c new file mode 100644 index 000000000..77614239f --- /dev/null +++ b/tests/strutil.c @@ -0,0 +1,118 @@ +#include "common/stringUtils.h" +#include "common/textModifier.h" + +#include +#include +#include + +static void verify(bool expression, const char* expressionStr, int lineNo) { + if (expression) { + return; + } + + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr); + exit(1); +} + +#define VERIFY(expression) verify((expression), #expression, __LINE__) + +int main(void) { + { + uint8_t width = 255; + uint8_t bytes = ffUtf8CharLenWidth("", 0, &width); + VERIFY(bytes == 0); + VERIFY(width == 0); + } + + { + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth("A", 1, &width); + VERIFY(bytes == 1); + VERIFY(width == 1); + } + + { + const char* ch = "\xE6\x96\x87"; // 文 U+6587 + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth(ch, 3, &width); + int expected = mk_wcwidth(0x6587); + VERIFY(bytes == 3); + VERIFY(width == (uint8_t) (expected < 0 ? 0 : expected)); + } + + { + const char* combining = "\xCC\x81"; // U+0301 + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth(combining, 2, &width); + int expected = mk_wcwidth(0x0301); + VERIFY(bytes == 2); + VERIFY(width == (uint8_t) (expected < 0 ? 0 : expected)); + } + + { + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth("\xE6\x96\x87", 1, &width); // truncated + VERIFY(bytes == 1); + VERIFY(width == 1); + } + + { + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth("\xE6" + "A", + 2, + &width); // invalid continuation + VERIFY(bytes == 1); + VERIFY(width == 1); + } + + { + VERIFY(ffUtf8StrWidth("abc", 3) == 3); + } + + { + const char* mixed = "A" + "\xE6\x96\x87" + "B"; + int wide = mk_wcwidth(0x6587); + uint32_t expected = 2 + (uint32_t) (wide < 0 ? 0 : wide); + VERIFY(ffUtf8StrWidth(mixed, 5) == expected); + } + + { + const char* combining = "A\xCC\x81"; + int wCombining = mk_wcwidth(0x0301); + uint32_t expected = 1 + (uint32_t) (wCombining < 0 ? 0 : wCombining); + VERIFY(ffUtf8StrWidth(combining, 3) == expected); + } + + { + VERIFY(ffUtf8StrWidth("\xE6" + "A", + 2) == 2); + } + + { + VERIFY(ffUtf8StrWidth("", 0) == 0); + VERIFY(ffUtf8StrWidth("A\0B", 3) == 1); + } + + { + const char* combining = "\xCC\x81"; + int wCombining = mk_wcwidth(0x0301); + uint32_t normalized = (uint32_t) (wCombining < 0 ? 0 : wCombining); + uint32_t expected = normalized > 0 ? normalized : 2; + VERIFY(ffUtf8StrWidth(combining, 2) == expected); + } + + { + const char* emoji = "\xF0\x9F\x98\x80"; // U+1F600 😀 + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth(emoji, 4, &width); + VERIFY(bytes == 4); + VERIFY(width == 2); // Most emoji are double-width + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +}