Wrap the strings, streams and collections the wishlist asked for

TODO.md section 3.1's high-priority list and section 3.6's data
structures. This is the surface akbasic went without: it makes 10 calls
into this library and 116 to raw libc, and 69 of those 116 are strlen,
strcmp, strncpy and strstr.

Three new translation units, because src/stdlib.c covering four times
what it did would stop being readable:

  src/string.c       lengths, bounded copy and concatenation, duplication,
                     comparison including the case-insensitive forms,
                     searching, the reentrant tokenisers, and a status
                     message that knows this library's own statuses as
                     well as errno's.
  src/stream.c       positioning, flushing and buffering, character and
                     line I/O, stream state, freopen/fdopen/tmpfile,
                     formatted input, and the file operations.
  src/collections.c  the list functions that were missing, a head/tail
                     container so append is O(1), a binary search tree,
                     FNV-1a, a fixed-capacity hash map and a growable
                     string buffer.

Two conventions run through all of it. The copying functions take the
destination size even where the libc function they are named for does
not, because strcpy(3) cannot be called safely without it, and
truncation is an error that writes nothing rather than a plausible
prefix -- this is the idiom akbasic writes out by hand at ten sites.
The searching functions treat "not found" as a successful answer of
NULL, because absent is an answer and raising on it would make every
caller handle a non-error.

The hash map is akbasic's src/symtab.c generalised: open-addressed with
linear probing over a caller-supplied slot array, keys copied into fixed
slots so the map owns them, tombstones on delete so a removal cannot cut
a probe chain, and a refusal rather than a resize when full.

Tests: 16 binaries, green under the normal and sanitizer builds. The
scanf wrappers take the number of conversions the caller expects, since
comparing scanf(3)'s return against that by hand is the check everyone
eventually forgets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 07:32:35 -04:00
parent 55eb0334c4
commit 98a0a8562d
13 changed files with 4923 additions and 13 deletions

448
tests/test_string.c Normal file
View File

