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>
259 lines
8.5 KiB
C
259 lines
8.5 KiB
C
/*
|
|
* The growable string buffer -- src/collections.c, TODO.md 3.6.
|
|
*
|
|
* The bounded formatting wrappers are the right answer when the destination is
|
|
* a fixed buffer and no answer at all when the length is not known in advance.
|
|
* This is that answer, and the properties worth holding onto are that it always
|
|
* grows enough, always stays NUL-terminated, and always says so when it cannot.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
static int test_init_and_free(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
AKSL_CHECK(buf.data != NULL);
|
|
AKSL_CHECK(buf.length == 0);
|
|
/* A zero request is raised to the minimum rather than refused. */
|
|
AKSL_CHECK(buf.capacity >= 32);
|
|
/* Valid as a C string immediately, with no finalise step. */
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "") == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
AKSL_CHECK(buf.data == NULL);
|
|
AKSL_CHECK(buf.capacity == 0);
|
|
/* Freeing twice is an error rather than a double free, as aksl_freep arranges. */
|
|
AKSL_CHECK_STATUS(aksl_strbuf_free(&buf), AKERR_NULLPOINTER);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strbuf_init(NULL, 16), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_free(NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
static int test_append_concatenates(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "hello"));
|
|
AKSL_CHECK_OK(aksl_strbuf_append_char(&buf, ' '));
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "world"));
|
|
AKSL_CHECK(buf.length == 11);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "hello world") == 0);
|
|
|
|
/* Appending nothing is a no-op, not an error. */
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, ""));
|
|
AKSL_CHECK(buf.length == 11);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Growth is the whole point. Appending far past the initial capacity has to
|
|
* reallocate, and everything written before the move has to survive it.
|
|
*/
|
|
static int test_growth_preserves_the_contents(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
size_t initial = 0;
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 32));
|
|
initial = buf.capacity;
|
|
|
|
for ( i = 0; i < 1000; i++ ) {
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "0123456789"));
|
|
}
|
|
AKSL_CHECK(buf.length == 10000);
|
|
AKSL_CHECK(buf.capacity > initial);
|
|
AKSL_CHECK(buf.capacity >= buf.length + 1);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strlen(s) == 10000);
|
|
/* Both ends, so a botched realloc shows up wherever it happened. */
|
|
AKSL_CHECK(strncmp(s, "0123456789", 10) == 0);
|
|
AKSL_CHECK(strcmp(s + 9990, "0123456789") == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/* Bytes rather than a string, so an embedded NUL can be appended deliberately. */
|
|
static int test_append_bytes_keeps_embedded_nuls(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
AKSL_CHECK_OK(aksl_strbuf_append_bytes(&buf, "ab\0cd", 5));
|
|
/* length counts all five; the C string view stops at the first NUL. */
|
|
AKSL_CHECK(buf.length == 5);
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strlen(s) == 2);
|
|
AKSL_CHECK(memcmp(s, "ab\0cd", 5) == 0);
|
|
/* And the terminator is still there past the end. */
|
|
AKSL_CHECK(s[5] == '\0');
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Formatted append. vsnprintf is measured first and then written, so a long
|
|
* result grows the buffer rather than truncating -- which is the difference
|
|
* between this and aksl_snprintf into a fixed array.
|
|
*/
|
|
static int test_appendf_formats_and_grows(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 8));
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s=%d", "x", 7));
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "x=7") == 0);
|
|
|
|
/* Far longer than the 8 bytes it started with. */
|
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s", "and a good deal more text than that"));
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "x=7 and a good deal more text than that") == 0);
|
|
|
|
/* Repeated formatted appends, to exercise the measure/write pair often. */
|
|
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
|
|
for ( i = 0; i < 200; i++ ) {
|
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "[%03d]", i));
|
|
}
|
|
AKSL_CHECK(buf.length == 1000);
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strncmp(s, "[000][001][002]", 15) == 0);
|
|
AKSL_CHECK(strcmp(s + 995, "[199]") == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/* An empty format produces nothing and is not an error. */
|
|
static int test_appendf_with_no_output(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s", ""));
|
|
AKSL_CHECK(buf.length == 0);
|
|
AKSL_CHECK(buf.data[0] == '\0');
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/* reset empties without releasing, so the capacity is reused. */
|
|
static int test_reset_keeps_the_capacity(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
size_t grown = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "something reasonably long to force a grow"));
|
|
grown = buf.capacity;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
|
|
AKSL_CHECK(buf.length == 0);
|
|
AKSL_CHECK(buf.capacity == grown);
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "") == 0);
|
|
|
|
/* And it still works afterwards. */
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "again"));
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "again") == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Every entry point checks that the buffer was initialised, so a zeroed
|
|
* aksl_StrBuf on the stack is an error rather than a NULL dereference.
|
|
*/
|
|
static int test_rejects_null_and_uninitialised(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
aksl_StrBuf zeroed;
|
|
const char *s = NULL;
|
|
|
|
memset((void *)&zeroed, 0x00, sizeof(zeroed));
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append(NULL, "x"), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append(&buf, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(NULL, "x", 1), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(&buf, NULL, 1), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append_char(NULL, 'x'), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(NULL, "x"), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&buf, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_reset(NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(NULL, &s), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&buf, NULL), AKERR_NULLPOINTER);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strbuf_append(&zeroed, "x"), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&zeroed, "x"), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_reset(&zeroed), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&zeroed, &s), AKERR_NULLPOINTER);
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The reason this type exists, written the way a caller would: build a report of
|
|
* unknown length out of pieces, without anyone having to size a buffer first.
|
|
*/
|
|
static int test_builds_a_report(void)
|
|
{
|
|
aksl_StrBuf buf;
|
|
const char *s = NULL;
|
|
static const char *names[3] = { "alpha", "beta", "gamma" };
|
|
int i = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "items:"));
|
|
for ( i = 0; i < 3; i++ ) {
|
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s(%d)", names[i], i));
|
|
}
|
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
|
AKSL_CHECK(strcmp(s, "items: alpha(0) beta(1) gamma(2)") == 0);
|
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_init_and_free);
|
|
AKSL_RUN(failures, test_append_concatenates);
|
|
AKSL_RUN(failures, test_growth_preserves_the_contents);
|
|
AKSL_RUN(failures, test_append_bytes_keeps_embedded_nuls);
|
|
AKSL_RUN(failures, test_appendf_formats_and_grows);
|
|
AKSL_RUN(failures, test_appendf_with_no_output);
|
|
AKSL_RUN(failures, test_reset_keeps_the_capacity);
|
|
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
|
|
AKSL_RUN(failures, test_builds_a_report);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|