Use the library's own error idioms inside the library
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 16m9s

Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.

akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.

__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.

Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.

New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).

Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 21:53:33 -04:00
parent 64da04e83b
commit 5ff87908e7
6 changed files with 255 additions and 102 deletions

View File

@@ -149,6 +149,15 @@ void akerr_init_errno(void);
*/
void __akerr_name_library_status(int status, const char *name);
/*
* Internal. Bounded string copy into a fixed buffer, always NUL-terminated.
* Raises AKERR_NULLPOINTER for a NULL destination or source and AKERR_VALUE for
* a capacity that leaves no room for a terminator. Exported so the library's
* own tests can drive those guards; not part of the consumer API.
*/
akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int capacity,
const char *source);
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \