Add collision-safe status code registry

Replace the consumer-sized status-name array with private sparse storage and accept arbitrary integer status values. Add explicit range reservations with overlap diagnostics, reserve the library's 0-255 compatibility band, and harden pointer and string boundary handling.

Update regression coverage and document the required migration for custom status-code consumers.
This commit is contained in:
2026-07-30 13:53:39 -04:00
parent 0bb3a4d52c
commit 11d21068df
9 changed files with 250 additions and 260 deletions

View File

@@ -125,11 +125,6 @@ foreach(_test IN LISTS AKERR_TESTS)
add_test(NAME ${_test} COMMAND test_${_test})
endforeach()
# err_maxval parses the generated header to check AKERR_MAX_ERR_VALUE covers
# every AKERR_* code, so it needs the path to the header it was built against.
target_compile_definitions(test_err_maxval PRIVATE
AKERR_GENERATED_HEADER="${GENERATED_AKERROR_H}")
set_tests_properties(
${AKERR_WILL_FAIL_TESTS}
PROPERTIES WILL_FAIL TRUE

View File

@@ -4,6 +4,50 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
![build badge](https://source.starfort.tech/andrew/libakerror/actions/workflows/ci.yaml/badge.svg?branch=main)
## Upgrade notice: custom status codes
This version changes how custom status-code names and ownership are managed. Applications and libraries upgrading from an earlier version should review every custom error-code definition and initialization path.
To comply with the new status-code rules:
1. Rebuild libakerror and all dependent libraries against the new header.
2. Remove `AKERR_MAX_ERR_VALUE` compile definitions and source references. The status-name registry is now sparse and accepts any `int`; consumers no longer configure its size.
3. Move custom status codes out of the reserved `0` through `255` range. Use fixed integer constants beginning at `256`, rather than offsets from `AKERR_LAST_ERRNO_VALUE`, so libc changes cannot move your codes.
4. Assign a distinct range to every library that may coexist in one process. Coordinate these ranges at the application or dependency-stack level.
5. After `akerr_init()` and before raising or naming custom errors, reserve the complete range with `akerr_reserve_status_range()`. Treat every result other than `AKERR_STATUS_RANGE_OK` as an initialization failure.
6. Register human-readable names with `akerr_name_for_status()` as before. Every co-resident library must reserve its range for collision detection to be effective.
For example:
```c
enum {
MYLIB_ERR_BASE = 256,
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
MYLIB_ERR_STORAGE,
MYLIB_ERR_LIMIT
};
int mylib_init(void)
{
int result;
akerr_init();
result = akerr_reserve_status_range(
MYLIB_ERR_BASE,
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
"mylib");
if (result != AKERR_STATUS_RANGE_OK) {
return result;
}
akerr_name_for_status(MYLIB_ERR_PARSE, "Parse Error");
akerr_name_for_status(MYLIB_ERR_STORAGE, "Storage Error");
return AKERR_STATUS_RANGE_OK;
}
```
Repeating the exact same reservation for the same owner is safe. Overlapping ranges, invalid ranges, and exhausted reservation capacity are rejected. Code that directly referenced the former `__AKERR_ERROR_NAMES` data symbol used an undocumented interface and must migrate to `akerr_name_for_status()`.
# Why?
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
@@ -64,12 +108,22 @@ Any function which uses the `PREPARE_ERROR` macro should have a return type of `
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
You can define additional error types by defining additional `AKERR_xxxxx` values. Error values up to 255 are reserved by the library (`AKERR_xxxxx` begins where `errno` stops), so please begin your error values at 256. When you add additional error codes, you need to define `-DAKERR_MAX_ERR_VALUE=n` to the compiler, where `n` is the maximum error code you have defined. If you define custom error codes, `AKERR_MAX_ERR_VALUE` must be >= 256 or the compiler will throw an error.
You can define additional error types as integer constants. Values 0 through 255 are reserved by libakerror; consumers should allocate codes starting at 256. Status names are stored sparsely, so there is no maximum status value and no compiler definition is needed.
Define a human-friendly name for the error with the `akerr_name_for_status` method somewhere in your code's initialization before the error may be used:
Libraries that may coexist in one process should reserve their ranges during initialization and treat any nonzero result as a startup error:
```c
akerr_name_for_status(129, "Some Error Code Description")
if (akerr_reserve_status_range(256, 16, "my-library") != AKERR_STATUS_RANGE_OK) {
/* Refuse to initialize: another component owns part of the range. */
}
```
Reservations are process-local, fixed-capacity, and idempotent when the same owner repeats the exact same range. They detect declared range collisions while preserving compile-time integer constants for `HANDLE`. Call `akerr_init()` before reserving ranges. Existing custom codes below 256 should migrate into a declared range at or above 256.
Define a human-friendly name for the error with `akerr_name_for_status` during initialization before the error may be used:
```c
akerr_name_for_status(256, "Some Error Code Description")
```

146
TODO.md
View File

@@ -1,143 +1,11 @@
# TODO
Working notes for `libakerror`. Filed from the `akbasic` project, which is becoming the third
codebase to define custom status codes against this library (after `libakgl`; `libakstdlib` defines
none of its own). The items below are all facets of one question — **who owns the status-code number
space, and when is it decided** — but they are separable, and §1 can land on its own.
The status-code ownership work has been consumed. The implementation now:
Verification level is stated per item. Items marked **[read off source]** were established by reading
the library, its CMake, its `.pc.in` and its consumers; they have not been reproduced with a probe
program. Where a probe would settle a question, the item says so.
- stores status names in a private, fixed-capacity sparse registry, removing the consumer-visible array ABI and `AKERR_MAX_ERR_VALUE`;
- accepts names for arbitrary `int` status values and always terminates truncated names;
- reserves status values 0 through 255 for libakerror;
- provides `akerr_reserve_status_range()` so independently developed libraries can declare ranges and detect overlaps at initialization; and
- tests arbitrary status values, range overlap/idempotency/overflow, owner and name limits, and both registry capacity limits.
---
## 1. The public header makes the name-table size part of the consumer ABI
**[read off source — the corruption path is reasoned from the linker model, not reproduced]**
`include/akerror.tmpl.h:51` declares:
```c
extern char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
```
The object's real size comes from whatever `src/error.c` was compiled with. Every consumer's
*declaration* of it comes from whatever that consumer was compiled with. Nothing reconciles the two:
`akerror.pc.in` ships only `-I${includedir}`, `cmake/akerror.cmake.in` adds no compile definitions,
and a consumer that raises the value does so in its own build. `libakgl` raises it to 256
(`CMakeLists.txt:44`, `target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE=256)`), which
propagates correctly *only* inside a build tree that vendored this library via `add_subdirectory`.
So a consumer linking an installed `libakerror.so` can hold a different notion of the array's size
than the `.so` does, in either direction:
- **Consumer declares smaller than the library was built with.** For a shared-library data symbol
referenced by an executable, the linker emits a copy relocation sized by the *executable's*
declaration and redirects the library's references to it. `akerr_init()` (`src/error.c:56`) then
memsets `(AKERR_MAX_ERR_VALUE+1) * 64` bytes using the library's larger value, into storage sized by
the smaller one. That is a BSS overflow at startup. **This is the reasoned worst case and is worth
confirming before acting on it** — build the library with `-DAKERR_MAX_ERR_VALUE=1024`, install it,
build a one-file consumer with the default, and run it under ASan.
- **Consumer declares larger.** No overflow, but `akerr_name_for_status()` bounds-checks against the
value *`error.c`* was compiled with (`src/error.c:121`), so the consumer's high codes never register
and every stack trace reports `"Unknown Error"`.
The `#elif AKERR_MAX_ERR_VALUE < 256 / #error` guard at `include/akerror.tmpl.h:47` cannot catch
either case; it only rejects values below 256, never a disagreement between translation units.
**The cheap fix is to delete line 51.** Nothing outside `src/error.c` references
`__AKERR_ERROR_NAMES` — the only other mentions in the tree are two explanatory comments
(`tests/err_maxval.c:6`, `tests/err_name_bounds.c:8`), neither of which touches the symbol. Making the
table static to `error.c` and reaching it exclusively through `akerr_name_for_status()` takes the size
out of the consumer-visible ABI entirely and turns `AKERR_MAX_ERR_VALUE` into a private build setting
of this library. That is worth doing regardless of which direction §2 goes, and it does not change any
documented API.
A regression test would compile two objects with deliberately different `AKERR_MAX_ERR_VALUE` values
and link them; today that is silently accepted.
## 2. There is no authority that allocates custom status codes, and it is decided too early
**[read off source]**
Codes are compile-time constants expressed as offsets from `AKERR_LAST_ERRNO_VALUE`. This library owns
`+1``+17` (`AKERR_NULLPOINTER``AKERR_BADEXC`; `+7` is unused, apparently a removed code).
`libakgl` continues the same sequence in its own header at `+18``+22`
(`include/akgl/error.h`). Nothing arbitrates that. The next library to want a code reads
`akgl/error.h`, picks `+23`, and hopes.
Two consequences, both silent:
- **Aliasing.** `__AKERR_ERROR_NAMES` is a single global keyed by the integer, so two owners choosing
the same offset means last-registration-wins for the name, and — worse — `HANDLE(e, FOO_ERR_X)`
catching `BAR_ERR_Y`, since `HANDLE` compares the same integer. `akbasic` is intended to link
*into* `libakgl` as a scripting engine, so co-resident custom codes are the expected configuration,
not a corner case.
- **Drift.** Because the codes are relative to `AKERR_LAST_ERRNO_VALUE`, which `scripts/generrno.sh`
derives from the host's `errno --list`, adding an 18th `AKERR_*` code here silently aliases
`AKGL_ERR_SDL`, and a libc that grows an errno moves every downstream code.
There is also a documentation/practice split worth resolving either way: `README.md:67` says values up
to 255 are reserved and consumers must start at 256, but `libakgl`'s codes land around 151 on glibc —
inside the reserved band — and the `#error` guard's 256 floor implies the README's rule while nothing
enforces it. Note too that `AKGL_ERR_SDL` (`+18`) already exceeds this library's *default*
`AKERR_MAX_ERR_VALUE` (`+17`), which is why `libakgl` must raise it; any `libakgl` consumer that
doesn't inherit that definition loses all of akgl's error names.
### On the shape of a fix — options, not a prescription
A static registry of reserved bases in this header (`AKERR_ERR_BASE_AKGL`, …) was considered and
rejected: it would require the library to enumerate every application that will ever link it. The
allocation decision belongs to the consumer; what's missing is a way to *declare* it and have
conflicts detected. Some directions, roughly in order of invasiveness — the trade-offs are laid out
so the choice can be made here, not imposed from outside.
**(a) Make the name table sparse, and the number space unbounded.** The dense
`[AKERR_MAX_ERR_VALUE+1][64]` array is the root of most of this: indexing by status forces an upper
bound, the bound has to be visible to size the array, and a consumer-visible bound is what creates §1.
Replacing it with a fixed-capacity table of `{ int status; char name[64]; }` searched by status (linear
scan, or open addressing if the constant grows) keeps the no-dynamic-allocation rule, keeps codes as
compile-time constants so `case` labels in `HANDLE` still work, keeps `akerr_name_for_status()`'s
signature, and makes *any* `int` a legal status. `AKERR_MAX_ERR_VALUE` then becomes a private capacity
constant, or disappears. This is the smallest change that dissolves the ABI problem, and it composes
with everything below.
**(b) Let consumers declare their range at init, and detect overlap loudly.** Something like
`akerr_reserve_status_range(base, count, "akbasic")`, called from each library's init, returning an
error context on overlap with an already-reserved range. The library predicts nobody; it just records
who claimed what and refuses a double claim. This converts the silent aliasing in §2 into a startup
failure with a name in the message, which is the actual goal. Cheap, additive, and testable. Pairs
naturally with (a).
**(c) Allocate codes at runtime.** `int akerr_register_status(const char *name)` handing out fresh
integers, with consumers storing them in globals initialised at startup. This is the fully dynamic
end, and it has a specific blocker worth knowing before it gets far: **the control-flow macros are
built on `switch`, and `HANDLE`/`HANDLE_GROUP` expand to `case` labels, which C requires to be integer
constant expressions.** A runtime-allocated code cannot appear in a `case`. Adopting this means
rewriting `PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` as an `if`/`else if` ladder.
That is not unthinkable — and it would incidentally let `HANDLE` match on things a `case` can't, like
a range or a predicate — but it touches the most load-bearing code in the library and every consumer's
error handling at once. Note that it would *not* by itself fix the "don't use `CATCH` or
`FAIL_*_BREAK` inside a loop" hazard (`README.md:237`): that comes from exiting via `break`, not from
`switch` specifically, and would need its own treatment.
**(d) Namespace the status instead of partitioning the integers.** Carry `(domain, code)` in
`akerr_ErrorContext` so each library numbers from 1 in its own domain and collision becomes
structurally impossible. Conceptually the cleanest and the most invasive: it changes the context
struct, every `HANDLE` call site across three repos, and the stack-trace format. Probably only worth
it if the macro layer is being reworked for (c) anyway.
Whichever way this goes, a migration note matters: `libakgl` currently depends on `+18``+22` being
valid *and* on `AKERR_MAX_ERR_VALUE=256` reaching its own translation units. Any change here wants a
coordinated bump of that submodule, or a compatibility window where the old offsets keep working.
## 3. `tests/err_maxval.c` guards the invariant that is going away
**[read off source]**
`tests/err_maxval.c` parses the generated `akerror.h` to assert `AKERR_MAX_ERR_VALUE` covers the
highest `AKERR_*` offset — a good guard against the historical bug it documents (max at `+15` while
`AKERR_BADEXC` is `+17`). If §1 or §2(a) lands, this test's premise changes: with the table private or
sparse, the thing worth asserting is no longer "the ceiling covers our own codes" but "a consumer code
outside our range still registers and retrieves". Worth rewriting alongside, not deleting — the
regression it was written for should stay covered in whatever form the ceiling takes.
Consumers with existing custom codes below 256 should migrate them to a declared range starting at 256 or higher. Range reservation is opt-in, so every co-resident library must participate for collisions to be detected.

View File

@@ -39,16 +39,10 @@
#define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
#define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
#ifndef AKERR_MAX_ERR_VALUE
/* Must be >= the highest AKERR_* offset above (AKERR_BADEXC, +17) so every
* library error code is indexable in __AKERR_ERROR_NAMES. Keep in sync when
* adding codes; tests/err_maxval.c guards this invariant. */
#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 17)
#elif AKERR_MAX_ERR_VALUE < 256
#error user-defined AKERR_MAX_ERR_VALUE must be >= 256
#endif
extern char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
#define AKERR_STATUS_RANGE_OK 0
#define AKERR_STATUS_RANGE_OVERLAP 1
#define AKERR_STATUS_RANGE_FULL 2
#define AKERR_STATUS_RANGE_INVALID 3
#define AKERR_MAX_ARRAY_ERROR 128
@@ -81,6 +75,7 @@ extern akerr_ErrorContext *__akerr_last_ignored;
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
char *akerr_name_for_status(int status, char *name);
int akerr_reserve_status_range(int first_status, int count, const char *owner);
void akerr_init();
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
void akerr_default_logger(const char *f, ...);

View File

@@ -10,18 +10,48 @@ akerr_ErrorContext *__akerr_last_ignored;
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
akerr_ErrorLogFunction akerr_log_method = NULL;
char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
#define AKERR_MAX_REGISTERED_STATUS_NAMES 512
#define AKERR_MAX_RESERVED_STATUS_RANGES 64
#define AKERR_MAX_STATUS_RANGE_OWNER_LENGTH 64
typedef struct
{
int status;
char name[AKERR_MAX_ERROR_NAME_LENGTH];
} akerr_StatusName;
typedef struct
{
int first;
int last;
char owner[AKERR_MAX_STATUS_RANGE_OWNER_LENGTH];
} akerr_StatusRange;
static akerr_StatusName akerr_status_names[AKERR_MAX_REGISTERED_STATUS_NAMES];
static int akerr_status_name_count;
static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES];
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)
{
strncpy(destination, source, (size_t)capacity - 1);
destination[capacity - 1] = '\0';
}
int akerr_valid_error_address(akerr_ErrorContext *ptr)
{
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
if ( ptr == NULL ) {
return 1;
}
return ((ptr >= &AKERR_ARRAY_ERROR[0]) &&
(ptr <= &AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR-1]));
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
if ( ptr == &AKERR_ARRAY_ERROR[i] ) {
return 1;
}
}
return 0;
}
void akerr_default_logger(const char *fmt, ...)
@@ -53,7 +83,13 @@ void akerr_init()
akerr_log_method = &akerr_default_logger;
}
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
memset((void *)&__AKERR_ERROR_NAMES[0], 0x00, ((AKERR_MAX_ERR_VALUE+1) * AKERR_MAX_ERROR_NAME_LENGTH));
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
akerr_status_name_count = 0;
akerr_status_range_count = 0;
/* errno and AKERR_* values are the library-owned compatibility band. */
(void)akerr_reserve_status_range(0, 256, "libakerror");
akerr_name_for_status(AKERR_NULLPOINTER, "Null Pointer Error");
akerr_name_for_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
@@ -114,15 +150,72 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
}
// returns or sets the name for the given status.
// Call with name = NULL to retrieve a status.
/* Return or set a name. Status magnitude is unrelated to storage size. */
char *akerr_name_for_status(int status, char *name)
{
if ( status < 0 || status > AKERR_MAX_ERR_VALUE ) {
return "Unknown Error";
int i;
for ( i = 0; i < akerr_status_name_count; i++ ) {
if ( akerr_status_names[i].status == status ) {
if ( name != NULL ) {
akerr_copy_string(akerr_status_names[i].name,
AKERR_MAX_ERROR_NAME_LENGTH, name);
}
return akerr_status_names[i].name;
}
}
if ( name != NULL ) {
strncpy((char *)&__AKERR_ERROR_NAMES[status], name, AKERR_MAX_ERROR_NAME_LENGTH);
if ( akerr_status_name_count == AKERR_MAX_REGISTERED_STATUS_NAMES ) {
return "Unknown Error";
}
i = akerr_status_name_count++;
akerr_status_names[i].status = status;
akerr_copy_string(akerr_status_names[i].name,
AKERR_MAX_ERROR_NAME_LENGTH, name);
return akerr_status_names[i].name;
}
return (char *)&__AKERR_ERROR_NAMES[status];
return "Unknown Error";
}
/* Reserve an inclusive status interval and reject collisions. */
int akerr_reserve_status_range(int first_status, int count, const char *owner)
{
int last_status;
if ( count <= 0 || owner == NULL || owner[0] == '\0' ||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
first_status > INT_MAX - (count - 1) ) {
return AKERR_STATUS_RANGE_INVALID;
}
last_status = first_status + count - 1;
for ( int i = 0; i < akerr_status_range_count; i++ ) {
if ( first_status <= akerr_status_ranges[i].last &&
last_status >= akerr_status_ranges[i].first ) {
if ( first_status == akerr_status_ranges[i].first &&
last_status == akerr_status_ranges[i].last &&
strcmp(owner, akerr_status_ranges[i].owner) == 0 ) {
return AKERR_STATUS_RANGE_OK;
}
if ( akerr_log_method != NULL ) {
akerr_log_method("Status range %d..%d requested by %s overlaps "
"%d..%d owned by %s\n",
first_status, last_status, owner,
akerr_status_ranges[i].first,
akerr_status_ranges[i].last,
akerr_status_ranges[i].owner);
}
return AKERR_STATUS_RANGE_OVERLAP;
}
}
if ( akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES ) {
return AKERR_STATUS_RANGE_FULL;
}
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);
akerr_status_range_count++;
return AKERR_STATUS_RANGE_OK;
}

