diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 664717b..df717a9 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -17,20 +17,48 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y cmake gcc moreutils - # Depends on libakerror@main - git clone https://source.starfort.tech/andrew/libakerror.git - cd libakerror - cmake -S . -B build - cmake --build build - cmake --install build + # This step used to clone and install libakerror@main as well. That was two + # different libakerrors depending on where you built: the install went to + # /usr/local, while the build below is top-level and so compiles + # deps/libakerror at the pinned commit via add_subdirectory -- the + # installed one was never actually linked against. TODO.md 2.3. + # + # 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 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 sudo cmake --install build ctest --test-dir build --output-on-failure - 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: runs-on: ubuntu-latest steps: @@ -45,28 +73,27 @@ jobs: # scripts/coverage.py needs nothing but python3 and gcc's own gcov, so # 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 - # and 45.1% of branches, so 90/40 fails on a real regression (a test - # deleted, or new untested code added) without tripping over rounding. - # The report is printed either way -- the uncovered lines it lists are the + # The gate is a ratchet, not a target. Across all four sources the suite + # covers 99.5% of lines (1643/1651), 46.0% of branches and 100% of + # functions (147/147), so 90/40 fails on a real regression -- a test + # 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. # - # Branch coverage is back over 45 (the aksl_version_* functions are fully - # covered, which lifted it from 44.3%), but the gate stays at 40: 0.1 - # points of headroom is not a ratchet, it is a coin flip on the next - # rounding change. + # Eight lines are uncovered and every one is deliberate: four + # `HANDLE(e, AKERR_ITERATOR_BREAK)` macro artifacts, the size_t overflow + # guard in strbuf_reserve (reaching it needs a buffer near SIZE_MAX), and + # 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. - # Nothing about this library's tests changed: line coverage held at - # 99.0% (200/202) and function coverage at 100% (21/21), but 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 in - # src/stdlib.c, and most of the added branches are not reachable from the - # way this library calls them. 337/661 became 481/1087 -- 144 more - # branches covered, 426 more branches counted. Chasing them here would be - # 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. + # Branch coverage sits far below line coverage because most branches are + # inside libakerror's FAIL_*/ATTEMPT/FINISH expansions -- pool exhaustion, + # stack-trace limits, akerr_valid_error_address failures -- which this + # library has no way to reach. Chasing them here would be 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. The gate stays at 40 for that reason and not for want of + # tests. - name: coverage run: | cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ diff --git a/.githooks/pre-push b/.githooks/pre-push index e71e5ad..81e9922 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -9,8 +9,9 @@ # # Runs by default, a few seconds all in: # -# * default build + ctest +# * default build + ctest (with -Werror, as CI has it) # * 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 # suite once per mutant): @@ -72,22 +73,40 @@ run() { 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" -run cmake -S . -B "$builddir/default" +run cmake -S . -B "$builddir/default" -DAKSL_WERROR=ON run cmake --build "$builddir/default" run ctest --test-dir "$builddir/default" --output-on-failure 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 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 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 # to watch it make progress. if ! python3 scripts/mutation_test.py \ --target src/stdlib.c \ + --target src/string.c \ + --target src/stream.c \ + --target src/collections.c \ --threshold "$MUTATION_THRESHOLD"; then echo >&2 echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2 diff --git a/.gitignore b/.gitignore index 11723fd..8692568 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,9 @@ coverage.xml *.swp .DS_Store 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 diff --git a/AGENTS.md b/AGENTS.md index bf8bee1..f67ec57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,27 @@ ## Project Structure & Module Organization -`libakstdlib` is a C shared library that wraps libc calls and small data -structures in `libakerror` error contexts. Public API declarations live in -`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are -one-file CTest executables under `tests/test_.c`, with shared test helpers -in `tests/aksl_capture.h`. 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/`. +`libakstdlib` is a C shared library that wraps libc calls and data structures in +`libakerror` error contexts. + +Public API declarations live in `include/akstdlib.h`, which is also where the +Doxygen documentation lives -- the header is the contract, the sources carry the +reasoning. The implementation is split by domain: + +| 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_.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 @@ -40,8 +53,15 @@ cmake --build build-coverage --target coverage ``` Run `cmake --build build --target mutation` only when you need the slower -mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the -local build directory, so treat it as a local convenience script. +mutation harness, and `cmake --build build --target docs` to regenerate the API +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 @@ -51,16 +71,45 @@ prefix, structs use `aksl_`, and tests use `test_.c` plus static `test_` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *` 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 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. `AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests. `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 -(branch), so new code needs tests in the same commit. Run +`tests/negative/` holds sources that must **fail** to compile. Each is an +`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 listing before proposing a change. Tests for behaviour that `TODO.md` records as defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract β€” diff --git a/CMakeLists.txt b/CMakeLists.txt index cdd9e0b..a114500 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,20 @@ cmake_minimum_required(VERSION 3.10) # shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and # akstdlibConfigVersion.cmake. Nothing else should spell a version number. # -# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose -# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's -# signature), so this library is not promising a stable API yet. -project(akstdlib VERSION 0.1.0 LANGUAGES C) +# 0.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed +# defects in TODO.md 2.1 changed documented behaviour and, in five places, +# signatures: +# +# 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 # 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 memory path + pool status_registry strbuf 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}) 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 # 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 @@ -445,6 +489,40 @@ elseif(AKSL_COVERAGE) "(scripts/coverage.py) will not be wired into CTest") 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 # 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 diff --git a/Doxyfile b/Doxyfile index e0ca0c0..685697e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -42,9 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "My Project" - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +PROJECT_NAME = "libakstdlib" # could be handy for archiving the generated documentation or if some version # 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 # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = - -# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +PROJECT_BRIEF = "libc wrappers that report failures through libakerror error contexts" # 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 # 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 # left blank the current directory will be used. -OUTPUT_DIRECTORY = - -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +OUTPUT_DIRECTORY = doxygen # sub-directories (in 2 levels) under the output directory of each output format # 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 @@ -157,9 +151,7 @@ ABBREVIATE_BRIEF = "The $name class" \ # description. # The default value is: NO. -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +ALWAYS_DETAILED_SEC = YES # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. @@ -209,9 +201,7 @@ SHORT_NAMES = NO # description.) # The default value is: NO. -JAVADOC_AUTOBRIEF = NO - -# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +JAVADOC_AUTOBRIEF = YES # such as # /*************** # 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. # The default value is: NO. -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +OPTIMIZE_OUTPUT_FOR_C = YES # Python sources only. Doxygen will then generate output that is more tailored # for that language. For instance, namespaces will be presented as packages, # qualified scopes will look different, etc. @@ -519,8 +507,6 @@ TIMESTAMP = NO # The default value is: 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. # The default value is: NO. @@ -543,8 +529,6 @@ EXTRACT_PACKAGE = NO # The default value is: 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, # only classes defined in header files are included. Does not have any effect # for Java sources. @@ -681,9 +665,7 @@ INLINE_INFO = YES # name. If set to NO, the members will appear in declaration order. # The default value is: YES. -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +SORT_MEMBER_DOCS = NO # descriptions of file, namespace and class members alphabetically by member # 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. @@ -853,8 +835,6 @@ WARNINGS = YES # The default value is: 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 # a documented function twice, or documenting parameters that don't exist or # using markup commands wrongly. @@ -877,9 +857,7 @@ WARN_IF_INCOMPLETE_DOC = YES # WARN_IF_INCOMPLETE_DOC # The default value is: NO. -WARN_NO_PARAMDOC = NO - -# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about +WARN_NO_PARAMDOC = YES # undocumented enumeration values. If set to NO, doxygen will accept # undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag # 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 # (stdout). -WARN_LOGFILE = - -#--------------------------------------------------------------------------- +WARN_LOGFILE = doxygen-warnings.log # Configuration options related to the input files #--------------------------------------------------------------------------- @@ -943,9 +919,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = - -# This tag can be used to specify the character encoding of the source files +INPUT = include src # 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 # documentation (see: @@ -1038,18 +1012,14 @@ FILE_PATTERNS = *.c \ # be searched for input files as well. # The default value is: NO. -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should be +RECURSIVE = NO # 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. # # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +EXCLUDE = src/aksl_internal.h # directories that are symbolic links (a Unix file system feature) are excluded # from the input. # 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. # The default value is: YES. -GENERATE_LATEX = YES - -# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a +GENERATE_LATEX = NO # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of # it. # The default directory is: latex. diff --git a/README.md b/README.md index d0c74ad..95813da 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,64 @@ `libakstdlib` wraps C standard library functions so that they report failures through [libakerror](https://source.starfort.tech/andrew/libakerror)'s `ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes -and `errno`. It also provides a few data structures built on the same -convention (a doubly-linked list and a binary tree). +and `errno`. It also provides data structures built on the same convention. 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, -which corner cases are still open, and which libc functions are not yet wrapped. +See `TODO.md` for the current state of the library and `UPGRADING.md` if you are +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 @@ -27,27 +79,29 @@ package the parent provides instead. ### 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 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_STRING` | `"0.1.0"` | same | -| `AKSL_VERSION_NUMBER` | `100` | same | -| `AKSL_VERSION_SONAME` | `"0.1"` | same | -| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` | -| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` | -| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` | +| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `2` / `0` | `include/akstdlib_version.h.in` | +| `AKSL_VERSION_STRING` | `"0.2.0"` | same | +| `AKSL_VERSION_NUMBER` | `200` | same | +| `AKSL_VERSION_SONAME` | `"0.2"` | same | +| shared library | `libakstdlib.so.0.2.0`, soname `libakstdlib.so.0.2` | `VERSION` / `SOVERSION` | +| `pkg-config --modversion akstdlib` | `0.2.0` | `akstdlib.pc` | +| `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 source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the copy in your build directory; change the template or `project()`. -It is `0.x` deliberately. `TODO.md` Β§2.1 still records four confirmed defects -whose fixes change documented behaviour, so the API is not being promised yet. -While the major version is `0`, **the soname carries `MAJOR.MINOR`**: 0.1 and 0.2 +It is `0.x` deliberately. The 0.1 β†’ 0.2 bump was itself an ABI break β€” fixing the +confirmed defects changed five signatures and the `ato*` contract, all of it +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 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 @@ -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. 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 -against 0.1.7, which is exactly the promise the shared soname makes. +match and the patch level is ignored β€” a caller built against 0.2.0 keeps working +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 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: ``` -compiled against : 0.1.0 (soname 0.1) -loaded : 0.2.0 (0.2.0) -MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2) +compiled against : 0.2.0 (soname 0.2) +loaded : 0.3.0 (0.3.0) +MISMATCH DETECTED: compiled against libakstdlib 0.2.0, loaded 0.3.0 (soname 0.3) ``` ### 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 leaks a slot from libakerror's error pool. -One file per area of the API: `convert` (the `ato*` family), `format` (the -`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path` -(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and -`status_registry` (this library's side of the libakerror status-registry -contract β€” see "The libakerror version floor" above). +One file per area of the API: `memory`, `format` (the `printf` family), `convert` +(the `ato*` family) and `strto` (the family underneath it), `stream` +(`fopen`/`fread`/`fwrite`/`fclose`) and `streamio` (everything else in +`src/stream.c`), `string`, `path` (`aksl_realpath`), `strhash`, `linkedlist`, +`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 `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_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 -means the known-good tests passed and the known-bad ones are still failing in the -documented way. When a defect is fixed, its test starts passing, CTest reports it -as failed with *unexpectedly passed*, and that is the cue to move it from -`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS`. +**Both of those lists are currently empty**, which is the news: all six confirmed +defects in `TODO.md` Β§2.1 are fixed, and the four tests that used to sit in +`AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they +test, where they now have to keep passing rather than keep failing visibly. The +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 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 -`-fno-sanitize-recover=all`. Several of the open items in `TODO.md` Β§2 only -misbehave under instrumentation β€” the uninitialised `%s` in `aksl_realpath`, the -unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf` -family β€” so new tests for those should be run this way. +`-fno-sanitize-recover=all`. Three of the defects fixed in 0.2.0 only misbehaved +under instrumentation β€” the uninitialised `%s` in `aksl_realpath`'s error path, +the unbounded `vsprintf` behind the old `aksl_sprintf`, and the missing `va_end` +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 @@ -246,25 +330,42 @@ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \ -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 -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. +**Where it stands.** All four sources, with the whole suite: -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, 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 -that gap wider without changing a line of this library: the branch denominator -went from 661 to 1087 as those macros grew, so the same tests that scored 51.0% -(337/661) dropped to 44.3% (481/1087). The gate moved 45 β†’ 40 to match; line and -function coverage did not move at all. Adding the `aksl_version_*` family and its -tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 β€” -0.1 points of headroom is not a ratchet. +libakerror's own suite rather than to this one. Every `FAIL_ZERO_RETURN` in the +tree contributes several branches that this library has no way to reach. The +libakerror 1.0.0 bump made that gap wider without changing a line here: the +branch denominator per call site grew, so identical tests scored lower. Chasing +the number would mean testing libakerror's macros, which is what libakerror's +mutation suite is for β€” macros expand at the call site, so coverage cannot see +them properly from either side. 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 diff --git a/TODO.md b/TODO.md index 770c10d..56986e8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,555 +1,178 @@ # 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 -(`fopen`, `fread`, `fwrite`, `fclose`, `malloc`, `free`, `memset`, `memcpy`, `printf`, -`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`). +Ordered by blast radius: what blocks other work first, what is merely wrong +second, what is missing last. -Items marked **[CONFIRMED]** were reproduced by building the library and running a probe -program against it, not just read off the source. +## Where the library stands + +| | | +|---|---| +| 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.1–1.9 can now be written -against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list. +### 1.1 The error pool is a process-global array with no locking -- [x] **The current tests cannot fail.** `tests/test_linkedlist.c` asserted nothing at all; - it printed node names and returned `0` unconditionally, so any of the list bugs in - Β§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_.c` β†’ executable `test_` β†’ CTest test ``, 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.2–1.6 were written; the remaining survivors are: +**This is what makes the library single-threaded**, and it is the largest open +item by some distance. - | surviving mutants | where | why | - |---|---|---| - | 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 | - | 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 | - | 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (Β§2.2.8) β€” defaulted, then never called | - | 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 | +`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 +this library takes a slot from it on any failure path, so two threads raising +errors concurrently can be handed the same slot and will corrupt each other's +message, status and stack trace. - The threshold is a regression ratchet, not a quality bar; raise it as those survivors - become assertions. Each one is a concrete missing test β€” `mutation-junit.xml` lists - them with `file:line` and the exact edit. -- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant - 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. +**Consequence.** `README.md` says plainly that the library is not thread-safe. +That is honest, and it is also a hard ceiling: Β§4's `pthread_*` and socket +wrappers cannot be written until this is resolved, because a threading API +nobody can call from a thread is not an API. -### 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`. -- [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 `IGNORE()` logs a context and never releases it -### 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. -- [x] `aksl_fopen("/nonexistent/path", "r", &fp)` β†’ `ENOENT` propagated as the status, with - 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. +**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete +the workaround here. -### 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 - count written through `count` and the produced text. (`aksl_printf` is checked by - pointing `stdout` at a temp file for the duration of the call.) -- [x] Each of the three with every pointer argument NULL in turn β†’ `AKERR_NULLPOINTER`. -- [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. +**Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure +at all: *"another target with the same name already exists"*. `CMakeLists.txt` +shadows `add_custom_target` for the duration of the `add_subdirectory()` call and +renames the dependency's to `akerror_coverage`. -### 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 -contract is asserted by `tests/test_convert_strict.c`, which is registered as a known -failure until Β§2.1.5 is fixed. +### 1.4 libakerror installs no `akerrorConfigVersion.cmake` -- [x] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and - leading whitespace. -- [x] NULL `nptr` and NULL `dest` for each β†’ `AKERR_NULLPOINTER`. -- [x] **Non-numeric input** (`"not a number"`) β€” **[CONFIRMED]** currently returns *success* - with `*dest == 0`. `AKERR_VALUE` is asserted by the known-failing test. -- [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. +**Consequence here.** `cmake/akstdlib.cmake.in` has to call +`find_dependency(akerror)` with no version, because a request for one would be +refused for want of a version file no matter what is installed. The 1.0.0 floor +therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in +`akstdlib.h`, neither of which covers a `find_package` consumer. -### 1.5 `aksl_realpath` - -Covered by `tests/test_path.c`. Every failure case there passes a zeroed `resolved_path`, -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. +**What closing it would touch.** One `write_basic_package_version_file()` call +upstream β€” libakstdlib already does this correctly and can be copied β€” then add +the `1.0.0` floor to the `find_dependency` here. --- -## 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` -and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible: -`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c`, -`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`. +`src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread` +and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the +whole library. -1. **`aksl_list_append` does not find the tail β€” it silently truncates the list.** - `src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding: - `tail` is set to `slow` *before* `slow` advances, so it tracks the node *behind the - 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. +The standard permits a short transfer with neither indicator set, so the branch +is correct to have. Every way of actually producing one on Linux sets `feof` or +`ferror` first, so nothing in the suite reaches it. -2. **`aksl_list_iterate` skips the first half of the list.** `src/stdlib.c:324`. After the - cycle-detection loop, `slow` is left at the list midpoint, and the visiting loop then - starts from `slow` instead of from `list`. The head node is never passed to the callback. - Fix: iterate from `list`, not from `slow`. +**Consequence.** Low: the code is a handful of lines and reviewed, but it is the +only error path in the library that has never executed. -3. **`AKERR_ITERATOR_BREAK` does not stop a tree traversal.** `src/stdlib.c:251`. The - recursive frame in which the callback raises the break handles it in its own - `PROCESS`/`HANDLE(e, AKERR_ITERATOR_BREAK)` block and returns *success*; the parent's - `PASS` therefore sees no error and continues on to the sibling subtree. Breaking at - `tree[3]` in pre-order still visits all 7 nodes. Fix by splitting the recursion: an - 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. +**What closing it would touch.** A `FILE *` over a custom stream β€” `fopencookie` +on glibc, `funopen` on the BSDs β€” whose read function returns a short count +without setting either flag. That is a platform-specific test helper in +`tests/aksl_capture.h` guarded on the platform, which is why it has not been +written yet rather than an oversight. -4. **`va_end` is never called.** `src/stdlib.c:96`, `:109`, `:122` each call `va_start` with - 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. +### 2.2 The string-buffer overflow guard is untestable -5. **`aksl_atoi`/`atol`/`atoll`/`atof` cannot report a conversion failure.** - `src/stdlib.c:135`–`169`. `atoi("not a number")` returns success with `0`; - `atoi("99999999999999999999")` returns success with a wrapped value. Since the entire - 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. +`src/collections.c`, the `capacity = needed` arm in `strbuf_reserve`. Reaching it +needs an `aksl_StrBuf` within a factor of two of `SIZE_MAX`, which is not a test, +it is a hang. The other two uncovered lines. -6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`. - - `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a - buffer, but the wrapper discards `result`, so the caller gets nothing and the buffer - 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. +**Consequence.** None known. It is there because doubling a capacity is a +multiplication, and an unguarded one is how a growable buffer turns into a heap +overflow. -### 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 - `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.3 `aksl_version_check()` ignores its `patch` argument -2. **`aksl_fopen` does not validate `pathname` or `mode`.** `fopen(NULL, ...)` is UB. Add - `AKERR_NULLPOINTER` guards. +`src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule β€” +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.** - - `ptr` is never NULL-checked in either function. - - Neither has an out-param for the number of members actually transferred, so a caller who - 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. +**Consequence.** None today. If a future compatibility rule needs the patch level +to participate, that is the line to change, and the `#if` in +`tests/test_version.c` is the test that encodes the rule. -4. **`aksl_sprintf` wraps `vsprintf`, which is unbounded.** There is no way for a caller to - 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. +### 2.4 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines -5. **No `format` attribute on the variadic wrappers.** Adding - `__attribute__((format(printf, 2, 3)))` (and the equivalents) restores the compile-time - format/argument checking that callers lose by going through the wrapper. - -6. **`aksl_strhash_djb2` sign-extends bytes β‰₯ 0x80.** `src/stdlib.c:181` iterates a - `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`. +Macro artifacts rather than gaps. In libakerror that macro begins with the +`break;` belonging to `PROCESS`'s `case 0:` arm, reachable only when a callback +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 +by a test that would have to manufacture it. --- -## 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 -an `aksl_`-prefixed wrapper that turns the documented failure modes into an -`akerr_ErrorContext`". +Recorded so nobody adds them thinking they were forgotten. Each is a decision, +and each can be revisited with an argument. -### 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`)** -- [ ] `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. +## 4. Not yet wrapped -**String β†’ number (`stdlib.h`)** -- [ ] `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtod`, `strtof`, `strtold` β€” the correct - foundation for Β§2.1.5. +Ordered by how much a caller of this library would miss them. Β§3.1 and Β§3.6 of +the old numbering are done; what follows is what is left. -**Strings (`string.h`)** -- [ ] `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` +### 4.1 POSIX file and process API -**Stream I/O (`stdio.h`)** -- [ ] `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) +The most likely next surface. Nothing here is blocked; it is simply not written. **`unistd.h` / `fcntl.h`** - [ ] `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` - [ ] `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`** - [ ] `stat`, `fstat`, `lstat`, `fstatat` - [ ] `statvfs`, `fstatvfs` **`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** -- [ ] `fork`, `execve` / `execvp` / `execl` family, `waitpid`, `wait` +- [ ] `fork`, the `exec*` family, `waitpid`, `wait` - [ ] `posix_spawn` - [ ] `system`, `popen`, `pclose` - [ ] `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` +`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`** - [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise` -### 3.3 Time +### 4.2 Time - [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday` - [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime` - [ ] `strftime`, `strptime` - [ ] `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 - akerror-aware comparator signature would be a genuine improvement. -- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` +### 4.3 Sorting, searching and the rest of `stdlib.h` + +- [ ] `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` -### 3.5 Lower priority / larger projects +### 4.4 Larger surfaces -- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, `send`/`sendto`/`sendmsg`, - `recv`/`recvfrom`/`recvmsg`, `shutdown`, `setsockopt`/`getsockopt`, `getaddrinfo`/ - `freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton` -- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_create1`/`epoll_ctl`/`epoll_wait` -- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, `raise`, - `signalfd` -- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`, `pthread_cond_*`, - `pthread_rwlock_*`, `sem_*` β€” blocked on resolving the thread-safety question in Β§1.9 - (libakerror's error pool is an unlocked process-global array). -- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror` +- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the + `send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`, + `getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`. + `getaddrinfo` is the interesting one: it has its own error space, neither + errno nor akerror, which needs mapping into a registered status range. +- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*` +- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, + `raise`, `signalfd`. A handler cannot raise an akerror context β€” the pool is + not async-signal-safe β€” so the wrapper covers installation and masking only, + 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_*` -- [ ] **Math**: the `math.h` functions that set `errno`/raise FP exceptions (`sqrt`, `log`, - `pow`, `acos`, …) β€” probably better served by a dedicated `libakmath`. +- [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions + (`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`, - `concat`, `free_all`, reverse iteration, and a head/tail-tracking container type so - `append` is O(1) instead of O(n). -- [ ] Tree: `insert`, `remove`, `find`, `height`, `count`, `free_all`, and the BFS traversal - the `lalloc`/`lfree`/`queue` parameters were designed for. -- [ ] Hash map built on `aksl_strhash_djb2`, plus additional hashes (FNV-1a, xxHash) and a - NUL-terminated `aksl_strhash_djb2_str` convenience form. -- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers - pleasant to use. +- [ ] A dynamic array / vector, on the same "caller owns the storage" terms as + `aksl_HashMap`. +- [ ] A hash map keyed on something other than a string β€” the current one copies + keys into fixed slots, which is right for identifiers and wrong for + anything large or binary. +- [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so; + sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then + refuses. Bounded rather than dangerous, but a red-black or AVL variant is + 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 -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. +## 5. Evidence from the first full consumer -**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and -**116 calls to raw libc**: +`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter +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 | -|---|---| -| `aksl_fopen` / `aksl_fclose` | 6 | -| `aksl_fprintf` | 2 | -| `aksl_fwrite` | 1 | -| `aksl_strhash_djb2` | 1 | +**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 +libc failures into error contexts", bypassed 92% of the time by the consumer most +committed to it. -| 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 | Β§3.1 strings | -| `strcmp` | 16 | Β§3.1 strings | -| `strncpy` | 15 | Β§3.1 strings | -| `memcpy` / `memset` | 16 | wrapped, but see Β§2.2.11 | -| `strtoll` / `strtod` | 2 | Β§3.1 string β†’ number | -| `fgets` | 2 | Β§3.1 stream I/O | -| `strstr` | 1 | Β§3.1 strings | +| `strlen` | 37 | `aksl_strlen` | +| `snprintf` | 28 | `aksl_snprintf` | +| `strcmp` | 16 | `aksl_strcmp` | +| `memcpy` / `memset` | 16 | `aksl_memcpy` / `aksl_memset` | +| `strncpy` | 15 | `aksl_strncpy` | +| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` | +| `fgets` | 2 | `aksl_fgets` | +| `strstr` | 1 | `aksl_strstr` | -A library whose value proposition is "turn silent libc failures into error contexts" is being -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. +**All four things the port had to write for itself now exist here.** -### 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 - biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s - error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi` - would have converted four *diagnosable errors* into *wrong answers* β€” `VAL("garbage")` - silently returning `0.0` rather than raising, and a malformed line number becoming line 0. - 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. +**And the four confirmed-with-impact items are closed.** Β§2.2.4's unbounded +`aksl_sprintf` is gone; Β§2.2.2's unchecked `aksl_fopen` arguments are checked, so +`akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go; +Β§2.2.6's sign-extended djb2 reads bytes unsigned; and Β§2.1.4's missing `va_end` β€” +which akbasic's stdio text sink ran on every line of program output β€” is fixed. -2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). Β§3.6 - already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over: - the interpreter needs one for variables, one for functions and one for labels per scope. It - is open-addressed with linear probing over a caller-supplied slot count, refuses rather than - resizes when full, and is the single most obviously-missing data structure in the library. - 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 ``'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.1–3 were -avoided rather than survived β€” akbasic's symbol tables are open-addressed precisely because -`aksl_list_append` truncates. +**Still true, and still shaping the wishlist.** akbasic uses no allocator, no +lists and no trees, drawing everything from fixed pools by design. A consumer that +does allocate would weight Β§4.1's `open`/`read`/`write` far higher than this one +does. The next thing worth doing is porting akbasic onto this release and +counting the calls again. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..a52c02e --- /dev/null +++ b/UPGRADING.md @@ -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 diff --git a/cmake/RunDoxygen.cmake b/cmake/RunDoxygen.cmake new file mode 100644 index 0000000..07df580 --- /dev/null +++ b/cmake/RunDoxygen.cmake @@ -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") diff --git a/include/akstdlib.h b/include/akstdlib.h index 1be20ac..dca62df 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -1,3 +1,34 @@ +/** + * @file akstdlib.h + * @brief libc wrappers that report failures through libakerror error contexts. + * + * Every entry point here returns `akerr_ErrorContext *` and is marked + * `AKERR_NOIGNORE`: NULL means success, anything else is an error the caller + * owns and must release. Results come back through out-params, because the + * return value is spoken for. + * + * Four conventions run through the whole library, and knowing them removes most + * of the per-function reading: + * + * - **A NULL out-param is a caller error**, not "don't care". If a function can + * tell you something, it insists on somewhere to put it. + * - **Finding nothing is success.** Searching functions write NULL or zero and + * return NULL. Absent is an ordinary answer to "where is this". + * - **Truncation is a failure.** Every function that writes into a caller's + * buffer takes that buffer's size and raises AKERR_OUTOFBOUNDS rather than + * quietly writing less, and writes nothing at all when it does. + * - **`errno` is cleared before each wrapped call** and read back with a + * fallback, so no error can carry status 0 -- which every `DETECT` and `CATCH` + * downstream would read as success. + * + * @warning This library is **not thread-safe**. libakerror hands out error + * contexts from a process-global array with no locking, and every function here + * takes a slot from it on any failure path. See README.md. + * + * @see README.md for the deviations from libc semantics, UPGRADING.md if you are + * coming from 0.1.0, and TODO.md for what is still open. + */ + #ifndef _AKSTDLIB_H_ #define _AKSTDLIB_H_ @@ -55,449 +86,2167 @@ extern "C" { * Restore the compile-time format/argument checking that callers otherwise lose * by going through a variadic wrapper: without it, printf("%d", "str") is caught * and aksl_printf(&n, "%d", "str") is not. TODO.md 2.2.5. + * + * tests/negative/format_mismatch.c is a compile that must fail, which is what + * proves these are still attached. */ #if defined(__GNUC__) || defined(__clang__) +/** @brief Mark a printf-style format argument for compile-time checking. */ #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \ __attribute__((format(printf, __fmt_index, __first_arg))) +/** @brief Mark a scanf-style format argument for compile-time checking. */ #define AKSL_SCANF_FORMAT(__fmt_index, __first_arg) \ __attribute__((format(scanf, __fmt_index, __first_arg))) #else +/** @brief No-op where the compiler has no format attribute. */ #define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) +/** @brief No-op where the compiler has no format attribute. */ #define AKSL_SCANF_FORMAT(__fmt_index, __first_arg) #endif +/* ====================================================================== */ +/** @name Types + * @{ + */ +/* ====================================================================== */ + +/** @brief A node in a doubly-linked list. Initialise with aksl_list_node_init. */ typedef struct aksl_ListNode { - void *data; - struct aksl_ListNode *next; - struct aksl_ListNode *prev; + void *data; /**< Whatever the caller is storing. Never read by this library. */ + struct aksl_ListNode *next; /**< The next node, or NULL at the tail. */ + struct aksl_ListNode *prev; /**< The previous node, or NULL at the head. */ } aksl_ListNode; +/** @brief A node in a binary tree. Initialise with aksl_tree_node_init. */ typedef struct aksl_TreeNode { - struct aksl_TreeNode *parent; - struct aksl_TreeNode *left; - struct aksl_TreeNode *right; - void *leaf; + struct aksl_TreeNode *parent; /**< Set and read by the aksl_tree_insert/remove family only. */ + struct aksl_TreeNode *left; /**< Left child, or NULL. */ + struct aksl_TreeNode *right; /**< Right child, or NULL. */ + void *leaf; /**< Whatever the caller is storing; the comparator's operand. */ } aksl_TreeNode; -#define AKSL_TREE_SEARCH_BFS 0 /** Breadth-first (left child before right) search mode for tree nodes */ -#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /** Breadth-first, right child before left, search mode for tree nodes */ -#define AKSL_TREE_SEARCH_DFS 2 /** Alias for AKSL_TREE_SEARCH_DFS_PREORDER */ -#define AKSL_TREE_SEARCH_DFS_PREORDER 2 /** Depth first pre-order (root, left, right) search mode for tree nodes */ -#define AKSL_TREE_SEARCH_DFS_INORDER 3 /** Depth first in-order (left, root, right) search mode for tree nodes */ -#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /** Depth first post-order (left, right, root) search mode for tree nodes */ -#define AKSL_TREE_SEARCH_VISIT 5 /** Visit the node and stop; do not traverse the children */ +#define AKSL_TREE_SEARCH_BFS 0 /**< Breadth-first, left child before right */ +#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /**< Breadth-first, right child before left */ +#define AKSL_TREE_SEARCH_DFS 2 /**< Alias for AKSL_TREE_SEARCH_DFS_PREORDER */ +#define AKSL_TREE_SEARCH_DFS_PREORDER 2 /**< Depth-first pre-order: root, left, right */ +#define AKSL_TREE_SEARCH_DFS_INORDER 3 /**< Depth-first in-order: left, root, right */ +#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /**< Depth-first post-order: left, right, root */ +#define AKSL_TREE_SEARCH_VISIT 5 /**< Visit the node and stop; do not traverse the children */ -/* - * How deep aksl_tree_iterate will walk before raising AKERR_OUTOFBOUNDS. +/** + * @brief How deep a tree walk goes before raising AKERR_OUTOFBOUNDS. * - * This is a real limit, not a formality: the depth-first walk is recursive, so - * without it a degenerate chain overflows the stack and a child pointing back at - * an ancestor recurses until the process dies. 256 is far past any balanced tree + * A real limit, not a formality: the depth-first walk is recursive, so without + * it a degenerate chain overflows the stack and a child pointing back at an + * ancestor recurses until the process dies. 256 is far past any balanced tree * that fits in memory (2^256 nodes) and comfortably short of a stack overflow, - * so in practice it only ever rejects the degenerate shapes it exists to reject. + * so in practice it only rejects the degenerate shapes it exists to reject. * * It also sizes the ancestor buffer aksl_tree_iterate keeps on its own stack -- * one pointer per level, so raising it costs 8 bytes of stack per level. */ #define AKSL_TREE_MAX_DEPTH 256 -/* - * Head/tail/length container. aksl_list_append has to walk the list to find its - * tail, so building n nodes with it is O(n^2); the container makes that O(1) and - * makes the length free. It owns no memory -- the nodes are still the caller's. +/** + * @brief A list that tracks its own head, tail and length. + * + * aksl_list_append has to walk the list to find its tail, so building n nodes + * with it is O(n^2); this makes that O(1) and makes the length free. It owns no + * memory -- the nodes are still the caller's. */ typedef struct aksl_List { - aksl_ListNode *head; - aksl_ListNode *tail; - size_t length; + aksl_ListNode *head; /**< First node, or NULL when empty. */ + aksl_ListNode *tail; /**< Last node, or NULL when empty. */ + size_t length; /**< Number of nodes. */ } aksl_List; +/** @brief Called once per node by aksl_list_iterate. Raise AKERR_ITERATOR_BREAK to stop. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodeIterator)(aksl_ListNode *node, void *data); +/** @brief Called once per node by aksl_tree_iterate. Raise AKERR_ITERATOR_BREAK to stop. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *node, void *data); +/** @brief Allocator, shaped like aksl_malloc. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest); +/** @brief Deallocator, shaped like aksl_free. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr); -/* Reports its answer through *matched (non-zero to accept) and may raise. */ +/** @brief Accepts or rejects a node for aksl_list_find, through *matched (non-zero to accept). May raise. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodePredicate)(aksl_ListNode *node, void *data, int *matched); -/* Negative, zero or positive through *dest, as strcmp(3) has it. */ +/** @brief Orders two leaves for the tree functions: negative, zero or positive through *dest, as strcmp(3). May raise. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeCompareFunc)(void *a, void *b, int *dest); +/** @brief Called once per live entry by aksl_hashmap_iterate. Raise AKERR_ITERATOR_BREAK to stop. */ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_HashMapIterator)(const char *key, void *value, void *data); -/* - * Version of the shared library actually loaded, as opposed to the - * AKSL_VERSION_* macros above, which record what the caller was *compiled* - * against. The two differ exactly when a stale libakstdlib.so is on the loader - * path, which is the failure these entry points exist to name. +/** @} */ + +/* ====================================================================== */ +/** @name Version * - * All three pointers are required; this library treats a NULL out-param as a - * caller error rather than as "don't care" (so does aksl_free). + * The AKSL_VERSION_* macros record what a caller was *compiled* against. These + * report the shared library actually *loaded*. The two differ exactly when a + * stale libakstdlib.so is on the loader path, which is the failure they exist to + * name. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief The loaded library's version. + * @param[out] major Major version. Required. + * @param[out] minor Minor version. Required. + * @param[out] patch Patch version. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. All three are required; this + * library treats a NULL out-param as a caller error rather than as + * "don't care". + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch); -/* - * The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither - * can fail -- they return pointers to string literals in the library's own - * .rodata -- so they return the string directly rather than an error context, - * the same exception akerr_name_for_status() makes for the same reason. +/** + * @brief The loaded library's version as "MAJOR.MINOR.PATCH". + * + * Cannot fail -- it returns a pointer to a string literal in the library's own + * .rodata -- so it returns the string directly rather than an error context, the + * same exception akerr_name_for_status() makes for the same reason. + * + * @return A string owned by the library. Do not free it. */ const char *aksl_version_string(void); + +/** + * @brief The loaded library's soname, as "MAJOR.MINOR" pre-1.0 and "MAJOR" from 1.0. + * @return A string owned by the library. Do not free it. + */ const char *aksl_version_soname(void); -/* - * Compare the caller's compiled-in version against the loaded library's. - * Compatibility is defined as "same soname", so pre-1.0 both major and minor - * must match and patch is ignored; from 1.0 only major will matter. Raises - * AKERR_VALUE naming both versions on a mismatch. +/** + * @brief Compare the caller's compiled-in version against the loaded library's. * - * Call it through AKSL_VERSION_CHECK() below rather than directly. The macro - * expands at *your* call site, so it captures the AKSL_VERSION_* the caller was - * built with; the function compares them against the values baked into the - * library. Passing the numbers by hand defeats the entire mechanism. + * Call it through AKSL_VERSION_CHECK() rather than directly. That macro expands + * at *your* call site, so it captures the AKSL_VERSION_* you were built with; + * this function compares them against the values baked into the library. Passing + * the numbers by hand defeats the entire mechanism. + * + * Compatibility is "same soname": pre-1.0 both major and minor must match and + * patch is ignored; from 1.0 only major will matter. + * + * @param[in] major The caller's compiled-in major version. + * @param[in] minor The caller's compiled-in minor version. + * @param[in] patch The caller's compiled-in patch version. Not compared; it is + * here so the error message can name the caller's full version. + * @throws AKERR_VALUE On a mismatch, naming both versions. + * @return NULL when the pairing is compatible, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch); +/** @brief Check the loaded library against the one this translation unit was built against. */ #define AKSL_VERSION_CHECK() \ aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH) -/* - * Stream I/O. - * - * fread and fwrite take a required size_t *nmemb_out. It is always written -- - * including on the failure paths -- so a caller who gets AKERR_EOF can find out - * how much data arrived before the stream ran out, which is the whole point of - * distinguishing EOF from an error. A transfer that comes up short with no - * stream error set is AKERR_IO, not silent success. - */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(const char *pathname, const char *mode, FILE **fp); -akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, size_t *nmemb_out); -akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out); -akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream); +/** @} */ -/* - * Memory. aksl_malloc and aksl_calloc reject a zero-size request outright rather - * than reporting whatever errno happened to hold when malloc(0) returned NULL. +/* ====================================================================== */ +/** @name Memory + * + * aksl_malloc and aksl_calloc reject a zero-size request outright rather than + * reporting whatever errno happened to hold when malloc(0) returned NULL. * aksl_realloc takes the pointer by reference and leaves it valid and untouched * on failure, which is realloc(3)'s classic leak closed off. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief Allocate `size` bytes. + * @param[in] size Bytes to allocate. Must be non-zero. + * @param[out] dst Receives the allocation, or NULL on any failure. Required. + * @throws AKERR_NULLPOINTER If dst is NULL. + * @throws AKERR_VALUE If size is 0. There is nothing useful to hand back either + * way, and malloc(0) is permitted to return NULL without setting errno -- + * which is how an error with status 0 used to get raised. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst); + +/** + * @brief Allocate `nmemb` * `size` zeroed bytes. + * @param[in] nmemb Number of members. Must be non-zero. + * @param[in] size Bytes per member. Must be non-zero. + * @param[out] dst Receives the allocation, or NULL on any failure. Required. + * @throws AKERR_NULLPOINTER If dst is NULL. + * @throws AKERR_VALUE If either nmemb or size is 0. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void **dst); + +/** + * @brief Resize an allocation in place, through the same pointer. + * + * realloc(3)'s trap, hidden: on failure it returns NULL *and leaves the original + * block valid*, so the near-universal `p = realloc(p, n)` leaks the original + * every time it fails. Here the pointer goes in and out through the same + * out-param and is left untouched -- still valid, still yours to free -- whenever + * an error is raised. + * + * @param[in,out] ptr The block to resize, updated on success. `*ptr` may be + * NULL, in which case this allocates. Required. + * @param[in] size New size in bytes. Must be non-zero. + * @throws AKERR_NULLPOINTER If ptr is NULL. + * @throws AKERR_VALUE If size is 0; use aksl_free to release the block. + * @throws ENOMEM If the allocation fails. `*ptr` still points at the original, + * still-valid block. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size); + +/** + * @brief Resize an array allocation, checking the multiplication for overflow. + * + * `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. + * + * @param[in,out] ptr The block to resize, updated on success. Required. + * @param[in] nmemb Number of members. Must be non-zero. + * @param[in] size Bytes per member. Must be non-zero. + * @throws AKERR_NULLPOINTER If ptr is NULL. + * @throws AKERR_VALUE If either nmemb or size is 0. + * @throws AKERR_OUTOFBOUNDS If `nmemb * size` overflows size_t. + * @throws ENOMEM If the allocation fails. `*ptr` still points at the original. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_reallocarray(void **ptr, size_t nmemb, size_t size); + +/** + * @brief aligned_alloc(3), with its two preconditions checked. + * @param[in] alignment Required alignment. Must be a power of two. + * @param[in] size Bytes to allocate. Must be a non-zero multiple of + * `alignment`, which aligned_alloc(3) requires and does not check. + * @param[out] dst Receives the allocation, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If dst is NULL. + * @throws AKERR_VALUE If alignment is 0 or not a power of two, if size is 0, or + * if size is not a multiple of alignment -- all undefined behaviour that + * usually just returns NULL. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_aligned_alloc(size_t alignment, size_t size, void **dst); + +/** + * @brief posix_memalign(3), the form that does not constrain `size`. + * + * posix_memalign(3) breaks the errno convention -- it *returns* the error number + * and leaves errno alone -- which is the kind of local irregularity a caller + * mis-handles once and never notices. + * + * @param[out] dst Receives the allocation, or NULL on failure. Required. + * @param[in] alignment Required alignment: a power of two, and a multiple of + * sizeof(void *). + * @param[in] size Bytes to allocate. Must be non-zero. + * @throws AKERR_NULLPOINTER If dst is NULL. + * @throws AKERR_VALUE If size is 0. + * @throws EINVAL If the alignment is unacceptable. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_posix_memalign(void **dst, size_t alignment, size_t size); + +/** + * @brief Release an allocation. + * @param[in] ptr The block to release. + * @throws AKERR_NULLPOINTER If ptr is NULL. This is deliberate and against + * libc: free(NULL) is legal and does nothing, but in a codebase that + * routes every allocation through aksl_malloc, freeing a pointer you + * believed was live and finding it NULL means something upstream did not + * happen -- and silence there is how a lost allocation is found three + * weeks later. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr); + +/** + * @brief Release an allocation and clear the caller's pointer. + * + * Prefer this to aksl_free where the pointer outlives the call: a cleared + * pointer cannot be used or freed twice. + * + * @param[in,out] ptr The block to release; set to NULL on success. Required. + * @throws AKERR_NULLPOINTER If ptr or *ptr is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr); + +/** + * @brief Fill `n` bytes of `s` with `c`. + * @param[out] s Destination. Required. + * @param[in] c Byte to write, converted to unsigned char. + * @param[in] n Number of bytes. 0 is a no-op, not an error. + * @throws AKERR_NULLPOINTER If s is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n); -/* Overlapping ranges are AKERR_VALUE, not undefined behaviour; use aksl_memmove. */ + +/** + * @brief Copy `n` bytes from `s` to `d`, which must not overlap. + * @param[out] d Destination. Required. + * @param[in] s Source. Required. + * @param[in] n Number of bytes. 0 is a no-op, not an error. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If the ranges overlap. memcpy(3) calls that undefined + * behaviour, which in practice means "works until a compiler version or + * a length changes". Use aksl_memmove when you mean to overlap. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, const void *s, size_t n); + +/** + * @brief Copy `n` bytes from `s` to `d`, which may overlap. + * @param[out] d Destination. Required. + * @param[in] s Source. Required. + * @param[in] n Number of bytes. 0 is a no-op, not an error. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memmove(void *d, const void *s, size_t n); + +/** + * @brief Compare `n` bytes of `a` and `b`. + * @param[in] a First buffer. Required. + * @param[in] b Second buffer. Required. + * @param[in] n Number of bytes to compare. + * @param[out] dest Negative, zero or positive as a sorts before, equal to, or + * after b. Required -- the return value is the error context. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memcmp(const void *a, const void *b, size_t n, int *dest); + +/** + * @brief Find the first `c` in the first `n` bytes of `s`. + * @param[in] s Buffer to search. Required. + * @param[in] c Byte to find, converted to unsigned char. + * @param[in] n Number of bytes to search. + * @param[out] dest The match, or NULL if there is none. Required. Not finding + * it is a successful answer, not an error. + * @throws AKERR_NULLPOINTER If s or dest is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, void **dest); -/* - * Formatted output. *count is the byte count written excluding the terminating - * NUL, and is 0 on every failure path. +/** @} */ + +/* ====================================================================== */ +/** @name Formatted output + * + * `*count` is the byte count written excluding the terminating NUL, and is 0 on + * every failure path -- never vsnprintf's -1, and never the length the output + * *would* have been. * * There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an * error-handling wrapper around an unbounded write is the sharp edge this - * library exists to remove. aksl_snprintf replaces it and treats truncation as - * AKERR_OUTOFBOUNDS rather than as a short success. + * library exists to remove. aksl_snprintf replaces it. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief printf(3) to stdout. + * @param[out] count Bytes written; 0 on failure. Required. + * @param[in] format printf format string. Required. Checked at compile time. + * @throws AKERR_NULLPOINTER If count or format is NULL. + * @throws AKERR_IO Or the errno the C library saw, if the write fails. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...) AKSL_PRINTF_FORMAT(2, 3); + +/** + * @brief fprintf(3) to a stream. + * @param[out] count Bytes written; 0 on failure. Required. + * @param[in] stream Destination stream. Required. + * @param[in] format printf format string. Required. Checked at compile time. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_IO Or the errno the C library saw, if the write fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4); + +/** + * @brief snprintf(3) into a bounded buffer, treating truncation as an error. + * + * snprintf(3) truncates, terminates and reports the length it *would* have + * written, leaving the caller to notice by comparing that against the buffer + * size -- the check this library exists to stop people forgetting. + * + * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. + * @param[out] str Destination buffer. Required. + * @param[in] size Size of `str` including the terminator. Must be non-zero. + * @param[in] format printf format string. Required. Checked at compile time. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If size is 0. + * @throws AKERR_OUTOFBOUNDS If the output does not fit, naming both lengths. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(4, 5); +/** + * @brief Format into a freshly allocated string. + * + * 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. There is no truncation case, because there is no + * fixed buffer to truncate against. + * + * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. + * @param[out] dest The formatted string, or NULL on failure. Required. It is + * yours; release it with aksl_free or aksl_freep. + * @param[in] format printf format string. Required. Checked at compile time. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_IO If the arguments cannot be formatted. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4); + +/** + * @brief Format into a freshly allocated string. The va_list form of aksl_asprintf. + * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. + * @param[out] dest The formatted string, or NULL on failure. Required. + * @param[in] format printf format string. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_IO If the arguments cannot be formatted. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const char *restrict format, va_list args); + +/** + * @brief vprintf(3) to stdout. The va_list form of aksl_printf. + * @param[out] count Bytes written; 0 on failure. Required. + * @param[in] format printf format string. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If count or format is NULL. + * @throws AKERR_IO Or the errno the C library saw, if the write fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args); + +/** + * @brief vfprintf(3) to a stream. The va_list form of aksl_fprintf. + * @param[out] count Bytes written; 0 on failure. Required. + * @param[in] stream Destination stream. Required. + * @param[in] format printf format string. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_IO Or the errno the C library saw, if the write fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stream, const char *restrict format, va_list args); + +/** + * @brief vsnprintf(3) into a bounded buffer. The va_list form of aksl_snprintf. + * @param[out] count Bytes written excluding the NUL; 0 on failure. Required. + * @param[out] str Destination buffer. Required. + * @param[in] size Size of `str` including the terminator. Must be non-zero. + * @param[in] format printf format string. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If size is 0. + * @throws AKERR_OUTOFBOUNDS If the output does not fit. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args); -/* - * String to number. +/** @} */ + +/* ====================================================================== */ +/** @name String to number * * These are strict, unlike the libc functions they are named for: * - * no digits consumed -> AKERR_VALUE - * trailing junk, endptr NULL -> AKERR_VALUE - * value outside the type -> ERANGE + * | condition | status | + * |---|---| + * | no digits consumed | AKERR_VALUE | + * | trailing junk, endptr NULL | AKERR_VALUE | + * | value outside the type | ERANGE | * - * and *dest is 0 on every failure. Pass a non-NULL endptr to opt out of the + * and `*dest` is 0 on every failure. Pass a non-NULL endptr to opt out of the * trailing-junk check and read a number off the front of a longer string. * - * The ato* forms are aksl_strto* with base 10 and a NULL endptr, so the whole - * string must be a number. "0x10" is AKERR_VALUE through aksl_atoi -- base 10 - * stops at the 'x' -- and 16 through aksl_strtol(nptr, NULL, 0, &dest). + * The ato* forms are the strto* ones with base 10 and a NULL endptr, so the + * whole string must be a number. "0x10" is therefore AKERR_VALUE through + * aksl_atoi -- base 10 stops at the 'x' -- and 16 through + * `aksl_strtol(nptr, NULL, 0, &dest)`. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief strtol(3) with an error channel. + * @param[in] nptr String to parse. Leading whitespace is accepted. Required. + * @param[out] endptr If non-NULL, receives the first unconsumed character and + * trailing junk is not an error. Pass NULL to require that the whole + * string be a number. + * @param[in] base 2 to 36, or 0 to auto-detect the 0x and 0 prefixes. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE If no digits were consumed, or on trailing junk when + * endptr is NULL. + * @throws ERANGE If the value does not fit a long. + * @throws EINVAL If the base is invalid. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtol(const char *nptr, char **endptr, int base, long *dest); + +/** + * @brief strtoll(3) with an error channel. + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[in] base 2 to 36, or 0 to auto-detect the prefix. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, or trailing junk with a NULL endptr. + * @throws ERANGE If the value does not fit a long long. + * @throws EINVAL If the base is invalid. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoll(const char *nptr, char **endptr, int base, long long *dest); + +/** + * @brief strtoul(3) with an error channel, and without its sign surprise. + * + * strtoul(3) accepts a leading '-' and hands back the negation wrapped into the + * unsigned range, so "-1" parses as ULONG_MAX and reports nothing at all. The + * sign is rejected here before the call. + * + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[in] base 2 to 36, or 0 to auto-detect the prefix. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, trailing junk with a NULL endptr, or a + * negative input. + * @throws ERANGE If the value does not fit an unsigned long. + * @throws EINVAL If the base is invalid. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoul(const char *nptr, char **endptr, int base, unsigned long *dest); + +/** + * @brief strtoull(3) with an error channel, and without its sign surprise. + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[in] base 2 to 36, or 0 to auto-detect the prefix. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, trailing junk with a NULL endptr, or a + * negative input. + * @throws ERANGE If the value does not fit an unsigned long long. + * @throws EINVAL If the base is invalid. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoull(const char *nptr, char **endptr, int base, unsigned long long *dest); + +/** + * @brief strtod(3) with an error channel. + * + * "inf", "infinity" and "nan" are accepted in any case, because strtod(3) + * accepts them and they are exact round-trips rather than approximations of + * something else. Underflow is ERANGE as well as overflow: it is the only signal + * strtod gives, and losing every significant digit of "1e-400" is a conversion + * failure however it is spelled. + * + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[out] dest The value; 0.0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, or trailing junk with a NULL endptr. + * @throws ERANGE On overflow or underflow. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtod(const char *nptr, char **endptr, double *dest); + +/** + * @brief strtof(3) with an error channel. + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[out] dest The value; 0.0f on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, or trailing junk with a NULL endptr. + * @throws ERANGE On overflow or underflow. float's range is far narrower than + * double's, so "1e300" is ERANGE here and fine through aksl_strtod. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtof(const char *nptr, char **endptr, float *dest); + +/** + * @brief strtold(3) with an error channel. + * @param[in] nptr String to parse. Required. + * @param[out] endptr First unconsumed character, or NULL to reject trailing junk. + * @param[out] dest The value; 0.0L on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits, or trailing junk with a NULL endptr. + * @throws ERANGE On overflow or underflow. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtold(const char *nptr, char **endptr, long double *dest); +/** + * @brief atoi(3) with an error channel: base 10, whole string. + * + * atoi(3) has no error channel at all -- "not a number" converts to 0 and an + * overflowing literal wraps -- which made this the one place in the library a + * libc failure passed unnoticed. + * + * @param[in] nptr String to parse. Required. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits or trailing junk, including "0x10", where + * base 10 stops at the 'x'. + * @throws ERANGE If the value does not fit an int. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_atoi(const char *nptr, int *dest); + +/** + * @brief atol(3) with an error channel: base 10, whole string. + * @param[in] nptr String to parse. Required. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits or trailing junk. + * @throws ERANGE If the value does not fit a long. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_atol(const char *nptr, long *dest); + +/** + * @brief atoll(3) with an error channel: base 10, whole string. + * @param[in] nptr String to parse. Required. + * @param[out] dest The value; 0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits or trailing junk. + * @throws ERANGE If the value does not fit a long long. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_atoll(const char *nptr, long long *dest); + +/** + * @brief atof(3) with an error channel: whole string. + * @param[in] nptr String to parse. Required. + * @param[out] dest The value; 0.0 on any failure. Required. + * @throws AKERR_NULLPOINTER If nptr or dest is NULL. + * @throws AKERR_VALUE On no digits or trailing junk. + * @throws ERANGE On overflow or underflow. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest); -/* - * realpath(3). `buflen` must be at least PATH_MAX, because realpath(3) offers no - * way to bound its own write -- an undersized buffer is AKERR_OUTOFBOUNDS rather - * than a buffer overflow. aksl_realpath_alloc sizes and allocates the result - * instead; *dest is then the caller's to release with aksl_free. +/** @} */ + +/* ====================================================================== */ +/** @name Paths and hashing + * @{ + */ +/* ====================================================================== */ + +/** + * @brief realpath(3) into a caller-supplied buffer. + * + * The error message names the input path only. realpath(3) leaves the + * destination unspecified on failure, so reading it back to report on it -- as + * this used to -- reads uninitialised memory in the error path itself. + * + * @param[in] path Path to resolve. Required. + * @param[out] resolved_path Destination. Required, and cleared before the call. + * @param[in] buflen Size of `resolved_path`. Must be at least PATH_MAX. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_OUTOFBOUNDS If buflen is below PATH_MAX. realpath(3) offers no + * way to bound its own write, so a shorter buffer cannot be used safely + * and is refused rather than overflowed. + * @throws ENOENT, ENOTDIR, ELOOP, EACCES Or whatever else realpath(3) reports. + * @return NULL on success, an error context otherwise. + * @see aksl_realpath_alloc if you have no PATH_MAX-sized buffer to hand. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path, size_t buflen); + +/** + * @brief realpath(3), allocating the result. + * @param[in] path Path to resolve. Required. + * @param[out] dest Receives the allocated result, or NULL on failure. Required. + * The buffer is yours; release it with aksl_free or aksl_freep. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ENOENT, ENOTDIR, ELOOP, EACCES Or whatever else realpath(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path, char **dest); -/* - * djb2. Length-driven, so embedded NUL bytes hash like any other byte; bytes - * >= 0x80 are read as unsigned, so the result matches canonical djb2 and does - * not depend on whether plain char is signed on the target. +/** + * @brief djb2 hash over an explicit length. + * + * Length-driven rather than NUL-driven, so an embedded NUL hashes like any other + * byte. Bytes are read unsigned, so the result matches canonical djb2 and does + * not depend on whether plain char is signed on the target -- which it does on + * x86 and ARM Linux, and did not use to. + * + * @param[in] str Bytes to hash. Required. + * @param[in] len Number of bytes. 0 yields the seed, 5381. + * @param[out] hashval The hash. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval); + +/** + * @brief djb2 hash of a NUL-terminated string. + * @param[in] str String to hash. Required. + * @param[out] hashval The hash. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval); -/* ---------------------------------------------------------------------- */ -/* Strings -- src/string.c */ -/* ---------------------------------------------------------------------- */ +/** @} */ -/* +/* ====================================================================== */ +/** @name Streams: open, read, write, close + * + * aksl_fread and aksl_fwrite take a required `size_t *nmemb_out`. It is always + * written -- including on the failure paths -- so a caller who gets AKERR_EOF can + * find out how much data arrived before the stream ran out, which is the whole + * point of distinguishing EOF from an error. A transfer that comes up short with + * no stream error set is AKERR_IO, not silent success. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief fopen(3). + * @param[in] pathname Path to open. Required; fopen(NULL, ...) is undefined + * behaviour and this used to hand it straight through. + * @param[in] mode fopen mode string. Required. + * @param[out] fp The stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws ENOENT, EACCES Or whatever else fopen(3) reports, with the pathname in + * the message. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(const char *pathname, const char *mode, FILE **fp); + +/** + * @brief fread(3), reporting how much was actually read. + * @param[out] ptr Destination buffer. Required. + * @param[in] size Bytes per member. + * @param[in] nmemb Members to read. + * @param[in] stream Stream to read from. Required. + * @param[out] nmemb_out Members actually read. Required, and written on every + * path including the failures. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_EOF If the stream ran out first. `*nmemb_out` says how much + * arrived, which is the reason this is distinguished from an error. + * @throws AKERR_IO Or the errno the C library saw, on a stream error -- and on a + * short read with neither EOF nor an error set, which the standard + * permits and which used to be reported as complete success. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, size_t *nmemb_out); + +/** + * @brief fwrite(3), reporting how much was actually written. + * @param[in] ptr Source buffer. Required. + * @param[in] size Bytes per member. + * @param[in] nmemb Members to write. + * @param[in] fp Stream to write to. Required. + * @param[out] nmemb_out Members actually written. Required, and written on every + * path including the failures. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_IO Or the errno the C library saw, on a stream error or a short + * write. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out); + +/** + * @brief fclose(3), surfacing a failed flush. + * + * A caller who checks every write and ignores the close loses buffered data + * silently, because the failure can only appear here. + * + * @param[in] stream Stream to close. Required. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws AKERR_IO Or the errno the C library saw. Any buffered data is lost. + * @return NULL on success, an error context otherwise. + * @warning Closing an already-closed stream is undefined behaviour and cannot be + * detected from here -- the FILE * is freed, so even reading it to check + * is the bug. This wrapper cannot help; do not do it. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream); + +/** @} */ + +/* ====================================================================== */ +/** @name Strings + * * Two conventions run through this whole section. * * The copying and concatenating functions take the size of the destination * buffer, even the ones named after libc functions that do not. strcpy(3) and - * strcat(3) cannot be called safely without knowing how much room there is, so - * a wrapper that took the same arguments would be an error-handling wrapper - * around a buffer overflow. dstsize is the whole buffer including the - * terminator -- pair `char buf[64]` with `sizeof(buf)`. Truncation is - * AKERR_OUTOFBOUNDS, and nothing is written when it happens, so ignoring the - * status leaves an empty string rather than a plausible-looking prefix. + * strcat(3) cannot be called safely without knowing how much room there is, so a + * wrapper taking the same arguments would be an error-handling wrapper around a + * buffer overflow. `dstsize` is the whole buffer including the terminator -- + * pair `char buf[64]` with `sizeof(buf)`. Truncation is AKERR_OUTOFBOUNDS and + * nothing is written when it happens, so ignoring the status leaves an empty + * string rather than a plausible-looking prefix. * * The searching functions answer through an out-param, and finding nothing is a - * successful answer of NULL rather than an error. The result points into the - * caller's own string and must not be freed. + * successful answer of NULL. The result points into the caller's own string, so + * it lives exactly as long as that string does and must not be freed. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief strlen(3). + * @param[in] s String to measure. Required. + * @param[out] dest The length, excluding the terminator. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest); + +/** + * @brief strnlen(3): the length, or `maxlen`, whichever is smaller. + * @param[in] s Buffer to measure. Required. + * @param[in] maxlen Upper bound on the scan. + * @param[out] dest The length. A result equal to maxlen means "at least this + * long" rather than "this long", which is the ambiguity that makes + * strnlen the right tool for a buffer that may not be terminated. + * Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest); +/** + * @brief Bounded strcpy(3). + * @param[out] dst Destination. Required, and emptied before the copy. + * @param[in] dstsize Size of `dst` including the terminator. Must be non-zero. + * @param[in] src Source string. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If dstsize is 0. + * @throws AKERR_OUTOFBOUNDS If `src` does not fit. `dst` is left empty rather + * than truncated: a caller who ignores the status gets nothing, which is + * far easier to notice than a plausible prefix. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src); -/* Always terminates, and never NUL-pads to n; strncpy(3) does both the other way. */ + +/** + * @brief Bounded strncpy(3) that always terminates and never NUL-pads. + * + * strncpy(3) does two surprising things this does not: it leaves the destination + * unterminated when the source is at least n bytes long, and it pads the + * remainder with NULs when the source is shorter, turning a short copy into a + * full-length write. + * + * @param[out] dst Destination. Required, and emptied before the copy. + * @param[in] dstsize Size of `dst` including the terminator. Must be non-zero. + * @param[in] src Source string. Required. + * @param[in] n At most this many bytes of `src` are considered. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If dstsize is 0. + * @throws AKERR_OUTOFBOUNDS If the bytes to copy do not fit. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n); + +/** + * @brief Bounded strcat(3). + * @param[in,out] dst Destination, already holding a terminated string. + * Required. + * @param[in] dstsize Size of `dst` including the terminator. Must be + * non-zero. + * @param[in] src String to append. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If dstsize is 0, or if `dst` is not terminated within + * dstsize -- refused rather than walked off the end of looking for a NUL + * that is not there. + * @throws AKERR_OUTOFBOUNDS If the result does not fit. `dst` is unchanged. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src); + +/** + * @brief Bounded strncat(3). + * @param[in,out] dst Destination, already holding a terminated string. + * Required. + * @param[in] dstsize Size of `dst` including the terminator. Must be + * non-zero. + * @param[in] src String to append. Required. + * @param[in] n At most this many bytes of `src` are appended. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If dstsize is 0, or `dst` is not terminated within it. + * @throws AKERR_OUTOFBOUNDS If the result does not fit. `dst` is unchanged. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n); -/* *dest is the caller's, to release with aksl_free or aksl_freep. */ +/** + * @brief strdup(3). + * @param[in] s String to copy. Required. + * @param[out] dest The copy, or NULL on failure. Required. It is yours; release + * it with aksl_free or aksl_freep. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest); + +/** + * @brief strndup(3): a copy of at most `n` bytes, always terminated. + * @param[in] s String to copy. Required. + * @param[in] n Maximum bytes to copy. + * @param[out] dest The copy, or NULL on failure. Required. It is yours. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest); +/** + * @brief strcmp(3). + * @param[in] a First string. Required. + * @param[in] b Second string. Required. + * @param[out] dest Negative, zero or positive as a sorts before, equal to, or + * after b. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest); + +/** + * @brief strncmp(3): compare at most `n` bytes. + * @param[in] a First string. Required. + * @param[in] b Second string. Required. + * @param[in] n Maximum bytes to compare. + * @param[out] dest The ordering. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest); + +/** + * @brief strcasecmp(3): compare ignoring case. + * @param[in] a First string. Required. + * @param[in] b Second string. Required. + * @param[out] dest The ordering. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest); + +/** + * @brief strncasecmp(3): compare at most `n` bytes, ignoring case. + * @param[in] a First string. Required. + * @param[in] b Second string. Required. + * @param[in] n Maximum bytes to compare. + * @param[out] dest The ordering. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest); + +/** + * @brief strcoll(3): compare according to the current locale. + * @param[in] a First string. Required. + * @param[in] b Second string. Required. + * @param[out] dest The ordering. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws EINVAL Or whatever errno strcoll(3) sets. It has no error return of + * its own, so a malformed multibyte sequence in the current locale shows + * up only as errno -- which is why errno is cleared first and consulted + * after. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest); +/** + * @brief strchr(3): the first `c` in `s`. + * @param[in] s String to search. Required. + * @param[in] c Character to find. + * @param[out] dest The match, or NULL if there is none. Required. Not finding it + * is a successful answer, not an error. + * @throws AKERR_NULLPOINTER If s or dest is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest); + +/** + * @brief strrchr(3): the last `c` in `s`. + * @param[in] s String to search. Required. + * @param[in] c Character to find. + * @param[out] dest The match, or NULL if there is none. Required. + * @throws AKERR_NULLPOINTER If s or dest is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest); + +/** + * @brief strstr(3): the first `needle` in `haystack`. + * @param[in] haystack String to search. Required. + * @param[in] needle Substring to find. An empty needle matches at the start. + * Required. + * @param[out] dest The match, or NULL if there is none. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest); + +/** + * @brief The first `needle` in `haystack`, ignoring case. + * + * Open-coded rather than wrapping strcasestr(3), which is a GNU extension in no + * standard, so that it does not depend on _GNU_SOURCE reaching every consumer's + * build. + * + * @param[in] haystack String to search. Required. + * @param[in] needle Substring to find. An empty needle matches at the start. + * Required. + * @param[out] dest The match, or NULL if there is none. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest); + +/** + * @brief strpbrk(3): the first character of `s` that appears in `accept`. + * @param[in] s String to search. Required. + * @param[in] accept Characters to look for. Required. + * @param[out] dest The match, or NULL if there is none. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest); + +/** + * @brief strspn(3): length of the prefix of `s` made only of `accept` characters. + * @param[in] s String to measure. Required. + * @param[in] accept Characters that may appear in the prefix. Required. + * @param[out] dest The length. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest); + +/** + * @brief strcspn(3): length of the prefix of `s` containing no `reject` character. + * @param[in] s String to measure. Required. + * @param[in] reject Characters that end the prefix. Required. + * @param[out] dest The length. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest); -/* - * The reentrant tokeniser only. strtok(3) keeps its state in a hidden static, - * so two interleaved tokenisations corrupt each other silently; it is not - * wrapped. Running out of tokens is success with *dest NULL. +/** + * @brief strtok_r(3): the next token, with the state in the caller's `saveptr`. + * + * The reentrant tokeniser only. strtok(3) keeps its state in a hidden static, so + * two interleaved tokenisations corrupt each other silently and any use from a + * thread is a bug; it is not wrapped at all. + * + * Adjacent delimiters are collapsed, so "a::b" is two tokens. Use aksl_strsep if + * you want the empty field between them. + * + * @param[in] str The string on the first call, NULL on every call after. + * @param[in] delim Delimiter characters. Required. + * @param[in,out] saveptr Tokeniser state, owned by the caller. Required. + * @param[out] dest The token, or NULL when there are none left. Required. + * Running out is how the loop ends, not a fault. + * @throws AKERR_NULLPOINTER If delim, saveptr or dest is NULL. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest); + +/** + * @brief strsep(3): the next field, keeping empty ones. + * + * Differs from aksl_strtok_r in returning the empty field between adjacent + * delimiters, which is what you want for parsing "a::b" as three fields. + * + * @param[in,out] stringp The remaining input, advanced past the field and set to + * NULL when done. Required. + * @param[in] delim Delimiter characters. Required. + * @param[out] dest The field, or NULL when there are none left. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest); -/* - * The message for a status, into the caller's buffer. Knows this library's own - * statuses as well as errno values, which strerror_r(3) could not. +/** + * @brief The message for a status, into the caller's buffer. + * + * Deliberately not strerror_r(3): there are two incompatible functions by that + * name and which one a translation unit gets depends on feature-test macros a + * consumer cannot influence from here. Going through libakerror's registry also + * names this library's own non-errno statuses -- AKERR_NULLPOINTER and the rest -- + * which strerror_r could never do. + * + * @param[in] status Status to describe: an errno value or an AKERR_* one. + * @param[out] buf Destination. Required, and emptied first. + * @param[in] buflen Size of `buf` including the terminator. Must be non-zero. + * @throws AKERR_NULLPOINTER If buf is NULL. + * @throws AKERR_VALUE If buflen is 0. + * @throws AKERR_OUTOFBOUNDS If the message does not fit. `buf` is left empty. + * @return NULL on success, an error context otherwise. A status nothing + * recognises is rendered as its own number, not as an empty string. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen); -/* ---------------------------------------------------------------------- */ -/* Streams -- src/stream.c */ -/* ---------------------------------------------------------------------- */ +/** @} */ -/* +/* ====================================================================== */ +/** @name Streams: everything else + * * Where a function has a genuine "nothing more to read" outcome, that is * AKERR_EOF rather than AKERR_IO, so a read loop can tell the end of its input * from the failure of it. That applies to aksl_fgetc, aksl_fgets, aksl_getline, * aksl_getdelim and aksl_fscanf. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief fseek(3). + * @param[in] stream Stream to reposition. Required. + * @param[in] offset Offset in bytes. + * @param[in] whence SEEK_SET, SEEK_CUR or SEEK_END. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws ESPIPE, EINVAL Or whatever else fseek(3) reports. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence); + +/** + * @brief ftell(3): the current position. + * @param[in] stream Stream to query. Required. + * @param[out] dest The position; 0 on failure. Required -- ftell(3) reports + * failure as -1L, which is also a perfectly ordinary thing for an + * arithmetic type to hold. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ESPIPE Or whatever else ftell(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest); + +/** + * @brief Seek to the start of a stream, reporting failure. + * + * rewind(3) is the one positioning call with no error return at all: it is + * fseek(stream, 0, SEEK_SET) with the result thrown away, and it clears the error + * indicator on the way past so even that evidence is gone. + * + * @param[in] stream Stream to rewind. Required. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws ESPIPE Or whatever else the underlying seek reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream); + +/** + * @brief fseeko(3): fseek with an off_t offset. + * @param[in] stream Stream to reposition. Required. + * @param[in] offset Offset in bytes. + * @param[in] whence SEEK_SET, SEEK_CUR or SEEK_END. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws ESPIPE, EINVAL Or whatever else fseeko(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence); + +/** + * @brief ftello(3): the current position as an off_t. + * @param[in] stream Stream to query. Required. + * @param[out] dest The position; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ESPIPE Or whatever else ftello(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest); + +/** + * @brief fgetpos(3): save an opaque stream position. + * + * Carries an fpos_t rather than a byte offset, which is what makes this the right + * pair for a stream in a multibyte locale where a byte offset is not enough to + * restore the conversion state. + * + * @param[in] stream Stream to query. Required. + * @param[out] pos The saved position. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ESPIPE Or whatever else fgetpos(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos); + +/** + * @brief fsetpos(3): restore a position saved by aksl_fgetpos. + * @param[in] stream Stream to reposition. Required. + * @param[in] pos A position from aksl_fgetpos. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ESPIPE, EINVAL Or whatever else fsetpos(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos); -/* NULL stream means "every output stream", as in fflush(3); it is not an error. */ +/** + * @brief fflush(3). + * @param[in] stream Stream to flush, or **NULL for every output stream** -- which + * is fflush(3)'s own documented behaviour and genuinely useful before + * a fork or an abort, so unlike almost everywhere else in this + * library a NULL argument here is not an error. + * @throws AKERR_IO Or the errno the C library saw. The buffered data is lost. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream); + +/** + * @brief setvbuf(3). + * @param[in] stream Stream to configure. Required. + * @param[in] buf Caller-supplied buffer, or NULL to let stdio allocate one. + * @param[in] mode _IOFBF, _IOLBF or _IONBF. + * @param[in] size Size of `buf`. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws AKERR_VALUE Or the errno the C library saw, if the call is refused. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size); +/** + * @brief fgetc(3), with EOF and error told apart. + * + * fgetc(3) folds three outcomes into one int: a byte, the end of the file, and a + * read error -- the last two both spelled EOF. Split apart, a read loop needs no + * ferror/feof dance at the bottom of it. + * + * @param[in] stream Stream to read from. Required. + * @param[out] dest The byte; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_EOF At the end of the stream. + * @throws AKERR_IO Or the errno the C library saw, on a read error. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest); + +/** + * @brief fputc(3). + * @param[in] c Byte to write. + * @param[in] stream Stream to write to. Required. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws AKERR_IO Or the errno the C library saw. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream); + +/** + * @brief ungetc(3): push one byte back so the next read returns it. + * @param[in] c Byte to push back. + * @param[in] stream Stream to push it onto. Required. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @throws AKERR_IO If the push-back is refused. One byte is all that is + * guaranteed; a second without an intervening read may well fail. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream); + +/** + * @brief fgets(3) into a bounded buffer, reporting the length read. + * @param[out] s Destination. Required, and emptied before the read. + * @param[in] size Size of `s` including the terminator. Must be non-zero and + * within int range, which is what fgets(3) takes. + * @param[in] stream Stream to read from. Required. + * @param[out] len_out Bytes read, newline included if there was one; 0 on + * failure. Required. A full buffer with no trailing newline is how a + * caller spots a line longer than the buffer -- which is a short + * read, not an error: the rest of the line is still in the stream. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If size is 0 or beyond INT_MAX. + * @throws AKERR_EOF At the end of the stream. + * @throws AKERR_IO Or the errno the C library saw, on a read error. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out); + +/** + * @brief fputs(3). + * @param[in] s String to write. Required. + * @param[in] stream Stream to write to. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_IO Or the errno the C library saw. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream); + +/** + * @brief getline(3): a whole line, growing the buffer as needed. + * + * Start with `char *line = NULL; size_t cap = 0;` and release the buffer once + * the loop is done -- not once per line. + * + * @param[in,out] lineptr The buffer, grown as needed. Required. `*lineptr` may + * be NULL on the first call. Release it with aksl_free. + * @param[in,out] n Capacity of `*lineptr`, updated when it grows. Required. + * @param[in] stream Stream to read from. Required. + * @param[out] len_out Bytes read, newline included; 0 on failure. Required. + * This is what distinguishes an embedded NUL from the end of the + * line, which strlen cannot. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_EOF At the end of the stream. + * @throws AKERR_IO Or the errno the C library saw, on a read error. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out); + +/** + * @brief getdelim(3): read up to `delim`, growing the buffer as needed. + * @param[in,out] lineptr The buffer, grown as needed. Required. + * @param[in,out] n Capacity of `*lineptr`, updated when it grows. Required. + * @param[in] delim Byte to stop at. + * @param[in] stream Stream to read from. Required. + * @param[out] len_out Bytes read, delimiter included; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_EOF At the end of the stream. + * @throws AKERR_IO Or the errno the C library saw, on a read error. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim, FILE *stream, size_t *len_out); +/** + * @brief feof(3): whether the end-of-file indicator is set. + * @param[in] stream Stream to query. Required. + * @param[out] dest Non-zero if the indicator is set. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest); + +/** + * @brief ferror(3): whether the error indicator is set. + * @param[in] stream Stream to query. Required. + * @param[out] dest Non-zero if the indicator is set. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest); + +/** + * @brief clearerr(3): clear both the EOF and error indicators. + * @param[in] stream Stream to clear. Required. + * @throws AKERR_NULLPOINTER If stream is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream); + +/** + * @brief fileno(3): the descriptor behind a stream. + * @param[in] stream Stream to query. Required. + * @param[out] dest The descriptor; -1 on failure. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws EBADF If the stream has no descriptor. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest); +/** + * @brief freopen(3): point an existing stream at a different file. + * @param[in] pathname File to open, or **NULL to reopen the same file with a + * new mode** -- the one thing freopen can do that fopen cannot, so + * unlike aksl_fopen's this argument is not checked. + * @param[in] mode fopen mode string. Required. + * @param[in] stream Stream to reuse. Required. + * @param[out] fp The reopened stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If mode, stream or fp is NULL. + * @throws ENOENT, EACCES Or whatever else freopen(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode, FILE *stream, FILE **fp); + +/** + * @brief fdopen(3): wrap an open descriptor in a stream. + * @param[in] fd Descriptor to wrap. Must not be negative. Closing the + * resulting stream closes it. + * @param[in] mode fopen mode string, which must agree with how `fd` was opened. + * Required. + * @param[out] fp The stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If mode or fp is NULL. + * @throws AKERR_VALUE If fd is negative. + * @throws EINVAL, EBADF Or whatever else fdopen(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp); + +/** + * @brief tmpfile(3): an unnamed temporary file, removed when it is closed. + * @param[out] fp The stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If fp is NULL. + * @throws AKERR_IO Or the errno the C library saw. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp); -/* - * The scanf family takes the number of conversions the caller expects, because - * comparing scanf(3)'s return against that number by hand at every call site is - * the check everyone eventually forgets -- and forgetting it leaves the - * unassigned arguments holding whatever they held before. Anything short of - * `expected` is AKERR_VALUE. Pass 0 to opt out and read *assigned yourself. +/** + * @brief sscanf(3), enforcing the number of conversions you expect. + * + * scanf(3) returns how many conversions succeeded, and the caller is expected to + * compare that against the number it wrote in the format string -- by hand, from + * memory, at every call site. Getting it wrong leaves the unassigned arguments + * holding whatever they held before, which for the usual uninitialised local is + * anything at all. + * + * @param[in] str String to parse. Required. + * @param[in] format scanf format string. Required. Checked at compile time. + * @param[in] expected Conversions that must succeed, or 0 to opt out and judge + * `*assigned` yourself. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded, naming both + * counts, or if no conversion was possible at all. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5); + +/** + * @brief fscanf(3), enforcing the number of conversions you expect. + * @param[in] stream Stream to parse. Required. + * @param[in] format scanf format string. Required. Checked at compile time. + * @param[in] expected Conversions that must succeed, or 0 to opt out. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_EOF If the stream ended before any conversion. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded. + * @throws AKERR_IO Or the errno the C library saw, on a read error. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5); + +/** + * @brief scanf(3) from stdin, enforcing the number of conversions you expect. + * @param[in] format scanf format string. Required. Checked at compile time. + * @param[in] expected Conversions that must succeed, or 0 to opt out. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @throws AKERR_NULLPOINTER If format or assigned is NULL. + * @throws AKERR_EOF If stdin ended before any conversion. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(1, 4); + +/** + * @brief scanf(3) from stdin. The va_list form of aksl_scanf. + * @param[in] format scanf format string. Required. + * @param[in] expected Conversions that must succeed, or 0 to opt out. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If format or assigned is NULL. + * @throws AKERR_EOF If stdin ended before any conversion. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected, int *assigned, va_list args); + +/** + * @brief vsscanf(3). The va_list form of aksl_sscanf. + * @param[in] str String to parse. Required. + * @param[in] format scanf format string. Required. + * @param[in] expected Conversions that must succeed, or 0 to opt out. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format, int expected, int *assigned, va_list args); + +/** + * @brief vfscanf(3). The va_list form of aksl_fscanf. + * @param[in] stream Stream to parse. Required. + * @param[in] format scanf format string. Required. + * @param[in] expected Conversions that must succeed, or 0 to opt out. + * @param[out] assigned Conversions that did succeed; 0 on failure. Required. + * @param[in] args Arguments. The caller owns it and must va_end it. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_EOF If the stream ended before any conversion. + * @throws AKERR_VALUE If fewer than `expected` conversions succeeded. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format, int expected, int *assigned, va_list args); +/** + * @brief remove(3): delete a file or an empty directory. + * @param[in] pathname Path to remove. Required. + * @throws AKERR_NULLPOINTER If pathname is NULL. + * @throws ENOENT, EACCES, ENOTEMPTY Or whatever else remove(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname); + +/** + * @brief rename(3). + * @param[in] oldpath Existing path. Required. + * @param[in] newpath New path. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws ENOENT, EACCES, EXDEV Or whatever else rename(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath); -/* - * The template is rewritten in place, so it must be a writable buffer ending in - * six literal X characters -- a string literal is a segfault, and that is - * AKERR_VALUE here rather than a crash. + +/** + * @brief mkstemp(3): create and open a uniquely named temporary file. + * @param[in,out] template_ Path template, **rewritten in place**, so it must be a + * writable buffer ending in six literal X characters. A string + * literal is a segfault. Required. The file is yours, including + * removing it. + * @param[out] fd The open descriptor; -1 on failure. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If the template does not end in six X characters -- + * checked here rather than left to the kernel. + * @throws EACCES, EEXIST Or whatever else mkstemp(3) reports. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd); + +/** + * @brief mkdtemp(3): create a uniquely named temporary directory. + * @param[in,out] template_ Path template, rewritten in place; must end in six + * literal X characters. Required. The directory is yours. + * @throws AKERR_NULLPOINTER If template_ is NULL. + * @throws AKERR_VALUE If the template does not end in six X characters. + * @throws EACCES, EEXIST Or whatever else mkdtemp(3) reports. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_); -// Linked list functions +/** @} */ + +/* ====================================================================== */ +/** @name Linked list + * + * Nothing here allocates. These relink nodes the caller already owns, so a + * caller drawing from a fixed pool can use all of them; aksl_list_free_all is + * the exception and it takes the free function to use. + * + * The bare-node functions take the head by reference wherever the head itself + * can move, which is the case a caller doing it by hand gets wrong. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief Initialise a list node. + * + * Every caller used to have to remember to memset a node before its first use, + * and a stack node that skipped it walked into garbage. + * + * @param[out] node Node to initialise. Required. + * @param[in] data Caller payload, or NULL. Never read by this library. + * @throws AKERR_NULLPOINTER If node is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data); + +/** + * @brief Append a node to the end of a list. O(n). + * @param[in,out] list The list's head. Required. + * @param[in,out] obj Node to append. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. + * @throws AKERR_VALUE If `obj` is already in the list -- relinking it would + * orphan everything between its old position and the tail. + * @return NULL on success, an error context otherwise. + * @see aksl_list_push for the O(1) form, via the aksl_List container. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj); -/* - * `head` is required. Popping the head node has to move the caller's own head - * pointer, and there is no way to do that from a node pointer alone -- the old - * one-argument form left the caller aimed at a detached node. + +/** + * @brief Unlink a node from a list. + * @param[in,out] head The list's head, updated when `node` was the head. + * Required: there is no way to move the caller's head pointer + * from a node pointer alone, and the one-argument form left the + * caller aimed at a detached node. + * @param[in,out] node Node to unlink; its own links are cleared. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node); + +/** + * @brief Call `iter` for each node, head first. + * @param[in] list The list's head. Required. + * @param[in] iter Callback. Required. + * @param[in] data Passed through to every invocation of `iter`. + * @throws AKERR_NULLPOINTER If list or iter is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. Checked before + * any node is visited. + * @return NULL on success -- including when the callback stopped the walk early + * with AKERR_ITERATOR_BREAK, which is a control signal and not a failure. + * Any other status the callback raises propagates out unchanged. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data); -// Tree functions -akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void *leaf); -akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data); - -/* ---------------------------------------------------------------------- */ -/* Collections -- src/collections.c */ -/* ---------------------------------------------------------------------- */ - -/* - * Nothing in this section allocates unless its name says so. The list and tree - * functions relink nodes the caller already owns, and the *_free_all forms take - * the free function to use -- pass NULL for aksl_free -- so a caller drawing - * from a fixed pool can hand over its own. Only aksl_strbuf_* owns memory. - * - * The bare-node list functions take the head by reference wherever the head - * itself can move, which is the case a caller doing it by hand gets wrong. +/** + * @brief Insert a node at the front of a list. + * @param[in,out] head The list's head, updated to `obj`. Required. `*head` may + * be NULL, making a one-node list. + * @param[in,out] obj Node to insert. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If `obj` is already the head. + * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_prepend(aksl_ListNode **head, aksl_ListNode *obj); + +/** + * @brief Insert `obj` directly after `node`. + * @param[in,out] node Node to insert after. Required. + * @param[in,out] obj Node to insert. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If `node` and `obj` are the same node. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_after(aksl_ListNode *node, aksl_ListNode *obj); + +/** + * @brief Insert `obj` directly before `node`. + * @param[in,out] head The list's head, updated when `node` was the head. + * Required for exactly that reason. + * @param[in,out] node Node to insert before. Required. + * @param[in,out] obj Node to insert. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_VALUE If `node` and `obj` are the same node. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_before(aksl_ListNode **head, aksl_ListNode *node, aksl_ListNode *obj); + +/** + * @brief Count the nodes in a list. O(n). + * @param[in] head The list's head, or NULL for an empty list -- which is length + * 0, not an error. + * @param[out] dest The count. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. + * @return NULL on success, an error context otherwise. + * @see aksl_List, whose length is a field rather than a walk. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_length(aksl_ListNode *head, size_t *dest); -/* NULL and success when nothing matches, as everywhere else in this library. */ + +/** + * @brief The first node a predicate accepts. + * @param[in] head The list's head, or NULL for an empty list. + * @param[in] pred Predicate. Required. + * @param[in] data Passed through to every invocation of `pred`. + * @param[out] dest The match, or NULL if nothing matched. Required. Not finding + * one is a successful answer. + * @throws AKERR_NULLPOINTER If pred or dest is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. + * @return NULL on success, or whatever the predicate raised -- the search stops + * there and the error propagates with its message intact. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_find(aksl_ListNode *head, aksl_ListNodePredicate pred, void *data, aksl_ListNode **dest); + +/** + * @brief Reverse a list in place, swapping every node's links. + * @param[in,out] head The list's head, updated to the old tail. Required. + * `*head` may be NULL, which is a no-op. + * @throws AKERR_NULLPOINTER If head is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_reverse(aksl_ListNode **head); + +/** + * @brief Link `other` onto the end of `head`. + * @param[in,out] head The destination list's head. Required. + * @param[in,out] other The list to append, or NULL for a no-op. + * @throws AKERR_NULLPOINTER If head is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If either list contains a cycle. + * @throws AKERR_VALUE If the two lists are the same, or `other` is already + * inside `head` -- either would make a cycle. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_concat(aksl_ListNode *head, aksl_ListNode *other); + +/** + * @brief Release every node in a list and clear the caller's head. + * + * A failure part-way through does not abandon the rest of the list: the first + * error is kept and returned once the walk is finished, so one bad free cannot + * become a leak of everything after it. + * + * @param[in,out] head The list's head, set to NULL. Required. + * @param[in] lfree Deallocator, or NULL for aksl_free. + * @throws AKERR_NULLPOINTER If head is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the list contains a cycle. + * @return NULL on success, or the first error `lfree` raised. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_free_all(aksl_ListNode **head, aksl_FreeFunc lfree); + +/** + * @brief Call `iter` for each node, tail first. + * + * Takes a tail rather than a head because that is what the caller has after a + * walk forwards -- and because a doubly-linked list that cannot be read backwards + * is just a linked list. + * + * @param[in] tail The list's last node. Required. + * @param[in] iter Callback. Required. + * @param[in] data Passed through to every invocation of `iter`. + * @throws AKERR_NULLPOINTER If tail or iter is NULL. + * @throws AKERR_CIRCULAR_REFERENCE If the prev links contain a cycle. + * @return NULL on success, including on AKERR_ITERATOR_BREAK. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail, aksl_ListNodeIterator iter, void *data); -/* The tracked container. push/unshift are O(1); length is a field, not a walk. */ +/** + * @brief Initialise an empty tracked list. + * @param[out] list Container to initialise. Required. + * @throws AKERR_NULLPOINTER If list is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_init(aksl_List *list); + +/** + * @brief Append a node to a tracked list. O(1). + * @param[in,out] list Container. Required. + * @param[in,out] obj Node to append. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_List *list, aksl_ListNode *obj); + +/** + * @brief Insert a node at the front of a tracked list. O(1). + * @param[in,out] list Container. Required. + * @param[in,out] obj Node to insert. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_unshift(aksl_List *list, aksl_ListNode *obj); + +/** + * @brief Unlink a node from a tracked list, keeping head, tail and length right. + * @param[in,out] list Container. Required. + * @param[in,out] node Node to unlink; its own links are cleared. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If `node` is not in this list -- the container's length and + * endpoints would otherwise silently stop describing it. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_remove(aksl_List *list, aksl_ListNode *node); + +/** + * @brief Empty a tracked list, releasing every node. + * @param[in,out] list Container. Required. Emptied whether or not every node + * released cleanly. + * @param[in] lfree Deallocator, or NULL for aksl_free. + * @throws AKERR_NULLPOINTER If list is NULL. + * @return NULL on success, or the first error `lfree` raised. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFunc lfree); -/* - * An unbalanced binary search tree. Inserting already-sorted data gives a +/** @} */ + +/* ====================================================================== */ +/** @name Tree + * + * A traversal that works on any binary tree, plus an unbalanced binary *search* + * tree built on top of it. Inserting already-sorted data into the latter gives a * degenerate chain, which the traversal then refuses past AKSL_TREE_MAX_DEPTH -- * bounded rather than dangerous, but this is not a balanced tree and does not - * claim to be. These are the functions that set and read aksl_TreeNode.parent. + * claim to be. + * + * The insert/remove pair are the functions that set and read + * aksl_TreeNode::parent; nothing else in the library touches it. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief Initialise a tree node. + * @param[out] node Node to initialise. Required. + * @param[in] leaf Caller payload, or NULL. The comparator's operand. + * @throws AKERR_NULLPOINTER If node is NULL. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void *leaf); + +/** + * @brief Walk a tree, calling `iter` on each node. + * + * The callback may stop the walk early by raising AKERR_ITERATOR_BREAK; this + * absorbs that one status and returns success, so an early exit is not an error. + * Any other status propagates out unchanged, with its message and stack trace + * intact. + * + * @param[in] root Root of the tree to walk. Required. + * @param[in] iter Callback. Required. + * @param[in] lalloc Allocator for the internal traversal queue, or NULL for + * aksl_malloc. **Breadth-first modes only**; the depth-first modes + * allocate nothing at all. + * @param[in] lfree Deallocator for what `lalloc` returned, or NULL for + * aksl_free. + * @param[in] searchmode One of the AKSL_TREE_SEARCH_* values. + * @param[in] data Passed through to every invocation of `iter`. + * @throws AKERR_NULLPOINTER If root or iter is NULL. + * @throws AKERR_VALUE On an unrecognised searchmode. This used to return success + * having visited nothing. + * @throws AKERR_OUTOFBOUNDS If the tree is deeper than AKSL_TREE_MAX_DEPTH. + * @throws AKERR_CIRCULAR_REFERENCE If a *depth-first* walk reaches a node that is + * its own ancestor. A breadth-first walk has no ancestor chain to compare + * against, so the same tree comes back as AKERR_OUTOFBOUNDS through the + * depth cap instead. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data); + +/** + * @brief Insert a node into a binary search tree. + * @param[in,out] root Root of the tree, set when the tree was empty. Required. + * @param[in,out] node Node to insert; its links are reset first. Required. + * @param[in] cmp Comparator over the nodes' `leaf` pointers. Required. + * @throws AKERR_NULLPOINTER If any pointer is NULL. + * @throws AKERR_OUTOFBOUNDS If the tree is already AKSL_TREE_MAX_DEPTH deep. + * @return NULL on success, or whatever the comparator raised. + * @note Equal keys go right, so insertion order is preserved among them and a + * duplicate is stored rather than refused. Check with aksl_tree_find first + * if you want uniqueness. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_insert(aksl_TreeNode **root, aksl_TreeNode *node, aksl_TreeCompareFunc cmp); + +/** + * @brief Find the node matching `leaf`. + * @param[in] root Root of the tree, or NULL for an empty one. + * @param[in] leaf The key to look for, as the comparator understands it. + * @param[in] cmp Comparator. Required. + * @param[out] dest The match, or NULL if there is none. Required. Not finding + * one is a successful answer. + * @throws AKERR_NULLPOINTER If cmp or dest is NULL. + * @throws AKERR_OUTOFBOUNDS If the tree is deeper than AKSL_TREE_MAX_DEPTH. + * @return NULL on success, or whatever the comparator raised. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_find(aksl_TreeNode *root, void *leaf, aksl_TreeCompareFunc cmp, aksl_TreeNode **dest); + +/** + * @brief Remove a node from a binary search tree, keeping it ordered. + * + * The removed node's own links are cleared, so it can go straight back into a + * pool or be inserted somewhere else without carrying stale pointers. + * + * @param[in,out] root Root of the tree, updated when the root itself is removed. + * Required. + * @param[in,out] node Node to remove. Required, and must be in this tree. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @throws AKERR_VALUE If the tree is empty. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_remove(aksl_TreeNode **root, aksl_TreeNode *node); + +/** + * @brief The height of a tree in nodes: 0 when empty, 1 for a single node. + * @param[in] root Root of the tree, or NULL. + * @param[out] dest The height. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_OUTOFBOUNDS If the tree is deeper than AKSL_TREE_MAX_DEPTH. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_height(aksl_TreeNode *root, int *dest); + +/** + * @brief The number of nodes in a tree. + * @param[in] root Root of the tree, or NULL. + * @param[out] dest The count. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_OUTOFBOUNDS If the tree is deeper than AKSL_TREE_MAX_DEPTH. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_count(aksl_TreeNode *root, size_t *dest); + +/** + * @brief Release every node in a tree and clear the caller's root. + * + * Post-order, necessarily: a node's children have to be released before the node + * that points at them, and any other order reads freed memory to find the second + * subtree. The first failure is kept and returned at the end rather than + * abandoning the rest of the tree. + * + * @param[in,out] root Root of the tree, set to NULL. Required. + * @param[in] lfree Deallocator, or NULL for aksl_free. + * @throws AKERR_NULLPOINTER If root is NULL. + * @throws AKERR_OUTOFBOUNDS If the tree is deeper than AKSL_TREE_MAX_DEPTH. + * @return NULL on success, or the first error `lfree` raised. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl_FreeFunc lfree); -/* - * FNV-1a alongside djb2. It XORs then multiplies rather than multiplying then - * adding, which mixes the low bits better on short keys that share a prefix -- - * which is what identifiers in a symbol table look like. - */ -akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval); -akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval); +/** @} */ -/* - * Fixed-capacity string-keyed hash map: open addressing, linear probing, the - * caller's slot array, keys copied into the slots so the map owns them. +/* ====================================================================== */ +/** @name Hash map + * + * Fixed-capacity, open-addressed, linear-probing, string-keyed. The caller + * supplies the slot array, keys are copied into fixed-size slots so the map owns + * them, and deletion leaves a tombstone rather than an empty slot -- clearing the + * slot outright would cut the probe chain of anything that hashed past it. * * It refuses rather than resizes when full, deliberately. A table that * reallocates is a table whose entry pointers move underneath anything holding * one; a table that cannot grow has a worst case you can state. The cost is that * the caller sizes it, which is why init takes the array rather than making one. + * @{ */ -#define AKSL_HASHMAP_MAX_KEY 64 -#define AKSL_HASHMAP_SLOT_EMPTY 0 /** Never used; ends a probe chain */ -#define AKSL_HASHMAP_SLOT_OCCUPIED 1 /** Holds a live key and value */ -#define AKSL_HASHMAP_SLOT_DELETED 2 /** Tombstone; reusable, but does not end a chain */ +/* ====================================================================== */ +#define AKSL_HASHMAP_MAX_KEY 64 /**< Longest key, terminator included */ +#define AKSL_HASHMAP_SLOT_EMPTY 0 /**< Never used; ends a probe chain */ +#define AKSL_HASHMAP_SLOT_OCCUPIED 1 /**< Holds a live key and value */ +#define AKSL_HASHMAP_SLOT_DELETED 2 /**< Tombstone; reusable, but does not end a chain */ + +/** @brief One slot in an aksl_HashMap. Allocate an array of these and hand it to aksl_hashmap_init. */ typedef struct aksl_HashEntry { - char key[AKSL_HASHMAP_MAX_KEY]; - void *value; - uint8_t state; + char key[AKSL_HASHMAP_MAX_KEY]; /**< The key, copied in; the map owns it. */ + void *value; /**< Caller payload. */ + uint8_t state; /**< One of AKSL_HASHMAP_SLOT_*. */ } aksl_HashEntry; +/** @brief A fixed-capacity string-keyed hash map over a caller-supplied slot array. */ typedef struct aksl_HashMap { - aksl_HashEntry *slots; - size_t capacity; - size_t count; + aksl_HashEntry *slots; /**< The caller's slot array. */ + size_t capacity; /**< Number of slots. */ + size_t count; /**< Live entries, tombstones excluded. */ } aksl_HashMap; +/** + * @brief Initialise a map over a caller-supplied slot array. + * @param[out] map Map to initialise. Required. + * @param[out] slots Slot array, which the caller owns and must keep alive for + * as long as the map. Required, and cleared here. + * @param[in] capacity Number of slots. Must be non-zero. + * @throws AKERR_NULLPOINTER If map or slots is NULL. + * @throws AKERR_VALUE If capacity is 0. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_init(aksl_HashMap *map, aksl_HashEntry *slots, size_t capacity); + +/** + * @brief Insert a key, or replace an existing key's value. + * @param[in,out] map Map. Required, and initialised. + * @param[in] key Key, copied into the map. Required. + * @param[in] value Caller payload, which may be NULL. + * @throws AKERR_NULLPOINTER If map or key is NULL, or the map is uninitialised. + * @throws AKERR_OUTOFBOUNDS If the key is at least AKSL_HASHMAP_MAX_KEY bytes -- + * refused rather than truncated, since a truncated key would silently + * collide with a different one sharing its prefix. Also if the map is + * full, naming the capacity it hit. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_put(aksl_HashMap *map, const char *key, void *value); -/* A missing key is *found = 0 and success; looking and not finding is not an error. */ + +/** + * @brief Look a key up. + * @param[in] map Map. Required, and initialised. + * @param[in] key Key to find. Required. + * @param[out] value The value, written only when the key is present. May be NULL + * if you only want to know whether the key is there. + * @param[out] found Non-zero when the key was present. Required. + * @throws AKERR_NULLPOINTER If map, key or found is NULL, or the map is + * uninitialised. + * @throws AKERR_OUTOFBOUNDS If the key is too long to be in the map at all. + * @return NULL on success. A key that is not there is `*found = 0` and success, + * not an error: looking something up and not finding it is the ordinary + * case in a symbol table. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_get(aksl_HashMap *map, const char *key, void **value, int *found); + +/** + * @brief Remove a key, leaving a tombstone. + * @param[in,out] map Map. Required, and initialised. + * @param[in] key Key to remove. Required. + * @param[out] removed Non-zero when the key was present. Optional. + * @throws AKERR_NULLPOINTER If map or key is NULL, or the map is uninitialised. + * @throws AKERR_OUTOFBOUNDS If the key is too long to be in the map at all. + * @return NULL on success. Removing a key that is not there is success with + * `*removed = 0`. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_remove(aksl_HashMap *map, const char *key, int *removed); + +/** + * @brief Call `iter` for each live entry. + * @param[in] map Map. Required, and initialised. + * @param[in] iter Callback. Required. + * @param[in] data Passed through to every invocation of `iter`. + * @throws AKERR_NULLPOINTER If map or iter is NULL, or the map is uninitialised. + * @return NULL on success, including on AKERR_ITERATOR_BREAK. + * @note Entries are visited in slot order, which is to say in no order a caller + * can predict or should rely on. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map, aksl_HashMapIterator iter, void *data); -/* - * Growable string buffer -- the one thing here that owns memory, which is what - * makes the bounded formatting wrappers usable when the output length is not - * known in advance. Capacity doubles, so n appends cost O(n) amortised, and the - * contents are always NUL-terminated so aksl_strbuf_cstr is valid at any point. +/** + * @brief FNV-1a hash over an explicit length. + * + * Alongside djb2 rather than instead of it: FNV-1a XORs then multiplies rather + * than multiplying then adding, which mixes the low bits better on short keys + * that share a prefix -- which is what identifiers in a symbol table look like. + * + * @param[in] str Bytes to hash. Required. + * @param[in] len Number of bytes. 0 yields the offset basis, 2166136261. + * @param[out] hashval The hash. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval); + +/** + * @brief FNV-1a hash of a NUL-terminated string. + * @param[in] str String to hash. Required. + * @param[out] hashval The hash. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval); + +/** @} */ + +/* ====================================================================== */ +/** @name Growable string buffer + * + * The one thing in this library that owns memory. The bounded formatting + * wrappers are the right answer when the destination is a fixed buffer and no + * answer at all when the output length is not known in advance; this is that + * answer. + * + * Capacity doubles, so n appends cost O(n) amortised. The contents are always + * NUL-terminated, so aksl_strbuf_cstr is valid at any point without a finalise + * step. + * @{ + */ +/* ====================================================================== */ + +/** @brief A growable, always-terminated character buffer. Initialise with aksl_strbuf_init. */ typedef struct aksl_StrBuf { - char *data; - size_t length; /** Bytes in use, terminator not counted */ - size_t capacity; /** Bytes allocated, terminator included */ + char *data; /**< The characters. NULL before init and after free. */ + size_t length; /**< Bytes in use, terminator not counted. */ + size_t capacity; /**< Bytes allocated, terminator included. */ } aksl_StrBuf; +/** + * @brief Allocate a string buffer. + * @param[out] buf Buffer to initialise. Required. + * @param[in] initial Requested capacity in bytes. Raised to a small minimum if + * it is lower; 0 is fine and means "you decide". + * @throws AKERR_NULLPOINTER If buf is NULL. + * @throws ENOMEM If the allocation fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_init(aksl_StrBuf *buf, size_t initial); + +/** + * @brief Append a NUL-terminated string. + * @param[in,out] buf Buffer. Required, and initialised. + * @param[in] s String to append. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL, or the buffer is + * uninitialised. + * @throws AKERR_OUTOFBOUNDS If the required capacity overflows size_t. + * @throws ENOMEM If growing the buffer fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append(aksl_StrBuf *buf, const char *s); + +/** + * @brief Append `n` bytes, which may include NULs. + * @param[in,out] buf Buffer. Required, and initialised. + * @param[in] s Bytes to append. Required. + * @param[in] n Number of bytes. + * @throws AKERR_NULLPOINTER If either pointer is NULL, or the buffer is + * uninitialised. + * @throws AKERR_OUTOFBOUNDS If the required capacity overflows size_t. + * @throws ENOMEM If growing the buffer fails. + * @return NULL on success, an error context otherwise. + * @note An embedded NUL is stored and counted in `length`, but the C string view + * from aksl_strbuf_cstr stops at the first one. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_bytes(aksl_StrBuf *buf, const char *s, size_t n); + +/** + * @brief Append one character. + * @param[in,out] buf Buffer. Required, and initialised. + * @param[in] c Character to append. + * @throws AKERR_NULLPOINTER If buf is NULL or uninitialised. + * @throws ENOMEM If growing the buffer fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_char(aksl_StrBuf *buf, char c); + +/** + * @brief Append formatted output, growing the buffer to fit. + * + * Unlike aksl_snprintf into a fixed array, a long result grows the destination + * rather than being truncation. + * + * @param[in,out] buf Buffer. Required, and initialised. + * @param[in] format printf format string. Required. Checked at compile time. + * @throws AKERR_NULLPOINTER If either pointer is NULL, or the buffer is + * uninitialised. + * @throws AKERR_IO If the arguments cannot be formatted. + * @throws ENOMEM If growing the buffer fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_appendf(aksl_StrBuf *buf, const char *format, ...) AKSL_PRINTF_FORMAT(2, 3); + +/** + * @brief Append formatted output. The va_list form of aksl_strbuf_appendf. + * @param[in,out] buf Buffer. Required, and initialised. + * @param[in] format printf format string. Required. + * @param[in] args Arguments. The caller owns it and must va_end it; this + * copies it internally rather than consuming it twice. + * @throws AKERR_NULLPOINTER If either pointer is NULL, or the buffer is + * uninitialised. + * @throws AKERR_IO If the arguments cannot be formatted. + * @throws ENOMEM If growing the buffer fails. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_vappendf(aksl_StrBuf *buf, const char *format, va_list args); + +/** + * @brief Empty a buffer without releasing its storage, so the capacity is reused. + * @param[in,out] buf Buffer. Required, and initialised. + * @throws AKERR_NULLPOINTER If buf is NULL or uninitialised. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_reset(aksl_StrBuf *buf); -/* Points into the buffer; the next append invalidates it. */ + +/** + * @brief The buffer's contents as a C string. + * @param[in] buf Buffer. Required, and initialised. + * @param[out] dest The contents. Required. + * @throws AKERR_NULLPOINTER If either pointer is NULL, or the buffer is + * uninitialised. + * @return NULL on success, an error context otherwise. + * @warning `*dest` points into the buffer, so the next append invalidates it. + * Copy it with aksl_strdup if it has to outlive one. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_cstr(aksl_StrBuf *buf, const char **dest); + +/** + * @brief Release a buffer's storage and zero it. + * + * Leaves the buffer in the state that makes a second free an error rather than a + * double free, exactly as aksl_freep arranges for a bare pointer. + * + * @param[in,out] buf Buffer. Required, and initialised. + * @throws AKERR_NULLPOINTER If buf is NULL or already released. + * @return NULL on success, an error context otherwise. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_free(aksl_StrBuf *buf); +/** @} */ + #ifdef __cplusplus } #endif diff --git a/src/stdlib.c b/src/stdlib.c index 691edc8..ca16ccc 100644 --- a/src/stdlib.c +++ b/src/stdlib.c @@ -116,6 +116,69 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size) SUCCEED_RETURN(e); } +/* + * reallocarray(3): a resize whose element count and element size are multiplied + * *with an overflow check*. `realloc(p, n * size)` is a heap overflow waiting for + * an n large enough to wrap, and the multiplication is exactly the place a + * caller does not think to look. Not every platform has it, so the check is done + * here rather than delegated. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_reallocarray(void **ptr, size_t nmemb, size_t size) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p", (void *)ptr); + FAIL_ZERO_RETURN(e, nmemb, AKERR_VALUE, "zero-size realloc (nmemb=0)"); + FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size realloc (size=0)"); + FAIL_NONZERO_RETURN(e, (nmemb > (size_t)-1 / size), AKERR_OUTOFBOUNDS, + "%zu x %zu overflows size_t", nmemb, size); + return aksl_realloc(ptr, nmemb * size); +} + +/* + * Aligned allocation. aligned_alloc(3) requires size to be a multiple of the + * alignment and the alignment to be a power of two supported by the + * implementation; violating either is undefined behaviour that usually just + * returns NULL, so both are checked here and reported as what they are. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_aligned_alloc(size_t alignment, size_t size, void **dst) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); + *dst = NULL; + FAIL_ZERO_RETURN(e, alignment, AKERR_VALUE, "alignment=0"); + FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation"); + FAIL_NONZERO_RETURN(e, ((alignment & (alignment - 1)) != 0), AKERR_VALUE, + "alignment %zu is not a power of two", alignment); + FAIL_NONZERO_RETURN(e, (size % alignment != 0), AKERR_VALUE, + "size %zu is not a multiple of alignment %zu", size, alignment); + errno = 0; + *dst = aligned_alloc(alignment, size); + FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), + "%zu bytes aligned to %zu", size, alignment); + SUCCEED_RETURN(e); +} + +/* + * posix_memalign(3) is the older interface, and the one that does not require + * size to be a multiple of the alignment. It also breaks the errno convention -- + * it *returns* the error number and leaves errno alone -- which is precisely the + * kind of local irregularity a caller mis-handles once and never notices. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_posix_memalign(void **dst, size_t alignment, size_t size) +{ + int failed = 0; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst); + *dst = NULL; + FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation"); + failed = posix_memalign(dst, alignment, size); + if ( failed != 0 ) { + *dst = NULL; + FAIL_RETURN(e, failed, "%zu bytes aligned to %zu", size, alignment); + } + SUCCEED_RETURN(e); +} + /* * free(NULL) is legal C and does nothing. This treats it as an error anyway, * deliberately and against libc: in a codebase that routes every allocation @@ -454,6 +517,68 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, return raised; } +/* + * The allocating form: no buffer, no size, no truncation to worry about. Where + * aksl_snprintf is right for a fixed destination, this is right when the length + * is not knowable in advance and the result is short-lived enough not to want a + * whole aksl_StrBuf. + * + * asprintf(3) is a GNU extension, so it is built here on the two-pass vsnprintf + * measure-then-write rather than called, which keeps this off _GNU_SOURCE. *dest + * is the caller's to release with aksl_free. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const char *restrict format, va_list args) +{ + va_list measure; + int needed = 0; + char *buf = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", + (void *)count, (void *)dest, (void *)format); + *count = 0; + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", + (void *)count, (void *)dest, (void *)format); + *dest = NULL; + FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p", + (void *)count, (void *)dest, (void *)format); + va_copy(measure, args); + needed = vsnprintf(NULL, 0, format, measure); + va_end(measure); + FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "could not format the arguments"); + ATTEMPT { + CATCH(e, aksl_malloc((size_t)needed + 1, (void **)&buf)); + /* + * Cannot truncate -- the buffer was sized from this same format and + * arguments -- but the return is checked anyway, because "cannot happen" + * is a claim about the line above rather than about vsnprintf. + */ + FAIL_NONZERO_BREAK(e, (vsnprintf(buf, (size_t)needed + 1, format, args) != needed), + AKERR_IO, "formatted output changed length between passes"); + *dest = buf; + *count = needed; + } CLEANUP { + if ( *dest == NULL && buf != NULL ) { + akerr_ErrorContext *released = aksl_free(buf); + if ( released != NULL ) { + released = akerr_release_error(released); + } + } + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const char *restrict format, ...) +{ + va_list args; + akerr_ErrorContext *raised = NULL; + + va_start(args, format); + raised = aksl_vasprintf(count, dest, format, args); + va_end(args); + return raised; +} + /* * String to number. * @@ -893,6 +1018,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void SUCCEED_RETURN(e); } +/** @cond INTERNAL */ /* * One entry in the breadth-first traversal queue. Internal, and deliberately not * aksl_ListNode: the walk needs to carry each node's depth alongside it, which is @@ -905,6 +1031,7 @@ typedef struct TreeQueueEntry { aksl_TreeNode *node; int depth; } TreeQueueEntry; +/** @endcond */ /* Append one tree node to the tail of the traversal queue. */ static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_push( @@ -1101,31 +1228,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk( SUCCEED_RETURN(e); } -/** - * @brief Iterate a binary tree in breadth-first or depth-first order, calling a callback on each node. - * - * The callback may stop the walk early by raising AKERR_ITERATOR_BREAK; this - * function absorbs that one status and returns success, so an early exit is not - * an error to the caller. Any other status the callback raises propagates out - * unchanged, with its message and stack trace intact. - * - * Traversal is bounded. A tree deeper than AKSL_TREE_MAX_DEPTH raises - * AKERR_OUTOFBOUNDS rather than overflowing the stack, and a depth-first walk - * over a tree whose child points back at one of its own ancestors raises - * AKERR_CIRCULAR_REFERENCE rather than recursing forever. - * - * @param[in] root The root of the tree to walk - * @param[in] iter An aksl_TreeNodeIterator called once for each node visited - * @param[in] lalloc An aksl_AllocFunc used to allocate the internal traversal queue, or NULL for aksl_malloc. Breadth-first modes only; the depth-first modes allocate nothing. - * @param[in] lfree An aksl_FreeFunc used to release what lalloc returned, or NULL for aksl_free - * @param[in] searchmode One of the AKSL_TREE_SEARCH_* defines - * @param[in] data Caller data passed through to every invocation of iter - * - * @throws AKERR_NULLPOINTER On NULL root or iter - * @throws AKERR_VALUE On an unrecognised searchmode - * @throws AKERR_OUTOFBOUNDS When the tree is deeper than AKSL_TREE_MAX_DEPTH - * @throws AKERR_CIRCULAR_REFERENCE When a depth-first walk reaches a node that is its own ancestor - * @return akerr_ErrorContext +/* + * Public entry point for every tree walk. The contract -- every status, every + * argument -- is documented on the declaration in include/akstdlib.h; what is + * here is why the code looks the way it does. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate( aksl_TreeNode *root, @@ -1198,17 +1304,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate( SUCCEED_RETURN(e); } -/** - * @brief Iterates over a linked list and execute a callback function on each node - * - * @param[in] list The linked list to iterate - * @param[in] iter An aksl_ListNodeIterator function which will be called for each node found - * @param[in] data Any user data that should be provided when the iterator is called - * - * @throws AKERR_NULLPOINTER on null pointer inputs - * @throws AKERR_CIRCULAR_REFERENCE when the linked list contains a circular reference - * - * @return akerr_ErrorContext +/* + * Contract documented on the declaration in include/akstdlib.h. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data) { diff --git a/src/stream.c b/src/stream.c index ee7e1c7..4d057cf 100644 --- a/src/stream.c +++ b/src/stream.c @@ -485,6 +485,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format, return raised; } +/* stdin, for symmetry with aksl_printf writing to stdout. */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format, + int expected, int *assigned, ...) +{ + va_list args; + akerr_ErrorContext *raised = NULL; + + va_start(args, assigned); + raised = aksl_vfscanf(stdin, format, expected, assigned, args); + va_end(args); + return raised; +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected, + int *assigned, va_list args) +{ + return aksl_vfscanf(stdin, format, expected, assigned, args); +} + /* ---------------------------------------------------------------------- */ /* Files */ /* ---------------------------------------------------------------------- */ diff --git a/tests/aksl_capture.h b/tests/aksl_capture.h index 91eefec..507a041 100644 --- a/tests/aksl_capture.h +++ b/tests/aksl_capture.h @@ -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 * AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_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 @@ -184,6 +193,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e) aksl_last_message[0] = '\0'; aksl_last_function[0] = '\0'; + aksl_last_file[0] = '\0'; + aksl_last_line = 0; if ( e == NULL ) { aksl_last_status = 0; return 0; @@ -191,6 +202,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e) aksl_last_status = e->status; 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_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 * 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_message[0] = '\0'; \ aksl_last_function[0] = '\0'; \ + aksl_last_file[0] = '\0'; \ + aksl_last_line = 0; \ } while ( 0 ) #define AKSL_CHECK_CONTAINS(needle) \ diff --git a/tests/negative/format_mismatch.c b/tests/negative/format_mismatch.c new file mode 100644 index 0000000..4d933b8 --- /dev/null +++ b/tests/negative/format_mismatch.c @@ -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 + +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; +} diff --git a/tests/negative/noignore.c b/tests/negative/noignore.c new file mode 100644 index 0000000..0c67890 --- /dev/null +++ b/tests/negative/noignore.c @@ -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 + +int main(void) +{ + void *ptr = NULL; + + /* Discarding the returned context. This is the line that must not build. */ + aksl_malloc(32, &ptr); + + return 0; +} diff --git a/tests/test_collections.c b/tests/test_collections.c index b362f63..9677d2e 100644 --- a/tests/test_collections.c +++ b/tests/test_collections.c @@ -149,6 +149,29 @@ static int test_insert_after_and_before(void) 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 */ /* ---------------------------------------------------------------------- */ @@ -177,6 +200,7 @@ static int test_find_returns_the_first_match_or_null(void) { aksl_ListNode node[N]; aksl_ListNode *found = (aksl_ListNode *)0x1; + aksl_ListNode *cyclic = NULL; int payload = 42; 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), 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], &match_data, NULL, NULL), AKERR_NULLPOINTER); return 0; @@ -336,6 +373,257 @@ static int test_free_all_releases_every_node(void) 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 */ /* ---------------------------------------------------------------------- */ @@ -619,6 +907,99 @@ static int test_tree_remove_all_three_cases(void) 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. */ 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_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_find_returns_the_first_match_or_null); AKSL_RUN(failures, test_reverse_flips_both_directions); AKSL_RUN(failures, test_concat_joins_two_lists); 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_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_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_find); 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_height_and_count); AKSL_RUN(failures, test_tree_free_all); diff --git a/tests/test_format.c b/tests/test_format.c index d5e2a8a..28f4b8c 100644 --- a/tests/test_format.c +++ b/tests/test_format.c @@ -224,6 +224,51 @@ static int test_printf_rejects_null_arguments(void) 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 * 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_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_variadic_wrappers_survive_repeated_calls); diff --git a/tests/test_memory.c b/tests/test_memory.c index e272300..e3f4e66 100644 --- a/tests/test_memory.c +++ b/tests/test_memory.c @@ -121,6 +121,84 @@ static int test_realloc_grows_and_preserves_the_old_block_on_failure(void) 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) { 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_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_free_rejects_null_pointer); diff --git a/tests/test_pool.c b/tests/test_pool.c new file mode 100644 index 0000000..4509be8 --- /dev/null +++ b/tests/test_pool.c @@ -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 +#include + +/* 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); +} diff --git a/tests/test_streamio.c b/tests/test_streamio.c index 4aecb6b..26e9366 100644 --- a/tests/test_streamio.c +++ b/tests/test_streamio.c @@ -520,6 +520,53 @@ static int test_fscanf_reads_from_a_stream(void) 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 */ /* ---------------------------------------------------------------------- */ @@ -603,6 +650,8 @@ int main(void) AKSL_RUN(failures, test_sscanf_enforces_the_expected_count); 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_mkstemp_and_mkdtemp); diff --git a/tests/test_strto.c b/tests/test_strto.c index 031b354..ea98800 100644 --- a/tests/test_strto.c +++ b/tests/test_strto.c @@ -111,6 +111,53 @@ static int test_no_digits_is_an_error_even_with_an_endptr(void) 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. */ 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_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_endptr_chains_across_a_list);