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>
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_memchraksl_snprintf,aksl_vprintf,aksl_vfprintf,aksl_vsnprintfaksl_strtol,aksl_strtoll,aksl_strtoul,aksl_strtoull,aksl_strtod,aksl_strtof,aksl_strtold- the whole of
src/string.candsrc/stream.c aksl_list_prepend/insert_after/insert_before/length/find/reverse/concat/free_all/iterate_reverse, and theaksl_Listcontainer so append is O(1)aksl_tree_insert/find/remove/height/count/free_allaksl_hashmap_*,aksl_strbuf_*,aksl_strhash_fnv1aaksl_list_node_initandaksl_tree_node_init, so a caller no longer has to remember tomemseta node before its first use- the header is
extern "C"-guarded and pulls in four standard headers rather than six