5 Commits
32 ... main

Author SHA1 Message Date
821200e618 Merge pull request 'Preserve required snprintf length on truncation' (#43) from 34 into main
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / sanitizers (push) Successful in 2m54s
libakstdlib CI Build / coverage (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 14m33s
Reviewed-on: #43
2026-08-03 18:31:01 -04:00
acb47a0d56 Preserve required snprintf length on truncation
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / sanitizers (push) Successful in 2m57s
libakstdlib CI Build / coverage (push) Successful in 2m46s
libakstdlib CI Build / mutation_test (push) Successful in 18m29s
2026-08-03 18:25:43 -04:00
4b22913c57 Merge pull request 'Add directory stream wrappers' (#40) from 10 into main
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 3m0s
libakstdlib CI Build / sanitizers (push) Successful in 2m59s
libakstdlib CI Build / coverage (push) Successful in 2m49s
libakstdlib CI Build / mutation_test (push) Successful in 15m43s
Reviewed-on: #40
2026-08-03 15:25:59 -04:00
b879a94119 Merge pull request 'Remove stale TODO section references' (#41) from 27 into main
Some checks failed
libakstdlib CI Build / cmake_build (push) Successful in 2m56s
libakstdlib CI Build / sanitizers (push) Successful in 2m53s
libakstdlib CI Build / mutation_test (push) Has been cancelled
libakstdlib CI Build / coverage (push) Has been cancelled
Reviewed-on: #41
2026-08-03 15:14:53 -04:00
a95ed6d13a Remove stale TODO section references
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / sanitizers (push) Successful in 2m52s
libakstdlib CI Build / coverage (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 12m9s
2026-08-03 13:46:00 -04:00
10 changed files with 69 additions and 46 deletions

View File

@@ -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. |

View File

@@ -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.**

View File

@@ -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);
/** @} */

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -3,7 +3,7 @@
*
* Numeric conversion, complete. The cases that used to live in
* tests/test_convert_strict.c -- registered as a known failure because the ato*
* family had no error channel at all (2.1.5) -- are folded back in here now that
* family had no error channel at all -- are folded back in here now that
* they pass: non-numeric input, empty input, trailing junk and overflow are all
* errors rather than silent wrong answers.
*
@@ -54,7 +54,7 @@ static int test_atoi_rejects_null_arguments(void)
}
/*
* The whole reason 2.1.5 mattered. atoi("not a number") returned success and 0,
* The whole reason strict conversion matters. atoi("not a number") returned success and 0,
* so a caller could not tell a parse failure from a legitimate zero -- and
* akbasic had to write ~60 lines of its own strtoll/strtod wrapper to avoid
* turning four diagnosable BASIC errors into four wrong answers.

View File

@@ -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;
}

View File

@@ -4,8 +4,8 @@
* The two confirmed list defects are fixed, so the tests that used to live in
* tests/test_list_append_chain.c and tests/test_list_iterate_head.c are folded
* back in here: aksl_list_append builds the whole chain rather than truncating
* it at the midpoint (2.1.1), and aksl_list_iterate starts at the head rather
* than at whatever node Floyd's slow pointer happened to stop on (2.1.2).
* it at the midpoint, and aksl_list_iterate starts at the head rather than at
* whatever node Floyd's slow pointer happened to stop on.
*
* The chain assertions build their lists with aksl_list_append now, which is the
* point -- they could not, while append was the thing under suspicion.

View File

@@ -1,6 +1,5 @@
/*
* Memory wrappers, complete, plus the additions from
* section 3.1.
* Memory wrappers, complete.
*
* The three cases the wrapper plan left open are all pinned here: malloc(0) is AKERR_VALUE
* rather than whatever errno happened to hold when the platform's malloc(0)

View File

@@ -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. */