diff --git a/src/basic/strv.c b/src/basic/strv.c index 0be282eb59..eb3cd8d4b2 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -8,7 +8,6 @@ #include "escape.h" #include "extract-word.h" #include "fileio.h" -#include "gunicode.h" #include "hashmap.h" #include "log.h" #include "memory-util.h" @@ -1241,7 +1240,7 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { bool in_prefix = true; /* still in the whitespace in the beginning of the line? */ size_t w = 0; - for (const char *p = start; *p != 0; p = utf8_next_char(p)) { + for (const char *p = start; *p != 0; ) { if (strchr(NEWLINE, *p)) { in_prefix = true; whitespace_begin = whitespace_end = NULL; @@ -1258,11 +1257,13 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { in_prefix = false; } - int cw = utf8_char_console_width(p); - if (cw < 0) { - log_debug_errno(cw, "Comment to line break contains invalid UTF-8, ignoring."); - cw = 1; - } + char32_t c; + int n = utf8_encoded_to_unichar(p, &c); + if (n < 0) + return log_debug_errno(n, "Line to break contains invalid UTF-8, refusing: %m"); + + int cw = unichar_console_width(c); + assert(cw >= 0); w += cw; @@ -1277,10 +1278,16 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { if (r < 0) return r; + /* Continue with the next line, starting at the first character after the + * whitespace we broke at. Note, that character has not been measured for + * the new line yet, hence reset the width and reprocess it. */ p = start = whitespace_end; whitespace_begin = whitespace_end = NULL; - w = cw; + w = 0; + continue; } + + p += n; } /* Process rest of the line */ diff --git a/src/test/test-strv.c b/src/test/test-strv.c index 386c54f787..53156a9412 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1267,6 +1267,20 @@ TEST(strv_rebreak_lines) { assert_se(strv_equal(a, b)); } + + /* Invalid UTF-8 is refused rather than measured. Previously a truncated multibyte lead byte at the + * end of a line was blind-skipped by its expected length, stepping over the NUL and reading past + * the end of the string. */ + ASSERT_ERROR(strv_rebreak_lines(STRV_MAKE("foo\xF0"), 10, &l), EINVAL); + ASSERT_NULL(l); + + ASSERT_ERROR(strv_rebreak_lines(STRV_MAKE("bar\xFC"), 10, &l), EINVAL); + ASSERT_NULL(l); + + /* Same, but reached after a line break, i.e. with the scan restarted at the character following + * the whitespace we broke at. */ + ASSERT_ERROR(strv_rebreak_lines(STRV_MAKE("a \xF0" "b"), 3, &l), EINVAL); + ASSERT_NULL(l); } TEST(strv_find_closest) {