View File

@@ -109,9 +109,4 @@ remaining survivors are dominated by:
Findings surfaced by mutation testing:
* **Fixed:** `AKERR_MAX_ERR_VALUE` was `AKERR_LAST_ERRNO_VALUE + 15`, below
`AKERR_NOT_IMPLEMENTED` (+16) and `AKERR_BADEXC` (+17). `akerr_name_for_status`
rejects any status `> AKERR_MAX_ERR_VALUE`, so those codes could never store or
return a name and the `akerr_name_for_status(AKERR_BADEXC, ...)` call in
`akerr_init` was dead code (which is why deleting it survived). The max is now
`+ 17`, and `tests/err_maxval.c` guards the invariant so it can't regress.
* **Superseded:** status names now use a private sparse registry, so the old public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer exist. `tests/err_maxval.c` covers arbitrary `int` values and registry exhaustion.

View File

@@ -6,7 +6,7 @@
* The library imports system errno codes and their descriptions at build time
* (scripts/generrno.sh -> akerr_init_errno). Verify:
* - a system errno (EACCES) has a registered, non-empty name;
* - an out-of-range status returns the "Unknown Error" sentinel;
* - an unregistered status returns the "Unknown Error" sentinel;
* - a system errno can be raised, propagated and handled like any AKERR_* code.
*/
@@ -28,7 +28,7 @@ int main(void)
AKERR_CHECK(nm[0] != '\0');
AKERR_CHECK(strcmp(nm, "Unknown Error") != 0);
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 5000, NULL),
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
"Unknown Error") == 0);
PREPARE_ERROR(e);

