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

179 lines
7.2 KiB
Markdown

# 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.
```c
/* 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.**
```c
/* 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.**
```c
/* 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.**
```c
/* 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.**
```c
/* 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