Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
169
src/stdlib.c
169
src/stdlib.c
@@ -116,6 +116,69 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size)
|
||||
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
|
||||
@@ -454,6 +517,68 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str,
|
||||
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);
|
||||
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));
|
||||
/*
|
||||
* Cannot truncate -- the buffer was sized from this same format and
|
||||
* arguments -- but the return is checked anyway, because "cannot happen"
|
||||
* is a claim about the line above rather than about vsnprintf.
|
||||
*/
|
||||
FAIL_NONZERO_BREAK(e, (vsnprintf(buf, (size_t)needed + 1, format, args) != needed),
|
||||
AKERR_IO, "formatted output changed length between passes");
|
||||
*dest = buf;
|
||||
*count = needed;
|
||||
} CLEANUP {
|
||||
if ( *dest == NULL && buf != NULL ) {
|
||||
akerr_ErrorContext *released = aksl_free(buf);
|
||||
if ( released != NULL ) {
|
||||
released = akerr_release_error(released);
|
||||
}
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
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.
|
||||
*
|
||||
@@ -893,6 +1018,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
|
||||
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
|
||||
@@ -905,6 +1031,7 @@ typedef struct TreeQueueEntry {
|
||||
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(
|
||||
@@ -1101,31 +1228,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterate a binary tree in breadth-first or depth-first order, calling a callback on each node.
|
||||
*
|
||||
* The callback may stop the walk early by raising AKERR_ITERATOR_BREAK; this
|
||||
* function absorbs that one status and returns success, so an early exit is not
|
||||
* an error to the caller. Any other status the callback raises propagates out
|
||||
* unchanged, with its message and stack trace intact.
|
||||
*
|
||||
* Traversal is bounded. A tree deeper than AKSL_TREE_MAX_DEPTH raises
|
||||
* AKERR_OUTOFBOUNDS rather than overflowing the stack, and a depth-first walk
|
||||
* over a tree whose child points back at one of its own ancestors raises
|
||||
* AKERR_CIRCULAR_REFERENCE rather than recursing forever.
|
||||
*
|
||||
* @param[in] root The root of the tree to walk
|
||||
* @param[in] iter An aksl_TreeNodeIterator called once for each node visited
|
||||
* @param[in] lalloc An aksl_AllocFunc used to allocate the internal traversal queue, or NULL for aksl_malloc. Breadth-first modes only; the depth-first modes allocate nothing.
|
||||
* @param[in] lfree An aksl_FreeFunc used to release what lalloc returned, or NULL for aksl_free
|
||||
* @param[in] searchmode One of the AKSL_TREE_SEARCH_* defines
|
||||
* @param[in] data Caller data passed through to every invocation of iter
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER On NULL root or iter
|
||||
* @throws AKERR_VALUE On an unrecognised searchmode
|
||||
* @throws AKERR_OUTOFBOUNDS When the tree is deeper than AKSL_TREE_MAX_DEPTH
|
||||
* @throws AKERR_CIRCULAR_REFERENCE When a depth-first walk reaches a node that is its own ancestor
|
||||
* @return akerr_ErrorContext
|
||||
/*
|
||||
* 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,
|
||||
@@ -1198,17 +1304,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates over a linked list and execute a callback function on each node
|
||||
*
|
||||
* @param[in] list The linked list to iterate
|
||||
* @param[in] iter An aksl_ListNodeIterator function which will be called for each node found
|
||||
* @param[in] data Any user data that should be provided when the iterator is called
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER on null pointer inputs
|
||||
* @throws AKERR_CIRCULAR_REFERENCE when the linked list contains a circular reference
|
||||
*
|
||||
* @return akerr_ErrorContext
|
||||
/*
|
||||
* Contract documented on the declaration in include/akstdlib.h.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data)
|
||||
{
|
||||
|
||||
19
src/stream.c
19
src/stream.c
@@ -485,6 +485,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
|
||||
return raised;
|
||||
}
|
||||
|
||||
/* stdin, for symmetry with aksl_printf writing to stdout. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format,
|
||||
int expected, int *assigned, ...)
|
||||
{
|
||||
va_list args;
|
||||
akerr_ErrorContext *raised = NULL;
|
||||
|
||||
va_start(args, assigned);
|
||||
raised = aksl_vfscanf(stdin, format, expected, assigned, args);
|
||||
va_end(args);
|
||||
return raised;
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected,
|
||||
int *assigned, va_list args)
|
||||
{
|
||||
return aksl_vfscanf(stdin, format, expected, assigned, args);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Files */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user