Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.
The defects:
2.1.1 aksl_list_append conflated Floyd cycle detection with finding
the tail, so `tail` tracked the node behind the midpoint. Any
append to a list of 2+ nodes silently dropped everything after
it. Two separate walks now: Floyd to prove the list is finite,
then a plain walk to the end.
2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor,
so the whole first half of the list -- head included -- was
never passed to the callback. It starts at the head.
2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
that raised it handled it and returned success, so the parent
carried on into the sibling subtree. The recursion is split out
and propagates the break; only the public entry swallows it.
2.1.4 va_end now matches every va_start on every path.
2.1.5 The ato* family had no error channel at all. Reimplemented over
a new strto* family with errno cleared, an endptr check and a
range check: AKERR_VALUE for junk, ERANGE for overflow.
2.1.6 aksl_realpath never checked resolved_path, could not be told
the buffer size, and formatted an unspecified buffer with %s on
its own error path. It takes a length; aksl_realpath_alloc is
the allocating form.
The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.
Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.
Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,10 +29,37 @@
|
||||
*/
|
||||
#include <akstdlib_version.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
/*
|
||||
* What this header needs in its own declarations, and no more:
|
||||
* stdio.h FILE
|
||||
* stddef.h size_t
|
||||
* stdint.h uint32_t
|
||||
* stdarg.h va_list, for the v* forms of the formatted-output wrappers
|
||||
*
|
||||
* It used to pull in stdlib.h and string.h as well, which nothing here needs and
|
||||
* which every consumer then got whether it wanted them or not. stddef.h in place
|
||||
* of stdlib.h is the same size_t at a fraction of the namespace. TODO.md 2.2.16.
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Restore the compile-time format/argument checking that callers otherwise lose
|
||||
* by going through a variadic wrapper: without it, printf("%d", "str") is caught
|
||||
* and aksl_printf(&n, "%d", "str") is not. TODO.md 2.2.5.
|
||||
*/
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \
|
||||
__attribute__((format(printf, __fmt_index, __first_arg)))
|
||||
#else
|
||||
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg)
|
||||
#endif
|
||||
|
||||
typedef struct aksl_ListNode {
|
||||
void *data;
|
||||
@@ -47,13 +74,27 @@ typedef struct aksl_TreeNode {
|
||||
void *leaf;
|
||||
} aksl_TreeNode;
|
||||
|
||||
#define AKSL_TREE_SEARCH_BFS 0 /** Breadth-first search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /** Right-hand breadth-first search mode for tree nodes. Currentl unsupported. */
|
||||
#define AKSL_TREE_SEARCH_BFS 0 /** Breadth-first (left child before right) search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /** Breadth-first, right child before left, search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_DFS 2 /** Alias for AKSL_TREE_SEARCH_DFS_PREORDER */
|
||||
#define AKSL_TREE_SEARCH_DFS_PREORDER 2 /** Depth first pre-order (root, left, right) search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_DFS_INORDER 3 /** Depth first in-order (left, root, right) search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /** Depth first post-order (left, right, root) search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_VISIT 5 /** Used when iterating through a tree structure as a control flag: don't traverse the children, just visit the node */
|
||||
#define AKSL_TREE_SEARCH_DFS_INORDER 3 /** Depth first in-order (left, root, right) search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /** Depth first post-order (left, right, root) search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_VISIT 5 /** Visit the node and stop; do not traverse the children */
|
||||
|
||||
/*
|
||||
* How deep aksl_tree_iterate will walk before raising AKERR_OUTOFBOUNDS.
|
||||
*
|
||||
* This is a real limit, not a formality: the depth-first walk is recursive, so
|
||||
* without it a degenerate chain overflows the stack and a child pointing back at
|
||||
* an ancestor recurses until the process dies. 256 is far past any balanced tree
|
||||
* that fits in memory (2^256 nodes) and comfortably short of a stack overflow,
|
||||
* so in practice it only ever rejects the degenerate shapes it exists to reject.
|
||||
*
|
||||
* It also sizes the ancestor buffer aksl_tree_iterate keeps on its own stack --
|
||||
* one pointer per level, so raising it costs 8 bytes of stack per level.
|
||||
*/
|
||||
#define AKSL_TREE_MAX_DEPTH 256
|
||||
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodeIterator)(aksl_ListNode *node, void *data);
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *node, void *data);
|
||||
@@ -96,35 +137,118 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int
|
||||
#define AKSL_VERSION_CHECK() \
|
||||
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(char *pathname, char *mode, FILE **fp);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(void *ptr, size_t size, size_t nmemb, FILE *fp);
|
||||
/*
|
||||
* Stream I/O.
|
||||
*
|
||||
* fread and fwrite take a required size_t *nmemb_out. It is always written --
|
||||
* including on the failure paths -- so a caller who gets AKERR_EOF can find out
|
||||
* how much data arrived before the stream ran out, which is the whole point of
|
||||
* distinguishing EOF from an error. A transfer that comes up short with no
|
||||
* stream error set is AKERR_IO, not silent success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(const char *pathname, const char *mode, FILE **fp);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, size_t *nmemb_out);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream);
|
||||
|
||||
/*
|
||||
* Memory. aksl_malloc and aksl_calloc reject a zero-size request outright rather
|
||||
* than reporting whatever errno happened to hold when malloc(0) returned NULL.
|
||||
* aksl_realloc takes the pointer by reference and leaves it valid and untouched
|
||||
* on failure, which is realloc(3)'s classic leak closed off.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, void *s, size_t n);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void **dst);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n);
|
||||
/* Overlapping ranges are AKERR_VALUE, not undefined behaviour; use aksl_memmove. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, const void *s, size_t n);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memmove(void *d, const void *s, size_t n);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcmp(const void *a, const void *b, size_t n, int *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, void **dest);
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_sprintf(int *count, char *restrict str, const char *restrict format, ...);
|
||||
/*
|
||||
* Formatted output. *count is the byte count written excluding the terminating
|
||||
* NUL, and is 0 on every failure path.
|
||||
*
|
||||
* There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an
|
||||
* error-handling wrapper around an unbounded write is the sharp edge this
|
||||
* library exists to remove. aksl_snprintf replaces it and treats truncation as
|
||||
* AKERR_OUTOFBOUNDS rather than as a short success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...) AKSL_PRINTF_FORMAT(2, 3);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(4, 5);
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stream, const char *restrict format, va_list args);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args);
|
||||
|
||||
/*
|
||||
* String to number.
|
||||
*
|
||||
* These are strict, unlike the libc functions they are named for:
|
||||
*
|
||||
* no digits consumed -> AKERR_VALUE
|
||||
* trailing junk, endptr NULL -> AKERR_VALUE
|
||||
* value outside the type -> ERANGE
|
||||
*
|
||||
* and *dest is 0 on every failure. Pass a non-NULL endptr to opt out of the
|
||||
* trailing-junk check and read a number off the front of a longer string.
|
||||
*
|
||||
* The ato* forms are aksl_strto* with base 10 and a NULL endptr, so the whole
|
||||
* string must be a number. "0x10" is AKERR_VALUE through aksl_atoi -- base 10
|
||||
* stops at the 'x' -- and 16 through aksl_strtol(nptr, NULL, 0, &dest).
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtol(const char *nptr, char **endptr, int base, long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoll(const char *nptr, char **endptr, int base, long long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoul(const char *nptr, char **endptr, int base, unsigned long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoull(const char *nptr, char **endptr, int base, unsigned long long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtod(const char *nptr, char **endptr, double *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtof(const char *nptr, char **endptr, float *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtold(const char *nptr, char **endptr, long double *dest);
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoi(const char *nptr, int *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_atol(const char *nptr, long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoll(const char *nptr, long long *dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest);
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path);
|
||||
/*
|
||||
* realpath(3). `buflen` must be at least PATH_MAX, because realpath(3) offers no
|
||||
* way to bound its own write -- an undersized buffer is AKERR_OUTOFBOUNDS rather
|
||||
* than a buffer overflow. aksl_realpath_alloc sizes and allocates the result
|
||||
* instead; *dest is then the caller's to release with aksl_free.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path, size_t buflen);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path, char **dest);
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(char *str, size_t len, uint32_t *hashval);
|
||||
/*
|
||||
* djb2. Length-driven, so embedded NUL bytes hash like any other byte; bytes
|
||||
* >= 0x80 are read as unsigned, so the result matches canonical djb2 and does
|
||||
* not depend on whether plain char is signed on the target.
|
||||
*/
|
||||
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);
|
||||
|
||||
// Linked list functions
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode *node);
|
||||
/*
|
||||
* `head` is required. Popping the head node has to move the caller's own head
|
||||
* pointer, and there is no way to do that from a node pointer alone -- the old
|
||||
* one-argument form left the caller aimed at a detached node.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data);
|
||||
|
||||
// Tree functions
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data, aksl_ListNode *queue);
|
||||
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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user