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:
@@ -125,11 +125,6 @@ foreach(_test IN LISTS AKERR_TESTS)
|
|||||||
add_test(NAME ${_test} COMMAND test_${_test})
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
endforeach()
|
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(
|
set_tests_properties(
|
||||||
${AKERR_WILL_FAIL_TESTS}
|
${AKERR_WILL_FAIL_TESTS}
|
||||||
PROPERTIES WILL_FAIL TRUE
|
PROPERTIES WILL_FAIL TRUE
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -4,6 +4,50 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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?
|
# Why?
|
||||||
|
|
||||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
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.
|
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
|
```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
146
TODO.md
@@ -1,143 +1,11 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
Working notes for `libakerror`. Filed from the `akbasic` project, which is becoming the third
|
The status-code ownership work has been consumed. The implementation now:
|
||||||
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.
|
|
||||||
|
|
||||||
Verification level is stated per item. Items marked **[read off source]** were established by reading
|
- stores status names in a private, fixed-capacity sparse registry, removing the consumer-visible array ABI and `AKERR_MAX_ERR_VALUE`;
|
||||||
the library, its CMake, its `.pc.in` and its consumers; they have not been reproduced with a probe
|
- accepts names for arbitrary `int` status values and always terminates truncated names;
|
||||||
program. Where a probe would settle a question, the item says so.
|
- 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.
|
||||||
|
|
||||||
---
|
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.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
@@ -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_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) */
|
#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
|
#define AKERR_STATUS_RANGE_OK 0
|
||||||
/* Must be >= the highest AKERR_* offset above (AKERR_BADEXC, +17) so every
|
#define AKERR_STATUS_RANGE_OVERLAP 1
|
||||||
* library error code is indexable in __AKERR_ERROR_NAMES. Keep in sync when
|
#define AKERR_STATUS_RANGE_FULL 2
|
||||||
* adding codes; tests/err_maxval.c guards this invariant. */
|
#define AKERR_STATUS_RANGE_INVALID 3
|
||||||
#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_MAX_ARRAY_ERROR 128
|
#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_release_error(akerr_ErrorContext *ptr);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||||
char *akerr_name_for_status(int status, char *name);
|
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_init();
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||||
void akerr_default_logger(const char *f, ...);
|
void akerr_default_logger(const char *f, ...);
|
||||||
|
|||||||
113
src/error.c
113
src/error.c
@@ -10,18 +10,48 @@ akerr_ErrorContext *__akerr_last_ignored;
|
|||||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
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];
|
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)
|
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||||
{
|
{
|
||||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
||||||
if ( ptr == NULL ) {
|
if ( ptr == NULL ) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return ((ptr >= &AKERR_ARRAY_ERROR[0]) &&
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
(ptr <= &AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR-1]));
|
if ( ptr == &AKERR_ARRAY_ERROR[i] ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void akerr_default_logger(const char *fmt, ...)
|
void akerr_default_logger(const char *fmt, ...)
|
||||||
@@ -53,7 +83,13 @@ void akerr_init()
|
|||||||
akerr_log_method = &akerr_default_logger;
|
akerr_log_method = &akerr_default_logger;
|
||||||
}
|
}
|
||||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
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_NULLPOINTER, "Null Pointer Error");
|
||||||
akerr_name_for_status(AKERR_OUTOFBOUNDS, "Out Of Bounds 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.
|
/* Return or set a name. Status magnitude is unrelated to storage size. */
|
||||||
// Call with name = NULL to retrieve a status.
|
|
||||||
char *akerr_name_for_status(int status, char *name)
|
char *akerr_name_for_status(int status, char *name)
|
||||||
{
|
{
|
||||||
if ( status < 0 || status > AKERR_MAX_ERR_VALUE ) {
|
int i;
|
||||||
return "Unknown Error";
|
|
||||||
|
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 ) {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,4 @@ remaining survivors are dominated by:
|
|||||||
|
|
||||||
Findings surfaced by mutation testing:
|
Findings surfaced by mutation testing:
|
||||||
|
|
||||||
* **Fixed:** `AKERR_MAX_ERR_VALUE` was `AKERR_LAST_ERRNO_VALUE + 15`, below
|
* **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.
|
||||||
`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.
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* The library imports system errno codes and their descriptions at build time
|
* The library imports system errno codes and their descriptions at build time
|
||||||
* (scripts/generrno.sh -> akerr_init_errno). Verify:
|
* (scripts/generrno.sh -> akerr_init_errno). Verify:
|
||||||
* - a system errno (EACCES) has a registered, non-empty name;
|
* - 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.
|
* - 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(nm[0] != '\0');
|
||||||
AKERR_CHECK(strcmp(nm, "Unknown Error") != 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);
|
"Unknown Error") == 0);
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
|
|||||||
@@ -1,92 +1,89 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
#include "err_capture.h"
|
#include "err_capture.h"
|
||||||
#include <regex.h>
|
#include <limits.h>
|
||||||
|
#include <string.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
|
|
||||||
|
|
||||||
|
/* Status magnitude is no longer coupled to a public array bound. */
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
FILE *fh = fopen(AKERR_GENERATED_HEADER, "r");
|
akerr_capture_install();
|
||||||
AKERR_CHECK(fh != NULL);
|
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) */
|
const char *long_name =
|
||||||
regex_t re;
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
||||||
const char *pattern =
|
char *stored = akerr_name_for_status(1000000, (char *)long_name);
|
||||||
"^[[:space:]]*#define[[:space:]]+(AKERR_[A-Za-z0-9_]+)[[:space:]]+"
|
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
||||||
"\\(AKERR_LAST_ERRNO_VALUE[[:space:]]*\\+[[:space:]]*([0-9]+)\\)";
|
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
||||||
AKERR_CHECK(regcomp(&re, pattern, REG_EXTENDED) == 0);
|
|
||||||
|
|
||||||
int highest_code = -1; /* highest offset among real error codes */
|
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
|
||||||
char highest_name[64] = "";
|
AKERR_STATUS_RANGE_OK);
|
||||||
int max_err_value = -1; /* offset parsed from AKERR_MAX_ERR_VALUE */
|
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
|
||||||
int code_count = 0;
|
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];
|
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||||
regmatch_t m[3];
|
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
||||||
while ( fgets(line, sizeof(line), fh) != NULL ) {
|
memset(owner63, 'a', sizeof(owner63) - 1);
|
||||||
if ( regexec(&re, line, 3, m, 0) != 0 ) {
|
owner63[sizeof(owner63) - 1] = '\0';
|
||||||
continue;
|
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];
|
AKERR_CHECK(akerr_reserve_status_range(500, 2, "endpoint") ==
|
||||||
int nlen = (int)(m[1].rm_eo - m[1].rm_so);
|
AKERR_STATUS_RANGE_OK);
|
||||||
if ( nlen >= (int)sizeof(name) ) {
|
AKERR_CHECK(akerr_reserve_status_range(499, 2, "left") ==
|
||||||
nlen = (int)sizeof(name) - 1;
|
AKERR_STATUS_RANGE_OVERLAP);
|
||||||
}
|
AKERR_CHECK(akerr_reserve_status_range(500, 1, "endpoint") ==
|
||||||
memcpy(name, line + m[1].rm_so, nlen);
|
AKERR_STATUS_RANGE_OVERLAP);
|
||||||
name[nlen] = '\0';
|
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];
|
/* Five ranges exist: library, component-a, owner63, INT_MAX, endpoint. */
|
||||||
int vlen = (int)(m[2].rm_eo - m[2].rm_so);
|
for ( int i = 0; i < 59; i++ ) {
|
||||||
if ( vlen >= (int)sizeof(num) ) {
|
AKERR_CHECK(akerr_reserve_status_range(1000 + (i * 2), 1, "fill") ==
|
||||||
vlen = (int)sizeof(num) - 1;
|
AKERR_STATUS_RANGE_OK);
|
||||||
}
|
}
|
||||||
memcpy(num, line + m[2].rm_so, vlen);
|
AKERR_CHECK(akerr_reserve_status_range(2000, 1, "too-many") ==
|
||||||
num[vlen] = '\0';
|
AKERR_STATUS_RANGE_FULL);
|
||||||
int offset = atoi(num);
|
|
||||||
|
|
||||||
if ( strcmp(name, "AKERR_MAX_ERR_VALUE") == 0 ) {
|
int filled = 0;
|
||||||
max_err_value = offset;
|
for ( int status = 2000000; status < 2000600; status++ ) {
|
||||||
} else {
|
if ( strcmp(akerr_name_for_status(status, "Filled"), "Filled") != 0 ) {
|
||||||
code_count++;
|
filled = 1;
|
||||||
if ( offset > highest_code ) {
|
break;
|
||||||
highest_code = offset;
|
|
||||||
snprintf(highest_name, sizeof(highest_name), "%s", name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
regfree(&re);
|
AKERR_CHECK(filled == 1);
|
||||||
fclose(fh);
|
|
||||||
|
|
||||||
/* We must have actually parsed both the codes and the ceiling. */
|
fprintf(stderr, "err_maxval ok\n");
|
||||||
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);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,7 @@
|
|||||||
#include "err_capture.h"
|
#include "err_capture.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/*
|
/* Unregistered status values return the sentinel regardless of magnitude. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
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(-1, NULL), "Unknown Error") == 0);
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(-9999, 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(1000000, NULL),
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 1, NULL),
|
|
||||||
"Unknown Error") == 0);
|
"Unknown Error") == 0);
|
||||||
|
|
||||||
/* A valid code must still resolve to its real name. */
|
/* A valid code must still resolve to its real name. */
|
||||||
|
|||||||
Reference in New Issue
Block a user