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>
This commit is contained in:
2026-07-31 08:00:16 -04:00
parent 98a0a8562d
commit 125eeb2109
22 changed files with 4017 additions and 973 deletions

View File

@@ -17,20 +17,48 @@ jobs:
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc moreutils sudo apt-get install -y cmake gcc moreutils
# Depends on libakerror@main # This step used to clone and install libakerror@main as well. That was two
git clone https://source.starfort.tech/andrew/libakerror.git # different libakerrors depending on where you built: the install went to
cd libakerror # /usr/local, while the build below is top-level and so compiles
cmake -S . -B build # deps/libakerror at the pinned commit via add_subdirectory -- the
cmake --build build # installed one was never actually linked against. TODO.md 2.3.
cmake --install build #
# The submodule wins. It is the version this repository pins, tests and
# ships against, and a CI that tests a different one is testing something
# nobody runs. `cmake --install` below still installs both, so the
# find_package and pkg-config paths are exercised.
- name: build and test - name: build and test
run: | run: |
cmake -S . -B build # -DAKSL_WERROR=ON here and not locally: a warning that stops the build
# mid-thought teaches people to turn warnings off, but a warning that
# reaches main is one nobody will ever look at again.
cmake -S . -B build -DAKSL_WERROR=ON
cmake --build build cmake --build build
sudo cmake --install build sudo cmake --install build
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
# The sanitizer build is its own job rather than a step on the one above,
# because three of the defects fixed in 0.2.0 only ever misbehaved under
# instrumentation and the tests that pin them are worth failing on their own.
sanitizers:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc
- name: build and test under ASan + UBSan
run: |
cmake -S . -B build-asan -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
- run: echo "🍏 This job's status is ${{ job.status }}."
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -45,28 +73,27 @@ jobs:
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so # scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
# there is no lcov/gcovr to install here. # there is no lcov/gcovr to install here.
# #
# The gate is a ratchet, not a target: src/stdlib.c is at 99.1% of lines # The gate is a ratchet, not a target. Across all four sources the suite
# and 45.1% of branches, so 90/40 fails on a real regression (a test # covers 99.5% of lines (1643/1651), 46.0% of branches and 100% of
# deleted, or new untested code added) without tripping over rounding. # functions (147/147), so 90/40 fails on a real regression -- a test
# The report is printed either way -- the uncovered lines it lists are the # deleted, or new untested code added -- without tripping over rounding.
# The report is printed either way; the uncovered lines it lists are the
# missing tests. # missing tests.
# #
# Branch coverage is back over 45 (the aksl_version_* functions are fully # Eight lines are uncovered and every one is deliberate: four
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1 # `HANDLE(e, AKERR_ITERATOR_BREAK)` macro artifacts, the size_t overflow
# points of headroom is not a ratchet, it is a coin flip on the next # guard in strbuf_reserve (reaching it needs a buffer near SIZE_MAX), and
# rounding change. # the short transfer with neither feof nor ferror set, which the standard
# permits and Linux never produces. README.md has the detail.
# #
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump. # Branch coverage sits far below line coverage because most branches are
# Nothing about this library's tests changed: line coverage held at # inside libakerror's FAIL_*/ATTEMPT/FINISH expansions -- pool exhaustion,
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch # stack-trace limits, akerr_valid_error_address failures -- which this
# denominator went from 661 to 1087 because the 1.0.0 PREPARE_ERROR / # library has no way to reach. Chasing them here would be testing
# FAIL_* macros expand to more branches at every call site in # libakerror's macros, which is libakerror's mutation suite's job: macros
# src/stdlib.c, and most of the added branches are not reachable from the # expand at the call site, so coverage cannot see them properly from
# way this library calls them. 337/661 became 481/1087 -- 144 more # either side. The gate stays at 40 for that reason and not for want of
# branches covered, 426 more branches counted. Chasing them here would be # tests.
# testing libakerror's macros, which is libakerror's mutation suite's job
# (macros expand at the call site, so coverage cannot see them properly
# from either side). Re-ratcheted rather than papered over.
- name: coverage - name: coverage
run: | run: |
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \

View File

@@ -9,8 +9,9 @@
# #
# Runs by default, a few seconds all in: # Runs by default, a few seconds all in:
# #
# * default build + ctest # * default build + ctest (with -Werror, as CI has it)
# * ASan/UBSan build + ctest # * ASan/UBSan build + ctest
# * doxygen -- fails on an undocumented public function
# #
# Opt in to the slow harness (~25 minutes -- it rebuilds and re-runs the whole # Opt in to the slow harness (~25 minutes -- it rebuilds and re-runs the whole
# suite once per mutant): # suite once per mutant):
@@ -72,22 +73,40 @@ run() {
fi fi
} }
# -DAKSL_WERROR=ON here and not in a plain local build: a warning that stops you
# mid-thought teaches people to turn warnings off, but a warning that reaches
# main is one nobody will look at again. This is the last point where it is
# still cheap to fix.
echo "pre-push: default build + ctest" echo "pre-push: default build + ctest"
run cmake -S . -B "$builddir/default" run cmake -S . -B "$builddir/default" -DAKSL_WERROR=ON
run cmake --build "$builddir/default" run cmake --build "$builddir/default"
run ctest --test-dir "$builddir/default" --output-on-failure run ctest --test-dir "$builddir/default" --output-on-failure
echo "pre-push: sanitizer build + ctest" echo "pre-push: sanitizer build + ctest"
run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON
run cmake --build "$builddir/asan" run cmake --build "$builddir/asan"
run ctest --test-dir "$builddir/asan" --output-on-failure run ctest --test-dir "$builddir/asan" --output-on-failure
# Doxygen runs with WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC on and EXTRACT_ALL
# off, so the target fails when a public function, parameter or return value has
# nobody describing it. Skipped where doxygen is not installed rather than
# failing: it is a documentation check, not a build dependency.
if command -v doxygen > /dev/null 2>&1; then
echo "pre-push: doxygen"
run cmake --build "$builddir/default" --target docs
else
echo "pre-push: doxygen not installed, skipping the documentation check"
fi
if [ "${AKSL_HOOK_MUTATION:-0}" = "1" ]; then if [ "${AKSL_HOOK_MUTATION:-0}" = "1" ]; then
echo "pre-push: mutation testing, threshold ${MUTATION_THRESHOLD}% (this takes a while)" echo "pre-push: mutation testing, threshold ${MUTATION_THRESHOLD}% (this takes a while)"
# Deliberately not wrapped in run(): this one is slow enough that you want # Deliberately not wrapped in run(): this one is slow enough that you want
# to watch it make progress. # to watch it make progress.
if ! python3 scripts/mutation_test.py \ if ! python3 scripts/mutation_test.py \
--target src/stdlib.c \ --target src/stdlib.c \
--target src/string.c \
--target src/stream.c \
--target src/collections.c \
--threshold "$MUTATION_THRESHOLD"; then --threshold "$MUTATION_THRESHOLD"; then
echo >&2 echo >&2
echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2 echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2

6
.gitignore vendored
View File

@@ -25,3 +25,9 @@ coverage.xml
*.swp *.swp
.DS_Store .DS_Store
compile_commands.json compile_commands.json
# Generated API documentation and its warning log. `cmake --build build --target
# docs` writes both; the log is the actionable part and is empty when clean.
doxygen/
doxygen-warnings.log
Doxyfile.generated

View File

