#include #include #include #include #include #include #include #include #include #include "aksl_internal.h" /* * Version of the library itself. These read the AKSL_VERSION_* macros as they * were when *this translation unit* was compiled, which is what makes them the * loaded library's version rather than the caller's: the caller's copy of the * same macros comes from whatever akstdlib_version.h it compiled against. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, major, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p", (void *)major, (void *)minor, (void *)patch); FAIL_ZERO_RETURN(e, minor, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p", (void *)major, (void *)minor, (void *)patch); FAIL_ZERO_RETURN(e, patch, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p", (void *)major, (void *)minor, (void *)patch); *major = AKSL_VERSION_MAJOR; *minor = AKSL_VERSION_MINOR; *patch = AKSL_VERSION_PATCH; SUCCEED_RETURN(e); } const char *aksl_version_string(void) { return AKSL_VERSION_STRING; } const char *aksl_version_soname(void) { return AKSL_VERSION_SONAME; } /* * Compatibility is "same soname". Pre-1.0 that is MAJOR.MINOR, so both are * compared and patch is deliberately ignored; from 1.0 the minor comparison * comes out, in step with the SOVERSION expression in CMakeLists.txt. The * caller's numbers arrive as arguments because AKSL_VERSION_CHECK() expanded * them at the caller's site -- see the macro in akstdlib.h. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch) { PREPARE_ERROR(e); (void)patch; FAIL_NONZERO_RETURN(e, (major != AKSL_VERSION_MAJOR || minor != AKSL_VERSION_MINOR), AKERR_VALUE, "compiled against libakstdlib %d.%d.%d, loaded %s (soname %s)", major, minor, patch, AKSL_VERSION_STRING, AKSL_VERSION_SONAME); SUCCEED_RETURN(e); } /* * malloc(0) is the awkward case, and it is why *dst is cleared first and errno * is cleared before the call. The standard lets malloc(0) return either a unique * pointer or NULL, and a NULL return there is not a failure and need not set * errno -- so the old code raised an error whose status was whatever errno * happened to be holding, quite possibly 0. A zero-size request is rejected on * its own terms instead, since there is nothing useful to hand back either way. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); *dst = NULL; FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation"); errno = 0; *dst = malloc(size); FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", size); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void **dst) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); *dst = NULL; FAIL_ZERO_RETURN(e, nmemb, AKERR_VALUE, "zero-size allocation (nmemb=0)"); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation (size=0)"); errno = 0; *dst = calloc(nmemb, size); FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), "%zu x %zu bytes", nmemb, size); SUCCEED_RETURN(e); } /* * realloc(3)'s trap, hidden: on failure it returns NULL *and leaves the original * block valid*, so the near-universal `p = realloc(p, n)` leaks the original * every time it fails. Here the old pointer goes in and out through the same * out-param, and is left untouched -- still valid, still the caller's to free -- * whenever an error is raised. TODO.md 3.1. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size) { void *resized = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p", (void *)ptr); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size realloc; use aksl_free to release the block"); errno = 0; resized = realloc(*ptr, size); FAIL_ZERO_RETURN(e, resized, AKSL_ERRNO_OR(ENOMEM), "%zu bytes; the original block at %p is still valid", size, *ptr); *ptr = resized; SUCCEED_RETURN(e); } /* * reallocarray(3): a resize whose element count and element size are multiplied * *with an overflow check*. `realloc(p, n * size)` is a heap overflow waiting for * an n large enough to wrap, and the multiplication is exactly the place a * caller does not think to look. Not every platform has it, so the check is done * here rather than delegated. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_reallocarray(void **ptr, size_t nmemb, size_t size) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p", (void *)ptr); FAIL_ZERO_RETURN(e, nmemb, AKERR_VALUE, "zero-size realloc (nmemb=0)"); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size realloc (size=0)"); FAIL_NONZERO_RETURN(e, (nmemb > (size_t)-1 / size), AKERR_OUTOFBOUNDS, "%zu x %zu overflows size_t", nmemb, size); return aksl_realloc(ptr, nmemb * size); } /* * Aligned allocation. aligned_alloc(3) requires size to be a multiple of the * alignment and the alignment to be a power of two supported by the * implementation; violating either is undefined behaviour that usually just * returns NULL, so both are checked here and reported as what they are. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_aligned_alloc(size_t alignment, size_t size, void **dst) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); *dst = NULL; FAIL_ZERO_RETURN(e, alignment, AKERR_VALUE, "alignment=0"); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation"); FAIL_NONZERO_RETURN(e, ((alignment & (alignment - 1)) != 0), AKERR_VALUE, "alignment %zu is not a power of two", alignment); FAIL_NONZERO_RETURN(e, (size % alignment != 0), AKERR_VALUE, "size %zu is not a multiple of alignment %zu", size, alignment); errno = 0; *dst = aligned_alloc(alignment, size); FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), "%zu bytes aligned to %zu", size, alignment); SUCCEED_RETURN(e); } /* * posix_memalign(3) is the older interface, and the one that does not require * size to be a multiple of the alignment. It also breaks the errno convention -- * it *returns* the error number and leaves errno alone -- which is precisely the * kind of local irregularity a caller mis-handles once and never notices. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_posix_memalign(void **dst, size_t alignment, size_t size) { int failed = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); *dst = NULL; FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation"); failed = posix_memalign(dst, alignment, size); if ( failed != 0 ) { *dst = NULL; FAIL_RETURN(e, failed, "%zu bytes aligned to %zu", size, alignment); } SUCCEED_RETURN(e); } /* * free(NULL) is legal C and does nothing. This treats it as an error anyway, * deliberately and against libc: in a codebase that routes every allocation * through aksl_malloc, freeing a pointer you believed was live and finding it * NULL means something upstream did not happen, and silence there is how a * double-free or a lost allocation gets found three weeks later. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "NULL"); free(ptr); SUCCEED_RETURN(e); } /* * Frees and clears in one step, so the pointer cannot be used or freed twice. * TODO.md 2.2.13 -- aksl_free leaves the caller holding a dangling pointer, and * "remember to NULL it afterwards" is exactly the discipline this library is * supposed to make unnecessary rather than merely possible. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p", (void *)ptr); FAIL_ZERO_RETURN(e, *ptr, AKERR_NULLPOINTER, "*ptr is NULL"); free(*ptr); *ptr = NULL; SUCCEED_RETURN(e); } /* * memset(3) and memcpy(3) cannot fail. They return their destination pointer * unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and * (memcpy(...) == d) checks were dead code that read as though there were a * failure mode to catch -- TODO.md 2.2.11. What is worth checking is the * arguments, which is all that is checked now. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p", s); memset(s, c, n); SUCCEED_RETURN(e); } /* * Overlapping ranges are rejected rather than left as undefined behaviour. This * is the one place this wrapper is stricter than memcpy(3) instead of merely * louder: memcpy with overlap is UB that usually appears to work, right up until * a compiler version or a buffer length changes and it does not. Callers who * mean to overlap want aksl_memmove, which is what memmove(3) is for. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, const void *s, size_t n) { /* * Through uintptr_t: comparing two pointers that are not into the same * object is undefined behaviour in C, and "is one inside the other" is * precisely a comparison of two pointers we do not yet know are related. */ uintptr_t dst = (uintptr_t)d; uintptr_t src = (uintptr_t)s; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, d, AKERR_NULLPOINTER, "d=%p, s=%p", d, s); FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "d=%p, s=%p", d, s); FAIL_NONZERO_RETURN(e, (n > 0 && dst < src + n && src < dst + n), AKERR_VALUE, "d=%p and s=%p overlap over %zu bytes; use aksl_memmove", d, s, n); memcpy(d, s, n); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_memmove(void *d, const void *s, size_t n) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, d, AKERR_NULLPOINTER, "d=%p, s=%p", d, s); FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "d=%p, s=%p", d, s); memmove(d, s, n); SUCCEED_RETURN(e); } /* * memcmp and memchr report their answer through an out-param rather than the * return value, because the return value is already spoken for by the error * context. A memchr that finds nothing is not an error -- *dest is NULL and the * call succeeds -- since "absent" is an ordinary answer to that question. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memcmp(const void *a, const void *b, size_t n, int *dest) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", a, b, (void *)dest); FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", a, b, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", a, b, (void *)dest); *dest = memcmp(a, b, n); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, void **dest) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", s, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", s, (void *)dest); *dest = memchr(s, c, n); SUCCEED_RETURN(e); } /* * pathname and mode are checked. fopen(NULL, ...) is undefined behaviour, and * this wrapper used to hand both straight through unexamined -- reachable from * user input in practice, which is why akbasic validates the filename itself * before calling DLOAD/DSAVE with a comment pointing at TODO.md 2.2.2. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen( const char *pathname, const char *mode, FILE **fp) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "pathname=%p, mode=%p, fp=%p", (void *)pathname, (void *)mode, (void *)fp); *fp = NULL; FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, mode=%p, fp=%p", (void *)pathname, (void *)mode, (void *)fp); FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "pathname=%p, mode=%p, fp=%p", (void *)pathname, (void *)mode, (void *)fp); errno = 0; *fp = fopen(pathname, mode); FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "%s", pathname); SUCCEED_RETURN(e); } /* * fread and fwrite both report how much they actually transferred, through a * required out-param. Three things were wrong with the old pair (TODO.md 2.2.3), * and the count is the fix for the worst of them: a caller who got AKERR_EOF had * no way to find out how much data had arrived before the stream ran out, which * makes the EOF status almost useless for the partial-read case it exists to * report. *nmemb_out is always written, including on the error paths. * * The other two: ptr was never NULL-checked in either function, and a transfer * that came up short with neither feof nor ferror set fell straight through to * SUCCEED_RETURN -- a short transfer reported as complete success, which is the * exact failure mode this library exists to prevent. That case is now AKERR_IO. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fread( void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out) { size_t nmemr = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nmemb_out, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); *nmemb_out = 0; FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); errno = 0; nmemr = fread(ptr, size, nmemb, fp); *nmemb_out = nmemr; if ( nmemr != nmemb ) { FAIL_NONZERO_RETURN(e, ferror(fp), AKSL_ERRNO_OR(AKERR_IO), "read %zu of %zu members", nmemr, nmemb); FAIL_NONZERO_RETURN(e, feof(fp), AKERR_EOF, "EOF after %zu of %zu members", nmemr, nmemb); FAIL_RETURN(e, AKERR_IO, "short read: %zu of %zu members, with neither EOF nor a stream error", nmemr, nmemb); } SUCCEED_RETURN(e); } /* * ferror before feof, and no feof at all on the write path. The old version * asked feof() first -- meaningless when writing -- and reported a failed write * with the message "Error reading file". */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite( const void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out) { size_t nmemw = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nmemb_out, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); *nmemb_out = 0; FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "ptr=%p, fp=%p, nmemb_out=%p", ptr, (void *)fp, (void *)nmemb_out); errno = 0; nmemw = fwrite(ptr, size, nmemb, fp); *nmemb_out = nmemw; if ( nmemw != nmemb ) { FAIL_NONZERO_RETURN(e, ferror(fp), AKSL_ERRNO_OR(AKERR_IO), "wrote %zu of %zu members", nmemw, nmemb); FAIL_RETURN(e, AKERR_IO, "short write: %zu of %zu members, with no stream error set", nmemw, nmemb); } SUCCEED_RETURN(e); } /* * Closing an already-closed stream is undefined behaviour and there is no way to * detect it from here -- the FILE * is freed, so even reading it to check is the * bug. This wrapper cannot help; do not do it. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "NULL"); errno = 0; FAIL_NONZERO_RETURN(e, fclose(stream), AKSL_ERRNO_OR(AKERR_IO), "fclose failed, and any buffered data is lost"); SUCCEED_RETURN(e); } /* * Formatted output. * * The va_list forms below do the work and the variadic forms are thin wrappers * over them, which is both what ยง3.1 of TODO.md asked for -- so consumers can * build their own variadic wrappers -- and what makes the va_end rule below * checkable in one place instead of three. * * Three things every one of these now does that the old versions did not: * * - va_end matches every va_start, on the failure path as well as the happy * one. Omitting it is undefined behaviour per the standard and leaks * register-save state on some ABIs. akbasic's text sink ran this UB on every * line of program output without anything visibly misbehaving, which is * exactly what made it worth fixing before something did. TODO.md 2.1.4. * - *count is written on every path. It used to be left holding vprintf's -1 * after a failure, so a caller who read the length rather than the status got * a negative byte count out of a function that had already failed. It is now * 0 whenever an error is raised. * - errno is cleared before the call and read back through AKSL_ERRNO_OR, so a * failure can never be reported with a stale -- or with a zero -- status. * * aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an * error-handling wrapper around an unbounded write is precisely the sharp edge * this library exists to remove. aksl_snprintf replaces it, and treats * truncation as the failure it is rather than as a short success. TODO.md 2.2.4. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args) { int written = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format); *count = 0; FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format); errno = 0; written = vprintf(format, args); FAIL_NONZERO_RETURN(e, (written < 0), AKSL_ERRNO_OR(AKERR_IO), "Short write"); *count = written; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); raised = aksl_vprintf(count, format, args); va_end(args); return raised; } akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stream, const char *restrict format, va_list args) { int written = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format); *count = 0; FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format); FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format); errno = 0; written = vfprintf(stream, format, args); FAIL_NONZERO_RETURN(e, (written < 0), AKSL_ERRNO_OR(AKERR_IO), "Short write"); *count = written; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); raised = aksl_vfprintf(count, stream, format, args); va_end(args); return raised; } /* * Truncation is an error, not a short success. snprintf(3) reports the length it * *would* have written and silently drops the rest, which is the single most * common way a bounded write goes wrong unnoticed; a caller who wanted to know * would have had to compare the return against the buffer size by hand, which is * the check this library exists to stop people forgetting. *count is the number * of bytes written excluding the terminating NUL, and is 0 on any failure. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args) { int needed = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); *count = 0; FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format); FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL"); errno = 0; needed = vsnprintf(str, size, format, args); FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error"); FAIL_NONZERO_RETURN(e, ((size_t)needed >= size), AKERR_OUTOFBOUNDS, "output truncated: %d bytes needed, %zu available", needed, size); *count = needed; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); raised = aksl_vsnprintf(count, str, size, format, args); va_end(args); return raised; } /* * The allocating form: no buffer, no size, no truncation to worry about. Where * aksl_snprintf is right for a fixed destination, this is right when the length * is not knowable in advance and the result is short-lived enough not to want a * whole aksl_StrBuf. * * asprintf(3) is a GNU extension, so it is built here on the two-pass vsnprintf * measure-then-write rather than called, which keeps this off _GNU_SOURCE. *dest * is the caller's to release with aksl_free. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const char *restrict format, va_list args) { va_list measure; int needed = 0; char *buf = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", (void *)count, (void *)dest, (void *)format); *count = 0; FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", (void *)count, (void *)dest, (void *)format); *dest = NULL; FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", (void *)count, (void *)dest, (void *)format); /* * Measured with a copy, because a vsnprintf consumes the va_list and using * the same one twice is undefined behaviour rather than merely wrong. */ va_copy(measure, args); needed = vsnprintf(NULL, 0, format, measure); va_end(measure); FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "could not format the arguments"); ATTEMPT { CATCH(e, aksl_malloc((size_t)needed + 1, (void **)&buf)); } CLEANUP { } PROCESS(e) { } FINISH(e, true); /* * No second check on the write, and no cleanup path for one. * * The buffer was sized by vsnprintf from this same format string and these * same arguments a few lines up, so the write cannot truncate. Guarding it * anyway would mean a failure branch nothing can reach and a free() beneath * it that nothing can execute -- dead code that reads as though there were a * failure mode to catch, which is exactly what TODO.md 2.2.11 recorded * against the old aksl_memset and aksl_memcpy and what removing those was * for. The invariant is stated here instead, where it can be read. */ vsnprintf(buf, (size_t)needed + 1, format, args); *dest = buf; *count = needed; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const char *restrict format, ...) { va_list args; akerr_ErrorContext *raised = NULL; va_start(args, format); raised = aksl_vasprintf(count, dest, format, args); va_end(args); return raised; } /* * String to number. * * The ato* family used to be the one place in this library where a libc failure * passed unnoticed: atoi("not a number") handed back success and 0, and * atoi("99999999999999999999") handed back success and a wrapped value, because * atoi(3) has no error channel at all. A library whose entire value proposition * is turning silent libc failures into error contexts cannot ship that. * TODO.md 2.1.5. * * The strto* wrappers below are the real implementation and the ato* wrappers * are three-line calls into them, which is what TODO.md 3.1 asked for on its own * account -- akbasic had to hand-write ~60 lines of exactly this (its * src/convert.c) because the library would not do it. * * The contract, for every function here: * * no digits consumed -> AKERR_VALUE * trailing junk, endptr NULL -> AKERR_VALUE * value outside the type -> ERANGE * *dest is 0 on every failure * * Pass a non-NULL endptr to opt out of the trailing-junk check and parse a * number out of the front of a longer string; the ato* forms pass NULL, so they * require the whole string to be a number. Leading whitespace is accepted * throughout, as strtol(3) accepts it. */ /* * strtoul(3) and strtoull(3) accept a leading '-' and hand back the negation * wrapped into the unsigned range, so "-1" parses as ULONG_MAX and reports no * error whatsoever. That is a silent failure of precisely the kind this library * exists to surface, so the sign is rejected before the call rather than after. */ static int has_negative_sign(const char *nptr) { while ( isspace((unsigned char)*nptr) ) { nptr++; } return (*nptr == '-') ? 1 : 0; } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtol(const char *nptr, char **endptr, int base, long *dest) { char *end = NULL; long value = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0; errno = 0; value = strtol(nptr, &end, base); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for long", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoll(const char *nptr, char **endptr, int base, long long *dest) { char *end = NULL; long long value = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0; errno = 0; value = strtoll(nptr, &end, base); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for long long", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoul(const char *nptr, char **endptr, int base, unsigned long *dest) { char *end = NULL; unsigned long value = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0; FAIL_NONZERO_RETURN(e, has_negative_sign(nptr), AKERR_VALUE, "\"%s\" is negative and the destination is unsigned", nptr); errno = 0; value = strtoul(nptr, &end, base); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for unsigned long", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoull(const char *nptr, char **endptr, int base, unsigned long long *dest) { char *end = NULL; unsigned long long value = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0; FAIL_NONZERO_RETURN(e, has_negative_sign(nptr), AKERR_VALUE, "\"%s\" is negative and the destination is unsigned", nptr); errno = 0; value = strtoull(nptr, &end, base); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for unsigned long long", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } /* * The floating-point forms report ERANGE for underflow as well as overflow, * because that is the only signal strtod(3) gives and losing all the significant * digits of "1e-400" is a conversion failure however it is spelled. "inf", * "infinity" and "nan" are accepted, in any case, since strtod accepts them and * they are exact round-trips rather than approximations of something else. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtod(const char *nptr, char **endptr, double *dest) { char *end = NULL; double value = 0.0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0.0; errno = 0; value = strtod(nptr, &end); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for double", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtof(const char *nptr, char **endptr, float *dest) { char *end = NULL; float value = 0.0f; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0.0f; errno = 0; value = strtof(nptr, &end); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for float", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_strtold(const char *nptr, char **endptr, long double *dest) { char *end = NULL; long double value = 0.0L; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, nptr, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0.0L; errno = 0; value = strtold(nptr, &end); FAIL_NONZERO_RETURN(e, (end == nptr), AKERR_VALUE, "no digits in \"%s\"", nptr); FAIL_NONZERO_RETURN(e, (errno == ERANGE), ERANGE, "\"%s\" is out of range for long double", nptr); FAIL_NONZERO_RETURN(e, (errno != 0), errno, "\"%s\"", nptr); if ( endptr != NULL ) { *endptr = end; } else { FAIL_NONZERO_RETURN(e, (*end != '\0'), AKERR_VALUE, "trailing junk \"%s\" after the number in \"%s\"", end, nptr); } *dest = value; SUCCEED_RETURN(e); } /* * The ato* forms keep their libc-compatible names and signatures but not libc's * silence: base 10, the whole string must be a number, and a value that does not * fit the destination type is ERANGE. "0x10" is therefore AKERR_VALUE here -- * base 10 stops at the 'x' and the rest is trailing junk. Reach for * aksl_strtol(nptr, NULL, 0, &dest) if you want the 0x/0 prefixes honoured. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_atoi(const char *nptr, int *dest) { long value = 0; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "nptr=%p, dest=%p", (void *)nptr, (void *)dest); *dest = 0; ATTEMPT { CATCH(e, aksl_strtol(nptr, NULL, 10, &value)); /* * strtol already caught anything outside long. This narrows to int, and * is compiled out where the two are the same width -- leaving it in * would be a comparison that -Wtype-limits can prove is never true. */ #if LONG_MAX > INT_MAX FAIL_NONZERO_BREAK(e, (value < INT_MIN || value > INT_MAX), ERANGE, "\"%s\" is out of range for int", nptr); #endif *dest = (int)value; } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_atol(const char *nptr, long *dest) { return aksl_strtol(nptr, NULL, 10, dest); } akerr_ErrorContext AKERR_NOIGNORE *aksl_atoll(const char *nptr, long long *dest) { return aksl_strtoll(nptr, NULL, 10, dest); } akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest) { return aksl_strtod(nptr, NULL, dest); } /* * realpath(3), with the destination buffer made expressible. * * The old two-argument form had three separate problems, all of them reachable * from ordinary use (TODO.md 2.1.6): resolved_path was never NULL-checked, so * realpath(path, NULL) allocated a buffer the wrapper then discarded and leaked; * there was no way for a caller to say how big the buffer was, so everyone had * to know to supply PATH_MAX bytes; and the failure path formatted resolved_path * with %s even though realpath(3) leaves its contents unspecified on failure -- * so for the normal caller, who passed an uninitialised stack buffer, the error * path itself read uninitialised memory and could crash. * * Taking the length fixes the first two and lets this refuse an undersized * buffer up front, since realpath(3) offers no way to bound its own write. The * message names the input path only. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path, size_t buflen) { char *result = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, resolved_path=%p", (void *)path, (void *)resolved_path); FAIL_ZERO_RETURN(e, resolved_path, AKERR_NULLPOINTER, "path=%p, resolved_path=%p", (void *)path, (void *)resolved_path); FAIL_NONZERO_RETURN(e, (buflen < PATH_MAX), AKERR_OUTOFBOUNDS, "buflen %zu is below PATH_MAX (%d); realpath(3) cannot be bounded " "below it, so a shorter buffer cannot be used safely", buflen, PATH_MAX); resolved_path[0] = '\0'; errno = 0; result = realpath(path, resolved_path); FAIL_ZERO_RETURN(e, result, AKSL_ERRNO_OR(AKERR_IO), "path=%s", path); SUCCEED_RETURN(e); } /* * The allocating variant. realpath(path, NULL) sizes and allocates the buffer * itself, which is the right answer when the caller has no PATH_MAX-sized buffer * to hand; *dest is the caller's to release with aksl_free. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path, char **dest) { char *result = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest); *dest = NULL; errno = 0; result = realpath(path, NULL); FAIL_ZERO_RETURN(e, result, AKSL_ERRNO_OR(AKERR_IO), "path=%s", path); *dest = result; SUCCEED_RETURN(e); } /* * djb2 over an explicit length, so embedded NUL bytes hash like any other byte. * * The cursor is an unsigned char *, not a char *. On x86 and ARM Linux plain * char is signed, so every byte >= 0x80 used to contribute a sign-extended * negative value: the hash differed from canonical djb2, and -- worse -- differed * between platforms depending on the signedness of char. Benign for the 7-bit * ASCII identifiers akbasic hashes, and quietly wrong for the first caller to * key a table on a filename or a UTF-8 string literal. TODO.md 2.2.6. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval) { const unsigned char *cursor = NULL; uint32_t h = 5381; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); FAIL_ZERO_RETURN(e, hashval, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); cursor = (const unsigned char *)str; while (len--) { h = ((h << 5) + h) + *cursor++; } *hashval = h; SUCCEED_RETURN(e); } /* The NUL-terminated convenience form TODO.md 3.6 asked for. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, hashval=%p", (void *)str, (void *)hashval); return aksl_strhash_djb2(str, strlen(str), hashval); } akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list"); FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "obj"); aksl_ListNode *slow = list; aksl_ListNode *fast = list; aksl_ListNode *tail = list; /* * Two separate walks, deliberately. The Floyd pass answers "is this list * finite"; it says nothing about where the tail is, because `slow` stops at * the midpoint. Conflating the two is what made this function truncate every * list of two or more nodes -- see TODO.md 2.1.1. */ while ( fast != NULL && fast->next != NULL ) { slow = slow->next; fast = fast->next->next; if ( fast == slow) { FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)list); } } /* * Only now is the list known to terminate, so this walk is guaranteed to. * It doubles as the aliasing check: appending a node that is already in the * list would relink it behind itself and orphan everything between its old * position and the tail, so it is refused. The walk was happening anyway, * which is what makes the check free. */ FAIL_NONZERO_RETURN(e, (tail == obj), AKERR_VALUE, "obj %p is already the head of this list", (void *)obj); while ( tail->next != NULL ) { tail = tail->next; FAIL_NONZERO_RETURN(e, (tail == obj), AKERR_VALUE, "obj %p is already in this list", (void *)obj); } tail->next = obj; obj->next = NULL; obj->prev = tail; SUCCEED_RETURN(e); } /* * `head` is required, not optional. Popping the head node used to leave the * caller's own head pointer aimed at a node that is no longer in the list, and * there was no way for the caller to learn the new one -- TODO.md 2.2.12. Taking * the head by reference makes the correct call the only call that compiles. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, head, AKERR_NULLPOINTER, "head=%p, node=%p", (void *)head, (void *)node); FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "head=%p, node=%p", (void *)head, (void *)node); if ( node->prev != NULL ) { node->prev->next = node->next; } if ( node->next != NULL ) { node->next->prev = node->prev; } if ( *head == node ) { *head = node->next; } node->next = NULL; node->prev = NULL; SUCCEED_RETURN(e); } /* * Zeroing initialisers. Every caller previously had to remember to memset a node * before its first use, because append and iterate both read next/prev -- and a * stack-allocated node that skipped it walked into garbage. TODO.md 2.2.14. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); node->data = data; node->next = NULL; node->prev = NULL; SUCCEED_RETURN(e); } akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void *leaf) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node"); node->parent = NULL; node->left = NULL; node->right = NULL; node->leaf = leaf; SUCCEED_RETURN(e); } /** @cond INTERNAL */ /* * One entry in the breadth-first traversal queue. Internal, and deliberately not * aksl_ListNode: the walk needs to carry each node's depth alongside it, which is * how a cyclic tree is stopped without a visited-set. The caller's lalloc/lfree * allocate and release these -- which is what they were always documented to do * and never actually did (TODO.md 2.2.8). */ typedef struct TreeQueueEntry { struct TreeQueueEntry *next; aksl_TreeNode *node; int depth; } TreeQueueEntry; /** @endcond */ /* Append one tree node to the tail of the traversal queue. */ static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_push( TreeQueueEntry **head, TreeQueueEntry **tail, aksl_AllocFunc lalloc, aksl_TreeNode *node, int depth) { TreeQueueEntry *entry = NULL; PREPARE_ERROR(e); ATTEMPT { CATCH(e, lalloc(sizeof(TreeQueueEntry), (void **)&entry)); FAIL_ZERO_BREAK(e, entry, AKERR_NULLPOINTER, "allocator returned success but no memory"); entry->next = NULL; entry->node = node; entry->depth = depth; if ( *tail == NULL ) { *head = entry; } else { (*tail)->next = entry; } *tail = entry; } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } /* * Release every entry still queued. Called from the CLEANUP block of the walk, * so it runs on the error and break paths as well as the successful one. An * lfree that itself fails cannot be allowed to abandon the rest of the queue, so * the first such error is remembered and returned only once the queue is empty. */ static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_drain( TreeQueueEntry **head, TreeQueueEntry **tail, aksl_FreeFunc lfree) { TreeQueueEntry *entry = NULL; akerr_ErrorContext *first = NULL; akerr_ErrorContext *this = NULL; PREPARE_ERROR(e); while ( *head != NULL ) { entry = *head; *head = entry->next; this = lfree(entry); if ( this != NULL && first == NULL ) { first = this; } else if ( this != NULL ) { this = akerr_release_error(this); } } *tail = NULL; if ( first != NULL ) { return first; } SUCCEED_RETURN(e); } /* * Breadth-first walk. Consumes the queue it is given; on any error it returns * with the remainder still queued, and the caller's CLEANUP drains it. * * AKSL_TREE_SEARCH_BFS enqueues left before right, AKSL_TREE_SEARCH_BFS_RIGHT * the other way round. A tree whose child points back at an ancestor grows the * recorded depth without bound rather than looping forever, so it is caught by * the depth cap -- as AKERR_OUTOFBOUNDS, not AKERR_CIRCULAR_REFERENCE, because * unlike the depth-first walk this one has no ancestor chain to compare against. */ static akerr_ErrorContext AKERR_NOIGNORE *tree_bfs_walk( TreeQueueEntry **head, TreeQueueEntry **tail, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data) { TreeQueueEntry *entry = NULL; aksl_TreeNode *node = NULL; aksl_TreeNode *first = NULL; aksl_TreeNode *second = NULL; int depth = 0; PREPARE_ERROR(e); while ( *head != NULL ) { entry = *head; *head = entry->next; if ( *head == NULL ) { *tail = NULL; } node = entry->node; depth = entry->depth; /* * Release the entry before doing anything that can fail, so the queue * the caller's CLEANUP drains never contains an entry we already * detached from it. */ PASS(e, lfree(entry)); FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, "tree deeper than AKSL_TREE_MAX_DEPTH (%d) at node %p", AKSL_TREE_MAX_DEPTH, (void *)node); PASS(e, iter(node, data)); if ( searchmode == AKSL_TREE_SEARCH_BFS_RIGHT ) { first = node->right; second = node->left; } else { first = node->left; second = node->right; } if ( first != NULL ) { PASS(e, tree_queue_push(head, tail, lalloc, first, depth + 1)); } if ( second != NULL ) { PASS(e, tree_queue_push(head, tail, lalloc, second, depth + 1)); } } SUCCEED_RETURN(e); } /* * The depth-first recursion. * * Split out from aksl_tree_iterate so that AKERR_ITERATOR_BREAK *propagates*. * This frame has no HANDLE block for it, so a break raised by the callback * unwinds every frame up to the public entry point, which swallows it exactly * once. In the old single-function form the frame that raised the break handled * it and returned success, the parent's PASS therefore saw nothing wrong, and * the walk carried on into the sibling subtree -- TODO.md 2.1.3. * * `path` is the chain of ancestors of `root` and `depth` its length. Checking * each node against its own ancestry turns a tree that loops back on itself from * infinite recursion into AKERR_CIRCULAR_REFERENCE, and the depth cap catches the * degenerate chain that is merely too deep to recurse over (TODO.md 2.2.7). */ static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk( aksl_TreeNode *root, aksl_TreeNodeIterator iter, uint8_t searchmode, void *data, aksl_TreeNode **path, int depth) { int i = 0; PREPARE_ERROR(e); FAIL_NONZERO_RETURN(e, (depth >= AKSL_TREE_MAX_DEPTH), AKERR_OUTOFBOUNDS, "tree deeper than AKSL_TREE_MAX_DEPTH (%d) at node %p", AKSL_TREE_MAX_DEPTH, (void *)root); for ( i = 0; i < depth; i++ ) { FAIL_NONZERO_RETURN(e, (path[i] == root), AKERR_CIRCULAR_REFERENCE, "node %p is its own ancestor at depth %d", (void *)root, depth); } path[depth] = root; ATTEMPT { switch ( searchmode ) { case AKSL_TREE_SEARCH_DFS_PREORDER: CATCH(e, iter(root, data)); if ( root->left != NULL ) { PASS(e, tree_dfs_walk(root->left, iter, searchmode, data, path, depth + 1)); } if ( root->right != NULL ) { PASS(e, tree_dfs_walk(root->right, iter, searchmode, data, path, depth + 1)); } break; case AKSL_TREE_SEARCH_DFS_POSTORDER: if ( root->left != NULL ) { PASS(e, tree_dfs_walk(root->left, iter, searchmode, data, path, depth + 1)); } if ( root->right != NULL ) { PASS(e, tree_dfs_walk(root->right, iter, searchmode, data, path, depth + 1)); } CATCH(e, iter(root, data)); break; case AKSL_TREE_SEARCH_DFS_INORDER: if ( root->left != NULL ) { PASS(e, tree_dfs_walk(root->left, iter, searchmode, data, path, depth + 1)); } CATCH(e, iter(root, data)); if ( root->right != NULL ) { PASS(e, tree_dfs_walk(root->right, iter, searchmode, data, path, depth + 1)); } break; case AKSL_TREE_SEARCH_VISIT: /* Visit this node and stop; the children are deliberately not walked. */ CATCH(e, iter(root, data)); break; } } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } /* * Public entry point for every tree walk. The contract -- every status, every * argument -- is documented on the declaration in include/akstdlib.h; what is * here is why the code looks the way it does. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate( aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data) { TreeQueueEntry *head = NULL; TreeQueueEntry *tail = NULL; /* * The ancestor chain for the depth-first walk, held here rather than spread * across the recursion so that a walk costs one bounded allocation-free * buffer no matter how the tree is shaped. */ aksl_TreeNode *path[AKSL_TREE_MAX_DEPTH]; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root=%p, iter=%p", (void *)root, (void *)iter); FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "root=%p, iter=%p", (void *)root, (void *)iter); if ( lalloc == NULL ) { lalloc = &aksl_malloc; } if ( lfree == NULL ) { lfree = &aksl_free; } /* * Validate the mode here, outside the ATTEMPT block, so that the walkers can * assume it. The switch used to have no default at all, so searchmode 99 -- * and AKSL_TREE_SEARCH_VISIT, which the header documented but nothing * implemented -- fell straight through to success having visited nothing * (TODO.md 2.2.9). */ switch ( searchmode ) { case AKSL_TREE_SEARCH_DFS_PREORDER: case AKSL_TREE_SEARCH_DFS_INORDER: case AKSL_TREE_SEARCH_DFS_POSTORDER: case AKSL_TREE_SEARCH_VISIT: case AKSL_TREE_SEARCH_BFS: case AKSL_TREE_SEARCH_BFS_RIGHT: break; default: FAIL_RETURN(e, AKERR_VALUE, "unknown searchmode %d", searchmode); } ATTEMPT { if ( searchmode == AKSL_TREE_SEARCH_BFS || searchmode == AKSL_TREE_SEARCH_BFS_RIGHT ) { CATCH(e, tree_queue_push(&head, &tail, lalloc, root, 0)); CATCH(e, tree_bfs_walk(&head, &tail, iter, lalloc, lfree, searchmode, data)); } else { CATCH(e, tree_dfs_walk(root, iter, searchmode, data, path, 0)); } } CLEANUP { /* * Drains on every path, which is the whole reason the walk above leaves * the queue alone when it fails. A failure to release the queue must not * mask the error that got us here, so it is logged and dropped -- but * dropped by hand rather than with IGNORE(), which logs the context and * then never releases it back to the pool. */ akerr_ErrorContext *drained = tree_queue_drain(&head, &tail, lfree); if ( drained != NULL ) { LOG_ERROR_WITH_MESSAGE(drained, "** traversal queue drain failed **"); drained = akerr_release_error(drained); } } PROCESS(e) { } HANDLE(e, AKERR_ITERATOR_BREAK) { // This is not an error condition, it's just telling us to stop early SUCCEED_RETURN(e); } FINISH(e, true); SUCCEED_RETURN(e); } /* * Contract documented on the declaration in include/akstdlib.h. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list"); FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter"); aksl_ListNode *slow = list; aksl_ListNode *fast = list; aksl_ListNode *node = list; while ( fast != NULL && fast->next != NULL ) { slow = slow->next; fast = fast->next->next; if ( fast == slow) { FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", (void *)list); } } /* * From `list`, not from `slow`. The Floyd pass above leaves `slow` at the * midpoint, and starting the visit there skipped the whole first half of the * list including the head -- see TODO.md 2.1.2. */ while ( node != NULL ) { ATTEMPT { CATCH(e, iter(node, data)); node = node->next; } CLEANUP { } PROCESS(e) { } HANDLE(e, AKERR_ITERATOR_BREAK) { // This is not an error condition, it's just telling us to stop early SUCCEED_RETURN(e); } FINISH(e, true); } SUCCEED_RETURN(e); }