Files
libakerror/tests/err_name_ownership.c

127 lines
5.9 KiB
C
Raw Normal View History

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
#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* Reserving a range used to be pure bookkeeping: akerr_name_for_status() would
* name any status for any caller, so two components could still register names
* for the same code -- and HANDLE the same code -- with nothing detecting it.
* Reservation only caught components that both opted in AND declared ranges
* that happened to overlap.
*
* Naming a status is now permitted only inside a reservation:
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
* - akerr_register_status_name() requires the range to belong to the caller,
* and raises AKERR_STATUS_NAME_* when it does not;
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
* - the legacy two-argument akerr_name_for_status() set path cannot identify
* its caller, so it can only require that *some* reservation covers 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
* status -- still enough to stop a code nobody claimed. It returns a name
* rather than an error context, so its refusals are logged instead.
* Either way the refusal is visible, because a name that fails to register
* degrades the status to "Unknown Error" in every later stack trace.
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
*/
int main(void)
{
akerr_capture_install();
akerr_init();
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
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "lib-a"));
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(512, 16, "lib-b"));
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
/* The owner of a range may name statuses inside it. */
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
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Parse Error"));
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 271, "A Last Error"));
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
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
/* Naming another owner's status is refused and names the real owner. */
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
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", 256, "B Hijack"),
AKERR_STATUS_NAME_FOREIGN);
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
AKERR_CHECK_MESSAGE_CONTAINS("lib-b");
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
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
/* Including the library's own reserved band. */
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
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value"),
AKERR_STATUS_NAME_FOREIGN);
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
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
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
/* A status nobody reserved cannot be named through either entry point. */
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
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 9999, "Unclaimed"),
AKERR_STATUS_NAME_UNRESERVED);
AKERR_CHECK_MESSAGE_CONTAINS("no reserved range");
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
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
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
/* The legacy path has no caller to raise into, so it logs the refusal.
* It also cannot name the caller, and must say so rather than printing a
* stray owner. */
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
akerr_capture_reset();
AKERR_CHECK(strcmp(akerr_name_for_status(9999, "Unclaimed Legacy"),
"Unknown Error") == 0);
AKERR_CHECK_CONTAINS("no reserved range");
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
AKERR_CHECK_CONTAINS("an unnamed caller");
AKERR_CHECK_CONTAINS("REFUSED STATUS NAME");
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
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
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
/* ... and hands the context it raised back to the pool. */
AKERR_CHECK(akerr_slots_in_use() == 0);
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
/* Boundaries: just outside lib-a's range is not lib-a's to name. */
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
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 255, "Below"),
AKERR_STATUS_NAME_FOREIGN);
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 272, "Above"),
AKERR_STATUS_NAME_UNRESERVED);
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
/* The legacy set path still works inside any reservation. */
AKERR_CHECK(strcmp(akerr_name_for_status(513, "B Legacy"), "B Legacy") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(513, NULL), "B Legacy") == 0);
/* Re-registering your own status overwrites the name. */
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
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Renamed"));
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
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Renamed") == 0);
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
/*
* Argument validation. Each message must identify the caller it refused,
* since that message is the whole report a consumer gets.
*/
AKERR_CHECK_RAISES(akerr_register_status_name(NULL, 257, "No Owner"),
AKERR_STATUS_NAME_INVALID);
AKERR_CHECK_MESSAGE_CONTAINS("257");
AKERR_CHECK_RAISES(akerr_register_status_name("", 257, "Empty Owner"),
AKERR_STATUS_NAME_INVALID);
AKERR_CHECK_MESSAGE_CONTAINS("Empty Owner");
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 257, NULL),
AKERR_STATUS_NAME_INVALID);
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
/* Over-long owner strings: 63 characters fit, 64 and beyond do not. */
char owner63[64];
char owner64[65];
char owner70[71];
memset(owner63, 'a', sizeof(owner63) - 1);
owner63[sizeof(owner63) - 1] = '\0';
memset(owner64, 'b', sizeof(owner64) - 1);
owner64[sizeof(owner64) - 1] = '\0';
memset(owner70, 'c', sizeof(owner70) - 1);
owner70[sizeof(owner70) - 1] = '\0';
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(600, 1, owner63));
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(owner63, 600, "Long Owner"));
AKERR_CHECK_RAISES(akerr_register_status_name(owner64, 600, "Too Long"),
AKERR_STATUS_NAME_INVALID);
AKERR_CHECK_MESSAGE_CONTAINS("63");
AKERR_CHECK_RAISES(akerr_register_status_name(owner70, 600, "Far Too Long"),
AKERR_STATUS_NAME_INVALID);
AKERR_CHECK(strcmp(akerr_name_for_status(600, NULL), "Long Owner") == 0);
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
/* A refused registration must not consume a slot or leave a partial entry. */
AKERR_CHECK(strcmp(akerr_name_for_status(257, NULL), "Unknown Error") == 0);
/* Lookup is unaffected by ownership -- anyone may read any name. */
AKERR_CHECK(strcmp(akerr_name_for_status(271, NULL), "A Last Error") == 0);
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
/* Every refusal above released its context. */
AKERR_CHECK(akerr_slots_in_use() == 0);
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
fprintf(stderr, "err_name_ownership ok\n");
return 0;
}