#include "akerror.h" #if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1 #include #include #include #endif // AKERR_USE_STDLIB akerr_ErrorContext __akerr_last_ditch; akerr_ErrorContext *__akerr_last_ignored; akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; akerr_ErrorLogFunction akerr_log_method = NULL; /* * Status-name registry. * * Storage is an open-addressed hash table keyed by status value. Both sizes are * private to this translation unit -- they are deliberately NOT in the public * header, because a consumer-visible table bound is exactly the ABI hazard this * registry replaced. Overriding them changes only this file, so a library and * its consumers can never disagree about the layout. * * The table is never resized or rehashed, so a pointer handed out by * akerr_name_for_status() stays valid for the life of the process. Entries are * never removed, so probing needs no tombstones. */ #ifndef AKERR_STATUS_NAME_SLOTS #define AKERR_STATUS_NAME_SLOTS 4096 #endif #ifndef AKERR_MAX_RESERVED_STATUS_RANGES #define AKERR_MAX_RESERVED_STATUS_RANGES 64 #endif /* Probing masks with SLOTS-1, so the slot count must be a power of two. This is * the C99-portable spelling of a static assertion (a negative array bound). */ typedef char akerr_assert_name_slots_pow2[ (AKERR_STATUS_NAME_SLOTS > 0 && (AKERR_STATUS_NAME_SLOTS & (AKERR_STATUS_NAME_SLOTS - 1)) == 0) ? 1 : -1]; /* Cap occupancy at 75% so linear probing always meets an empty slot. */ #define AKERR_MAX_REGISTERED_STATUS_NAMES \ (AKERR_STATUS_NAME_SLOTS - (AKERR_STATUS_NAME_SLOTS / 4)) #define AKERR_MAX_STATUS_RANGE_OWNER_LENGTH 64 typedef struct { int status; int used; 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_STATUS_NAME_SLOTS]; static int akerr_status_name_count; static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES]; static int akerr_status_range_count; akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR]; static void akerr_copy_string(char *destination, int capacity, const char *source) { if ( capacity <= 0 ) { return; } strncpy(destination, source, (size_t)capacity - 1); destination[capacity - 1] = '\0'; } /* * Compare against each element address rather than testing the address range. * A range test also accepts pointers into the *interior* of an element, which * would then be treated as the head of an akerr_ErrorContext and written * through. Keep this an element-wise scan; it is not a missed optimization. */ int akerr_valid_error_address(akerr_ErrorContext *ptr) { // Is this within the memory region occupied by AKERR_ARRAY_ERROR? if ( ptr == NULL ) { return 1; } for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { if ( ptr == &AKERR_ARRAY_ERROR[i] ) { return 1; } } return 0; } void akerr_default_logger(const char *fmt, ...) { #if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1 va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); #else return; #endif } /* * 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 * a consumer reserving its range before anything else touches the library * cannot have that reservation wiped by a later first-use of the pool -- see * themselves as already initialized instead of recursing. */ void akerr_init() { static int inited = 0; if ( inited == 0 ) { inited = 1; for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext)); AKERR_ARRAY_ERROR[i].arrayid = i; AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf; } __akerr_last_ignored = NULL; memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext)); __akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf; if ( akerr_log_method == NULL ) { akerr_log_method = &akerr_default_logger; } akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error; 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. * 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); /* 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"); #if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB)) akerr_init_errno(); #endif } } void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx) { if ( errctx == NULL ) { exit(1); } exit(errctx->status); } akerr_ErrorContext *akerr_next_error() { for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) { return &AKERR_ARRAY_ERROR[i]; } } return (akerr_ErrorContext *)NULL; } akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err) { int oldid = 0; if ( err == NULL ) { akerr_ErrorContext *errctx = &__akerr_last_ditch; FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer"); } if ( err->refcount > 0 ) { err->refcount -= 1; } if ( err->refcount == 0 ) { oldid = err->arrayid; memset(err, 0x00, sizeof(akerr_ErrorContext)); err->stacktracebufptr = (char *)&err->stacktracebuf; err->arrayid = oldid; return NULL; } return err; } /* * Scatter the status across the table. Status values are typically dense runs * (errno 1..N, then a library's block at its base), which linear probing on the * raw value would pile into one cluster, so mix the bits first. */ static unsigned akerr_status_hash(int status) { unsigned h = (unsigned)status; h ^= h >> 16; h *= 0x85ebca6bu; h ^= h >> 13; h *= 0xc2b2ae35u; h ^= h >> 16; return h & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1); } /* * Find the slot holding `status`. With create != 0, claim a free slot for it if * it is not present yet. Returns NULL when the status is absent and either no * slot was requested or the registry is full. * * The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails * silently: off by one in either direction and the probe indexes past * akerr_status_names, writing into whatever BSS follows rather than crashing. * A test that only counts how many names registered before the table filled * cannot see that -- a probe sequence collapsed to two slots still registers * "some" names. Any change to the probe sequence, the occupancy cap, or the * power-of-two assumption needs a test that reads every entry back by its own * distinct value; tests/err_maxval.c does. */ static akerr_StatusName *akerr_status_slot(int status, int create) { unsigned slot = akerr_status_hash(status); for ( int probe = 0; probe < AKERR_STATUS_NAME_SLOTS; probe++ ) { akerr_StatusName *entry = &akerr_status_names[slot]; if ( entry->used == 0 ) { if ( create == 0 || akerr_status_name_count >= AKERR_MAX_REGISTERED_STATUS_NAMES ) { return NULL; } entry->used = 1; entry->status = status; entry->name[0] = '\0'; akerr_status_name_count++; return entry; } if ( entry->status == status ) { return entry; } slot = (slot + 1u) & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1); } /* Unreachable: occupancy is capped below the slot count, so the probe above * always meets a free slot. Present so a future change to that cap cannot * turn this into a runaway loop. */ return NULL; } /* The reservation covering `status`, or NULL if nobody has claimed it. */ static akerr_StatusRange *akerr_range_for_status(int status) { for ( int i = 0; i < akerr_status_range_count; i++ ) { if ( status >= akerr_status_ranges[i].first && status <= akerr_status_ranges[i].last ) { return &akerr_status_ranges[i]; } } return NULL; } /* * 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. */ static int akerr_store_status_name(const char *owner, int status, const char *name) { akerr_StatusRange *range; akerr_StatusName *entry; if ( name == NULL ) { return AKERR_STATUS_NAME_INVALID; } if ( owner != NULL && ( owner[0] == '\0' || strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ) ) { return AKERR_STATUS_NAME_INVALID; } 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; } 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; } 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; } akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name); return AKERR_STATUS_NAME_OK; } /* 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_init(); if ( owner == NULL ) { return AKERR_STATUS_NAME_INVALID; } return akerr_store_status_name(owner, status, name); } /* Return or set a name. Status magnitude is unrelated to storage size. */ 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"; } entry = akerr_status_slot(status, 0); if ( entry == NULL ) { return "Unknown Error"; } return entry->name; } /* Reserve an inclusive status interval and reject collisions. */ int akerr_reserve_status_range(int first_status, int count, const char *owner) { int last_status; 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; } 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 ) { 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; } 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; }