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>
179 lines
7.2 KiB
Markdown
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 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.
|
|
|
|
```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
|