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>
1346 lines
51 KiB
C
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);
|
|
}
|