Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned private int enumerations, which was the one place in the library where a failure was not an akerr_ErrorContext *. They now return one like everything else: NULL on success, and on refusal an error whose status is a real code in the library's reserved band, so it can be CATCH-ed, HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are marked AKERR_NOIGNORE, so discarding the result warns at compile time. AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining seven codes move into the AKERR_* offset span and get registered names. AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span in the reserved-band static assert and the exhaustiveness sweep. The refusal detail that used to go straight to akerr_log_method now travels in the error message, so a caller that handles the error decides whether it is reported. The two-argument akerr_name_for_status() set path is the exception: it returns a name and cannot raise, so it logs and releases. akerr_init() likewise has no caller to raise into, so failing to reserve its own band or name its own codes is logged and fatal -- that can only happen on a misconfigured build, and continuing would degrade every later stack trace to "Unknown Error". Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and rewrite its return-code tables in terms of the statuses now raised. Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5% (was 77.6%; the new survivors are the fatal init path, which needs a library built with an undersized name table -- TODO item 7). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,7 @@ set(AKERR_TESTS
|
||||
err_maxval
|
||||
err_name_ownership
|
||||
err_registry_init_order
|
||||
err_status_exception
|
||||
err_refcount_double_fail
|
||||
err_stacktrace_bounds
|
||||
err_name_bounds
|
||||
|
||||
185
README.md
185
README.md
@@ -4,154 +4,13 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||