@@ -0,0 +1,448 @@
/*
* String wrappers -- src/string.c, TODO.md section 3.1.
*
* The two contracts worth testing hardest are the ones that differ from libc:
* every copying function takes the destination size and treats truncation as an
* error that writes nothing, and every searching function treats "not found" as
* a successful answer of NULL.
*/
#include "aksl_capture.h"
#include <errno.h>
/* ---------------------------------------------------------------------- */
/* Length */
/* ---------------------------------------------------------------------- */
static int test_strlen_and_strnlen(void)
{
size_t n = 99;
AKSL_CHECK_OK(aksl_strlen("hello", &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_OK(aksl_strlen("", &n));
AKSL_CHECK(n == 0);
/* strnlen stops at maxlen whether or not it found a terminator. */
AKSL_CHECK_OK(aksl_strnlen("hello", 3, &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strnlen("hello", 99, &n));
AKSL_CHECK(n == 5);
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strlen("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen(NULL, 1, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strnlen("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Copying */
/* ---------------------------------------------------------------------- */
static int test_strcpy_copies_within_the_buffer(void)
{
char buf[8];
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "abc"));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Exactly filling the buffer, terminator included, is not truncation. */
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "1234567"));
AKSL_CHECK(strcmp(buf, "1234567") == 0);
return 0;
}
/*
* The whole point of taking dstsize. akbasic writes this check by hand at ten
* sites: a length test, then strncpy, then an explicit NUL.
*/
static int test_strcpy_truncation_is_an_error_and_writes_nothing(void)
{
char buf[8] = "previous";
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcpy(buf, sizeof(buf), "12345678"),
AKERR_OUTOFBOUNDS, "destination holds");
/*
* Empty, not a truncated prefix. A caller who ignores the status gets
* nothing, which is far easier to notice than a plausible-looking "1234567".
*/
AKSL_CHECK(buf[0] == '\0');
return 0;
}
/*
* strncpy(3) leaves the destination unterminated when the source is at least n
* bytes, and NUL-pads the whole remainder when it is shorter. This does neither.
*/
static int test_strncpy_always_terminates_and_never_pads(void)
{
char buf[16];
memset(buf, 'Z', sizeof(buf));
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abcdef", 3));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* Not padded: everything past the terminator is untouched. */
AKSL_CHECK(buf[4] == 'Z');
/* n larger than the source just copies the source. */
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abc", 99));
AKSL_CHECK(strcmp(buf, "abc") == 0);
/* n bytes that do not fit the destination is still an error. */
AKSL_CHECK_STATUS(aksl_strncpy(buf, 4, "abcdef", 6), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
return 0;
}
static int test_strcat_appends_within_the_buffer(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), "cd"));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), ""));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "0123456789ab"),
AKERR_OUTOFBOUNDS, "in use plus");
/* The existing contents survive a refused append. */
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
/* An unterminated destination is refused rather than walked off the end of. */
static int test_strcat_refuses_an_unterminated_destination(void)
{
char buf[4];
memset(buf, 'x', sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "y"),
AKERR_VALUE, "not terminated");
return 0;
}
static int test_strncat_appends_at_most_n(void)
{
char buf[16] = "ab";
AKSL_CHECK_OK(aksl_strncat(buf, sizeof(buf), "cdef", 2));
AKSL_CHECK(strcmp(buf, "abcd") == 0);
AKSL_CHECK_STATUS(aksl_strncat(buf, 6, "xyz", 3), AKERR_OUTOFBOUNDS);
AKSL_CHECK(strcmp(buf, "abcd") == 0);
return 0;
}
static int test_copying_rejects_null_and_zero_size(void)
{
char buf[8] = "";
AKSL_CHECK_STATUS(aksl_strcpy(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncpy(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncpy(buf, 0, "x", 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strcat(NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strncat(NULL, 8, "x", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncat(buf, 0, "x", 1), AKERR_VALUE);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Duplication */
/* ---------------------------------------------------------------------- */
static int test_strdup_and_strndup(void)
{
char *copy = NULL;
AKSL_CHECK_OK(aksl_strdup("duplicate me", &copy));
AKSL_CHECK(copy != NULL);
AKSL_CHECK(strcmp(copy, "duplicate me") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK(copy == NULL);
AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, &copy));
AKSL_CHECK(strcmp(copy, "duplicate") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
/* n past the end of the string just copies the string. */
AKSL_CHECK_OK(aksl_strndup("ab", 99, &copy));
AKSL_CHECK(strcmp(copy, "ab") == 0);
AKSL_CHECK_OK(aksl_freep((void **)&copy));
AKSL_CHECK_STATUS(aksl_strdup(NULL, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, &copy), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strndup("x", 1, NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Comparison */
/* ---------------------------------------------------------------------- */
static int test_comparisons(void)
{
int r = 99;
AKSL_CHECK_OK(aksl_strcmp("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcmp("abc", "abd", &r));
AKSL_CHECK(r < 0);
AKSL_CHECK_OK(aksl_strcmp("abd", "abc", &r));
AKSL_CHECK(r > 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 3, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 4, &r));
AKSL_CHECK(r != 0);
/* The three case-folding sites akbasic writes out by hand. */
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "print", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "input", &r));
AKSL_CHECK(r != 0);
AKSL_CHECK_OK(aksl_strncasecmp("PRINTXX", "printYY", 5, &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_OK(aksl_strcoll("abc", "abc", &r));
AKSL_CHECK(r == 0);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasecmp("a", "b", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strncasecmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", NULL, &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcoll("a", "b", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Searching */
/* ---------------------------------------------------------------------- */
/*
* Every one of these treats absence as an answer. A library that raised on it
* would have every caller catching a non-error, and burning a pool slot to do
* so.
*/
static int test_searching_reports_absence_as_success(void)
{
const char *s = "the quick brown fox";
char *at = (char *)0x1;
size_t n = 99;
AKSL_CHECK_OK(aksl_strchr(s, 'q', &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strrchr(s, 'o', &at));
AKSL_CHECK(at == s + 17);
AKSL_CHECK_OK(aksl_strrchr(s, 'Z', &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strstr(s, "brown", &at));
AKSL_CHECK(at == s + 10);
AKSL_CHECK_OK(aksl_strstr(s, "purple", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strpbrk(s, "xq", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strpbrk(s, "ZY", &at));
AKSL_CHECK(at == NULL);
AKSL_CHECK_OK(aksl_strspn("aaabbb", "a", &n));
AKSL_CHECK(n == 3);
AKSL_CHECK_OK(aksl_strcspn("aaabbb", "b", &n));
AKSL_CHECK(n == 3);
return 0;
}
/* The open-coded case-insensitive search, including its edge cases. */
static int test_strcasestr(void)
{
const char *s = "The Quick Brown Fox";
char *at = (char *)0x1;
AKSL_CHECK_OK(aksl_strcasestr(s, "quick", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "QUICK", &at));
AKSL_CHECK(at == s + 4);
AKSL_CHECK_OK(aksl_strcasestr(s, "purple", &at));
AKSL_CHECK(at == NULL);
/* An empty needle matches at the start, as strstr(3) has it. */
AKSL_CHECK_OK(aksl_strcasestr(s, "", &at));
AKSL_CHECK(at == s);
/* A needle longer than the haystack cannot match, and must not read past. */
AKSL_CHECK_OK(aksl_strcasestr("ab", "abcdef", &at));
AKSL_CHECK(at == NULL);
/* A match right at the end. */
AKSL_CHECK_OK(aksl_strcasestr(s, "fox", &at));
AKSL_CHECK(at == s + 16);
return 0;
}
static int test_searching_rejects_null_arguments(void)
{
char *at = NULL;
size_t n = 0;
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strrchr("a", 'a', NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strstr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcasestr("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk(NULL, "a", &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strpbrk("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strspn("a", "a", NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn(NULL, "a", &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcspn("a", "a", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Tokenising */
/* ---------------------------------------------------------------------- */
static int test_strtok_r_walks_the_tokens(void)
{
char input[] = "one,two,,three";
char *save = NULL;
char *tok = NULL;
int seen = 0;
AKSL_CHECK_OK(aksl_strtok_r(input, ",", &save, &tok));
AKSL_CHECK(tok != NULL && strcmp(tok, "one") == 0);
seen++;
while ( 1 ) {
AKSL_CHECK_OK(aksl_strtok_r(NULL, ",", &save, &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
/* strtok_r collapses the empty field between the two commas. */
AKSL_CHECK(seen == 3);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, NULL, &save, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", &save, NULL), AKERR_NULLPOINTER);
return 0;
}
/* strsep keeps the empty fields, which is why it exists alongside strtok_r. */
static int test_strsep_keeps_empty_fields(void)
{
char input[] = "one,two,,three";
char *cursor = input;
char *tok = NULL;
int seen = 0;
while ( cursor != NULL ) {
AKSL_CHECK_OK(aksl_strsep(&cursor, ",", &tok));
if ( tok == NULL ) {
break;
}
seen++;
}
AKSL_CHECK(seen == 4);
AKSL_CHECK_STATUS(aksl_strsep(NULL, ",", &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, NULL, &tok), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strsep(&cursor, ",", NULL), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */
/* Status messages */
/* ---------------------------------------------------------------------- */
/*
* aksl_strerror knows this library's own statuses as well as errno values,
* which is the reason it is not a strerror_r wrapper: strerror_r could never
* name AKERR_NULLPOINTER.
*/
static int test_strerror_names_both_kinds_of_status(void)
{
char buf[128];
AKSL_CHECK_OK(aksl_strerror(ENOENT, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
AKSL_CHECK(strcmp(buf, "Unknown status 2") != 0);
AKSL_CHECK_OK(aksl_strerror(AKERR_NULLPOINTER, buf, sizeof(buf)));
AKSL_CHECK(buf[0] != '\0');
/* A status nothing has a name for is rendered as its number. */
AKSL_CHECK_OK(aksl_strerror(-98765, buf, sizeof(buf)));
AKSL_CHECK(strcmp(buf, "Unknown status -98765") == 0);
/* Too small a buffer is an error, and leaves nothing rather than a prefix. */
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 2), AKERR_OUTOFBOUNDS);
AKSL_CHECK(buf[0] == '\0');
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, NULL, 16), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 0), AKERR_VALUE);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_strlen_and_strnlen);
AKSL_RUN(failures, test_strcpy_copies_within_the_buffer);
AKSL_RUN(failures, test_strcpy_truncation_is_an_error_and_writes_nothing);
AKSL_RUN(failures, test_strncpy_always_terminates_and_never_pads);
AKSL_RUN(failures, test_strcat_appends_within_the_buffer);
AKSL_RUN(failures, test_strcat_refuses_an_unterminated_destination);
AKSL_RUN(failures, test_strncat_appends_at_most_n);
AKSL_RUN(failures, test_copying_rejects_null_and_zero_size);
AKSL_RUN(failures, test_strdup_and_strndup);
AKSL_RUN(failures, test_comparisons);
AKSL_RUN(failures, test_searching_reports_absence_as_success);
AKSL_RUN(failures, test_strcasestr);
AKSL_RUN(failures, test_searching_rejects_null_arguments);
AKSL_RUN(failures, test_strtok_r_walks_the_tokens);
AKSL_RUN(failures, test_strsep_keeps_empty_fields);
AKSL_RUN(failures, test_strerror_names_both_kinds_of_status);
AKSL_REPORT(failures);
}