View File

@@ -1,92 +1,89 @@
#include "akerror.h"
#include "err_capture.h"
#include <regex.h>
/*
* AKERR_MAX_ERR_VALUE sizes the __AKERR_ERROR_NAMES table and is the upper bound
* akerr_name_for_status() accepts. If it is smaller than the highest AKERR_*
* code, those codes silently lose their names (this was a real bug: the max was
* +15 while AKERR_BADEXC is +17).
*
* Rather than hardcode the list of codes (which rots the moment someone adds a
* code), this test parses the generated akerror.h, discovers every
* #define AKERR_<NAME> (AKERR_LAST_ERRNO_VALUE + <N>)
* finds the highest offset actually defined, and verifies AKERR_MAX_ERR_VALUE
* covers it. The path to the header the library was built from is injected by
* CMake as AKERR_GENERATED_HEADER.
*/
#ifndef AKERR_GENERATED_HEADER
#error "AKERR_GENERATED_HEADER (path to generated akerror.h) must be defined"
#endif
#include <limits.h>
#include <string.h>
/* Status magnitude is no longer coupled to a public array bound. */
int main(void)
{
FILE *fh = fopen(AKERR_GENERATED_HEADER, "r");
AKERR_CHECK(fh != NULL);
akerr_capture_install();
akerr_init();
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, "Maximum Status"),
"Maximum Status") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, "Minimum Status"),
"Minimum Status") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL),
"Maximum Status") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL),
"Minimum Status") == 0);
/* #define AKERR_NAME (AKERR_LAST_ERRNO_VALUE + N) */
regex_t re;
const char *pattern =
"^[[:space:]]*#define[[:space:]]+(AKERR_[A-Za-z0-9_]+)[[:space:]]+"
"\\(AKERR_LAST_ERRNO_VALUE[[:space:]]*\\+[[:space:]]*([0-9]+)\\)";
AKERR_CHECK(regcomp(&re, pattern, REG_EXTENDED) == 0);
const char *long_name =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
char *stored = akerr_name_for_status(1000000, (char *)long_name);
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
int highest_code = -1; /* highest offset among real error codes */
char highest_name[64] = "";
int max_err_value = -1; /* offset parsed from AKERR_MAX_ERR_VALUE */
int code_count = 0;
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(260, 2, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_CONTAINS("component-a");
AKERR_CHECK(akerr_reserve_status_range(255, 1, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 2, "overflow") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 0, "empty") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, -1, "negative") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, NULL) ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, "") ==
AKERR_STATUS_RANGE_INVALID);
char line[4096];
regmatch_t m[3];
while ( fgets(line, sizeof(line), fh) != NULL ) {
if ( regexec(&re, line, 3, m, 0) != 0 ) {
continue;
}
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
memset(owner63, 'a', sizeof(owner63) - 1);
owner63[sizeof(owner63) - 1] = '\0';
memset(owner64, 'b', sizeof(owner64) - 1);
owner64[sizeof(owner64) - 1] = '\0';
AKERR_CHECK(akerr_reserve_status_range(400, 1, owner63) ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(401, 1, owner64) ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "int-max") ==
AKERR_STATUS_RANGE_OK);
char name[64];
int nlen = (int)(m[1].rm_eo - m[1].rm_so);
if ( nlen >= (int)sizeof(name) ) {
nlen = (int)sizeof(name) - 1;
}
memcpy(name, line + m[1].rm_so, nlen);
name[nlen] = '\0';
AKERR_CHECK(akerr_reserve_status_range(500, 2, "endpoint") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(499, 2, "left") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(501, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 2, "other") ==
AKERR_STATUS_RANGE_OVERLAP);
char num[16];
int vlen = (int)(m[2].rm_eo - m[2].rm_so);
if ( vlen >= (int)sizeof(num) ) {
vlen = (int)sizeof(num) - 1;
}
memcpy(num, line + m[2].rm_so, vlen);
num[vlen] = '\0';
int offset = atoi(num);
/* Five ranges exist: library, component-a, owner63, INT_MAX, endpoint. */
for ( int i = 0; i < 59; i++ ) {
AKERR_CHECK(akerr_reserve_status_range(1000 + (i * 2), 1, "fill") ==
AKERR_STATUS_RANGE_OK);
}
AKERR_CHECK(akerr_reserve_status_range(2000, 1, "too-many") ==
AKERR_STATUS_RANGE_FULL);
if ( strcmp(name, "AKERR_MAX_ERR_VALUE") == 0 ) {
max_err_value = offset;
} else {
code_count++;
if ( offset > highest_code ) {
highest_code = offset;
snprintf(highest_name, sizeof(highest_name), "%s", name);
}
int filled = 0;
for ( int status = 2000000; status < 2000600; status++ ) {
if ( strcmp(akerr_name_for_status(status, "Filled"), "Filled") != 0 ) {
filled = 1;
break;
}
}
regfree(&re);
fclose(fh);
AKERR_CHECK(filled == 1);
/* We must have actually parsed both the codes and the ceiling. */
AKERR_CHECK(code_count > 0);
AKERR_CHECK(highest_code > 0);
AKERR_CHECK(max_err_value > 0);
/* Guard against parsing a different header than the one compiled in. */
AKERR_CHECK(max_err_value == (AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE));
/* The actual invariant: every defined code is indexable in the names table. */
AKERR_CHECK(max_err_value >= highest_code);
fprintf(stderr,
"err_maxval ok (%d codes parsed, highest %s at +%d, max +%d)\n",
code_count, highest_name, highest_code, max_err_value);
fprintf(stderr, "err_maxval ok\n");
return 0;
}

View File

@@ -2,13 +2,7 @@
#include "err_capture.h"
#include <string.h>
/*
* akerr_name_for_status must reject out-of-range status codes at BOTH ends.
* It already guarded the upper bound, but a negative status indexed
* __AKERR_ERROR_NAMES[negative] -- an out-of-bounds read (and an out-of-bounds
* write when a name is supplied). Every out-of-range status must return the
* "Unknown Error" sentinel instead.
*/
/* Unregistered status values return the sentinel regardless of magnitude. */
int main(void)
{
@@ -18,8 +12,7 @@ int main(void)
AKERR_CHECK(strcmp(akerr_name_for_status(-1, NULL), "Unknown Error") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(-9999, NULL), "Unknown Error") == 0);
/* Above range (already handled; kept as a guard against regressions). */
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 1, NULL),
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
"Unknown Error") == 0);
/* A valid code must still resolve to its real name. */