diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ddce08..cdd9e0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,8 +189,15 @@ set(libdir "\${exec_prefix}/lib") set(includedir "\${prefix}/include") configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY) +# One translation unit per surface. src/stdlib.c was the whole library when it +# covered 16 libc calls; it is split by domain now that it does not, so that a +# file can still be read start to finish. src/aksl_internal.h carries what they +# share and is not installed. add_library(akstdlib SHARED src/stdlib.c + src/string.c + src/stream.c + src/collections.c ) add_library(akstdlib::akstdlib ALIAS akstdlib) @@ -288,14 +295,19 @@ install(FILES # "unexpectedly passed" -- that is the cue to move # it up into AKSL_TESTS. set(AKSL_TESTS + collections convert format + hashmap linkedlist memory path status_registry + strbuf stream + streamio strhash + string strto tree version diff --git a/include/akstdlib.h b/include/akstdlib.h index 6c579aa..1be20ac 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -44,6 +44,8 @@ #include #include #include +/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ +#include #ifdef __cplusplus extern "C" { @@ -57,8 +59,11 @@ extern "C" { #if defined(__GNUC__) || defined(__clang__) #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \ __attribute__((format(printf, __fmt_index, __first_arg))) +#define AKSL_SCANF_FORMAT(__fmt_index, __first_arg) \ + __attribute__((format(scanf, __fmt_index, __first_arg))) #else #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) +#define AKSL_SCANF_FORMAT(__fmt_index, __first_arg) #endif typedef struct aksl_ListNode { @@ -96,10 +101,26 @@ typedef struct aksl_TreeNode { */ #define AKSL_TREE_MAX_DEPTH 256 +/* + * Head/tail/length container. aksl_list_append has to walk the list to find its + * tail, so building n nodes with it is O(n^2); the container makes that O(1) and + * makes the length free. It owns no memory -- the nodes are still the caller's. + */ +typedef struct aksl_List { + aksl_ListNode *head; + aksl_ListNode *tail; + size_t length; +} aksl_List; + typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodeIterator)(aksl_ListNode *node, void *data); typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *node, void *data); typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest); typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr); +/* Reports its answer through *matched (non-zero to accept) and may raise. */ +typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodePredicate)(aksl_ListNode *node, void *data, int *matched); +/* Negative, zero or positive through *dest, as strcmp(3) has it. */ +typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeCompareFunc)(void *a, void *b, int *dest); +typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_HashMapIterator)(const char *key, void *value, void *data); /* * Version of the shared library actually loaded, as opposed to the @@ -232,6 +253,128 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval); akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval); +/* ---------------------------------------------------------------------- */ +/* Strings -- src/string.c */ +/* ---------------------------------------------------------------------- */ + +/* + * Two conventions run through this whole section. + * + * The copying and concatenating functions take the size of the destination + * buffer, even the ones named after libc functions that do not. strcpy(3) and + * strcat(3) cannot be called safely without knowing how much room there is, so + * a wrapper that took the same arguments would be an error-handling wrapper + * around a buffer overflow. dstsize is the whole buffer including the + * terminator -- pair `char buf[64]` with `sizeof(buf)`. Truncation is + * AKERR_OUTOFBOUNDS, and nothing is written when it happens, so ignoring the + * status leaves an empty string rather than a plausible-looking prefix. + * + * The searching functions answer through an out-param, and finding nothing is a + * successful answer of NULL rather than an error. The result points into the + * caller's own string and must not be freed. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src); +/* Always terminates, and never NUL-pads to n; strncpy(3) does both the other way. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n); + +/* *dest is the caller's, to release with aksl_free or aksl_freep. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest); + +/* + * The reentrant tokeniser only. strtok(3) keeps its state in a hidden static, + * so two interleaved tokenisations corrupt each other silently; it is not + * wrapped. Running out of tokens is success with *dest NULL. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest); + +/* + * The message for a status, into the caller's buffer. Knows this library's own + * statuses as well as errno values, which strerror_r(3) could not. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen); + +/* ---------------------------------------------------------------------- */ +/* Streams -- src/stream.c */ +/* ---------------------------------------------------------------------- */ + +/* + * Where a function has a genuine "nothing more to read" outcome, that is + * AKERR_EOF rather than AKERR_IO, so a read loop can tell the end of its input + * from the failure of it. That applies to aksl_fgetc, aksl_fgets, aksl_getline, + * aksl_getdelim and aksl_fscanf. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence); +akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence); +akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos); + +/* NULL stream means "every output stream", as in fflush(3); it is not an error. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out); +akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim, FILE *stream, size_t *len_out); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode, FILE *stream, FILE **fp); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp); + +/* + * The scanf family takes the number of conversions the caller expects, because + * comparing scanf(3)'s return against that number by hand at every call site is + * the check everyone eventually forgets -- and forgetting it leaves the + * unassigned arguments holding whatever they held before. Anything short of + * `expected` is AKERR_VALUE. Pass 0 to opt out and read *assigned yourself. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5); +akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5); +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format, int expected, int *assigned, va_list args); +akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format, int expected, int *assigned, va_list args); + +akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname); +akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath); +/* + * The template is rewritten in place, so it must be a writable buffer ending in + * six literal X characters -- a string literal is a segfault, and that is + * AKERR_VALUE here rather than a crash. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd); +akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_); + // Linked list functions akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data); akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj); @@ -247,6 +390,114 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_L akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void *leaf); akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data); +/* ---------------------------------------------------------------------- */ +/* Collections -- src/collections.c */ +/* ---------------------------------------------------------------------- */ + +/* + * Nothing in this section allocates unless its name says so. The list and tree + * functions relink nodes the caller already owns, and the *_free_all forms take + * the free function to use -- pass NULL for aksl_free -- so a caller drawing + * from a fixed pool can hand over its own. Only aksl_strbuf_* owns memory. + * + * The bare-node list functions take the head by reference wherever the head + * itself can move, which is the case a caller doing it by hand gets wrong. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_prepend(aksl_ListNode **head, aksl_ListNode *obj); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_after(aksl_ListNode *node, aksl_ListNode *obj); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_before(aksl_ListNode **head, aksl_ListNode *node, aksl_ListNode *obj); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_length(aksl_ListNode *head, size_t *dest); +/* NULL and success when nothing matches, as everywhere else in this library. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_find(aksl_ListNode *head, aksl_ListNodePredicate pred, void *data, aksl_ListNode **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_reverse(aksl_ListNode **head); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_concat(aksl_ListNode *head, aksl_ListNode *other); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_free_all(aksl_ListNode **head, aksl_FreeFunc lfree); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail, aksl_ListNodeIterator iter, void *data); + +/* The tracked container. push/unshift are O(1); length is a field, not a walk. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_init(aksl_List *list); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_List *list, aksl_ListNode *obj); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_unshift(aksl_List *list, aksl_ListNode *obj); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_remove(aksl_List *list, aksl_ListNode *node); +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFunc lfree); + +/* + * An unbalanced binary search tree. Inserting already-sorted data gives a + * degenerate chain, which the traversal then refuses past AKSL_TREE_MAX_DEPTH -- + * bounded rather than dangerous, but this is not a balanced tree and does not + * claim to be. These are the functions that set and read aksl_TreeNode.parent. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_insert(aksl_TreeNode **root, aksl_TreeNode *node, aksl_TreeCompareFunc cmp); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_find(aksl_TreeNode *root, void *leaf, aksl_TreeCompareFunc cmp, aksl_TreeNode **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_remove(aksl_TreeNode **root, aksl_TreeNode *node); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_height(aksl_TreeNode *root, int *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_count(aksl_TreeNode *root, size_t *dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl_FreeFunc lfree); + +/* + * FNV-1a alongside djb2. It XORs then multiplies rather than multiplying then + * adding, which mixes the low bits better on short keys that share a prefix -- + * which is what identifiers in a symbol table look like. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval); + +/* + * Fixed-capacity string-keyed hash map: open addressing, linear probing, the + * caller's slot array, keys copied into the slots so the map owns them. + * + * It refuses rather than resizes when full, deliberately. A table that + * reallocates is a table whose entry pointers move underneath anything holding + * one; a table that cannot grow has a worst case you can state. The cost is that + * the caller sizes it, which is why init takes the array rather than making one. + */ +#define AKSL_HASHMAP_MAX_KEY 64 +#define AKSL_HASHMAP_SLOT_EMPTY 0 /** Never used; ends a probe chain */ +#define AKSL_HASHMAP_SLOT_OCCUPIED 1 /** Holds a live key and value */ +#define AKSL_HASHMAP_SLOT_DELETED 2 /** Tombstone; reusable, but does not end a chain */ + +typedef struct aksl_HashEntry { + char key[AKSL_HASHMAP_MAX_KEY]; + void *value; + uint8_t state; +} aksl_HashEntry; + +typedef struct aksl_HashMap { + aksl_HashEntry *slots; + size_t capacity; + size_t count; +} aksl_HashMap; + +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_init(aksl_HashMap *map, aksl_HashEntry *slots, size_t capacity); +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_put(aksl_HashMap *map, const char *key, void *value); +/* A missing key is *found = 0 and success; looking and not finding is not an error. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_get(aksl_HashMap *map, const char *key, void **value, int *found); +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_remove(aksl_HashMap *map, const char *key, int *removed); +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map, aksl_HashMapIterator iter, void *data); + +/* + * Growable string buffer -- the one thing here that owns memory, which is what + * makes the bounded formatting wrappers usable when the output length is not + * known in advance. Capacity doubles, so n appends cost O(n) amortised, and the + * contents are always NUL-terminated so aksl_strbuf_cstr is valid at any point. + */ +typedef struct aksl_StrBuf { + char *data; + size_t length; /** Bytes in use, terminator not counted */ + size_t capacity; /** Bytes allocated, terminator included */ +} aksl_StrBuf; + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_init(aksl_StrBuf *buf, size_t initial); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append(aksl_StrBuf *buf, const char *s); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_bytes(aksl_StrBuf *buf, const char *s, size_t n); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_char(aksl_StrBuf *buf, char c); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_appendf(aksl_StrBuf *buf, const char *format, ...) AKSL_PRINTF_FORMAT(2, 3); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_vappendf(aksl_StrBuf *buf, const char *format, va_list args); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_reset(aksl_StrBuf *buf); +/* Points into the buffer; the next append invalidates it. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_cstr(aksl_StrBuf *buf, const char **dest); +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_free(aksl_StrBuf *buf); + #ifdef __cplusplus } #endif diff --git a/src/aksl_internal.h b/src/aksl_internal.h new file mode 100644 index 0000000..1937c7e --- /dev/null +++ b/src/aksl_internal.h @@ -0,0 +1,25 @@ +#ifndef _AKSL_INTERNAL_H_ +#define _AKSL_INTERNAL_H_ + +/* + * Shared internals. Not installed, not part of the public API -- anything here + * is free to change without so much as a patch bump. + */ + +#include + +/* + * errno as an akerror status, with a fallback for when it is not one. + * + * A libc call is allowed to fail without touching errno -- malloc(0) may return + * NULL and leave it alone -- and errno may equally be a leftover from some + * earlier, unrelated, *successful* call. Reporting it raw produces a FAIL whose + * status is 0, which every downstream DETECT and CATCH reads as success while + * the context still holds a pool slot: an error that is invisible and leaks at + * the same time. Every errno-sourced status in this library goes through here, + * and every wrapped call clears errno first so the value read back is its own. + * TODO.md 2.2.1. + */ +#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback)) + +#endif // _AKSL_INTERNAL_H_ diff --git a/src/collections.c b/src/collections.c new file mode 100644 index 0000000..2e2ac52 --- /dev/null +++ b/src/collections.c @@ -0,0 +1,1130 @@ +/* + * Data structures -- TODO.md section 3.6. + * + * Not libc wrappers. These are the parts of the list and tree API that were + * visibly missing, plus the two structures the first real consumer had to write + * for itself: a string-keyed hash table (akbasic's src/symtab.c, ~130 lines, + * needed three times over for variables, functions and labels) and a growable + * string buffer, without which the bounded formatting wrappers are unpleasant to + * use for anything of unknown length. + * + * Two things hold throughout. + * + * Nothing here allocates unless its name says so. The list and tree functions + * relink nodes the caller already owns; aksl_list_free_all and + * aksl_tree_free_all take the free function to use, so a caller drawing from a + * fixed pool can hand over its own. Only aksl_strbuf_* owns memory, and it says + * so in the type name. + * + * The hash map is fixed-capacity and refuses rather than resizes when full, + * which is the same decision akbasic made and for the same reason: a table that + * silently reallocates is a table whose pointers move underneath you, and one + * that cannot grow is one whose worst case you can state. + */ + +#include + +#include +#include +#include +#include +#include + +#include "aksl_internal.h" + +/* ====================================================================== */ +/* Linked list */ +/* ====================================================================== */ + +/* + * Walk to the tail, refusing a cyclic list. Shared by the functions below that + * need the end; the bound is Floyd's, exactly as aksl_list_append does it, so + * every whole-list walk in this library fails the same way on the same input. + */ +static akerr_ErrorContext AKERR_NOIGNORE *list_tail(aksl_ListNode *head, aksl_ListNode **dest) +{ + aksl_ListNode *slow = head; + aksl_ListNode *fast = head; + aksl_ListNode *tail = head; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head"); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); + *dest = NULL; + while ( fast != NULL && fast->next != NULL ) { + slow = slow->next; + fast = fast->next->next; + if ( fast == slow ) { + FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)head); + } + } + while ( tail->next != NULL ) { + tail = tail->next; + } + *dest = tail; + SUCCEED_RETURN(e); +} + +/* + * obj becomes the new head. Takes the head by reference for the same reason + * aksl_list_pop does: the caller's own pointer has to move, and there is no way + * to do that from a node pointer alone. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_prepend(aksl_ListNode **head, aksl_ListNode *obj) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, obj=%p", (void *)head, (void *)obj); + FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "head=%p, obj=%p", (void *)head, (void *)obj); + FAIL_NONZERO_RETURN(e, (*head == obj), AKERR_VALUE, + "obj %p is already the head of this list", (void *)obj); + obj->prev = NULL; + obj->next = *head; + if ( *head != NULL ) { + (*head)->prev = obj; + } + *head = obj; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_after(aksl_ListNode *node, aksl_ListNode *obj) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node=%p, obj=%p", (void *)node, (void *)obj); + FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "node=%p, obj=%p", (void *)node, (void *)obj); + FAIL_NONZERO_RETURN(e, (node == obj), AKERR_VALUE, + "cannot insert node %p after itself", (void *)obj); + obj->prev = node; + obj->next = node->next; + if ( node->next != NULL ) { + node->next->prev = obj; + } + node->next = obj; + SUCCEED_RETURN(e); +} + +/* + * head is required because inserting before the current head moves it, which is + * the case a caller is most likely to get wrong by hand. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_before(aksl_ListNode **head, + aksl_ListNode *node, + aksl_ListNode *obj) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p", + (void *)head, (void *)node, (void *)obj); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p", + (void *)head, (void *)node, (void *)obj); + FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "head=%p, node=%p, obj=%p", + (void *)head, (void *)node, (void *)obj); + FAIL_NONZERO_RETURN(e, (node == obj), AKERR_VALUE, + "cannot insert node %p before itself", (void *)obj); + obj->next = node; + obj->prev = node->prev; + if ( node->prev != NULL ) { + node->prev->next = obj; + } + node->prev = obj; + if ( *head == node ) { + *head = obj; + } + SUCCEED_RETURN(e); +} + +/* A NULL head is an empty list of length 0, not an error. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_length(aksl_ListNode *head, size_t *dest) +{ + aksl_ListNode *slow = head; + aksl_ListNode *fast = head; + aksl_ListNode *walk = head; + size_t n = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); + *dest = 0; + while ( fast != NULL && fast->next != NULL ) { + slow = slow->next; + fast = fast->next->next; + if ( fast == slow ) { + FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)head); + } + } + while ( walk != NULL ) { + n++; + walk = walk->next; + } + *dest = n; + SUCCEED_RETURN(e); +} + +/* + * The first node the predicate accepts, or NULL if none does -- absent is an + * answer, as everywhere else in this library. The predicate reports through an + * out-param and may raise, in which case the search stops and the error + * propagates with its message intact. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_find(aksl_ListNode *head, + aksl_ListNodePredicate pred, + void *data, + aksl_ListNode **dest) +{ + aksl_ListNode *walk = head; + aksl_ListNode *tail = NULL; + int matched = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, pred, AKERR_NULLPOINTER, "pred=%p, dest=%p", (void *)pred, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pred=%p, dest=%p", (void *)pred, (void *)dest); + *dest = NULL; + /* Bound the walk before making it, so a cyclic list cannot spin forever. */ + if ( head != NULL ) { + PASS(e, list_tail(head, &tail)); + } + while ( walk != NULL ) { + PASS(e, pred(walk, data, &matched)); + if ( matched != 0 ) { + *dest = walk; + break; + } + walk = walk->next; + } + SUCCEED_RETURN(e); +} + +/* Reverses in place, swapping every node's links and moving the caller's head. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_reverse(aksl_ListNode **head) +{ + aksl_ListNode *walk = NULL; + aksl_ListNode *prev = NULL; + aksl_ListNode *next = NULL; + aksl_ListNode *tail = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head"); + if ( *head == NULL ) { + SUCCEED_RETURN(e); + } + PASS(e, list_tail(*head, &tail)); + walk = *head; + while ( walk != NULL ) { + next = walk->next; + walk->next = prev; + walk->prev = next; + prev = walk; + walk = next; + } + *head = prev; + SUCCEED_RETURN(e); +} + +/* + * Links `other` onto the end of `head`. Both must be acyclic and disjoint -- + * concatenating a list with itself, or with something already inside it, would + * make a cycle, so the tail walk that finds the join point also refuses it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_concat(aksl_ListNode *head, aksl_ListNode *other) +{ + aksl_ListNode *tail = NULL; + aksl_ListNode *walk = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, other=%p", (void *)head, (void *)other); + if ( other == NULL ) { + SUCCEED_RETURN(e); + } + FAIL_NONZERO_RETURN(e, (head == other), AKERR_VALUE, + "cannot concatenate a list with itself"); + PASS(e, list_tail(head, &tail)); + for ( walk = head; walk != NULL; walk = walk->next ) { + FAIL_NONZERO_RETURN(e, (walk == other), AKERR_VALUE, + "node %p is already in the destination list", (void *)other); + } + tail->next = other; + other->prev = tail; + SUCCEED_RETURN(e); +} + +/* + * Releases every node through lfree, or through aksl_free when it is NULL, and + * clears the caller's head. The next pointer is read before the node goes, which + * is the whole trick; freeing forwards without that reads freed memory on the + * very next iteration. + * + * A failure part-way through does not abandon the rest of the list: the first + * error is kept and returned once the walk is finished, so a bad free cannot + * turn into a leak of everything after it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_free_all(aksl_ListNode **head, aksl_FreeFunc lfree) +{ + aksl_ListNode *walk = NULL; + aksl_ListNode *next = NULL; + aksl_ListNode *tail = NULL; + akerr_ErrorContext *first = NULL; + akerr_ErrorContext *raised = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head"); + if ( lfree == NULL ) { + lfree = &aksl_free; + } + if ( *head == NULL ) { + SUCCEED_RETURN(e); + } + PASS(e, list_tail(*head, &tail)); + walk = *head; + *head = NULL; + while ( walk != NULL ) { + next = walk->next; + raised = lfree(walk); + if ( raised != NULL && first == NULL ) { + first = raised; + } else if ( raised != NULL ) { + raised = akerr_release_error(raised); + } + walk = next; + } + if ( first != NULL ) { + return first; + } + SUCCEED_RETURN(e); +} + +/* + * Iterates from `tail` back towards the head. Given a tail rather than a head + * because that is what the caller has after a walk forwards, and because a + * doubly-linked list that cannot be read backwards is just a linked list. + * AKERR_ITERATOR_BREAK stops it, as it does going the other way. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail, + aksl_ListNodeIterator iter, + void *data) +{ + aksl_ListNode *slow = tail; + aksl_ListNode *fast = tail; + aksl_ListNode *node = tail; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, tail, AKERR_NULLPOINTER, "tail=%p, iter=%p", (void *)tail, (void *)iter); + FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "tail=%p, iter=%p", (void *)tail, (void *)iter); + while ( fast != NULL && fast->prev != NULL ) { + slow = slow->prev; + fast = fast->prev->prev; + if ( fast == slow ) { + FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)tail); + } + } + while ( node != NULL ) { + ATTEMPT { + CATCH(e, iter(node, data)); + node = node->prev; + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_ITERATOR_BREAK) { + // Not an error condition; it is telling us to stop early + SUCCEED_RETURN(e); + } FINISH(e, true); + } + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* The tracked list container */ +/* ---------------------------------------------------------------------- */ + +/* + * aksl_list_append has to walk the whole list to find the tail, so building a + * list of n nodes with it is O(n^2). That is fine for the handful of nodes the + * bare-node API was written for and wrong for anything larger, which is what + * TODO.md 3.6 means by "a head/tail-tracking container type so append is O(1)". + * + * The container holds the length as well, so aksl_list_length stops being a + * walk. It owns no memory -- the nodes are still the caller's -- so there is no + * aksl_list_destroy; use aksl_list_clear if the nodes need releasing. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_init(aksl_List *list) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list"); + list->head = NULL; + list->tail = NULL; + list->length = 0; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_List *list, aksl_ListNode *obj) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj); + FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj); + obj->next = NULL; + obj->prev = list->tail; + if ( list->tail != NULL ) { + list->tail->next = obj; + } else { + list->head = obj; + } + list->tail = obj; + list->length += 1; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_unshift(aksl_List *list, aksl_ListNode *obj) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj); + FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "list=%p, obj=%p", (void *)list, (void *)obj); + obj->prev = NULL; + obj->next = list->head; + if ( list->head != NULL ) { + list->head->prev = obj; + } else { + list->tail = obj; + } + list->head = obj; + list->length += 1; + SUCCEED_RETURN(e); +} + +/* + * Unlinks a node the caller already holds. It must be in this list -- the + * container's length and endpoints would otherwise silently stop describing it + * -- so removing a node that is not is AKERR_VALUE rather than quiet corruption. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_remove(aksl_List *list, aksl_ListNode *node) +{ + aksl_ListNode *walk = NULL; + int found = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list=%p, node=%p", (void *)list, (void *)node); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "list=%p, node=%p", (void *)list, (void *)node); + for ( walk = list->head; walk != NULL; walk = walk->next ) { + if ( walk == node ) { + found = 1; + break; + } + } + FAIL_ZERO_RETURN(e, found, AKERR_VALUE, "node %p is not in this list", (void *)node); + if ( node->prev != NULL ) { + node->prev->next = node->next; + } else { + list->head = node->next; + } + if ( node->next != NULL ) { + node->next->prev = node->prev; + } else { + list->tail = node->prev; + } + node->next = NULL; + node->prev = NULL; + list->length -= 1; + SUCCEED_RETURN(e); +} + +/* Empties the container, releasing every node through lfree (aksl_free if NULL). */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFunc lfree) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list"); + ATTEMPT { + CATCH(e, aksl_list_free_all(&list->head, lfree)); + } CLEANUP { + /* The container is emptied whether or not every node released cleanly; + * leaving it describing nodes that are half gone would be worse. */ + list->head = NULL; + list->tail = NULL; + list->length = 0; + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +/* ====================================================================== */ +/* Binary search tree */ +/* ====================================================================== */ + +/* + * An ordered tree needs an ordering, so these take a comparator. It reports + * through an out-param and may raise, like every other callback here. + * + * These are the functions that set and read aksl_TreeNode.parent, which was + * declared and then never touched by anything in the library (TODO.md 2.2.15). + * aksl_tree_remove needs it: relinking a node's replacement means telling that + * node's parent about it, and finding the parent by walking from the root again + * would turn a removal into a second search. + * + * This is a plain unbalanced BST. Inserting already-sorted data gives a + * degenerate chain, which aksl_tree_iterate then refuses past + * AKSL_TREE_MAX_DEPTH -- so it is bounded rather than dangerous, but it is not + * a balanced tree and does not pretend to be. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_insert(aksl_TreeNode **root, + aksl_TreeNode *node, + aksl_TreeCompareFunc cmp) +{ + aksl_TreeNode *walk = NULL; + int order = 0; + int depth = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p", + (void *)root, (void *)node, (void *)cmp); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p", + (void *)root, (void *)node, (void *)cmp); + FAIL_ZERO_RETURN(e, cmp, AKERR_NULLPOINTER, "root=%p, node=%p, cmp=%p", + (void *)root, (void *)node, (void *)cmp); + node->left = NULL; + node->right = NULL; + node->parent = NULL; + if ( *root == NULL ) { + *root = node; + SUCCEED_RETURN(e); + } + walk = *root; + while ( 1 ) { + FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, + "tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH); + depth++; + PASS(e, cmp(node->leaf, walk->leaf, &order)); + if ( order < 0 ) { + if ( walk->left == NULL ) { + walk->left = node; + node->parent = walk; + break; + } + walk = walk->left; + } else { + /* + * Equal keys go right, so insertion order is preserved among them + * and a duplicate is stored rather than refused. A caller who wants + * uniqueness checks with aksl_tree_find first. + */ + if ( walk->right == NULL ) { + walk->right = node; + node->parent = walk; + break; + } + walk = walk->right; + } + } + SUCCEED_RETURN(e); +} + +/* NULL and success when the key is not present. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_find(aksl_TreeNode *root, void *leaf, + aksl_TreeCompareFunc cmp, + aksl_TreeNode **dest) +{ + aksl_TreeNode *walk = root; + int order = 0; + int depth = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, cmp, AKERR_NULLPOINTER, "cmp=%p, dest=%p", (void *)cmp, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "cmp=%p, dest=%p", (void *)cmp, (void *)dest); + *dest = NULL; + while ( walk != NULL ) { + FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, + "tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH); + depth++; + PASS(e, cmp(leaf, walk->leaf, &order)); + if ( order == 0 ) { + *dest = walk; + break; + } + walk = (order < 0) ? walk->left : walk->right; + } + SUCCEED_RETURN(e); +} + +/* Attach `child` (which may be NULL) where `node` used to hang off its parent. */ +static void tree_replace(aksl_TreeNode **root, aksl_TreeNode *node, aksl_TreeNode *child) +{ + if ( node->parent == NULL ) { + *root = child; + } else if ( node->parent->left == node ) { + node->parent->left = child; + } else { + node->parent->right = child; + } + if ( child != NULL ) { + child->parent = node->parent; + } +} + +/* + * The textbook three cases: no children, one child, two children. The last is + * the interesting one -- the node's in-order successor (leftmost of the right + * subtree) takes its place, because that is the only value that keeps every + * ordering invariant on both sides. + * + * The removed node's own links are cleared, so it can go straight back into a + * pool or be inserted somewhere else without carrying stale pointers. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_remove(aksl_TreeNode **root, aksl_TreeNode *node) +{ + aksl_TreeNode *successor = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root=%p, node=%p", (void *)root, (void *)node); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "root=%p, node=%p", (void *)root, (void *)node); + FAIL_ZERO_RETURN(e, *root, AKERR_VALUE, "the tree is empty"); + + if ( node->left == NULL ) { + tree_replace(root, node, node->right); + } else if ( node->right == NULL ) { + tree_replace(root, node, node->left); + } else { + successor = node->right; + while ( successor->left != NULL ) { + successor = successor->left; + } + if ( successor->parent != node ) { + tree_replace(root, successor, successor->right); + successor->right = node->right; + successor->right->parent = successor; + } + tree_replace(root, node, successor); + successor->left = node->left; + successor->left->parent = successor; + } + node->parent = NULL; + node->left = NULL; + node->right = NULL; + SUCCEED_RETURN(e); +} + +/* Height in nodes: an empty tree is 0, a single node is 1. */ +static akerr_ErrorContext AKERR_NOIGNORE *tree_height(aksl_TreeNode *root, int depth, int *dest) +{ + int left = 0; + int right = 0; + PREPARE_ERROR(e); + FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, + "tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH); + if ( root == NULL ) { + *dest = 0; + SUCCEED_RETURN(e); + } + PASS(e, tree_height(root->left, depth + 1, &left)); + PASS(e, tree_height(root->right, depth + 1, &right)); + *dest = 1 + (left > right ? left : right); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_height(aksl_TreeNode *root, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); + *dest = 0; + return tree_height(root, 0, dest); +} + +static akerr_ErrorContext AKERR_NOIGNORE *tree_count(aksl_TreeNode *root, int depth, size_t *dest) +{ + size_t left = 0; + size_t right = 0; + PREPARE_ERROR(e); + FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, + "tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH); + if ( root == NULL ) { + *dest = 0; + SUCCEED_RETURN(e); + } + PASS(e, tree_count(root->left, depth + 1, &left)); + PASS(e, tree_count(root->right, depth + 1, &right)); + *dest = 1 + left + right; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_count(aksl_TreeNode *root, size_t *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); + *dest = 0; + return tree_count(root, 0, dest); +} + +/* + * Post-order, necessarily: a node's children have to be released before the + * node that points at them, and any other order reads freed memory to find the + * second subtree. The first failure is kept and returned at the end rather than + * abandoning the rest of the tree, as aksl_list_free_all does. + */ +static akerr_ErrorContext AKERR_NOIGNORE *tree_free_all(aksl_TreeNode *root, int depth, + aksl_FreeFunc lfree, + akerr_ErrorContext **first) +{ + aksl_TreeNode *left = NULL; + aksl_TreeNode *right = NULL; + akerr_ErrorContext *raised = NULL; + PREPARE_ERROR(e); + FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, + "tree deeper than AKSL_TREE_MAX_DEPTH (%d)", AKSL_TREE_MAX_DEPTH); + if ( root == NULL ) { + SUCCEED_RETURN(e); + } + left = root->left; + right = root->right; + PASS(e, tree_free_all(left, depth + 1, lfree, first)); + PASS(e, tree_free_all(right, depth + 1, lfree, first)); + raised = lfree(root); + if ( raised != NULL && *first == NULL ) { + *first = raised; + } else if ( raised != NULL ) { + raised = akerr_release_error(raised); + } + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl_FreeFunc lfree) +{ + akerr_ErrorContext *first = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root"); + if ( lfree == NULL ) { + lfree = &aksl_free; + } + ATTEMPT { + CATCH(e, tree_free_all(*root, 0, lfree, &first)); + } CLEANUP { + *root = NULL; + } PROCESS(e) { + } FINISH(e, true); + if ( first != NULL ) { + return first; + } + SUCCEED_RETURN(e); +} + +/* ====================================================================== */ +/* Hashes */ +/* ====================================================================== */ + +/* + * FNV-1a, the other half of TODO.md 3.6's hash request. It differs from djb2 in + * XOR-then-multiply rather than multiply-then-add, which mixes the low bits + * rather better -- worth having when the keys are short and share a prefix, + * which is exactly what identifiers in a symbol table look like. + * + * Bytes are read unsigned here for the same reason they are in djb2: a + * sign-extended byte would make the hash depend on whether plain char happens to + * be signed on the target. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval) +{ + const unsigned char *cursor = NULL; + uint32_t h = 2166136261u; /* FNV offset basis, 32-bit */ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); + FAIL_ZERO_RETURN(e, hashval, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); + cursor = (const unsigned char *)str; + while ( len-- ) { + h ^= (uint32_t)*cursor++; + h *= 16777619u; /* FNV prime, 32-bit */ + } + *hashval = h; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); + return aksl_strhash_fnv1a(str, strlen(str), hashval); +} + +/* ====================================================================== */ +/* Hash map */ +/* ====================================================================== */ + +/* + * Fixed-capacity, open-addressed, linear-probing, string-keyed. + * + * The shape is akbasic's src/symtab.c, which TODO.md 3.6 says is "worth lifting + * more or less verbatim": the caller supplies the slot array, the map refuses + * rather than resizes when full, and the keys are copied into fixed-size slots + * so the map owns them and a caller cannot outlive its own key strings. + * + * Refusing rather than resizing is the deliberate part. A table that reallocates + * is a table whose entry pointers move underneath anything holding one; a table + * that cannot grow has a worst case you can state and a failure you can see. The + * cost is that the caller has to size it, which is why aksl_hashmap_init takes + * the slot array rather than allocating one. + * + * Deletion leaves a tombstone rather than an empty slot, because clearing the + * slot outright would break the probe chain of anything that hashed past it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_init(aksl_HashMap *map, + aksl_HashEntry *slots, + size_t capacity) +{ + size_t i = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, slots=%p", (void *)map, (void *)slots); + FAIL_ZERO_RETURN(e, slots, AKERR_NULLPOINTER, "map=%p, slots=%p", (void *)map, (void *)slots); + FAIL_ZERO_RETURN(e, capacity, AKERR_VALUE, "capacity=0"); + for ( i = 0; i < capacity; i++ ) { + slots[i].state = AKSL_HASHMAP_SLOT_EMPTY; + slots[i].key[0] = '\0'; + slots[i].value = NULL; + } + map->slots = slots; + map->capacity = capacity; + map->count = 0; + SUCCEED_RETURN(e); +} + +/* + * Find the slot a key belongs in. + * + * *found is the index of the key if it is present. *insert is where it would go + * if it is not -- the first tombstone seen, or the empty slot the probe stopped + * at -- so a put that has to insert does not have to probe twice. Either may + * come back as capacity, meaning "no such thing". + */ +static akerr_ErrorContext AKERR_NOIGNORE *hashmap_probe(aksl_HashMap *map, const char *key, + size_t *found, size_t *insert) +{ + uint32_t h = 0; + size_t idx = 0; + size_t probed = 0; + PREPARE_ERROR(e); + *found = map->capacity; + *insert = map->capacity; + PASS(e, aksl_strhash_djb2_str(key, &h)); + idx = (size_t)h % map->capacity; + for ( probed = 0; probed < map->capacity; probed++ ) { + if ( map->slots[idx].state == AKSL_HASHMAP_SLOT_EMPTY ) { + if ( *insert == map->capacity ) { + *insert = idx; + } + /* An empty slot ends the probe chain: the key is not here. */ + break; + } + if ( map->slots[idx].state == AKSL_HASHMAP_SLOT_DELETED ) { + if ( *insert == map->capacity ) { + *insert = idx; + } + } else if ( strcmp(map->slots[idx].key, key) == 0 ) { + *found = idx; + break; + } + idx = (idx + 1) % map->capacity; + } + SUCCEED_RETURN(e); +} + +/* + * Inserts, or replaces the value of an existing key. A key longer than + * AKSL_HASHMAP_MAX_KEY-1 is AKERR_OUTOFBOUNDS rather than a truncated key that + * would collide with a different one sharing its prefix. A full map is + * AKERR_OUTOFBOUNDS too, naming the capacity it hit. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_put(aksl_HashMap *map, const char *key, void *value) +{ + size_t found = 0; + size_t insert = 0; + size_t keylen = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key); + FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key); + FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised"); + keylen = strlen(key); + FAIL_NONZERO_RETURN(e, (keylen >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS, + "key of %zu bytes exceeds AKSL_HASHMAP_MAX_KEY (%d)", + keylen, AKSL_HASHMAP_MAX_KEY); + PASS(e, hashmap_probe(map, key, &found, &insert)); + if ( found != map->capacity ) { + map->slots[found].value = value; + SUCCEED_RETURN(e); + } + FAIL_NONZERO_RETURN(e, (insert == map->capacity), AKERR_OUTOFBOUNDS, + "map is full at %zu entries; it does not resize", map->capacity); + memcpy(map->slots[insert].key, key, keylen + 1); + map->slots[insert].value = value; + map->slots[insert].state = AKSL_HASHMAP_SLOT_OCCUPIED; + map->count += 1; + SUCCEED_RETURN(e); +} + +/* + * A key that is not present is *found = 0 and success, not an error -- looking + * something up and not finding it is the ordinary case in a symbol table, and a + * caller should not have to catch an error to discover it. *value is written + * only when the key is there. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_get(aksl_HashMap *map, const char *key, + void **value, int *found) +{ + size_t at = 0; + size_t insert = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p", + (void *)map, (void *)key, (void *)found); + FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p", + (void *)map, (void *)key, (void *)found); + FAIL_ZERO_RETURN(e, found, AKERR_NULLPOINTER, "map=%p, key=%p, found=%p", + (void *)map, (void *)key, (void *)found); + FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised"); + *found = 0; + FAIL_NONZERO_RETURN(e, (strlen(key) >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS, + "key exceeds AKSL_HASHMAP_MAX_KEY (%d)", AKSL_HASHMAP_MAX_KEY); + PASS(e, hashmap_probe(map, key, &at, &insert)); + if ( at != map->capacity ) { + *found = 1; + if ( value != NULL ) { + *value = map->slots[at].value; + } + } + SUCCEED_RETURN(e); +} + +/* Removing a key that is not there is success with *removed = 0. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_remove(aksl_HashMap *map, const char *key, int *removed) +{ + size_t at = 0; + size_t insert = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key); + FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "map=%p, key=%p", (void *)map, (void *)key); + FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised"); + if ( removed != NULL ) { + *removed = 0; + } + FAIL_NONZERO_RETURN(e, (strlen(key) >= AKSL_HASHMAP_MAX_KEY), AKERR_OUTOFBOUNDS, + "key exceeds AKSL_HASHMAP_MAX_KEY (%d)", AKSL_HASHMAP_MAX_KEY); + PASS(e, hashmap_probe(map, key, &at, &insert)); + if ( at != map->capacity ) { + /* + * A tombstone, not an empty slot: emptying it would cut the probe chain + * of every key that hashed to an earlier slot and walked past this one. + */ + map->slots[at].state = AKSL_HASHMAP_SLOT_DELETED; + map->slots[at].key[0] = '\0'; + map->slots[at].value = NULL; + map->count -= 1; + if ( removed != NULL ) { + *removed = 1; + } + } + SUCCEED_RETURN(e); +} + +/* + * Visits every live entry, in slot order -- which is to say in no order the + * caller can predict or should rely on. AKERR_ITERATOR_BREAK stops it, as + * everywhere else. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map, + aksl_HashMapIterator iter, + void *data) +{ + size_t i = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, map, AKERR_NULLPOINTER, "map=%p, iter=%p", (void *)map, (void *)iter); + FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "map=%p, iter=%p", (void *)map, (void *)iter); + FAIL_ZERO_RETURN(e, map->slots, AKERR_NULLPOINTER, "map is not initialised"); + for ( i = 0; i < map->capacity; i++ ) { + if ( map->slots[i].state != AKSL_HASHMAP_SLOT_OCCUPIED ) { + continue; + } + ATTEMPT { + CATCH(e, iter(map->slots[i].key, map->slots[i].value, data)); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_ITERATOR_BREAK) { + // Not an error condition; it is telling us to stop early + SUCCEED_RETURN(e); + } FINISH(e, true); + } + SUCCEED_RETURN(e); +} + +/* ====================================================================== */ +/* Growable string buffer */ +/* ====================================================================== */ + +/* + * The one thing here that owns memory. + * + * The bounded formatting wrappers are the right answer when the destination is + * a fixed buffer, and no answer at all when the output length is not known in + * advance -- which is why TODO.md 3.6 asks for this to "make the snprintf and + * strcat wrappers pleasant to use". Building a diagnostic, a serialised record + * or a generated line means appending to something that grows. + * + * Capacity doubles, so n appends cost O(n) amortised rather than O(n^2). The + * buffer is always NUL-terminated, so aksl_strbuf_cstr is valid at any point + * without a finalise step -- and the terminator is not counted in `length`, so + * embedded NULs are possible but the C string view stops at the first one. + */ +#define AKSL_STRBUF_MIN_CAPACITY 32 + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_init(aksl_StrBuf *buf, size_t initial) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf"); + buf->data = NULL; + buf->length = 0; + buf->capacity = 0; + if ( initial < AKSL_STRBUF_MIN_CAPACITY ) { + initial = AKSL_STRBUF_MIN_CAPACITY; + } + ATTEMPT { + CATCH(e, aksl_malloc(initial, (void **)&buf->data)); + buf->capacity = initial; + buf->data[0] = '\0'; + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +/* Make room for `extra` more bytes plus the terminator, doubling as needed. */ +static akerr_ErrorContext AKERR_NOIGNORE *strbuf_reserve(aksl_StrBuf *buf, size_t extra) +{ + size_t needed = 0; + size_t capacity = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised"); + /* size_t overflow would turn a huge append into a tiny allocation. */ + FAIL_NONZERO_RETURN(e, (extra > (size_t)-1 - buf->length - 1), AKERR_OUTOFBOUNDS, + "appending %zu bytes to %zu overflows size_t", extra, buf->length); + needed = buf->length + extra + 1; + if ( needed <= buf->capacity ) { + SUCCEED_RETURN(e); + } + capacity = buf->capacity; + while ( capacity < needed ) { + /* Doubling, but never past what size_t can hold. */ + if ( capacity > ((size_t)-1) / 2 ) { + capacity = needed; + break; + } + capacity *= 2; + } + ATTEMPT { + CATCH(e, aksl_realloc((void **)&buf->data, capacity)); + buf->capacity = capacity; + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append(aksl_StrBuf *buf, const char *s) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s); + return aksl_strbuf_append_bytes(buf, s, strlen(s)); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_bytes(aksl_StrBuf *buf, const char *s, size_t n) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "buf=%p, s=%p", (void *)buf, (void *)s); + ATTEMPT { + CATCH(e, strbuf_reserve(buf, n)); + memcpy(buf->data + buf->length, s, n); + buf->length += n; + buf->data[buf->length] = '\0'; + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_char(aksl_StrBuf *buf, char c) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf"); + return aksl_strbuf_append_bytes(buf, &c, 1); +} + +/* + * Formatted append. vsnprintf is called twice on purpose: once with a zero + * length to ask how much room the result needs, and again once that room + * exists. The alternative -- guess, then retry on truncation -- is the same two + * calls in the bad case and a wasted guess in the good one. + * + * The va_list is copied because a vsnprintf leaves it consumed, and using the + * same one twice is undefined behaviour rather than merely wrong. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_vappendf(aksl_StrBuf *buf, const char *format, va_list args) +{ + va_list measure; + int needed = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, format=%p", (void *)buf, (void *)format); + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "buf=%p, format=%p", (void *)buf, (void *)format); + va_copy(measure, args); + needed = vsnprintf(NULL, 0, format, measure); + va_end(measure); + FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format the arguments"); + ATTEMPT { + CATCH(e, strbuf_reserve(buf, (size_t)needed)); + /* + * The reserve above guarantees needed + 1 bytes, so this cannot + * truncate -- vsnprintf's return is checked all the same, because + * "cannot happen" is a claim about today's reserve. + */ + FAIL_NONZERO_BREAK(e, + (vsnprintf(buf->data + buf->length, + buf->capacity - buf->length, format, args) != needed), + AKERR_IO, "formatted output changed length between passes"); + buf->length += (size_t)needed; + buf->data[buf->length] = '\0'; + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_appendf(aksl_StrBuf *buf, const char *format, ...) +{ + va_list args; + akerr_ErrorContext *raised = NULL; + + va_start(args, format); + raised = aksl_strbuf_vappendf(buf, format, args); + va_end(args); + return raised; +} + +/* Empties without releasing, so the capacity is reused by the next round. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_reset(aksl_StrBuf *buf) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf"); + FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised"); + buf->length = 0; + buf->data[0] = '\0'; + SUCCEED_RETURN(e); +} + +/* + * The contents as a C string. Points into the buffer, so it is invalidated by + * the next append -- copy it with aksl_strdup if it has to outlive one. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_cstr(aksl_StrBuf *buf, const char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p, dest=%p", (void *)buf, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "buf=%p, dest=%p", (void *)buf, (void *)dest); + FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is not initialised"); + *dest = buf->data; + SUCCEED_RETURN(e); +} + +/* + * Releases the storage and leaves the buffer in the state aksl_strbuf_init would + * refuse to leave it in -- zeroed, so a second free is an error rather than a + * double free, exactly as aksl_freep arranges for a bare pointer. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_free(aksl_StrBuf *buf) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf"); + FAIL_ZERO_RETURN(e, buf->data, AKERR_NULLPOINTER, "buffer is already released"); + ATTEMPT { + CATCH(e, aksl_free(buf->data)); + } CLEANUP { + buf->data = NULL; + buf->length = 0; + buf->capacity = 0; + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} diff --git a/src/stdlib.c b/src/stdlib.c index 15b0549..691edc8 100644 --- a/src/stdlib.c +++ b/src/stdlib.c @@ -8,19 +8,7 @@ #include #include -/* - * errno as an akerror status, with a fallback for when it is not one. - * - * A libc call is allowed to fail without touching errno -- malloc(0) may return - * NULL and leave it alone -- and errno may equally be a leftover from some - * earlier, unrelated, *successful* call. Reporting it raw produced a FAIL whose - * status was 0, which every downstream DETECT and CATCH reads as success while - * the context still holds a pool slot: an error that is invisible and leaks at - * the same time. Every errno-sourced status in this file goes through here, and - * every wrapped call clears errno first so the value read back is its own. - * TODO.md 2.2.1. - */ -#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback)) +#include "aksl_internal.h" /* * Version of the library itself. These read the AKSL_VERSION_* macros as they diff --git a/src/stream.c b/src/stream.c new file mode 100644 index 0000000..ee7e1c7 --- /dev/null +++ b/src/stream.c @@ -0,0 +1,551 @@ +/* + * stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1. + * + * Positioning, flushing, character and line I/O, stream state, formatted input, + * and the file-level operations that go with them. + * + * The recurring theme, and the reason most of these are worth wrapping at all, + * is that stdio reports failure through a return value that is easy to mistake + * for data. ftell(3) returns -1L, fgetc(3) returns EOF, fgets(3) returns NULL, + * and sscanf(3) returns a count that a caller has to compare against the number + * of conversions it wrote out by hand. Every one of those is a silent failure + * waiting for someone to forget the check once. + * + * Where a function has a genuine "nothing more to read" outcome -- fgets at the + * end of a file, getline at EOF -- that is AKERR_EOF rather than AKERR_IO, so a + * read loop can tell the end of its input from the failure of its input. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "aksl_internal.h" + +/* ---------------------------------------------------------------------- */ +/* Positioning */ +/* ---------------------------------------------------------------------- */ + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, fseek(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO), + "fseek to offset %ld whence %d", offset, whence); + SUCCEED_RETURN(e); +} + +/* + * ftell(3) reports failure as -1L, which is also a perfectly ordinary thing for + * an arithmetic type to hold. Out through *dest, with the failure as a status. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest) +{ + long pos = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = 0; + errno = 0; + pos = ftell(stream); + FAIL_NONZERO_RETURN(e, (pos == -1L), AKSL_ERRNO_OR(AKERR_IO), "ftell failed"); + *dest = pos; + SUCCEED_RETURN(e); +} + +/* + * rewind(3) is the one positioning call with no error return at all: it is + * fseek(stream, 0, SEEK_SET) with the result thrown away, and it clears the + * error indicator on the way past so even that evidence is gone. This is the + * fseek, so a rewind that cannot happen is reported rather than assumed. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, fseek(stream, 0L, SEEK_SET), AKSL_ERRNO_OR(AKERR_IO), + "rewind failed"); + clearerr(stream); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, fseeko(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO), + "fseeko to offset %lld whence %d", (long long)offset, whence); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest) +{ + off_t pos = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = 0; + errno = 0; + pos = ftello(stream); + FAIL_NONZERO_RETURN(e, (pos == (off_t)-1), AKSL_ERRNO_OR(AKERR_IO), "ftello failed"); + *dest = pos; + SUCCEED_RETURN(e); +} + +/* + * fgetpos/fsetpos carry an opaque fpos_t rather than a byte offset, which is + * what makes them the right pair for a stream in a multibyte locale where a + * byte offset is not enough to restore the conversion state. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos); + FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos); + errno = 0; + FAIL_NONZERO_RETURN(e, fgetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fgetpos failed"); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos); + FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos); + errno = 0; + FAIL_NONZERO_RETURN(e, fsetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fsetpos failed"); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Flushing and buffering */ +/* ---------------------------------------------------------------------- */ + +/* + * NULL is legal here and means "every output stream", which is fflush(3)'s own + * documented behaviour and genuinely useful before a fork or an abort -- so + * unlike almost everywhere else in this library, a NULL argument is not an + * error. It is also the only way to find out that buffered data could not be + * written before the process goes away. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream) +{ + PREPARE_ERROR(e); + errno = 0; + FAIL_NONZERO_RETURN(e, fflush(stream), AKSL_ERRNO_OR(AKERR_IO), + "fflush failed and the buffered data is lost"); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, setvbuf(stream, buf, mode, size), AKSL_ERRNO_OR(AKERR_VALUE), + "setvbuf mode %d size %zu", mode, size); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Character and line I/O */ +/* ---------------------------------------------------------------------- */ + +/* + * fgetc(3) folds three outcomes into one int: a byte, the end of the file, and + * a read error, the last two both spelled EOF. Split apart here -- *dest is the + * byte, AKERR_EOF is the end, and anything else is the error -- so a read loop + * can be written without the ferror/feof dance at the bottom of it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest) +{ + int c = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = 0; + errno = 0; + c = fgetc(stream); + if ( c == EOF ) { + FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgetc failed"); + FAIL_RETURN(e, AKERR_EOF, "end of stream"); + } + *dest = c; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, (fputc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), + "fputc of 0x%02x failed", (unsigned)c & 0xffu); + SUCCEED_RETURN(e); +} + +/* Pushes one byte back so the next read returns it. One byte is all that is + * guaranteed; a second ungetc without a read in between may well fail. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, (ungetc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), + "ungetc of 0x%02x failed", (unsigned)c & 0xffu); + SUCCEED_RETURN(e); +} + +/* + * fgets(3) into a bounded buffer, with the length read reported and the three + * outcomes separated. A line too long for the buffer is *not* an error -- it is + * a short read that leaves the rest of the line in the stream, which is how + * fgets works and how a caller reading fixed-size chunks wants it -- but + * *len_out lets the caller notice, because a full buffer with no trailing + * newline is exactly that case. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out) +{ + char *result = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p", + (void *)s, (void *)stream, (void *)len_out); + *len_out = 0; + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p", + (void *)s, (void *)stream, (void *)len_out); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p", + (void *)s, (void *)stream, (void *)len_out); + /* fgets takes an int, and a size that does not fit one is a caller error. */ + FAIL_NONZERO_RETURN(e, (size == 0 || size > (size_t)INT_MAX), AKERR_VALUE, + "size %zu is outside the range fgets accepts", size); + s[0] = '\0'; + errno = 0; + result = fgets(s, (int)size, stream); + if ( result == NULL ) { + FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgets failed"); + FAIL_RETURN(e, AKERR_EOF, "end of stream"); + } + *len_out = strlen(s); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream); + errno = 0; + FAIL_NONZERO_RETURN(e, (fputs(s, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), "fputs failed"); + SUCCEED_RETURN(e); +} + +/* + * getline(3) grows *lineptr as needed, so the caller starts with + * `char *line = NULL; size_t cap = 0;` and releases the buffer with aksl_free + * once the loop is done -- not once per line. *len_out is the length read, + * which is what tells an embedded NUL from the end of the line; strlen cannot. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out) +{ + ssize_t got = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + *len_out = 0; + FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + errno = 0; + got = getline(lineptr, n, stream); + if ( got < 0 ) { + FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getline failed"); + FAIL_RETURN(e, AKERR_EOF, "end of stream"); + } + *len_out = (size_t)got; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim, + FILE *stream, size_t *len_out) +{ + ssize_t got = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + *len_out = 0; + FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p", + (void *)lineptr, (void *)n, (void *)stream, (void *)len_out); + errno = 0; + got = getdelim(lineptr, n, delim, stream); + if ( got < 0 ) { + FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getdelim failed"); + FAIL_RETURN(e, AKERR_EOF, "end of stream"); + } + *len_out = (size_t)got; + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Stream state */ +/* ---------------------------------------------------------------------- */ + +/* + * feof and ferror answer through an out-param, exposed so that a caller never + * has to reach into a FILE * itself. They are thin, and that is the point: the + * NULL check is the whole value. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = feof(stream); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = ferror(stream); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream); + clearerr(stream); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest) +{ + int fd = -1; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest); + *dest = -1; + errno = 0; + fd = fileno(stream); + FAIL_NONZERO_RETURN(e, (fd < 0), AKSL_ERRNO_OR(EBADF), "stream has no descriptor"); + *dest = fd; + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Opening by other means */ +/* ---------------------------------------------------------------------- */ + +akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode, + FILE *stream, FILE **fp) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p", + (void *)pathname, (void *)mode, (void *)stream, (void *)fp); + *fp = NULL; + FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p", + (void *)pathname, (void *)mode, (void *)stream, (void *)fp); + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p", + (void *)pathname, (void *)mode, (void *)stream, (void *)fp); + /* + * pathname NULL is legal and means "reopen the same file with a new mode", + * which is the one thing freopen can do that fopen cannot -- so it is not + * checked, unlike aksl_fopen's. + */ + errno = 0; + *fp = freopen(pathname, mode, stream); + FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "%s", pathname != NULL ? pathname : "(same file)"); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp); + *fp = NULL; + FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp); + FAIL_NONZERO_RETURN(e, (fd < 0), AKERR_VALUE, "fd=%d is not a descriptor", fd); + errno = 0; + *fp = fdopen(fd, mode); + FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "fd=%d mode=%s", fd, mode); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "fp=%p", (void *)fp); + *fp = NULL; + errno = 0; + *fp = tmpfile(); + FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "could not create a temporary file"); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Formatted input */ +/* ---------------------------------------------------------------------- */ + +/* + * The scanf family's return value is the number of conversions that succeeded, + * which the caller is expected to compare against the number it wrote in the + * format string -- by hand, from memory, at every call site. Getting that wrong + * leaves the unassigned arguments holding whatever they held before, which for + * the usual uninitialised local is anything at all. + * + * So `expected` is an argument: say how many conversions must succeed, and + * anything less is AKERR_VALUE with both counts in the message. Pass 0 to opt + * out and read the count from *assigned yourself. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format, + int expected, int *assigned, va_list args) +{ + int got = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p", + (void *)str, (void *)format, (void *)assigned); + *assigned = 0; + FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p", + (void *)str, (void *)format, (void *)assigned); + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p", + (void *)str, (void *)format, (void *)assigned); + errno = 0; + got = vsscanf(str, format, args); + FAIL_NONZERO_RETURN(e, (got == EOF), AKSL_ERRNO_OR(AKERR_VALUE), + "no conversions were possible on \"%s\"", str); + *assigned = got; + FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE, + "%d of %d conversions succeeded on \"%s\"", got, expected, str); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format, + int expected, int *assigned, ...) +{ + va_list args; + akerr_ErrorContext *raised = NULL; + + va_start(args, assigned); + raised = aksl_vsscanf(str, format, expected, assigned, args); + va_end(args); + return raised; +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format, + int expected, int *assigned, va_list args) +{ + int got = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p", + (void *)stream, (void *)format, (void *)assigned); + *assigned = 0; + FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p", + (void *)stream, (void *)format, (void *)assigned); + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p", + (void *)stream, (void *)format, (void *)assigned); + errno = 0; + got = vfscanf(stream, format, args); + if ( got == EOF ) { + FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fscanf failed"); + FAIL_RETURN(e, AKERR_EOF, "end of stream before any conversion"); + } + *assigned = got; + FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE, + "%d of %d conversions succeeded", got, expected); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format, + int expected, int *assigned, ...) +{ + va_list args; + akerr_ErrorContext *raised = NULL; + + va_start(args, assigned); + raised = aksl_vfscanf(stream, format, expected, assigned, args); + va_end(args); + return raised; +} + +/* ---------------------------------------------------------------------- */ +/* Files */ +/* ---------------------------------------------------------------------- */ + +akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p", (void *)pathname); + errno = 0; + FAIL_NONZERO_RETURN(e, remove(pathname), AKSL_ERRNO_OR(AKERR_IO), "%s", pathname); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, oldpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p", + (void *)oldpath, (void *)newpath); + FAIL_ZERO_RETURN(e, newpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p", + (void *)oldpath, (void *)newpath); + errno = 0; + FAIL_NONZERO_RETURN(e, rename(oldpath, newpath), AKSL_ERRNO_OR(AKERR_IO), + "%s -> %s", oldpath, newpath); + SUCCEED_RETURN(e); +} + +/* + * mkstemp and mkdtemp both rewrite the template in place, so the template must + * be a writable buffer ending in exactly six X characters -- a string literal + * is a segfault, and that is checked here rather than left to the kernel. The + * caller owns the resulting file or directory, including removing it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd) +{ + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, fd, AKERR_NULLPOINTER, "template=%p, fd=%p", (void *)template_, (void *)fd); + *fd = -1; + FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p, fd=%p", + (void *)template_, (void *)fd); + len = strlen(template_); + FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0), + AKERR_VALUE, + "template \"%s\" must end in six literal X characters", template_); + errno = 0; + *fd = mkstemp(template_); + FAIL_NONZERO_RETURN(e, (*fd < 0), AKSL_ERRNO_OR(AKERR_IO), "template \"%s\"", template_); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_) +{ + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p", (void *)template_); + len = strlen(template_); + FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0), + AKERR_VALUE, + "template \"%s\" must end in six literal X characters", template_); + errno = 0; + FAIL_ZERO_RETURN(e, mkdtemp(template_), AKSL_ERRNO_OR(AKERR_IO), + "template \"%s\"", template_); + SUCCEED_RETURN(e); +} diff --git a/src/string.c b/src/string.c new file mode 100644 index 0000000..6ccf1dc --- /dev/null +++ b/src/string.c @@ -0,0 +1,486 @@ +/* + * string.h wrappers -- TODO.md section 3.1. + * + * This is the section akbasic needed most and could not have: across its source + * it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of + * them raw because there was nothing here to call instead. Ten of those sites + * are the same idiom written out by hand -- a length check, then strncpy, then + * an explicit NUL -- which is exactly the "truncation reported as an error + * rather than silently accepted" that TODO.md 3.1 asks for. + * + * Two conventions run through the whole file. + * + * The copying functions take the size of the destination, and the ones named + * after libc functions that do not take one take it anyway. strcpy(3) and + * strcat(3) cannot be called safely without knowing how much room the + * destination has, and a wrapper that accepts the same arguments as strcpy + * would be an error-handling wrapper around a buffer overflow. Truncation is + * AKERR_OUTOFBOUNDS: it is the failure these functions exist to report, and + * nothing is written to the destination when it happens, so a caller who + * ignores the status does not get a half-copied string either. aksl_strncpy + * also always terminates, which strncpy(3) famously does not. + * + * The searching functions answer through an out-param and treat "not found" as + * a successful answer of NULL rather than as an error -- absent is an ordinary + * answer to "where is this", and a library that raised on it would have every + * caller handling a non-error as an error. + */ + +#include + +#include +#include +#include +#include +#include + +#include "aksl_internal.h" + +/* ---------------------------------------------------------------------- */ +/* Length */ +/* ---------------------------------------------------------------------- */ + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = strlen(s); + SUCCEED_RETURN(e); +} + +/* + * strnlen(3) stops at maxlen whether or not it found a terminator, so a result + * equal to maxlen means "at least this long" rather than "this long". That + * ambiguity is the reason to use it -- it is how you measure a buffer that may + * not be terminated -- so it is reported rather than hidden: *dest is the + * length and the call succeeds either way. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = strnlen(s, maxlen); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Copying and concatenation */ +/* ---------------------------------------------------------------------- */ + +/* + * Bounded copy. dstsize is the whole destination buffer, terminator included, + * so `char buf[64]` pairs with `sizeof(buf)`. A source that does not fit is + * AKERR_OUTOFBOUNDS and the destination is left as an empty string rather than + * as a truncated one -- a caller who ignores the error gets nothing, which is + * far easier to notice than a plausible-looking prefix. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src) +{ + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator"); + dst[0] = '\0'; + len = strlen(src); + FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS, + "source is %zu bytes and the destination holds %zu including the terminator", + len, dstsize); + memcpy(dst, src, len + 1); + SUCCEED_RETURN(e); +} + +/* + * At most n bytes of src, and always terminated. + * + * strncpy(3) does two surprising things that this does not: it leaves the + * destination unterminated when the source is at least n bytes long, and it + * pads the remainder with NULs when the source is shorter, which turns a short + * copy into a full-length write. Here n bounds how much of the source is + * considered, dstsize bounds the write, and the result is always a C string. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n) +{ + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator"); + dst[0] = '\0'; + len = strnlen(src, n); + FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS, + "%zu bytes to copy and the destination holds %zu including the terminator", + len, dstsize); + memcpy(dst, src, len); + dst[len] = '\0'; + SUCCEED_RETURN(e); +} + +/* + * Bounded append. dstsize is again the whole buffer, so the room actually + * available is dstsize minus what is already in there. The destination must + * already be a terminated string within dstsize; if it is not, that is + * AKERR_VALUE rather than a walk off the end looking for a NUL that is not + * there. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src) +{ + size_t used = 0; + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator"); + used = strnlen(dst, dstsize); + FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE, + "destination is not terminated within its %zu bytes", dstsize); + len = strlen(src); + FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS, + "%zu bytes in use plus %zu to append exceeds the %zu-byte destination", + used, len, dstsize); + memcpy(dst + used, src, len + 1); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n) +{ + size_t used = 0; + size_t len = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src); + FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator"); + used = strnlen(dst, dstsize); + FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE, + "destination is not terminated within its %zu bytes", dstsize); + len = strnlen(src, n); + FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS, + "%zu bytes in use plus %zu to append exceeds the %zu-byte destination", + used, len, dstsize); + memcpy(dst + used, src, len); + dst[used + len] = '\0'; + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Duplication */ +/* ---------------------------------------------------------------------- */ + +/* *dest is the caller's, to release with aksl_free or aksl_freep. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest) +{ + char *copy = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = NULL; + errno = 0; + copy = strdup(s); + FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", strlen(s) + 1); + *dest = copy; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest) +{ + char *copy = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = NULL; + errno = 0; + copy = strndup(s, n); + FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", n + 1); + *dest = copy; + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Comparison */ +/* ---------------------------------------------------------------------- */ + +/* + * The comparison result goes through *dest because the return value is spoken + * for by the error context. The sign is the usual one: negative, zero or + * positive as a sorts before, equal to, or after b. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + *dest = strcmp(a, b); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + *dest = strncmp(a, b, n); + SUCCEED_RETURN(e); +} + +/* + * Case-insensitive comparison. akbasic folds case by hand at three sites -- + * twice with an open-coded loop and once through -- because BASIC + * verb and function names are case-insensitive while variable names are not. + * All three of those are this call. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + *dest = strcasecmp(a, b); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + *dest = strncasecmp(a, b, n); + SUCCEED_RETURN(e); +} + +/* + * Locale-aware collation. strcoll(3) has no error return of its own, so a + * malformed multibyte sequence in the current locale shows up only as errno + * being set -- which is why errno is cleared first and consulted after. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest); + errno = 0; + *dest = strcoll(a, b); + FAIL_NONZERO_RETURN(e, errno, errno, "strcoll failed in the current locale"); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Searching */ +/* ---------------------------------------------------------------------- */ + +/* + * Every function in this section writes NULL to *dest and succeeds when there + * is nothing to find. "Absent" is an answer, not a failure; raising on it would + * make every caller handle a non-error as an error, and the pool slot that + * error consumed would be pure waste. + * + * *dest points into the caller's own string, so it lives exactly as long as the + * string does and must not be freed. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = strchr(s, c); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest); + *dest = strrchr(s, c); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + *dest = strstr(haystack, needle); + SUCCEED_RETURN(e); +} + +/* + * Case-insensitive search. strcasestr(3) is a GNU extension that also exists on + * the BSDs and musl, but it is not in any standard, so it is open-coded here + * rather than depending on _GNU_SOURCE reaching every consumer's build. The + * naive scan is the same complexity as glibc's fallback and this is not the hot + * path in anything. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest) +{ + size_t hlen = 0; + size_t nlen = 0; + size_t i = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p", + (void *)haystack, (void *)needle, (void *)dest); + *dest = NULL; + nlen = strlen(needle); + /* An empty needle matches at the start, as strstr(3) has it. */ + if ( nlen == 0 ) { + *dest = (char *)haystack; + SUCCEED_RETURN(e); + } + hlen = strlen(haystack); + if ( nlen <= hlen ) { + for ( i = 0; i <= hlen - nlen; i++ ) { + if ( strncasecmp(haystack + i, needle, nlen) == 0 ) { + *dest = (char *)(haystack + i); + break; + } + } + } + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + *dest = strpbrk(s, accept); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p", + (void *)s, (void *)accept, (void *)dest); + *dest = strspn(s, accept); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p", + (void *)s, (void *)reject, (void *)dest); + FAIL_ZERO_RETURN(e, reject, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p", + (void *)s, (void *)reject, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p", + (void *)s, (void *)reject, (void *)dest); + *dest = strcspn(s, reject); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* Tokenising */ +/* ---------------------------------------------------------------------- */ + +/* + * The reentrant tokeniser only. strtok(3) keeps its state in a hidden static, + * which makes two interleaved tokenisations silently corrupt each other and + * makes any use from a thread a bug, so it is not wrapped here at all -- see + * the note on threads in README.md. + * + * Running out of tokens is success with *dest NULL, for the same reason a failed + * search is: it is how the loop ends, not a fault. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p", + (void *)delim, (void *)saveptr, (void *)dest); + FAIL_ZERO_RETURN(e, saveptr, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p", + (void *)delim, (void *)saveptr, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p", + (void *)delim, (void *)saveptr, (void *)dest); + *dest = strtok_r(str, delim, saveptr); + SUCCEED_RETURN(e); +} + +/* + * strsep(3) differs from strtok_r in returning empty tokens between adjacent + * delimiters, which is what you want for parsing "a::b" as three fields rather + * than two. It advances *stringp itself and sets it to NULL when done. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, stringp, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p", + (void *)stringp, (void *)delim, (void *)dest); + FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p", + (void *)stringp, (void *)delim, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p", + (void *)stringp, (void *)delim, (void *)dest); + *dest = strsep(stringp, delim); + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* errno messages */ +/* ---------------------------------------------------------------------- */ + +/* + * The message for a status, into the caller's own buffer. + * + * Deliberately not built on strerror_r(3). There are two incompatible functions + * by that name -- the XSI one returns int, the GNU one returns char * and may + * not touch the buffer at all -- and which one a translation unit gets depends + * on feature-test macros that a consumer of this library cannot influence from + * in here. strerror(3) itself is not thread-safe, so that is no way out either. + * + * libakerror already carries the full errno name table (its generrno.sh builds + * one at configure time), and its registry also knows the names of this + * library's own non-errno statuses -- AKERR_NULLPOINTER, AKERR_ITERATOR_BREAK + * and the rest -- which strerror_r could never name. So the lookup goes there, + * and a status neither table recognises comes back as its own number rather + * than as the bare string "Unknown Error", which tells the reader nothing. + * + * The message is truncated to nothing rather than to a prefix if it does not + * fit, on the same principle as aksl_strcpy above. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen) +{ + const char *name = NULL; + int needed = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p", (void *)buf); + FAIL_ZERO_RETURN(e, buflen, AKERR_VALUE, "buflen=0 leaves no room for a terminator"); + buf[0] = '\0'; + name = akerr_name_for_status(status, NULL); + /* + * akerr_name_for_status never returns NULL; it returns this exact string + * for anything it does not have a name for. + */ + if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) { + needed = snprintf(buf, buflen, "Unknown status %d", status); + } else { + needed = snprintf(buf, buflen, "%s", name); + } + FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format status %d", status); + if ( (size_t)needed >= buflen ) { + buf[0] = '\0'; + FAIL_RETURN(e, AKERR_OUTOFBOUNDS, + "the message for status %d needs %d bytes and the buffer holds %zu", + status, needed + 1, buflen); + } + SUCCEED_RETURN(e); +} diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..50da858 --- /dev/null +++ b/test.sh @@ -0,0 +1,4 @@ + cmake -S . -B build + cmake --build build + ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml" + python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65 diff --git a/tests/test_collections.c b/tests/test_collections.c new file mode 100644 index 0000000..b362f63 --- /dev/null +++ b/tests/test_collections.c @@ -0,0 +1,730 @@ +/* + * List and tree additions -- src/collections.c, TODO.md section 3.6. + * + * The bare-node list functions, the tracked aksl_List container, and the binary + * search tree. The hash map and string buffer have their own files. + * + * Nodes here are stack arrays, which is the point: none of these functions + * allocates, so a caller drawing from a fixed pool can use all of them. The two + * *_free_all tests use aksl_malloc explicitly, because releasing is the one + * thing that has to know where the memory came from. + */ + +#include "aksl_capture.h" + +#define N 5 + +/* ---------------------------------------------------------------------- */ +/* Helpers */ +/* ---------------------------------------------------------------------- */ + +typedef struct VisitLog +{ + int count; + aksl_ListNode *seen[16]; + int break_at; +} VisitLog; + +static void visitlog_init(VisitLog *log) +{ + memset((void *)log, 0x00, sizeof(VisitLog)); + log->break_at = -1; +} + +static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data) +{ + VisitLog *log = NULL; + int idx = 0; + + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); + FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data"); + log = (VisitLog *)data; + idx = log->count; + if ( idx < 16 ) { + log->seen[idx] = node; + } + log->count += 1; + if ( log->break_at == idx ) { + FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at visit %d", idx); + } + SUCCEED_RETURN(e); +} + +/* Accepts the node whose data pointer equals `data`. */ +static akerr_ErrorContext AKERR_NOIGNORE *match_data(aksl_ListNode *node, void *data, int *matched) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); + FAIL_ZERO_RETURN(e, matched, AKERR_NULLPOINTER, "matched"); + *matched = (node->data == data) ? 1 : 0; + SUCCEED_RETURN(e); +} + +/* A predicate that fails, to prove the error comes back out of the search. */ +static akerr_ErrorContext AKERR_NOIGNORE *failing_predicate(aksl_ListNode *node, void *data, int *matched) +{ + PREPARE_ERROR(e); + (void)node; + (void)data; + (void)matched; + FAIL_RETURN(e, AKERR_VALUE, "predicate refused to answer"); +} + +/* Build node[0..n) into a chain and hand back the head. */ +static void build_chain(aksl_ListNode *node, int n) +{ + int i = 0; + + for ( i = 0; i < n; i++ ) { + memset((void *)&node[i], 0x00, sizeof(aksl_ListNode)); + } + for ( i = 1; i < n; i++ ) { + node[i - 1].next = &node[i]; + node[i].prev = &node[i - 1]; + } +} + +/* ---------------------------------------------------------------------- */ +/* Insertion */ +/* ---------------------------------------------------------------------- */ + +static int test_prepend_moves_the_head(void) +{ + aksl_ListNode node[3]; + aksl_ListNode *head = NULL; + + build_chain(node, 3); + head = &node[1]; + node[1].prev = NULL; + + AKSL_CHECK_OK(aksl_list_prepend(&head, &node[0])); + AKSL_CHECK(head == &node[0]); + AKSL_CHECK(node[0].next == &node[1]); + AKSL_CHECK(node[0].prev == NULL); + AKSL_CHECK(node[1].prev == &node[0]); + + /* Prepending onto an empty list makes a one-node list. */ + head = NULL; + memset((void *)&node[2], 0x00, sizeof(node[2])); + AKSL_CHECK_OK(aksl_list_prepend(&head, &node[2])); + AKSL_CHECK(head == &node[2]); + AKSL_CHECK(node[2].next == NULL); + + AKSL_CHECK_STATUS(aksl_list_prepend(&head, &node[2]), AKERR_VALUE); + AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_prepend(&head, NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_insert_after_and_before(void) +{ + aksl_ListNode node[4]; + aksl_ListNode *head = &node[0]; + + build_chain(node, 2); + memset((void *)&node[2], 0x00, sizeof(node[2])); + memset((void *)&node[3], 0x00, sizeof(node[3])); + + /* node[0] -> node[2] -> node[1] */ + AKSL_CHECK_OK(aksl_list_insert_after(&node[0], &node[2])); + AKSL_CHECK(node[0].next == &node[2]); + AKSL_CHECK(node[2].prev == &node[0]); + AKSL_CHECK(node[2].next == &node[1]); + AKSL_CHECK(node[1].prev == &node[2]); + + /* Inserting before the head moves it, which is why head is required. */ + AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[0], &node[3])); + AKSL_CHECK(head == &node[3]); + AKSL_CHECK(node[3].next == &node[0]); + AKSL_CHECK(node[0].prev == &node[3]); + + AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], &node[0]), AKERR_VALUE); + AKSL_CHECK_STATUS(aksl_list_insert_after(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_insert_after(&node[0], NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], &node[0]), AKERR_VALUE); + AKSL_CHECK_STATUS(aksl_list_insert_before(NULL, &node[0], &node[1]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_insert_before(&head, NULL, &node[1]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_insert_before(&head, &node[0], NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Inspection */ +/* ---------------------------------------------------------------------- */ + +static int test_length_counts_and_refuses_cycles(void) +{ + aksl_ListNode node[N]; + size_t n = 99; + + /* An empty list is length 0, not an error. */ + AKSL_CHECK_OK(aksl_list_length(NULL, &n)); + AKSL_CHECK(n == 0); + + build_chain(node, N); + AKSL_CHECK_OK(aksl_list_length(&node[0], &n)); + AKSL_CHECK(n == N); + + node[N - 1].next = &node[0]; + AKSL_CHECK_STATUS(aksl_list_length(&node[0], &n), AKERR_CIRCULAR_REFERENCE); + + AKSL_CHECK_STATUS(aksl_list_length(&node[0], NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_find_returns_the_first_match_or_null(void) +{ + aksl_ListNode node[N]; + aksl_ListNode *found = (aksl_ListNode *)0x1; + int payload = 42; + int absent = 0; + + build_chain(node, N); + node[2].data = &payload; + + AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &payload, &found)); + AKSL_CHECK(found == &node[2]); + + /* Nothing matches: NULL and success, not an error. */ + AKSL_CHECK_OK(aksl_list_find(&node[0], &match_data, &absent, &found)); + AKSL_CHECK(found == NULL); + + /* An empty list finds nothing, equally without complaint. */ + AKSL_CHECK_OK(aksl_list_find(NULL, &match_data, &payload, &found)); + AKSL_CHECK(found == NULL); + + /* A predicate that raises stops the search and propagates. */ + AKSL_CHECK_STATUS_MSG_CONTAINS( + aksl_list_find(&node[0], &failing_predicate, NULL, &found), + AKERR_VALUE, "predicate refused"); + + AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Rearranging */ +/* ---------------------------------------------------------------------- */ + +static int test_reverse_flips_both_directions(void) +{ + aksl_ListNode node[N]; + aksl_ListNode *head = NULL; + aksl_ListNode *walk = NULL; + int i = 0; + + build_chain(node, N); + head = &node[0]; + + AKSL_CHECK_OK(aksl_list_reverse(&head)); + AKSL_CHECK(head == &node[N - 1]); + + /* Forwards through the reversed list is backwards through the array. */ + walk = head; + for ( i = N - 1; i >= 0; i-- ) { + AKSL_CHECK(walk == &node[i]); + walk = walk->next; + } + AKSL_CHECK(walk == NULL); + + /* And the prev links were flipped too, not just the next ones. */ + walk = &node[0]; + for ( i = 0; i < N; i++ ) { + AKSL_CHECK(walk == &node[i]); + walk = walk->prev; + } + AKSL_CHECK(walk == NULL); + + /* Reversing an empty list is a no-op, not an error. */ + head = NULL; + AKSL_CHECK_OK(aksl_list_reverse(&head)); + AKSL_CHECK(head == NULL); + AKSL_CHECK_STATUS(aksl_list_reverse(NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_concat_joins_two_lists(void) +{ + aksl_ListNode first[3]; + aksl_ListNode second[2]; + size_t n = 0; + + build_chain(first, 3); + build_chain(second, 2); + + AKSL_CHECK_OK(aksl_list_concat(&first[0], &second[0])); + AKSL_CHECK(first[2].next == &second[0]); + AKSL_CHECK(second[0].prev == &first[2]); + AKSL_CHECK_OK(aksl_list_length(&first[0], &n)); + AKSL_CHECK(n == 5); + + /* Concatenating with NULL is a no-op; with itself would make a cycle. */ + AKSL_CHECK_OK(aksl_list_concat(&first[0], NULL)); + AKSL_CHECK_STATUS(aksl_list_concat(&first[0], &first[0]), AKERR_VALUE); + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_concat(&first[0], &second[1]), + AKERR_VALUE, "already in the destination"); + AKSL_CHECK_STATUS(aksl_list_concat(NULL, &second[0]), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Reverse iteration */ +/* ---------------------------------------------------------------------- */ + +static int test_iterate_reverse_walks_back_to_the_head(void) +{ + aksl_ListNode node[N]; + VisitLog log; + int i = 0; + + build_chain(node, N); + visitlog_init(&log); + + AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log)); + AKSL_CHECK(log.count == N); + for ( i = 0; i < N; i++ ) { + AKSL_CHECK(log.seen[i] == &node[N - 1 - i]); + } + + /* The break works going this way too. */ + visitlog_init(&log); + log.break_at = 1; + AKSL_CHECK_OK(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log)); + AKSL_CHECK(log.count == 2); + + /* And a cycle in the prev links is refused, as it is in the next links. */ + node[0].prev = &node[N - 1]; + visitlog_init(&log); + AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[N - 1], &record_visit, &log), + AKERR_CIRCULAR_REFERENCE); + + AKSL_CHECK_STATUS(aksl_list_iterate_reverse(NULL, &record_visit, &log), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_iterate_reverse(&node[0], NULL, &log), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Releasing */ +/* ---------------------------------------------------------------------- */ + +static int test_free_all_releases_every_node(void) +{ + aksl_ListNode *head = NULL; + aksl_ListNode *node = NULL; + int i = 0; + + /* A heap list, since this is the one operation that has to own its memory. */ + for ( i = 0; i < N; i++ ) { + AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node)); + AKSL_CHECK_OK(aksl_list_node_init(node, NULL)); + if ( head == NULL ) { + head = node; + } else { + AKSL_CHECK_OK(aksl_list_append(head, node)); + } + } + + AKSL_CHECK_OK(aksl_list_free_all(&head, NULL)); + AKSL_CHECK(head == NULL); + + /* An empty list frees cleanly, and a second call is a no-op rather than a + * double free, because the head was cleared. */ + AKSL_CHECK_OK(aksl_list_free_all(&head, NULL)); + AKSL_CHECK_STATUS(aksl_list_free_all(NULL, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* The tracked container */ +/* ---------------------------------------------------------------------- */ + +/* + * The reason the container exists: aksl_list_append walks to the tail every + * time, so building n nodes with it is O(n^2). push is O(1) and the length is a + * field rather than a walk. + */ +static int test_container_push_and_unshift(void) +{ + aksl_List list; + aksl_ListNode node[N]; + int i = 0; + + AKSL_CHECK_OK(aksl_list_init(&list)); + AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0); + + for ( i = 0; i < N; i++ ) { + memset((void *)&node[i], 0x00, sizeof(node[i])); + AKSL_CHECK_OK(aksl_list_push(&list, &node[i])); + AKSL_CHECK(list.length == (size_t)(i + 1)); + AKSL_CHECK(list.tail == &node[i]); + } + AKSL_CHECK(list.head == &node[0]); + AKSL_CHECK(node[0].prev == NULL); + AKSL_CHECK(node[N - 1].next == NULL); + + AKSL_CHECK_OK(aksl_list_init(&list)); + for ( i = 0; i < N; i++ ) { + memset((void *)&node[i], 0x00, sizeof(node[i])); + AKSL_CHECK_OK(aksl_list_unshift(&list, &node[i])); + AKSL_CHECK(list.head == &node[i]); + AKSL_CHECK(list.tail == &node[0]); + } + AKSL_CHECK(list.length == N); + + AKSL_CHECK_STATUS(aksl_list_init(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_push(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_push(&list, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_unshift(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_unshift(&list, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* Removing keeps head, tail and length describing the list. */ +static int test_container_remove_maintains_the_endpoints(void) +{ + aksl_List list; + aksl_ListNode node[3]; + aksl_ListNode stranger; + int i = 0; + + AKSL_CHECK_OK(aksl_list_init(&list)); + for ( i = 0; i < 3; i++ ) { + memset((void *)&node[i], 0x00, sizeof(node[i])); + AKSL_CHECK_OK(aksl_list_push(&list, &node[i])); + } + memset((void *)&stranger, 0x00, sizeof(stranger)); + + /* Middle. */ + AKSL_CHECK_OK(aksl_list_remove(&list, &node[1])); + AKSL_CHECK(list.length == 2); + AKSL_CHECK(list.head == &node[0] && list.tail == &node[2]); + AKSL_CHECK(node[0].next == &node[2] && node[2].prev == &node[0]); + + /* Head. */ + AKSL_CHECK_OK(aksl_list_remove(&list, &node[0])); + AKSL_CHECK(list.head == &node[2] && list.tail == &node[2]); + AKSL_CHECK(list.length == 1); + + /* Last one out empties both endpoints. */ + AKSL_CHECK_OK(aksl_list_remove(&list, &node[2])); + AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0); + + /* A node that is not in this list is refused rather than corrupting it. */ + AKSL_CHECK_OK(aksl_list_push(&list, &node[0])); + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_remove(&list, &stranger), + AKERR_VALUE, "not in this list"); + AKSL_CHECK(list.length == 1); + + AKSL_CHECK_STATUS(aksl_list_remove(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_list_remove(&list, NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_container_clear(void) +{ + aksl_List list; + aksl_ListNode *node = NULL; + int i = 0; + + AKSL_CHECK_OK(aksl_list_init(&list)); + for ( i = 0; i < N; i++ ) { + AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node)); + AKSL_CHECK_OK(aksl_list_node_init(node, NULL)); + AKSL_CHECK_OK(aksl_list_push(&list, node)); + } + AKSL_CHECK(list.length == N); + + AKSL_CHECK_OK(aksl_list_clear(&list, NULL)); + AKSL_CHECK(list.head == NULL && list.tail == NULL && list.length == 0); + AKSL_CHECK_STATUS(aksl_list_clear(NULL, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Binary search tree */ +/* ---------------------------------------------------------------------- */ + +/* Compares the ints the leaf pointers point at. */ +static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a"); + FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "b"); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest"); + *dest = *(int *)a - *(int *)b; + SUCCEED_RETURN(e); +} + +typedef struct OrderLog +{ + int count; + int seen[16]; +} OrderLog; + +static akerr_ErrorContext AKERR_NOIGNORE *record_leaf(aksl_TreeNode *node, void *data) +{ + OrderLog *log = NULL; + + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); + FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data"); + log = (OrderLog *)data; + if ( log->count < 16 ) { + log->seen[log->count] = *(int *)node->leaf; + } + log->count += 1; + SUCCEED_RETURN(e); +} + +/* + * Insertion order 5 3 8 1 4 7 9 builds a tree whose in-order walk is sorted -- + * which is the whole invariant a search tree exists to maintain, so asserting it + * is worth more than asserting any particular shape. + */ +static int test_tree_insert_orders_the_leaves(void) +{ + static int values[7] = { 5, 3, 8, 1, 4, 7, 9 }; + aksl_TreeNode node[7]; + aksl_TreeNode *root = NULL; + OrderLog log; + int i = 0; + + for ( i = 0; i < 7; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + AKSL_CHECK(root == &node[0]); + + memset((void *)&log, 0x00, sizeof(log)); + AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL, + AKSL_TREE_SEARCH_DFS_INORDER, &log)); + AKSL_CHECK(log.count == 7); + for ( i = 1; i < log.count; i++ ) { + AKSL_CHECK(log.seen[i - 1] < log.seen[i]); + } + + AKSL_CHECK_STATUS(aksl_tree_insert(NULL, &node[0], &compare_ints), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_tree_insert(&root, NULL, &compare_ints), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_tree_insert(&root, &node[0], NULL), AKERR_NULLPOINTER); + return 0; +} + +/* + * TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by + * anything in the library. These are the functions that set it, and + * aksl_tree_remove is the one that needs it. + */ +static int test_tree_insert_sets_the_parent_links(void) +{ + static int values[3] = { 5, 3, 8 }; + aksl_TreeNode node[3]; + aksl_TreeNode *root = NULL; + int i = 0; + + for ( i = 0; i < 3; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + + AKSL_CHECK(node[0].parent == NULL); /* the root */ + AKSL_CHECK(node[1].parent == &node[0]); + AKSL_CHECK(node[2].parent == &node[0]); + AKSL_CHECK(node[0].left == &node[1]); + AKSL_CHECK(node[0].right == &node[2]); + return 0; +} + +static int test_tree_find(void) +{ + static int values[5] = { 5, 3, 8, 1, 9 }; + int wanted = 8; + int absent = 6; + aksl_TreeNode node[5]; + aksl_TreeNode *root = NULL; + aksl_TreeNode *found = (aksl_TreeNode *)0x1; + int i = 0; + + for ( i = 0; i < 5; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + + AKSL_CHECK_OK(aksl_tree_find(root, &wanted, &compare_ints, &found)); + AKSL_CHECK(found == &node[2]); + + /* Absent is NULL and success. */ + AKSL_CHECK_OK(aksl_tree_find(root, &absent, &compare_ints, &found)); + AKSL_CHECK(found == NULL); + + /* An empty tree finds nothing, equally without complaint. */ + AKSL_CHECK_OK(aksl_tree_find(NULL, &wanted, &compare_ints, &found)); + AKSL_CHECK(found == NULL); + + AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, NULL, &found), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_tree_find(root, &wanted, &compare_ints, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* + * All three removal cases, each checked by re-walking the tree and confirming it + * is still sorted and one node shorter. The two-child case is the interesting + * one: the in-order successor takes the node's place, which is the only value + * that keeps the ordering invariant on both sides. + */ +static int test_tree_remove_all_three_cases(void) +{ + static int values[7] = { 5, 3, 8, 1, 4, 7, 9 }; + aksl_TreeNode node[7]; + aksl_TreeNode *root = NULL; + OrderLog log; + size_t count = 0; + int i = 0; + + for ( i = 0; i < 7; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + + /* Leaf: node[3] holds 1 and has no children. */ + AKSL_CHECK_OK(aksl_tree_remove(&root, &node[3])); + AKSL_CHECK(node[3].parent == NULL && node[3].left == NULL && node[3].right == NULL); + AKSL_CHECK_OK(aksl_tree_count(root, &count)); + AKSL_CHECK(count == 6); + + /* One child: node[1] holds 3 and now has only its right child (4). */ + AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1])); + AKSL_CHECK_OK(aksl_tree_count(root, &count)); + AKSL_CHECK(count == 5); + + /* Two children: the root, 5, with 4 on the left and 8 on the right. */ + AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0])); + AKSL_CHECK_OK(aksl_tree_count(root, &count)); + AKSL_CHECK(count == 4); + AKSL_CHECK(root != &node[0]); + AKSL_CHECK(root->parent == NULL); + + /* Still sorted after all of that, which is the invariant that matters. */ + memset((void *)&log, 0x00, sizeof(log)); + AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL, + AKSL_TREE_SEARCH_DFS_INORDER, &log)); + AKSL_CHECK(log.count == 4); + for ( i = 1; i < log.count; i++ ) { + AKSL_CHECK(log.seen[i - 1] < log.seen[i]); + } + + AKSL_CHECK_STATUS(aksl_tree_remove(NULL, &node[0]), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_tree_remove(&root, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* Removing the last node empties the tree rather than leaving a dangling root. */ +static int test_tree_remove_the_only_node(void) +{ + int value = 1; + aksl_TreeNode node; + aksl_TreeNode *root = NULL; + size_t count = 99; + + AKSL_CHECK_OK(aksl_tree_node_init(&node, &value)); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node, &compare_ints)); + AKSL_CHECK_OK(aksl_tree_remove(&root, &node)); + AKSL_CHECK(root == NULL); + AKSL_CHECK_OK(aksl_tree_count(root, &count)); + AKSL_CHECK(count == 0); + return 0; +} + +static int test_tree_height_and_count(void) +{ + static int values[7] = { 5, 3, 8, 1, 4, 7, 9 }; + static int chain[4] = { 1, 2, 3, 4 }; + aksl_TreeNode node[7]; + aksl_TreeNode *root = NULL; + size_t count = 99; + int height = 99; + int i = 0; + + /* Empty. */ + AKSL_CHECK_OK(aksl_tree_height(NULL, &height)); + AKSL_CHECK(height == 0); + AKSL_CHECK_OK(aksl_tree_count(NULL, &count)); + AKSL_CHECK(count == 0); + + /* Balanced by construction: 7 nodes, 3 levels. */ + for ( i = 0; i < 7; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + AKSL_CHECK_OK(aksl_tree_count(root, &count)); + AKSL_CHECK(count == 7); + AKSL_CHECK_OK(aksl_tree_height(root, &height)); + AKSL_CHECK(height == 3); + + /* Sorted input gives a degenerate chain: 4 nodes, 4 levels. This is a plain + * unbalanced BST and does not pretend otherwise. */ + root = NULL; + for ( i = 0; i < 4; i++ ) { + AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &chain[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints)); + } + AKSL_CHECK_OK(aksl_tree_height(root, &height)); + AKSL_CHECK(height == 4); + + AKSL_CHECK_STATUS(aksl_tree_height(root, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_tree_count(root, NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_tree_free_all(void) +{ + static int values[5] = { 5, 3, 8, 1, 9 }; + aksl_TreeNode *root = NULL; + aksl_TreeNode *node = NULL; + int i = 0; + + for ( i = 0; i < 5; i++ ) { + AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node)); + AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i])); + AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints)); + } + + AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL)); + AKSL_CHECK(root == NULL); + + /* An empty tree frees cleanly and a second call is a no-op. */ + AKSL_CHECK_OK(aksl_tree_free_all(&root, NULL)); + AKSL_CHECK_STATUS(aksl_tree_free_all(NULL, NULL), AKERR_NULLPOINTER); + return 0; +} + +int main(void) +{ + int failures = 0; + + akerr_init(); + + AKSL_RUN(failures, test_prepend_moves_the_head); + AKSL_RUN(failures, test_insert_after_and_before); + AKSL_RUN(failures, test_length_counts_and_refuses_cycles); + AKSL_RUN(failures, test_find_returns_the_first_match_or_null); + AKSL_RUN(failures, test_reverse_flips_both_directions); + AKSL_RUN(failures, test_concat_joins_two_lists); + AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head); + AKSL_RUN(failures, test_free_all_releases_every_node); + + AKSL_RUN(failures, test_container_push_and_unshift); + AKSL_RUN(failures, test_container_remove_maintains_the_endpoints); + AKSL_RUN(failures, test_container_clear); + + AKSL_RUN(failures, test_tree_insert_orders_the_leaves); + AKSL_RUN(failures, test_tree_insert_sets_the_parent_links); + AKSL_RUN(failures, test_tree_find); + AKSL_RUN(failures, test_tree_remove_all_three_cases); + AKSL_RUN(failures, test_tree_remove_the_only_node); + AKSL_RUN(failures, test_tree_height_and_count); + AKSL_RUN(failures, test_tree_free_all); + + AKSL_REPORT(failures); +} diff --git a/tests/test_hashmap.c b/tests/test_hashmap.c new file mode 100644 index 0000000..bd61c76 --- /dev/null +++ b/tests/test_hashmap.c @@ -0,0 +1,417 @@ +/* + * The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6. + * + * "The single most obviously-missing data structure in the library", by the + * TODO's own account: akbasic needed one three times over -- variables, + * functions and labels -- and wrote ~130 lines of open-addressed table to get it. + * + * The behaviour worth testing hardest is the tombstone. Deleting a key that + * something else probed past has to leave a marker rather than an empty slot, + * because an empty slot ends a probe chain and would make the later key + * unreachable while it is still sitting there. + */ + +#include "aksl_capture.h" + +#define CAP 16 + +typedef struct SeenLog +{ + int count; + char keys[CAP][AKSL_HASHMAP_MAX_KEY]; + int break_at; +} SeenLog; + +static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data) +{ + SeenLog *log = NULL; + + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key"); + FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data"); + (void)value; + log = (SeenLog *)data; + if ( log->count < CAP ) { + snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key); + } + log->count += 1; + if ( log->break_at == log->count - 1 ) { + FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop"); + } + SUCCEED_RETURN(e); +} + +/* ---------------------------------------------------------------------- */ +/* FNV-1a */ +/* ---------------------------------------------------------------------- */ + +/* + * The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261, + * then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the + * vectors from the FNV reference material. + */ +static int test_fnv1a_known_answers(void) +{ + uint32_t h = 0; + + AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h)); + AKSL_CHECK(h == 2166136261u); + AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h)); + AKSL_CHECK(h == 0xe40c292cu); + AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h)); + AKSL_CHECK(h == 0xbf9cf968u); + + /* The NUL-terminated form agrees with the length-driven one. */ + AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h)); + AKSL_CHECK(h == 0xbf9cf968u); + + AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER); + return 0; +} + +/* High bytes are unsigned here for the same reason they are in djb2. */ +static int test_fnv1a_high_bit_bytes_are_unsigned(void) +{ + char buf[2] = { (char)0xff, (char)0xfe }; + uint32_t h = 0; + uint32_t expected = 2166136261u; + + expected = (expected ^ 0xffu) * 16777619u; + expected = (expected ^ 0xfeu) * 16777619u; + + AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h)); + AKSL_CHECK(h == expected); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Put and get */ +/* ---------------------------------------------------------------------- */ + +static int test_put_and_get(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + int a = 1; + int b = 2; + void *value = NULL; + int found = 99; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + AKSL_CHECK(map.count == 0); + AKSL_CHECK(map.capacity == CAP); + + AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a)); + AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b)); + AKSL_CHECK(map.count == 2); + + AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found)); + AKSL_CHECK(found == 1); + AKSL_CHECK(value == &a); + AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found)); + AKSL_CHECK(found == 1); + AKSL_CHECK(value == &b); + + /* A key that is not there is found = 0 and success, not an error: looking + * something up and not finding it is the ordinary case for a symbol table. */ + value = (void *)0x1; + AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found)); + AKSL_CHECK(found == 0); + AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */ + + /* NULL value out-param is allowed: sometimes you only want to know if it is there. */ + AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found)); + AKSL_CHECK(found == 1); + return 0; +} + +/* Putting an existing key replaces the value rather than adding a second entry. */ +static int test_put_replaces_an_existing_key(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + int a = 1; + int b = 2; + void *value = NULL; + int found = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a)); + AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b)); + AKSL_CHECK(map.count == 1); + + AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found)); + AKSL_CHECK(found == 1); + AKSL_CHECK(value == &b); + return 0; +} + +/* The map copies keys into its slots, so it owns them and cannot outlive them. */ +static int test_the_map_owns_its_keys(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + char key[16] = "transient"; + int a = 1; + int found = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a)); + + /* Scribble over the caller's copy of the key. */ + memset(key, 'Z', sizeof(key) - 1); + + AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found)); + AKSL_CHECK(found == 1); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Bounds */ +/* ---------------------------------------------------------------------- */ + +/* + * Full is an error naming the capacity, not a silent resize. A table that + * reallocates is a table whose entry pointers move underneath anything holding + * one; this one has a worst case you can state. + */ +static int test_a_full_map_refuses_rather_than_resizing(void) +{ + aksl_HashEntry slots[4]; + aksl_HashMap map; + char key[16]; + int i = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); + for ( i = 0; i < 4; i++ ) { + snprintf(key, sizeof(key), "k%d", i); + AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); + } + AKSL_CHECK(map.count == 4); + + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL), + AKERR_OUTOFBOUNDS, "does not resize"); + AKSL_CHECK(map.count == 4); + + /* Everything already in there is still reachable and still correct. */ + for ( i = 0; i < 4; i++ ) { + int found = 0; + snprintf(key, sizeof(key), "k%d", i); + AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found)); + AKSL_CHECK(found == 1); + } + return 0; +} + +/* + * A key too long for a slot is refused rather than truncated. Truncation would + * silently collide two different keys that share a prefix, which is the worst + * possible failure for a symbol table -- a lookup that returns the wrong thing. + */ +static int test_an_oversized_key_is_refused(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + char longkey[AKSL_HASHMAP_MAX_KEY + 8]; + int found = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + memset(longkey, 'k', sizeof(longkey) - 1); + longkey[sizeof(longkey) - 1] = '\0'; + + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL), + AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY"); + AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS); + AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS); + + /* One byte short of the limit is fine: it is a limit, not an off-by-one. */ + longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0'; + AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL)); + AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found)); + AKSL_CHECK(found == 1); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Removal and tombstones */ +/* ---------------------------------------------------------------------- */ + +static int test_remove(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + int a = 1; + int found = 99; + int removed = 99; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a)); + + AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed)); + AKSL_CHECK(removed == 1); + AKSL_CHECK(map.count == 0); + AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found)); + AKSL_CHECK(found == 0); + + /* Removing what is not there is success with removed = 0. */ + AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed)); + AKSL_CHECK(removed == 0); + /* The out-param is optional. */ + AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL)); + return 0; +} + +/* + * The tombstone case, which is the one an open-addressed table gets wrong. + * + * A tiny capacity forces collisions, so several keys share a probe chain. + * Deleting one from the middle of that chain must not cut the keys behind it + * loose: if the slot were simply emptied, the probe for a later key would stop + * there and report the key missing while it was still sitting in the table. + */ +static int test_removal_does_not_break_a_probe_chain(void) +{ + aksl_HashEntry slots[4]; + aksl_HashMap map; + char key[16]; + int found = 0; + int i = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); + for ( i = 0; i < 4; i++ ) { + snprintf(key, sizeof(key), "k%d", i); + AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); + } + + /* Take out each key in turn and confirm every remaining one is still found. */ + for ( i = 0; i < 4; i++ ) { + int j = 0; + snprintf(key, sizeof(key), "k%d", i); + AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL)); + for ( j = i + 1; j < 4; j++ ) { + snprintf(key, sizeof(key), "k%d", j); + AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found)); + AKSL_CHECK(found == 1); + } + } + AKSL_CHECK(map.count == 0); + return 0; +} + +/* A tombstone is reusable, so a table churned through repeatedly does not fill up. */ +static int test_tombstones_are_reused(void) +{ + aksl_HashEntry slots[4]; + aksl_HashMap map; + char key[16]; + int i = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); + /* Far more insert/remove cycles than there are slots. */ + for ( i = 0; i < 64; i++ ) { + snprintf(key, sizeof(key), "cycle%d", i); + AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); + AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL)); + } + AKSL_CHECK(map.count == 0); + /* Still usable afterwards. */ + AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL)); + AKSL_CHECK(map.count == 1); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Iteration */ +/* ---------------------------------------------------------------------- */ + +static int test_iterate_visits_every_live_entry(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + SeenLog log; + char key[16]; + int i = 0; + + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + for ( i = 0; i < 5; i++ ) { + snprintf(key, sizeof(key), "k%d", i); + AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); + } + /* One removed, so there is a tombstone for the walk to skip. */ + AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL)); + + memset((void *)&log, 0x00, sizeof(log)); + log.break_at = -1; + AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log)); + AKSL_CHECK(log.count == 4); + + /* The break stops it, as everywhere else in this library. */ + memset((void *)&log, 0x00, sizeof(log)); + log.break_at = 1; + AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log)); + AKSL_CHECK(log.count == 2); + return 0; +} + +static int test_rejects_null_and_uninitialised(void) +{ + aksl_HashEntry slots[CAP]; + aksl_HashMap map; + aksl_HashMap empty; + SeenLog log; + int found = 0; + + memset((void *)&empty, 0x00, sizeof(empty)); + memset((void *)&log, 0x00, sizeof(log)); + AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); + + AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE); + + AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER); + + /* A map that was never initialised is caught rather than dereferenced. */ + AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER); + return 0; +} + +int main(void) +{ + int failures = 0; + + akerr_init(); + + AKSL_RUN(failures, test_fnv1a_known_answers); + AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned); + + AKSL_RUN(failures, test_put_and_get); + AKSL_RUN(failures, test_put_replaces_an_existing_key); + AKSL_RUN(failures, test_the_map_owns_its_keys); + + AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing); + AKSL_RUN(failures, test_an_oversized_key_is_refused); + + AKSL_RUN(failures, test_remove); + AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain); + AKSL_RUN(failures, test_tombstones_are_reused); + + AKSL_RUN(failures, test_iterate_visits_every_live_entry); + AKSL_RUN(failures, test_rejects_null_and_uninitialised); + + AKSL_REPORT(failures); +} diff --git a/tests/test_strbuf.c b/tests/test_strbuf.c new file mode 100644 index 0000000..4e25ae2 --- /dev/null +++ b/tests/test_strbuf.c @@ -0,0 +1,258 @@ +/* + * 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); +} diff --git a/tests/test_streamio.c b/tests/test_streamio.c new file mode 100644 index 0000000..4aecb6b --- /dev/null +++ b/tests/test_streamio.c @@ -0,0 +1,610 @@ +/* + * Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1. + * + * tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers + * positioning, flushing, character and line I/O, stream state, and formatted + * input. + * + * The recurring assertion is the one the section exists for: where stdio folds + * "no more data" and "something broke" into a single sentinel -- EOF from + * fgetc, NULL from fgets, -1L from ftell -- the wrapper separates them, so + * AKERR_EOF ends a read loop and anything else is a fault. + */ + +#include "aksl_capture.h" + +#include +#include + +/* A temp file containing exactly `text`, opened for reading. */ +static int open_with_text(const char *text, char *path, size_t pathlen, FILE **fp) +{ + FILE *out = NULL; + + if ( aksl_temp_file(path, pathlen) != 0 ) { + return 1; + } + out = fopen(path, "w"); + if ( out == NULL ) { + return 1; + } + if ( text[0] != '\0' && fputs(text, out) == EOF ) { + fclose(out); + return 1; + } + if ( fclose(out) != 0 ) { + return 1; + } + *fp = fopen(path, "r"); + return (*fp == NULL) ? 1 : 0; +} + +/* ---------------------------------------------------------------------- */ +/* Positioning */ +/* ---------------------------------------------------------------------- */ + +static int test_seek_and_tell(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + long pos = -1; + int c = 0; + + AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_ftell(fp, &pos)); + AKSL_CHECK(pos == 0); + + AKSL_CHECK_OK(aksl_fseek(fp, 4, SEEK_SET)); + AKSL_CHECK_OK(aksl_ftell(fp, &pos)); + AKSL_CHECK(pos == 4); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == '4'); + + AKSL_CHECK_OK(aksl_fseek(fp, -1, SEEK_END)); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == '9'); + + /* rewind is the fseek whose failure rewind(3) throws away. */ + AKSL_CHECK_OK(aksl_rewind(fp)); + AKSL_CHECK_OK(aksl_ftell(fp, &pos)); + AKSL_CHECK(pos == 0); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_seeko_and_tello(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + off_t pos = -1; + + AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_fseeko(fp, 6, SEEK_SET)); + AKSL_CHECK_OK(aksl_ftello(fp, &pos)); + AKSL_CHECK(pos == 6); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_getpos_and_setpos(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + fpos_t saved; + int c = 0; + + AKSL_CHECK(open_with_text("abcdef", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_fseek(fp, 2, SEEK_SET)); + AKSL_CHECK_OK(aksl_fgetpos(fp, &saved)); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'c'); + AKSL_CHECK_OK(aksl_fsetpos(fp, &saved)); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'c'); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_positioning_rejects_null(void) +{ + long pos = 0; + off_t opos = 0; + fpos_t fpos; + + AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ftell(stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_rewind(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fseeko(NULL, 0, SEEK_SET), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ftello(NULL, &opos), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ftello(stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgetpos(NULL, &fpos), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgetpos(stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fsetpos(NULL, &fpos), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fsetpos(stdin, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* Seeking a pipe is ESPIPE, which is the failure rewind(3) would have hidden. */ +static int test_seek_on_a_pipe_reports_espipe(void) +{ + FILE *fp = popen("echo hello", "r"); + long pos = 0; + + AKSL_CHECK(fp != NULL); + AKSL_CHECK_STATUS(aksl_fseek(fp, 0, SEEK_SET), ESPIPE); + AKSL_CHECK_STATUS(aksl_ftell(fp, &pos), ESPIPE); + AKSL_CHECK_STATUS(aksl_rewind(fp), ESPIPE); + pclose(fp); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Character and line I/O */ +/* ---------------------------------------------------------------------- */ + +/* + * fgetc(3) returns EOF for both the end of the file and a read error. Split + * apart, a read loop needs no ferror/feof dance at the bottom of it. + */ +static int test_fgetc_separates_eof_from_error(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + int c = 0; + int seen = 0; + + AKSL_CHECK(open_with_text("ab", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'a'); + seen++; + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'b'); + seen++; + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fgetc(fp, &c), AKERR_EOF, "end of stream"); + AKSL_CHECK(seen == 2); + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + /* A stream with no read permission is the error half of the same sentinel. */ + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + AKSL_CHECK_OK(aksl_fopen(path, "w", &fp)); + AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), EBADF); + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_fgetc(NULL, &c), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgetc(stdin, NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_fputc_and_ungetc(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + int c = 0; + + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + AKSL_CHECK_OK(aksl_fopen(path, "w", &fp)); + AKSL_CHECK_OK(aksl_fputc('x', fp)); + AKSL_CHECK_OK(aksl_fputc('y', fp)); + AKSL_CHECK_OK(aksl_fclose(fp)); + + AKSL_CHECK_OK(aksl_fopen(path, "r", &fp)); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'x'); + /* Put it back and read it again. */ + AKSL_CHECK_OK(aksl_ungetc(c, fp)); + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK(c == 'x'); + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_fputc('x', NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ungetc('x', NULL), AKERR_NULLPOINTER); + return 0; +} + +static int test_fgets_and_fputs(void) +{ + char path[AKSL_TMP_MAX]; + char line[64]; + FILE *fp = NULL; + size_t len = 0; + + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + AKSL_CHECK_OK(aksl_fopen(path, "w", &fp)); + AKSL_CHECK_OK(aksl_fputs("first\n", fp)); + AKSL_CHECK_OK(aksl_fputs("second\n", fp)); + AKSL_CHECK_OK(aksl_fclose(fp)); + + AKSL_CHECK_OK(aksl_fopen(path, "r", &fp)); + AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len)); + AKSL_CHECK(strcmp(line, "first\n") == 0); + AKSL_CHECK(len == 6); + AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len)); + AKSL_CHECK(strcmp(line, "second\n") == 0); + /* The end of the input is AKERR_EOF, which is how the loop terminates. */ + AKSL_CHECK_STATUS(aksl_fgets(line, sizeof(line), fp, &len), AKERR_EOF); + AKSL_CHECK(len == 0); + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +/* + * A line longer than the buffer is a short read, not an error -- that is what + * fgets does and what a caller reading fixed chunks wants. The reported length + * is how the caller notices: a full buffer with no newline is exactly this case. + */ +static int test_fgets_reports_a_long_line_as_a_short_read(void) +{ + char path[AKSL_TMP_MAX]; + char line[4]; + FILE *fp = NULL; + size_t len = 0; + + AKSL_CHECK(open_with_text("abcdefgh\n", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len)); + AKSL_CHECK(len == 3); + AKSL_CHECK(strcmp(line, "abc") == 0); + AKSL_CHECK(line[len - 1] != '\n'); + /* The rest of the line is still there. */ + AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len)); + AKSL_CHECK(strcmp(line, "def") == 0); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_fgets(NULL, 4, stdin, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgets(line, 4, NULL, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgets(line, 4, stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fgets(line, 0, stdin, &len), AKERR_VALUE); + return 0; +} + +/* + * getline grows the buffer, so the caller starts with NULL/0 and frees once at + * the end -- not once per line. The reported length is what distinguishes an + * embedded NUL from the end of the line; strlen cannot. + */ +static int test_getline_grows_its_buffer(void) +{ + char path[AKSL_TMP_MAX]; + char *line = NULL; + size_t cap = 0; + size_t len = 0; + FILE *fp = NULL; + int lines = 0; + + AKSL_CHECK(open_with_text("short\na much longer line than the first one\n", + path, sizeof(path), &fp) == 0); + + while ( 1 ) { + akerr_ErrorContext *raised = aksl_getline(&line, &cap, fp, &len); + int status = aksl_take(raised); + if ( status == AKERR_EOF ) { + break; + } + AKSL_CHECK(status == 0); + AKSL_CHECK(len > 0); + AKSL_CHECK(line != NULL); + lines++; + } + AKSL_CHECK(lines == 2); + /* One buffer for the whole file, grown as needed. */ + AKSL_CHECK(cap >= 42); + AKSL_CHECK_OK(aksl_freep((void **)&line)); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_getdelim_splits_on_any_byte(void) +{ + char path[AKSL_TMP_MAX]; + char *field = NULL; + size_t cap = 0; + size_t len = 0; + FILE *fp = NULL; + int fields = 0; + + AKSL_CHECK(open_with_text("a:b:c", path, sizeof(path), &fp) == 0); + + while ( 1 ) { + akerr_ErrorContext *raised = aksl_getdelim(&field, &cap, ':', fp, &len); + int status = aksl_take(raised); + if ( status == AKERR_EOF ) { + break; + } + AKSL_CHECK(status == 0); + fields++; + } + AKSL_CHECK(fields == 3); + AKSL_CHECK_OK(aksl_freep((void **)&field)); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_getline(NULL, &cap, stdin, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_getline(&field, NULL, stdin, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_getline(&field, &cap, NULL, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_getline(&field, &cap, stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_getdelim(NULL, &cap, ':', stdin, &len), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_getdelim(&field, &cap, ':', stdin, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Stream state */ +/* ---------------------------------------------------------------------- */ + +static int test_stream_state(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + int flag = 99; + int fd = -1; + int c = 0; + + AKSL_CHECK(open_with_text("a", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_feof(fp, &flag)); + AKSL_CHECK(flag == 0); + AKSL_CHECK_OK(aksl_ferror(fp, &flag)); + AKSL_CHECK(flag == 0); + AKSL_CHECK_OK(aksl_fileno(fp, &fd)); + AKSL_CHECK(fd >= 0); + + /* Read past the end to set the EOF indicator, then clear it. */ + AKSL_CHECK_OK(aksl_fgetc(fp, &c)); + AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), AKERR_EOF); + AKSL_CHECK_OK(aksl_feof(fp, &flag)); + AKSL_CHECK(flag != 0); + AKSL_CHECK_OK(aksl_clearerr(fp)); + AKSL_CHECK_OK(aksl_feof(fp, &flag)); + AKSL_CHECK(flag == 0); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_feof(NULL, &flag), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_feof(stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ferror(NULL, &flag), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_ferror(stdin, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_clearerr(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fileno(NULL, &fd), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fileno(stdin, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Flushing, buffering and opening */ +/* ---------------------------------------------------------------------- */ + +static int test_fflush_and_setvbuf(void) +{ + char path[AKSL_TMP_MAX]; + char buffer[BUFSIZ]; + FILE *fp = NULL; + + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + AKSL_CHECK_OK(aksl_fopen(path, "w", &fp)); + AKSL_CHECK_OK(aksl_setvbuf(fp, buffer, _IOFBF, sizeof(buffer))); + AKSL_CHECK_OK(aksl_fputs("buffered", fp)); + AKSL_CHECK_OK(aksl_fflush(fp)); + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + /* NULL means "every output stream" and is not an error, unlike everywhere else. */ + AKSL_CHECK_OK(aksl_fflush(NULL)); + AKSL_CHECK_STATUS(aksl_setvbuf(NULL, NULL, _IONBF, 0), AKERR_NULLPOINTER); + return 0; +} + +static int test_tmpfile_fdopen_and_freopen(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + FILE *reopened = NULL; + int fd = -1; + size_t moved = 0; + + /* tmpfile: no name, gone when it is closed. */ + AKSL_CHECK_OK(aksl_tmpfile(&fp)); + AKSL_CHECK(fp != NULL); + AKSL_CHECK_OK(aksl_fwrite("x", 1, 1, fp, &moved)); + AKSL_CHECK_OK(aksl_fclose(fp)); + + /* fdopen: wrap a descriptor this test owns. */ + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + fd = open(path, O_RDONLY); + AKSL_CHECK(fd >= 0); + AKSL_CHECK_OK(aksl_fdopen(fd, "r", &fp)); + AKSL_CHECK_OK(aksl_fclose(fp)); /* closes fd too */ + + /* freopen: point an existing stream at a different file. */ + AKSL_CHECK_OK(aksl_fopen(path, "r", &fp)); + AKSL_CHECK_OK(aksl_freopen(path, "r", fp, &reopened)); + AKSL_CHECK(reopened != NULL); + AKSL_CHECK_OK(aksl_fclose(reopened)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_tmpfile(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fdopen(-1, "r", &fp), AKERR_VALUE); + AKSL_CHECK_STATUS(aksl_fdopen(0, NULL, &fp), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fdopen(0, "r", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", NULL, &fp), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_freopen("/tmp", NULL, stdin, &fp), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", stdin, NULL), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Formatted input */ +/* ---------------------------------------------------------------------- */ + +/* + * The reason the scanf wrappers take an expected count: scanf(3) returns how + * many conversions succeeded, and comparing that against the number written in + * the format string is a check every caller has to do by hand and eventually + * forgets. Forgetting leaves the unassigned arguments holding whatever they + * held before. + */ +static int test_sscanf_enforces_the_expected_count(void) +{ + int a = 0; + int b = 0; + int assigned = 0; + + AKSL_CHECK_OK(aksl_sscanf("12 34", "%d %d", 2, &assigned, &a, &b)); + AKSL_CHECK(assigned == 2); + AKSL_CHECK(a == 12 && b == 34); + + /* One conversion short: an error rather than a stale second variable. */ + a = 0; + b = 999; + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sscanf("12 xy", "%d %d", 2, &assigned, &a, &b), + AKERR_VALUE, "1 of 2 conversions"); + AKSL_CHECK(assigned == 1); + AKSL_CHECK(b == 999); + + /* expected 0 opts out and hands the count back for the caller to judge. */ + AKSL_CHECK_OK(aksl_sscanf("12 xy", "%d %d", 0, &assigned, &a, &b)); + AKSL_CHECK(assigned == 1); + + /* Nothing matched at all. */ + AKSL_CHECK_STATUS(aksl_sscanf("", "%d", 1, &assigned, &a), AKERR_VALUE); + + AKSL_CHECK_STATUS(aksl_sscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_sscanf("1", NULL, 1, &assigned), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_sscanf("1", "%d", 1, NULL, &a), AKERR_NULLPOINTER); + return 0; +} + +static int test_fscanf_reads_from_a_stream(void) +{ + char path[AKSL_TMP_MAX]; + FILE *fp = NULL; + int a = 0; + int b = 0; + int assigned = 0; + + AKSL_CHECK(open_with_text("7 8\n", path, sizeof(path), &fp) == 0); + + AKSL_CHECK_OK(aksl_fscanf(fp, "%d %d", 2, &assigned, &a, &b)); + AKSL_CHECK(assigned == 2); + AKSL_CHECK(a == 7 && b == 8); + + /* Nothing left: the end of the stream, not a conversion failure. */ + AKSL_CHECK_STATUS(aksl_fscanf(fp, "%d", 1, &assigned, &a), AKERR_EOF); + + AKSL_CHECK_OK(aksl_fclose(fp)); + AKSL_CHECK(unlink(path) == 0); + + AKSL_CHECK_STATUS(aksl_fscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fscanf(stdin, NULL, 1, &assigned), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fscanf(stdin, "%d", 1, NULL, &a), AKERR_NULLPOINTER); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Files */ +/* ---------------------------------------------------------------------- */ + +static int test_remove_and_rename(void) +{ + char path[AKSL_TMP_MAX]; + char moved[AKSL_TMP_MAX + 8]; + + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + AKSL_CHECK((size_t)snprintf(moved, sizeof(moved), "%s.moved", path) < sizeof(moved)); + + AKSL_CHECK_OK(aksl_rename(path, moved)); + AKSL_CHECK(access(path, F_OK) != 0); + AKSL_CHECK(access(moved, F_OK) == 0); + AKSL_CHECK_OK(aksl_remove(moved)); + AKSL_CHECK(access(moved, F_OK) != 0); + + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_remove("/nonexistent/aksl/file"), + ENOENT, "/nonexistent/aksl/file"); + AKSL_CHECK_STATUS(aksl_remove(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_rename(NULL, "b"), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_rename("a", NULL), AKERR_NULLPOINTER); + return 0; +} + +/* + * mkstemp and mkdtemp rewrite the template in place, so a template that is not + * a writable buffer of the right shape is a crash waiting to happen. The shape + * is checked here rather than left to the kernel. + */ +static int test_mkstemp_and_mkdtemp(void) +{ + char file_template[] = "/tmp/aksl_streamio_XXXXXX"; + char dir_template[] = "/tmp/aksl_streamio_dir_XXXXXX"; + char bad[] = "/tmp/aksl_no_placeholder"; + int fd = -1; + + AKSL_CHECK_OK(aksl_mkstemp(file_template, &fd)); + AKSL_CHECK(fd >= 0); + AKSL_CHECK(strcmp(file_template, "/tmp/aksl_streamio_XXXXXX") != 0); + AKSL_CHECK(close(fd) == 0); + AKSL_CHECK_OK(aksl_remove(file_template)); + + AKSL_CHECK_OK(aksl_mkdtemp(dir_template)); + AKSL_CHECK(access(dir_template, F_OK) == 0); + AKSL_CHECK(rmdir(dir_template) == 0); + + AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_mkstemp(bad, &fd), AKERR_VALUE, "six literal X"); + AKSL_CHECK(fd == -1); + AKSL_CHECK_STATUS(aksl_mkdtemp(bad), AKERR_VALUE); + AKSL_CHECK_STATUS(aksl_mkstemp(NULL, &fd), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_mkstemp(file_template, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_mkdtemp(NULL), AKERR_NULLPOINTER); + return 0; +} + +int main(void) +{ + int failures = 0; + + akerr_init(); + + AKSL_RUN(failures, test_seek_and_tell); + AKSL_RUN(failures, test_seeko_and_tello); + AKSL_RUN(failures, test_getpos_and_setpos); + AKSL_RUN(failures, test_positioning_rejects_null); + AKSL_RUN(failures, test_seek_on_a_pipe_reports_espipe); + + AKSL_RUN(failures, test_fgetc_separates_eof_from_error); + AKSL_RUN(failures, test_fputc_and_ungetc); + AKSL_RUN(failures, test_fgets_and_fputs); + AKSL_RUN(failures, test_fgets_reports_a_long_line_as_a_short_read); + AKSL_RUN(failures, test_getline_grows_its_buffer); + AKSL_RUN(failures, test_getdelim_splits_on_any_byte); + + AKSL_RUN(failures, test_stream_state); + AKSL_RUN(failures, test_fflush_and_setvbuf); + AKSL_RUN(failures, test_tmpfile_fdopen_and_freopen); + + AKSL_RUN(failures, test_sscanf_enforces_the_expected_count); + AKSL_RUN(failures, test_fscanf_reads_from_a_stream); + + AKSL_RUN(failures, test_remove_and_rename); + AKSL_RUN(failures, test_mkstemp_and_mkdtemp); + + AKSL_REPORT(failures); +} diff --git a/tests/test_string.c b/tests/test_string.c new file mode 100644 index 0000000..620ed4c --- /dev/null +++ b/tests/test_string.c @@ -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 + +/* ---------------------------------------------------------------------- */ +/* 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", ©)); + AKSL_CHECK(copy != NULL); + AKSL_CHECK(strcmp(copy, "duplicate me") == 0); + AKSL_CHECK_OK(aksl_freep((void **)©)); + AKSL_CHECK(copy == NULL); + + AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, ©)); + AKSL_CHECK(strcmp(copy, "duplicate") == 0); + AKSL_CHECK_OK(aksl_freep((void **)©)); + + /* n past the end of the string just copies the string. */ + AKSL_CHECK_OK(aksl_strndup("ab", 99, ©)); + AKSL_CHECK(strcmp(copy, "ab") == 0); + AKSL_CHECK_OK(aksl_freep((void **)©)); + + AKSL_CHECK_STATUS(aksl_strdup(NULL, ©), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, ©), 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); +}