diff --git a/src/basic/strv.c b/src/basic/strv.c index eb3cd8d4b2..a05da77e50 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -1214,6 +1214,31 @@ int string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const c return string_strv_hashmap_put_internal(PLAIN_HASHMAP(*h), key, value); } +static int rebreak_flush_line( + char ***broken, + const char *start, + const char *end, + bool in_prefix, + const char *whitespace_begin, + const char *whitespace_end) { + + assert(broken); + assert(start); + assert(end >= start); + + if (in_prefix) /* Never seen anything non-whitespace? Generate empty line! */ + return strv_extend(broken, ""); + + if (whitespace_begin && !whitespace_end) /* Ends in whitespace? Chop it off! */ + end = whitespace_begin; + + _cleanup_free_ char *truncated = strndup(start, end - start); + if (!truncated) + return -ENOMEM; + + return strv_consume(broken, TAKE_PTR(truncated)); +} + int strv_rebreak_lines(char **l, size_t width, char ***ret) { _cleanup_strv_free_ char **broken = NULL; int r; @@ -1224,7 +1249,8 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { * * Goes through all entries in *l, and line-breaks each line that is longer than the specified * character width. Breaks at the end of words/beginning of whitespace. Lines that do not contain whitespace are not - * broken. Retains whitespace at beginning of lines, removes it at end of lines. */ + * broken. Retains whitespace at beginning of lines, removes it at end of lines. Newlines embedded + * in an entry are hard line breaks, i.e. result in a separate output entry each. */ if (width == SIZE_MAX) { /* NOP? */ broken = strv_copy(l); @@ -1238,13 +1264,25 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { STRV_FOREACH(i, l) { const char *start = *i, *whitespace_begin = NULL, *whitespace_end = NULL; bool in_prefix = true; /* still in the whitespace in the beginning of the line? */ + bool after_newline = false; /* did we just break at a newline embedded in the input? */ size_t w = 0; for (const char *p = start; *p != 0; ) { if (strchr(NEWLINE, *p)) { + /* A newline embedded in the input is a hard line break: flush out what we + * collected so far as its own line, and continue with a new one right after + * it. */ + r = rebreak_flush_line(&broken, start, p, in_prefix, whitespace_begin, whitespace_end); + if (r < 0) + return r; + + p += p[0] == '\r' && p[1] == '\n' ? 2 : 1; /* Treat CRLF as a single break */ + start = p; in_prefix = true; + after_newline = true; whitespace_begin = whitespace_end = NULL; w = 0; + continue; } else if (strchr(WHITESPACE, *p)) { if (!in_prefix && (!whitespace_begin || whitespace_end)) { whitespace_begin = p; @@ -1282,6 +1320,7 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { * 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; + after_newline = false; whitespace_begin = whitespace_end = NULL; w = 0; continue; @@ -1290,18 +1329,14 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) { p += n; } - /* Process rest of the line */ + /* Process rest of the line, except if all that's left after the last newline we already + * flushed out above is whitespace (possibly nothing): a trailing newline terminates the + * last line, it doesn't add a new, empty one. */ assert(start); - if (in_prefix) /* Never seen anything non-whitespace? Generate empty line! */ - r = strv_extend(&broken, ""); - else if (whitespace_begin && !whitespace_end) { /* Ends in whitespace? Chop it off! */ - _cleanup_free_ char *truncated = strndup(start, whitespace_begin - start); - if (!truncated) - return -ENOMEM; + if (after_newline && in_prefix) + continue; - r = strv_consume(&broken, TAKE_PTR(truncated)); - } else /* Otherwise use line as is */ - r = strv_extend(&broken, start); + r = rebreak_flush_line(&broken, start, start + strlen(start), in_prefix, whitespace_begin, whitespace_end); if (r < 0) return r; } diff --git a/src/test/test-strv.c b/src/test/test-strv.c index 53156a9412..f94f1e328f 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1281,6 +1281,38 @@ TEST(strv_rebreak_lines) { * the whitespace we broke at. */ ASSERT_ERROR(strv_rebreak_lines(STRV_MAKE("a \xF0" "b"), 3, &l), EINVAL); ASSERT_NULL(l); + + /* Newlines embedded in the input are hard line breaks. Previously everything before such a newline + * was silently dropped if nothing but whitespace followed it. */ + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo\n"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo"))); + l = strv_free(l); + + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo\n "), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo"))); + l = strv_free(l); + + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo\nbar"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo", "bar"))); + l = strv_free(l); + + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo \n bar \n"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo", " bar"))); + l = strv_free(l); + + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo\n\nbar"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo", "", "bar"))); + l = strv_free(l); + + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("foo\r\nbar\r\n"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("foo", "bar"))); + l = strv_free(l); + + /* And the resulting lines are properly broken to the requested width, rather than staying glued + * together in a single, over-long entry. */ + ASSERT_OK(strv_rebreak_lines(STRV_MAKE("hello world\nfoo bar baz quux waldo"), 10, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("hello", "world", "foo bar", "baz quux", "waldo"))); + l = strv_free(l); } TEST(strv_find_closest) {