project() now carries VERSION 0.1.0, and is the single place a version number is spelled. It flows into generated version macros, the shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried no soname at all. 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose fixes change documented behaviour, so the API is not being promised yet. While the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt and the #if in tests/test_version.c encode that rule and are tested against each other. include/akstdlib_version.h.in is configured into the build tree as akstdlib_version.h and installed beside akstdlib.h. It defines AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME. AKSL_VERSION_NUMBER is computed rather than written as a literal, because a literal 000100 is octal in C and would make 0.1.0 compare as 64; test_version.c asserts it against the runtime components, so a rewrite to a literal fails. Those macros record what a caller was compiled against. aksl_version(), aksl_version_string() and aksl_version_soname() report what actually loaded, and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both. It is a macro so that it expands at the caller's site and captures the caller's numbers; the function compares them against the ones baked into the library. Compatibility is "same soname", so patch is ignored -- a caller built against 0.1.0 keeps working against 0.1.7. Normally the soname catches a mismatch at load time and the check never fires. It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under the 0.1 filename loads happily, and only the check notices. write_basic_package_version_file() uses SameMinorVersion to mirror the soname, falling back to ExactVersion below CMake 3.11 where that mode does not exist. The fallback is stricter than the soname rule -- it pins the patch level too -- but never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad one. Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches and 25/25 functions. That puts branch coverage back over the old 45 gate, but the gate stays at 40: 0.1 points of headroom is not a ratchet. ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config --modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and 1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0 dropped in under the 0.1 filename caught by it, and an embedded add_subdirectory build keeping its own version rather than the parent's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
402 lines
14 KiB
C
402 lines
14 KiB
C
#include <akstdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
|
|
/*
|
|
* Version of the library itself. These read the AKSL_VERSION_* macros as they
|
|
* were when *this translation unit* was compiled, which is what makes them the
|
|
* loaded library's version rather than the caller's: the caller's copy of the
|
|
* same macros comes from whatever akstdlib_version.h it compiled against.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, major, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
|
(void *)major, (void *)minor, (void *)patch);
|
|
FAIL_ZERO_RETURN(e, minor, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
|
(void *)major, (void *)minor, (void *)patch);
|
|
FAIL_ZERO_RETURN(e, patch, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
|
(void *)major, (void *)minor, (void *)patch);
|
|
*major = AKSL_VERSION_MAJOR;
|
|
*minor = AKSL_VERSION_MINOR;
|
|
*patch = AKSL_VERSION_PATCH;
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
const char *aksl_version_string(void)
|
|
{
|
|
return AKSL_VERSION_STRING;
|
|
}
|
|
|
|
const char *aksl_version_soname(void)
|
|
{
|
|
return AKSL_VERSION_SONAME;
|
|
}
|
|
|
|
/*
|
|
* Compatibility is "same soname". Pre-1.0 that is MAJOR.MINOR, so both are
|
|
* compared and patch is deliberately ignored; from 1.0 the minor comparison
|
|
* comes out, in step with the SOVERSION expression in CMakeLists.txt. The
|
|
* caller's numbers arrive as arguments because AKSL_VERSION_CHECK() expanded
|
|
* them at the caller's site -- see the macro in akstdlib.h.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
(void)patch;
|
|
FAIL_NONZERO_RETURN(e,
|
|
(major != AKSL_VERSION_MAJOR || minor != AKSL_VERSION_MINOR),
|
|
AKERR_VALUE,
|
|
"compiled against libakstdlib %d.%d.%d, loaded %s (soname %s)",
|
|
major, minor, patch,
|
|
AKSL_VERSION_STRING, AKSL_VERSION_SONAME);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL");
|
|
*dst = malloc(size);
|
|
FAIL_ZERO_RETURN(e, *dst, errno, "%ld bytes", size);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "NULL");
|
|
free(ptr);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p", s);
|
|
FAIL_ZERO_RETURN(e, memset(s, c, n), errno, "Failed to memset");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, void *s, size_t n)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, d, AKERR_NULLPOINTER, "d=%p, s=%p", d, s);
|
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "d=%p, s=%p", d, s);
|
|
FAIL_ZERO_RETURN(e, (memcpy(d, s, n) == d), errno, "Failed to memcpy");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
|
|
char *pathname,
|
|
char *mode,
|
|
FILE **fp)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL");
|
|
*fp = fopen(pathname, mode);
|
|
FAIL_ZERO_RETURN(e, *fp, errno, "%s", pathname);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(
|
|
void *ptr,
|
|
size_t size, size_t nmemb,
|
|
FILE *fp)
|
|
{
|
|
size_t nmemr;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL");
|
|
nmemr = fread(ptr, size, nmemb, fp);
|
|
if ( nmemr != nmemb ) {
|
|
FAIL_NONZERO_RETURN(e, feof(fp), AKERR_EOF, "EOF reached");
|
|
FAIL_NONZERO_RETURN(e, ferror(fp), AKERR_IO, "Error reading file");
|
|
}
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(
|
|
void *ptr,
|
|
size_t size, size_t nmemb,
|
|
FILE *fp)
|
|
{
|
|
size_t nmemw;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL argument");
|
|
nmemw = fwrite(ptr, size, nmemb, fp);
|
|
if ( nmemw != nmemb ) {
|
|
FAIL_NONZERO_RETURN(e, feof(fp), AKERR_EOF, "EOF reached");
|
|
FAIL_NONZERO_RETURN(e, ferror(fp), AKERR_IO, "Error reading file");
|
|
}
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "NULL");
|
|
FAIL_NONZERO_RETURN(e, fclose(stream), errno, "Failed to fclose");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...)
|
|
{
|
|
va_list args;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format);
|
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format);
|
|
va_start(args, format);
|
|
*count = vprintf(format, args);
|
|
FAIL_NONZERO_RETURN(e, (*count == -1), errno, "Short write");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...)
|
|
{
|
|
va_list args;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
|
va_start(args, format);
|
|
*count = vfprintf(stream, format, args);
|
|
FAIL_NONZERO_RETURN(e, (*count == -1), errno, "Short write");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_sprintf(int *count, char *restrict str, const char *restrict format, ...)
|
|
{
|
|
va_list args;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
|
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
|
va_start(args, format);
|
|
*count = vsprintf(str, format, args);
|
|
FAIL_NONZERO_RETURN(e, (*count == -1), errno, "Short write");
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoi(const char *nptr, int *dest)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
*dest = atoi(nptr);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_atol(const char *nptr, long *dest)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
*dest = atol(nptr);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoll(const char *nptr, long long *dest)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
*dest = atoll(nptr);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest);
|
|
*dest = atof(nptr);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path)
|
|
{
|
|
char *result = NULL;
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)resolved_path);
|
|
result = realpath(path, resolved_path);
|
|
FAIL_ZERO_RETURN(e, result, errno, "path=%s, dest=%s", path, resolved_path);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(char *str, size_t len, uint32_t *hashval)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str");
|
|
FAIL_ZERO_RETURN(e, hashval, AKERR_NULLPOINTER, "hashval");
|
|
uint32_t h = 5381;
|
|
while (len--) {
|
|
h = ((h << 5) + h) + *str++;
|
|
}
|
|
*hashval = h;
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
|
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "obj");
|
|
aksl_ListNode *slow = list;
|
|
aksl_ListNode *fast = list;
|
|
aksl_ListNode *tail = list;
|
|
while ( fast != NULL && fast->next != NULL ) {
|
|
tail = slow;
|
|
slow = slow->next;
|
|
fast = fast->next->next;
|
|
if ( fast == slow) {
|
|
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
|
}
|
|
}
|
|
if ( fast != NULL ) {
|
|
tail = fast;
|
|
}
|
|
tail->next = obj;
|
|
obj->next = NULL;
|
|
obj->prev = tail;
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode *node)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
if ( node->prev != NULL ) {
|
|
node->prev->next = node->next;
|
|
}
|
|
if ( node->next != NULL ) {
|
|
node->next->prev = node->prev;
|
|
}
|
|
node->next = NULL;
|
|
node->prev = NULL;
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief Iterates over a tree structure in breadth or depth first order, executing a callback function on each node.
|
|
*
|
|
* This function recursively calls itself to traverse a binary tree datastructure.
|
|
*
|
|
* @param[in] root The tree structure to search
|
|
* @param[in] iter An aksl_TreeNodeIterator function which will be called for each node found
|
|
* @param[in] lalloc An aksl_AllocFunc function which will be used to allocate aksl_ListNode elements for the search, or NULL to use the default allocator (aksl_malloc)
|
|
* @param[in] lfree An aksl_FreeFunc function which will be used to free the elements procured with lalloc, or NULL to use the default free function (aksl_free)
|
|
* @param[in] searchmode One of the AKSL_TREE_SEARCH_BFS* defines
|
|
* @param[in] data Any user data that should be provided when the iterator is called
|
|
* @param[in] queue The linked list node to use as the head of the queue. The caller should pass NULL here.
|
|
*
|
|
* @throws AKERR_NULLPOINTER On null pointer inputs
|
|
* @return akerr_ErrorContext
|
|
*/
|
|
|
|
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)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root");
|
|
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
|
if ( lalloc == NULL ) {
|
|
lalloc = &aksl_malloc;
|
|
}
|
|
if ( lfree == NULL ) {
|
|
lfree = &aksl_free;
|
|
}
|
|
ATTEMPT {
|
|
switch ( searchmode ) {
|
|
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
|
CATCH(e, iter(root, data));
|
|
if ( root->left != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
if ( root-> right != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
break;
|
|
case AKSL_TREE_SEARCH_DFS_POSTORDER:
|
|
if ( root->left != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
if ( root-> right != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
CATCH(e, iter(root, data));
|
|
break;
|
|
case AKSL_TREE_SEARCH_DFS_INORDER:
|
|
if ( root->left != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
CATCH(e, iter(root, data));
|
|
if ( root-> right != NULL ) {
|
|
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
|
}
|
|
break;
|
|
case AKSL_TREE_SEARCH_BFS:
|
|
case AKSL_TREE_SEARCH_BFS_RIGHT:
|
|
FAIL_RETURN(e, AKERR_NOT_IMPLEMENTED, "Searchmode %d", searchmode);
|
|
break;
|
|
}
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} HANDLE(e, AKERR_ITERATOR_BREAK) {
|
|
// This is not an error condition, it's just telling us to stop early
|
|
SUCCEED_RETURN(e);
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief Iterates over a linked list and execute a callback function on each node
|
|
*
|
|
* @param[in] list The linked list to iterate
|
|
* @param[in] iter An aksl_ListNodeIterator function which will be called for each node found
|
|
* @param[in] data Any user data that should be provided when the iterator is called
|
|
*
|
|
* @throws AKERR_NULLPOINTER on null pointer inputs
|
|
* @throws AKERR_CIRCULAR_REFERENCE when the linked list contains a circular reference
|
|
*
|
|
* @return akerr_ErrorContext
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
|
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
|
aksl_ListNode *slow = list;
|
|
aksl_ListNode *fast = list;
|
|
while ( fast != NULL && fast->next != NULL ) {
|
|
slow = slow->next;
|
|
fast = fast->next->next;
|
|
if ( fast == slow) {
|
|
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
|
}
|
|
}
|
|
while ( slow != NULL ) {
|
|
ATTEMPT {
|
|
CATCH(e, iter(slow, data));
|
|
slow = slow->next;
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} HANDLE(e, AKERR_ITERATOR_BREAK) {
|
|
// This is not an error condition, it's just telling us to stop early
|
|
SUCCEED_RETURN(e);
|
|
} FINISH(e, true);
|
|
}
|
|
SUCCEED_RETURN(e);
|
|
}
|