diff --git a/src/display_model.c b/src/display_model.c index e5aa48d..ce350b2 100644 --- a/src/display_model.c +++ b/src/display_model.c @@ -123,10 +123,20 @@ static unsigned int utf8_char_width(const char **p) if ((s[0] & 0xE0) == 0xC0) { byte_len = 2U; - display_cols = 1U; } else if ((s[0] & 0xF0) == 0xE0) { byte_len = 3U; - /* Decode code point to determine width */ + } else if ((s[0] & 0xF8) == 0xF0) { + byte_len = 4U; + } + + for (unsigned int i = 1U; i < byte_len; ++i) { + if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) { + byte_len = i; + break; + } + } + + if (byte_len == 3U) { unsigned int cp = ((unsigned int)(s[0] & 0x0F) << 12) | ((unsigned int)(s[1] & 0x3F) << 6) | (unsigned int)(s[2] & 0x3F); @@ -142,9 +152,7 @@ static unsigned int utf8_char_width(const char **p) (cp >= 0xFFE0 && cp <= 0xFFE6)) { display_cols = 2U; } - } else if ((s[0] & 0xF8) == 0xF0) { - byte_len = 4U; - /* Most 4-byte characters (supplementary planes) including CJK ext. */ + } else if (byte_len == 4U) { unsigned int cp = ((unsigned int)(s[0] & 0x07) << 18) | ((unsigned int)(s[1] & 0x3F) << 12) | ((unsigned int)(s[2] & 0x3F) << 6) | @@ -154,14 +162,6 @@ static unsigned int utf8_char_width(const char **p) } } - /* Validate continuation bytes */ - for (unsigned int i = 1U; i < byte_len && s[i] != '\0'; ++i) { - if ((s[i] & 0xC0) != 0x80) { - byte_len = i; - break; - } - } - *p = (const char *)(s + byte_len); return display_cols; } diff --git a/tests/display_model_test.c b/tests/display_model_test.c index 9963dc1..323b927 100644 --- a/tests/display_model_test.c +++ b/tests/display_model_test.c @@ -550,6 +550,33 @@ static void test_burst_replay(void) ++g_tests_passed; } +static void test_wrap_truncated_utf8_stays_in_bounds(void) +{ + const unsigned char prefix[] = {0xF0U, 0x9FU, 0x92U}; + for (size_t length = 1U; length <= sizeof(prefix); ++length) { + char *text = malloc(length + 1U); + TEST_ASSERT(text != NULL, "allocate exact truncated UTF-8 buffer"); + memcpy(text, prefix, length); + text[length] = '\0'; + + display_line_t out[4]; + size_t produced = display_model_wrap_line(text, 80, 77, out, 4); + free(text); + TEST_ASSERT_EQ(produced, 1U, "truncated UTF-8 should produce one line"); + } + ++g_tests_passed; +} + +static void test_wrap_invalid_utf8_continuation(void) +{ + const char text[] = "\xE2" "A\xA2"; + display_line_t out[4]; + size_t produced = display_model_wrap_line(text, 80, 78, out, 4); + TEST_ASSERT_EQ(produced, 1U, "invalid continuation should not loop"); + TEST_ASSERT_EQ(out[0].message_id, 78U, "message id should survive invalid input"); + ++g_tests_passed; +} + /* ---- Main ---- */ int main(void) @@ -573,6 +600,8 @@ int main(void) test_empty_model(); test_long_wrap_content_preserved(); test_burst_replay(); + test_wrap_truncated_utf8_stays_in_bounds(); + test_wrap_invalid_utf8_continuation(); printf("\n%d passed, %d failed\n", g_tests_passed, g_tests_failed);