From 0620370dd93895ecf74306a0f21bf73bcd2671dc Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 16:33:09 -0400 Subject: [PATCH 1/2] Remove unused snprintf count parameter --- UPGRADING.md | 5 ++- include/akstdlib.h | 13 +++---- src/stdlib.c | 21 ++++-------- tests/negative/format_mismatch.c | 3 +- tests/test_format.c | 59 +++++++++++--------------------- tests/test_pool.c | 8 ++--- 6 files changed, 38 insertions(+), 71 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index e4d6f99..58dba83 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -83,11 +83,10 @@ having visited nothing. /* before */ aksl_sprintf(&count, buf, "%s=%d", key, value); /* after */ -aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value); +aksl_snprintf(buf, sizeof(buf), "%s=%d", key, value); ``` -Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` is -`0` on any failure rather than `vsprintf`'s `-1`. +Truncation is `AKERR_OUTOFBOUNDS` rather than a short success. **`aksl_realpath` takes the destination's length.** diff --git a/include/akstdlib.h b/include/akstdlib.h index 6dab689..1afe09a 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -444,9 +444,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v /* ====================================================================== */ /** @name Formatted output * - * `*count` is the byte count written excluding the terminating NUL, and is 0 on - * every failure path -- never vsnprintf's -1, and never the length the output - * *would* have been. + * Bounded output is checked for truncation and reports an error when the result + * does not fit. * * There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an * error-handling wrapper around an unbounded write is the sharp edge this @@ -483,16 +482,15 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea * written, leaving the caller to notice by comparing that against the buffer * size -- the check this library exists to stop people forgetting. * - * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. * @param[out] str Destination buffer. Required. * @param[in] size Size of `str` including the terminator. Must be non-zero. * @param[in] format printf format string. Required. Checked at compile time. - * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_NULLPOINTER If str or format is NULL. * @throws AKERR_VALUE If size is 0. * @throws AKERR_OUTOFBOUNDS If the output does not fit, naming both lengths. * @return NULL on success, an error context otherwise. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(4, 5); +akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4); /** * @brief Format into a freshly allocated string. @@ -551,7 +549,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre /** * @brief vsnprintf(3) into a bounded buffer. The va_list form of aksl_snprintf. - * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. * @param[out] str Destination buffer. Required. * @param[in] size Size of `str` including the terminator. Must be non-zero. * @param[in] format printf format string. Required. @@ -561,7 +558,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre * @throws AKERR_OUTOFBOUNDS If the output does not fit. * @return NULL on success, an error context otherwise. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args); +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args); /** @} */ diff --git a/src/stdlib.c b/src/stdlib.c index 2fb5a59..259844b 100644 --- a/src/stdlib.c +++ b/src/stdlib.c @@ -416,10 +416,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream) * register-save state on some ABIs. akbasic's text sink ran this UB on every * line of program output without anything visibly misbehaving, which is * exactly what made it worth fixing before something did. - * - *count is written on every path. It used to be left holding vprintf's -1 - * after a failure, so a caller who read the length rather than the status got - * a negative byte count out of a function that had already failed. It is now - * 0 whenever an error is raised. * - errno is cleared before the call and read back through AKSL_ERRNO_OR, so a * failure can never be reported with a stale -- or with a zero -- status. * @@ -485,34 +481,31 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea * *would* have written and silently drops the rest, which is the single most * common way a bounded write goes wrong unnoticed; a caller who wanted to know * would have had to compare the return against the buffer size by hand, which is - * the check this library exists to stop people forgetting. *count is the number - * of bytes written excluding the terminating NUL, and is 0 on any failure. + * the check this library exists to stop people forgetting. The bounded wrapper + * reports truncation instead. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args) +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args) { int needed = 0; PREPARE_ERROR(e); - FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); - *count = 0; - FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); - FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); + FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format); + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL"); errno = 0; needed = vsnprintf(str, size, format, args); FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error"); FAIL_NONZERO_RETURN(e, ((size_t)needed >= size), AKERR_OUTOFBOUNDS, "output truncated: %d bytes needed, %zu available", needed, size); - *count = needed; SUCCEED_RETURN(e); } -akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) +akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); - raised = aksl_vsnprintf(count, str, size, format, args); + raised = aksl_vsnprintf(str, size, format, args); va_end(args); return raised; } diff --git a/tests/negative/format_mismatch.c b/tests/negative/format_mismatch.c index c9f554f..b244505 100644 --- a/tests/negative/format_mismatch.c +++ b/tests/negative/format_mismatch.c @@ -16,11 +16,10 @@ int main(void) { char buf[64]; - int count = 0; akerr_ErrorContext *raised = NULL; /* %d against a string. This is the line that must not build. */ - raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int"); + raised = aksl_snprintf(buf, sizeof(buf), "%d", "not an int"); return raised == NULL ? 0 : 1; } diff --git a/tests/test_format.c b/tests/test_format.c index 39ba642..a06b4fe 100644 --- a/tests/test_format.c +++ b/tests/test_format.c @@ -2,10 +2,8 @@ * Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and * their va_list forms. * - * Formatted output, complete. Each happy path asserts both halves of the - * contract -- the byte count handed back through *count and the text that - * actually landed somewhere -- and every pointer argument is checked for its - * NULL guard. + * Formatted output, complete. Each happy path asserts the text that actually + * landed somewhere, and every pointer argument is checked for its NULL guard. * * Readback goes through plain libc rather than aksl_fread so that a failure here * points at the formatted-output wrapper under test and not at the stream @@ -39,14 +37,12 @@ static long read_file(const char *path, char *buf, size_t n) return (long)got; } -static int test_snprintf_writes_text_and_count(void) +static int test_snprintf_writes_text(void) { char buf[64]; - int count = -1; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7)); - AKSL_CHECK(count == 3); + AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s=%d", "x", 7)); AKSL_CHECK(strcmp(buf, "x=7") == 0); return 0; } @@ -54,10 +50,8 @@ static int test_snprintf_writes_text_and_count(void) static int test_snprintf_empty_format_writes_nothing(void) { char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' }; - int count = -1; - AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "")); - AKSL_CHECK(count == 0); + AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", "")); AKSL_CHECK(buf[0] == '\0'); return 0; } @@ -66,18 +60,17 @@ static int test_snprintf_empty_format_writes_nothing(void) * The case that could not be written while the wrapper was aksl_sprintf: output * longer than the destination. snprintf(3) would truncate, NUL-terminate and * report the length it *would* have written, leaving the caller to notice; here - * it is an error, and *count is 0 rather than the would-have-been length. + * it is an error. */ static int test_snprintf_truncation_is_an_error(void) { char buf[8]; - int count = -1; memset(buf, 0x00, sizeof(buf)); AKSL_CHECK_STATUS_MSG_CONTAINS( - aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"), + aksl_snprintf(buf, sizeof(buf), "%s", "far too long for eight bytes"), AKERR_OUTOFBOUNDS, "truncated"); - AKSL_CHECK(count == 0); + AKSL_CHECK(strcmp(buf, "far too") == 0); return 0; } @@ -85,34 +78,27 @@ static int test_snprintf_truncation_is_an_error(void) static int test_snprintf_boundary_is_exact(void) { char buf[8]; - int count = -1; /* 7 characters plus the NUL is exactly sizeof(buf). */ - AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567")); - AKSL_CHECK(count == 7); + AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", "1234567")); AKSL_CHECK(strcmp(buf, "1234567") == 0); - count = -1; - AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"), + AKSL_CHECK_STATUS(aksl_snprintf(buf, sizeof(buf), "%s", "12345678"), AKERR_OUTOFBOUNDS); - AKSL_CHECK(count == 0); return 0; } static int test_snprintf_rejects_null_arguments_and_zero_size(void) { char buf[8]; - int count = 0; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), - AKERR_NULLPOINTER, "count="); - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, 8, "x"), AKERR_NULLPOINTER, "str="); - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, sizeof(buf), NULL), AKERR_NULLPOINTER, "format="); /* size 0 leaves no room even for the terminator, so there is nothing to do. */ - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, 0, "x"), AKERR_VALUE, "size=0"); return 0; } @@ -274,14 +260,14 @@ static int test_asprintf_allocates_to_fit(void) * wanted them exposed so consumers can write their own variadic wrappers. This * is a consumer doing exactly that. */ -static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n, +static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(char *buf, size_t n, const char *fmt, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, fmt); - raised = aksl_vsnprintf(count, buf, n, fmt, args); + raised = aksl_vsnprintf(buf, n, fmt, args); va_end(args); return raised; } @@ -289,17 +275,14 @@ static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf static int test_va_list_forms_are_usable_from_outside(void) { char buf[32]; - int count = -1; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3)); - AKSL_CHECK(count == 5); + AKSL_CHECK_OK(consumer_wrapper(buf, sizeof(buf), "%s/%d", "via", 3)); AKSL_CHECK(strcmp(buf, "via/3") == 0); /* The error contract survives the extra layer intact. */ - AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"), + AKSL_CHECK_STATUS(consumer_wrapper(buf, 4, "%s", "too long"), AKERR_OUTOFBOUNDS); - AKSL_CHECK(count == 0); return 0; } @@ -312,14 +295,12 @@ static int test_va_list_forms_are_usable_from_outside(void) static int test_variadic_wrappers_survive_repeated_calls(void) { char buf[128]; - int count = 0; int i = 0; for ( i = 0; i < 512; i++ ) { - AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%d %s %ld %c %f", + AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%d %s %ld %c %f", i, "iteration", (long)i, 'x', (double)i)); - AKSL_CHECK(count > 0); - AKSL_CHECK((size_t)count == strlen(buf)); + AKSL_CHECK(strlen(buf) > 0); } return 0; } @@ -330,7 +311,7 @@ int main(void) akerr_init(); - AKSL_RUN(failures, test_snprintf_writes_text_and_count); + AKSL_RUN(failures, test_snprintf_writes_text); AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing); AKSL_RUN(failures, test_snprintf_truncation_is_an_error); AKSL_RUN(failures, test_snprintf_boundary_is_exact); diff --git a/tests/test_pool.c b/tests/test_pool.c index c22869c..84258fe 100644 --- a/tests/test_pool.c +++ b/tests/test_pool.c @@ -109,14 +109,13 @@ static int test_stream_wrappers_do_not_leak_slots(void) static int test_format_wrappers_do_not_leak_slots(void) { char buf[16]; - int count = 0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER); - AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER); - AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"), + AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "x")); + AKSL_CHECK_STATUS(aksl_snprintf(buf, 4, "%s", "far too long"), AKERR_OUTOFBOUNDS); AKSL_CHECK(aksl_slots_in_use() == 0); } @@ -266,7 +265,6 @@ static int test_traversal_failures_do_not_leak_slots(void) static int test_errors_name_their_origin_in_stdlib(void) { void *ptr = NULL; - int count = 0; char resolved[PATH_MAX]; uint32_t h = 0; aksl_ListNode node; @@ -295,7 +293,7 @@ static int test_errors_name_their_origin_in_stdlib(void) AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0); - AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_snprintf(NULL, 8, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0); /* Likewise, the ato* forms are calls into the strto* ones. */ -- 2.43.0 From acb47a0d56a5efdb3ccb7074012afca0a59338c7 Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 18:25:43 -0400 Subject: [PATCH 2/2] Preserve required snprintf length on truncation --- README.md | 2 +- UPGRADING.md | 4 +-- include/akstdlib.h | 13 +++++---- src/stdlib.c | 26 ++++++++++------- tests/negative/format_mismatch.c | 3 +- tests/test_format.c | 49 +++++++++++++++++++++----------- tests/test_pool.c | 7 +++-- 7 files changed, 64 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 258d77e..466919c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ that will surprise you: | `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. | | `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. | | `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. | -| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. | +| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success; `*count` receives the required length. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. | | `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. | | `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. | | `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. | diff --git a/UPGRADING.md b/UPGRADING.md index 58dba83..01250de 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -83,10 +83,10 @@ having visited nothing. /* before */ aksl_sprintf(&count, buf, "%s=%d", key, value); /* after */ -aksl_snprintf(buf, sizeof(buf), "%s=%d", key, value); +aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value); ``` -Truncation is `AKERR_OUTOFBOUNDS` rather than a short success. +Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` receives the required length. **`aksl_realpath` takes the destination's length.** diff --git a/include/akstdlib.h b/include/akstdlib.h index 1afe09a..496964a 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -445,7 +445,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v /** @name Formatted output * * Bounded output is checked for truncation and reports an error when the result - * does not fit. + * does not fit. `*count` receives the number of bytes written, or the complete + * output length when truncation occurs. * * There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an * error-handling wrapper around an unbounded write is the sharp edge this @@ -456,7 +457,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v /** * @brief printf(3) to stdout. - * @param[out] count Bytes written; 0 on failure. Required. + * @param[out] count Bytes written. Required. * @param[in] format printf format string. Required. Checked at compile time. * @throws AKERR_NULLPOINTER If count or format is NULL. * @throws AKERR_IO Or the errno the C library saw, if the write fails. @@ -466,7 +467,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict /** * @brief fprintf(3) to a stream. - * @param[out] count Bytes written; 0 on failure. Required. + * @param[out] count Bytes written. Required. * @param[in] stream Destination stream. Required. * @param[in] format printf format string. Required. Checked at compile time. * @throws AKERR_NULLPOINTER If any pointer is NULL. @@ -482,6 +483,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea * written, leaving the caller to notice by comparing that against the buffer * size -- the check this library exists to stop people forgetting. * + * @param[out] count Bytes written, or the required length on truncation. Required. * @param[out] str Destination buffer. Required. * @param[in] size Size of `str` including the terminator. Must be non-zero. * @param[in] format printf format string. Required. Checked at compile time. @@ -490,7 +492,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea * @throws AKERR_OUTOFBOUNDS If the output does not fit, naming both lengths. * @return NULL on success, an error context otherwise. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4); +akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(4, 5); /** * @brief Format into a freshly allocated string. @@ -551,6 +553,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre * @brief vsnprintf(3) into a bounded buffer. The va_list form of aksl_snprintf. * @param[out] str Destination buffer. Required. * @param[in] size Size of `str` including the terminator. Must be non-zero. + * @param[out] count Bytes written, or the required length on truncation. Required. * @param[in] format printf format string. Required. * @param[in] args Arguments. The caller owns it and must va_end it. * @throws AKERR_NULLPOINTER If any pointer is NULL. @@ -558,7 +561,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre * @throws AKERR_OUTOFBOUNDS If the output does not fit. * @return NULL on success, an error context otherwise. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args); +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args); /** @} */ diff --git a/src/stdlib.c b/src/stdlib.c index 259844b..0775dc4 100644 --- a/src/stdlib.c +++ b/src/stdlib.c @@ -418,6 +418,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream) * exactly what made it worth fixing before something did. * - errno is cleared before the call and read back through AKSL_ERRNO_OR, so a * failure can never be reported with a stale -- or with a zero -- status. + * - The bounded form returns the complete required length through *count even + * when truncation raises AKERR_OUTOFBOUNDS; callers use the error context for + * failure details, not the count as a success indicator. * * aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an * error-handling wrapper around an unbounded write is precisely the sharp edge @@ -482,30 +485,31 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea * common way a bounded write goes wrong unnoticed; a caller who wanted to know * would have had to compare the return against the buffer size by hand, which is * the check this library exists to stop people forgetting. The bounded wrapper - * reports truncation instead. + * reports truncation instead, and hands the required length back through *count. */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args) +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args) { - int needed = 0; PREPARE_ERROR(e); - FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format); - FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format); + FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); + *count = 0; + FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL"); errno = 0; - needed = vsnprintf(str, size, format, args); - FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error"); - FAIL_NONZERO_RETURN(e, ((size_t)needed >= size), AKERR_OUTOFBOUNDS, - "output truncated: %d bytes needed, %zu available", needed, size); + *count = vsnprintf(str, size, format, args); + FAIL_NONZERO_RETURN(e, (*count < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error"); + FAIL_NONZERO_RETURN(e, ((size_t)*count >= size), AKERR_OUTOFBOUNDS, + "output truncated: %d bytes needed, %zu available", *count, size); SUCCEED_RETURN(e); } -akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...) +akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); - raised = aksl_vsnprintf(str, size, format, args); + raised = aksl_vsnprintf(count, str, size, format, args); va_end(args); return raised; } diff --git a/tests/negative/format_mismatch.c b/tests/negative/format_mismatch.c index b244505..c9f554f 100644 --- a/tests/negative/format_mismatch.c +++ b/tests/negative/format_mismatch.c @@ -16,10 +16,11 @@ int main(void) { char buf[64]; + int count = 0; akerr_ErrorContext *raised = NULL; /* %d against a string. This is the line that must not build. */ - raised = aksl_snprintf(buf, sizeof(buf), "%d", "not an int"); + raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int"); return raised == NULL ? 0 : 1; } diff --git a/tests/test_format.c b/tests/test_format.c index a06b4fe..cf4ab25 100644 --- a/tests/test_format.c +++ b/tests/test_format.c @@ -40,9 +40,11 @@ static long read_file(const char *path, char *buf, size_t n) static int test_snprintf_writes_text(void) { char buf[64]; + int count = -1; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s=%d", "x", 7)); + AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7)); + AKSL_CHECK(count == 3); AKSL_CHECK(strcmp(buf, "x=7") == 0); return 0; } @@ -50,8 +52,10 @@ static int test_snprintf_writes_text(void) static int test_snprintf_empty_format_writes_nothing(void) { char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' }; + int count = -1; - AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", "")); + AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "")); + AKSL_CHECK(count == 0); AKSL_CHECK(buf[0] == '\0'); return 0; } @@ -65,11 +69,13 @@ static int test_snprintf_empty_format_writes_nothing(void) static int test_snprintf_truncation_is_an_error(void) { char buf[8]; + int count = -1; memset(buf, 0x00, sizeof(buf)); AKSL_CHECK_STATUS_MSG_CONTAINS( - aksl_snprintf(buf, sizeof(buf), "%s", "far too long for eight bytes"), + aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"), AKERR_OUTOFBOUNDS, "truncated"); + AKSL_CHECK(count == 28); AKSL_CHECK(strcmp(buf, "far too") == 0); return 0; } @@ -78,27 +84,31 @@ static int test_snprintf_truncation_is_an_error(void) static int test_snprintf_boundary_is_exact(void) { char buf[8]; + int count = -1; /* 7 characters plus the NUL is exactly sizeof(buf). */ - AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", "1234567")); + AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567")); + AKSL_CHECK(count == 7); AKSL_CHECK(strcmp(buf, "1234567") == 0); - AKSL_CHECK_STATUS(aksl_snprintf(buf, sizeof(buf), "%s", "12345678"), + AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"), AKERR_OUTOFBOUNDS); + AKSL_CHECK(count == 8); return 0; } static int test_snprintf_rejects_null_arguments_and_zero_size(void) { char buf[8]; + int count = 0; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, 8, "x"), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER, "str="); - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, sizeof(buf), NULL), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL), AKERR_NULLPOINTER, "format="); /* size 0 leaves no room even for the terminator, so there is nothing to do. */ - AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, 0, "x"), + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"), AKERR_VALUE, "size=0"); return 0; } @@ -260,29 +270,32 @@ static int test_asprintf_allocates_to_fit(void) * wanted them exposed so consumers can write their own variadic wrappers. This * is a consumer doing exactly that. */ -static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(char *buf, size_t n, +static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n, const char *fmt, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, fmt); - raised = aksl_vsnprintf(buf, n, fmt, args); + raised = aksl_vsnprintf(count, buf, n, fmt, args); va_end(args); return raised; } static int test_va_list_forms_are_usable_from_outside(void) { - char buf[32]; + char buf[32]; + int count = -1; memset(buf, 0x00, sizeof(buf)); - AKSL_CHECK_OK(consumer_wrapper(buf, sizeof(buf), "%s/%d", "via", 3)); + AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3)); + AKSL_CHECK(count == 5); AKSL_CHECK(strcmp(buf, "via/3") == 0); /* The error contract survives the extra layer intact. */ - AKSL_CHECK_STATUS(consumer_wrapper(buf, 4, "%s", "too long"), - AKERR_OUTOFBOUNDS); + AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"), + AKERR_OUTOFBOUNDS); + AKSL_CHECK(count == 8); return 0; } @@ -294,13 +307,15 @@ static int test_va_list_forms_are_usable_from_outside(void) */ static int test_variadic_wrappers_survive_repeated_calls(void) { - char buf[128]; + char buf[128]; + int count = 0; int i = 0; for ( i = 0; i < 512; i++ ) { - AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%d %s %ld %c %f", + AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%d %s %ld %c %f", i, "iteration", (long)i, 'x', (double)i)); - AKSL_CHECK(strlen(buf) > 0); + AKSL_CHECK(count > 0); + AKSL_CHECK((size_t)count == strlen(buf)); } return 0; } diff --git a/tests/test_pool.c b/tests/test_pool.c index 84258fe..7b6b36f 100644 --- a/tests/test_pool.c +++ b/tests/test_pool.c @@ -109,13 +109,14 @@ static int test_stream_wrappers_do_not_leak_slots(void) static int test_format_wrappers_do_not_leak_slots(void) { char buf[16]; + int count = 0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER); - AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "x")); - AKSL_CHECK_STATUS(aksl_snprintf(buf, 4, "%s", "far too long"), + AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "x")); + AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"), AKERR_OUTOFBOUNDS); AKSL_CHECK(aksl_slots_in_use() == 0); } @@ -293,7 +294,7 @@ static int test_errors_name_their_origin_in_stdlib(void) AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0); - AKSL_CHECK_STATUS(aksl_snprintf(NULL, 8, "x"), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_snprintf(NULL, NULL, 8, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0); /* Likewise, the ato* forms are calls into the strto* ones. */ -- 2.43.0