string-util: add upper bound to ellipsize_mem UTF-8 walks

Validate UTF-8 characters against the caller-provided byte slice.
This stops truncated sequences from borrowing bytes past old_length.

Follow-up for e69cc9eb36
This commit is contained in:
Luca Boccassi
2026-07-14 16:43:28 +01:00
parent 9ac37aa0c7
commit 66600e15c9
2 changed files with 55 additions and 10 deletions
+49 -10
View File
@@ -357,6 +357,33 @@ static char *ascii_ellipsize_mem(const char *s, size_t old_length, size_t new_le
return t;
}
/* Walk backwards from 'end' (exclusive) to the start of the last complete UTF-8 character, without
* descending below 'start', validate it and return a pointer to its first byte, optionally decoding it
* into *ret_c. Returns NULL on a truncated or otherwise malformed sequence. */
static const char* find_previous_unichar(const char *start, const char *end, char32_t *ret_c) {
const char *p;
int r;
assert(start);
assert(end);
assert(end > start);
for (p = end; p > start; ) {
p--;
if (((uint8_t) *p & 0xc0) != 0x80) /* Found a non-continuation byte, i.e. a character start. */
break;
}
r = utf8_encoded_valid_unichar(p, end - p);
if (r < 0 || p + r != end)
return NULL;
if (ret_c)
assert_se(utf8_encoded_to_unichar(p, ret_c) == r);
return p;
}
char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigned percent) {
size_t x, k, len, len2;
const char *i, *j;
@@ -400,11 +427,13 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
continue; /* ANSI sequences don't take up any space in output */
}
char32_t c;
r = utf8_encoded_to_unichar(i, &c);
r = utf8_encoded_valid_unichar(i, s + old_length - i);
if (r < 0)
return NULL;
char32_t c;
assert_se(utf8_encoded_to_unichar(i, &c) == r);
int w = unichar_iswide(c) ? 2 : 1;
if (k + w > x)
break;
@@ -431,9 +460,9 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
continue;
}
tt = utf8_prev_char(t);
r = utf8_encoded_to_unichar(tt, &c);
if (r < 0)
/* Find the previous complete UTF-8 character inside the retained suffix. */
tt = find_previous_unichar(i, t, &c);
if (!tt)
return NULL;
w = unichar_iswide(c) ? 2 : 1;
@@ -451,11 +480,21 @@ char* ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne
if (k >= new_length) {
/* Make space for ellipsis, if required and possible. We know that the edge character is not
* part of an ANSI sequence (because then we'd skip it). If the last character we looked at
* was wide, we don't need to make space. */
if (j < s + old_length)
j = utf8_next_char(j);
else if (i > s)
i = utf8_prev_char(i);
* was wide, we don't need to make space.
* Move the edge by one complete UTF-8 character within the input slice. */
if (j < s + old_length) {
r = utf8_encoded_valid_unichar(j, s + old_length - j);
if (r < 0)
return NULL;
j += r;
} else if (i > s) {
const char *tt = find_previous_unichar(s, i, NULL);
if (!tt)
return NULL;
i = tt;
}
}
len = i - s;