@@ -2,14 +2,27 @@
## Project Structure & Module Organization ## Project Structure & Module Organization
`libakstdlib` is a C shared library that wraps libc calls and small data `libakstdlib` is a C shared library that wraps libc calls and data structures in
structures in `libakerror` error contexts. Public API declarations live in `libakerror` error contexts.
`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are
one-file CTest executables under `tests/test_<name>.c`, with shared test helpers Public API declarations live in `include/akstdlib.h`, which is also where the
in `tests/aksl_capture.h`. CMake package templates are in `cmake/` and Doxygen documentation lives -- the header is the contract, the sources carry the
`akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a reasoning. The implementation is split by domain:
submodule rather than editing generated files under `build/`, `build-asan/` or
`build-coverage/`. | File | Covers |
|---|---|
| `src/stdlib.c` | memory, formatted output, string-to-number, realpath, djb2, and the list/tree traversal entry points |
| `src/string.c` | the `string.h` surface |
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
| `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
| `src/aksl_internal.h` | shared internals; not installed, not public |
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must
*fail* to compile -- see the testing section below. CMake package templates are
in `cmake/` and `akstdlib.pc.in`. The vendored dependency is `deps/libakerror`;
update it as a submodule rather than editing generated files under `build/`,
`build-asan/` or `build-coverage/`.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
@@ -40,8 +53,15 @@ cmake --build build-coverage --target coverage
``` ```
Run `cmake --build build --target mutation` only when you need the slower Run `cmake --build build --target mutation` only when you need the slower
mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the mutation harness, and `cmake --build build --target docs` to regenerate the API
local build directory, so treat it as a local convenience script. documentation -- that one fails if any public function, parameter or return value
is undocumented, so it is a check as well as a generator.
`.githooks/pre-push` runs the default build, the sanitizer build and the
documentation check before a push; enable it with
`git config core.hooksPath .githooks`. `rebuild.sh` installs to
`/home/andrew/local` and removes the local build directory, so treat it as a
local convenience script.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
@@ -51,16 +71,45 @@ prefix, structs use `aksl_<Name>`, and tests use `test_<feature>.c` plus static
`test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *` `test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *`
return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern. return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
Four conventions hold across the whole library, and a new wrapper that breaks one
of them is wrong even if it compiles and passes:
- **A NULL out-param is a caller error**, not "don't care".
- **Finding nothing is success** -- searching functions write NULL or zero and
return NULL.
- **Truncation is a failure.** Take the destination's size, raise
AKERR_OUTOFBOUNDS, and write nothing rather than a prefix.
- **Clear `errno` before the wrapped call** and read it back through
`AKSL_ERRNO_OR`, so no error can carry status 0 -- which every downstream
`DETECT` reads as success.
Every new public function needs a Doxygen block on its **declaration** with
`@brief`, a `@param` per parameter, a `@throws` per status it can raise, and
`@return`. The `docs` target fails otherwise.
The build is `-Wall -Wextra` and CI adds `-Werror`. `-Wpedantic` is deliberately
off: libakerror's `FAIL_*` macros trip "ISO C99 requires at least one argument
for the ..." on their own expansion, not on anything at the call site.
## Testing Guidelines ## Testing Guidelines
Add a new test by creating `tests/test_mything.c` and adding `mything` to the Add a new test by creating `tests/test_mything.c` and adding `mything` to the
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero. right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests. `AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one `AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix. starts unexpectedly passing, move it into `AKSL_TESTS` with the fix. Both of the
latter are currently empty -- all six confirmed defects are fixed -- but the
mechanism stays for the next one.
`src/stdlib.c` is at 99.1% line coverage and CI gates it at 90 (line) / 40 `tests/negative/` holds sources that must **fail** to compile. Each is an
(branch), so new code needs tests in the same commit. Run `EXCLUDE_FROM_ALL` target built with `-Werror` and registered as a `WILL_FAIL`
CTest entry, so the test passes only when the compile fails. They cover the two
guarantees the compiler enforces and nothing else does: `AKERR_NOIGNORE` and the
format attributes. Drop either in a refactor and every ordinary test still
passes.
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
gates at 90 (line) / 40 (branch), so new code needs tests in the same commit. Run
`cmake --build build-coverage --target coverage` and check the uncovered-line `cmake --build build-coverage --target coverage` and check the uncovered-line
listing before proposing a change. Tests for behaviour that `TODO.md` records as listing before proposing a change. Tests for behaviour that `TODO.md` records as
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract — defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —

View File

@@ -4,10 +4,20 @@ cmake_minimum_required(VERSION 3.10)
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and # shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
# akstdlibConfigVersion.cmake. Nothing else should spell a version number. # akstdlibConfigVersion.cmake. Nothing else should spell a version number.
# #
# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose # 0.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed
# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's # defects in TODO.md 2.1 changed documented behaviour and, in five places,
# signature), so this library is not promising a stable API yet. # signatures:
project(akstdlib VERSION 0.1.0 LANGUAGES C) #
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new
# aksl_list_pop takes the head by reference
# aksl_tree_iterate lost its `queue` parameter
# aksl_fread/fwrite take a required transferred-count out-param
# aksl_sprintf is gone; aksl_snprintf replaces it
#
# plus the ato* family, which now reports a bad conversion rather than returning
# 0. Pre-1.0 the soname is MAJOR.MINOR, so 0.1 and 0.2 are different ABIs and a
# consumer built against 0.1 will not silently load this. See UPGRADING.md.
project(akstdlib VERSION 0.2.0 LANGUAGES C)
# The granularity at which the ABI is allowed to break, and therefore the # The granularity at which the ABI is allowed to break, and therefore the
# soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library # soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library
@@ -302,6 +312,7 @@ set(AKSL_TESTS
linkedlist linkedlist
memory memory
path path
pool
status_registry status_registry
strbuf strbuf
stream stream
@@ -339,6 +350,39 @@ foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
list(APPEND AKSL_TEST_TARGETS test_${_test}) list(APPEND AKSL_TEST_TARGETS test_${_test})
endforeach() endforeach()
# Negative compile tests -- TODO.md 1.3 and 1.9.
#
# Two properties of this library are enforced by the compiler and by nothing
# else: AKERR_NOIGNORE makes discarding a returned error context an error, and
# AKSL_PRINTF_FORMAT restores the format/argument checking a caller loses by
# going through a variadic wrapper. Both are attributes on declarations in
# include/akstdlib.h. If either is dropped in a refactor, every test still
# passes, the library still builds, and nothing looks wrong -- the guarantee just
# quietly stops existing.
#
# So each is asserted by a source file that must *fail* to compile. The target
# is EXCLUDE_FROM_ALL and built with -Werror; the CTest entry runs that build and
# is marked WILL_FAIL, so a successful compile fails the suite.
#
# Only where the compiler supports the attributes at all -- elsewhere
# AKSL_PRINTF_FORMAT expands to nothing and the file would compile correctly.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
foreach(_neg noignore format_mismatch)
add_executable(negative_${_neg} EXCLUDE_FROM_ALL tests/negative/${_neg}.c)
target_link_libraries(negative_${_neg} PRIVATE akstdlib)
target_compile_options(negative_${_neg} PRIVATE -Werror)
add_test(NAME negative_${_neg}
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}
--target negative_${_neg})
set_tests_properties(negative_${_neg} PROPERTIES
WILL_FAIL TRUE
TIMEOUT 120
# These invoke the build system, so they must not run while another build
# of the same tree is in flight.
RUN_SERIAL TRUE)
endforeach()
endif()
# tests/test_memory.c asks for SIZE_MAX/2 bytes to check that a refused # tests/test_memory.c asks for SIZE_MAX/2 bytes to check that a refused
# allocation reports ENOMEM and leaves *dst NULL. Plain malloc returns NULL and # allocation reports ENOMEM and leaves *dst NULL. Plain malloc returns NULL and
# sets ENOMEM, which is the case under test; ASan's allocator instead treats a # sets ENOMEM, which is the case under test; ASan's allocator instead treats a
@@ -445,6 +489,40 @@ elseif(AKSL_COVERAGE)
"(scripts/coverage.py) will not be wired into CTest") "(scripts/coverage.py) will not be wired into CTest")
endif() endif()
# API documentation:
# cmake --build build --target docs
#
# The Doxyfile carries no PROJECT_NUMBER. The version lives in exactly one place
# -- the project() call at the top of this file -- and a version written into the
# Doxyfile as well would be a second place to forget. Doxygen reads its
# configuration from stdin when given "-", so the number is appended on the way
# past and the checked-in file stays version-free.
#
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are both on, and EXTRACT_ALL is off:
# an undocumented function is a warning rather than a silently empty page. The
# target fails if the log is non-empty, which is what makes "documented" a
# property the build checks rather than an impression.
find_program(AKSL_DOXYGEN doxygen)
if(AKSL_DOXYGEN)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKSL_DOCS_TARGET docs)
else()
set(AKSL_DOCS_TARGET akstdlib_docs)
endif()
add_custom_target(${AKSL_DOCS_TARGET}
COMMAND ${CMAKE_COMMAND} -E echo
"Generating API documentation for akstdlib ${PROJECT_VERSION}"
COMMAND ${CMAKE_COMMAND}
-DAKSL_DOXYGEN=${AKSL_DOXYGEN}
-DAKSL_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}
-DAKSL_VERSION=${PROJECT_VERSION}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunDoxygen.cmake
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running doxygen"
)
endif()
# Mutation testing: break the library in small ways and confirm the test suite # Mutation testing: break the library in small ways and confirm the test suite
# notices. This is a meta-check on the tests themselves, and it rebuilds and # notices. This is a meta-check on the tests themselves, and it rebuilds and
# re-runs the whole suite once per mutant, so it is a manual target rather than # re-runs the whole suite once per mutant, so it is a manual target rather than

View File

@@ -42,9 +42,7 @@ DOXYFILE_ENCODING = UTF-8
# title of most generated pages and in a few other places. # title of most generated pages and in a few other places.
# The default value is: My Project. # The default value is: My Project.
PROJECT_NAME = "My Project" PROJECT_NAME = "libakstdlib"
# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
@@ -54,9 +52,7 @@ PROJECT_NUMBER =
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
# quick idea about the purpose of the project. Keep the description short. # quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF = PROJECT_BRIEF = "libc wrappers that report failures through libakerror error contexts"
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
# in the documentation. The maximum height of the logo should not exceed 55 # in the documentation. The maximum height of the logo should not exceed 55
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy # pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory. # the logo to the output directory.
@@ -68,9 +64,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If # entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used. # left blank the current directory will be used.
OUTPUT_DIRECTORY = OUTPUT_DIRECTORY = doxygen
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format # sub-directories (in 2 levels) under the output directory of each output format
# and will distribute the generated files over these directories. Enabling this # and will distribute the generated files over these directories. Enabling this
# option can be useful when feeding doxygen a huge amount of source files, where # option can be useful when feeding doxygen a huge amount of source files, where
@@ -157,9 +151,7 @@ ABBREVIATE_BRIEF = "The $name class" \
# description. # description.
# The default value is: NO. # The default value is: NO.
ALWAYS_DETAILED_SEC = NO ALWAYS_DETAILED_SEC = YES
# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
# inherited members of a class in the documentation of that class as if those # inherited members of a class in the documentation of that class as if those
# members were ordinary class members. Constructors, destructors and assignment # members were ordinary class members. Constructors, destructors and assignment
# operators of the base classes will not be shown. # operators of the base classes will not be shown.
@@ -209,9 +201,7 @@ SHORT_NAMES = NO
# description.) # description.)
# The default value is: NO. # The default value is: NO.
JAVADOC_AUTOBRIEF = NO JAVADOC_AUTOBRIEF = YES
# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
# such as # such as
# /*************** # /***************
# as being the beginning of a Javadoc-style comment "banner". If set to NO, the # as being the beginning of a Javadoc-style comment "banner". If set to NO, the
@@ -291,9 +281,7 @@ ALIASES =
# members will be omitted, etc. # members will be omitted, etc.
# The default value is: NO. # The default value is: NO.
OPTIMIZE_OUTPUT_FOR_C = NO OPTIMIZE_OUTPUT_FOR_C = YES
# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
# Python sources only. Doxygen will then generate output that is more tailored # Python sources only. Doxygen will then generate output that is more tailored
# for that language. For instance, namespaces will be presented as packages, # for that language. For instance, namespaces will be presented as packages,
# qualified scopes will look different, etc. # qualified scopes will look different, etc.
@@ -519,8 +507,6 @@ TIMESTAMP = NO
# The default value is: NO. # The default value is: NO.
EXTRACT_ALL = NO EXTRACT_ALL = NO
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation. # be included in the documentation.
# The default value is: NO. # The default value is: NO.
@@ -543,8 +529,6 @@ EXTRACT_PACKAGE = NO
# The default value is: NO. # The default value is: NO.
EXTRACT_STATIC = NO EXTRACT_STATIC = NO
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO, # locally in source files will be included in the documentation. If set to NO,
# only classes defined in header files are included. Does not have any effect # only classes defined in header files are included. Does not have any effect
# for Java sources. # for Java sources.
@@ -681,9 +665,7 @@ INLINE_INFO = YES
# name. If set to NO, the members will appear in declaration order. # name. If set to NO, the members will appear in declaration order.
# The default value is: YES. # The default value is: YES.
SORT_MEMBER_DOCS = YES SORT_MEMBER_DOCS = NO
# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
# descriptions of file, namespace and class members alphabetically by member # descriptions of file, namespace and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order. Note that # name. If set to NO, the members will appear in declaration order. Note that
# this will also influence the order of the classes in the class list. # this will also influence the order of the classes in the class list.
@@ -853,8 +835,6 @@ WARNINGS = YES
# The default value is: YES. # The default value is: YES.
WARN_IF_UNDOCUMENTED = YES WARN_IF_UNDOCUMENTED = YES
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as documenting some parameters in # potential errors in the documentation, such as documenting some parameters in
# a documented function twice, or documenting parameters that don't exist or # a documented function twice, or documenting parameters that don't exist or
# using markup commands wrongly. # using markup commands wrongly.
@@ -877,9 +857,7 @@ WARN_IF_INCOMPLETE_DOC = YES
# WARN_IF_INCOMPLETE_DOC # WARN_IF_INCOMPLETE_DOC
# The default value is: NO. # The default value is: NO.
WARN_NO_PARAMDOC = NO WARN_NO_PARAMDOC = YES
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
# undocumented enumeration values. If set to NO, doxygen will accept # undocumented enumeration values. If set to NO, doxygen will accept
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag # undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled. # will automatically be disabled.
@@ -931,9 +909,7 @@ WARN_LINE_FORMAT = "at line $line of file $file"
# specified the warning and error messages are written to standard output # specified the warning and error messages are written to standard output
# (stdout). # (stdout).
WARN_LOGFILE = WARN_LOGFILE = doxygen-warnings.log
#---------------------------------------------------------------------------
# Configuration options related to the input files # Configuration options related to the input files
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -943,9 +919,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched. # Note: If this tag is empty the current directory is searched.
INPUT = INPUT = include src
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv # libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see: # documentation (see:
@@ -1038,18 +1012,14 @@ FILE_PATTERNS = *.c \
# be searched for input files as well. # be searched for input files as well.
# The default value is: NO. # The default value is: NO.
RECURSIVE = YES RECURSIVE = NO
# The EXCLUDE tag can be used to specify files and/or directories that should be
# excluded from the INPUT source files. This way you can easily exclude a # excluded from the INPUT source files. This way you can easily exclude a
# subdirectory from a directory tree whose root is specified with the INPUT tag. # subdirectory from a directory tree whose root is specified with the INPUT tag.
# #
# Note that relative paths are relative to the directory from which doxygen is # Note that relative paths are relative to the directory from which doxygen is
# run. # run.
EXCLUDE = EXCLUDE = src/aksl_internal.h
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded # directories that are symbolic links (a Unix file system feature) are excluded
# from the input. # from the input.
# The default value is: NO. # The default value is: NO.
@@ -1968,9 +1938,7 @@ EXTRA_SEARCH_MAPPINGS =
# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. # If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
# The default value is: YES. # The default value is: YES.
GENERATE_LATEX = YES GENERATE_LATEX = NO
# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it. # it.
# The default directory is: latex. # The default directory is: latex.

205
README.md
View File

@@ -5,12 +5,64 @@
`libakstdlib` wraps C standard library functions so that they report failures `libakstdlib` wraps C standard library functions so that they report failures
through [libakerror](https://source.starfort.tech/andrew/libakerror)'s through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
`ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes `ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes
and `errno`. It also provides a few data structures built on the same and `errno`. It also provides data structures built on the same convention.
convention (a doubly-linked list and a binary tree).
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`. Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
See `TODO.md` for the current state of the library: what is covered by tests, See `TODO.md` for the current state of the library and `UPGRADING.md` if you are
which corner cases are still open, and which libc functions are not yet wrapped. coming from 0.1.0, which this release breaks.
## What it wraps
| Area | Source | Functions |
|---|---|---|
| Memory | `src/stdlib.c` | `malloc` `calloc` `realloc` `free` `freep` `memset` `memcpy` `memmove` `memcmp` `memchr` |
| Formatted output | `src/stdlib.c` | `printf` `fprintf` `snprintf` and their `v*` forms |
| String → number | `src/stdlib.c` | `strtol` `strtoll` `strtoul` `strtoull` `strtod` `strtof` `strtold`, and `atoi` `atol` `atoll` `atof` on top of them |
| Paths and hashing | `src/stdlib.c` | `realpath` `realpath_alloc` `strhash_djb2` `strhash_djb2_str` |
| Strings | `src/string.c` | `strlen` `strnlen` `strcpy` `strncpy` `strcat` `strncat` `strdup` `strndup` `strcmp` `strncmp` `strcasecmp` `strncasecmp` `strcoll` `strchr` `strrchr` `strstr` `strcasestr` `strpbrk` `strspn` `strcspn` `strtok_r` `strsep` `strerror` |
| Streams | `src/stream.c` | `fopen` `fread` `fwrite` `fclose` `fseek` `ftell` `rewind` `fseeko` `ftello` `fgetpos` `fsetpos` `fflush` `setvbuf` `fgetc` `fputc` `ungetc` `fgets` `fputs` `getline` `getdelim` `feof` `ferror` `clearerr` `fileno` `freopen` `fdopen` `tmpfile` `sscanf` `fscanf` `remove` `rename` `mkstemp` `mkdtemp` |
| Collections | `src/collections.c` | doubly-linked list (bare-node and tracked-container forms), binary search tree, breadth- and depth-first traversal, fixed-capacity hash map, growable string buffer, FNV-1a |
### Where it deviates from libc, and why
The whole point is to make silent failures loud, so several wrappers are
deliberately stricter than the function they are named for. These are the ones
that will surprise you:
| Wrapper | Deviation |
|---|---|
| `aksl_free(NULL)` | `AKERR_NULLPOINTER`. `free(NULL)` is legal and does nothing; in a codebase that routes every allocation through `aksl_malloc`, a pointer you believed was live turning out NULL means something upstream did not happen. |
| `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. |
| `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. |
| `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. |
| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. |
| `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. |
| `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. |
| `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. |
| `aksl_realpath` | Takes the destination's length and refuses anything below `PATH_MAX`, because `realpath(3)` cannot be bounded. |
| `aksl_list_pop` | Takes the head by reference, because popping the head has to move it. |
| Searching (`strchr`, `strstr`, `memchr`, `aksl_list_find`, `aksl_hashmap_get`, …) | Finding nothing is **success** with a NULL or zero result, not an error. Absent is an ordinary answer. |
| `aksl_strtok` | Does not exist. `strtok(3)` keeps its state in a hidden static; use `aksl_strtok_r` or `aksl_strsep`. |
### Thread safety
**This library is not thread-safe, and cannot be made so from here.**
libakerror hands out error contexts from `AKERR_ARRAY_ERROR`, a process-global
array with no locking, and every entry point in this library takes a slot from it
on any failure path. Two threads raising errors concurrently can be handed the
same slot. `errno` is thread-local so the wrapped calls themselves are fine; the
error reporting is not.
There is no TSan test here because there is nothing to verify — the answer is
known and it is "no". Fixing it means locking or thread-local storage in
libakerror's pool, which is that library's decision to make; `TODO.md` §1.9
records it. Until then: confine libakstdlib calls to one thread, or serialise
them yourself.
The wrappers add no state of their own beyond that. `aksl_strtok_r` and
`aksl_strsep` keep their state in the caller's `saveptr`, and no function here
uses a static buffer.
## Building ## Building
@@ -27,27 +79,29 @@ package the parent provides instead.
### This library's own version ### This library's own version
`libakstdlib` is at **0.1.0**. The version lives in exactly one place — the `libakstdlib` is at **0.2.0**. The version lives in exactly one place — the
`project()` call in `CMakeLists.txt` — and flows from there into everything `project()` call in `CMakeLists.txt` — and flows from there into everything
else, so a bump is a one-line edit: else, so a bump is a one-line edit:
| Artifact | Value at 0.1.0 | From | | Artifact | Value at 0.2.0 | From |
| --- | --- | --- | | --- | --- | --- |
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `1` / `0` | `include/akstdlib_version.h.in` | | `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `2` / `0` | `include/akstdlib_version.h.in` |
| `AKSL_VERSION_STRING` | `"0.1.0"` | same | | `AKSL_VERSION_STRING` | `"0.2.0"` | same |
| `AKSL_VERSION_NUMBER` | `100` | same | | `AKSL_VERSION_NUMBER` | `200` | same |
| `AKSL_VERSION_SONAME` | `"0.1"` | same | | `AKSL_VERSION_SONAME` | `"0.2"` | same |
| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` | | shared library | `libakstdlib.so.0.2.0`, soname `libakstdlib.so.0.2` | `VERSION` / `SOVERSION` |
| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` | | `pkg-config --modversion akstdlib` | `0.2.0` | `akstdlib.pc` |
| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` | | `find_package(akstdlib 0.2)` | accepted; `0.1` and `1.0` refused | `akstdlibConfigVersion.cmake` |
`akstdlib_version.h` is **generated** — that is why there is no such file in the `akstdlib_version.h` is **generated** — that is why there is no such file in the
source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the
copy in your build directory; change the template or `project()`. copy in your build directory; change the template or `project()`.
It is `0.x` deliberately. `TODO.md` §2.1 still records four confirmed defects It is `0.x` deliberately. The 0.1 → 0.2 bump was itself an ABI break — fixing the
whose fixes change documented behaviour, so the API is not being promised yet. confirmed defects changed five signatures and the `ato*` contract, all of it
While the major version is `0`, **the soname carries `MAJOR.MINOR`**: 0.1 and 0.2 listed in `UPGRADING.md` — and the API is not being promised until the wishlist
in `TODO.md` §3 has settled. While the major version is `0`, **the soname carries
`MAJOR.MINOR`**: 0.1 and 0.2
are different ABIs and the loader will not substitute one for the other. At 1.0 are different ABIs and the loader will not substitute one for the other. At 1.0
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
`CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two `CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two
@@ -75,18 +129,18 @@ function that compares against the values baked into the library. Calling
`aksl_version_check()` with hand-written numbers defeats the whole mechanism. `aksl_version_check()` with hand-written numbers defeats the whole mechanism.
Compatibility is defined as "same soname", so pre-1.0 both major and minor must Compatibility is defined as "same soname", so pre-1.0 both major and minor must
match and the patch level is ignored — a caller built against 0.1.0 keeps working match and the patch level is ignored — a caller built against 0.2.0 keeps working
against 0.1.7, which is exactly the promise the shared soname makes. against 0.2.7, which is exactly the promise the shared soname makes.
In normal use the soname catches the mismatch first, at load time, and the check In normal use the soname catches the mismatch first, at load time, and the check
never fires. It earns its keep when the soname is bypassed: a hand-install that never fires. It earns its keep when the soname is bypassed: a hand-install that
drops a 0.2.0 build in under the 0.1 filename, or a package that strips drops a 0.3.0 build in under the 0.2 filename, or a package that strips
versioning. Then the loader is happy and only the check notices: versioning. Then the loader is happy and only the check notices:
``` ```
compiled against : 0.1.0 (soname 0.1) compiled against : 0.2.0 (soname 0.2)
loaded : 0.2.0 (0.2.0) loaded : 0.3.0 (0.3.0)
MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2) MISMATCH DETECTED: compiled against libakstdlib 0.2.0, loaded 0.3.0 (soname 0.3)
``` ```
### The libakerror version floor ### The libakerror version floor
@@ -144,11 +198,22 @@ assert on the status it returns, `aksl_temp_file()` for tests that need a real
file to work on, and an `AKSL_RUN()` driver that additionally fails any test which file to work on, and an `AKSL_RUN()` driver that additionally fails any test which
leaks a slot from libakerror's error pool. leaks a slot from libakerror's error pool.
One file per area of the API: `convert` (the `ato*` family), `format` (the One file per area of the API: `memory`, `format` (the `printf` family), `convert`
`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path` (the `ato*` family) and `strto` (the family underneath it), `stream`
(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and (`fopen`/`fread`/`fwrite`/`fclose`) and `streamio` (everything else in
`status_registry` (this library's side of the libakerror status-registry `src/stream.c`), `string`, `path` (`aksl_realpath`), `strhash`, `linkedlist`,
contract — see "The libakerror version floor" above). `tree`, `collections` (the list and tree additions), `hashmap`, `strbuf`,
`version`, `status_registry` (this library's side of the libakerror
status-registry contract — see "The libakerror version floor" above), and `pool`.
`pool` is the odd one out: it asserts two cross-cutting properties rather than
any function's behaviour. Every failure path is driven `AKERR_MAX_ARRAY_ERROR + 10`
times with the pool checked after each round, because a wrapper that raises an
error and forgets to release it does not fail visibly — it fails a hundred-odd
calls later in whatever unrelated code asks for a slot next. And every error is
checked to name the function and file it was actually raised from, which is what
catches a `FAIL` that migrates into a shared helper during a refactor: the status
stays right, the message stays right, and the origin quietly starts lying.
To add a test, drop `tests/test_mything.c` in place and add `mything` to To add a test, drop `tests/test_mything.c` in place and add `mything` to
`AKSL_TESTS` in `CMakeLists.txt`. `AKSL_TESTS` in `CMakeLists.txt`.
@@ -162,11 +227,21 @@ of them invert the meaning of "Passed":
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. | | `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. | | `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
So `ctest` reporting all green does **not** mean the library is defect-free — it **Both of those lists are currently empty**, which is the news: all six confirmed
means the known-good tests passed and the known-bad ones are still failing in the defects in `TODO.md` §2.1 are fixed, and the four tests that used to sit in
documented way. When a defect is fixed, its test starts passing, CTest reports it `AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they
as failed with *unexpectedly passed*, and that is the cue to move it from test, where they now have to keep passing rather than keep failing visibly. The
`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS`. mechanism stays for the next one. When a defect is fixed its known-failing test
starts passing, CTest reports it as failed with *unexpectedly passed*, and that
is the cue to move it into `AKSL_TESTS`.
Two more entries, `negative_noignore` and `negative_format_mismatch`, are
compile-time assertions rather than programs. Each builds a source file under
`tests/negative/` with `-Werror` and is marked `WILL_FAIL`, so the test passes
only when the compile *fails*. They exist because `AKERR_NOIGNORE` and
`AKSL_PRINTF_FORMAT` are enforced by the compiler and by nothing else: drop
either attribute in a refactor and every test still passes, the library still
builds, and the guarantee just quietly stops existing.
Every test is capped with a 30-second CTest `TIMEOUT`. The list and tree code is Every test is capped with a 30-second CTest `TIMEOUT`. The list and tree code is
full of loops whose termination hangs on a single condition, so a bug of that full of loops whose termination hangs on a single condition, so a bug of that
@@ -181,10 +256,19 @@ ctest --test-dir build-asan --output-on-failure
``` ```
Builds the library, the tests and the vendored libakerror with ASan + UBSan and Builds the library, the tests and the vendored libakerror with ASan + UBSan and
`-fno-sanitize-recover=all`. Several of the open items in `TODO.md` §2 only `-fno-sanitize-recover=all`. Three of the defects fixed in 0.2.0 only misbehaved
misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath`, the under instrumentation — the uninitialised `%s` in `aksl_realpath`'s error path,
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf` the unbounded `vsprintf` behind the old `aksl_sprintf`, and the missing `va_end`
family — so new tests for those should be run this way. in the `printf` family — and the tests that pin them are written to be run this
way. `tests/test_path.c` deliberately passes an *uninitialised* buffer on every
failure path for exactly that reason.
One test needs help from the sanitizer to test the same thing the normal build
does: `tests/test_memory.c` asks for `SIZE_MAX / 2` bytes to check that a refused
allocation reports `ENOMEM` and leaves `*dst` NULL. Plain `malloc` returns NULL;
ASan treats a request that large as a bug in the caller and aborts before `malloc`
returns at all. `CMakeLists.txt` sets `ASAN_OPTIONS=allocator_may_return_null=1`
for that one binary so the contract under test stays the same in both builds.
### 3. Code coverage ### 3. Code coverage
@@ -246,25 +330,42 @@ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40 -DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
``` ```
**Where it stands.** `src/stdlib.c` is at **99.1% of lines (217/219)**, **45.1% of **Where it stands.** All four sources, with the whole suite:
branches (519/1151)** and **25/25 functions**, so 90/40 above is a ratchet with
headroom rather than a target. The two uncovered lines are both
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the
`break;` belonging to `PROCESS`'s `case 0:` arm, which is only reachable when a
callback returns a non-NULL error context whose status is *zero*. That is the
pathological case §2.2.1 of `TODO.md` exists to remove, so it is left uncovered
deliberately rather than pinned by a test.
Branch coverage sits far below line coverage because most branches in this file | file | lines | branches | functions |
|---|---|---|---|
| `src/collections.c` | 99.3% (601/605) | 43.5% | 100% (43/43) |
| `src/stdlib.c` | 99.3% (557/561) | 46.5% | 100% (50/50) |
| `src/stream.c` | 100% (274/274) | 43.4% | 100% (31/31) |
| `src/string.c` | 100% (211/211) | 53.8% | 100% (23/23) |
| **total** | **99.5% (1643/1651)** | **46.0%** | **100% (147/147)** |
so the 90/40 gate above is a ratchet with headroom rather than a target. Eight
lines are uncovered and each is uncovered on purpose:
- **Four `} HANDLE(e, AKERR_ITERATOR_BREAK) {`** lines. In libakerror that macro
begins with the `break;` belonging to `PROCESS`'s `case 0:` arm, which is only
reachable when a callback returns a non-NULL context whose status is *zero*
the pathological case §2.2.1 exists to remove. Left uncovered rather than
pinned by a test that would have to manufacture it.
- **Two in `strbuf_reserve`**, the `size_t` overflow guard on a doubling that
would wrap. Reaching it needs a buffer within a factor of two of `SIZE_MAX`,
which is not a test, it is a hang.
- **Two in `aksl_fread`/`aksl_fwrite`**, the short transfer with *neither* `feof`
nor `ferror` set. Every way of producing a short transfer on Linux sets one or
the other; the branch is there because the standard permits neither, not
because anything reaches it. `TODO.md` §1.2 records it as still open.
Branch coverage sits far below line coverage because most branches in these files
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion, are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
libakerror's own suite rather than to this one. The libakerror 1.0.0 bump made libakerror's own suite rather than to this one. Every `FAIL_ZERO_RETURN` in the
that gap wider without changing a line of this library: the branch denominator tree contributes several branches that this library has no way to reach. The
went from 661 to 1087 as those macros grew, so the same tests that scored 51.0% libakerror 1.0.0 bump made that gap wider without changing a line here: the
(337/661) dropped to 44.3% (481/1087). The gate moved 45 → 40 to match; line and branch denominator per call site grew, so identical tests scored lower. Chasing
function coverage did not move at all. Adding the `aksl_version_*` family and its the number would mean testing libakerror's macros, which is what libakerror's
tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 — mutation suite is for — macros expand at the call site, so coverage cannot see
0.1 points of headroom is not a ratchet. them properly from either side.
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines
until per-line counts stop matching the source — so a coverage build is not the until per-line counts stop matching the source — so a coverage build is not the

838
TODO.md
View File

@@ -1,555 +1,178 @@
# TODO # TODO
Working notes for `libakstdlib` — the akerror-wrapped libc surface. Working notes for `libakstdlib`. **Outstanding items only** — anything fixed comes
out of this file and goes into `UPGRADING.md`, the tests, or a comment beside the
code, whichever is the right place to be reminded of it.
Scope of the current library (`include/akstdlib.h`, `src/stdlib.c`): 16 libc wrappers Ordered by blast radius: what blocks other work first, what is merely wrong
(`fopen`, `fread`, `fwrite`, `fclose`, `malloc`, `free`, `memset`, `memcpy`, `printf`, second, what is missing last.
`fprintf`, `sprintf`, `atoi`, `atol`, `atoll`, `atof`, `realpath`), one hash helper
(`aksl_strhash_djb2`), a doubly-linked list (`append`/`pop`/`iterate`) and a binary tree
iterator (`aksl_tree_iterate`).
Items marked **[CONFIRMED]** were reproduced by building the library and running a probe ## Where the library stands
program against it, not just read off the source.
| | |
|---|---|
| Wrapped | 147 public functions across `src/stdlib.c`, `src/string.c`, `src/stream.c`, `src/collections.c` |
| Tests | 17 CTest binaries plus 2 negative-compile entries, green under the default and sanitizer builds |
| Line coverage | 99.5% (1643/1651) |
| Function coverage | 100% (147/147) |
| Doxygen | 100%, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
The six confirmed defects that used to head this file are fixed and
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a
result, is in `UPGRADING.md`.
--- ---
## 1. Unit tests that should be written ## 1. Blocked on libakerror
### 1.0 Test-harness prerequisites — **DONE** (branch `feature/test-harness`) These are not fixable from inside this repository. Each one currently costs
something here, and the cost is what makes them worth carrying.
`ctest` is green: 5/5, no *Not Run* entries. Everything in §1.11.9 can now be written ### 1.1 The error pool is a process-global array with no locking
against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
- [x] **The current tests cannot fail.** `tests/test_linkedlist.c` asserted nothing at all; **This is what makes the library single-threaded**, and it is the largest open
it printed node names and returned `0` unconditionally, so any of the list bugs in item by some distance.
§2.1 would pass it. Rewritten as 15 assertion-based cases covering the append, iterate
and pop behaviour that is correct today.
- [x] **`tests/test_tree.c` was red.** `ctest` reported `tree (Failed)`, exit 136, because
`parms.steps` was never reset between the three searches, so the second assertion
compared an accumulated `14` against `7`. Each search now builds its own tree and
params. The file records that its step counts do not actually distinguish the three
traversal orders — real order assertions remain §1.8.
- [x] **Adopt libakerror's test conventions.** `tests/aksl_capture.h` provides `AKSL_CHECK()`
(an `NDEBUG`-proof assert), `AKSL_CHECK_STATUS()`/`AKSL_CHECK_OK()` which run an
akerror-returning call, assert on its status and release the context,
`AKSL_CHECK_MSG_CONTAINS()`, a capturing `akerr_log_method`, `aksl_slots_in_use()` for
pool-leak checks, and an `AKSL_RUN()` driver that additionally fails any test which
leaks an error-pool slot.
- [x] **One test source per behaviour, registered with CTest via a list variable.**
`tests/test_<name>.c` → executable `test_<name>` → CTest test `<name>`, driven by
`AKSL_TESTS` in `CMakeLists.txt`.
- [x] **Add a `WILL_FAIL` category.** Two lists: `AKSL_WILL_FAIL_TESTS` for tests that abort
by design (empty for now) and `AKSL_KNOWN_FAILING_TESTS` for the confirmed defects in
§2.1 — see the note at the head of §2.1.
- [x] **Fix the four permanently-failing submodule tests.** `deps/libakerror` is added
`EXCLUDE_FROM_ALL`, so akerror's `add_test` entries registered but its test binaries
were never built, and `ctest` reported `err_catch`, `err_cleanup`, `err_trace`,
`err_improper_closure` as *Not Run* → failed on every run. Neither the pinned commit
nor upstream `main` guards that registration, CMake has no way to un-register a test,
and `set_tests_properties` cannot reach across directory scopes — so `add_test` and
`set_tests_properties` are shadowed for the duration of the `add_subdirectory` call.
The dependency has its own CI; this suite now contains only this project's tests.
- [x] **Wire in a memory-error build.** `cmake -S . -B build-asan -DAKSL_SANITIZE=ON` adds
`-fsanitize=address,undefined -fno-sanitize-recover=all` to the library, the tests and
the vendored libakerror. Currently clean, and it is the intended way to test the items
in §2 that only misbehave under instrumentation (uninitialised `%s` in
`aksl_realpath`, unbounded `vsprintf`, missing `va_end`).
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
`include/akstdlib.h` (178 mutants) and exposed as
`cmake --build build --target mutation`. CI runs the narrower `src/stdlib.c` set (173
mutants) as its own `mutation_test` job with `--threshold 80` and a JUnit report.
**Current score: 89.6%** — 155 killed, 18 survived. It was 46.8% (81 killed, 92
survived) before §1.21.6 were written; the remaining survivors are:
| surviving mutants | where | why | `AKERR_ARRAY_ERROR` is a fixed array in `deps/libakerror/src/error.c`, handed out
|---|---|---| by `akerr_next_error()` with no synchronisation of any kind. Every entry point in
| 5 | deleted statements whose absence nothing observes (`free(ptr)`, `obj->next = NULL`, three `SUCCEED_RETURN`s) | needs an assertion on the side effect, not on the status | this library takes a slot from it on any failure path, so two threads raising
| 4 | `aksl_list_append`'s cycle/tail walk | the function is broken here (§2.1.1); its known-failing test cannot pin the internals yet | errors concurrently can be handed the same slot and will corrupt each other's
| 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (§2.2.8) — defaulted, then never called | message, status and stack trace.
| 3 | `*count == -1` in the `printf` family | `*count` on the error path is itself a contract gap (§1.3) |
| 2 | `feof` in `aksl_fwrite`, `break` after the BFS `FAIL_RETURN` | unreachable in practice |
The threshold is a regression ratchet, not a quality bar; raise it as those survivors **Consequence.** `README.md` says plainly that the library is not thread-safe.
become assertions. Each one is a concrete missing test — `mutation-junit.xml` lists That is honest, and it is also a hard ceiling: §4's `pthread_*` and socket
them with `file:line` and the exact edit. wrappers cannot be written until this is resolved, because a threading API
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant nobody can call from a thread is not an API.
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
loop, so `ctest` waited forever, the mutation harness killed `ctest` at its own
120 s timeout, and the orphaned test binary kept spinning at 100% CPU while holding
the pipe open — the run wedged and had to be killed by hand. `TIMEOUT 30` on every
test makes `ctest` reap its own child, so those mutants are now killed in 30 s and
leave nothing behind. It also guards the real suite against the same class of bug.
- [x] **CI could not have run any of this.** `actions/checkout@v4` was not fetching
submodules, so `add_subdirectory(deps/libakerror)` had nothing to descend into and the
configure step failed before ever reaching the tests. Added `submodules: recursive`,
and swapped `cmake --build build --target test` for `ctest --output-on-failure` so a
failure is diagnosable from the job log. The deeper problem — CI installing
`libakerror@main` while the build actually compiles the pinned submodule — is §2.3.
### 1.1 Memory wrappers **What closing it would touch.** libakerror's pool — either a mutex around
`akerr_next_error`/`akerr_release_error`, or thread-local slot arrays, which
would suit the bounded-preallocation style better and cost nothing on the
single-threaded path. Then a TSan job in `.gitea/workflows/ci.yaml` and a
concurrent smoke test here, and the warning in `README.md` comes out.
- [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`. ### 1.2 `IGNORE()` logs a context and never releases it
- [x] `aksl_malloc(n, NULL)``AKERR_NULLPOINTER`, message content asserted.
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
be `0`. See §2.2.1.
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
NULL rather than garbage.
- [x] `aksl_free(NULL)``AKERR_NULLPOINTER` (assert the documented behaviour, since
`free(NULL)` is legal C and callers will be surprised).
- [x] `aksl_free` happy path, and a malloc→free round trip under ASan.
- [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL`
`AKERR_NULLPOINTER`.
- [x] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
(`AKERR_VALUE`) or documented as UB like `memcpy`.
### 1.2 File I/O `deps/libakerror/include/akerror.tmpl.h:308`. The macro assigns the context to
`__akerr_last_ignored`, logs it, and stops. The slot is never returned to the
pool, so every `IGNORE()` on a failing call leaks one — and after
`AKERR_MAX_ARRAY_ERROR` of them the pool is exhausted and `ENSURE_ERROR_READY`
calls `exit(1)`.
Covered by `tests/test_stream.c`. **Consequence here.** `aksl_tree_iterate`'s `CLEANUP` block (`src/stdlib.c`)
cannot use `IGNORE()` to drop a queue-drain failure and open-codes the
log-then-release by hand instead, with a comment saying why. It is four lines
that should be one.
- [x] `aksl_fopen` happy path on a temp file; `*fp` is written. **What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
- [x] `aksl_fopen("/nonexistent/path", "r", &fp)``ENOENT` propagated as the status, with the workaround here.
the pathname in the message.
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
`EACCES`.
- [x] `aksl_fopen(path, mode, NULL)``AKERR_NULLPOINTER`.
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
see §2.2.2. Test once the guards exist.
- [x] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
`AKERR_IO`; `fp == NULL``AKERR_NULLPOINTER`. `ptr == NULL` is still unchecked and
untested (see §2.2.3).
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
contract is; today this silently returns success and the caller cannot tell how many
members were read.
- [x] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; `fp == NULL`
`AKERR_NULLPOINTER`. The full-device (`/dev/full`) case is still open.
- [x] `aksl_fclose` happy path; `NULL``AKERR_NULLPOINTER`. Double-close is still
undecided, so it is neither caught nor tested.
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
surfaces `errno`.
- [x] Round-trip test: `fopen``fwrite``fclose``fopen``fread` → compare bytes.
### 1.3 Formatted output ### 1.3 libakerror does not namespace its `coverage` target when embedded
Covered by `tests/test_format.c`. `deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
not `coverage`.
- [x] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte **Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure
count written through `count` and the produced text. (`aksl_printf` is checked by at all: *"another target with the same name already exists"*. `CMakeLists.txt`
pointing `stdout` at a temp file for the duration of the call.) shadows `add_custom_target` for the duration of the `add_subdirectory()` call and
- [x] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`. renames the dependency's to `akerror_coverage`.
- [x] `aksl_fprintf` to a read-only stream → the `errno` it saw (`EBADF`) with "Short write"
in the message. `*count` is still left holding `-1`; the test asserts the status only,
so it will not have to change when that is fixed.
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
- [x] Regression test for the missing `va_end` (§2.1.4) — 512 variadic calls in a loop, which
the sanitizer build also runs.
- [ ] Format-string/arg mismatch is caught at compile time once
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
### 1.4 String → number **What closing it would touch.** Apply the same
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test upstream that the
`mutation` target already has; then delete the shadow here, which sits directly
above the `add_test` shadow and shares its comment.
Happy paths and NULL guards are covered by `tests/test_convert.c`; the strict-conversion ### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
contract is asserted by `tests/test_convert_strict.c`, which is registered as a known
failure until §2.1.5 is fixed.
- [x] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and **Consequence here.** `cmake/akstdlib.cmake.in` has to call
leading whitespace. `find_dependency(akerror)` with no version, because a request for one would be
- [x] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`. refused for want of a version file no matter what is installed. The 1.0.0 floor
- [x] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success* therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in
with `*dest == 0`. `AKERR_VALUE` is asserted by the known-failing test. `akstdlib.h`, neither of which covers a `find_package` consumer.
- [x] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
a garbage value (`-1` on this box). `ERANGE` is asserted by the known-failing test.
- [x] Empty string, `" "` and `"12abc"` (trailing junk) → `AKERR_VALUE`, in the known-failing
test. `"0x10"` and `"inf"`/`"nan"` for `atof` are still open.
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
### 1.5 `aksl_realpath` **What closing it would touch.** One `write_basic_package_version_file()` call
upstream — libakstdlib already does this correctly and can be copied — then add
Covered by `tests/test_path.c`. Every failure case there passes a zeroed `resolved_path`, the `1.0.0` floor to the `find_dependency` here.
because the wrapper's own error path formats that buffer with `%s` (§2.1.6).
- [x] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
- [x] Non-existent path → `ENOENT`.
- [x] A path component that is not a directory → `ENOTDIR`.
- [ ] Symlink loop → `ELOOP`.
- [x] `path == NULL``AKERR_NULLPOINTER`.
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
case in §2.1.6; run it under ASan/MSan.
### 1.6 `aksl_strhash_djb2`
Covered by `tests/test_strhash.c`.
- [x] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
pre-computed 32-bit values.
- [x] `len == 0` returns 5381 regardless of `str` contents.
- [x] NULL `str` and NULL `hashval``AKERR_NULLPOINTER`.
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
expected value must be the `unsigned char` one (5868578; the wrapper returns the
sign-extended 5859874 today). Every vector in the test file is 7-bit ASCII precisely so
that it says nothing about this case either way.
- [x] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
- [x] Same input → same output across two calls (no hidden state).
### 1.7 Linked list
- [ ] **`aksl_list_append` builds a correct chain of N nodes** — **[CONFIRMED BROKEN]**, see
§2.1.1. Appending `n1..n4` to `n0` yields the chain `n0 -> n4`; `n1`, `n2`, `n3` are
silently dropped. This is the single most important test to add.
- [ ] `append` sets `obj->prev` to the real tail and `obj->next` to NULL.
- [ ] `append` onto an empty (single, zeroed) node.
- [ ] `append` NULL list / NULL obj → `AKERR_NULLPOINTER`.
- [ ] `append` onto a list containing a cycle → `AKERR_CIRCULAR_REFERENCE`, for a self-loop,
a 2-node cycle, and a cycle that does not include the head.
- [ ] `append` a node that is already in the list (aliasing) — define and test the contract.
- [ ] **`aksl_list_iterate` visits every node exactly once, starting at the head** —
**[CONFIRMED BROKEN]**, see §2.1.2: it starts iterating from the *midpoint* left behind
by the cycle detector, so the head is never visited. Assert both the visit count and
the visit order.
- [ ] `iterate` over a single-node list visits exactly one node.
- [ ] `iterate` NULL list / NULL iter → `AKERR_NULLPOINTER`.
- [ ] `iterate` over a cyclic list → `AKERR_CIRCULAR_REFERENCE` (self-loop, 2-node,
tail-to-middle).
- [ ] `iterate` where the callback raises `AKERR_ITERATOR_BREAK` → iteration stops at that
node, the wrapper returns success, and the visit count proves the early exit.
- [ ] `iterate` where the callback raises some *other* error → that error propagates out
unchanged, with the callback's message intact.
- [ ] `aksl_list_pop` on a middle node relinks `prev`/`next` correctly and clears the popped
node's pointers.
- [ ] `pop` on the head node, on the tail node, and on a single-node list.
- [ ] `pop(NULL)``AKERR_NULLPOINTER`.
- [ ] `pop` then `iterate` — the list is still traversable and the popped node is gone.
- [ ] Pool-accounting test: after a long sequence of list operations including failures,
`akerr_slots_in_use() == 0`.
### 1.8 Tree
- [ ] Visit **order** assertions, not just counts, for `DFS_PREORDER`, `DFS_INORDER` and
`DFS_POSTORDER` over the 7-node tree — record the visited node pointers into an array
and compare against the expected sequence. The current test only counts steps, which
cannot distinguish the three orders (all three visit 7 nodes).
- [ ] **`AKERR_ITERATOR_BREAK` aborts the whole traversal** — **[CONFIRMED BROKEN]**, see
§2.1.3. The pre-order case exists as `tests/test_tree_iterate_break.c`: pre-order
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
- [x] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT``AKERR_NOT_IMPLEMENTED`
today, asserted in `tests/test_tree.c` along with the callback never being called;
replace with real order assertions once implemented (§3 / §2.2.9).
- [x] `aksl_tree_iterate` NULL `root` / NULL `iter``AKERR_NULLPOINTER`, and a callback error
that is *not* `AKERR_ITERATOR_BREAK` propagates out to the caller.
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
having visited nothing. Should be `AKERR_VALUE`.
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
the same silent-success hole. Either implement it or reject it.
- [ ] `root == NULL` / `iter == NULL``AKERR_NULLPOINTER`.
- [ ] Single-node tree (no children) visits exactly once, in all three orders.
- [ ] Left-only and right-only degenerate chains.
- [ ] Callback raising a non-`ITERATOR_BREAK` error propagates out of the recursion with the
original status and message.
- [ ] Custom `lalloc`/`lfree` are actually invoked — **currently they are stored and never
called** (§2.2.8), so this test will fail until BFS lands.
- [ ] Deep/degenerate tree (e.g. 100k-node left chain) — documents the recursion-depth limit
(§2.2.7).
- [ ] A tree containing a cycle (child pointing back at an ancestor) — currently infinite
recursion; test for `AKERR_CIRCULAR_REFERENCE` once guarded.
### 1.9 Cross-cutting
- [ ] **Error-pool accounting**: for *every* wrapper, a test that drives the failure path
`AKERR_MAX_ARRAY_ERROR + 10` times and asserts the pool does not leak
(`akerr_slots_in_use() == 0` after each handled error).
- [ ] **Stack-trace content**: assert that the file/function/line recorded by each wrapper's
`FAIL` points at `src/stdlib.c` and the right function name.
- [ ] **`AKERR_NOIGNORE` is effective**: a negative compile test where a wrapper's return is
discarded and `-Werror=unused-result` fires.
- [ ] **Thread safety**: libakerror's `AKERR_ARRAY_ERROR` is a process-global array with no
locking. If `libakstdlib` is meant to be callable from threads, add a concurrent
smoke test under TSan — and if it is not, say so in the README.
--- ---
## 2. Corner cases in existing functionality that should be resolved ## 2. Known gaps in what is already wrapped
### 2.1 Confirmed defects (reproduced against the built library) ### 2.1 A short transfer with neither EOF nor a stream error is untested
Four of these now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS` `src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread`
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible: and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c`, whole library.
`tests/test_tree_iterate_break.c` and `tests/test_convert_strict.c`. When one of these
defects is fixed, CTest reports that test as failed with *unexpectedly passed* — that is the
cue to move it into `AKSL_TESTS`.
1. **`aksl_list_append` does not find the tail — it silently truncates the list.** The standard permits a short transfer with neither indicator set, so the branch
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding: is correct to have. Every way of actually producing one on Linux sets `feof` or
`tail` is set to `slow` *before* `slow` advances, so it tracks the node *behind the `ferror` first, so nothing in the suite reaches it.
midpoint*, not the tail. Appending `n1`, `n2`, `n3`, `n4` to `n0` produces the chain
`n0 -> n4`; `n1``n3` are unlinked and lost. The fix is to keep the cycle check but walk
a separate cursor to the real tail (`while (tail->next) tail = tail->next;`), or run
Floyd first and then walk to the end.
2. **`aksl_list_iterate` skips the first half of the list.** `src/stdlib.c:324`. After the **Consequence.** Low: the code is a handful of lines and reviewed, but it is the
cycle-detection loop, `slow` is left at the list midpoint, and the visiting loop then only error path in the library that has never executed.
starts from `slow` instead of from `list`. The head node is never passed to the callback.
Fix: iterate from `list`, not from `slow`.
3. **`AKERR_ITERATOR_BREAK` does not stop a tree traversal.** `src/stdlib.c:251`. The **What closing it would touch.** A `FILE *` over a custom stream — `fopencookie`
recursive frame in which the callback raises the break handles it in its own on glibc, `funopen` on the BSDs — whose read function returns a short count
`PROCESS`/`HANDLE(e, AKERR_ITERATOR_BREAK)` block and returns *success*; the parent's without setting either flag. That is a platform-specific test helper in
`PASS` therefore sees no error and continues on to the sibling subtree. Breaking at `tests/aksl_capture.h` guarded on the platform, which is why it has not been
`tree[3]` in pre-order still visits all 7 nodes. Fix by splitting the recursion: an written yet rather than an oversight.
internal helper that propagates `AKERR_ITERATOR_BREAK` unhandled, and a public entry point
that swallows it exactly once at the top. `tests/test_tree.c` currently passes only
because the search target is the last node in all three orders.
4. **`va_end` is never called.** `src/stdlib.c:96`, `:109`, `:122` each call `va_start` with ### 2.2 The string-buffer overflow guard is untestable
no matching `va_end` on any path — happy or error. This is undefined behaviour per the C
standard and leaks register-save state on some ABIs.
5. **`aksl_atoi`/`atol`/`atoll`/`atof` cannot report a conversion failure.** `src/collections.c`, the `capacity = needed` arm in `strbuf_reserve`. Reaching it
`src/stdlib.c:135``169`. `atoi("not a number")` returns success with `0`; needs an `aksl_StrBuf` within a factor of two of `SIZE_MAX`, which is not a test,
`atoi("99999999999999999999")` returns success with a wrapped value. Since the entire it is a hang. The other two uncovered lines.
point of this library is turning silent libc failures into error contexts, these should be
reimplemented over `strtol`/`strtoll`/`strtod` with `errno = 0` before the call, an
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
`tests/test_convert_strict.c` asserts that contract and is registered as a known failure.
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`. **Consequence.** None known. It is there because doubling a capacity is a
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a multiplication, and an unguarded one is how a growable buffer turns into a heap
buffer, but the wrapper discards `result`, so the caller gets nothing and the buffer overflow.
leaks.
- On failure the message formats `resolved_path` with `%s` while `realpath` leaves it
unspecified — for the normal caller who passed an uninitialised stack buffer, the error
path itself reads uninitialised memory and can crash.
- There is no way to express the buffer's size, so callers must know to supply `PATH_MAX`
bytes. Consider `aksl_realpath(const char *path, char *buf, size_t buflen)`, or an
allocating variant that returns the malloc'd pointer through an out-param.
### 2.2 Latent issues and API contract gaps **What closing it would touch.** Nothing worth doing. Recorded so the coverage
listing does not read as an oversight.
1. **`errno` is used as an error status where it may be stale.** `aksl_malloc` reports ### 2.3 `aksl_version_check()` ignores its `patch` argument
`errno` when `malloc` returns NULL, but `malloc(0)` may legitimately return NULL without
setting `errno`, producing a `FAIL` with `status == 0` — an error context that every
`DETECT`/`CATCH` will read as *success* while still holding a pool slot. Guard every
`errno`-sourced status with a fallback (`errno ? errno : AKERR_IO`) and set `errno = 0`
before the call being wrapped.
2. **`aksl_fopen` does not validate `pathname` or `mode`.** `fopen(NULL, ...)` is UB. Add `src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule —
`AKERR_NULLPOINTER` guards. patch level never breaks the ABI — but the parameter exists only so the error
message can name the caller's full version.
3. **`aksl_fread`/`aksl_fwrite` lose the transfer count and hide short transfers.** **Consequence.** None today. If a future compatibility rule needs the patch level
- `ptr` is never NULL-checked in either function. to participate, that is the line to change, and the `#if` in
- Neither has an out-param for the number of members actually transferred, so a caller who `tests/test_version.c` is the test that encodes the rule.
gets `AKERR_EOF` cannot tell how much data arrived. Add `size_t *nmemb_out`.
- If `nmemr != nmemb` but neither `feof` nor `ferror` is set, both functions fall through
to `SUCCEED_RETURN` — a short transfer reported as complete success.
- `aksl_fwrite`'s error message reads `"Error reading file"` (`src/stdlib.c:83`), and
checking `feof()` on a write path is meaningless.
4. **`aksl_sprintf` wraps `vsprintf`, which is unbounded.** There is no way for a caller to ### 2.4 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
bound the destination. Add `aksl_snprintf`/`aksl_vsnprintf` and consider deprecating the
`sprintf` wrapper, or reject it outright — an "error-handling" wrapper around an
unbounded write is a sharp edge the library exists to remove.
5. **No `format` attribute on the variadic wrappers.** Adding Macro artifacts rather than gaps. In libakerror that macro begins with the
`__attribute__((format(printf, 2, 3)))` (and the equivalents) restores the compile-time `break;` belonging to `PROCESS`'s `case 0:` arm, reachable only when a callback
format/argument checking that callers lose by going through the wrapper. returns a non-NULL context whose status is *zero* — which is the pathological case
the errno-fallback work removed. Left uncovered deliberately rather than pinned
6. **`aksl_strhash_djb2` sign-extends bytes ≥ 0x80.** `src/stdlib.c:181` iterates a by a test that would have to manufacture it.
`char *`, which is signed on x86/ARM Linux, so high bytes contribute a sign-extended
negative value and the hash differs from the canonical djb2 — and differs across
platforms where `char` is unsigned. Iterate a `const unsigned char *`. Also take
`const char *` in the signature so callers need not cast away constness.
7. **`aksl_tree_iterate` recursion is unbounded and cycle-blind.** A deep or degenerate tree
overflows the stack, and a child pointer that loops back to an ancestor recurses forever.
Add a depth limit (raising `AKERR_OUTOFBOUNDS`) and/or a visited check raising
`AKERR_CIRCULAR_REFERENCE`, consistent with what the list functions already do.
8. **`lalloc`, `lfree` and `queue` are dead parameters.** `lalloc`/`lfree` are defaulted and
then never called; `queue` is entirely unused (it is the only `-Wunused-parameter` warning
in the file). The doc comment even tells the caller to "pass NULL here", which is a sign
the queue belongs in an internal helper rather than the public signature. Either
implement BFS so they are used, or drop them from the public API until it is.
9. **Unknown `searchmode` values return success.** The `switch` in `aksl_tree_iterate` has no
`default:`; `searchmode == 99` and the header-defined `AKSL_TREE_SEARCH_VISIT` (5) both
fall straight through to `SUCCEED_RETURN` having visited nothing. Add
`default: FAIL_RETURN(e, AKERR_VALUE, ...)`.
10. **`AKSL_TREE_SEARCH_BFS`/`BFS_RIGHT` are unimplemented** (`AKERR_NOT_IMPLEMENTED`), and
`AKSL_TREE_SEARCH_VISIT` is declared in the header with a documented meaning but no
implementation anywhere.
11. **`aksl_memset`'s and `aksl_memcpy`'s inner checks are dead code.** `memset` returns `s`
and `memcpy` returns `d`, both unconditionally; neither can fail. The
`FAIL_ZERO_RETURN(e, memset(...), errno, ...)` and `(memcpy(...) == d)` checks can never
fire and only obscure the intent. Also, `memcpy` with overlapping ranges is UB — either
document that or dispatch to `memmove`.
12. **`aksl_list_pop` cannot tell the caller the new head.** Popping the head leaves the
caller's head pointer dangling at a now-detached node. Add an out-param for the new head,
or a `aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)` form.
13. **`aksl_free` does not clear the caller's pointer**, so double-free remains easy. Consider
an `aksl_freep(void **ptr)` that frees and NULLs.
14. **No initialisers for the public structs.** Every caller must remember to `memset` an
`aksl_ListNode`/`aksl_TreeNode` to zero before use (both existing tests do). Add
`aksl_list_node_init`/`aksl_tree_node_init` or `AKSL_LIST_NODE_INIT` macros.
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; `akstdlib.h` pulls in
`stdio.h`/`stdlib.h`/`string.h`/`stdint.h` into every consumer's namespace. The version
macros this item also asked for are **done**: `AKSL_VERSION_MAJOR`/`_MINOR`/`_PATCH`/
`_STRING`/`_NUMBER`/`_SONAME` are generated into `akstdlib_version.h` from
`project(akstdlib VERSION …)`, with `aksl_version()`/`aksl_version_string()`/
`aksl_version_soname()` and `AKSL_VERSION_CHECK()` reporting the *loaded* library so a
header/`.so` mismatch is nameable. See `tests/test_version.c` and `README.md`.
17. **Doxygen coverage is 2 functions out of 24.** A `Doxyfile` exists but only
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict). The four
`aksl_version_*` entry points have prose comments in the header but no Doxygen tags.
### 2.3 Build, CI and repository
- [ ] **`CMakeLists.txt:4-6` sets `CMAKE_CXX_FLAGS` in a C-only project**, so `-g -ggdb -pg`
never reach the C compiler. Use `CMAKE_C_FLAGS` — or better, drop `-pg` from the
default build entirely (it is what produces the stray `gmon.out` sitting untracked in
the repo root) and let `CMAKE_BUILD_TYPE` control debug info.
- [ ] **No warning flags.** Add `-Wall -Wextra` (and ideally `-Werror` in CI). The only
current warning is the unused `queue` parameter, so the cost of turning them on is low.
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
- [x] **The `deps/libakerror` submodule was pinned 22 commits behind `main`** (pinned at
`4fad0ce "Add gitea workflow"`). Bumped to `5ff8790`, which is libakerror **1.0.0**
the release that made the status-name table private, moved consumer status codes to a
band starting at `AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced,
and gave the library an soname. See `deps/libakerror/UPGRADING.md`. The bump also
brings the refcount-leak fix, the stack-trace buffer-overflow fix and the
format-string fix. Nothing in this library's sources used the removed
`AKERR_MAX_ERR_VALUE`, `__AKERR_ERROR_NAMES`, `AKERR_STATUS_RANGE_OK` or
`AKERR_STATUS_NAME_OK`, so the source change was confined to the build, the packaging
and one compile-time guard. What it did move is recorded in the three items below.
- [ ] **libakerror does not namespace its `coverage` target when embedded**, only its
`mutation` target (`deps/libakerror/CMakeLists.txt:172` vs `:189`). A
`-DAKSL_COVERAGE=ON` top-level build therefore failed to configure at all —
*"another target with the same name already exists"* — the moment the dependency
gained a coverage target. Worked around in `CMakeLists.txt` by shadowing
`add_custom_target` for the duration of the `add_subdirectory()` call and renaming the
dependency's to `akerror_coverage`, alongside the existing `add_test` shadow. **Fix
upstream and delete the workaround**: libakerror should apply the same
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test to `coverage` that it
already applies to `mutation`.
- [ ] **Branch coverage of `src/stdlib.c` fell from 51.0% to 44.3% on the 1.0.0 bump**, with
no change to this library's sources or tests. Line coverage held at 99.0% (200/202)
and function coverage at 100% (21/21); the branch *denominator* went from 661 to 1087
because the 1.0.0 `PREPARE_ERROR`/`FAIL_*` macros expand to more branches at every
call site. 337/661 became 481/1087 — 144 more branches covered, 426 more counted. The
CI branch gate was re-ratcheted 45 → 40 to match. Most of the added branches are
unreachable from the way this library calls the macros, so raising the number here
would mean testing libakerror's macros rather than this library; that belongs to
libakerror's mutation suite, since macros expand at the call site and coverage cannot
see them properly from either side. Revisit if the gap ever hides a real regression.
- [x] **`project()` declared no `VERSION`**, so `@PROJECT_VERSION@` expanded to nothing, the
installed `akstdlib.pc` shipped an empty `Version:` field, and `libakstdlib.so` carried
no soname. Now `project(akstdlib VERSION 0.1.0)`, with `SOVERSION` `0.1`
(`MAJOR.MINOR` while major is 0, `MAJOR` from 1.0 — the `if()` in `CMakeLists.txt` and
the matching `#if` in `tests/test_version.c` encode the rule and are tested against
each other), an `akstdlibConfigVersion.cmake` at `SameMinorVersion`, and version macros
generated into `akstdlib_version.h`. Verified: `find_package(akstdlib 0.1)` is accepted
while `0.2` and `1.0` are refused, and a 0.2.0 build dropped in under the 0.1 filename
is caught by `AKSL_VERSION_CHECK()`.
- [ ] **libakerror still installs no `akerrorConfigVersion.cmake`**, so
`cmake/akstdlib.cmake.in` has to call `find_dependency(akerror)` with no version — a
request for one would be refused for want of a version file regardless of what is
installed. libakstdlib now does this correctly and libakerror does not; fix it there
with the same `write_basic_package_version_file()` call, then add the `1.0.0` floor to
the `find_dependency` here. Until then the floor rests on `akstdlib.pc`'s `Requires:`
and the `#error` guard in `akstdlib.h`.
- [ ] **`aksl_version_check()` ignores its `patch` argument** (`src/stdlib.c`, the `(void)patch`),
which is correct for the current "same soname" rule but means the parameter exists only
so the error message can name the caller's full version. If a future rule needs patch
to participate, that is the line to change.
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
`libakerror@main` and installs it, while the build it then runs is top-level and so
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
depending on where you build, and the installed one is never actually linked. Pick
one. (The submodule is at least *present* in CI now: §1.0 added
`submodules: recursive` to the checkout, without which configure failed outright.)
- [x] **`ctest` was red** (`tree` failing plus four *Not Run* submodule tests) — fixed in
§1.0. The build badge in `README.md` means something again.
- [ ] **Untracked build litter** in the repo root: `build/`, `gmon.out`,
`CMakeLists.txt-akstdlib`, and a dozen `*~` editor backups. Add a `.gitignore`
(libakerror has one; this repo does not).
- [ ] **`README.md` is a build badge and nothing else.** It needs at minimum: what the
library is, the akerror wrapper contract, the list of wrapped functions, and the
deviations from libc semantics.
- [ ] **Indentation is inconsistent**`src/stdlib.c` mixes hard tabs and 4-space indents
within the same functions. libakerror standardised on Stroustrup style
(commit `e5f7616`); apply the same here, ideally with a checked-in `.clang-format`.
--- ---
## 3. libc functions not yet wrapped ## 3. Deliberate omissions
Ordered roughly by how much a caller of this library would miss them. Each entry means "add Recorded so nobody adds them thinking they were forgotten. Each is a decision,
an `aksl_`-prefixed wrapper that turns the documented failure modes into an and each can be revisited with an argument.
`akerr_ErrorContext`".
### 3.1 High priority — gaps in areas the library already covers | Not wrapped | Why |
|---|---|
| `sprintf`, `vsprintf` | Cannot be bounded. An error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. `aksl_snprintf` and `aksl_asprintf` cover both real uses. |
| `strtok` | Keeps its state in a hidden static, so two interleaved tokenisations corrupt each other silently and any use from a thread is a bug. `aksl_strtok_r` and `aksl_strsep` cover it. |
| `strcpy`, `strcat` as libc spells them | Cannot be called safely without the destination's size. The wrappers take it. |
| `setbuf` | Exactly `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ)` and strictly less expressive. Wrapping it would add a second way to say one thing. |
| `perror` | Writes to stderr and consults a global. `aksl_strerror` is the akerror-native equivalent and knows this library's own statuses as well as errno's. |
| `strerror_r` | Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. `aksl_strerror` is built on libakerror's registry instead. |
**Memory (`stdlib.h`, `string.h`)** ---
- [ ] `calloc`, `realloc`, `reallocarray``realloc`'s "returns NULL and the old pointer is
still valid" trap is exactly what this library should be hiding.
- [ ] `aligned_alloc`, `posix_memalign`
- [ ] `memmove`, `memcmp`, `memchr`
**Bounded string formatting (`stdio.h`)** ## 4. Not yet wrapped
- [ ] `snprintf`, `vsnprintf` — needed to make §2.2.4 fixable.
- [ ] `vprintf`, `vfprintf`, `vsprintf` — the `va_list` forms, so consumers can build their
own variadic wrappers on top.
- [ ] `asprintf` / `vasprintf` (GNU) as an allocating alternative.
**String → number (`stdlib.h`)** Ordered by how much a caller of this library would miss them. §3.1 and §3.6 of
- [ ] `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtod`, `strtof`, `strtold` — the correct the old numbering are done; what follows is what is left.
foundation for §2.1.5.
**Strings (`string.h`)** ### 4.1 POSIX file and process API
- [ ] `strlen`, `strnlen`
- [ ] `strcpy`, `strncpy`, `strcat`, `strncat` — with truncation reported as an error rather
than silently accepted, which is the whole value proposition here.
- [ ] `strdup`, `strndup`
- [ ] `strcmp`, `strncmp`, `strcasecmp`, `strncasecmp`, `strcoll`
- [ ] `strchr`, `strrchr`, `strstr`, `strcasestr`, `strpbrk`, `strspn`, `strcspn`
- [ ] `strtok_r`, `strsep`
- [ ] `strerror_r`
**Stream I/O (`stdio.h`)** The most likely next surface. Nothing here is blocked; it is simply not written.
- [ ] `fseek`, `ftell`, `rewind`, `fgetpos`, `fsetpos`, `fseeko`, `ftello`
- [ ] `fflush`
- [ ] `fgets`, `fputs`, `fgetc`/`getc`/`getchar`, `fputc`/`putc`/`putchar`, `ungetc`
- [ ] `getline`, `getdelim`
- [ ] `freopen`, `fdopen`, `fileno`
- [ ] `setvbuf`, `setbuf`
- [ ] `clearerr`, `feof`, `ferror` — thin, but worth exposing so callers never touch raw
`FILE *` internals.
- [ ] `sscanf`, `fscanf`, `scanf` — the return-value semantics (items matched vs `EOF`) are a
classic silent-failure source.
- [ ] `remove`, `rename`, `tmpfile`, `mkstemp`, `mkdtemp`
- [ ] `perror` — or an akerror-native equivalent.
### 3.2 POSIX file and process API (likely the next major surface)
**`unistd.h` / `fcntl.h`** **`unistd.h` / `fcntl.h`**
- [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek` - [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek`
@@ -563,153 +186,144 @@ an `aksl_`-prefixed wrapper that turns the documented failure modes into an
- [ ] `sysconf`, `pathconf` - [ ] `sysconf`, `pathconf`
- [ ] `sleep`, `usleep`, `nanosleep` - [ ] `sleep`, `usleep`, `nanosleep`
The short-read/short-write contract is the interesting part: `read(2)` returning
fewer bytes than asked for is *normal* on a pipe or a socket and a failure on a
regular file, so the wrapper needs the same transferred-count out-param
`aksl_fread` has, and callers need to be told which case they are in.
**`sys/stat.h`** **`sys/stat.h`**
- [ ] `stat`, `fstat`, `lstat`, `fstatat` - [ ] `stat`, `fstat`, `lstat`, `fstatat`
- [ ] `statvfs`, `fstatvfs` - [ ] `statvfs`, `fstatvfs`
**`dirent.h`** **`dirent.h`**
- [ ] `opendir`, `fdopendir`, `readdir`, `readdir_r`, `closedir`, `rewinddir`, `scandir` - [ ] `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir`, `scandir`
`readdir(3)` returning NULL for both "end of directory" and "error, check errno"
is the same conflation `aksl_fgetc` and `aksl_fgets` already untangle, and should
be untangled the same way: AKERR_EOF for the end, the errno for the error.
**Process control** **Process control**
- [ ] `fork`, `execve` / `execvp` / `execl` family, `waitpid`, `wait` - [ ] `fork`, the `exec*` family, `waitpid`, `wait`
- [ ] `posix_spawn` - [ ] `posix_spawn`
- [ ] `system`, `popen`, `pclose` - [ ] `system`, `popen`, `pclose`
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid` - [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a hook on shutdown. - [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a shutdown hook
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv` - [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
`fork` needs thinking about before it is wrapped: the error pool is inherited by
the child, and any context live at the moment of the fork exists twice
afterwards.
**`sys/mman.h`** **`sys/mman.h`**
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise` - [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
### 3.3 Time ### 4.2 Time
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday` - [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime` - [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
- [ ] `strftime`, `strptime` - [ ] `strftime`, `strptime`
- [ ] `clock`, `times` - [ ] `clock`, `times`
### 3.4 Sorting, searching and misc `stdlib.h` `strftime(3)` returns 0 for both "the output was empty" and "it did not fit",
which is the bounded-write ambiguity `aksl_snprintf` already resolves.
- [ ] `qsort`, `qsort_r`, `bsearch` — comparator errors currently have nowhere to go; an ### 4.3 Sorting, searching and the rest of `stdlib.h`
akerror-aware comparator signature would be a genuine improvement.
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` - [ ] `qsort`, `qsort_r`, `bsearch` — a comparator that fails currently has
nowhere to put the error. An akerror-aware comparator signature, shaped
like the `aksl_TreeCompareFunc` the tree functions already take, would be a
genuine improvement over libc rather than a wrapper around it.
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` — note that
`abs(INT_MIN)` is undefined behaviour, which is exactly the kind of silent
trap worth surfacing.
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random` - [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
### 3.5 Lower priority / larger projects ### 4.4 Larger surfaces
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, `send`/`sendto`/`sendmsg`, - [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the
`recv`/`recvfrom`/`recvmsg`, `shutdown`, `setsockopt`/`getsockopt`, `getaddrinfo`/ `send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`,
`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton` `getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`.
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_create1`/`epoll_ctl`/`epoll_wait` `getaddrinfo` is the interesting one: it has its own error space, neither
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, `raise`, errno nor akerror, which needs mapping into a registered status range.
`signalfd` - [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*`
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`, `pthread_cond_*`, - [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`,
`pthread_rwlock_*`, `sem_*` — blocked on resolving the thread-safety question in §1.9 `raise`, `signalfd`. A handler cannot raise an akerror context — the pool is
(libakerror's error pool is an unlocked process-global array). not async-signal-safe — so the wrapper covers installation and masking only,
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror` and that limit should be documented rather than discovered.
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`,
`pthread_cond_*`, `pthread_rwlock_*`, `sem_*`. **Blocked on §1.1.**
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`. `dlerror(3)`
is the same "returns a string and clears itself" trap as `strerror`.
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*` - [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
- [ ] **Math**: the `math.h` functions that set `errno`/raise FP exceptions (`sqrt`, `log`, - [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions
`pow`, `acos`, …) — probably better served by a dedicated `libakmath`. (`sqrt`, `log`, `pow`, `acos`, …). Probably a dedicated `libakmath` rather
than more surface here — the failure model is `fetestexcept`, not errno, and
it does not resemble anything else in this library.
### 3.6 Non-libc additions the current data structures imply ### 4.5 Data structures
Not libc wrappers, but the existing list/tree API is visibly incomplete: The list and tree API is complete against what §3.6 asked for. What a second
consumer might want next:
- [ ] List: `prepend`, `insert_after`/`insert_before`, `length`, `find`, `reverse`, - [ ] A dynamic array / vector, on the same "caller owns the storage" terms as
`concat`, `free_all`, reverse iteration, and a head/tail-tracking container type so `aksl_HashMap`.
`append` is O(1) instead of O(n). - [ ] A hash map keyed on something other than a string — the current one copies
- [ ] Tree: `insert`, `remove`, `find`, `height`, `count`, `free_all`, and the BFS traversal keys into fixed slots, which is right for identifiers and wrong for
the `lalloc`/`lfree`/`queue` parameters were designed for. anything large or binary.
- [ ] Hash map built on `aksl_strhash_djb2`, plus additional hashes (FNV-1a, xxHash) and a - [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so;
NUL-terminated `aksl_strhash_djb2_str` convenience form. sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers refuses. Bounded rather than dangerous, but a red-black or AVL variant is
pleasant to use. the honest fix if anyone inserts sorted data in earnest.
## 4. Evidence from the first full consumer ---
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter built on this ## 5. Evidence from the first full consumer
library and `libakerror`. It is the first consumer to exercise the whole surface rather than a
corner of it, so what it *could not* use is worth recording: it prioritizes the wishlist in §3
by what a real port actually reaches for, and it confirms which §2 entries have teeth.
**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and `akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter
**116 calls to raw libc**: built on this library and `libakerror`. It was the first consumer to exercise the
whole surface rather than a corner of it, and what it *could not* use is what
prioritised the work above.
| `aksl_*` used | Count | **The number that started it.** Across `src/`, akbasic made **10 calls into this
|---|---| library and 116 to raw libc** — a library whose value proposition is "turn silent
| `aksl_fopen` / `aksl_fclose` | 6 | libc failures into error contexts", bypassed 92% of the time by the consumer most
| `aksl_fprintf` | 2 | committed to it.
| `aksl_fwrite` | 1 |
| `aksl_strhash_djb2` | 1 |
| Raw libc it had to use instead | Count | Wanted from §3 | | Raw libc it had to use | Count | Now available as |
|---|---|---| |---|---|---|
| `snprintf` | 28 | §3.1 bounded string formatting | | `strlen` | 37 | `aksl_strlen` |
| `strlen` | 37 | §3.1 strings | | `snprintf` | 28 | `aksl_snprintf` |
| `strcmp` | 16 | §3.1 strings | | `strcmp` | 16 | `aksl_strcmp` |
| `strncpy` | 15 | §3.1 strings | | `memcpy` / `memset` | 16 | `aksl_memcpy` / `aksl_memset` |
| `memcpy` / `memset` | 16 | wrapped, but see §2.2.11 | | `strncpy` | 15 | `aksl_strncpy` |
| `strtoll` / `strtod` | 2 | §3.1 string → number | | `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
| `fgets` | 2 | §3.1 stream I/O | | `fgets` | 2 | `aksl_fgets` |
| `strstr` | 1 | §3.1 strings | | `strstr` | 1 | `aksl_strstr` |
A library whose value proposition is "turn silent libc failures into error contexts" is being **All four things the port had to write for itself now exist here.**
bypassed 92% of the time by the consumer most committed to it. Every one of those calls is a
place where a failure goes unreported.
### 4.1 What the port had to write for itself 1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines).
The `aksl_strto*` family is that, with the endptr/`errno`/range contract.
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family because
routing four diagnosable errors through it would have turned them into wrong
answers — `VAL("garbage")` silently returning `0.0`. That ban can be lifted:
the `ato*` forms report failures now. **`src/convert.c` can be deleted.**
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130
lines, needed three times over). `aksl_hashmap_*` is that table generalised,
with tombstones on delete, which the original did not have.
3. **The bounded-copy-with-truncation-as-error idiom, at ten sites.**
`aksl_strcpy` and `aksl_strncpy` are exactly that idiom.
4. **Uppercase folding for case-insensitive lookup, three times.**
`aksl_strcasecmp` and `aksl_strncasecmp`.
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). This is §2.1.5 **And the four confirmed-with-impact items are closed.** §2.2.4's unbounded
biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s `aksl_sprintf` is gone; §2.2.2's unchecked `aksl_fopen` arguments are checked, so
error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi` `akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go;
would have converted four *diagnosable errors* into *wrong answers*`VAL("garbage")` §2.2.6's sign-extended djb2 reads bytes unsigned; and §2.1.4's missing `va_end`
silently returning `0.0` rather than raising, and a malformed line number becoming line 0. which akbasic's stdio text sink ran on every line of program output — is fixed.
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family for this reason. **The
fix wanted is exactly §3.1's `strtol` family plus the endptr/`errno`/range contract §2.1.5
describes**; when it lands, `src/convert.c` gets deleted.
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). §3.6 **Still true, and still shaping the wishlist.** akbasic uses no allocator, no
already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over: lists and no trees, drawing everything from fixed pools by design. A consumer that
the interpreter needs one for variables, one for functions and one for labels per scope. It does allocate would weight §4.1's `open`/`read`/`write` far higher than this one
is open-addressed with linear probing over a caller-supplied slot count, refuses rather than does. The next thing worth doing is porting akbasic onto this release and
resizes when full, and is the single most obviously-missing data structure in the library. counting the calls again.
Worth lifting more or less verbatim if you want a starting point.
3. **The bounded-copy-with-truncation-as-error idiom, ten times.** `strncpy` followed by an
explicit NUL and a preceding length check appears at ten sites across six files. §3.1 lists
`strncpy` "with truncation reported as an error rather than silently accepted, which is the
whole value proposition here" — agreed, and the repetition is the evidence.
4. **Uppercase folding for case-insensitive lookup, three times.** Verb and function names are
case-insensitive in BASIC while variable names are not, so the port folds case before every
keyword lookup: by hand at `src/environment.c:197` and `src/parser_commands.c:113`, and
through `<ctype.h>`'s `toupper` in `src/verbs.c`. §3.1's `strcasecmp`/`strncasecmp` would
remove all three.
### 4.2 Confirmed with impact
- **§2.2.4 (`aksl_sprintf` is unbounded)** — akbasic never calls it, deliberately. Its 28
`snprintf` calls all write into fixed buffers whose size is the whole point, and there is no
`aksl_snprintf` to route them through. This is the single highest-value item in §3.1 for a
consumer of this shape.
- **§2.2.2 (`aksl_fopen` does not validate `pathname`/`mode`)** — real, and reached from user
input. akbasic's `DLOAD`/`DSAVE` take a filename from a BASIC string expression, so an empty
or unevaluated one is reachable from a running program; `akbasic_cmd_dload` validates the
name itself before handing it over, with a comment pointing here.
- **§2.2.6 (`aksl_strhash_djb2` sign-extends high bytes)** — benign for akbasic, which only
ever hashes 7-bit ASCII identifiers, and noted in its §1.3 as a hazard for anyone who later
keys a table on a filename or a string literal. Still worth fixing; the wrong answer is
platform-dependent, which is the bad kind of benign.
- **§2.1.4 (`va_end` is never called)** — akbasic's stdio text sink is built on `aksl_fprintf`
and therefore runs this UB on every line of program output. Nothing has misbehaved on
x86-64 glibc, which is exactly what makes it worth fixing before something does.
### 4.3 Not needed, and why
Recorded so the wishlist does not get prioritized purely by this consumer's shape. akbasic
uses **no** allocator (§3.1 memory), **no** lists or trees (§3.6), and **no** `aksl_realpath`:
it draws everything from fixed pools by design, and the `ak*` no-dynamic-allocation rule means
`calloc`/`realloc` would go unused here even if they existed. A consumer that does allocate
would weight §3.1's memory section far higher. The list and tree defects in §2.1.13 were
avoided rather than survived — akbasic's symbol tables are open-addressed precisely because
`aksl_list_append` truncates.

178
UPGRADING.md Normal file
View File

@@ -0,0 +1,178 @@
# Upgrading
## 0.1.0 → 0.2.0
This is an ABI break and a source break. Pre-1.0 the soname is `MAJOR.MINOR`, so
`libakstdlib.so.0.1` and `libakstdlib.so.0.2` are different libraries as far as
the loader is concerned and a consumer built against 0.1 will not silently pick
this up. Rebuild against the new header.
`AKSL_VERSION_CHECK()` catches the pairing at runtime if a stale `.so` ever does
end up on the path.
Everything below comes out of `TODO.md` sections 2.1 and 2.2 — the confirmed
defects, all of which were reproduced against the 0.1.0 library before being
fixed.
### Behaviour changes with the same signature
**`aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` report bad conversions.**
This is the one to look at first, because the compiler will not tell you about
it. They used to return success for anything: `aksl_atoi("not a number", &n)`
gave you `0`, and `aksl_atoi("99999999999999999999", &n)` gave you a wrapped
value. Now:
| input | before | after |
|---|---|---|
| `"42"` | success, 42 | success, 42 |
| `"not a number"` | **success, 0** | `AKERR_VALUE` |
| `""` or `" "` | **success, 0** | `AKERR_VALUE` |
| `"12abc"` | **success, 12** | `AKERR_VALUE` |
| `"0x10"` | **success, 0** | `AKERR_VALUE` (base 10 stops at the `x`) |
| `"99999999999999999999"` | **success, garbage** | `ERANGE` |
`*dest` is `0` on every failure path. If you were relying on the old silence —
treating an unparseable string as zero on purpose — that is now an error you
have to handle or deliberately ignore.
For `"0x10"` and friends, use `aksl_strtol(nptr, NULL, 0, &dest)`, which honours
the `0x` and `0` prefixes. The whole `aksl_strto*` family is new and is what the
`ato*` forms are built on.
**Errors no longer carry status `0`.** Any wrapper that reported `errno` could
previously raise an error whose status was whatever `errno` happened to hold,
including `0` — which every `DETECT` and `CATCH` downstream reads as *success*
while the context still holds a pool slot. `errno` is now cleared before each
wrapped call and read back through a fallback. If you were matching on a
specific status, check it is still the one you get; several calls that used to
report a flat `AKERR_IO` now report the real `errno` (a read from a write-only
stream is `EBADF`, not `AKERR_IO`).
**`aksl_malloc(0, &p)` is `AKERR_VALUE`.** It used to depend on whether the
platform's `malloc(0)` returned NULL.
**`aksl_memcpy` refuses overlapping ranges** with `AKERR_VALUE`. Use
`aksl_memmove`.
**`aksl_list_append` refuses a node already in the list** with `AKERR_VALUE`.
It used to relink it and orphan everything between its old position and the tail.
**`aksl_list_append` and `aksl_list_iterate` are correct now.** If you worked
around either defect — appending one node at a time and fixing up the links by
hand, or starting your own iteration from the head because the callback never
saw it — that workaround is now wrong. `append` truncated any list of two or
more nodes; `iterate` skipped everything before the list midpoint.
**`AKERR_ITERATOR_BREAK` stops a tree traversal.** It previously did not: the
walk ran to completion regardless. Code that raised it and relied on the
traversal continuing anyway (unlikely, but it was the behaviour) will now stop.
**Tree traversal is bounded.** A tree deeper than `AKSL_TREE_MAX_DEPTH` (256) is
`AKERR_OUTOFBOUNDS`, and a depth-first walk over a tree whose child points back
at an ancestor is `AKERR_CIRCULAR_REFERENCE`. Both used to run until the process
died.
**An unrecognised `searchmode` is `AKERR_VALUE`.** It used to return success
having visited nothing.
### Signature changes
**`aksl_sprintf` is gone.** It wrapped `vsprintf`, which cannot be bounded.
```c
/* before */
aksl_sprintf(&count, buf, "%s=%d", key, value);
/* after */
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
```
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` is
`0` on any failure rather than `vsprintf`'s `-1`.
**`aksl_realpath` takes the destination's length.**
```c
/* before */
char resolved[PATH_MAX];
aksl_realpath(path, resolved);
/* after */
char resolved[PATH_MAX];
aksl_realpath(path, resolved, sizeof(resolved));
```
`buflen` must be at least `PATH_MAX`; `realpath(3)` cannot be bounded below it,
so a shorter buffer is `AKERR_OUTOFBOUNDS` rather than an overflow. If you have
no `PATH_MAX`-sized buffer to hand, `aksl_realpath_alloc(path, &dest)` allocates
one and hands it over — release it with `aksl_free`.
**`aksl_fread` and `aksl_fwrite` take a transferred-count out-param.**
```c
/* before */
aksl_fread(buf, 1, sizeof(buf), fp);
/* after */
size_t got = 0;
aksl_fread(buf, 1, sizeof(buf), fp, &got);
```
It is required, not optional, because it is the only way a caller who gets
`AKERR_EOF` can find out how much data arrived. It is written on every path
including the failure ones. A short transfer with neither EOF nor a stream error
set is now `AKERR_IO` rather than success.
**`aksl_list_pop` takes the head by reference.**
```c
/* before */
aksl_list_pop(node);
/* after */
aksl_list_pop(&head, node); /* head is updated when node was the head */
```
Popping the head used to leave the caller's own head pointer aimed at a
now-detached node with no way to learn the new one.
**`aksl_tree_iterate` lost its `queue` parameter.**
```c
/* before */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data, NULL);
/* after */
aksl_tree_iterate(root, iter, NULL, NULL, mode, data);
```
The doc comment told callers to pass `NULL`, which is a sign the queue belonged
in an internal helper. `lalloc` and `lfree` stay, and they are actually used now
— the breadth-first modes are implemented, where before they raised
`AKERR_NOT_IMPLEMENTED`.
**`aksl_strhash_djb2` takes `const char *`** and reads bytes as unsigned. Callers
no longer have to cast away constness. The hash value **changes** for any input
containing a byte ≥ 0x80: it now matches canonical djb2 instead of depending on
whether plain `char` is signed on the target. 7-bit ASCII keys are unaffected. If
you have persisted djb2 values across a restart, they will not match.
**`aksl_fopen` takes `const char *`** for both `pathname` and `mode`, and checks
them — `fopen(NULL, ...)` is undefined behaviour and used to go straight through.
**`aksl_memcpy` takes `const void *` for its source.**
### Additions
Nothing here breaks anything; see `README.md` for the full surface.
- `aksl_calloc`, `aksl_realloc`, `aksl_freep`, `aksl_memmove`, `aksl_memcmp`,
`aksl_memchr`
- `aksl_snprintf`, `aksl_vprintf`, `aksl_vfprintf`, `aksl_vsnprintf`
- `aksl_strtol`, `aksl_strtoll`, `aksl_strtoul`, `aksl_strtoull`, `aksl_strtod`,
`aksl_strtof`, `aksl_strtold`
- the whole of `src/string.c` and `src/stream.c`
- `aksl_list_prepend` / `insert_after` / `insert_before` / `length` / `find` /
`reverse` / `concat` / `free_all` / `iterate_reverse`, and the `aksl_List`
container so append is O(1)
- `aksl_tree_insert` / `find` / `remove` / `height` / `count` / `free_all`
- `aksl_hashmap_*`, `aksl_strbuf_*`, `aksl_strhash_fnv1a`
- `aksl_list_node_init` and `aksl_tree_node_init`, so a caller no longer has to
remember to `memset` a node before its first use
- the header is `extern "C"`-guarded and pulls in four standard headers rather
than six

49
cmake/RunDoxygen.cmake Normal file
View File

@@ -0,0 +1,49 @@
# Run doxygen with PROJECT_NUMBER supplied from CMake, and fail on any warning.
#
# Driven by the `docs` target in the top-level CMakeLists.txt. It is a separate
# script rather than a COMMAND because it has to do three things in sequence --
# feed the configuration in, then read the log back, then decide -- and a
# custom-target command list cannot branch on the result of an earlier command.
#
# The version comes in from CMake rather than living in the Doxyfile so that
# project() stays the single place a version number is written.
if(NOT AKSL_DOXYGEN OR NOT AKSL_SOURCE_DIR OR NOT AKSL_VERSION)
message(FATAL_ERROR "RunDoxygen.cmake needs AKSL_DOXYGEN, AKSL_SOURCE_DIR and AKSL_VERSION")
endif()
set(_doxyfile "${AKSL_SOURCE_DIR}/Doxyfile")
set(_logfile "${AKSL_SOURCE_DIR}/doxygen-warnings.log")
file(READ "${_doxyfile}" _config)
# Later settings win in a Doxyfile, so appending is enough to override.
set(_config "${_config}\nPROJECT_NUMBER = ${AKSL_VERSION}\n")
set(_generated "${AKSL_SOURCE_DIR}/Doxyfile.generated")
file(WRITE "${_generated}" "${_config}")
execute_process(
COMMAND "${AKSL_DOXYGEN}" "${_generated}"
WORKING_DIRECTORY "${AKSL_SOURCE_DIR}"
RESULT_VARIABLE _result
OUTPUT_QUIET
)
file(REMOVE "${_generated}")
if(NOT _result EQUAL 0)
message(FATAL_ERROR "doxygen exited ${_result}")
endif()
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are on, so anything in the log is a
# public function, parameter or return value nobody has described. Treat it the
# way an uncovered line or a surviving mutant is treated: as work, not as noise.
if(EXISTS "${_logfile}")
file(READ "${_logfile}" _warnings)
string(STRIP "${_warnings}" _warnings)
if(NOT _warnings STREQUAL "")
message("${_warnings}")
message(FATAL_ERROR
"doxygen reported undocumented entities; see doxygen-warnings.log")
endif()
endif()
message(STATUS "API documentation written to ${AKSL_SOURCE_DIR}/doxygen/html")

File diff suppressed because it is too large Load Diff

View File

@@ -116,6 +116,69 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size)
SUCCEED_RETURN(e); 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, * 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 * 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; 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. * String to number.
* *
@@ -893,6 +1018,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
/** @cond INTERNAL */
/* /*
* One entry in the breadth-first traversal queue. Internal, and deliberately not * 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 * 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; aksl_TreeNode *node;
int depth; int depth;
} TreeQueueEntry; } TreeQueueEntry;
/** @endcond */
/* Append one tree node to the tail of the traversal queue. */ /* Append one tree node to the tail of the traversal queue. */
static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_push( static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_push(
@@ -1101,31 +1228,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
/** /*
* @brief Iterate a binary tree in breadth-first or depth-first order, calling a callback on each node. * Public entry point for every tree walk. The contract -- every status, every
* * argument -- is documented on the declaration in include/akstdlib.h; what is
* The callback may stop the walk early by raising AKERR_ITERATOR_BREAK; this * here is why the code looks the way it does.
* 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
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate( akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
aksl_TreeNode *root, aksl_TreeNode *root,
@@ -1198,17 +1304,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
/** /*
* @brief Iterates over a linked list and execute a callback function on each node * Contract documented on the declaration in include/akstdlib.h.
*
* @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
*/ */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data) akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data)
{ {

View File

@@ -485,6 +485,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
return raised; 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 */ /* Files */
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */

View File

@@ -170,6 +170,15 @@ static char aksl_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
/* Sized to match akerr_ErrorContext.function, which akerror declares with /* Sized to match akerr_ErrorContext.function, which akerror declares with
* AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */ * AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */
static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH]; static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
/*
* The source location the error was raised from. Captured so that
* tests/test_pool.c can assert an error came from the wrapper it names rather
* than from somewhere further down -- an error that reports the wrong origin is
* worse than useless when the only debugging you get is a log file after the
* fact, which is the stated reason this library exists.
*/
static char aksl_last_file[AKERR_MAX_ERROR_FNAME_LENGTH];
static int aksl_last_line = 0;
/* /*
* Record the status/message/function of a returned context, release it back to * Record the status/message/function of a returned context, release it back to
@@ -184,6 +193,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
aksl_last_message[0] = '\0'; aksl_last_message[0] = '\0';
aksl_last_function[0] = '\0'; aksl_last_function[0] = '\0';
aksl_last_file[0] = '\0';
aksl_last_line = 0;
if ( e == NULL ) { if ( e == NULL ) {
aksl_last_status = 0; aksl_last_status = 0;
return 0; return 0;
@@ -191,6 +202,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
aksl_last_status = e->status; aksl_last_status = e->status;
snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message); snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message);
snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function); snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function);
snprintf(aksl_last_file, sizeof(aksl_last_file), "%s", e->fname);
aksl_last_line = e->lineno;
/* /*
* akerr_release_error is marked warn_unused_result, and a (void) cast does * akerr_release_error is marked warn_unused_result, and a (void) cast does
* not silence that in GCC, so the result is assigned and discarded. * not silence that in GCC, so the result is assigned and discarded.
@@ -260,6 +273,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
aksl_last_status = 0; \ aksl_last_status = 0; \
aksl_last_message[0] = '\0'; \ aksl_last_message[0] = '\0'; \
aksl_last_function[0] = '\0'; \ aksl_last_function[0] = '\0'; \
aksl_last_file[0] = '\0'; \
aksl_last_line = 0; \
} while ( 0 ) } while ( 0 )
#define AKSL_CHECK_CONTAINS(needle) \ #define AKSL_CHECK_CONTAINS(needle) \

View File

@@ -0,0 +1,26 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5.
*
* This file must NOT compile. It is built by the CTest entry
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
*
* What it asserts: the format attributes on the variadic wrappers are attached
* and effective. Going through a wrapper is exactly how a caller loses the
* compile-time format checking it would have had calling printf(3) directly --
* printf("%d", "str") is caught and an unattributed wrapper's equivalent is not.
* AKSL_PRINTF_FORMAT is what restores it, and this is what proves it is there.
*/
#include <akstdlib.h>
int main(void)
{
char buf[64];
int count = 0;
akerr_ErrorContext *raised = NULL;
/* %d against a string. This is the line that must not build. */
raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int");
return raised == NULL ? 0 : 1;
}

26
tests/negative/noignore.c Normal file
View File

@@ -0,0 +1,26 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md section 1.9.
*
* This file must NOT compile. It is built by the CTest entry `negative_noignore`
* with -Werror, and that test is marked WILL_FAIL, so a successful build is a
* test failure.
*
* What it asserts: AKERR_NOIGNORE is actually attached and actually effective.
* Every entry point in this library returns an error context the caller must
* look at, and the whole design rests on discarding one being hard rather than
* merely inadvisable. AKERR_NOIGNORE expands to warn_unused_result, which does
* nothing at all if it is dropped from a declaration during a refactor -- and
* nothing about the resulting library would look wrong.
*/
#include <akstdlib.h>
int main(void)
{
void *ptr = NULL;
/* Discarding the returned context. This is the line that must not build. */
aksl_malloc(32, &ptr);
return 0;
}

View File

@@ -149,6 +149,29 @@ static int test_insert_after_and_before(void)
return 0; return 0;
} }
/*
* Inserting before a node that is *not* the head, which relinks the node in
* front of it as well -- a different path from inserting before the head, where
* there is no such node.
*/
static int test_insert_before_a_middle_node(void)
{
aksl_ListNode node[3];
aksl_ListNode fresh;
aksl_ListNode *head = &node[0];
build_chain(node, 3);
memset((void *)&fresh, 0x00, sizeof(fresh));
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[2], &fresh));
AKSL_CHECK(head == &node[0]); /* unchanged, unlike the head case */
AKSL_CHECK(node[1].next == &fresh);
AKSL_CHECK(fresh.prev == &node[1]);
AKSL_CHECK(fresh.next == &node[2]);
AKSL_CHECK(node[2].prev == &fresh);
return 0;
}
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
/* Inspection */ /* Inspection */
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
@@ -177,6 +200,7 @@ static int test_find_returns_the_first_match_or_null(void)
{ {
aksl_ListNode node[N]; aksl_ListNode node[N];
aksl_ListNode *found = (aksl_ListNode *)0x1; aksl_ListNode *found = (aksl_ListNode *)0x1;
aksl_ListNode *cyclic = NULL;
int payload = 42; int payload = 42;
int absent = 0; int absent = 0;
@@ -199,6 +223,19 @@ static int test_find_returns_the_first_match_or_null(void)
aksl_list_find(&node[0], &failing_predicate, NULL, &found), aksl_list_find(&node[0], &failing_predicate, NULL, &found),
AKERR_VALUE, "predicate refused"); AKERR_VALUE, "predicate refused");
/*
* A cyclic list is refused before the walk starts rather than searched
* forever. Every whole-list function shares one bounded tail walk, so this
* covers the bound for find, reverse, concat and free_all alike.
*/
node[N - 1].next = &node[0];
cyclic = &node[0];
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, &payload, &found),
AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK_STATUS(aksl_list_reverse(&cyclic), AKERR_CIRCULAR_REFERENCE);
AKSL_CHECK_STATUS(aksl_list_concat(&node[0], &node[2]), AKERR_CIRCULAR_REFERENCE);
node[N - 1].next = NULL;
AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER);
return 0; return 0;
@@ -336,6 +373,257 @@ static int test_free_all_releases_every_node(void)
return 0; return 0;
} }
/*
* A free function that fails on one node in the middle.
*
* Both *_free_all functions go out of their way to keep the first error and
* carry on rather than returning immediately, because abandoning the walk would
* leak everything after the node that failed -- turning one bad free into a leak
* of the whole remaining structure. This is the test that says so.
*/
/* Defined with the tree tests below; declared here because the tree free-all
* case belongs beside the list one rather than beside its comparator. */
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest);
static int failing_free_countdown = 0;
static int failing_free_calls = 0;
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
{
PREPARE_ERROR(e);
failing_free_calls += 1;
if ( failing_free_calls == failing_free_countdown ) {
/* Refuse, but still release the memory: the point is the error path,
* not a deliberate leak for the sanitizer to find. */
free(ptr);
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
}
free(ptr);
SUCCEED_RETURN(e);
}
/*
* And one that refuses every time, so more than one error is raised during a
* single walk. Only the first is kept and handed back; the rest have to be
* released, or a walk over n nodes with a broken free would consume n pool slots
* and exhaust the pool -- the failure mode the whole pool-accounting section of
* TODO.md 1.9 exists to catch.
*/
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
{
PREPARE_ERROR(e);
failing_free_calls += 1;
free(ptr);
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
}
/*
* And one that refuses from a given call onwards, which is what it takes to fail
* repeatedly *inside* a drain: the dequeues that come first have to succeed, or
* the walk stops with a real error before it ever reaches the break.
*/
static int failing_free_from = 0;
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_from(void *ptr)
{
PREPARE_ERROR(e);
failing_free_calls += 1;
free(ptr);
if ( failing_free_calls >= failing_free_from ) {
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
}
SUCCEED_RETURN(e);
}
static int test_free_all_releases_the_errors_it_does_not_return(void)
{
aksl_ListNode *head = NULL;
aksl_ListNode *node = NULL;
aksl_TreeNode *root = NULL;
aksl_TreeNode *tnode = NULL;
static int values[N] = { 5, 3, 8, 1, 9 };
int i = 0;
for ( i = 0; i < N; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
if ( head == NULL ) {
head = node;
} else {
AKSL_CHECK_OK(aksl_list_append(head, node));
}
}
failing_free_calls = 0;
/* The first of N errors comes back; the other N-1 must not leak. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_always_fails),
AKERR_VALUE, "free refused on call 1");
AKSL_CHECK(failing_free_calls == N);
AKSL_CHECK(aksl_slots_in_use() == 0);
for ( i = 0; i < N; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&tnode));
AKSL_CHECK_OK(aksl_tree_node_init(tnode, &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, tnode, &compare_ints));
}
failing_free_calls = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_always_fails),
AKERR_VALUE, "free refused on call 1");
AKSL_CHECK(failing_free_calls == N);
AKSL_CHECK(aksl_slots_in_use() == 0);
return 0;
}
static int test_list_free_all_reports_the_first_failure_but_frees_everything(void)
{
aksl_ListNode *head = NULL;
aksl_ListNode *node = NULL;
int i = 0;
for ( i = 0; i < N; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
if ( head == NULL ) {
head = node;
} else {
AKSL_CHECK_OK(aksl_list_append(head, node));
}
}
failing_free_calls = 0;
failing_free_countdown = 2; /* fail on the second of five */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_fails_once),
AKERR_VALUE, "free refused on call 2");
/* Every node was still visited: the walk did not stop at the failure. */
AKSL_CHECK(failing_free_calls == N);
AKSL_CHECK(head == NULL);
return 0;
}
static int test_tree_free_all_reports_the_first_failure_but_frees_everything(void)
{
static int values[5] = { 5, 3, 8, 1, 9 };
aksl_TreeNode *root = NULL;
aksl_TreeNode *node = NULL;
int i = 0;
for ( i = 0; i < 5; i++ ) {
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node));
AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints));
}
failing_free_calls = 0;
failing_free_countdown = 2;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_fails_once),
AKERR_VALUE, "free refused on call 2");
AKSL_CHECK(failing_free_calls == 5);
AKSL_CHECK(root == NULL);
return 0;
}
/*
* The same idea for the breadth-first traversal queue: a failing lfree during
* the drain must not stop the drain, and must not mask the error that got the
* walk there in the first place. The traversal itself still succeeds -- the
* drain failure is logged and dropped, because losing the caller's real error to
* report a bookkeeping one would be the worse trade.
*/
static aksl_TreeNode *break_on_node = NULL;
static akerr_ErrorContext AKERR_NOIGNORE *break_at(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)data;
if ( node == break_on_node ) {
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop here");
}
SUCCEED_RETURN(e);
}
static akerr_ErrorContext AKERR_NOIGNORE *plain_alloc(size_t size, void **dest)
{
return aksl_malloc(size, dest);
}
static int test_bfs_queue_drain_survives_a_failing_free(void)
{
aksl_TreeNode tree[3];
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
/*
* The lfree calls, in order:
* 1 the root's queue entry, released as it is dequeued
* 2 the left child's entry, likewise
* -- the callback breaks on the left child, leaving the right child queued
* 3 the right child's entry, released by the drain in CLEANUP
*
* Failing on call 3 is therefore a failure inside the drain. The traversal
* still succeeds: the break is not an error, and losing that answer in order
* to report a bookkeeping failure would be the worse trade -- so the drain
* error is logged and dropped, which is what this asserts.
*/
break_on_node = &tree[1];
failing_free_calls = 0;
failing_free_countdown = 3;
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
&plain_alloc, &free_that_fails_once,
AKSL_TREE_SEARCH_BFS, NULL));
AKSL_CHECK(failing_free_calls == 3);
/* And the pool is intact afterwards: the dropped context was released. */
AKSL_CHECK(aksl_slots_in_use() == 0);
return 0;
}
/*
* More than one failure inside a single drain, which is the case where the drain
* has to release the errors it is not keeping. A seven-node tree broken on the
* third visit leaves two entries queued:
*
* dequeue 0 (free 1), enqueue 1 and 2
* dequeue 1 (free 2), enqueue 3 and 4 queue: 2 3 4
* dequeue 2 (free 3), callback breaks queue: 3 4
* drain frees 3 and 4 (free 4, free 5)
*
* With a free that refuses every time, the drain raises twice and must return at
* most one context to be dropped -- keeping both would leak a pool slot per
* queued node, which over a large tree exhausts the pool outright.
*/
static int test_bfs_queue_drain_releases_repeated_failures(void)
{
aksl_TreeNode tree[7];
int i = 0;
for ( i = 0; i < 7; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
tree[1].left = &tree[3];
tree[1].right = &tree[4];
tree[2].left = &tree[5];
tree[2].right = &tree[6];
break_on_node = &tree[2];
failing_free_calls = 0;
failing_free_from = 4; /* the first drain call */
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
&plain_alloc, &free_that_fails_from,
AKSL_TREE_SEARCH_BFS, NULL));
/* Three dequeues plus two drained entries. */
AKSL_CHECK(failing_free_calls == 5);
AKSL_CHECK(aksl_slots_in_use() == 0);
return 0;
}
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
/* The tracked container */ /* The tracked container */
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
@@ -619,6 +907,99 @@ static int test_tree_remove_all_three_cases(void)
return 0; return 0;
} }
/*
* The mirror images of the cases above: a node that is its parent's *right*
* child, and a node whose only child is on the left. Both take different
* branches through tree_replace, and neither is reached by the test above.
*/
static int test_tree_remove_mirrored_shapes(void)
{
static int values[4] = { 5, 8, 7, 6 };
aksl_TreeNode node[4];
aksl_TreeNode *root = NULL;
aksl_TreeNode *found = NULL;
int missing = 8;
size_t count = 0;
int i = 0;
/*
* 5 node[0]
* \
* 8 node[1], a right child
* /
* 7 node[2], whose only child is on the left
* /
* 6 node[3]
*/
for ( i = 0; i < 4; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK(node[0].right == &node[1]);
AKSL_CHECK(node[1].left == &node[2]);
AKSL_CHECK(node[2].left == &node[3]);
/* A right child with a single left child underneath it. */
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 3);
AKSL_CHECK_OK(aksl_tree_find(root, &missing, &compare_ints, &found));
AKSL_CHECK(found == NULL);
/* 7 took its place as the root's right child. */
AKSL_CHECK(node[0].right == &node[2]);
AKSL_CHECK(node[2].parent == &node[0]);
/* And now a node whose only child is on the left. */
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[2]));
AKSL_CHECK_OK(aksl_tree_count(root, &count));
AKSL_CHECK(count == 2);
AKSL_CHECK(node[0].right == &node[3]);
AKSL_CHECK(node[3].parent == &node[0]);
return 0;
}
/*
* The two-children case where the successor is not the removed node's own right
* child, so the successor has to be lifted out of its position first. The other
* removal test happens to hit the adjacent-successor path.
*/
static int test_tree_remove_with_a_distant_successor(void)
{
static int values[5] = { 5, 3, 9, 7, 8 };
aksl_TreeNode node[5];
aksl_TreeNode *root = NULL;
OrderLog log;
int i = 0;
/*
* 5 node[0]
* / \
* 3 9 node[1], node[2]
* /
* 7 node[3] -- the in-order successor of 5, two levels down
* \
* 8 node[4]
*/
for ( i = 0; i < 5; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
}
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0]));
/* 7 becomes the root, and its own right child (8) is not lost. */
AKSL_CHECK(root == &node[3]);
AKSL_CHECK(root->parent == NULL);
memset((void *)&log, 0x00, sizeof(log));
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
AKSL_TREE_SEARCH_DFS_INORDER, &log));
AKSL_CHECK(log.count == 4);
for ( i = 1; i < log.count; i++ ) {
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
}
return 0;
}
/* Removing the last node empties the tree rather than leaving a dangling root. */ /* Removing the last node empties the tree rather than leaving a dangling root. */
static int test_tree_remove_the_only_node(void) static int test_tree_remove_the_only_node(void)
{ {
@@ -707,12 +1088,18 @@ int main(void)
AKSL_RUN(failures, test_prepend_moves_the_head); AKSL_RUN(failures, test_prepend_moves_the_head);
AKSL_RUN(failures, test_insert_after_and_before); AKSL_RUN(failures, test_insert_after_and_before);
AKSL_RUN(failures, test_insert_before_a_middle_node);
AKSL_RUN(failures, test_length_counts_and_refuses_cycles); AKSL_RUN(failures, test_length_counts_and_refuses_cycles);
AKSL_RUN(failures, test_find_returns_the_first_match_or_null); AKSL_RUN(failures, test_find_returns_the_first_match_or_null);
AKSL_RUN(failures, test_reverse_flips_both_directions); AKSL_RUN(failures, test_reverse_flips_both_directions);
AKSL_RUN(failures, test_concat_joins_two_lists); AKSL_RUN(failures, test_concat_joins_two_lists);
AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head); AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head);
AKSL_RUN(failures, test_free_all_releases_every_node); AKSL_RUN(failures, test_free_all_releases_every_node);
AKSL_RUN(failures, test_free_all_releases_the_errors_it_does_not_return);
AKSL_RUN(failures, test_list_free_all_reports_the_first_failure_but_frees_everything);
AKSL_RUN(failures, test_tree_free_all_reports_the_first_failure_but_frees_everything);
AKSL_RUN(failures, test_bfs_queue_drain_survives_a_failing_free);
AKSL_RUN(failures, test_bfs_queue_drain_releases_repeated_failures);
AKSL_RUN(failures, test_container_push_and_unshift); AKSL_RUN(failures, test_container_push_and_unshift);
AKSL_RUN(failures, test_container_remove_maintains_the_endpoints); AKSL_RUN(failures, test_container_remove_maintains_the_endpoints);
@@ -722,6 +1109,8 @@ int main(void)
AKSL_RUN(failures, test_tree_insert_sets_the_parent_links); AKSL_RUN(failures, test_tree_insert_sets_the_parent_links);
AKSL_RUN(failures, test_tree_find); AKSL_RUN(failures, test_tree_find);
AKSL_RUN(failures, test_tree_remove_all_three_cases); AKSL_RUN(failures, test_tree_remove_all_three_cases);
AKSL_RUN(failures, test_tree_remove_mirrored_shapes);
AKSL_RUN(failures, test_tree_remove_with_a_distant_successor);
AKSL_RUN(failures, test_tree_remove_the_only_node); AKSL_RUN(failures, test_tree_remove_the_only_node);
AKSL_RUN(failures, test_tree_height_and_count); AKSL_RUN(failures, test_tree_height_and_count);
AKSL_RUN(failures, test_tree_free_all); AKSL_RUN(failures, test_tree_free_all);

View File

@@ -224,6 +224,51 @@ static int test_printf_rejects_null_arguments(void)
return 0; return 0;
} }
/*
* The allocating form. No fixed buffer means no truncation case, which is what
* makes it the right answer when the length is not knowable in advance and the
* result is too short-lived to want a whole aksl_StrBuf.
*/
static int test_asprintf_allocates_to_fit(void)
{
char *out = NULL;
int count = -1;
int i = 0;
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s=%d", "key", 42));
AKSL_CHECK(out != NULL);
AKSL_CHECK(strcmp(out, "key=42") == 0);
AKSL_CHECK(count == 6);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Far longer than any buffer a fixed-size wrapper would have used. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%01000d", 7));
AKSL_CHECK(count == 1000);
AKSL_CHECK(strlen(out) == 1000);
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* An empty result is a valid empty string, not NULL. */
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK(out != NULL && out[0] == '\0');
AKSL_CHECK_OK(aksl_freep((void **)&out));
/* Repeatedly, so a leak shows up under the sanitizer build. */
for ( i = 0; i < 256; i++ ) {
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "iteration %d of %d", i, 256));
AKSL_CHECK((size_t)count == strlen(out));
AKSL_CHECK_OK(aksl_freep((void **)&out));
}
out = (char *)0x1;
AKSL_CHECK_STATUS(aksl_asprintf(NULL, &out, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_asprintf(&count, &out, NULL), AKERR_NULLPOINTER);
/* Cleared before the format is even looked at. */
AKSL_CHECK(out == NULL);
return 0;
}
/* /*
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1 * The va_list forms are what the variadic ones are built on, and TODO.md 3.1
* wanted them exposed so consumers can write their own variadic wrappers. This * wanted them exposed so consumers can write their own variadic wrappers. This
@@ -298,6 +343,7 @@ int main(void)
AKSL_RUN(failures, test_printf_writes_to_stdout); AKSL_RUN(failures, test_printf_writes_to_stdout);
AKSL_RUN(failures, test_printf_rejects_null_arguments); AKSL_RUN(failures, test_printf_rejects_null_arguments);
AKSL_RUN(failures, test_asprintf_allocates_to_fit);
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside); AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls); AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);

View File

@@ -121,6 +121,84 @@ static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
return 0; return 0;
} }
/*
* reallocarray's whole reason for existing: `realloc(p, n * size)` is a heap
* overflow waiting for an n large enough to wrap, and the multiplication is the
* place a caller does not think to look.
*/
static int test_reallocarray_checks_the_multiplication(void)
{
void *ptr = NULL;
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(4, &ptr));
AKSL_CHECK_OK(aksl_reallocarray(&ptr, 16, 8));
buf = (unsigned char *)ptr;
for ( i = 0; i < 128; i++ ) {
buf[i] = (unsigned char)i;
}
AKSL_CHECK_OK(aksl_free(ptr));
ptr = NULL;
/* SIZE_MAX/4 members of 8 bytes wraps; without the check this would be a
* plausible-looking small allocation followed by a very large write. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_reallocarray(&ptr, SIZE_MAX / 4, 8),
AKERR_OUTOFBOUNDS, "overflows size_t");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_reallocarray(NULL, 1, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 0, 1), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 1, 0), AKERR_VALUE);
return 0;
}
/*
* aligned_alloc(3) requires size to be a multiple of the alignment and the
* alignment to be a power of two. Violating either is undefined behaviour that
* usually just returns NULL, so both are checked and reported as what they are.
*/
static int test_aligned_alloc_checks_its_preconditions(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_aligned_alloc(64, 128, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % 64) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(24, 48, &ptr),
AKERR_VALUE, "not a power of two");
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(64, 100, &ptr),
AKERR_VALUE, "not a multiple of alignment");
AKSL_CHECK_STATUS(aksl_aligned_alloc(0, 64, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 128, NULL), AKERR_NULLPOINTER);
return 0;
}
/*
* posix_memalign(3) breaks the errno convention: it returns the error number and
* leaves errno alone. The wrapper reports it as a status like everything else.
*/
static int test_posix_memalign(void)
{
void *ptr = (void *)0x1;
AKSL_CHECK_OK(aksl_posix_memalign(&ptr, sizeof(void *) * 2, 100));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK(((uintptr_t)ptr % (sizeof(void *) * 2)) == 0);
AKSL_CHECK_OK(aksl_free(ptr));
/* Not a multiple of sizeof(void *), which posix_memalign refuses. */
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 3, 100), EINVAL);
AKSL_CHECK(ptr == NULL);
AKSL_CHECK_STATUS(aksl_posix_memalign(NULL, 16, 100), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 16, 0), AKERR_VALUE);
return 0;
}
static int test_free_rejects_null_pointer(void) static int test_free_rejects_null_pointer(void)
{ {
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL), AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
@@ -324,6 +402,9 @@ int main(void)
AKSL_RUN(failures, test_malloc_free_round_trip); AKSL_RUN(failures, test_malloc_free_round_trip);
AKSL_RUN(failures, test_calloc_zeroes_and_guards); AKSL_RUN(failures, test_calloc_zeroes_and_guards);
AKSL_RUN(failures, test_reallocarray_checks_the_multiplication);
AKSL_RUN(failures, test_aligned_alloc_checks_its_preconditions);
AKSL_RUN(failures, test_posix_memalign);
AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure); AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure);
AKSL_RUN(failures, test_free_rejects_null_pointer); AKSL_RUN(failures, test_free_rejects_null_pointer);

410
tests/test_pool.c Normal file
View File

@@ -0,0 +1,410 @@
/*
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
*
* Two of them, and neither is about what any individual function computes:
*
* Error-pool accounting. libakerror hands out error contexts from a fixed
* process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises
* an error and forgets to release it does not fail visibly -- it fails
* AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask
* for a slot next, by which time the leak is nowhere near the symptom. So
* every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is
* past exhaustion several times over, with the pool checked after each round.
*
* Stack-trace content. An error that reports the wrong origin is worse than
* useless when the only debugging you get is a log file after the fact, which
* is the stated reason this library exists at all. Each assertion here names
* the function the error must say it came from and the source file it must
* name, so a FAIL that migrates into a helper is caught.
*
* AKSL_RUN already checks the pool after every test in every file. This one
* checks it inside the loop as well, because a leak of one slot per call needs
* more than one call to show up.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
/* Rounds enough to exhaust the pool several times over. */
#define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10)
/*
* Assert the last error came from `func` in a file whose name ends in `file`.
* The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is
* an absolute path under CMake, so this matches on the tail rather than the
* whole thing.
*/
static int came_from(const char *func, const char *file)
{
size_t flen = strlen(file);
size_t rlen = strlen(aksl_last_file);
if ( strcmp(aksl_last_function, func) != 0 ) {
fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n",
aksl_last_function, func);
return 1;
}
if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) {
fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n",
aksl_last_file, file);
return 1;
}
if ( aksl_last_line <= 0 ) {
fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line);
return 1;
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Pool accounting */
/* ---------------------------------------------------------------------- */
static int test_memory_wrappers_do_not_leak_slots(void)
{
void *ptr = NULL;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_stream_wrappers_do_not_leak_slots(void)
{
FILE *fp = NULL;
size_t moved = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT);
AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_format_wrappers_do_not_leak_slots(void)
{
char buf[16];
int count = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_conversion_wrappers_do_not_leak_slots(void)
{
int iout = 0;
long lout = 0;
unsigned long ulout = 0;
double dout = 0.0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE);
AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_string_wrappers_do_not_leak_slots(void)
{
char buf[8];
char *at = NULL;
size_t n = 0;
int r = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_collection_wrappers_do_not_leak_slots(void)
{
aksl_ListNode node;
aksl_ListNode *head = NULL;
aksl_List list;
aksl_TreeNode tree;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
size_t n = 0;
int i = 0;
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
(void)n;
return 0;
}
/*
* A traversal that fails part-way through has the most opportunities to leak: an
* error is raised inside a callback, propagated up through several frames, and
* handled or not at the top. Drive that repeatedly too.
*/
static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_VALUE, "callback always fails");
}
static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks");
}
static int test_traversal_failures_do_not_leak_slots(void)
{
aksl_TreeNode tree[3];
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
for ( i = 0; i < ROUNDS; i++ ) {
/* Propagated out of the recursion. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_VALUE);
/* Swallowed at the top -- the released-not-returned path. */
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL));
/* And through the breadth-first walk, which also drains a queue. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL),
AKERR_VALUE);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL));
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Stack-trace content */
/* ---------------------------------------------------------------------- */
/*
* Each of these asserts that the error names the function that raised it and
* the file that function lives in. This is what catches a FAIL that gets moved
* into a shared helper during a refactor: the status stays right, the message
* stays right, and the origin quietly starts pointing at the wrong place.
*/
static int test_errors_name_their_origin_in_stdlib(void)
{
void *ptr = NULL;
int count = 0;
char resolved[PATH_MAX];
uint32_t h = 0;
aksl_ListNode node;
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0);
/*
* aksl_printf forwards to aksl_vprintf, so the error legitimately comes
* from the latter -- the variadic form is a three-line wrapper with no FAIL
* of its own. Asserting the real origin rather than the one a reader might
* expect is the point of recording it.
*/
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
/* Likewise, the ato* forms are calls into the strto* ones. */
AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0);
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_string_and_stream(void)
{
char buf[8];
size_t n = 0;
long pos = 0;
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_collections(void)
{
aksl_ListNode node;
aksl_List list;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
char longkey[AKSL_HASHMAP_MAX_KEY + 4];
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
memset(longkey, 'k', sizeof(longkey) - 1);
longkey[sizeof(longkey) - 1] = '\0';
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots);
AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib);
AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream);
AKSL_RUN(failures, test_errors_name_their_origin_in_collections);
AKSL_REPORT(failures);
}

View File

@@ -520,6 +520,53 @@ static int test_fscanf_reads_from_a_stream(void)
return 0; return 0;
} }
/*
* aksl_scanf reads stdin, so stdin is pointed at a temp file for the duration of
* the call and restored through a dup of the original descriptor -- the same
* dance tests/test_format.c does for aksl_printf and stdout, and for the same
* reason: nothing between the freopen and the dup2 may return early.
*/
static int test_scanf_reads_stdin(void)
{
char path[AKSL_TMP_MAX];
FILE *fp = NULL;
akerr_ErrorContext *err = NULL;
int a = 0;
int b = 0;
int assigned = 0;
int saved = -1;
int restored = -1;
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fp = fopen(path, "w");
AKSL_CHECK(fp != NULL);
AKSL_CHECK(fputs("11 22\n", fp) != EOF);
AKSL_CHECK(fclose(fp) == 0);
saved = dup(fileno(stdin));
AKSL_CHECK(saved >= 0);
if ( freopen(path, "r", stdin) == NULL ) {
close(saved);
AKSL_CHECK(0);
}
err = aksl_scanf("%d %d", 2, &assigned, &a, &b);
restored = dup2(saved, fileno(stdin));
close(saved);
clearerr(stdin);
AKSL_CHECK(restored >= 0);
AKSL_CHECK(aksl_take(err) == 0);
AKSL_CHECK(assigned == 2);
AKSL_CHECK(a == 11 && b == 22);
AKSL_CHECK(unlink(path) == 0);
AKSL_CHECK_STATUS(aksl_scanf(NULL, 1, &assigned), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_scanf("%d", 1, NULL, &a), AKERR_NULLPOINTER);
return 0;
}
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
/* Files */ /* Files */
/* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */
@@ -603,6 +650,8 @@ int main(void)
AKSL_RUN(failures, test_sscanf_enforces_the_expected_count); AKSL_RUN(failures, test_sscanf_enforces_the_expected_count);
AKSL_RUN(failures, test_fscanf_reads_from_a_stream); AKSL_RUN(failures, test_fscanf_reads_from_a_stream);
AKSL_RUN(failures, test_scanf_reads_stdin);
AKSL_RUN(failures, test_remove_and_rename); AKSL_RUN(failures, test_remove_and_rename);
AKSL_RUN(failures, test_mkstemp_and_mkdtemp); AKSL_RUN(failures, test_mkstemp_and_mkdtemp);

View File

@@ -111,6 +111,53 @@ static int test_no_digits_is_an_error_even_with_an_endptr(void)
return 0; return 0;
} }
/*
* Every form writes through endptr, not just the two the other tests happen to
* use. Each type has its own function body, so "strtol handles endptr" says
* nothing whatsoever about strtoull.
*/
static int test_every_form_writes_the_endptr(void)
{
const char *input = "42rest";
char *end = NULL;
long lout = 0;
long long llout = 0;
unsigned long ulout = 0;
unsigned long long ullout = 0;
double dout = 0.0;
float fout = 0.0f;
long double ldout = 0.0L;
end = NULL;
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &lout));
AKSL_CHECK(lout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoll(input, &end, 10, &llout));
AKSL_CHECK(llout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoul(input, &end, 10, &ulout));
AKSL_CHECK(ulout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtoull(input, &end, 10, &ullout));
AKSL_CHECK(ullout == 42 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtod(input, &end, &dout));
AKSL_CHECK(dout == 42.0 && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtof(input, &end, &fout));
AKSL_CHECK(fout == 42.0f && strcmp(end, "rest") == 0);
end = NULL;
AKSL_CHECK_OK(aksl_strtold(input, &end, &ldout));
AKSL_CHECK(ldout == 42.0L && strcmp(end, "rest") == 0);
return 0;
}
/* Successive calls chained through endptr walk a whole list of numbers. */ /* Successive calls chained through endptr walk a whole list of numbers. */
static int test_endptr_chains_across_a_list(void) static int test_endptr_chains_across_a_list(void)
{ {
@@ -277,6 +324,7 @@ int main(void)
AKSL_RUN(failures, test_invalid_base_is_rejected); AKSL_RUN(failures, test_invalid_base_is_rejected);
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped); AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
AKSL_RUN(failures, test_every_form_writes_the_endptr);
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr); AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
AKSL_RUN(failures, test_endptr_chains_across_a_list); AKSL_RUN(failures, test_endptr_chains_across_a_list);