|
||||
|
||||
## Upgrade notice: custom status codes (1.0.0)
|
||||
## Upgrading from a pre-1.0.0 release
|
||||
|
||||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
||||
registry, and makes status-code ownership explicit and enforced. This is a
|
||||
source and ABI break. The library now carries a version and an soname
|
||||
(`libakerror.so.1`), so a stale installed library can no longer be silently
|
||||
paired with a newer header — but anything built against a pre-1.0.0 header must
|
||||
be rebuilt.
|
||||
1.0.0 replaced the consumer-sized status-name array with a private, ownership-
|
||||
enforced registry. That is a source and ABI break: see
|
||||
[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity
|
||||
limits and how to raise them, and the thread-safety rules.
|
||||
|
||||
What was removed:
|
||||
|
||||
* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so
|
||||
consumers no longer size it. Delete every compile definition and source
|
||||
reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless.
|
||||
* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that
|
||||
touched this data symbol was using an undocumented interface; use
|
||||
`akerr_name_for_status()`.
|
||||
|
||||
To migrate:
|
||||
|
||||
1. Rebuild libakerror and every dependent library against the new header.
|
||||
2. Move custom status codes out of the reserved `0`–`255` band. Use fixed
|
||||
integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather
|
||||
than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno
|
||||
cannot move your codes.
|
||||
3. Assign a distinct range to every library that may coexist in one process, and
|
||||
coordinate those ranges at the application or dependency-stack level.
|
||||
4. Reserve the complete range with `akerr_reserve_status_range()` during
|
||||
initialization. Treat any result other than `AKERR_STATUS_RANGE_OK` as an
|
||||
initialization failure.
|
||||
5. Register names with `akerr_register_status_name()`, passing the same owner
|
||||
string you reserved with. **You can no longer name a status you have not
|
||||
reserved** — see "Ownership is enforced" below.
|
||||
|
||||
For example:
|
||||
|
||||
```c
|
||||
enum {
|
||||
MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */
|
||||
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_STORAGE,
|
||||
MYLIB_ERR_LIMIT
|
||||
};
|
||||
|
||||
#define MYLIB_OWNER "mylib"
|
||||
|
||||
int mylib_init(void)
|
||||
{
|
||||
if (akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||||
return MYLIB_INIT_FAILED; /* another component owns part of the range */
|
||||
}
|
||||
|
||||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, "Parse Error");
|
||||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, "Storage Error");
|
||||
return MYLIB_INIT_OK;
|
||||
}
|
||||
```
|
||||
|
||||
You do not need to call `akerr_init()` first. Every registry entry point calls
|
||||
it for you, so a library that reserves its range before anything else in the
|
||||
process has touched libakerror keeps that reservation. (Before 1.0.0 this was
|
||||
silently destructive: `akerr_init()` cleared the tables, so a reservation made
|
||||
too early was discarded and the next component to claim the same range was told
|
||||
it was free.)
|
||||
|
||||
### Ownership is enforced
|
||||
|
||||
Reserving a range is no longer advisory bookkeeping. A status may only be named
|
||||
from inside a reservation:
|
||||
|
||||
* `akerr_register_status_name(owner, status, name)` requires that `status` fall
|
||||
in a range reserved by `owner`. Naming another component's status fails with
|
||||
`AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with
|
||||
`AKERR_STATUS_NAME_UNRESERVED`.
|
||||
* The two-argument `akerr_name_for_status(status, name)` set path still works,
|
||||
but it cannot identify its caller, so it can only require that *some*
|
||||
reservation covers the status. Prefer the owned form: it is the one that
|
||||
catches a component writing into a range that is not its own.
|
||||
|
||||
Every refusal is reported through `akerr_log_method` and names the real owner,
|
||||
because a name that fails to register degrades that code to `"Unknown Error"` in
|
||||
every later stack trace. Registration failures are not fatal by themselves —
|
||||
check the return value during initialization if you want them to be.
|
||||
|
||||
This detects *name* collisions. It cannot detect two components compiling the
|
||||
same integer into a `HANDLE` label without ever registering a name, so every
|
||||
co-resident library should still reserve its range.
|
||||
|
||||
### Return codes
|
||||
|
||||
`akerr_reserve_status_range()`:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `AKERR_STATUS_RANGE_OK` (0) | Range reserved, or an identical range was already reserved by the same owner |
|
||||
| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (logged, with the owner) |
|
||||
| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain |
|
||||
| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` |
|
||||
|
||||
`akerr_register_status_name()`:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `AKERR_STATUS_NAME_OK` (0) | Name registered |
|
||||
| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status |
|
||||
| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else |
|
||||
| `AKERR_STATUS_NAME_FULL` | The name registry is full |
|
||||
| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name |
|
||||
|
||||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
||||
*superset* of your own range is not — it is reported as an overlap. Reserve the
|
||||
whole range in one call.
|
||||
|
||||
### Capacity
|
||||
|
||||
Both tables are fixed size, allocated in BSS, and never grow:
|
||||
|
||||
| Limit | Default | Build-time override |
|
||||
| --- | --- | --- |
|
||||
| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=<power of two>` |
|
||||
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
|
||||
|
||||
`akerr_init()` consumes one name slot per host errno value plus one per
|
||||
`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in
|
||||
the process to share. That figure varies with the host libc, so treat it as
|
||||
approximate rather than a budget to fill.
|
||||
|
||||
Both overrides are CMake cache variables and apply `PRIVATE` to the library
|
||||
target. The tables live entirely inside `src/error.c`, so raising them changes
|
||||
nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced,
|
||||
where a mismatch between the library and its consumers corrupted memory. Set
|
||||
them when configuring libakerror itself:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384
|
||||
```
|
||||
|
||||
Exhausting either table is reported through `akerr_log_method` and returned to
|
||||
the caller; it is never silent.
|
||||
|
||||
### Thread safety
|
||||
|
||||
The registry is process-global mutable state with no locking. Reserve ranges and
|
||||
register names during single-threaded initialization, before spawning threads.
|
||||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
||||
once registration has finished.
|
||||
|
||||
# Why?
|
||||
|
||||
@@ -220,13 +79,26 @@ names are stored sparsely, so any `int` is a legal status and no compile
|
||||
definition is needed to use large values.
|
||||
|
||||
Every library that may coexist in one process must reserve its range during
|
||||
initialization and treat any nonzero result as a startup error:
|
||||
initialization. `akerr_reserve_status_range()` and
|
||||
`akerr_register_status_name()` report failure the way everything else in this
|
||||
library does — they return `akerr_ErrorContext *`, and they are marked
|
||||
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
||||
`PASS` up out of your initialization:
|
||||
|
||||
```c
|
||||
#define MYLIB_OWNER "my-library"
|
||||
|
||||
if (akerr_reserve_status_range(256, 16, MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||||
/* Refuse to initialize: another component owns part of the range. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* Another component owning part of the range propagates to our caller. */
|
||||
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||
|
||||
/* Then name each code, quoting the owner you reserved with. */
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
||||
"Some Error Code Description"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -234,15 +106,12 @@ Reservations are process-local, fixed-capacity, and idempotent when the same
|
||||
owner repeats the exact same range. They preserve compile-time integer constants
|
||||
so `HANDLE` still works, since `case` labels require them.
|
||||
|
||||
Then name each code, quoting the owner you reserved with:
|
||||
|
||||
```c
|
||||
akerr_register_status_name(MYLIB_OWNER, 256, "Some Error Code Description");
|
||||
```
|
||||
|
||||
Naming a status outside your reservation is refused and logged. See
|
||||
[the upgrade notice](#upgrade-notice-custom-status-codes-100) for the return
|
||||
codes, the capacity limits and how to raise them, and thread-safety rules.
|
||||
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
||||
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
||||
rules.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
17
TODO.md
17
TODO.md
@@ -35,8 +35,8 @@ inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
|
||||
## 3. No registry introspection
|
||||
|
||||
There is no way to ask who owns a status, or to enumerate reservations. The
|
||||
"coordinate ranges at the dependency-stack level" advice in README.md therefore
|
||||
has no tooling behind it.
|
||||
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
|
||||
therefore has no tooling behind it.
|
||||
|
||||
A read-only accessor plus a dump through `akerr_log_method` would let a startup
|
||||
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
|
||||
@@ -63,6 +63,19 @@ exists for migration. Once consumers have moved to
|
||||
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
|
||||
`akerr_name_for_status()` as pure lookup.
|
||||
|
||||
## 7. The library's own startup failure path is untested
|
||||
|
||||
`__akerr_name_library_status()` and the band reservation in `akerr_init()` log
|
||||
and then terminate when the library cannot name its own codes. Nothing exercises
|
||||
that: it needs a build whose `AKERR_STATUS_NAME_SLOTS` is too small to hold the
|
||||
library's own entries, and that macro is `PRIVATE` to the library target, so a
|
||||
test executable cannot set it. Mutation testing reports those lines as surviving
|
||||
mutants for this reason.
|
||||
|
||||
Closing it means a second library target built with a tiny table plus a
|
||||
`WILL_FAIL` test linked against it — worth doing when the CMake gains a
|
||||
sanitizer variant (item 1), since that adds the same machinery.
|
||||
|
||||
## Unrelated pre-existing issues
|
||||
|
||||
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||
|
||||
208
UPGRADING.md
Normal file
208
UPGRADING.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Upgrade notice: custom status codes (1.0.0)
|
||||
|
||||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
||||
registry, and makes status-code ownership explicit and enforced. This is a
|
||||
source and ABI break. The library now carries a version and an soname
|
||||
(`libakerror.so.1`), so a stale installed library can no longer be silently
|
||||
paired with a newer header — but anything built against a pre-1.0.0 header must
|
||||
be rebuilt.
|
||||
|
||||
What was removed:
|
||||
|
||||
* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so
|
||||
consumers no longer size it. Delete every compile definition and source
|
||||
reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless.
|
||||
* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that
|
||||
touched this data symbol was using an undocumented interface; use
|
||||
`akerr_name_for_status()`.
|
||||
* `AKERR_STATUS_RANGE_OK` and `AKERR_STATUS_NAME_OK` — the registry functions no
|
||||
longer return an `int`. Success is a `NULL` `akerr_ErrorContext *`, like every
|
||||
other function in the library. See "The registry raises errors" below.
|
||||
|
||||
To migrate:
|
||||
|
||||
1. Rebuild libakerror and every dependent library against the new header.
|
||||
2. Move custom status codes out of the reserved `0`–`255` band. Use fixed
|
||||
integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather
|
||||
than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno
|
||||
cannot move your codes.
|
||||
3. Assign a distinct range to every library that may coexist in one process, and
|
||||
coordinate those ranges at the application or dependency-stack level.
|
||||
4. Reserve the complete range with `akerr_reserve_status_range()` during
|
||||
initialization, and treat the error it returns like any other error: `CATCH`
|
||||
it, `PASS` it, or let it propagate out of your init function.
|
||||
5. Register names with `akerr_register_status_name()`, passing the same owner
|
||||
string you reserved with. **You can no longer name a status you have not
|
||||
reserved** — see "Ownership is enforced" below.
|
||||
|
||||
For example:
|
||||
|
||||
```c
|
||||
enum {
|
||||
MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */
|
||||
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_STORAGE,
|
||||
MYLIB_ERR_LIMIT
|
||||
};
|
||||
|
||||
#define MYLIB_OWNER "mylib"
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* Any collision propagates out of mylib_init() to the caller. */
|
||||
PASS(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER));
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE,
|
||||
"Parse Error"));
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE,
|
||||
"Storage Error"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
If your `init` cannot return an `akerr_ErrorContext *` — a C API with an `int`
|
||||
return, say — catch the error and convert it. Close with `FINISH_NORETURN`
|
||||
rather than `FINISH`: nothing can propagate out of a function that does not
|
||||
return an error context, and `FINISH(errctx, false)` in an `int`-returning
|
||||
function compiles the (dead) propagation branch anyway, which warns.
|
||||
|
||||
```c
|
||||
int mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int rc = MYLIB_INIT_OK;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "mylib could not claim its status range");
|
||||
rc = MYLIB_INIT_FAILED; /* another component owns part of the range */
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
|
||||
You do not need to call `akerr_init()` first. Every registry entry point calls
|
||||
it for you, so a library that reserves its range before anything else in the
|
||||
process has touched libakerror keeps that reservation. (Before 1.0.0 this was
|
||||
silently destructive: `akerr_init()` cleared the tables, so a reservation made
|
||||
too early was discarded and the next component to claim the same range was told
|
||||
it was free.)
|
||||
|
||||
## Ownership is enforced
|
||||
|
||||
Reserving a range is no longer advisory bookkeeping. A status may only be named
|
||||
from inside a reservation:
|
||||
|
||||
* `akerr_register_status_name(owner, status, name)` requires that `status` fall
|
||||
in a range reserved by `owner`. Naming another component's status fails with
|
||||
`AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with
|
||||
`AKERR_STATUS_NAME_UNRESERVED`.
|
||||
* The two-argument `akerr_name_for_status(status, name)` set path still works,
|
||||
but it cannot identify its caller, so it can only require that *some*
|
||||
reservation covers the status. Prefer the owned form: it is the one that
|
||||
catches a component writing into a range that is not its own.
|
||||
|
||||
Every refusal is reported, because a name that fails to register degrades that
|
||||
code to `"Unknown Error"` in every later stack trace.
|
||||
|
||||
This detects *name* collisions. It cannot detect two components compiling the
|
||||
same integer into a `HANDLE` label without ever registering a name, so every
|
||||
co-resident library should still reserve its range.
|
||||
|
||||
## The registry raises errors
|
||||
|
||||
`akerr_reserve_status_range()` and `akerr_register_status_name()` return
|
||||
`akerr_ErrorContext *`, exactly like the rest of the library. They are marked
|
||||
`AKERR_NOIGNORE`, so discarding the result is a compile-time warning, and an
|
||||
error you catch but do not handle propagates out of your init function instead
|
||||
of leaving you with a range you do not actually own.
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
||||
/* Another component owns part of it -- the message names which. */
|
||||
} FINISH(errctx, true);
|
||||
```
|
||||
|
||||
`akerr_reserve_status_range()` raises:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| — (returns `NULL`) | Range reserved, or an identical range was already reserved by the same owner |
|
||||
| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (the message names the owner) |
|
||||
| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain |
|
||||
| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` |
|
||||
|
||||
`akerr_register_status_name()` raises:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| — (returns `NULL`) | Name registered |
|
||||
| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status |
|
||||
| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else |
|
||||
| `AKERR_STATUS_NAME_FULL` | The name registry is full |
|
||||
| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name |
|
||||
|
||||
These are ordinary status codes inside the library's reserved band, so they can
|
||||
be matched with `HANDLE`, grouped with `HANDLE_GROUP`, and they print with a
|
||||
name and a stack trace when they go unhandled.
|
||||
|
||||
`akerr_name_for_status(status, name)` is the one exception: it returns a name
|
||||
rather than an error context, so it cannot raise. A refused registration through
|
||||
that path is reported through `akerr_log_method` and reads back as
|
||||
`"Unknown Error"`.
|
||||
|
||||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
||||
*superset* of your own range is not — it raises `AKERR_STATUS_RANGE_OVERLAP`.
|
||||
Reserve the whole range in one call.
|
||||
|
||||
The library holds itself to the same rule at startup: if `akerr_init()` cannot
|
||||
reserve its own `0`–`255` band or name one of its own codes, it logs the failure
|
||||
and terminates the program. That can only happen in a misconfigured build (a
|
||||
name table too small for the library's own entries), and the alternative is a
|
||||
process whose stack traces silently read `"Unknown Error"` for built-in codes.
|
||||
|
||||
## Capacity
|
||||
|
||||
Both tables are fixed size, allocated in BSS, and never grow:
|
||||
|
||||
| Limit | Default | Build-time override |
|
||||
| --- | --- | --- |
|
||||
| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=<power of two>` |
|
||||
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
|
||||
|
||||
`akerr_init()` consumes one name slot per host errno value plus one per
|
||||
`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in
|
||||
the process to share. That figure varies with the host libc, so treat it as
|
||||
approximate rather than a budget to fill.
|
||||
|
||||
Both overrides are CMake cache variables and apply `PRIVATE` to the library
|
||||
target. The tables live entirely inside `src/error.c`, so raising them changes
|
||||
nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced,
|
||||
where a mismatch between the library and its consumers corrupted memory. Set
|
||||
them when configuring libakerror itself:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384
|
||||
```
|
||||
|
||||
Exhausting either table raises an error to the caller; it is never silent.
|
||||
|
||||
## Thread safety
|
||||
|
||||
The registry is process-global mutable state with no locking. Reserve ranges and
|
||||
register names during single-threaded initialization, before spawning threads.
|
||||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
||||
once registration has finished.
|
||||
@@ -39,6 +39,25 @@
|
||||
#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) */
|
||||
|
||||
/*
|
||||
* Registry failures. These are ordinary status codes, not a private return
|
||||
* enumeration: akerr_reserve_status_range() and akerr_register_status_name()
|
||||
* return akerr_ErrorContext * like everything else in this library, so a refused
|
||||
* reservation can be CATCH-ed, HANDLE-d, PASS-ed, or left unhandled to produce a
|
||||
* stack trace and stop the program. Success returns NULL.
|
||||
*/
|
||||
#define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */
|
||||
#define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
||||
#define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */
|
||||
#define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */
|
||||
#define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */
|
||||
#define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
||||
#define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */
|
||||
|
||||
/* The last status the library defines for itself. Everything from
|
||||
* AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name. */
|
||||
#define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID
|
||||
|
||||
/*
|
||||
* Status values 0 through 255 are reserved by libakerror at akerr_init() time:
|
||||
* the host's errno values plus the AKERR_* codes above. Consumers allocate from
|
||||
@@ -49,19 +68,6 @@
|
||||
#define AKERR_RESERVED_STATUS_COUNT 256
|
||||
#define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
|
||||
|
||||
/* Results of akerr_reserve_status_range(). */
|
||||
#define AKERR_STATUS_RANGE_OK 0 /** The range is now reserved by the caller */
|
||||
#define AKERR_STATUS_RANGE_OVERLAP 1 /** Some part of the range is already owned by someone else */
|
||||
#define AKERR_STATUS_RANGE_FULL 2 /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
||||
#define AKERR_STATUS_RANGE_INVALID 3 /** Bad count, bad owner string, or the range overflows int */
|
||||
|
||||
/* Results of akerr_register_status_name(). */
|
||||
#define AKERR_STATUS_NAME_OK 0 /** The name is registered */
|
||||
#define AKERR_STATUS_NAME_UNRESERVED 1 /** No owner has reserved a range containing this status */
|
||||
#define AKERR_STATUS_NAME_FOREIGN 2 /** The status lies in a range reserved by a different owner */
|
||||
#define AKERR_STATUS_NAME_FULL 3 /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
||||
#define AKERR_STATUS_NAME_INVALID 4 /** NULL/empty/over-long owner, or a NULL name */
|
||||
|
||||
/*
|
||||
* The library reserves status values 0 through 255 for itself (see akerr_init),
|
||||
* which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is
|
||||
@@ -69,7 +75,7 @@
|
||||
* unusually large errno space these codes could escape the band and collide
|
||||
* with consumer codes allocated at 256. Fail the build instead.
|
||||
*/
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_BADEXC < 256) ? 1 : -1];
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
||||
|
||||
#define AKERR_MAX_ARRAY_ERROR 128
|
||||
|
||||
@@ -105,32 +111,43 @@ akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||
* Look up (name == NULL) or register (name != NULL) the display name for a
|
||||
* status. Registration succeeds only if some owner has reserved a range
|
||||
* containing `status`; prefer akerr_register_status_name(), which also checks
|
||||
* that the range belongs to you and reports why a registration was refused.
|
||||
* that the range belongs to you and raises an error saying why a registration
|
||||
* was refused. This entry point cannot return an error context, so a refusal
|
||||
* here is reported through akerr_log_method and reads back as "Unknown Error".
|
||||
* Never returns NULL -- an unregistered status reads back as "Unknown Error".
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name);
|
||||
|
||||
/*
|
||||
* Register a display name for a status you own. Returns AKERR_STATUS_NAME_OK,
|
||||
* or one of the AKERR_STATUS_NAME_* codes above; every refusal is also logged
|
||||
* through akerr_log_method. `owner` must match the owner string passed to
|
||||
* akerr_reserve_status_range() for the range containing `status`.
|
||||
* Register a display name for a status you own. `owner` must match the owner
|
||||
* string passed to akerr_reserve_status_range() for the range containing
|
||||
* `status`. Returns NULL on success, or an error context whose status is one of
|
||||
* the AKERR_STATUS_NAME_* codes above -- CATCH it, HANDLE it, or let it
|
||||
* propagate.
|
||||
*/
|
||||
int akerr_register_status_name(const char *owner, int status, const char *name);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
||||
|
||||
/*
|
||||
* Claim `count` status values starting at `first_status` for `owner`. Repeating
|
||||
* an identical reservation for the same owner is a no-op; any other collision
|
||||
* is refused and logged. Returns one of the AKERR_STATUS_RANGE_* codes; treat
|
||||
* anything other than AKERR_STATUS_RANGE_OK as an initialization failure.
|
||||
* an identical reservation for the same owner is a no-op; any other collision is
|
||||
* refused. Returns NULL on success, or an error context whose status is one of
|
||||
* the AKERR_STATUS_RANGE_* codes above. Treat any error as an initialization
|
||||
* failure: either handle it or let it propagate out of your init function.
|
||||
*/
|
||||
int akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *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, ...);
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
||||
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
|
||||
void akerr_init_errno(void);
|
||||
/*
|
||||
* Internal. Names a status in the library's own reserved band on behalf of
|
||||
* akerr_init() and the generated errno table, which have no caller to raise
|
||||
* into: a failure here is logged and terminates the program. Not part of the
|
||||
* consumer API -- use akerr_register_status_name(), which raises instead.
|
||||
*/
|
||||
void __akerr_name_library_status(int status, const char *name);
|
||||
|
||||
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
|
||||
akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \
|
||||
|
||||
@@ -8,13 +8,24 @@ mkdir -p ${outdir}/include
|
||||
rm -f ${outdir}/src/errno.c
|
||||
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
|
||||
echo "#include <errno.h>" >> ${outdir}/src/errno.c
|
||||
cat >> ${outdir}/src/errno.c <<'EOF'
|
||||
|
||||
/*
|
||||
* These names belong to the library's own reserved band, and this runs from
|
||||
* akerr_init(), which has no caller to raise into -- so it goes through
|
||||
* __akerr_name_library_status(), which reports a refusal and terminates rather
|
||||
* than leaving every later stack trace to print "Unknown Error" for an errno.
|
||||
* Keeping the branch in src/error.c also keeps this generated file free of
|
||||
* control flow no test can reach.
|
||||
*/
|
||||
EOF
|
||||
echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c
|
||||
maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1)
|
||||
errno --list | while read LINE; do
|
||||
define=$(echo "$LINE" | cut -d ' ' -f 1);
|
||||
value=$(echo "$LINE" | cut -d ' ' -f 2);
|
||||
desc=$(echo "$LINE" | cut -d ' ' -f 3-);
|
||||
echo " akerr_register_status_name(AKERR_LIBRARY_OWNER, ${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||
done;
|
||||
echo "}" >> ${outdir}/src/errno.c
|
||||
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h
|
||||
|
||||
212
src/error.c
212
src/error.c
@@ -106,6 +106,30 @@ void akerr_default_logger(const char *fmt, ...)
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* The library naming its own codes. The registry entry points raise errors like
|
||||
* everything else in the library, but akerr_init() has no caller to raise into,
|
||||
* so this is where the buck stops: log the failure and take the unhandled-error
|
||||
* path, which terminates the program.
|
||||
*
|
||||
* This is fatal on purpose. The library cannot name its own status codes only
|
||||
* if the build is misconfigured -- a name table too small to hold even the
|
||||
* library's own entries, or a reservation that did not take -- and the
|
||||
* consequence of continuing is every later stack trace in the process printing
|
||||
* "Unknown Error" for a code the library defines. That is a startup defect, and
|
||||
* it is far cheaper to see it at init than to debug it from a degraded trace.
|
||||
*/
|
||||
void __akerr_name_library_status(int status, const char *name)
|
||||
{
|
||||
akerr_ErrorContext *errctx = akerr_register_status_name(AKERR_LIBRARY_OWNER,
|
||||
status, name);
|
||||
if ( errctx != NULL ) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** libakerror could not name its own status **");
|
||||
akerr_handler_unhandled_error(errctx);
|
||||
RELEASE_ERROR(errctx);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
||||
* -- and the public registry entry points, which all call akerr_init() so that
|
||||
@@ -138,27 +162,44 @@ void akerr_init()
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. */
|
||||
(void)akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER);
|
||||
akerr_ErrorContext *reserve_err =
|
||||
akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER);
|
||||
if ( reserve_err != NULL ) {
|
||||
/* Fatal for the same reason as __akerr_name_library_status():
|
||||
* without this band the library owns nothing, so none of the names
|
||||
* below can register either. */
|
||||
LOG_ERROR_WITH_MESSAGE(reserve_err,
|
||||
"** libakerror could not reserve its own status band **");
|
||||
akerr_handler_unhandled_error(reserve_err);
|
||||
RELEASE_ERROR(reserve_err);
|
||||
}
|
||||
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_API, "API Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ATTRIBUTE, "Attribute Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_TYPE, "Type Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_KEY, "Key Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_INDEX, "Index Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_FORMAT, "Format Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_IO, "Input Output Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_VALUE, "Value Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_RELATIONSHIP, "Relationship Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_EOF, "End Of File");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
__akerr_name_library_status(AKERR_API, "API Error");
|
||||
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
||||
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
||||
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
||||
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
||||
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
||||
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
||||
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
||||
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
@@ -280,76 +321,91 @@ static akerr_StatusRange *akerr_range_for_status(int status)
|
||||
* Shared body of both registration entry points. A NULL owner means the caller
|
||||
* did not identify itself (the legacy two-argument akerr_name_for_status path):
|
||||
* the status must still lie inside *some* reservation, but we cannot check that
|
||||
* it is the caller's. Every refusal is logged -- a name that silently fails to
|
||||
* register degrades into "Unknown Error" in stack traces, which is exactly the
|
||||
* kind of quiet loss this registry exists to prevent.
|
||||
* it is the caller's. Every refusal raises an error -- a name that silently
|
||||
* fails to register degrades into "Unknown Error" in stack traces, which is
|
||||
* exactly the kind of quiet loss this registry exists to prevent -- so the
|
||||
* message carries everything a caller needs to see in a stack trace.
|
||||
*/
|
||||
static int akerr_store_status_name(const char *owner, int status, const char *name)
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
{
|
||||
akerr_StatusRange *range;
|
||||
akerr_StatusName *entry;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
if ( name == NULL ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
if ( owner != NULL && ( owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ) ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d (\"%s\"): the owner string is "
|
||||
"empty or longer than %d characters",
|
||||
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
||||
}
|
||||
|
||||
range = akerr_range_for_status(status);
|
||||
if ( range == NULL ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Refusing to name status %d (\"%s\") for %s: no "
|
||||
"reserved range contains it. Call "
|
||||
"akerr_reserve_status_range() first.\n",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
return AKERR_STATUS_NAME_UNRESERVED;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_UNRESERVED,
|
||||
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
||||
"range contains it. Call akerr_reserve_status_range() first.",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
}
|
||||
if ( owner != NULL && strcmp(owner, range->owner) != 0 ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Refusing to name status %d (\"%s\") for %s: that "
|
||||
"status is in range %d..%d owned by %s.\n",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
}
|
||||
return AKERR_STATUS_NAME_FOREIGN;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_FOREIGN,
|
||||
"Refusing to name status %d (\"%s\") for %s: that status is "
|
||||
"in range %d..%d owned by %s.",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
}
|
||||
|
||||
entry = akerr_status_slot(status, 1);
|
||||
if ( entry == NULL ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Status name registry is full (%d entries); "
|
||||
"dropping name \"%s\" for status %d. Rebuild "
|
||||
"libakerror with a larger AKERR_STATUS_NAME_SLOTS.\n",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
}
|
||||
return AKERR_STATUS_NAME_FULL;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_NAME_FULL,
|
||||
"Status name registry is full (%d entries); dropping name "
|
||||
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
||||
"AKERR_STATUS_NAME_SLOTS.",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
}
|
||||
akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name);
|
||||
return AKERR_STATUS_NAME_OK;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* Register a name for a status inside a range the caller reserved. */
|
||||
int akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
akerr_init();
|
||||
if ( owner == NULL ) {
|
||||
return AKERR_STATUS_NAME_INVALID;
|
||||
}
|
||||
return akerr_store_status_name(owner, status, name);
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (owner != NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d: the owner string is NULL. "
|
||||
"Pass the same owner you reserved the range with.",
|
||||
status);
|
||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* Return or set a name. Status magnitude is unrelated to storage size. */
|
||||
/*
|
||||
* Return or set a name. Status magnitude is unrelated to storage size.
|
||||
*
|
||||
* The set path is the legacy two-argument form and cannot hand an error back to
|
||||
* its caller (it returns a name), so a refusal is logged here and released.
|
||||
* akerr_register_status_name() is the form that raises.
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
akerr_StatusName *entry;
|
||||
|
||||
akerr_init();
|
||||
if ( name != NULL &&
|
||||
akerr_store_status_name(NULL, status, name) != AKERR_STATUS_NAME_OK ) {
|
||||
return "Unknown Error";
|
||||
if ( name != NULL ) {
|
||||
akerr_ErrorContext *errctx = akerr_store_status_name(NULL, status, name);
|
||||
if ( errctx != NULL ) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **");
|
||||
RELEASE_ERROR(errctx);
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
entry = akerr_status_slot(status, 0);
|
||||
if ( entry == NULL ) {
|
||||
@@ -359,15 +415,21 @@ char *akerr_name_for_status(int status, char *name)
|
||||
}
|
||||
|
||||
/* Reserve an inclusive status interval and reject collisions. */
|
||||
int akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
{
|
||||
int last_status;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
akerr_init();
|
||||
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;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_INVALID,
|
||||
"Invalid status range reservation: %d status values from "
|
||||
"%d for %s (count must be positive, the owner string "
|
||||
"non-empty and shorter than %d characters, and the range "
|
||||
"must not overflow int)",
|
||||
count, first_status, owner == NULL ? "(null)" : owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
||||
}
|
||||
last_status = first_status + count - 1;
|
||||
|
||||
@@ -377,27 +439,23 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
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;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
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;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_OVERLAP,
|
||||
"Status range %d..%d requested by %s overlaps %d..%d "
|
||||
"owned by %s",
|
||||
first_status, last_status, owner,
|
||||
akerr_status_ranges[i].first,
|
||||
akerr_status_ranges[i].last,
|
||||
akerr_status_ranges[i].owner);
|
||||
}
|
||||
}
|
||||
if ( akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES ) {
|
||||
if ( akerr_log_method != NULL ) {
|
||||
akerr_log_method("Status range table is full (%d ranges); "
|
||||
"refusing %d..%d for %s.\n",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
}
|
||||
return AKERR_STATUS_RANGE_FULL;
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_FULL,
|
||||
"Status range table is full (%d ranges); refusing %d..%d "
|
||||
"for %s.",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
}
|
||||
|
||||
akerr_status_ranges[akerr_status_range_count].first = first_status;
|
||||
@@ -405,5 +463,5 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
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;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,47 @@ static int __attribute__((unused)) akerr_slots_in_use(void)
|
||||
AKERR_CHECK((errctx)->status == (expected_status)); \
|
||||
} while ( 0 )
|
||||
|
||||
/*
|
||||
* Helpers for functions that report failure by returning akerr_ErrorContext *.
|
||||
* Both release the context they consume, so a test that makes thousands of
|
||||
* failing calls cannot exhaust the pool. AKERR_CHECK_RAISES keeps a copy of the
|
||||
* message for AKERR_CHECK_MESSAGE_CONTAINS, since the context is gone by then.
|
||||
*/
|
||||
static char __attribute__((unused)) akerr_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||
|
||||
#define AKERR_CHECK_SUCCEEDS(expr) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
if ( __akerr_result != NULL ) { \
|
||||
fprintf(stderr, "UNEXPECTED ERROR from %s: %d (%s): %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
akerr_name_for_status(__akerr_result->status, NULL), \
|
||||
__akerr_result->message, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_RAISES(expr, expected_status) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
AKERR_CHECK(__akerr_result != NULL); \
|
||||
snprintf(akerr_last_message, sizeof(akerr_last_message), "%s", \
|
||||
__akerr_result->message); \
|
||||
if ( __akerr_result->status != (expected_status) ) { \
|
||||
fprintf(stderr, "WRONG STATUS from %s: got %d, want %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
#expected_status, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
AKERR_CHECK(__akerr_result == NULL); \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_MESSAGE_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_last_message, (needle)) != NULL)
|
||||
|
||||
#define AKERR_CHECK_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
||||
|
||||
|
||||
@@ -35,6 +35,13 @@ static const struct {
|
||||
{ AKERR_ITERATOR_BREAK, "Iterator Break" },
|
||||
{ AKERR_NOT_IMPLEMENTED, "Not Implemented" },
|
||||
{ AKERR_BADEXC, "Invalid akerr_ErrorContext" },
|
||||
{ AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap" },
|
||||
{ AKERR_STATUS_RANGE_FULL, "Status Range Table Full" },
|
||||
{ AKERR_STATUS_RANGE_INVALID, "Invalid Status Range" },
|
||||
{ AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name" },
|
||||
{ AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name" },
|
||||
{ AKERR_STATUS_NAME_FULL, "Status Name Registry Full" },
|
||||
{ AKERR_STATUS_NAME_INVALID, "Invalid Status Name" },
|
||||
};
|
||||
|
||||
int main(void)
|
||||
@@ -52,7 +59,9 @@ int main(void)
|
||||
* AKERR_LAST_ERRNO_VALUE + 7 is the one deliberate hole (a removed code);
|
||||
* anything else nameless is a code someone added without registering it.
|
||||
*/
|
||||
for ( int offset = 1; offset <= AKERR_BADEXC - AKERR_LAST_ERRNO_VALUE; offset++ ) {
|
||||
for ( int offset = 1;
|
||||
offset <= AKERR_LAST_LIBRARY_STATUS - AKERR_LAST_ERRNO_VALUE;
|
||||
offset++ ) {
|
||||
int code = AKERR_LAST_ERRNO_VALUE + offset;
|
||||
char *nm = akerr_name_for_status(code, NULL);
|
||||
if ( offset == 7 ) {
|
||||
@@ -67,7 +76,7 @@ int main(void)
|
||||
}
|
||||
|
||||
/* Every AKERR_* code must sit inside the band the library reserves. */
|
||||
AKERR_CHECK(AKERR_BADEXC < AKERR_FIRST_CONSUMER_STATUS);
|
||||
AKERR_CHECK(AKERR_LAST_LIBRARY_STATUS < AKERR_FIRST_CONSUMER_STATUS);
|
||||
|
||||
fprintf(stderr, "err_error_names ok\n");
|
||||
return 0;
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
* Covers: arbitrary int status values, name truncation, range reservation
|
||||
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
|
||||
* capacity limits -- the range table and the name table -- each of which must
|
||||
* report the failure rather than dropping the registration quietly.
|
||||
* raise rather than dropping the registration quietly.
|
||||
*
|
||||
* Every refusal is an error context the caller owns, so each check below also
|
||||
* asserts the context returns to the pool; a leak here would exhaust the
|
||||
* 128-slot pool long before these loops finish.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
@@ -20,120 +24,119 @@ int main(void)
|
||||
akerr_init();
|
||||
|
||||
/* Any int is a legal status, at either extreme of the range. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MIN, 1, "min-owner") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "max-owner") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MIN, 1, "min-owner"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MAX, 1, "max-owner"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status"));
|
||||
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);
|
||||
|
||||
/* A name longer than the buffer is truncated and always terminated. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(1000000, 1, "trunc") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
|
||||
const char *long_name =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
||||
AKERR_CHECK(akerr_register_status_name("trunc", 1000000, long_name) ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("trunc", 1000000, long_name));
|
||||
char *stored = akerr_name_for_status(1000000, NULL);
|
||||
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
||||
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
||||
|
||||
/* Reservation: overlap detection, and idempotency for an exact repeat. */
|
||||
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_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "component-b"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("component-a");
|
||||
|
||||
/* The library's own 0..255 band is reserved and cannot be encroached on. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(255, 1, "component-b") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER) ==
|
||||
AKERR_STATUS_RANGE_OK); /* exact repeat by the owner */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(255, 1, "component-b"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
/* An exact repeat by the owner is still a no-op. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
|
||||
/* Argument validation. */
|
||||
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);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(INT_MAX, 2, "overflow"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 0, "empty"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("empty"); /* the message names the caller */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, -1, "negative"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, NULL),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, ""),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
|
||||
/* Owner strings: 63 chars fit, 64 do not. */
|
||||
/* Owner strings: 63 chars fit, 64 do not, and neither does anything past. */
|
||||
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
||||
char owner70[AKERR_MAX_ERROR_NAME_LENGTH + 7];
|
||||
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);
|
||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
||||
owner70[sizeof(owner70) - 1] = '\0';
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(400, 1, owner63));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(401, 1, owner64),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(402, 1, owner70),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
|
||||
/* Partial overlaps at either endpoint, and a same-range different owner. */
|
||||
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); /* subset, not an exact repeat */
|
||||
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); /* same range, wrong owner */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(500, 2, "endpoint"));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(499, 2, "left"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 1, "endpoint"),
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(501, 1, "endpoint"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 2, "other"),
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
|
||||
|
||||
/* Claim room for the name-exhaustion sweep before filling the range table. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(2000000, 100000, "fill") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(2000000, 100000, "fill"));
|
||||
|
||||
/*
|
||||
* Range table capacity. The limit is private to src/error.c on purpose, so
|
||||
* discover it by filling rather than by hardcoding it here.
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
int ranges_added = 0;
|
||||
int range_rc = AKERR_STATUS_RANGE_OK;
|
||||
akerr_ErrorContext *range_err = NULL;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
range_rc = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
||||
if ( range_rc != AKERR_STATUS_RANGE_OK ) {
|
||||
range_err = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
||||
if ( range_err != NULL ) {
|
||||
break;
|
||||
}
|
||||
ranges_added++;
|
||||
}
|
||||
AKERR_CHECK(ranges_added > 0);
|
||||
AKERR_CHECK(range_rc == AKERR_STATUS_RANGE_FULL);
|
||||
AKERR_CHECK_CONTAINS("range table is full");
|
||||
AKERR_CHECK_STATUS(range_err, AKERR_STATUS_RANGE_FULL);
|
||||
AKERR_CHECK(strstr(range_err->message, "range table is full") != NULL);
|
||||
RELEASE_ERROR(range_err);
|
||||
AKERR_CHECK(range_err == NULL);
|
||||
|
||||
/*
|
||||
* Name table capacity. Exhaustion must be reported, not silent: a dropped
|
||||
* name degrades every future stack trace for that code to "Unknown Error".
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
int full_at = -1;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "Filled %d", i);
|
||||
int rc = akerr_register_status_name("fill", 2000000 + i, name);
|
||||
if ( rc != AKERR_STATUS_NAME_OK ) {
|
||||
AKERR_CHECK(rc == AKERR_STATUS_NAME_FULL);
|
||||
akerr_ErrorContext *name_err =
|
||||
akerr_register_status_name("fill", 2000000 + i, name);
|
||||
if ( name_err != NULL ) {
|
||||
AKERR_CHECK_STATUS(name_err, AKERR_STATUS_NAME_FULL);
|
||||
AKERR_CHECK(strstr(name_err->message, "registry is full") != NULL);
|
||||
AKERR_CHECK(strstr(name_err->message, "AKERR_STATUS_NAME_SLOTS") != NULL);
|
||||
RELEASE_ERROR(name_err);
|
||||
AKERR_CHECK(name_err == NULL);
|
||||
full_at = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
AKERR_CHECK_CONTAINS("registry is full");
|
||||
AKERR_CHECK_CONTAINS("AKERR_STATUS_NAME_SLOTS");
|
||||
|
||||
/*
|
||||
* The table must actually hold everything it accepted. A probe sequence
|
||||
@@ -154,6 +157,9 @@ int main(void)
|
||||
"Unknown Error") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
||||
|
||||
/* Thousands of refusals later, every context went back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
* that happened to overlap.
|
||||
*
|
||||
* Naming a status is now permitted only inside a reservation:
|
||||
* - akerr_register_status_name() requires the range to belong to the caller;
|
||||
* - akerr_register_status_name() requires the range to belong to the caller,
|
||||
* and raises AKERR_STATUS_NAME_* when it does not;
|
||||
* - the legacy two-argument akerr_name_for_status() set path cannot identify
|
||||
* its caller, so it can only require that *some* reservation covers the
|
||||
* status -- still enough to stop a code nobody claimed.
|
||||
* Every refusal is logged, because a name that fails to register degrades the
|
||||
* status to "Unknown Error" in every later stack trace.
|
||||
* status -- still enough to stop a code nobody claimed. It returns a name
|
||||
* rather than an error context, so its refusals are logged instead.
|
||||
* Either way the refusal is visible, because a name that fails to register
|
||||
* degrades the status to "Unknown Error" in every later stack trace.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
@@ -23,68 +25,92 @@ int main(void)
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "lib-a") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(512, 16, "lib-b") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "lib-a"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(512, 16, "lib-b"));
|
||||
|
||||
/* The owner of a range may name statuses inside it. */
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 256, "A Parse Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 271, "A Last Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Parse Error"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 271, "A Last Error"));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
||||
|
||||
/* Naming another owner's status is refused and names the real owner. */
|
||||
akerr_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-b", 256, "B Hijack") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_CONTAINS("lib-a");
|
||||
AKERR_CHECK_CONTAINS("lib-b");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", 256, "B Hijack"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-b");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
||||
|
||||
/* Including the library's own reserved band. */
|
||||
akerr_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
|
||||
|
||||
/* A status nobody reserved cannot be named through either entry point. */
|
||||
akerr_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 9999, "Unclaimed") ==
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
AKERR_CHECK_CONTAINS("no reserved range");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 9999, "Unclaimed"),
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("no reserved range");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
||||
|
||||
/* The legacy path has no caller to raise into, so it logs the refusal.
|
||||
* It also cannot name the caller, and must say so rather than printing a
|
||||
* stray owner. */
|
||||
akerr_capture_reset();
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, "Unclaimed Legacy"),
|
||||
"Unknown Error") == 0);
|
||||
AKERR_CHECK_CONTAINS("no reserved range");
|
||||
AKERR_CHECK_CONTAINS("an unnamed caller");
|
||||
AKERR_CHECK_CONTAINS("REFUSED STATUS NAME");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
||||
/* ... and hands the context it raised back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* Boundaries: just outside lib-a's range is not lib-a's to name. */
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 255, "Below") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 272, "Above") ==
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 255, "Below"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 272, "Above"),
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
|
||||
/* The legacy set path still works inside any reservation. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, "B Legacy"), "B Legacy") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, NULL), "B Legacy") == 0);
|
||||
|
||||
/* Re-registering your own status overwrites the name. */
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 256, "A Renamed") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Renamed"));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Renamed") == 0);
|
||||
|
||||
/* Argument validation. */
|
||||
AKERR_CHECK(akerr_register_status_name(NULL, 257, "No Owner") ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(akerr_register_status_name("", 257, "Empty Owner") ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 257, NULL) ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
/*
|
||||
* Argument validation. Each message must identify the caller it refused,
|
||||
* since that message is the whole report a consumer gets.
|
||||
*/
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(NULL, 257, "No Owner"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("257");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("", 257, "Empty Owner"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("Empty Owner");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 257, NULL),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
||||
|
||||
/* Over-long owner strings: 63 characters fit, 64 and beyond do not. */
|
||||
char owner63[64];
|
||||
char owner64[65];
|
||||
char owner70[71];
|
||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
||||
owner63[sizeof(owner63) - 1] = '\0';
|
||||
memset(owner64, 'b', sizeof(owner64) - 1);
|
||||
owner64[sizeof(owner64) - 1] = '\0';
|
||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
||||
owner70[sizeof(owner70) - 1] = '\0';
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(600, 1, owner63));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(owner63, 600, "Long Owner"));
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner64, 600, "Too Long"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("63");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner70, 600, "Far Too Long"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(600, NULL), "Long Owner") == 0);
|
||||
|
||||
/* A refused registration must not consume a slot or leave a partial entry. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(257, NULL), "Unknown Error") == 0);
|
||||
@@ -92,6 +118,9 @@ int main(void)
|
||||
/* Lookup is unaffected by ownership -- anyone may read any name. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(271, NULL), "A Last Error") == 0);
|
||||
|
||||
/* Every refusal above released its context. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_name_ownership ok\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ int main(void)
|
||||
akerr_capture_install();
|
||||
|
||||
/* Cold call: no akerr_init(), no PREPARE_ERROR anywhere yet. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "early-lib") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("early-lib", 256, "Early Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("early-lib", 256, "Early Error"));
|
||||
|
||||
/* Something else now uses the library for the first time. */
|
||||
akerr_init();
|
||||
@@ -34,22 +32,23 @@ int main(void)
|
||||
|
||||
/* The early reservation and its name must both have survived. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Early Error") == 0);
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "late-lib") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_CONTAINS("early-lib");
|
||||
AKERR_CHECK(akerr_reserve_status_range(260, 2, "late-lib") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(256, 16, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("early-lib");
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
|
||||
/* The library's own initialization still happened exactly once. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
||||
"Null Pointer Error") == 0);
|
||||
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER) ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
|
||||
/* An identical repeat by the original owner is still idempotent. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "early-lib") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
|
||||
/* Raising from a cold registry must not strand a pool slot either. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_registry_init_order ok\n");
|
||||
return 0;
|
||||
|
||||
100
tests/err_status_exception.c
Normal file
100
tests/err_status_exception.c
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* The registry entry points report failure the same way every other function in
|
||||
* this library does: they return akerr_ErrorContext *. A refused reservation is
|
||||
* therefore an ordinary error, and everything that works on an ordinary error
|
||||
* must work on it -- CATCH, HANDLE, PASS, propagation to the caller, and the
|
||||
* stack trace an unhandled one prints.
|
||||
*
|
||||
* This is what distinguishes the current design from the integer return codes
|
||||
* it replaced: a consumer that ignores the result gets a compiler warning
|
||||
* (AKERR_NOIGNORE), and a consumer that catches it but does not handle it gets
|
||||
* the error propagated out of its init function rather than a silently
|
||||
* unreserved range.
|
||||
*/
|
||||
|
||||
#define TEST_OWNER "exception-test"
|
||||
|
||||
/* A consumer init function in the shape the documentation recommends. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *component_init(int base, const char *owner)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(base, 4, owner));
|
||||
CATCH(errctx, akerr_register_status_name(owner, base, "Component Error"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* A successful reservation raises nothing at all. */
|
||||
AKERR_CHECK_SUCCEEDS(component_init(256, TEST_OWNER));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/*
|
||||
* A collision propagates out of the component's init and is caught here.
|
||||
* HANDLE proves the status is a real, distinct exception a consumer can
|
||||
* dispatch on -- not an opaque nonzero int.
|
||||
*/
|
||||
int handled_overlap = 0;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, component_init(258, "other-component"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
||||
handled_overlap = 1;
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
handled_overlap = -1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK(handled_overlap == 1);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* The name-side refusals dispatch the same way. */
|
||||
int handled_foreign = 0;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_register_status_name("interloper", 256, "Hijack"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_NAME_FOREIGN) {
|
||||
handled_foreign = 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK(handled_foreign == 1);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
||||
|
||||
/*
|
||||
* An unhandled refusal carries a stack trace naming the real owner, so the
|
||||
* report a consumer gets is the library's normal one.
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(AKERR_VALUE, 1, "encroacher"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "Reservation refused");
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK_CONTAINS("Reservation refused");
|
||||
AKERR_CHECK_CONTAINS("Status Range Overlap");
|
||||
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
AKERR_CHECK_CONTAINS("encroacher");
|
||||
AKERR_CHECK_CONTAINS("error.c");
|
||||
|
||||
/* Nothing above stranded a pool slot. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_status_exception ok\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user