This patch changes the string-buffer's *printf-based functions to use vsnzprintf instead of vsnprintf, thus - removing the INT_MAX limitation on the size of the result, - guaranteeing an errno value upon failure. (Recall that vsnprintf() does not guarantee an errno value when it fails.)
2024-09-24 Bruno Haible <br...@clisp.org> string-buffer: Remove INT_MAX limitation. * lib/string-buffer.h (sb_appendvf, sb_appendf): Document that errno is set upon failure. * lib/string-buffer-printf.c: Include <errno.h>. (sb_appendvf): Call vsnzprintf instead of vsnprintf. Ensure errno is set upon failure. (sb_appendf): Likewise. * modules/string-buffer (Depends-on): Add vsnzprintf-posix. Remove vsnprintf-posix. * tests/test-string-buffer.c (main): Test error code from sb_appendf. diff --git a/lib/string-buffer-printf.c b/lib/string-buffer-printf.c index 16ef4e4144..6e5ce78b35 100644 --- a/lib/string-buffer-printf.c +++ b/lib/string-buffer-printf.c @@ -25,6 +25,7 @@ extern int sb_ensure_more_bytes (struct string_buffer *buffer, size_t increment); +#include <errno.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> @@ -35,14 +36,15 @@ sb_appendvf (struct string_buffer *buffer, const char *formatstring, { va_list list_copy; - /* Make a bit of room, so that the probability that the first vsnprintf() call - succeeds is high. */ + /* Make a bit of room, so that the probability that the first vsnzprintf() + call succeeds is high. */ size_t room = buffer->allocated - buffer->length; if (room < 64) { if (sb_ensure_more_bytes (buffer, 64) < 0) { buffer->error = true; + errno = ENOMEM; return -1; } room = buffer->allocated - buffer->length; @@ -50,17 +52,18 @@ sb_appendvf (struct string_buffer *buffer, const char *formatstring, va_copy (list_copy, list); - /* First vsnprintf() call. */ - int ret = vsnprintf (buffer->data + buffer->length, room, formatstring, list); + /* First vsnzprintf() call. */ + ptrdiff_t ret = vsnzprintf (buffer->data + buffer->length, room, + formatstring, list); if (ret < 0) { - /* Failed. */ + /* Failed. errno is set. */ buffer->error = true; ret = -1; } else { - if ((size_t) ret <= room) + if (ret <= room) { /* The result has fit into room bytes. */ buffer->length += (size_t) ret; @@ -68,35 +71,36 @@ sb_appendvf (struct string_buffer *buffer, const char *formatstring, } else { - /* The result was truncated. Make more room, for a second vsnprintf() - call. */ + /* The result was truncated. Make more room, for a second + vsnzprintf() call. */ if (sb_ensure_more_bytes (buffer, (size_t) ret) < 0) { buffer->error = true; + errno = ENOMEM; ret = -1; } else { - /* Second vsnprintf() call. */ + /* Second vsnzprintf() call. */ room = buffer->allocated - buffer->length; - ret = vsnprintf (buffer->data + buffer->length, room, - formatstring, list_copy); + ret = vsnzprintf (buffer->data + buffer->length, room, + formatstring, list_copy); if (ret < 0) { - /* Failed. */ + /* Failed. errno is set. */ buffer->error = true; ret = -1; } else { - if ((size_t) ret <= room) + if (ret <= room) { /* The result has fit into room bytes. */ buffer->length += (size_t) ret; ret = 0; } else - /* The return values of the vsnprintf() calls are not + /* The return values of the vsnzprintf() calls are not consistent. */ abort (); } @@ -113,14 +117,15 @@ sb_appendf (struct string_buffer *buffer, const char *formatstring, ...) { va_list args; - /* Make a bit of room, so that the probability that the first vsnprintf() call - succeeds is high. */ + /* Make a bit of room, so that the probability that the first vsnzprintf() + call succeeds is high. */ size_t room = buffer->allocated - buffer->length; if (room < 64) { if (sb_ensure_more_bytes (buffer, 64) < 0) { buffer->error = true; + errno = ENOMEM; return -1; } room = buffer->allocated - buffer->length; @@ -128,17 +133,18 @@ sb_appendf (struct string_buffer *buffer, const char *formatstring, ...) va_start (args, formatstring); - /* First vsnprintf() call. */ - int ret = vsnprintf (buffer->data + buffer->length, room, formatstring, args); + /* First vsnzprintf() call. */ + ptrdiff_t ret = vsnzprintf (buffer->data + buffer->length, room, + formatstring, args); if (ret < 0) { - /* Failed. */ + /* Failed. errno is set. */ buffer->error = true; ret = -1; } else { - if ((size_t) ret <= room) + if (ret <= room) { /* The result has fit into room bytes. */ buffer->length += (size_t) ret; @@ -146,37 +152,38 @@ sb_appendf (struct string_buffer *buffer, const char *formatstring, ...) } else { - /* The result was truncated. Make more room, for a second vsnprintf() - call. */ + /* The result was truncated. Make more room, for a second + vsnzprintf() call. */ if (sb_ensure_more_bytes (buffer, (size_t) ret) < 0) { buffer->error = true; + errno = ENOMEM; ret = -1; } else { - /* Second vsnprintf() call. */ + /* Second vsnzprintf() call. */ room = buffer->allocated - buffer->length; va_end (args); va_start (args, formatstring); - ret = vsnprintf (buffer->data + buffer->length, room, - formatstring, args); + ret = vsnzprintf (buffer->data + buffer->length, room, + formatstring, args); if (ret < 0) { - /* Failed. */ + /* Failed. errno is set. */ buffer->error = true; ret = -1; } else { - if ((size_t) ret <= room) + if (ret <= room) { /* The result has fit into room bytes. */ buffer->length += (size_t) ret; ret = 0; } else - /* The return values of the vsnprintf() calls are not + /* The return values of the vsnzprintf() calls are not consistent. */ abort (); } diff --git a/lib/string-buffer.h b/lib/string-buffer.h index c854aaa809..9220ab301d 100644 --- a/lib/string-buffer.h +++ b/lib/string-buffer.h @@ -52,7 +52,10 @@ extern int sb_append (struct string_buffer *buffer, const char *str); /* Appends the result of the printf-compatible FORMATSTRING with the argument list LIST to BUFFER. - Returns 0, or -1 in case of error. */ + Returns 0, or -1 with errno set in case of error. + Error code EOVERFLOW can only occur when a width > INT_MAX is used. + Therefore, if the format string is valid and does not use %ls/%lc + directives nor widths, the only possible error code is ENOMEM. */ extern int sb_appendvf (struct string_buffer *buffer, const char *formatstring, va_list list) #if (__GNUC__ + (__GNUC_MINOR__ >= 4) > 4) && !defined __clang__ @@ -64,7 +67,10 @@ extern int sb_appendvf (struct string_buffer *buffer, /* Appends the result of the printf-compatible FORMATSTRING with the following arguments to BUFFER. - Returns 0, or -1 in case of error. */ + Returns 0, or -1 with errno set in case of error. + Error code EOVERFLOW can only occur when a width > INT_MAX is used. + Therefore, if the format string is valid and does not use %ls/%lc + directives nor widths, the only possible error code is ENOMEM. */ extern int sb_appendf (struct string_buffer *buffer, const char *formatstring, ...) #if (__GNUC__ + (__GNUC_MINOR__ >= 4) > 4) && !defined __clang__ diff --git a/modules/string-buffer b/modules/string-buffer index 76c07d94ac..2010641b2b 100644 --- a/modules/string-buffer +++ b/modules/string-buffer @@ -10,7 +10,7 @@ Depends-on: stdbool attribute stdarg -vsnprintf-posix +vsnzprintf-posix configure.ac: diff --git a/tests/test-string-buffer.c b/tests/test-string-buffer.c index 8538ac0cec..d492837b39 100644 --- a/tests/test-string-buffer.c +++ b/tests/test-string-buffer.c @@ -20,6 +20,7 @@ #include "string-buffer.h" +#include <errno.h> #include <string.h> #include <wchar.h> @@ -94,19 +95,33 @@ main () /* Test printf-like formatting failure. On all systems except AIX, trying to convert the wide-character 0x76543210 to a multibyte string (in the "C" locale) fails. - On all systems where REPLACE_VSNPRINTF=1 (this includes AIX), i.e. where - the Gnulib implementation of vsnprintf() is used), invalid format - directives make the *printf call fail. */ + On all systems, invalid format directives make the vsnzprintf() call + fail. */ { struct string_buffer buffer; + int ret; sb_init (&buffer); sb_append (&buffer, "<"); - sb_appendf (&buffer, "%lc", (wint_t) 0x76543210); + + ret = sb_appendf (&buffer, "%lc", (wint_t) 0x76543210); + #if !defined _AIX + ASSERT (ret < 0); + ASSERT (errno == EILSEQ); + #endif + sb_append (&buffer, "|"); - sb_appendf (&buffer, invalid_format_string_1, 1); + + ret = sb_appendf (&buffer, invalid_format_string_1, 1); + ASSERT (ret < 0); + ASSERT (errno == EINVAL); + sb_append (&buffer, "|"); - sb_appendf (&buffer, invalid_format_string_2, 2); + + ret = sb_appendf (&buffer, invalid_format_string_2, 2); + ASSERT (ret < 0); + ASSERT (errno == EINVAL); + sb_append (&buffer, ">"); char *s = sb_dupfree (&buffer); ASSERT (s == NULL);