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>
255 lines
13 KiB
C
255 lines
13 KiB
C
#ifndef _AKSTDLIB_H_
|
|
#define _AKSTDLIB_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
/*
|
|
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
|
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
|
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
|
* the library gained an soname -- so a translation unit that pairs this header
|
|
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
|
*
|
|
* libakerror publishes no version macro, so this feature-tests on
|
|
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
|
* version number that does not exist. Without the guard a stale installed header
|
|
* fails much further in, as a pile of unrelated errors inside src/stdlib.c.
|
|
*
|
|
* See deps/libakerror/UPGRADING.md.
|
|
*/
|
|
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
|
#error "libakstdlib requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
|
#endif
|
|
|
|
/*
|
|
* AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
|
|
* Generated by CMake from include/akstdlib_version.h.in, which is why there is
|
|
* no such file in the source tree -- it is configured into the build tree and
|
|
* installed alongside this header.
|
|
*/
|
|
#include <akstdlib_version.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;
|
|
struct aksl_ListNode *next;
|
|
struct aksl_ListNode *prev;
|
|
} aksl_ListNode;
|
|
|
|
typedef struct aksl_TreeNode {
|
|
struct aksl_TreeNode *parent;
|
|
struct aksl_TreeNode *left;
|
|
struct aksl_TreeNode *right;
|
|
void *leaf;
|
|
} aksl_TreeNode;
|
|
|
|
#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 */
|
|
#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);
|
|
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
|
|
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
|
|
|
|
/*
|
|
* Version of the shared library actually loaded, as opposed to the
|
|
* AKSL_VERSION_* macros above, which record what the caller was *compiled*
|
|
* against. The two differ exactly when a stale libakstdlib.so is on the loader
|
|
* path, which is the failure these entry points exist to name.
|
|
*
|
|
* All three pointers are required; this library treats a NULL out-param as a
|
|
* caller error rather than as "don't care" (so does aksl_free).
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch);
|
|
|
|
/*
|
|
* The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither
|
|
* can fail -- they return pointers to string literals in the library's own
|
|
* .rodata -- so they return the string directly rather than an error context,
|
|
* the same exception akerr_name_for_status() makes for the same reason.
|
|
*/
|
|
const char *aksl_version_string(void);
|
|
const char *aksl_version_soname(void);
|
|
|
|
/*
|
|
* Compare the caller's compiled-in version against the loaded library's.
|
|
* Compatibility is defined as "same soname", so pre-1.0 both major and minor
|
|
* must match and patch is ignored; from 1.0 only major will matter. Raises
|
|
* AKERR_VALUE naming both versions on a mismatch.
|
|
*
|
|
* Call it through AKSL_VERSION_CHECK() below rather than directly. The macro
|
|
* expands at *your* call site, so it captures the AKSL_VERSION_* the caller was
|
|
* built with; the function compares them against the values baked into the
|
|
* library. Passing the numbers by hand defeats the entire mechanism.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch);
|
|
|
|
#define AKSL_VERSION_CHECK() \
|
|
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
|
|
|
|
/*
|
|
* 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_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);
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/*
|
|
* 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);
|
|
/*
|
|
* `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_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
|