Files
libakstdlib/UPGRADING.md
Andrew Kesterson 125eeb2109 Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.

The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.

Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.

Section 1.9, the cross-cutting tests:

  tests/test_pool.c    drives every failure path AKERR_MAX_ARRAY_ERROR
                       + 10 times and checks the pool after each round,
                       because a wrapper that leaks a slot fails a
                       hundred calls later in unrelated code. It also
                       asserts that each error names the function and
                       file it was raised from, which is what catches a
                       FAIL that migrates into a helper during a
                       refactor: status right, message right, origin
                       quietly lying.
  tests/negative/      two sources that must FAIL to compile, built with
                       -Werror and registered WILL_FAIL. AKERR_NOIGNORE
                       and the format attributes are enforced by the
                       compiler and by nothing else; drop either and
                       every ordinary test still passes.

Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.

Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.

CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.

Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00

7.2 KiB

Upgrading

0.1.0 → 0.2.0

This is an ABI break and a source break. Pre-1.0 the soname is MAJOR.MINOR, so libakstdlib.so.0.1 and libakstdlib.so.0.2 are different libraries as far as the loader is concerned and a consumer built against 0.1 will not silently pick this up. Rebuild against the new header.

AKSL_VERSION_CHECK() catches the pairing at runtime if a stale .so ever does end up on the path.

Everything below comes out of TODO.md sections 2.1 and 2.2 — the confirmed defects, all of which were reproduced against the 0.1.0 library before being fixed.

Behaviour changes with the same signature

aksl_atoi, aksl_atol, aksl_atoll, aksl_atof report bad conversions. This is the one to look at first, because the compiler will not tell you about it. They used to return success for anything: aksl_atoi("not a number", &n) gave you 0, and aksl_atoi("99999999999999999999", &n) gave you a wrapped value. Now:

input before after
"42" success, 42 success, 42
"not a number" success, 0 AKERR_VALUE
"" or " " success, 0 AKERR_VALUE
"12abc" success, 12 AKERR_VALUE
"0x10" success, 0 AKERR_VALUE (base 10 stops at the x)
"99999999999999999999" success, garbage ERANGE

*dest is 0 on every failure path. If you were relying on the old silence — treating an unparseable string as zero on purpose — that is now an error you have to handle or deliberately ignore.

For "0x10" and friends, use aksl_strtol(nptr, NULL, 0, &dest), which honours the 0x and 0 prefixes. The whole aksl_strto* family is new and is what the ato* forms are built on.

Errors no longer carry status 0. Any wrapper that reported errno could previously raise an error whose status was whatever errno happened to hold, including 0 — which every DETECT and CATCH downstream reads as success while the context still holds a pool slot. errno is now cleared before each wrapped call and read back through a fallback. If you were matching on a specific status, check it is still the one you get; several calls that used to report a flat AKERR_IO now report the real errno (a read from a write-only stream is EBADF, not AKERR_IO).

aksl_malloc(0, &p) is AKERR_VALUE. It used to depend on whether the platform's malloc(0) returned NULL.

aksl_memcpy refuses overlapping ranges with AKERR_VALUE. Use aksl_memmove.

aksl_list_append refuses a node already in the list with AKERR_VALUE. It used to relink it and orphan everything between its old position and the tail.

aksl_list_append and aksl_list_iterate are correct now. If you worked around either defect — appending one node at a time and fixing up the links by hand, or starting your own iteration from the head because the callback never saw it — that workaround is now wrong. append truncated any list of two or more nodes; iterate skipped everything before the list midpoint.

AKERR_ITERATOR_BREAK stops a tree traversal. It previously did not: the walk ran to completion regardless. Code that raised it and relied on the traversal continuing anyway (unlikely, but it was the behaviour) will now stop.

Tree traversal is bounded. A tree deeper than AKSL_TREE_MAX_DEPTH (256) is AKERR_OUTOFBOUNDS, and a depth-first walk over a tree whose child points back at an ancestor is AKERR_CIRCULAR_REFERENCE. Both used to run until the process died.

An unrecognised searchmode is AKERR_VALUE. It used to return success having visited nothing.

Signature changes

aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded.

/* before */
aksl_sprintf(&count, buf, "%s=%d", key, value);
/* after */
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);

Truncation is AKERR_OUTOFBOUNDS rather than a short success, and *count is 0 on any failure rather than vsprintf's -1.

aksl_realpath takes the destination's length.

/* before */
char resolved[PATH_MAX];
aksl_realpath(path, resolved);
/* after */
char resolved[PATH_MAX];
aksl_realpath(path, resolved, sizeof(resolved));

buflen must be at least PATH_MAX; realpath(3) cannot be bounded below it, so a shorter buffer is AKERR_OUTOFBOUNDS rather than an overflow. If you have no PATH_MAX-sized buffer to hand, aksl_realpath_alloc(path, &dest) allocates one and hands it over — release it with aksl_free.

aksl_fread and aksl_fwrite take a transferred-count out-param.

/* before */
aksl_fread(buf, 1, sizeof(buf), fp);
/* after */
size_t got = 0;
aksl_fread(buf, 1, sizeof(buf), fp, &got);

It is required, not optional, because it is the only way a caller who gets AKERR_EOF can find out how much data arrived. It is written on every path including the failure ones. A short transfer with neither EOF nor a stream error set is now AKERR_IO rather than success.

aksl_list_pop takes the head by reference.

/* before */
aksl_list_pop(node);
/* after */
aksl_list_pop(&head, node);   /* head is updated when node was the head */

Popping the head used to leave the caller's own head pointer aimed at a now-detached node with no way to learn the new one.

aksl_tree_iterate lost its queue parameter.

/* before */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data, NULL);
/* after */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data);

The doc comment told callers to pass NULL, which is a sign the queue belonged in an internal helper. lalloc and lfree stay, and they are actually used now — the breadth-first modes are implemented, where before they raised AKERR_NOT_IMPLEMENTED.

aksl_strhash_djb2 takes const char * and reads bytes as unsigned. Callers no longer have to cast away constness. The hash value changes for any input containing a byte ≥ 0x80: it now matches canonical djb2 instead of depending on whether plain char is signed on the target. 7-bit ASCII keys are unaffected. If you have persisted djb2 values across a restart, they will not match.

aksl_fopen takes const char * for both pathname and mode, and checks them — fopen(NULL, ...) is undefined behaviour and used to go straight through.

aksl_memcpy takes const void * for its source.

Additions

Nothing here breaks anything; see README.md for the full surface.

  • aksl_calloc, aksl_realloc, aksl_freep, aksl_memmove, aksl_memcmp, aksl_memchr
  • aksl_snprintf, aksl_vprintf, aksl_vfprintf, aksl_vsnprintf
  • aksl_strtol, aksl_strtoll, aksl_strtoul, aksl_strtoull, aksl_strtod, aksl_strtof, aksl_strtold
  • the whole of src/string.c and src/stream.c
  • aksl_list_prepend / insert_after / insert_before / length / find / reverse / concat / free_all / iterate_reverse, and the aksl_List container so append is O(1)
  • aksl_tree_insert / find / remove / height / count / free_all
  • aksl_hashmap_*, aksl_strbuf_*, aksl_strhash_fnv1a
  • aksl_list_node_init and aksl_tree_node_init, so a caller no longer has to remember to memset a node before its first use
  • the header is extern "C"-guarded and pulls in four standard headers rather than six