Use the library's own error idioms inside the library
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:
@@ -130,6 +130,8 @@ set(AKERR_TESTS
|
||||
err_name_ownership
|
||||
err_registry_init_order
|
||||
err_status_exception
|
||||
err_copy_string
|
||||
err_library_status_fatal
|
||||
err_refcount_double_fail
|
||||
err_stacktrace_bounds
|
||||
err_name_bounds
|
||||
@@ -142,6 +144,7 @@ set(AKERR_TESTS
|
||||
set(AKERR_WILL_FAIL_TESTS
|
||||
err_trace
|
||||
err_improper_closure
|
||||
err_library_status_fatal
|
||||
)
|
||||
|
||||
foreach(_test IN LISTS AKERR_TESTS)
|
||||
|
||||
23
TODO.md
23
TODO.md
@@ -63,19 +63,26 @@ 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.
|
||||
|
||||
## 7. The library's own startup failure path is untested
|
||||
## 7. `akerr_init()`'s own reservation failure 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.
|
||||
`tests/err_library_status_fatal.c` covers the terminal path in
|
||||
`__akerr_name_library_status()` by naming a status the library does not own. The
|
||||
band reservation in `akerr_init()` has no such handle: it can only fail in a
|
||||
build whose tables are too small for the library's own entries, and both sizes
|
||||
are `PRIVATE` to the library target, so a test executable cannot set them.
|
||||
|
||||
Closing it means a second library target built with a tiny table plus a
|
||||
Closing it means a second library target built with tiny tables 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.
|
||||
|
||||
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
||||
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
||||
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when
|
||||
that specific failure fires, and every `PASS` site around a call that cannot
|
||||
fail carries about twenty-five. Validating more inputs therefore lowers the
|
||||
ratio by construction. Before adding defensive checks, expect to add a test that
|
||||
drives them, as `tests/err_copy_string.c` does.
|
||||
|
||||
## Unrelated pre-existing issues
|
||||
|
||||
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||
|
||||
@@ -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); \
|
||||
|
||||
|
||||
233
src/error.c
233
src/error.c
@@ -64,13 +64,32 @@ static int akerr_status_range_count;
|
||||
|
||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
static void akerr_copy_string(char *destination, int capacity, const char *source)
|
||||
/*
|
||||
* Bounded copy into a fixed buffer. Every argument is checked: this writes
|
||||
* through a caller-supplied pointer for a caller-supplied length, so a NULL or
|
||||
* a non-positive capacity here is a memory error waiting to happen, not
|
||||
* something to absorb and return from quietly.
|
||||
*
|
||||
* Exported under the internal __akerr_ prefix rather than kept static so that
|
||||
* tests/err_copy_string.c can reach these guards. Both in-library callers
|
||||
* validate their arguments first, so nothing else can drive them.
|
||||
*/
|
||||
akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
||||
const char *source)
|
||||
{
|
||||
if ( capacity <= 0 ) {
|
||||
return;
|
||||
}
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, (destination == NULL || source == NULL),
|
||||
AKERR_NULLPOINTER,
|
||||
"__akerr_copy_string got a NULL %s",
|
||||
destination == NULL ? "destination buffer" : "source string");
|
||||
FAIL_NONZERO_RETURN(errctx, (capacity <= 0), AKERR_VALUE,
|
||||
"__akerr_copy_string got a capacity of %d; a buffer must "
|
||||
"have room for at least a terminator", capacity);
|
||||
|
||||
strncpy(destination, source, (size_t)capacity - 1);
|
||||
destination[capacity - 1] = '\0';
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -107,27 +126,33 @@ void akerr_default_logger(const char *fmt, ...)
|
||||
}
|
||||
|
||||
/*
|
||||
* The library naming its own codes. The registry entry points raise errors like
|
||||
* everything else in the library, but akerr_init() has no caller to raise into,
|
||||
* so this is where the buck stops: log the failure and take the unhandled-error
|
||||
* path, which terminates the program.
|
||||
* The library naming its own codes.
|
||||
*
|
||||
* This is fatal on purpose. The library cannot name its own status codes only
|
||||
* if the build is misconfigured -- a name table too small to hold even the
|
||||
* library's own entries, or a reservation that did not take -- and the
|
||||
* akerr_init() returns void and runs before any consumer frame exists, so there
|
||||
* is nothing to PASS an error to: this *is* the top of the stack. FINISH_NORETURN
|
||||
* is the library's idiom for that position -- the same one main() uses -- so an
|
||||
* unhandled failure prints its stack trace and goes to
|
||||
* akerr_handler_unhandled_error, which terminates.
|
||||
*
|
||||
* That is fatal on purpose. The library can only fail to name its own status
|
||||
* codes if the build is misconfigured -- a name table too small to hold even
|
||||
* the library's own entries, or a reservation that did not take -- and the
|
||||
* consequence of continuing is every later stack trace in the process printing
|
||||
* "Unknown Error" for a code the library defines. That is a startup defect, and
|
||||
* it is far cheaper to see it at init than to debug it from a degraded trace.
|
||||
*
|
||||
* The generated errno table calls this rather than registering names directly,
|
||||
* so that all of this control flow lives here in one place.
|
||||
*/
|
||||
void __akerr_name_library_status(int status, const char *name)
|
||||
{
|
||||
akerr_ErrorContext *errctx = akerr_register_status_name(AKERR_LIBRARY_OWNER,
|
||||
status, name);
|
||||
if ( errctx != NULL ) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** libakerror could not name its own status **");
|
||||
akerr_handler_unhandled_error(errctx);
|
||||
RELEASE_ERROR(errctx);
|
||||
}
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_register_status_name(AKERR_LIBRARY_OWNER, status, name));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -161,19 +186,17 @@ void akerr_init()
|
||||
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. */
|
||||
akerr_ErrorContext *reserve_err =
|
||||
akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER);
|
||||
if ( reserve_err != NULL ) {
|
||||
/* Fatal for the same reason as __akerr_name_library_status():
|
||||
* without this band the library owns nothing, so none of the names
|
||||
* below can register either. */
|
||||
LOG_ERROR_WITH_MESSAGE(reserve_err,
|
||||
"** libakerror could not reserve its own status band **");
|
||||
akerr_handler_unhandled_error(reserve_err);
|
||||
RELEASE_ERROR(reserve_err);
|
||||
}
|
||||
* permitted inside a reserved range. Terminal for the same reason as
|
||||
* __akerr_name_library_status(), and handled the same way: without this
|
||||
* band the library owns nothing, so none of the names below could
|
||||
* register either. */
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
@@ -334,55 +357,59 @@ static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *ow
|
||||
akerr_StatusName *entry;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
if ( name == NULL ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
if ( owner != NULL && ( owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ) ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d (\"%s\"): the owner string is "
|
||||
"empty or longer than %d characters",
|
||||
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
||||
}
|
||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner == NULL ? "an unnamed caller" : owner);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(owner != NULL && ( owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH )),
|
||||
AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d (\"%s\"): the owner string "
|
||||
"is empty or longer than %d characters",
|
||||
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
||||
|
||||
range = akerr_range_for_status(status);
|
||||
if ( range == NULL ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_UNRESERVED,
|
||||
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
||||
"range contains it. Call akerr_reserve_status_range() first.",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
if ( owner != NULL && strcmp(owner, range->owner) != 0 ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_FOREIGN,
|
||||
"Refusing to name status %d (\"%s\") for %s: that status is "
|
||||
"in range %d..%d owned by %s.",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, range, AKERR_STATUS_NAME_UNRESERVED,
|
||||
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
||||
"range contains it. Call akerr_reserve_status_range() first.",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(owner != NULL && strcmp(owner, range->owner) != 0),
|
||||
AKERR_STATUS_NAME_FOREIGN,
|
||||
"Refusing to name status %d (\"%s\") for %s: that status "
|
||||
"is in range %d..%d owned by %s.",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
|
||||
entry = akerr_status_slot(status, 1);
|
||||
if ( entry == NULL ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_FULL,
|
||||
"Status name registry is full (%d entries); dropping name "
|
||||
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
||||
"AKERR_STATUS_NAME_SLOTS.",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
}
|
||||
akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name);
|
||||
FAIL_ZERO_RETURN(errctx, entry, AKERR_STATUS_NAME_FULL,
|
||||
"Status name registry is full (%d entries); dropping name "
|
||||
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
||||
"AKERR_STATUS_NAME_SLOTS.",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
|
||||
PASS(errctx, __akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* Register a name for a status inside a range the caller reserved. */
|
||||
/*
|
||||
* Register a name for a status inside a range the caller reserved. Both strings
|
||||
* are checked here rather than only inside akerr_store_status_name(): the store
|
||||
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
|
||||
* arriving through *this* entry point would be read as "caller did not identify
|
||||
* itself" and skip the ownership check entirely.
|
||||
*/
|
||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (owner != NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d: the owner string is NULL. "
|
||||
"Pass the same owner you reserved the range with.",
|
||||
status);
|
||||
FAIL_NONZERO_RETURN(errctx, (owner == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d: the owner string is NULL. "
|
||||
"Pass the same owner you reserved the range with.",
|
||||
status);
|
||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner);
|
||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -390,9 +417,15 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
|
||||
/*
|
||||
* Return or set a name. Status magnitude is unrelated to storage size.
|
||||
*
|
||||
* The set path is the legacy two-argument form and cannot hand an error back to
|
||||
* its caller (it returns a name), so a refusal is logged here and released.
|
||||
* The set path is the legacy two-argument form. It returns a name, so it cannot
|
||||
* hand an error back to its caller and cannot raise: it handles the refusal
|
||||
* here, converting it to the "Unknown Error" sentinel the way any function that
|
||||
* must return a value converts a caught error into one.
|
||||
* akerr_register_status_name() is the form that raises.
|
||||
*
|
||||
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
|
||||
* calls it to render a status into a stack trace, so it must not itself need an
|
||||
* error context.
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
@@ -400,10 +433,19 @@ char *akerr_name_for_status(int status, char *name)
|
||||
|
||||
akerr_init();
|
||||
if ( name != NULL ) {
|
||||
akerr_ErrorContext *errctx = akerr_store_status_name(NULL, status, name);
|
||||
if ( errctx != NULL ) {
|
||||
PREPARE_ERROR(errctx);
|
||||
int refused = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **");
|
||||
RELEASE_ERROR(errctx);
|
||||
refused = 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
if ( refused != 0 ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
@@ -420,17 +462,17 @@ akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, cons
|
||||
int last_status;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
if ( count <= 0 || owner == NULL || owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
||||
first_status > INT_MAX - (count - 1) ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_INVALID,
|
||||
"Invalid status range reservation: %d status values from "
|
||||
"%d for %s (count must be positive, the owner string "
|
||||
"non-empty and shorter than %d characters, and the range "
|
||||
"must not overflow int)",
|
||||
count, first_status, owner == NULL ? "(null)" : owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
||||
}
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(count <= 0 || owner == NULL || owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
||||
first_status > INT_MAX - (count - 1)),
|
||||
AKERR_STATUS_RANGE_INVALID,
|
||||
"Invalid status range reservation: %d status values from "
|
||||
"%d for %s (count must be positive, the owner string "
|
||||
"non-empty and shorter than %d characters, and the range "
|
||||
"must not overflow int)",
|
||||
count, first_status, owner == NULL ? "(null)" : owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
||||
last_status = first_status + count - 1;
|
||||
|
||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||
@@ -450,18 +492,21 @@ akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, cons
|
||||
akerr_status_ranges[i].owner);
|
||||
}
|
||||
}
|
||||
if ( akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES ) {
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_FULL,
|
||||
"Status range table is full (%d ranges); refusing %d..%d "
|
||||
"for %s.",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
}
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES),
|
||||
AKERR_STATUS_RANGE_FULL,
|
||||
"Status range table is full (%d ranges); refusing %d..%d "
|
||||
"for %s.",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
|
||||
/* The owner copy is what commits the entry, so the count only advances
|
||||
* once it has succeeded -- a half-written reservation would claim the
|
||||
* range under an empty owner nobody could ever match. */
|
||||
akerr_status_ranges[akerr_status_range_count].first = first_status;
|
||||
akerr_status_ranges[akerr_status_range_count].last = last_status;
|
||||
akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner);
|
||||
PASS(errctx, __akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner));
|
||||
akerr_status_range_count++;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
58
tests/err_copy_string.c
Normal file
58
tests/err_copy_string.c
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* __akerr_copy_string() is the only place in the library that writes through a
|
||||
* caller-supplied pointer for a caller-supplied length, so it validates every
|
||||
* argument and raises rather than returning quietly. Both in-library callers
|
||||
* check their arguments before calling it, so this test is what drives those
|
||||
* guards -- without it they are unreachable code that no build ever exercises.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char buf[8];
|
||||
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
/* The happy path: bounded, and always terminated. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abc"));
|
||||
AKERR_CHECK(strcmp(buf, "abc") == 0);
|
||||
|
||||
/* A source longer than the buffer is truncated, never overrun. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abcdefghijkl"));
|
||||
AKERR_CHECK(strlen(buf) == sizeof(buf) - 1);
|
||||
AKERR_CHECK(buf[sizeof(buf) - 1] == '\0');
|
||||
AKERR_CHECK(strcmp(buf, "abcdefg") == 0);
|
||||
|
||||
/* A capacity of exactly one holds nothing but the terminator. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, 1, "abc"));
|
||||
AKERR_CHECK(buf[0] == '\0');
|
||||
AKERR_CHECK(buf[1] == 'x'); /* and wrote nothing past its capacity */
|
||||
|
||||
/* NULL pointers raise instead of faulting, and the message says which. */
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(NULL, (int)sizeof(buf), "abc"),
|
||||
AKERR_NULLPOINTER);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("destination");
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, (int)sizeof(buf), NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("source");
|
||||
|
||||
/* A capacity with no room for a terminator is a value error, not a write. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, 0, "abc"), AKERR_VALUE);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("capacity of 0");
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, -1, "abc"), AKERR_VALUE);
|
||||
AKERR_CHECK(buf[0] == 'x'); /* nothing was written */
|
||||
|
||||
/* Each refusal handed its context back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_copy_string ok\n");
|
||||
return 0;
|
||||
}
|
||||
31
tests/err_library_status_fatal.c
Normal file
31
tests/err_library_status_fatal.c
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
|
||||
/*
|
||||
* The library naming one of its own codes is not allowed to fail quietly.
|
||||
* __akerr_name_library_status() runs from akerr_init() and from the generated
|
||||
* errno table, neither of which has a caller to raise into, so a refusal there
|
||||
* goes through FINISH_NORETURN: stack trace, then akerr_handler_unhandled_error,
|
||||
* which terminates the process.
|
||||
*
|
||||
* In a correct build that can only happen with a name table too small to hold
|
||||
* the library's own entries, which no test can configure (the slot count is
|
||||
* PRIVATE to the library target). Calling the helper for a status the library
|
||||
* does not own reaches the same refusal, so this test covers the terminal path
|
||||
* itself.
|
||||
*
|
||||
* Registered in AKERR_WILL_FAIL_TESTS: reaching the end of main() means the
|
||||
* failure was swallowed, and that is the bug this test exists to catch.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
|
||||
/* Nobody has reserved 9999, so this registration is refused. */
|
||||
__akerr_name_library_status(9999, "Not The Library's To Name");
|
||||
|
||||
fprintf(stderr, "err_library_status_fatal: a refused library-status "
|
||||
"registration did NOT terminate\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user