2026-07-30 13:22:50 -04:00
|
|
|
# TODO
|
|
|
|
|
|
2026-07-30 18:19:53 -04:00
|
|
|
Working notes for `libakerror`. Outstanding items only.
|
|
|
|
|
|
|
|
|
|
## 1. The test suite has no sanitizer run
|
|
|
|
|
|
|
|
|
|
Mutation testing caught an out-of-bounds probe in the status-name hash table
|
|
|
|
|
that the suite could not: the failure mode was a write into adjacent BSS, which
|
|
|
|
|
does not crash, so every test still passed. Sharpening one test closed that
|
|
|
|
|
instance, but ASan would have caught the whole class directly and independently
|
|
|
|
|
of how sharp the assertions are.
|
|
|
|
|
|
|
|
|
|
Add a `-fsanitize=address,undefined` build to `.gitea/workflows/ci.yaml`, or a
|
|
|
|
|
CMake option alongside `AKERR_COVERAGE`. This is the highest-value item here: it
|
|
|
|
|
covers the whole library, not just the registry, and the library's fixed pools
|
|
|
|
|
and manual buffer arithmetic are exactly what it is good at.
|
|
|
|
|
|
|
|
|
|
## 2. `HANDLE`-level status aliasing is still undetectable
|
|
|
|
|
|
|
|
|
|
Two components can compile the same integer into a `case` label without ever
|
|
|
|
|
reserving a range or registering a name, and nothing sees it. Ownership
|
|
|
|
|
enforcement covers *naming*, which is the part the library mediates; the `case`
|
|
|
|
|
label never reaches it.
|
|
|
|
|
|
|
|
|
|
Closing this needs the `if`/`else if` handler ladder — rewriting
|
|
|
|
|
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` so status matching
|
|
|
|
|
is not restricted to integer constant expressions. That would also allow
|
|
|
|
|
matching on ranges or predicates, and would let a handler resolve a code through
|
|
|
|
|
its owner. It touches the most load-bearing code in the library and every
|
|
|
|
|
consumer's error handling at once, so it wants its own change.
|
|
|
|
|
|
|
|
|
|
Note it would *not* by itself fix the "don't use `CATCH` or `FAIL_*_BREAK`
|
|
|
|
|
inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
|
|
|
|
|
|
|
|
|
|
## 3. No registry introspection
|
|
|
|
|
|
|
|
|
|
There is no way to ask who owns a status, or to enumerate reservations. The
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
|
|
|
|
|
therefore has no tooling behind it.
|
2026-07-30 18:19:53 -04:00
|
|
|
|
|
|
|
|
A read-only accessor plus a dump through `akerr_log_method` would let a startup
|
|
|
|
|
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
|
|
|
|
|
and the natural next step for multi-component adoption.
|
|
|
|
|
|
|
|
|
|
## 4. No way to release a reservation
|
|
|
|
|
|
|
|
|
|
A plugin host that `dlopen`s many distinct plugins over a process lifetime
|
|
|
|
|
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
|
|
|
|
an identical repeat by the same owner is idempotent.
|
|
|
|
|
|
|
|
|
|
## 5. The registry is not thread safe
|
|
|
|
|
|
|
|
|
|
Global mutable state, no locking, and `akerr_status_name_count++` is not atomic.
|
|
|
|
|
Currently documented as an initialization-time-only API rather than enforced. If
|
|
|
|
|
components start initializing on separate threads this needs either a lock or a
|
|
|
|
|
documented once-per-process init barrier.
|
|
|
|
|
|
|
|
|
|
## 6. Deprecate the two-argument name-registration path
|
|
|
|
|
|
|
|
|
|
`akerr_name_for_status(status, name)` cannot identify its caller, so it can only
|
|
|
|
|
check that *some* reservation covers the status, not that the caller owns it. It
|
|
|
|
|
exists for migration. Once consumers have moved to
|
|
|
|
|
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
|
|
|
|
|
`akerr_name_for_status()` as pure lookup.
|
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
|
|
|
|
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.
AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.
The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".
Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.
Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
|
|
|
## 7. The library's own startup failure path is untested
|
|
|
|
|
|
|
|
|
|
`__akerr_name_library_status()` and the band reservation in `akerr_init()` log
|
|
|
|
|
and then terminate when the library cannot name its own codes. Nothing exercises
|
|
|
|
|
that: it needs a build whose `AKERR_STATUS_NAME_SLOTS` is too small to hold the
|
|
|
|
|
library's own entries, and that macro is `PRIVATE` to the library target, so a
|
|
|
|
|
test executable cannot set it. Mutation testing reports those lines as surviving
|
|
|
|
|
mutants for this reason.
|
|
|
|
|
|
|
|
|
|
Closing it means a second library target built with a tiny table plus a
|
|
|
|
|
`WILL_FAIL` test linked against it — worth doing when the CMake gains a
|
|
|
|
|
sanitizer variant (item 1), since that adds the same machinery.
|
|
|
|
|
|
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
|
|
|
## Unrelated pre-existing issues
|
|
|
|
|
|
|
|
|
|
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
|
|
|
|
and `NULL` are used unconditionally but only included under the stdlib branch.
|
2026-07-30 18:19:53 -04:00
|
|
|
The README's dependency list states what a replacement must provide, but the
|
|
|
|
|
header still needs its includes untangled for that configuration to work.
|
|
|
|
|
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
|
|
|
|
defined and never read. Dead line.
|