Files
libakstdlib/UPGRADING.md
Tachikoma 8a76b88648 Move outstanding work from TODO.md into the issue tracker
Twenty-six issues on source.starfort.tech/andrew/libakstdlib, labelled by kind
and blast radius and milestoned by what they can land in: 0.2.x for anything
that breaks no ABI, 0.3.0 for new public symbols, 1.0.0 for the large surfaces.
Everything filed carries status::grooming.

The four items blocked on libakerror are filed in both places -- the fix is
there and the bill is here -- and TODO.md keeps a table saying which is which.

What stays in TODO.md is what a tracker has no place for: where the library
stands, why six libc functions are deliberately not wrapped, which eight
uncovered lines are uncoverable rather than untested, and the akbasic call
counts that prioritised everything built since. 377 lines to 189.

Filed alongside: 25 file headers in tests/ and src/ cite TODO.md section
numbers from a numbering the file itself had already abandoned. They label
completed work, so nothing about the code is misleading -- only the pointer is.
Left for its own commit rather than churning 25 files here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 18:55:34 -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 was recorded in TODO.md before the move to the tracker — 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