Files
libakstdlib/src/stdlib.c
Andrew Kesterson 3ad3994762 Test aksl_vscanf, and drop a check that could not fail
Coverage caught both. aksl_vscanf was declared, documented and never
called by anything, which is what 100% function coverage is for -- it is
the only metric here that notices a whole function nobody exercises.

aksl_vasprintf had a second vsnprintf whose return it compared against
the first, and a CLEANUP block to free the buffer when they disagreed.
They cannot disagree: the buffer was sized by vsnprintf from the same
format string and the same arguments a few lines earlier. So that was a
failure branch nothing can reach and a free() beneath it that nothing
can execute -- exactly the dead code TODO.md 2.2.11 recorded against the
old aksl_memset and aksl_memcpy, and removing those was the point. The
invariant is a comment now, where it can be read.

Back to 99.5% of lines (1708/1716) and 100% of functions (154/154), with
the eight uncovered lines the ones TODO.md already accounts for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:07:25 -04:00

1346 lines
51 KiB
C

#include <akstdlib.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}