#include "akerror.h" #include "lock.h" #if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1 #include #include #include #endif // AKERR_USE_STDLIB /* * Per-thread state. * * The last-ditch context is where a failure gets reported when there is no pool * slot to report it from -- akerr_release_error(NULL). One shared copy would * have two threads formatting a message into the same buffer, so each thread * gets its own. Thread-local storage is zero initialized, which is why * akerr_last_ditch_context() below sets the stack-trace cursor lazily rather * than akerr_init() setting it for everyone: akerr_init() runs on one thread * and cannot reach the others' copies. * * It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage * is allocated per thread only when that thread first touches the library's * thread-local block, and the alternative is a shared buffer that two threads * can be writing at once. */ static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch; AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored; akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; akerr_ErrorLogFunction akerr_log_method = NULL; /* * One recursive lock over both the error pool and the status registry. See * src/lock.h for why it is one lock, and why it is recursive. * * Everything below that touches either table does so through a function whose * name ends in _locked, called from a wrapper that takes the lock and releases * it on the single return path. The wrappers exist because the locked bodies * are written with the FAIL_*_RETURN macros, which return from the middle of a * function -- so those bodies cannot be the ones holding the lock. */ static akerr_Mutex akerr_state_lock; /* * 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]; /* * Bounded copy into a fixed buffer. Every argument is checked: this writes * through a caller-supplied pointer for a caller-supplied length, so a NULL or * a non-positive capacity here is a memory error waiting to happen, not * something to absorb and return from quietly. * * Exported under the internal __akerr_ prefix rather than kept static so that * tests/err_copy_string.c can reach these guards. Both in-library callers * validate their arguments first, so nothing else can drive them. */ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity, const char *source) { PREPARE_ERROR(errctx); FAIL_NONZERO_RETURN(errctx, (destination == NULL || source == NULL), AKERR_NULLPOINTER, "__akerr_copy_string got a NULL %s", destination == NULL ? "destination buffer" : "source string"); FAIL_NONZERO_RETURN(errctx, (capacity <= 0), AKERR_VALUE, "__akerr_copy_string got a capacity of %d; a buffer must " "have room for at least a terminator", capacity); strncpy(destination, source, (size_t)capacity - 1); destination[capacity - 1] = '\0'; SUCCEED_RETURN(errctx); } /* * 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. * * Takes no lock: the addresses of the pool slots are fixed for the life of the * process, and nothing here reads a slot's contents. */ 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 } /* * The library naming its own codes. * * akerr_init() returns void and runs before any consumer frame exists, so there * is nothing to PASS an error to: this *is* the top of the stack. FINISH_NORETURN * is the library's idiom for that position -- the same one main() uses -- so an * unhandled failure prints its stack trace and goes to * akerr_handler_unhandled_error, which terminates. * * That is fatal on purpose. The library can only fail to name its own status * codes 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. * * The generated errno table calls this rather than registering names directly, * so that all of this control flow lives here in one place. */ void __akerr_name_library_status(int status, const char *name) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akerr_register_status_name(AKERR_LIBRARY_OWNER, status, name)); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); } /* * The calling thread's last-ditch context. * * Thread-local storage starts zeroed, so a NULL stack-trace cursor means this * thread has not used its copy yet. Checking the cursor rather than a separate * flag keeps the whole thing self-describing: the cursor is the one field that * must not be zero for the context to be usable at all. */ static akerr_ErrorContext *akerr_last_ditch_context(void) { if ( __akerr_last_ditch.stacktracebufptr == NULL ) { memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext)); __akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf; } return &__akerr_last_ditch; } static AKERR_THREAD_LOCAL int akerr_initializing; static akerr_Once akerr_state_once = AKERR_ONCE_INIT; /* * Runs exactly once per process, under akerr_once(). Everything it calls -- * the registry entry points, and the pool underneath them -- calls akerr_init() * itself, so the first thing it does is raise the re-entry flag; see * akerr_init() below. * * The lock is initialized before anything that could take it. That ordering is * the reason this work lives in a once-routine instead of a * check-a-flag-and-go: a second thread arriving while this one is still * populating the tables must block until they are complete, not walk them. */ static void akerr_init_state(void) { akerr_mutex_init(&akerr_state_lock); akerr_initializing = 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; (void)akerr_last_ditch_context(); 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. Terminal for the same reason as * __akerr_name_library_status(), and handled the same way: without this * band the library owns nothing, so none of the names below could * register either. */ PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT, AKERR_LIBRARY_OWNER)); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); /* 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_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 akerr_initializing = 0; } /* * Idempotent, and safe to call from any thread at any time. Every public entry * point calls it -- 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 -- which means it is also on the path of everything * akerr_init_state() itself calls. * * The re-entry guard is thread local, and has to be: only the thread running * the once-routine may skip past it. A second thread arriving mid-initialization * must block inside akerr_once() until the tables are complete, which a shared * flag set at the top of initialization would have let it walk right past. */ void akerr_init() { if ( akerr_initializing != 0 ) { return; } akerr_once(&akerr_state_once, &akerr_init_state); } /* * Every way out of the library's status space goes through here, so that a * status becomes an exit code exactly one way no matter who is leaving. * * Only 0 through AKERR_EXIT_STATUS_MAX survive the trip -- see the note on * AKERR_EXIT_STATUS_UNREPRESENTABLE in the header for why there is no wider * exit() to reach for. Everything else exits with that sentinel instead of its * low byte, because the low byte is either a lie (status 300 exiting 44, which * is some other error's code) or a disaster (status 256, the first status a * consumer can own, exiting 0 and telling the shell the program succeeded). * * Status 0 exits 0, because 0 is this library's success status and an exit code * of 0 is what success is called out here. What keeps an *unhandled* error from * exiting 0 is not this function: PROCESS opens with `case 0`, which marks a * zero status handled, so a successful context can never reach * FINISH_NORETURN's call to the handler in the first place. * * Nothing is logged here. Callers arrive from a position that has already * reported -- FINISH_NORETURN logs the stack trace, carrying the status at full * width, before it calls the handler -- and a second line naming a number the * trace already gave would only invite the reader to trust the exit code. */ void akerr_exit(int status) { if ( status < 0 || status > AKERR_EXIT_STATUS_MAX ) { exit(AKERR_EXIT_STATUS_UNREPRESENTABLE); } exit(status); } /* * The last stop for an unhandled error. A handler invoked with no error at all * has no status to report and nothing akerr_exit() could map, so it exits 1 * directly: a plain generic failure. */ void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx) { if ( errctx == NULL ) { exit(1); } akerr_exit(errctx->status); } /* * Claim the lowest free slot. Finding it and taking the reference are one * operation under the lock: a scan that returned an unclaimed slot would hand * the same one to every thread that scanned before the first of them got around * to incrementing the count. */ akerr_ErrorContext *akerr_next_error() { akerr_ErrorContext *found = (akerr_ErrorContext *)NULL; akerr_init(); akerr_mutex_lock(&akerr_state_lock); for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) { found = &AKERR_ARRAY_ERROR[i]; found->refcount = 1; break; } } akerr_mutex_unlock(&akerr_state_lock); return found; } /* * The wipe returns the slot to the pool, so it and the decrement that triggers * it are one operation under the lock. Otherwise a thread that saw the count * reach zero could be handed the slot by akerr_next_error() and start writing * its error into it while the releasing thread was still memsetting it. */ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err) { int oldid = 0; akerr_ErrorContext *remaining = err; akerr_init(); if ( err == NULL ) { akerr_ErrorContext *errctx = akerr_last_ditch_context(); FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer"); } akerr_mutex_lock(&akerr_state_lock); 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; remaining = NULL; } akerr_mutex_unlock(&akerr_state_lock); return remaining; } /* * 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. Caller holds akerr_state_lock. * * 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. Caller * holds akerr_state_lock. */ 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 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. * * Caller holds akerr_state_lock. The FAIL_* macros below re-enter the library * to build their error -- a pool slot from akerr_next_error(), and a status * name for the stack trace -- and that re-entry is why the lock is recursive. */ static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name_locked(const char *owner, int status, const char *name) { akerr_StatusRange *range; akerr_StatusName *entry; PREPARE_ERROR(errctx); FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID, "Refusing to name status %d for %s: the name is NULL", status, owner == NULL ? "an unnamed caller" : owner); FAIL_NONZERO_RETURN(errctx, (owner != NULL && ( owner[0] == '\0' || strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH )), 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); FAIL_ZERO_RETURN(errctx, range, 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); FAIL_NONZERO_RETURN(errctx, (owner != NULL && strcmp(owner, range->owner) != 0), 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); FAIL_ZERO_RETURN(errctx, entry, 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); PASS(errctx, __akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name)); SUCCEED_RETURN(errctx); } /* * Register a name for a status inside a range the caller reserved. Both strings * are checked here rather than only inside akerr_store_status_name_locked(): the * store accepts a NULL owner for the legacy akerr_name_for_status() path, so a * NULL arriving through *this* entry point would be read as "caller did not * identify itself" and skip the ownership check entirely. * * Caller holds akerr_state_lock. */ static akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name_locked(const char *owner, int status, const char *name) { PREPARE_ERROR(errctx); FAIL_NONZERO_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); FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID, "Refusing to name status %d for %s: the name is NULL", status, owner); PASS(errctx, akerr_store_status_name_locked(owner, status, name)); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name) { akerr_ErrorContext *errctx; akerr_init(); akerr_mutex_lock(&akerr_state_lock); errctx = akerr_register_status_name_locked(owner, status, name); akerr_mutex_unlock(&akerr_state_lock); return errctx; } /* * Return or set a name. Status magnitude is unrelated to storage size. * * The set path is the legacy two-argument form. It returns a name, so it cannot * hand an error back to its caller and cannot raise: it handles the refusal * here, converting it to the "Unknown Error" sentinel the way any function that * must return a value converts a caught error into one. * akerr_register_status_name() is the form that raises. * * The lookup path (name == NULL) deliberately stays clear of all of this. FAIL * calls it to render a status into a stack trace, so it must not itself need an * error context. * * The name is returned by pointer into the registry, which never resizes and * never removes an entry, so the pointer is good for the life of the process. * Its *contents* are stable as long as nobody registers a second name for the * same status: a rename overwrites the buffer in place, and a lookup on another * thread can be reading it. Register names during initialization -- renaming a * live status while other threads run is the one registry operation the lock * cannot make safe, because the reader is outside it by then. */ static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner, int status, const char *name) { akerr_ErrorContext *errctx; akerr_mutex_lock(&akerr_state_lock); errctx = akerr_store_status_name_locked(owner, status, name); akerr_mutex_unlock(&akerr_state_lock); return errctx; } char *akerr_name_for_status(int status, char *name) { akerr_StatusName *entry; char *found = "Unknown Error"; akerr_init(); if ( name != NULL ) { PREPARE_ERROR(errctx); int refused = 0; /* The store takes and releases the lock itself, so the handler below * calls akerr_log_method -- consumer code, which may do anything at all * including calling back into this library -- without holding it. */ ATTEMPT { CATCH(errctx, akerr_store_status_name(NULL, status, name)); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **"); refused = 1; } FINISH_NORETURN(errctx); if ( refused != 0 ) { return "Unknown Error"; } } akerr_mutex_lock(&akerr_state_lock); entry = akerr_status_slot(status, 0); if ( entry != NULL ) { found = entry->name; } akerr_mutex_unlock(&akerr_state_lock); return found; } /* Reserve an inclusive status interval and reject collisions. Caller holds * akerr_state_lock: the overlap scan and the entry that follows it are one * decision, so two threads claiming overlapping ranges at once must not be able * to both find the table clear. */ static akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range_locked(int first_status, int count, const char *owner) { int last_status; PREPARE_ERROR(errctx); FAIL_NONZERO_RETURN(errctx, (count <= 0 || owner == NULL || owner[0] == '\0' || strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH || first_status > INT_MAX - (count - 1)), 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; 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 ) { SUCCEED_RETURN(errctx); } 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); } } FAIL_NONZERO_RETURN(errctx, (akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES), 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); /* The owner copy is what commits the entry, so the count only advances * once it has succeeded -- a half-written reservation would claim the * range under an empty owner nobody could ever match. */ akerr_status_ranges[akerr_status_range_count].first = first_status; akerr_status_ranges[akerr_status_range_count].last = last_status; PASS(errctx, __akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner)); akerr_status_range_count++; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner) { akerr_ErrorContext *errctx; akerr_init(); akerr_mutex_lock(&akerr_state_lock); errctx = akerr_reserve_status_range_locked(first_status, count, owner); akerr_mutex_unlock(&akerr_state_lock); return errctx; }