Wrap the strings, streams and collections the wishlist asked for

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

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

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

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

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

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

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

View File

@@ -189,8 +189,15 @@ set(libdir "\${exec_prefix}/lib")
set(includedir "\${prefix}/include") set(includedir "\${prefix}/include")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY) 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 add_library(akstdlib SHARED
src/stdlib.c src/stdlib.c
src/string.c
src/stream.c
src/collections.c
) )
add_library(akstdlib::akstdlib ALIAS akstdlib) add_library(akstdlib::akstdlib ALIAS akstdlib)
@@ -288,14 +295,19 @@ install(FILES
# "unexpectedly passed" -- that is the cue to move # "unexpectedly passed" -- that is the cue to move
# it up into AKSL_TESTS. # it up into AKSL_TESTS.
set(AKSL_TESTS set(AKSL_TESTS
collections
convert convert
format format
hashmap
linkedlist linkedlist
memory memory
path path
status_registry status_registry
strbuf
stream stream
streamio
strhash strhash
string
strto strto
tree tree
version version

View File

@@ -44,6 +44,8 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
#include <sys/types.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -57,8 +59,11 @@ extern "C" {
#if defined(__GNUC__) || defined(__clang__) #if defined(__GNUC__) || defined(__clang__)
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \ #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \
__attribute__((format(printf, __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 #else
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg)
#define AKSL_SCANF_FORMAT(__fmt_index, __first_arg)
#endif #endif
typedef struct aksl_ListNode { typedef struct aksl_ListNode {
@@ -96,10 +101,26 @@ typedef struct aksl_TreeNode {
*/ */
#define AKSL_TREE_MAX_DEPTH 256 #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_ListNodeIterator)(aksl_ListNode *node, void *data);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *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_AllocFunc)(size_t size, void **dest);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr); 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 * 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(const char *str, size_t len, uint32_t *hashval);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, 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 // Linked list functions
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data); 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); 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_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); 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 #ifdef __cplusplus
} }
#endif #endif

25
src/aksl_internal.h Normal file
View File

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

1130
src/collections.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,19 +8,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
/* #include "aksl_internal.h"
* 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))
/* /*
* Version of the library itself. These read the AKSL_VERSION_* macros as they * Version of the library itself. These read the AKSL_VERSION_* macros as they

551
src/stream.c Normal file
View File

@@ -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 <akstdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#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);
}

486
src/string.c Normal file
View File

@@ -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 <akstdlib.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#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 <ctype.h> -- 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);
}

4
test.sh Normal file
View File

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

730
tests/test_collections.c Normal file
View File

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

417
tests/test_hashmap.c Normal file
View File

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

258
tests/test_strbuf.c Normal file
View File

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

610
tests/test_streamio.c Normal file
View File

@@ -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 <errno.h>
#include <fcntl.h>
/* 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);
}

448
tests/test_string.c Normal file
View File

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