diff --git a/CMakeLists.txt b/CMakeLists.txt index 953765f..39285f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index f092510..5dc8525 100644 --- a/README.md +++ b/README.md @@ -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") ``` diff --git a/TODO.md b/TODO.md index dc40fec..5fca80d 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/include/akerror.tmpl.h b/include/akerror.tmpl.h index aa102ce..56bab3f 100644 --- a/include/akerror.tmpl.h +++ b/include/akerror.tmpl.h @@ -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, ...); diff --git a/src/error.c b/src/error.c index e31b731..44ff506 100644 --- a/src/error.c +++ b/src/error.c @@ -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; } diff --git a/tests/MUTATION.md b/tests/MUTATION.md index 2b3a646..c73b117 100644 --- a/tests/MUTATION.md +++ b/tests/MUTATION.md @@ -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. diff --git a/tests/err_errno.c b/tests/err_errno.c index 251042a..8fa54cc 100644 --- a/tests/err_errno.c +++ b/tests/err_errno.c @@ -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); diff --git a/tests/err_maxval.c b/tests/err_maxval.c index 1dcdcaa..3849e24 100644 --- a/tests/err_maxval.c +++ b/tests/err_maxval.c @@ -1,92 +1,89 @@ #include "akerror.h" #include "err_capture.h" -#include - -/* - * 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_ (AKERR_LAST_ERRNO_VALUE + ) - * 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 +#include +/* 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; } diff --git a/tests/err_name_bounds.c b/tests/err_name_bounds.c index 578ad71..0b4c0b8 100644 --- a/tests/err_name_bounds.c +++ b/tests/err_name_bounds.c @@ -2,13 +2,7 @@ #include "err_capture.h" #include -/* - * 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. */