38 Commits

Author SHA1 Message Date
0620370dd9 Remove unused snprintf count parameter
Some checks are pending
libakstdlib CI Build / coverage (push) Waiting to run
libakstdlib CI Build / mutation_test (push) Waiting to run
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / sanitizers (push) Successful in 2m56s
2026-08-03 16:33:09 -04:00
83ff77608f Address directory wrapper review
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m58s
libakstdlib CI Build / sanitizers (push) Successful in 2m57s
libakstdlib CI Build / coverage (push) Successful in 2m47s
libakstdlib CI Build / mutation_test (push) Successful in 13m26s
2026-08-03 15:00:56 -04:00
d5e5e95c61 Add directory stream wrappers
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m47s
libakstdlib CI Build / sanitizers (push) Successful in 2m54s
libakstdlib CI Build / cmake_build (push) Successful in 2m59s
libakstdlib CI Build / mutation_test (push) Successful in 12m31s
2026-08-03 13:45:59 -04:00
2b79aca103 Merge pull request 'Wrap file metadata calls' (#31) from 9 into main
All checks were successful
libakstdlib CI Build / sanitizers (push) Successful in 2m51s
libakstdlib CI Build / cmake_build (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m52s
libakstdlib CI Build / mutation_test (push) Successful in 12m4s
Reviewed-on: #31
2026-08-03 11:05:43 -04:00
15e9104d9e Test file metadata failure paths
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m51s
libakstdlib CI Build / cmake_build (push) Successful in 2m59s
libakstdlib CI Build / sanitizers (push) Successful in 3m4s
libakstdlib CI Build / mutation_test (push) Successful in 12m14s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 10:43:47 -04:00
01034fc668 Document file metadata wrapper behavior
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m44s
libakstdlib CI Build / cmake_build (push) Successful in 2m56s
libakstdlib CI Build / sanitizers (push) Successful in 3m1s
libakstdlib CI Build / mutation_test (push) Successful in 12m58s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 09:02:19 -04:00
8c94231167 Wrap file metadata calls
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 3m2s
libakstdlib CI Build / sanitizers (push) Successful in 3m3s
libakstdlib CI Build / coverage (push) Successful in 3m1s
libakstdlib CI Build / mutation_test (push) Successful in 12m46s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 07:34:16 -04:00
be725f8cf2 Merge pull request 'Document libc wrapper contract' (#30) from 9 into main
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m53s
libakstdlib CI Build / sanitizers (push) Successful in 3m3s
libakstdlib CI Build / coverage (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 11m59s
Reviewed-on: #30
2026-08-03 06:53:05 -04:00
58f426abce Document libc wrapper contract
All checks were successful
libakstdlib CI Build / coverage (push) Successful in 2m47s
libakstdlib CI Build / sanitizers (push) Successful in 2m57s
libakstdlib CI Build / cmake_build (push) Successful in 3m15s
libakstdlib CI Build / mutation_test (push) Successful in 12m35s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 06:38:21 -04:00
55d986c631 Drop the TODO.md section numbers from 26 files
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 5m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 12m39s
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a
numbering the file had already abandoned before the move to the tracker. They
label completed work, so the pointer was the only wrong part.

The citation is removed and the sentence kept, which is what issue #27
recommended: these are labels, not references, and a label carrying a
version-dependent pointer goes stale again at the next reorganisation. Where a
pointer earns its place it names what actually holds the content now --
UPGRADING.md for the confirmed defects, libakerror #15 for the target
namespacing, issue #7 for the mutation survivors.

README.md and akstdlib.h sent readers to TODO.md for 'what is still open';
they name the tracker.

Verified: cmake --build build && ctest --test-dir build, 19/19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:27 -04:00
d6a1cd8ca8 Merge pull request 'Move outstanding work from TODO.md into the issue tracker' (#28) from todo-to-issues into main
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m56s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 14m49s
Reviewed-on: #28
2026-08-02 19:47:21 -04:00
c95f8e9770 Point AGENTS.md at the issue tracker for outstanding work
The agent instructions now say where new work goes -- an issue on the forge,
labelled by kind and blast radius and carrying status::grooming until its scope
is settled -- and that TODO.md is the record of where the library stands, what
is deliberately not wrapped, and which uncovered lines are uncoverable rather
than untested.

Also: a defect in a dependency is filed against that dependency. Three defects
in libakerror that cost this library a workaround each sat in this repository's
own notes without anybody upstream being able to see them, which is how a
workaround gets written once per consumer.

The KNOWN_FAILING_TESTS and pull-request rules now name the issue rather than a
TODO entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 19:24:45 -04:00
22208f0aa0 Correct the error-pool entry: the pin is 1.0.0, upstream locks it
The largest item in the file said the error pool has no synchronisation of any
kind and that libakstdlib is therefore permanently single-threaded pending a
decision upstream. That is true of deps/libakerror as pinned -- 1.0.0, zero
occurrences of akerr_mutex_lock, no lock.h, no thread tests -- and false of
libakerror 2.0.1, which holds one recursive lock over the pool and the status
registry and ships docs/thread-safety.md.

libakgl and akbasic both pin 2.0.1 already.

Issue #2 is retitled and rescoped to the submodule bump and the concurrent test
that has to prove it, rather than a wait on somebody else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 18:57:39 -04:00
8a76b88648 Move outstanding work from TODO.md into the issue tracker
Twenty-six issues on source.starfort.tech/andrew/libakstdlib, labelled by kind
and blast radius and milestoned by what they can land in: 0.2.x for anything
that breaks no ABI, 0.3.0 for new public symbols, 1.0.0 for the large surfaces.
Everything filed carries status::grooming.

The four items blocked on libakerror are filed in both places -- the fix is
there and the bill is here -- and TODO.md keeps a table saying which is which.

What stays in TODO.md is what a tracker has no place for: where the library
stands, why six libc functions are deliberately not wrapped, which eight
uncovered lines are uncoverable rather than untested, and the akbasic call
counts that prioritised everything built since. 377 lines to 189.

Filed alongside: 25 file headers in tests/ and src/ cite TODO.md section
numbers from a numbering the file itself had already abandoned. They label
completed work, so nothing about the code is misleading -- only the pointer is.
Left for its own commit rather than churning 25 files here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 18:55:34 -04:00
c0c18f5a6a Record that there is no directory-reading wrapper
opendir/readdir/closedir have no aksl_* counterpart, so a consumer that
wants to list a directory has to call them itself and report through errno
in a file where everything else reports through an akerr_ErrorContext *.

Found from akbasic, which needs it for Commodore BASIC's DIRECTORY verb and
refuses the verb rather than working around the gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 21:50:07 -04:00
c0a1c18c87 Gate mutation testing on a measured score, not an inherited one
The threshold had been 80 against src/stdlib.c alone. There are three
more sources now and nobody had measured them, so that number was a
guess carried forward.

Measured: 72.3%, 188 of a 260-mutant sample from the 1701 the four
sources generate. Gate set to 65 -- a ratchet with headroom for the
runner and for the sample shifting as sources change, not a target.

A sample rather than the whole set, because 1701 rebuilds and test runs
is hours. --max-mutants samples by even index rather than at random, so
the same 260 run every time and the gate stays reproducible; sampling
all four files beats exhausting one of them, which is what this job did
before.

72.3% against the 89.6% reported at 0.1.0 is a change in denominator,
not a regression in the tests. That figure covered one 561-line file;
this covers four totalling 1716 lines, and most of the added surface is
argument validation whose mutants are frequently *equivalent* -- 12 of
the 72 survivors are `errno = 0` deleted from a wrapper whose libc call
always sets errno, which no test that could be written would catch. The
README breaks all 72 down and says which are worth acting on; TODO.md
2.4 carries the three clusters that are.

Two of them were real and are fixed here and in the previous commit: the
right child's `depth + 1` in the depth-first walk, and aksl_tree_remove
on an empty tree, which without its guard dereferences NULL. Neither had
a test; both do now. That is what the harness is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:15:08 -04:00
602759e491 Cap the depth of a right-leaning tree too
Mutation testing found it: changing the right child's `depth + 1` to
`depth + 0` in the depth-first recursion survived the entire suite. The
depth cap was only ever tested against a chain that leans left, so
nothing said whether the right-hand descent counted at all -- a tree
that goes right for a million nodes would have recursed until the
process died, which is precisely what AKSL_TREE_MAX_DEPTH exists to
prevent.

The right-leaning chain is now tested in all three depth-first orders
and breadth-first, which carries its depth on the queue entry instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:09:04 -04:00
90134ca0f6 Test aksl_vscanf, and drop a check that could not fail
Coverage caught both. aksl_vscanf was declared, documented and never
called by anything, which is what 100% function coverage is for -- it is
the only metric here that notices a whole function nobody exercises.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:07:25 -04:00
8c1746aba6 Test the installed package, not just the build tree
Found by building a consumer against a fresh install: an installed
libakstdlib was not usable. akstdlibConfig.cmake calls
find_dependency(akerror), and the top-level build pulls the submodule in
EXCLUDE_FROM_ALL, so `cmake --install` on this project installs only
this project and leaves that dependency unresolvable.

CI had been hiding it by installing libakerror@main in a separate step
-- a different libakerror from the one actually compiled, which is the
inconsistency TODO.md 2.3 recorded and the previous commit removed.
Removing it exposed the real gap.

CI now installs deps/libakerror, the same commit the top-level build
compiles, so there is one libakerror in play and the install is
consumable.

tests/consumer/ is the check that would have caught it: a standalone
project, configured against CMAKE_PREFIX_PATH rather than as part of
this build, because being part of this build is exactly what would let
it pass without testing anything. It exercises what only an install has
-- find_package with a version request against the generated version
file, find_dependency(akerror) resolving, and the exported
akstdlib::akstdlib target -- and touches one function from each of the
four sources, so a library installed with a source file missing from its
link line fails there rather than in the next consumer to find it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:05:09 -04:00
b22de3e34c Reindent to the canonical stroustrup style
Whitespace only -- `git diff -w` is empty. TODO.md 2.3 recorded that the
tree mixed hard tabs and space indents within the same functions;
tests/aksl_capture.h was the worst of it, with the assertion macros
space-indented and everything around them tabbed.

The style is the one libakerror standardised on and the one AGENTS.md
already describes: Emacs cc-mode "stroustrup", c-basic-offset 4,
indent-tabs-mode on, tab-width 8. A correct file is a fixed point of
indent-region under those settings, and the tree is one now -- a second
pass produces no diff.

scripts/reindent.el is that pass, checked in so "correct" is something
you can run rather than something you have to remember. It is not
clang-format and deliberately so: AGENTS.md forbids introducing one
without discussion, and this is a good illustration of why. cc-mode
indents every declaration in akstdlib.h one level for the extern "C" { }
wrapping the file body, so the script sets inextern-lang to 0 -- a
cc-mode offset with no clang-format equivalent, and without it the first
run moved 364 lines in the wrong direction.

Its own commit, no behaviour change, and the suite is green either side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:02:55 -04:00
a2ec96e88f Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.

The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.

Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.

Section 1.9, the cross-cutting tests:

  tests/test_pool.c    drives every failure path AKERR_MAX_ARRAY_ERROR
                       + 10 times and checks the pool after each round,
                       because a wrapper that leaks a slot fails a
                       hundred calls later in unrelated code. It also
                       asserts that each error names the function and
                       file it was raised from, which is what catches a
                       FAIL that migrates into a helper during a
                       refactor: status right, message right, origin
                       quietly lying.
  tests/negative/      two sources that must FAIL to compile, built with
                       -Werror and registered WILL_FAIL. AKERR_NOIGNORE
                       and the format attributes are enforced by the
                       compiler and by nothing else; drop either and
                       every ordinary test still passes.

Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.

Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.

CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.

Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:00:16 -04:00
c61e59b9a5 Wrap the strings, streams and collections the wishlist asked for
TODO.md section 3.1's high-priority list and section 3.6's data
structures. This is the surface akbasic went without: it makes 10 calls
into this library and 116 to raw libc, and 69 of those 116 are strlen,
strcmp, strncpy and strstr.

Three new translation units, because src/stdlib.c covering four times
what it did would stop being readable:

  src/string.c       lengths, bounded copy and concatenation, duplication,
                     comparison including the case-insensitive forms,
                     searching, the reentrant tokenisers, and a status
                     message that knows this library's own statuses as
                     well as errno's.
  src/stream.c       positioning, flushing and buffering, character and
                     line I/O, stream state, freopen/fdopen/tmpfile,
                     formatted input, and the file operations.
  src/collections.c  the list functions that were missing, a head/tail
                     container so append is O(1), a binary search tree,
                     FNV-1a, a fixed-capacity hash map and a growable
                     string buffer.

Two conventions run through all of it. The copying functions take the
destination size even where the libc function they are named for does
not, because strcpy(3) cannot be called safely without it, and
truncation is an error that writes nothing rather than a plausible
prefix -- this is the idiom akbasic writes out by hand at ten sites.
The searching functions treat "not found" as a successful answer of
NULL, because absent is an answer and raising on it would make every
caller handle a non-error.

The hash map is akbasic's src/symtab.c generalised: open-addressed with
linear probing over a caller-supplied slot array, keys copied into fixed
slots so the map owns them, tombstones on delete so a removal cannot cut
a probe chain, and a refusal rather than a resize when full.

Tests: 16 binaries, green under the normal and sanitizer builds. The
scanf wrappers take the number of conversions the caller expects, since
comparing scanf(3)'s return against that by hand is the check everyone
eventually forgets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:32:35 -04:00
a5b19f1d8d Fix the six confirmed defects and close the API contract gaps
TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.

The defects:

  2.1.1  aksl_list_append conflated Floyd cycle detection with finding
         the tail, so `tail` tracked the node behind the midpoint. Any
         append to a list of 2+ nodes silently dropped everything after
         it. Two separate walks now: Floyd to prove the list is finite,
         then a plain walk to the end.
  2.1.2  aksl_list_iterate started visiting from Floyd's `slow` cursor,
         so the whole first half of the list -- head included -- was
         never passed to the callback. It starts at the head.
  2.1.3  AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
         that raised it handled it and returned success, so the parent
         carried on into the sibling subtree. The recursion is split out
         and propagates the break; only the public entry swallows it.
  2.1.4  va_end now matches every va_start on every path.
  2.1.5  The ato* family had no error channel at all. Reimplemented over
         a new strto* family with errno cleared, an endptr check and a
         range check: AKERR_VALUE for junk, ERANGE for overflow.
  2.1.6  aksl_realpath never checked resolved_path, could not be told
         the buffer size, and formatted an unspecified buffer with %s on
         its own error path. It takes a length; aksl_realpath_alloc is
         the allocating form.

The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.

Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.

Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 07:14:11 -04:00
ac9f383a91 Record what the first full consumer had to work around
akbasic is a ~6,300-line C interpreter built on this library. It is the first
consumer to exercise the whole surface rather than a corner of it, so what it
could not use is worth writing down: it prioritizes the section 3 wishlist by
what a real port actually reaches for, and it says which section 2 entries have
teeth.

The number that matters: across src/, akbasic makes 10 calls into this library
and 116 to raw libc. A library whose value proposition is turning silent libc
failures into error contexts is being bypassed 92% of the time by the consumer
most committed to it.

Four things it had to write for itself, each of which maps onto an existing
unchecked box. A strict strtoll/strtod wrapper, because 2.1.5 means aksl_atoi
would have turned four diagnosable errors into wrong answers -- VAL("garbage")
answering 0.0 instead of raising. A fixed-capacity string-keyed hash table,
which is 3.6's "hash map built on aksl_strhash_djb2", needed three times over
for variables, functions and labels. The bounded-copy-with-truncation-as-error
idiom at ten sites across six files. And case folding at three sites.

Also confirms 2.2.4 (aksl_sprintf unbounded -- akbasic deliberately never calls
it, and has 28 snprintf sites with no aksl_snprintf to route them through),
2.2.2 (aksl_fopen validation, reached from user input via DLOAD/DSAVE), 2.2.6
(the djb2 sign extension, benign here only because BASIC identifiers are ASCII)
and 2.1.4 (the missing va_end, which akbasic's text sink runs on every line of
program output).

Section 4.3 records what akbasic did *not* need, so the wishlist does not get
reordered purely by one consumer's shape: it allocates nothing and uses no lists
or trees, so a consumer that does allocate would weight 3.1's memory section far
higher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 06:29:01 -04:00
024fe6cde2 Version the library at 0.1.0
project() now carries VERSION 0.1.0, and is the single place a version number
is spelled. It flows into generated version macros, the shared library's
VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new
akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to
nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried
no soname at all.

0.x on purpose: TODO.md section 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 are
different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt
and the #if in tests/test_version.c encode that rule and are tested against
each other.

include/akstdlib_version.h.in is configured into the build tree as
akstdlib_version.h and installed beside akstdlib.h. It defines
AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
AKSL_VERSION_NUMBER is computed rather than written as a literal, because a
literal 000100 is octal in C and would make 0.1.0 compare as 64;
test_version.c asserts it against the runtime components, so a rewrite to a
literal fails.

Those macros record what a caller was compiled against. aksl_version(),
aksl_version_string() and aksl_version_soname() report what actually loaded,
and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both.
It is a macro so that it expands at the caller's site and captures the
caller's numbers; the function compares them against the ones baked into the
library. Compatibility is "same soname", so patch is ignored -- a caller built
against 0.1.0 keeps working against 0.1.7.

Normally the soname catches a mismatch at load time and the check never fires.
It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under
the 0.1 filename loads happily, and only the check notices.

write_basic_package_version_file() uses SameMinorVersion to mirror the soname,
falling back to ExactVersion below CMake 3.11 where that mode does not exist.
The fallback is stricter than the soname rule -- it pins the patch level too --
but never laxer, and wrongly refusing a good pairing beats wrongly accepting a
bad one.

Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches
and 25/25 functions. That puts branch coverage back over the old 45 gate, but
the gate stays at 40: 0.1 points of headroom is not a ratchet.

ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of
tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config
--modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and
1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0
dropped in under the 0.1 filename caught by it, and an embedded
add_subdirectory build keeping its own version rather than the parent's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 22:40:49 -04:00
f60787a852 Upgrade to libakerror 1.0.0
Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the
status-name table private, moves consumer status codes to a band starting
at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than
treating it as advisory, and gives the library an soname. See
deps/libakerror/UPGRADING.md.

src/stdlib.c needed no changes. This library defines no status codes of its
own -- it raises libakerror's AKERR_* codes and propagates errno, both
inside libakerror's reserved 0-255 band -- and it never referenced
AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or
AKERR_STATUS_NAME_OK. What moved was everything around the code:

A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror
namespaces its `mutation` target when embedded but not its `coverage`
target, so it collided with ours. Shadow add_custom_target for the duration
of the add_subdirectory() call and rename the dependency's to
akerror_coverage, alongside the existing add_test shadow. Fix upstream and
delete the workaround; recorded in TODO.md.

Pin the 1.0.0 floor three ways, since no single one covers every
consumption path: an #error in akstdlib.h feature-testing
AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro;
Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers
-lakerror transitively; and find_dependency(akerror) in
akstdlibConfig.cmake. The last was already broken before this bump -- the
template still carried its MyLibraryConfig placeholder with the dependency
commented out, so any external find_package(akstdlib) failed with a bare
"akerror::akerror not found" out of the generated targets file.

Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or
test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches
at every call site, so 337/661 became 481/1087 -- 144 more branches covered,
426 more counted. Line coverage held at 99.0% (200/202) and function
coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than
chase branches that belong to libakerror's own suite.

tests/test_status_registry.c pins the contract that made the status-code
migration a no-op: libakstdlib reserves no consumer range, so an application
may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it,
and every status this library raises is inside the reserved band with a name
actually registered -- an unnamed one degrades to "Unknown Error" in every
later stack trace, which nothing else would notice. It exercises the new
ownership enforcement too, so the "reserves nothing" assertion cannot pass
vacuously.

ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6%
(155/173, unchanged). Also verified out of tree: the #error fires as the
first diagnostic against a stale akerror.h, pkg-config refuses akerror
0.9.0, and an external find_package(akstdlib) consumer builds and runs
against a temp-prefix install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 22:20:54 -04:00
681ca1d3c7 Namespace the coverage target when embedded
Follows a87cbfb: a sibling dependency may ship a `coverage` target of its
own, so a non-top-level build gets `akstdlib_coverage` instead. The
top-level name is unchanged, and so is every documented command.

Verified: cmake --build build-coverage --target coverage still runs the
suite (14/14) and reports 99.0% of lines; the mutation target still
resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 02:08:25 -04:00
be6d2f6d7c Test the libc wrappers: 52% -> 99% line coverage
Every wrapper outside the list and tree code was untested. Six new test
files close that, following the plan already written in TODO.md 1.2-1.6:

  test_stream.c   fopen/fread/fwrite/fclose -- happy paths, the round
                  trip, AKERR_EOF on a short read, AKERR_IO on a stream
                  opened in the wrong mode, ENOENT, and the NULL guards
  test_format.c   printf/fprintf/sprintf -- text *and* count asserted
                  (stdout is pointed at a temp file to check aksl_printf),
                  all eight NULL guards, EBADF on a read-only stream, and
                  512 variadic calls in a loop as sanitizer cover for the
                  missing va_end
  test_convert.c  ato{i,l,ll,f} happy paths, negatives, leading
                  whitespace, NULL guards
  test_path.c     realpath on a file and on a symlink, both compared
                  against realpath(3) since TMPDIR may itself be a link;
                  ENOENT, ENOTDIR, NULL path
  test_strhash.c  djb2 known-answer vectors, len == 0, embedded NUL,
                  stability, NULL guards
  test_convert_strict.c
                  known-failing (2.1.5): the AKERR_VALUE / ERANGE
                  contract the ato* family cannot express today

test_tree.c gains the BFS AKERR_NOT_IMPLEMENTED contract, NULL arguments,
and a callback error that is not AKERR_ITERATOR_BREAK propagating out.

Tests deliberately say nothing about behaviour TODO.md records as
defective -- unchecked ptr/mode/resolved_path, short transfers reported as
success, *count left at -1, the djb2 sign extension -- so the eventual fix
does not have to come with a test rewrite. Each failure case in
test_path.c passes a zeroed buffer, because the wrapper's own error path
formats resolved_path with %s (2.1.6).

aksl_capture.h gains aksl_temp_file() with an atexit unlink backstop.
Without it every test that fails before its own unlink leaves temp files
behind -- which is the normal case for a known-failing test, and happens
173 times over in a mutation run.

Coverage on src/stdlib.c: 52.0% -> 99.0% of lines (200/202), 23.6% ->
51.0% of branches, 8/21 -> 21/21 functions. The two uncovered lines are
both `} HANDLE(e, AKERR_ITERATOR_BREAK) {`, where the macro starts with
the `break;` of PROCESS's `case 0:` arm -- reachable only via a non-NULL
error context whose status is zero, the pathology 2.2.1 exists to remove.

Mutation score on src/stdlib.c: 46.8% -> 89.6% (155/173 killed). CI, the
pre-push hook and the docs ratchet from 40 to 80 accordingly, and the 18
survivors are grouped by cause in TODO.md and README.md. A new CI
coverage job gates at 90% lines / 45% branches.

Verified:
  ctest --test-dir build            # 12/12
  ctest --test-dir build-asan       # 12/12 under ASan + UBSan
  ctest --test-dir build-coverage   # 14/14, report attached
  ctest --test-dir build -j8 --repeat until-fail:3
  gcc -Wall -Wextra -c on all nine test files  # no warnings
  python3 scripts/mutation_test.py --target src/stdlib.c  # 89.6%
No temp files left in /tmp after any of the above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 02:07:36 -04:00
a776c5a568 Add code coverage to the CTest suite
New AKSL_COVERAGE option instruments the library and its tests with
--coverage -O0 and wires the report into the suite itself, so a plain
ctest --test-dir build-coverage both runs the tests and produces coverage.

Two CTest entries do the work, held in order by a CTest fixture rather
than by declaration order so they also hold under ctest -j:
coverage_reset (FIXTURES_SETUP) clears the .gcda counters before any test,
since gcov counts are cumulative and would otherwise fold in earlier runs;
coverage_report (FIXTURES_CLEANUP) aggregates gcov output afterwards.
AKSL_COVERAGE_THRESHOLD / AKSL_COVERAGE_BRANCH_THRESHOLD gate the report,
the same regression-ratchet idea as the mutation score. The `coverage`
target builds, runs and prints in one step.

scripts/coverage.py parses gcov's JSON output, aggregates line, branch and
function counts across translation units, and lists every uncovered line
and never-called function -- the actionable half, as with surviving
mutants. Python stdlib plus gcc's own gcov only: no lcov, gcovr or
genhtml. It also writes coverage-summary.txt (CTest hides the output of a
passing test) and a Cobertura coverage.xml for CI publishers.

Instrumentation is per target, so deps/libakerror stays out of the report.
The mutation harness now ignores build*/ and gcov artifacts when copying
the tree, so a coverage build does not slow it down.

Baseline on src/stdlib.c: 52.0% of lines, 23.6% of branches, 8 of 21
functions. The uncovered functions are the untested wrappers the mutation
survivors already point at (printf, ato*, stream, realpath, strhash).

Verified:
  cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
  cmake --build build-coverage --target coverage      # 8/8, report printed
  ctest --test-dir build-coverage -j8                 # fixture order holds
  cmake -S . -B build-coverage -DAKSL_COVERAGE=ON -DAKSL_COVERAGE_THRESHOLD=60
  ctest --test-dir build-coverage --output-on-failure  # gate fails as expected
  ctest --test-dir build --output-on-failure           # 6/6, no .gcda emitted
  ctest --test-dir build-asan --output-on-failure      # 6/6
  scripts/mutation_test.py --target src/stdlib.c --list # 173 mutants, unchanged
Totals match gcov itself: 51.98% of 202 lines, 23.60% of 661 branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-30 02:07:35 -04:00
b63d1a0503 Namespace the embedded mutation target 2026-07-29 18:02:21 -04:00
c5386492b4 Add CLAUDE.md 2026-07-29 17:42:38 -04:00
bd401b2452 Add AGENTS.md 2026-07-29 17:29:49 -04:00
ac570890f9 Make error-status assertions authoritative
Replace standalone message assertions with a helper that checks the returned akerr status first, then validates message content only as secondary context. Drop ambiguous memcpy message checks where both NULL guards use the same format string.
2026-07-29 15:47:17 -04:00
54165a615b Add memory wrapper tests
Cover deterministic TODO 1.1 memory-wrapper behavior with a new CTest target. Exercise malloc/free round trips, NULL argument handling, memset fill/no-op semantics, and memcpy copy/no-op/null-pointer paths.

Harden AKSL_CHECK_OK so success requires no returned error context, catching wrappers that accidentally raise status-0 errors from stale errno.

Co-authored-by: Codex GPT-5.5 Default <codex-gpt-5.5-default@openai.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-29 14:42:27 -04:00
13efe5e91b Fix miscounted break-visit expectation in TODO.md 1.8
The bullet said breaking at tree[3] in pre-order should assert 4 visits.
Pre-order is 0, 1, 3, 4, 2, 5, 6, so the correct count is 3 -- which is what
tests/test_tree_iterate_break.c already asserts. The TODO was written before
that test existed and had the number wrong, not the test.

Also point the bullet at the existing pre-order test, and give the in-order
and post-order traversals with their break points so the remaining work does
not need the count re-derived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-29 12:22:25 -04:00
7ca7d08d7e Document the test harnesses and add a pre-push hook
README.md was a build badge and nothing else. It now covers what the library
is, how to build it, and how to run each of the three harnesses locally: the
ctest suite, the AKSL_SANITIZE ASan/UBSan build, and mutation testing.

The section on reading ctest output matters most: two of the three test lists
are marked WILL_FAIL, so an all-green run 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.

.githooks/pre-push runs the two fast harnesses (default build + ctest, then
sanitizer build + ctest) before a push leaves the machine. It skips when there
is nothing to test (branch deletions, empty pushes), fails the push with the
captured output on any error, and builds under .git/aksl-prepush so it never
disturbs your own build/. The slow mutation gate is opt-in via
AKSL_HOOK_MUTATION=1, and git push --no-verify bypasses everything.

Enable with: git config core.hooksPath .githooks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-29 11:51:28 -04:00
824272a4d7 Add mutation testing to the Gitea workflow
A separate mutation_test job runs scripts/mutation_test.py over src/stdlib.c
with a --threshold gate and publishes a JUnit report, mirroring libakerror's
CI. Checkout needs submodules: recursive -- the harness copies the repo and
builds the copy top-level, so deps/libakerror has to be there.

Measured score on the section 1.0 suite is 46.8% (81/173 killed). The gate is
set at 40: a regression ratchet, not a quality bar. Survivors cluster in the
wrappers that have no tests yet (printf family, ato* family, realpath, the
memory and stream wrappers); the list and tree code that section 1.0 does
cover leaves only 5 survivors between them.

Also caps every test with TIMEOUT 30. Measuring the score turned this up: a
mutant deleting `fast = fast->next->next` makes aksl_list_iterate spin
forever, so ctest never returned, the harness killed ctest at its own 120s
timeout, and the orphaned test binary kept burning a core while holding the
pipe open -- the run wedged and needed killing by hand. With a per-test
timeout ctest reaps its own child, those mutants are killed in 30s, and
nothing is left behind. It guards the real suite against the same shape of
bug too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-29 10:35:46 -04:00
fe0d1145a1 Build a real test harness (TODO.md section 1.0)
The suite could not fail and did not run. test_linkedlist.c asserted nothing
at all -- it printed node names and returned 0 -- so every confirmed list
defect passed it. test_tree.c was red on every run because parms.steps was
never reset between its three searches, and four libakerror tests registered
but never built, reporting Not Run. CI, meanwhile, was not fetching the
submodule, so configure failed before reaching any of it.

- tests/aksl_capture.h: AKSL_CHECK (NDEBUG-proof), AKSL_CHECK_STATUS/_OK to
  run an akerror-returning call, assert its status and release the context,
  a capturing akerr_log_method, aksl_slots_in_use(), and an AKSL_RUN driver
  that fails any test leaking an error-pool slot.
- test_linkedlist.c: rewritten as 15 assertion cases over the append,
  iterate and pop behaviour that is correct today.
- test_tree.c: each search builds its own tree and params.
- Registration driven by AKSL_TESTS, plus AKSL_WILL_FAIL_TESTS (aborts by
  design) and AKSL_KNOWN_FAILING_TESTS, which marks WILL_FAIL the three new
  tests asserting correct behaviour for the confirmed defects in TODO.md
  2.1.1-2.1.3. Fixing a defect flips its test to "unexpectedly passed",
  which is the cue to promote it into AKSL_TESTS.
- add_test/set_tests_properties are shadowed across the libakerror
  add_subdirectory call: CMake cannot un-register a test and
  set_tests_properties cannot cross directory scopes. The dependency has
  its own CI.
- AKSL_SANITIZE=ON builds library, tests and dependency with ASan+UBSan.
- scripts/mutation_test.py ported and retargeted at src/stdlib.c; wired to
  a manual `mutation` target, not a CI gate until 1.1-1.9 exist.
- CI: checkout with submodules: recursive, and ctest --output-on-failure.

ctest is now 5/5 green in both the default and sanitizer builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-28 12:39:54 -04:00
34 changed files with 899 additions and 487 deletions

View File

@@ -21,7 +21,7 @@ jobs:
# 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.
# installed one was never actually linked against.
#
# 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
@@ -155,7 +155,7 @@ jobs:
# new surface being argument validation whose mutants are frequently
# equivalent. `errno = 0` deleted from a wrapper whose libc call always
# sets errno cannot be distinguished by any test that could be written.
# The survivors worth acting on are named in TODO.md; raise the gate as
# The survivors worth acting on are named in issue #7; raise the gate as
# they become assertions.
- name: mutation testing
run: |

View File

@@ -14,6 +14,7 @@ reasoning. The implementation is split by domain:
| `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/dir.c` | directory stream open/read/rewind/close |
| `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
| `src/aksl_internal.h` | shared internals; not installed, not public |
@@ -76,6 +77,10 @@ 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:
- **Preserve the libc contract unless there is a compelling, documented reason
not to.** libc behaviour is the standard to meet. This library changes only
the error transport (to `akerror`) and, where libc returns a value, the result
shape (through a caller-provided destination pointer).
- **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.
@@ -93,13 +98,19 @@ 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.
**Do not wrap a libc function that cannot fail and provides no failure or
operation-status result.** There is no `akerror` context to carry. A value such
as `umask()`'s previous mask is not an operation-status result, so `umask()` is
not a wrapper candidate.
## 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. Both of the
`AKSL_KNOWN_FAILING_TESTS` assert defects that have an open issue; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix and close the
issue. Both of the
latter are currently empty -- all six confirmed defects are fixed -- but the
mechanism stays for the next one.
@@ -119,7 +130,7 @@ file, `find_dependency(akerror)` resolving, and the exported
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
listing before proposing a change. Tests for behaviour an open issue records as
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
eventual fix into a test failure.
@@ -130,10 +141,26 @@ Recent commits use short imperative summaries, for example `Add memory wrapper
tests` and `Make error-status assertions authoritative`. Keep commits focused
and include tests with behavior changes. Pull requests should describe the
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
link the relevant `TODO.md` item or issue when fixing a known defect.
link the issue it closes.
## Agent-Specific Instructions
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
<https://source.starfort.tech/andrew/libakstdlib/issues> — `tea issues create
--repo andrew/libakstdlib` — naming the file and line, the functional
consequence, and what closing it would touch. Label it by kind and blast radius
and leave `status::grooming` on it until its scope and approach are settled.
**Do not add outstanding items to `TODO.md`**: that file is the record of where
the library stands, what is deliberately not wrapped, and which uncovered lines
are uncoverable rather than untested. A description of work still to do goes
stale the moment somebody does it, which is why the two are separated.
**A defect in a dependency is filed against that dependency.** `libakerror` has
a tracker on the same forge, and three defects that cost this library a
workaround each sat in this repository's own notes for months without anybody
upstream being able to see them. Comment the workaround at its site with the
words "filed upstream" and delete it when the fix lands.
Do not modify generated build trees, profiling artifacts, or untracked scratch
files unless explicitly asked. Prefer small, test-backed changes and update
`README.md` or `TODO.md` when changing documented workflows or known failures.
`README.md` when changing documented workflows.

View File

@@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.10)
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
#
# 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,
# defects listed in UPGRADING.md changed documented behaviour and, in five places,
# signatures:
#
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new
@@ -54,7 +54,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
endif()
# Warnings. The only warning in the tree when these went on was the unused
# `queue` parameter of aksl_tree_iterate, which TODO.md 2.2.8 had already
# `queue` parameter of aksl_tree_iterate, which UPGRADING.md had already
# recorded as a dead parameter -- so the cost of turning them on was one
# already-known defect, and the cost of leaving them off was every future one.
#
@@ -63,7 +63,7 @@ endif()
# requires at least one argument for the ...", which is a complaint about the
# macro's shape rather than about anything at this call site. Every FAIL_* in
# src/stdlib.c passes at least one argument regardless -- see the note in
# TODO.md 2.3 -- so the code is pedantic-clean; it is the expansion that is not.
# libakerror issue #15 -- so the code is pedantic-clean; it is the expansion that is not.
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
add_compile_options(-Wall -Wextra)
# CI turns this on. Locally it is off, because a warning that stops the build
@@ -78,7 +78,7 @@ endif()
# Sanitizer build, off by default:
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
# Set before the dependency is added so libakerror is instrumented too --
# several of the defects in TODO.md section 2 (the uninitialised %s in
# several of the defects listed in UPGRADING.md (the uninitialised %s in
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
# the printf family) only show up under ASan/UBSan.
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
@@ -173,7 +173,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
# configure at all. Rename the dependency's on the way past rather than
# dropping it: its coverage script drives its own instrumented build tree, so
# `cmake --build build-coverage --target akerror_coverage` still does the right
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md.
# thing. Remove this once the dependency namespaces it upstream -- libakerror issue #15.
function(add_custom_target _name)
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
_add_custom_target(akerror_coverage ${ARGN})
@@ -207,6 +207,8 @@ add_library(akstdlib SHARED
src/stdlib.c
src/string.c
src/stream.c
src/stat.c
src/dir.c
src/collections.c
)
@@ -298,7 +300,7 @@ install(FILES
# reaching FINISH_NORETURN, a deliberate contract
# violation), so a non-zero exit is a pass.
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
# defect from TODO.md section 2.1. They fail until
# defect; see UPGRADING.md. They fail until
# the defect is fixed, and are marked WILL_FAIL so
# the suite stays green and the gap stays visible.
# When one is fixed CTest reports it as failed with
@@ -307,6 +309,7 @@ install(FILES
set(AKSL_TESTS
collections
convert
dir
format
hashmap
linkedlist
@@ -317,6 +320,7 @@ set(AKSL_TESTS
strbuf
stream
streamio
stat
strhash
string
strto
@@ -329,7 +333,7 @@ set(AKSL_WILL_FAIL_TESTS
# Empty, and that is the news. It held four entries -- convert_strict,
# list_append_chain, list_iterate_head and tree_iterate_break -- one for each
# confirmed defect in TODO.md 2.1. All four are fixed, so each of those files
# confirmed defect listed in UPGRADING.md. All four are fixed, so each of those files
# was folded back into the test for the thing it was testing (tests/
# test_convert.c, tests/test_linkedlist.c and tests/test_tree.c) where it now
# has to keep passing rather than merely keep failing visibly.
@@ -350,7 +354,7 @@ 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.
# Negative compile tests: the format attributes and AKERR_NOIGNORE.
#
# 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

View File

@@ -8,7 +8,8 @@ through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
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 and `UPGRADING.md` if you are
Outstanding work is in [the issue tracker](https://source.starfort.tech/andrew/libakstdlib/issues).
See `TODO.md` for where the library stands and what is deliberately not wrapped, and `UPGRADING.md` if you are
coming from 0.1.0, which this release breaks.
## What it wraps
@@ -56,7 +57,7 @@ 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
libakerror's pool, which is that library's decision to make; issue #2
records it. Until then: confine libakstdlib calls to one thread, or serialise
them yourself.
@@ -100,7 +101,7 @@ copy in your build directory; change the template or `project()`.
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
in the tracker 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
@@ -225,10 +226,10 @@ of them invert the meaning of "Passed":
|---|---|
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `UPGRADING.md`). Also marked `WILL_FAIL`. |
**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
defects recorded in `UPGRADING.md` 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
@@ -322,7 +323,7 @@ so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
on the way past — it drives its own instrumented build tree, so
`cmake --build build-coverage --target akerror_coverage` still works. The
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
workaround goes away when libakerror namespaces it upstream; see issue #4.
CTest hides the output of a passing test, so `coverage_report` also writes
`build-coverage/coverage-summary.txt` (the same text report) and
@@ -379,7 +380,7 @@ lines are uncovered and each is uncovered on purpose:
- **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.
because anything reaches it. Issue #6 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,

392
TODO.md
View File

@@ -1,11 +1,16 @@
# TODO
# Record
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.
**Outstanding work is in the issue tracker, not in this file:**
<https://source.starfort.tech/andrew/libakstdlib/issues>
Ordered by blast radius: what blocks other work first, what is merely wrong
second, what is missing last.
What stays here is what a tracker has no place for: where the library stands, and
the decisions that would otherwise be re-litigated — what is deliberately *not*
wrapped, and which uncovered lines are uncoverable rather than untested.
Issues are labelled by kind and blast radius, and milestoned by what they can land
in: `0.2.x` for anything that breaks no ABI, `0.3.0` for new public symbols,
`1.0.0` for the large surfaces. Everything filed carries `status::grooming` until
it has been through grooming.
## Where the library stands
@@ -19,168 +24,61 @@ second, what is missing last.
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
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`.
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
is in `UPGRADING.md`.
---
## What libakerror costs this library
## 1. Blocked on libakerror
Three of these are not fixable from inside this repository, and each costs
something here. They are filed in both places, because the fix is there and the
bill is here. The fourth turned out not to be blocked at all:
These are not fixable from inside this repository. Each one currently costs
something here, and the cost is what makes them worth carrying.
| Here | Upstream | What it costs |
|---|---|---|
| #2 | — | **Corrected while filing.** The unlocked error pool is a property of the libakerror this repository *pins* (1.0.0), not of libakerror (2.0.1, which locks it). `libakgl` and `akbasic` are both on 2.0.1. The work is a submodule bump and the verification that goes with it, not a wait |
| #3 | libakerror | `IGNORE()` leaks a context, so `aksl_tree_iterate` open-codes log-then-release in four lines that should be one |
| #4 | libakerror | The `coverage` target is not namespaced when embedded, so `-DAKSL_COVERAGE=ON` fails to configure and `CMakeLists.txt` shadows `add_custom_target` to work around it |
| #5 | libakerror | No `akerrorConfigVersion.cmake`, so `find_dependency(akerror)` cannot ask for the 1.0.0 floor |
### 1.1 The error pool is a process-global array with no locking
## Uncovered lines that are uncoverable
**This is what makes the library single-threaded**, and it is the largest open
item by some distance.
Eight lines, and this is what they are, so the coverage listing does not read as an
oversight.
`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.
**Two are the short-transfer branch** in `aksl_fread`/`aksl_fwrite` — a short
transfer with neither EOF nor a stream error. The standard permits it, so the
branch is correct to have; every way of actually producing one on Linux sets `feof`
or `ferror` first. **It is the only error path in the library that has never
executed**, and reaching it needs a `FILE *` over a custom stream (`fopencookie`,
`funopen`). That is #6.
**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.
**Two are the string-buffer overflow guard** — 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.** It is there because doubling a
capacity is a multiplication, and an unguarded one is how a growable buffer turns
into a heap overflow. Nothing to do.
**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.
**Four are `HANDLE(e, AKERR_ITERATOR_BREAK)` lines**, and they are 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* — the pathological case the errno-fallback work removed. Left
uncovered deliberately rather than pinned by a test that would have to manufacture
it.
### 1.2 `IGNORE()` logs a context and never releases it
## `aksl_version_check()` ignores its `patch` argument
`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)`.
**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.
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
the workaround here.
### 1.3 libakerror does not namespace its `coverage` target when embedded
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
not `coverage`.
**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`.
**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.
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
**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.
**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. Known gaps in what is already wrapped
### 2.1 A short transfer with neither EOF nor a stream error is untested
`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.
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.
**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.
**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.
### 2.2 The string-buffer overflow guard is untestable
`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.
**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.
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
listing does not read as an oversight.
### 2.3 `aksl_version_check()` ignores its `patch` argument
`src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule —
`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.
**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.
No consequence 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.
### 2.4 Surviving mutants worth turning into assertions
## Deliberate omissions
The mutation harness samples 260 of 1701 mutants and kills 72.3% of them. Most of
the 72 survivors are equivalent mutants rather than missing tests — `README.md`
has the full breakdown — but three clusters are real work:
- **`FINISH(e, true)``FINISH(e, false)`, 3 survivors.** An error swallowed
instead of propagated out of an `ATTEMPT` block, and nothing notices. Each one
is a call whose failure path is exercised but whose *propagation* is not: the
test asserts the status the callee raised without checking it came from the
callee rather than being re-raised locally. `tests/test_pool.c`'s origin
assertions are the shape of the fix.
- **`SUCCEED_RETURN` deleted, 7 survivors.** The function falls off the end and
returns whatever is in the return register, which is NULL often enough to pass.
These need an assertion on the *side effect* — the buffer that was filled, the
node that was linked — rather than on the returned status.
- **`FAIL_*` guards deleted, ~6 of the 14 in that group.** Each is an argument
check nothing drives. The other 8 in the group are constant shifts that no test
can catch, because the test names the same constant symbolically and moves with
it.
Two survivors in this class were real and are fixed: the right child's
`depth + 1` in the depth-first walk, and `aksl_tree_remove` on an empty tree.
**What closing them would touch.** Only `tests/`. Raise the `--threshold` in
`.gitea/workflows/ci.yaml` and `.githooks/pre-push` in step, as a ratchet.
### 2.5 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
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. Deliberate omissions
Recorded so nobody adds them thinking they were forgotten. Each is a decision,
and each can be revisited with an argument.
Recorded so nobody adds them thinking they were forgotten. **Each is a decision,
and each can be revisited with an argument.**
| Not wrapped | Why |
|---|---|
@@ -191,126 +89,23 @@ and each can be revisited with an argument.
| `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. |
---
## What the mutation survivors mean
## 4. Not yet wrapped
The harness samples 260 of 1701 mutants and kills 72.3%. **Most of the 72 survivors
are equivalent mutants rather than missing tests** — `README.md` has the full
breakdown — and knowing which is which is the point, because a ratchet built on the
wrong number is a ratchet that stops moving.
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.
Three clusters are real work and are #7. Two survivors in that class were real and
are fixed: the right child's `depth + 1` in the depth-first walk, and
`aksl_tree_remove` on an empty tree.
### 4.1 POSIX file and process API
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`
- [ ] `readv`, `writev`
- [ ] `dup`, `dup2`, `pipe`, `fcntl`
- [ ] `fsync`, `fdatasync`, `truncate`, `ftruncate`
- [ ] `unlink`, `link`, `symlink`, `readlink`, `rmdir`, `mkdir`
- [ ] `access`, `faccessat`, `chmod`, `fchmod`, `chown`, `fchown`, `umask`
- [ ] `chdir`, `fchdir`, `getcwd`
- [ ] `isatty`, `ttyname_r`
- [ ] `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`, `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`, 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 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`
### 4.2 Time
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
- [ ] `strftime`, `strptime`
- [ ] `clock`, `times`
`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.
### 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`
### 4.4 Larger surfaces
- [ ] **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` 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.
### 4.5 Data structures
The list and tree API is complete against what §3.6 asked for. What a second
consumer might want next:
- [ ] 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.
---
## 5. Evidence from the first full consumer
## 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 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.
whole surface rather than a corner of it, **and what it could not use is what
prioritised everything that has been built since.**
**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
@@ -330,48 +125,27 @@ committed to it.
**All four things the port had to write for itself now exist here.**
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). The
`aksl_strto*` family is that, with the endptr/`errno`/range contract. akbasic
formally banned 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.
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`.
**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
**And the four confirmed-with-impact defects are closed.** The unbounded
`aksl_sprintf` is gone; `aksl_fopen`'s 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.
the sign-extended djb2 reads bytes unsigned; and the missing `va_end` which
akbasic's stdio text sink ran on every line of program output — is fixed.
**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.
## Requested by consumers
1. **No directory-reading wrapper.** There is no `aksl_opendir` / `aksl_readdir` /
`aksl_closedir`, so a consumer that wants to list a directory has to call
`opendir(3)` itself and step outside the error convention every other call in
its file follows — reporting through `errno` where everything around it
reports through an `akerr_ErrorContext *`.
`akbasic` hit this implementing Commodore BASIC's `DIRECTORY` verb, and
refuses the verb rather than working around it: `src/runtime_disk.c` reports
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper
yet". The shape it wants is the one the `aksl_f*` family already has — a
handle out through a pointer parameter, `NULL` on success, `ENOENT` and
`EACCES` propagated as themselves.
A wrapper would want tests for: a directory that does not exist, one that
cannot be searched, an empty one, and one whose entries outlast a single
read — plus whatever the `d_type` portability story turns out to be, since
not every filesystem fills it in.
**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 the `open`/`read`/`write` work far higher than this one
does**, so one consumer's count is evidence, not a plan. Re-counting against this
release is #26.

View File

@@ -10,7 +10,7 @@ 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
Everything below was recorded in `TODO.md` before the move to the tracker — the confirmed
defects, all of which were reproduced against the 0.1.0 library before being
fixed.
@@ -83,11 +83,10 @@ having visited nothing.
/* before */
aksl_sprintf(&count, buf, "%s=%d", key, value);
/* after */
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
aksl_snprintf(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`.
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success.
**`aksl_realpath` takes the destination's length.**

View File

@@ -26,7 +26,8 @@
* 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.
* coming from 0.1.0. Outstanding work is in the issue tracker;
* TODO.md is the record of where the library stands.
*/
#ifndef _AKSTDLIB_H_
@@ -62,6 +63,7 @@
/*
* What this header needs in its own declarations, and no more:
* dirent.h DIR, struct dirent
* stdio.h FILE
* stddef.h size_t
* stdint.h uint32_t
@@ -69,12 +71,16 @@
*
* It used to pull in stdlib.h and string.h as well, which nothing here needs and
* which every consumer then got whether it wanted them or not. stddef.h in place
* of stdlib.h is the same size_t at a fraction of the namespace. TODO.md 2.2.16.
* of stdlib.h is the same size_t at a fraction of the namespace.
*/
#include <dirent.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
#include <sys/types.h>
@@ -85,7 +91,7 @@ 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.
* and aksl_printf(&n, "%d", "str") is not.
*
* tests/negative/format_mismatch.c is a compile that must fail, which is what
* proves these are still attached.
@@ -438,9 +444,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
/* ====================================================================== */
/** @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.
* Bounded output is checked for truncation and reports an error when the result
* does not fit.
*
* 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
@@ -477,16 +482,15 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea
* 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_NULLPOINTER If str or format 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);
akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4);
/**
* @brief Format into a freshly allocated string.
@@ -545,7 +549,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre
/**
* @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.
@@ -555,7 +558,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stre
* @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);
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args);
/** @} */
@@ -807,6 +810,151 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32
/** @} */
/* ====================================================================== */
/** @name File and filesystem metadata
*
* The stat family reports into caller-owned POSIX structs rather than a smaller
* library-defined copy: the platform owns the fields, and a wrapper should not
* discard a field merely because this library does not currently use it. libc
* failures retain their errno value as the status, so callers can distinguish
* absent paths from inaccessible ones without parsing an error message.
* @{
*/
/* ====================================================================== */
/**
* @brief stat(2).
* @param[in] pathname Path to inspect. Required.
* @param[out] dest File metadata. Required.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If stat(2) failed and left errno at 0.
* @throws (errno) The errno stat(2) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest);
/**
* @brief lstat(2), inspecting a symbolic link itself.
* @param[in] pathname Path to inspect. Required.
* @param[out] dest File metadata. Required.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If lstat(2) failed and left errno at 0.
* @throws (errno) The errno lstat(2) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest);
/**
* @brief fstat(2).
* @param[in] fd Open file descriptor.
* @param[out] dest File metadata. Required.
* @throws AKERR_NULLPOINTER If dest is NULL.
* @throws AKERR_IO If fstat(2) failed and left errno at 0.
* @throws (errno) The errno fstat(2) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest);
/**
* @brief fstatat(2).
* @param[in] dirfd Directory descriptor, or AT_FDCWD.
* @param[in] pathname Path to inspect. Required.
* @param[out] dest File metadata. Required.
* @param[in] flags libc fstatat flags, passed unchanged.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If fstatat(2) failed and left errno at 0.
* @throws (errno) The errno fstatat(2) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags);
/**
* @brief statvfs(3).
* @param[in] path Path on the filesystem. Required.
* @param[out] dest Filesystem metadata. Required.
* @throws AKERR_NULLPOINTER If path or dest is NULL.
* @throws AKERR_IO If statvfs(3) failed and left errno at 0.
* @throws (errno) The errno statvfs(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest);
/**
* @brief fstatvfs(3).
* @param[in] fd Open file descriptor.
* @param[out] dest Filesystem metadata. Required.
* @throws AKERR_NULLPOINTER If dest is NULL.
* @throws AKERR_IO If fstatvfs(3) failed and left errno at 0.
* @throws (errno) The errno fstatvfs(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest);
/** @} */
/* ====================================================================== */
/** @name Directories
*
* Directory entries are copied into caller-owned storage. `d_type` may be
* `DT_UNKNOWN`; callers that require a type must fall back to aksl_stat or
* aksl_fstatat.
* @{
*/
/* ====================================================================== */
/**
* @brief Open a directory stream by path.
* @param[in] pathname Directory path. Required.
* @param[out] dest Open directory stream, or NULL on failure. Required.
* @throws AKERR_NULLPOINTER If pathname or dest is NULL.
* @throws AKERR_IO If opendir(3) fails without setting errno.
* @throws (errno) The errno opendir(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest);
/**
* @brief Open a directory stream from a file descriptor.
* @param[in] fd Open directory descriptor. Ownership transfers on success.
* @param[out] dest Open directory stream, or NULL on failure. Required.
* @throws AKERR_NULLPOINTER If dest is NULL.
* @throws AKERR_IO If fdopendir(3) fails without setting errno.
* @throws (errno) The errno fdopendir(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest);
/**
* @brief Read and copy the next directory entry.
* @param[in] dirp Open directory stream. Required.
* @param[out] dest Caller-owned storage for the copied entry. Required.
* @throws AKERR_NULLPOINTER If dirp or dest is NULL.
* @throws AKERR_EOF At the end of the directory stream.
* @throws AKERR_IO If readdir(3) fails without setting errno.
* @throws (errno) The errno readdir(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest);
/**
* @brief Close a directory stream.
* @param[in] dirp Open directory stream. Required.
* @throws AKERR_NULLPOINTER If dirp is NULL.
* @throws AKERR_IO If closedir(3) fails without setting errno.
* @throws (errno) The errno closedir(3) set, reported directly as the status.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp);
/**
* @brief Reset a directory stream to its beginning.
* @param[in] dirp Open directory stream. Required.
* @throws AKERR_NULLPOINTER If dirp is NULL.
* @return NULL on success, an error context otherwise.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewinddir(DIR *dirp);
/** @} */
/* ====================================================================== */
/** @name Streams: open, read, write, close
*

View File

@@ -18,7 +18,7 @@
* the context still holds a pool slot: an error that is invisible and leaks at
* the same time. Every errno-sourced status in this library goes through here,
* and every wrapped call clears errno first so the value read back is its own.
* TODO.md 2.2.1.
* See UPGRADING.md.
*/
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))

View File

@@ -1,5 +1,5 @@
/*
* Data structures -- TODO.md section 3.6.
* Data structures.
*
* Not libc wrappers. These are the parts of the list and tree API that were
* visibly missing, plus the two structures the first real consumer had to write
@@ -328,7 +328,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail
* aksl_list_append has to walk the whole list to find the tail, so building a
* list of n nodes with it is O(n^2). That is fine for the handful of nodes the
* bare-node API was written for and wrong for anything larger, which is what
* TODO.md 3.6 means by "a head/tail-tracking container type so append is O(1)".
* the collections plan meant by "a head/tail-tracking container type so append is O(1)".
*
* The container holds the length as well, so aksl_list_length stops being a
* walk. It owns no memory -- the nodes are still the caller's -- so there is no
@@ -440,7 +440,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFun
* through an out-param and may raise, like every other callback here.
*
* These are the functions that set and read aksl_TreeNode.parent, which was
* declared and then never touched by anything in the library (TODO.md 2.2.15).
* declared and then never touched by anything in the library.
* aksl_tree_remove needs it: relinking a node's replacement means telling that
* node's parent about it, and finding the parent by walking from the root again
* would turn a removal into a second search.
@@ -691,7 +691,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl
/* ====================================================================== */
/*
* FNV-1a, the other half of TODO.md 3.6's hash request. It differs from djb2 in
* FNV-1a, the other half of the collections plan's hash request. It differs from djb2 in
* XOR-then-multiply rather than multiply-then-add, which mixes the low bits
* rather better -- worth having when the keys are short and share a prefix,
* which is exactly what identifiers in a symbol table look like.
@@ -730,7 +730,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint3
/*
* Fixed-capacity, open-addressed, linear-probing, string-keyed.
*
* The shape is akbasic's src/symtab.c, which TODO.md 3.6 says is "worth lifting
* The shape is akbasic's src/symtab.c, which the collections plan called "worth lifting
* more or less verbatim": the caller supplies the slot array, the map refuses
* rather than resizes when full, and the keys are copied into fixed-size slots
* so the map owns them and a caller cannot outlive its own key strings.
@@ -939,7 +939,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map,
*
* 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 -- which is why TODO.md 3.6 asks for this to "make the snprintf and
* advance -- which is why the collections plan asked for this to "make the snprintf and
* strcat wrappers pleasant to use". Building a diagnostic, a serialised record
* or a generated line means appending to something that grows.
*

92
src/dir.c Normal file
View File

@@ -0,0 +1,92 @@
/*
* POSIX directory-stream wrappers.
*
* opendir(3), fdopendir(3), readdir(3), closedir(3), and rewinddir(3) expose
* three different failure conventions between them. These wrappers turn all
* three into error contexts and make end-of-directory an explicit AKERR_EOF.
*/
#include <akstdlib.h>
#include <errno.h>
#include "aksl_internal.h"
/*
* opendir(3) returns NULL for failure. Clear *dest first so a failed open
* cannot leave the caller holding a stale directory stream.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p",
(void *)pathname, (void *)dest);
*dest = NULL;
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p",
(void *)pathname, (void *)dest);
errno = 0;
*dest = opendir(pathname);
FAIL_ZERO_RETURN(e, *dest, AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname);
SUCCEED_RETURN(e);
}
/*
* fdopendir(3) takes ownership of fd only when it succeeds. On success the
* matching aksl_closedir call closes both the stream and its descriptor.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest);
*dest = NULL;
errno = 0;
*dest = fdopendir(fd);
FAIL_ZERO_RETURN(e, *dest, AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd);
SUCCEED_RETURN(e);
}
/*
* readdir(3) owns and may reuse its returned storage. Copy the entry into the
* caller's destination, and use errno to distinguish failure from exhaustion.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest)
{
struct dirent *entry = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p, dest=%p",
(void *)dirp, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirp=%p, dest=%p",
(void *)dirp, (void *)dest);
/* readdir uses errno to distinguish failure from end-of-directory. */
errno = 0;
entry = readdir(dirp);
if ( entry == NULL ) {
FAIL_NONZERO_RETURN(e, errno, AKSL_ERRNO_OR(AKERR_IO), "readdir failed");
FAIL_RETURN(e, AKERR_EOF, "end of directory");
}
*dest = *entry;
SUCCEED_RETURN(e);
}
/* closedir(3) reports its failure directly and invalidates dirp on success. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p", (void *)dirp);
errno = 0;
FAIL_NONZERO_RETURN(e, closedir(dirp), AKSL_ERRNO_OR(AKERR_IO),
"closedir failed");
SUCCEED_RETURN(e);
}
/*
* rewinddir(3) has no failure return. Preserve that contract after rejecting
* a NULL stream, which would otherwise be undefined behaviour.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewinddir(DIR *dirp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p", (void *)dirp);
rewinddir(dirp);
SUCCEED_RETURN(e);
}

108
src/stat.c Normal file
View File

@@ -0,0 +1,108 @@
/*
* sys/stat.h and sys/statvfs.h metadata wrappers.
*
* These calls make information about a file or filesystem available only by a
* return value that must be checked alongside a caller-owned POSIX struct. The
* wrappers put failure in the return value, where AKERR_NOIGNORE prevents it
* from being silently dropped, while preserving the errno that distinguishes
* missing paths, inaccessible paths, and invalid descriptors. errno is cleared
* immediately before each libc call, so a broken libc that reports failure
* without setting errno is still an AKERR_IO failure rather than status 0.
*/
#include <akstdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include "aksl_internal.h"
/*
* stat(2) follows pathname through any symbolic links and writes the target's
* metadata into the caller-owned struct stat. A file that is gone, cannot be
* searched, or lives below a non-directory component is reported as the errno
* from libc rather than as a library-specific status.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, stat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname);
SUCCEED_RETURN(e);
}
/*
* lstat(2) is stat(2) without the final symbolic-link traversal. That is the
* difference a caller needs when it is deciding whether a path is a link or
* when the link's ownership and mode are the metadata of interest.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, lstat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname);
SUCCEED_RETURN(e);
}
/*
* fstat(2) gets metadata from an already-open descriptor, so it has no path
* lookup race and remains useful after the file has been renamed or unlinked.
* A closed or otherwise invalid descriptor reports EBADF from libc.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, fstat(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd);
SUCCEED_RETURN(e);
}
/*
* fstatat(2) is the directory-descriptor form of stat(2). pathname is resolved
* relative to dirfd unless it is absolute, and flags retain the libc choices
* such as inspecting a link itself. Keeping those flags unchanged prevents this
* wrapper from inventing a smaller policy than the POSIX call already exposes.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d, pathname=%s, flags=0x%x", dirfd, pathname, flags);
SUCCEED_RETURN(e);
}
/*
* statvfs(3) writes information about the mounted filesystem containing path,
* not merely the named file. The result includes the filesystem block sizes and
* available space the caller needs before it decides whether an operation fits.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, statvfs(path, dest), AKSL_ERRNO_OR(AKERR_IO), "path=%s", path);
SUCCEED_RETURN(e);
}
/*
* fstatvfs(3) is the descriptor form of statvfs(3). It asks the filesystem that
* owns fd for the same capacity and flag information without resolving a path
* again, and reports a bad descriptor through the errno libc supplies.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest);
errno = 0;
FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd);
SUCCEED_RETURN(e);
}

View File

@@ -99,7 +99,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void *
* block valid*, so the near-universal `p = realloc(p, n)` leaks the original
* every time it fails. Here the old pointer goes in and out through the same
* out-param, and is left untouched -- still valid, still the caller's to free --
* whenever an error is raised. TODO.md 3.1.
* whenever an error is raised.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size)
{
@@ -196,7 +196,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr)
/*
* Frees and clears in one step, so the pointer cannot be used or freed twice.
* TODO.md 2.2.13 -- aksl_free leaves the caller holding a dangling pointer, and
* aksl_free leaves the caller holding a dangling pointer, and
* "remember to NULL it afterwards" is exactly the discipline this library is
* supposed to make unnecessary rather than merely possible.
*/
@@ -214,7 +214,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr)
* memset(3) and memcpy(3) cannot fail. They return their destination pointer
* unconditionally, so the old FAIL_ZERO_RETURN(e, memset(...), ...) and
* (memcpy(...) == d) checks were dead code that read as though there were a
* failure mode to catch -- TODO.md 2.2.11. What is worth checking is the
* failure mode to catch. What is worth checking is the
* arguments, which is all that is checked now.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n)
@@ -289,7 +289,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
* pathname and mode are checked. fopen(NULL, ...) is undefined behaviour, and
* this wrapper used to hand both straight through unexamined -- reachable from
* user input in practice, which is why akbasic validates the filename itself
* before calling DLOAD/DSAVE with a comment pointing at TODO.md 2.2.2.
* before calling DLOAD/DSAVE with a comment pointing at.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
const char *pathname,
@@ -312,7 +312,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(
/*
* fread and fwrite both report how much they actually transferred, through a
* required out-param. Three things were wrong with the old pair (TODO.md 2.2.3),
* required out-param. Three things were wrong with the old pair,
* and the count is the fix for the worst of them: a caller who got AKERR_EOF had
* no way to find out how much data had arrived before the stream ran out, which
* makes the EOF status almost useless for the partial-read case it exists to
@@ -405,7 +405,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
* Formatted output.
*
* The va_list forms below do the work and the variadic forms are thin wrappers
* over them, which is both what §3.1 of TODO.md asked for -- so consumers can
* over them, which is both what the wrapper contract asked for -- so consumers can
* build their own variadic wrappers -- and what makes the va_end rule below
* checkable in one place instead of three.
*
@@ -415,18 +415,14 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
* one. Omitting it is undefined behaviour per the standard and leaks
* register-save state on some ABIs. akbasic's text sink ran this UB on every
* line of program output without anything visibly misbehaving, which is
* exactly what made it worth fixing before something did. TODO.md 2.1.4.
* - *count is written on every path. It used to be left holding vprintf's -1
* after a failure, so a caller who read the length rather than the status got
* a negative byte count out of a function that had already failed. It is now
* 0 whenever an error is raised.
* exactly what made it worth fixing before something did.
* - errno is cleared before the call and read back through AKSL_ERRNO_OR, so a
* failure can never be reported with a stale -- or with a zero -- status.
*
* aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an
* error-handling wrapper around an unbounded write is precisely the sharp edge
* this library exists to remove. aksl_snprintf replaces it, and treats
* truncation as the failure it is rather than as a short success. TODO.md 2.2.4.
* truncation as the failure it is rather than as a short success.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args)
@@ -485,34 +481,31 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea
* *would* have written and silently drops the rest, which is the single most
* common way a bounded write goes wrong unnoticed; a caller who wanted to know
* would have had to compare the return against the buffer size by hand, which is
* the check this library exists to stop people forgetting. *count is the number
* of bytes written excluding the terminating NUL, and is 0 on any failure.
* the check this library exists to stop people forgetting. The bounded wrapper
* reports truncation instead.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args)
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list args)
{
int needed = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
*count = 0;
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p", (void *)str, (void *)format);
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL");
errno = 0;
needed = vsnprintf(str, size, format, args);
FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error");
FAIL_NONZERO_RETURN(e, ((size_t)needed >= size), AKERR_OUTOFBOUNDS,
"output truncated: %d bytes needed, %zu available", needed, size);
*count = needed;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...)
akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(char *restrict str, size_t size, const char *restrict format, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, format);
raised = aksl_vsnprintf(count, str, size, format, args);
raised = aksl_vsnprintf(str, size, format, args);
va_end(args);
return raised;
}
@@ -561,7 +554,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const
* same arguments a few lines up, so the write cannot truncate. Guarding it
* anyway would mean a failure branch nothing can reach and a free() beneath
* it that nothing can execute -- dead code that reads as though there were a
* failure mode to catch, which is exactly what TODO.md 2.2.11 recorded
* failure mode to catch, which is exactly what was recorded
* against the old aksl_memset and aksl_memcpy and what removing those was
* for. The invariant is stated here instead, where it can be read.
*/
@@ -590,10 +583,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const
* atoi("99999999999999999999") handed back success and a wrapped value, because
* atoi(3) has no error channel at all. A library whose entire value proposition
* is turning silent libc failures into error contexts cannot ship that.
* TODO.md 2.1.5.
* See UPGRADING.md.
*
* The strto* wrappers below are the real implementation and the ato* wrappers
* are three-line calls into them, which is what TODO.md 3.1 asked for on its own
* are three-line calls into them, which is what the wrapper contract asked for on its own
* account -- akbasic had to hand-write ~60 lines of exactly this (its
* src/convert.c) because the library would not do it.
*
@@ -846,7 +839,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest)
* realpath(3), with the destination buffer made expressible.
*
* The old two-argument form had three separate problems, all of them reachable
* from ordinary use (TODO.md 2.1.6): resolved_path was never NULL-checked, so
* from ordinary use: resolved_path was never NULL-checked, so
* realpath(path, NULL) allocated a buffer the wrapper then discarded and leaked;
* there was no way for a caller to say how big the buffer was, so everyone had
* to know to supply PATH_MAX bytes; and the failure path formatted resolved_path
@@ -904,7 +897,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path
* negative value: the hash differed from canonical djb2, and -- worse -- differed
* between platforms depending on the signedness of char. Benign for the 7-bit
* ASCII identifiers akbasic hashes, and quietly wrong for the first caller to
* key a table on a filename or a UTF-8 string literal. TODO.md 2.2.6.
* key a table on a filename or a UTF-8 string literal.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval)
{
@@ -921,7 +914,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len
SUCCEED_RETURN(e);
}
/* The NUL-terminated convenience form TODO.md 3.6 asked for. */
/* The NUL-terminated convenience form the collections plan asked for. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval)
{
PREPARE_ERROR(e);
@@ -941,7 +934,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
* Two separate walks, deliberately. The Floyd pass answers "is this list
* finite"; it says nothing about where the tail is, because `slow` stops at
* the midpoint. Conflating the two is what made this function truncate every
* list of two or more nodes -- see TODO.md 2.1.1.
* list of two or more nodes.
*/
while ( fast != NULL && fast->next != NULL ) {
slow = slow->next;
@@ -973,7 +966,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
/*
* `head` is required, not optional. Popping the head node used to leave the
* caller's own head pointer aimed at a node that is no longer in the list, and
* there was no way for the caller to learn the new one -- TODO.md 2.2.12. Taking
* there was no way for the caller to learn the new one. Taking
* the head by reference makes the correct call the only call that compiles.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)
@@ -998,7 +991,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_List
/*
* Zeroing initialisers. Every caller previously had to remember to memset a node
* before its first use, because append and iterate both read next/prev -- and a
* stack-allocated node that skipped it walked into garbage. TODO.md 2.2.14.
* stack-allocated node that skipped it walked into garbage.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data)
{
@@ -1027,7 +1020,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
* aksl_ListNode: the walk needs to carry each node's depth alongside it, which is
* how a cyclic tree is stopped without a visited-set. The caller's lalloc/lfree
* allocate and release these -- which is what they were always documented to do
* and never actually did (TODO.md 2.2.8).
* and never actually did.
*/
typedef struct TreeQueueEntry {
struct TreeQueueEntry *next;
@@ -1165,12 +1158,12 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_bfs_walk(
* unwinds every frame up to the public entry point, which swallows it exactly
* once. In the old single-function form the frame that raised the break handled
* it and returned success, the parent's PASS therefore saw nothing wrong, and
* the walk carried on into the sibling subtree -- TODO.md 2.1.3.
* the walk carried on into the sibling subtree.
*
* `path` is the chain of ancestors of `root` and `depth` its length. Checking
* each node against its own ancestry turns a tree that loops back on itself from
* infinite recursion into AKERR_CIRCULAR_REFERENCE, and the depth cap catches the
* degenerate chain that is merely too deep to recurse over (TODO.md 2.2.7).
* degenerate chain that is merely too deep to recurse over.
*/
static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
aksl_TreeNode *root,
@@ -1266,7 +1259,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
* assume it. The switch used to have no default at all, so searchmode 99 --
* and AKSL_TREE_SEARCH_VISIT, which the header documented but nothing
* implemented -- fell straight through to success having visited nothing
* (TODO.md 2.2.9).
* See UPGRADING.md.
*/
switch ( searchmode ) {
case AKSL_TREE_SEARCH_DFS_PREORDER:
@@ -1328,7 +1321,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_L
/*
* From `list`, not from `slow`. The Floyd pass above leaves `slow` at the
* midpoint, and starting the visit there skipped the whole first half of the
* list including the head -- see TODO.md 2.1.2.
* list including the head.
*/
while ( node != NULL ) {
ATTEMPT {

View File

@@ -1,5 +1,5 @@
/*
* stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1.
* stdio.h wrappers beyond fopen/fread/fwrite/fclose.
*
* Positioning, flushing, character and line I/O, stream state, formatted input,
* and the file-level operations that go with them.

View File

@@ -1,12 +1,12 @@
/*
* string.h wrappers -- TODO.md section 3.1.
* string.h wrappers.
*
* This is the section akbasic needed most and could not have: across its source
* it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of
* them raw because there was nothing here to call instead. Ten of those sites
* are the same idiom written out by hand -- a length check, then strncpy, then
* an explicit NUL -- which is exactly the "truncation reported as an error
* rather than silently accepted" that TODO.md 3.1 asks for.
* rather than silently accepted" that the wrapper contract asks for.
*
* Two conventions run through the whole file.
*

View File

@@ -1,5 +1,5 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5.
* NEGATIVE COMPILE TEST.
*
* This file must NOT compile. It is built by the CTest entry
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
@@ -16,11 +16,10 @@
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");
raised = aksl_snprintf(buf, sizeof(buf), "%d", "not an int");
return raised == NULL ? 0 : 1;
}

View File

@@ -1,5 +1,5 @@
/*
* NEGATIVE COMPILE TEST -- TODO.md section 1.9.
* NEGATIVE COMPILE TEST.
*
* 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

View File

@@ -1,5 +1,5 @@
/*
* List and tree additions -- src/collections.c, TODO.md section 3.6.
* List and tree additions -- src/collections.c.
*
* The bare-node list functions, the tracked aksl_List container, and the binary
* search tree. The hash map and string buffer have their own files.
@@ -407,7 +407,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
* 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.
* the cross-cutting wrapper contract exists to catch.
*/
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
{
@@ -800,7 +800,7 @@ static int test_tree_insert_orders_the_leaves(void)
}
/*
* TODO.md 2.2.15: aksl_TreeNode.parent was declared and never touched by
* aksl_TreeNode.parent was declared and never touched by
* anything in the library. These are the functions that set it, and
* aksl_tree_remove is the one that needs it.
*/

View File

@@ -1,7 +1,7 @@
/*
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
*
* TODO.md section 1.4, complete. The cases that used to live in
* Numeric conversion, complete. The cases that used to live in
* tests/test_convert_strict.c -- registered as a known failure because the ato*
* family had no error channel at all (2.1.5) -- are folded back in here now that
* they pass: non-numeric input, empty input, trailing junk and overflow are all
@@ -95,7 +95,7 @@ static int test_trailing_junk_is_a_value_error(void)
/*
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
* rest is trailing junk. TODO.md 1.4 left this open; the answer is that the
* rest is trailing junk. The wrapper plan left this open; the answer is that the
* prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/
* test_strto.c holds that half.
*/
@@ -149,7 +149,7 @@ static int test_atoi_narrows_to_int_range(void)
return 0;
}
/* TODO.md 1.4: the type boundaries round-trip exactly rather than nearly. */
/* The type boundaries round-trip exactly rather than nearly. */
static int test_type_boundaries_round_trip(void)
{
char buf[64];
@@ -229,7 +229,7 @@ static int test_atof_converts_and_rejects_null(void)
}
/*
* TODO.md 1.4 left "inf" and "nan" open. They are accepted, because strtod(3)
* The wrapper plan left "inf" and "nan" open. They are accepted, because strtod(3)
* accepts them and because they are exact round-trips rather than approximations
* of something else -- a caller who did not want them has a domain check to make
* that this library cannot make for it.

154
tests/test_dir.c Normal file
View File

@@ -0,0 +1,154 @@
#include "aksl_capture.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
static int make_directory(char *path, size_t size)
{
const char *tmp = getenv("TMPDIR");
if ( tmp == NULL || tmp[0] == '\0' ) {
tmp = "/tmp";
}
if ( (size_t)snprintf(path, size, "%s/aksl_dir_XXXXXX", tmp) >= size ) {
return 1;
}
return mkdtemp(path) == NULL;
}
static int test_open_errors_and_nulls(void)
{
char file[AKSL_TMP_MAX];
DIR *dirp = (DIR *)1;
struct dirent entry;
/* opendir propagates missing-path and non-directory failures. */
AKSL_CHECK_STATUS(aksl_opendir("/nonexistent/aksl/dir", &dirp), ENOENT);
AKSL_CHECK(dirp == NULL);
AKSL_CHECK(aksl_temp_file(file, sizeof(file)) == 0);
AKSL_CHECK_STATUS(aksl_opendir(file, &dirp), ENOTDIR);
/* Every pointer required by the wrapped operation rejects NULL. */
AKSL_CHECK_STATUS(aksl_opendir(NULL, &dirp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_opendir(".", NULL), AKERR_NULLPOINTER);
/* fdopendir propagates an invalid descriptor and validates its out-param. */
AKSL_CHECK_STATUS(aksl_fdopendir(-1, &dirp), EBADF);
AKSL_CHECK_STATUS(aksl_fdopendir(0, NULL), AKERR_NULLPOINTER);
/* The remaining wrappers reject NULL streams and destinations. */
AKSL_CHECK_STATUS(aksl_readdir(NULL, &entry), AKERR_NULLPOINTER);
AKSL_CHECK_OK(aksl_opendir(".", &dirp));
AKSL_CHECK_STATUS(aksl_readdir(dirp, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_OK(aksl_closedir(dirp));
AKSL_CHECK_STATUS(aksl_closedir(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_rewinddir(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(unlink(file) == 0);
return 0;
}
static int test_copy_eof_rewind_and_fdopendir(void)
{
char path[AKSL_TMP_MAX], first_path[AKSL_TMP_MAX], second_path[AKSL_TMP_MAX];
struct dirent entry, saved;
char first_read[sizeof(entry.d_name)];
DIR *dirp = NULL;
int fd = -1, seen_first = 0, seen_second = 0;
AKSL_CHECK(make_directory(path, sizeof(path)) == 0);
AKSL_CHECK(snprintf(first_path, sizeof(first_path), "%s/first", path) < (int)sizeof(first_path));
AKSL_CHECK(snprintf(second_path, sizeof(second_path), "%s/second", path) < (int)sizeof(second_path));
fd = open(first_path, O_CREAT | O_WRONLY, 0600);
AKSL_CHECK(fd >= 0);
AKSL_CHECK(close(fd) == 0);
fd = open(second_path, O_CREAT | O_WRONLY, 0600);
AKSL_CHECK(fd >= 0);
AKSL_CHECK(close(fd) == 0);
AKSL_CHECK_OK(aksl_opendir(path, &dirp));
do {
akerr_ErrorContext *error = aksl_readdir(dirp, &entry);
if ( error != NULL ) {
int status = error->status;
RELEASE_ERROR(error);
AKSL_CHECK(status == AKERR_EOF);
break;
}
if ( strcmp(entry.d_name, "first") == 0 ) { saved = entry; seen_first++; }
if ( strcmp(entry.d_name, "second") == 0 ) seen_second++;
} while ( 1 );
AKSL_CHECK(seen_first == 1 && seen_second == 1);
AKSL_CHECK(strcmp(saved.d_name, "first") == 0);
AKSL_CHECK_OK(aksl_rewinddir(dirp));
AKSL_CHECK_OK(aksl_readdir(dirp, &entry));
AKSL_CHECK(snprintf(first_read, sizeof(first_read), "%s", entry.d_name) < (int)sizeof(first_read));
AKSL_CHECK_OK(aksl_readdir(dirp, &entry));
AKSL_CHECK_OK(aksl_rewinddir(dirp));
AKSL_CHECK_OK(aksl_readdir(dirp, &entry));
AKSL_CHECK(strcmp(entry.d_name, first_read) == 0);
AKSL_CHECK_OK(aksl_closedir(dirp));
fd = open(path, O_RDONLY | O_DIRECTORY);
AKSL_CHECK(fd >= 0);
AKSL_CHECK_OK(aksl_fdopendir(fd, &dirp));
AKSL_CHECK_OK(aksl_readdir(dirp, &entry));
AKSL_CHECK_OK(aksl_closedir(dirp));
AKSL_CHECK(unlink(first_path) == 0);
AKSL_CHECK(unlink(second_path) == 0);
AKSL_CHECK(rmdir(path) == 0);
return 0;
}
static int test_empty_directory_reaches_eof_after_dot_entries(void)
{
char path[AKSL_TMP_MAX];
struct dirent entry;
DIR *dirp = NULL;
int count = 0, saw_dot = 0, saw_dotdot = 0;
AKSL_CHECK(make_directory(path, sizeof(path)) == 0);
AKSL_CHECK_OK(aksl_opendir(path, &dirp));
for ( ;; ) {
akerr_ErrorContext *error = aksl_readdir(dirp, &entry);
if ( error != NULL ) {
int status = error->status;
RELEASE_ERROR(error);
AKSL_CHECK(status == AKERR_EOF);
break;
}
count++;
if ( strcmp(entry.d_name, ".") == 0 ) saw_dot++;
if ( strcmp(entry.d_name, "..") == 0 ) saw_dotdot++;
}
AKSL_CHECK(count == 2 && saw_dot == 1 && saw_dotdot == 1);
AKSL_CHECK_OK(aksl_closedir(dirp));
AKSL_CHECK(rmdir(path) == 0);
return 0;
}
static int test_permission_denied(void)
{
char path[AKSL_TMP_MAX];
DIR *dirp = NULL;
AKSL_CHECK(make_directory(path, sizeof(path)) == 0);
AKSL_CHECK(chmod(path, 0000) == 0);
if ( geteuid() == 0 ) {
fprintf(stderr, " (skipped: running as root, chmod 000 denies nothing)\n");
} else {
AKSL_CHECK_STATUS(aksl_opendir(path, &dirp), EACCES);
}
AKSL_CHECK(chmod(path, 0700) == 0);
AKSL_CHECK(rmdir(path) == 0);
return 0;
}
int main(void)
{
int failures = 0;
AKSL_RUN(failures, test_open_errors_and_nulls);
AKSL_RUN(failures, test_copy_eof_rewind_and_fdopendir);
AKSL_RUN(failures, test_empty_directory_reaches_eof_after_dot_entries);
AKSL_RUN(failures, test_permission_denied);
AKSL_REPORT(failures);
}

View File

@@ -2,16 +2,14 @@
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
* their va_list forms.
*
* TODO.md section 1.3, now complete. Each happy path asserts both halves of the
* contract -- the byte count handed back through *count and the text that
* actually landed somewhere -- and every pointer argument is checked for its
* NULL guard.
* Formatted output, complete. Each happy path asserts the text that actually
* landed somewhere, and every pointer argument is checked for its NULL guard.
*
* Readback goes through plain libc rather than aksl_fread so that a failure here
* points at the formatted-output wrapper under test and not at the stream
* wrappers, which tests/test_stream.c covers.
*
* aksl_sprintf is gone (TODO.md 2.2.4) and aksl_snprintf takes its place, so the
* aksl_sprintf is gone and aksl_snprintf takes its place, so the
* destination-overflow case that could not previously be written is here: it is
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported.
*/
@@ -39,14 +37,12 @@ static long read_file(const char *path, char *buf, size_t n)
return (long)got;
}
static int test_snprintf_writes_text_and_count(void)
static int test_snprintf_writes_text(void)
{
char buf[64];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7));
AKSL_CHECK(count == 3);
AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s=%d", "x", 7));
AKSL_CHECK(strcmp(buf, "x=7") == 0);
return 0;
}
@@ -54,10 +50,8 @@ static int test_snprintf_writes_text_and_count(void)
static int test_snprintf_empty_format_writes_nothing(void)
{
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
int count = -1;
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", ""));
AKSL_CHECK(count == 0);
AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", ""));
AKSL_CHECK(buf[0] == '\0');
return 0;
}
@@ -66,18 +60,17 @@ static int test_snprintf_empty_format_writes_nothing(void)
* The case that could not be written while the wrapper was aksl_sprintf: output
* longer than the destination. snprintf(3) would truncate, NUL-terminate and
* report the length it *would* have written, leaving the caller to notice; here
* it is an error, and *count is 0 rather than the would-have-been length.
* it is an error.
*/
static int test_snprintf_truncation_is_an_error(void)
{
char buf[8];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
aksl_snprintf(buf, sizeof(buf), "%s", "far too long for eight bytes"),
AKERR_OUTOFBOUNDS, "truncated");
AKSL_CHECK(count == 0);
AKSL_CHECK(strcmp(buf, "far too") == 0);
return 0;
}
@@ -85,34 +78,27 @@ static int test_snprintf_truncation_is_an_error(void)
static int test_snprintf_boundary_is_exact(void)
{
char buf[8];
int count = -1;
/* 7 characters plus the NUL is exactly sizeof(buf). */
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567"));
AKSL_CHECK(count == 7);
AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%s", "1234567"));
AKSL_CHECK(strcmp(buf, "1234567") == 0);
count = -1;
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
AKSL_CHECK_STATUS(aksl_snprintf(buf, sizeof(buf), "%s", "12345678"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
static int test_snprintf_rejects_null_arguments_and_zero_size(void)
{
char buf[8];
int count = 0;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, buf, sizeof(buf), "x"),
AKERR_NULLPOINTER, "count=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"),
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, 8, "x"),
AKERR_NULLPOINTER, "str=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, sizeof(buf), NULL),
AKERR_NULLPOINTER, "format=");
/* size 0 leaves no room even for the terminator, so there is nothing to do. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"),
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(buf, 0, "x"),
AKERR_VALUE, "size=0");
return 0;
}
@@ -139,7 +125,7 @@ static int test_fprintf_writes_to_stream(void)
/*
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
* errno it saw (EBADF on glibc). *count is 0 afterwards, not vfprintf's -1:
* TODO.md 1.3 recorded the negative count as a contract gap, and this is the
* The wrapper plan recorded the negative count as a contract gap, and this is the
* assertion that closes it.
*/
static int test_fprintf_to_read_only_stream_reports_errno(void)
@@ -270,18 +256,18 @@ static int test_asprintf_allocates_to_fit(void)
}
/*
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1
* The va_list forms are what the variadic ones are built on, and the wrapper contract
* wanted them exposed so consumers can write their own variadic wrappers. This
* is a consumer doing exactly that.
*/
static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n,
static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(char *buf, size_t n,
const char *fmt, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, fmt);
raised = aksl_vsnprintf(count, buf, n, fmt, args);
raised = aksl_vsnprintf(buf, n, fmt, args);
va_end(args);
return raised;
}
@@ -289,22 +275,19 @@ static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf
static int test_va_list_forms_are_usable_from_outside(void)
{
char buf[32];
int count = -1;
memset(buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
AKSL_CHECK(count == 5);
AKSL_CHECK_OK(consumer_wrapper(buf, sizeof(buf), "%s/%d", "via", 3));
AKSL_CHECK(strcmp(buf, "via/3") == 0);
/* The error contract survives the extra layer intact. */
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
AKSL_CHECK_STATUS(consumer_wrapper(buf, 4, "%s", "too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(count == 0);
return 0;
}
/*
* Regression cover for the missing va_end (TODO.md 2.1.4). Nothing here can
* Regression cover for the missing va_end. Nothing here can
* assert on register-save state directly; the point is to run the variadic
* wrappers enough times, with enough arguments, that the sanitizer build has
* something to trip over.
@@ -312,14 +295,12 @@ static int test_va_list_forms_are_usable_from_outside(void)
static int test_variadic_wrappers_survive_repeated_calls(void)
{
char buf[128];
int count = 0;
int i = 0;
for ( i = 0; i < 512; i++ ) {
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%d %s %ld %c %f",
AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "%d %s %ld %c %f",
i, "iteration", (long)i, 'x', (double)i));
AKSL_CHECK(count > 0);
AKSL_CHECK((size_t)count == strlen(buf));
AKSL_CHECK(strlen(buf) > 0);
}
return 0;
}
@@ -330,7 +311,7 @@ int main(void)
akerr_init();
AKSL_RUN(failures, test_snprintf_writes_text_and_count);
AKSL_RUN(failures, test_snprintf_writes_text);
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
AKSL_RUN(failures, test_snprintf_boundary_is_exact);

View File

@@ -1,5 +1,5 @@
/*
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6.
* The fixed-capacity hash map and FNV-1a -- src/collections.c.
*
* "The single most obviously-missing data structure in the library", by the
* TODO's own account: akbasic needed one three times over -- variables,

View File

@@ -1,5 +1,5 @@
/*
* Linked list -- TODO.md section 1.7, complete.
* Linked list.
*
* The two confirmed list defects are fixed, so the tests that used to live in
* tests/test_list_append_chain.c and tests/test_list_iterate_head.c are folded
@@ -59,7 +59,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void
/* ---------------------------------------------------------------------- */
/*
* TODO.md 2.2.14: every caller used to have to remember to memset a node before
* every caller used to have to remember to memset a node before
* its first use, and a stack node that skipped it walked straight into garbage.
*/
static int test_node_init_zeroes_the_links(void)
@@ -103,7 +103,7 @@ static int test_append_single_node(void)
* The defect that made this library's list unusable: `tail` was assigned from
* Floyd's `slow` cursor *before* slow advanced, so it tracked the node behind
* the midpoint rather than the last node. Appending n1..n4 to n0 produced the
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1.
* chain "n0 -> n4" and silently dropped n1, n2 and n3.
*/
static int test_append_builds_the_whole_chain(void)
{
@@ -218,7 +218,7 @@ static int test_append_detects_cycle_below_the_head(void)
}
/*
* TODO.md 1.7 asked for the aliasing contract to be defined. It is refusal:
* The wrapper plan asked for the aliasing contract to be defined. It is refusal:
* relinking a node that is already in the list would orphan everything between
* its old position and the tail, so the tail walk -- which happens anyway --
* doubles as the check.
@@ -281,7 +281,7 @@ static int test_iterate_single_node(void)
* Every node, exactly once, in order, starting at the head. The cycle check
* leaves Floyd's `slow` cursor at the list midpoint, and the visiting loop used
* to start from there -- so the whole first half of the list, head included, was
* never passed to the callback at all. TODO.md 2.1.2.
* never passed to the callback at all.
*/
static int test_iterate_visits_every_node_from_the_head(void)
{
@@ -470,7 +470,7 @@ static int test_pop_middle_node(void)
}
/*
* TODO.md 2.2.12: popping the head used to leave the caller's own head pointer
* popping the head used to leave the caller's own head pointer
* aimed at a node that was no longer in the list, with no way to learn the new
* one. That is what the head out-param is for, and this is the assertion.
*/
@@ -572,7 +572,7 @@ static int test_pop_then_iterate(void)
}
/*
* TODO.md 1.7's pool-accounting case. AKSL_RUN already asserts that each test
* The wrapper plan's pool-accounting case. AKSL_RUN already asserts that each test
* leaves the pool as it found it; this one drives enough failures in a row to
* exhaust the pool several times over, which is where a wrapper that raises an
* error and forgets to release it shows up as an outright exhaustion rather than

View File

@@ -1,8 +1,8 @@
/*
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
* Memory wrappers, complete, plus the additions from
* section 3.1.
*
* The three cases 1.1 left open are all pinned here: malloc(0) is AKERR_VALUE
* The three cases the wrapper plan left open are all pinned here: malloc(0) is AKERR_VALUE
* rather than whatever errno happened to hold when the platform's malloc(0)
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
@@ -32,7 +32,7 @@ static int test_malloc_rejects_null_destination(void)
}
/*
* TODO.md 1.1 asked for this contract to be pinned down. malloc(0) is allowed to
* The wrapper plan asked for this contract to be pinned down. malloc(0) is allowed to
* return either a unique pointer or NULL, and a NULL there is not a failure and
* need not set errno -- so the old wrapper could raise an error whose status was
* 0, which every DETECT downstream reads as success while the context holds a
@@ -296,7 +296,7 @@ static int test_memcpy_zero_length_is_noop(void)
}
/*
* TODO.md 1.1's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
* The wrapper plan's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
* it undefined behaviour, which in practice means "works until the day a
* compiler version or a length changes and it does not". Callers who mean to
* overlap want aksl_memmove, and the message says so.

View File

@@ -1,12 +1,12 @@
/*
* aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete.
* aksl_realpath and aksl_realpath_alloc.
*
* The happy paths compare against realpath(3) itself rather than against a
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
* and friends) and the resolved answer is what the platform says it is.
*
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path
* used to be the crash case: the wrapper's own error path
* formatted the buffer with %s while realpath(3) leaves its contents
* unspecified on failure, so the library read uninitialised memory while
* reporting an error. The message names only the input path now, and this test
@@ -122,7 +122,7 @@ static int test_rejects_null_arguments(void)
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
AKERR_NULLPOINTER, "path=");
/*
* TODO.md 2.1.6: this used to be unchecked, and realpath(path, NULL)
* this used to be unchecked, and realpath(path, NULL)
* allocated a buffer that the wrapper then discarded and leaked.
*/
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),

View File

@@ -1,5 +1,5 @@
/*
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
* Cross-cutting properties every wrapper has to have.
*
* Two of them, and neither is about what any individual function computes:
*
@@ -109,14 +109,13 @@ static int test_stream_wrappers_do_not_leak_slots(void)
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"),
AKSL_CHECK_OK(aksl_snprintf(buf, sizeof(buf), "x"));
AKSL_CHECK_STATUS(aksl_snprintf(buf, 4, "%s", "far too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
@@ -266,7 +265,6 @@ static int test_traversal_failures_do_not_leak_slots(void)
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;
@@ -295,7 +293,7 @@ static int test_errors_name_their_origin_in_stdlib(void)
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_STATUS(aksl_snprintf(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. */

134
tests/test_stat.c Normal file
View File

@@ -0,0 +1,134 @@
/* File metadata wrapper tests. */
#include "aksl_capture.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
/*
* A bit libc has never assigned in the AT_ space. fstatat(2) must reject it
* with EINVAL rather than ignoring it, which is what proves flags reach libc
* unmasked. Re-check this constant if AT_ ever grows into 0x40000000.
*/
#define AKSL_TEST_AT_INVALID 0x40000000
static int test_stat_success_and_fstat(void)
{
char path[AKSL_TMP_MAX];
int fd = -1;
struct stat path_dest;
struct stat fd_dest;
struct statvfs vfs_dest;
/* A real file makes the mode and four-byte size observable to stat(2). */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
fd = open(path, O_RDWR);
AKSL_CHECK(fd >= 0);
AKSL_CHECK(write(fd, "stat", 4) == 4);
AKSL_CHECK_OK(aksl_stat(path, &path_dest));
AKSL_CHECK(S_ISREG(path_dest.st_mode));
AKSL_CHECK(path_dest.st_size == 4);
/* fstat(2) addresses the same opened inode, not a second pathname lookup. */
AKSL_CHECK_OK(aksl_fstat(fd, &fd_dest));
AKSL_CHECK(fd_dest.st_ino == path_dest.st_ino);
/* fstatvfs(3) describes the backing filesystem and has a usable block size. */
AKSL_CHECK_OK(aksl_fstatvfs(fd, &vfs_dest));
AKSL_CHECK(vfs_dest.f_frsize != 0);
AKSL_CHECK(close(fd) == 0);
/* A descriptor that was just closed must surface libc's EBADF. */
AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF);
AKSL_CHECK_STATUS(aksl_fstatvfs(fd, &vfs_dest), EBADF);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_stat_paths_and_fstatat(void)
{
char path[AKSL_TMP_MAX];
char child[AKSL_TMP_MAX * 2];
char directory[AKSL_TMP_MAX];
char linkpath[AKSL_TMP_MAX * 2];
char *basename = NULL;
int dirfd = -1;
struct stat dest;
struct stat ldest;
struct stat path_dest;
struct statvfs vdest;
/* Missing paths and a regular file used as a directory preserve errno. */
AKSL_CHECK_STATUS(aksl_stat("/nonexistent/aksl/stat", &dest), ENOENT);
AKSL_CHECK_STATUS(aksl_lstat("/nonexistent/aksl/stat", &ldest), ENOENT);
/* The temporary path is a regular file, so adding a child tests ENOTDIR. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
AKSL_CHECK(snprintf(child, sizeof(child), "%s/child", path) < (int)sizeof(child));
AKSL_CHECK_OK(aksl_stat(path, &path_dest));
AKSL_CHECK_STATUS(aksl_stat(child, &dest), ENOTDIR);
AKSL_CHECK(snprintf(linkpath, sizeof(linkpath), "%s.link", path) < (int)sizeof(linkpath));
AKSL_CHECK(symlink(path, linkpath) == 0);
/* stat follows the link while lstat reports the link object itself. */
AKSL_CHECK_OK(aksl_stat(linkpath, &dest));
AKSL_CHECK_OK(aksl_lstat(linkpath, &ldest));
AKSL_CHECK(S_ISREG(dest.st_mode));
AKSL_CHECK(S_ISLNK(ldest.st_mode));
/* A real dirfd must resolve a relative name against that directory. */
AKSL_CHECK(snprintf(directory, sizeof(directory), "%s", path) < (int)sizeof(directory));
basename = strrchr(directory, '/');
AKSL_CHECK(basename != NULL);
*basename = '\0';
dirfd = open(directory, O_RDONLY | O_DIRECTORY);
AKSL_CHECK(dirfd >= 0);
AKSL_CHECK_OK(aksl_fstatat(dirfd, strrchr(path, '/') + 1, &dest, 0));
AKSL_CHECK(dest.st_ino == path_dest.st_ino);
AKSL_CHECK(close(dirfd) == 0);
/* A valid non-zero flag must reach libc, making this call an lstat. */
AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, linkpath, &dest, AT_SYMLINK_NOFOLLOW));
AKSL_CHECK(S_ISLNK(dest.st_mode));
/* Invalid flags must replace stale errno with the EINVAL libc reports. */
errno = E2BIG;
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, AKSL_TEST_AT_INVALID), EINVAL);
/* statvfs reports ENOENT before it can describe a missing filesystem path. */
AKSL_CHECK_STATUS(aksl_statvfs("/nonexistent/aksl/stat", &vdest), ENOENT);
/* statvfs reports the filesystem containing the current directory. */
AKSL_CHECK_OK(aksl_statvfs(".", &vdest));
AKSL_CHECK(vdest.f_frsize != 0);
AKSL_CHECK(unlink(linkpath) == 0);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
static int test_stat_null_arguments(void)
{
char path[AKSL_TMP_MAX];
struct stat dest;
struct statvfs vdest;
/* Create a valid path so each failure below isolates a NULL argument. */
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
/* Every wrapper refuses either missing caller-owned input or output storage. */
AKSL_CHECK_STATUS(aksl_stat(NULL, &dest), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_stat(path, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_lstat(NULL, &dest), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_lstat(path, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fstat(0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, NULL, &dest, 0), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, NULL, 0), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_statvfs(NULL, &vdest), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_statvfs(path, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fstatvfs(0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(unlink(path) == 0);
return 0;
}
int main(void)
{
int failures = 0;
AKSL_RUN(failures, test_stat_success_and_fstat);
AKSL_RUN(failures, test_stat_paths_and_fstatat);
AKSL_RUN(failures, test_stat_null_arguments);
AKSL_REPORT(failures);
}

View File

@@ -1,5 +1,5 @@
/*
* The growable string buffer -- src/collections.c, TODO.md 3.6.
* The growable string buffer -- src/collections.c.
*
* The bounded formatting wrappers are the right answer when the destination is
* a fixed buffer and no answer at all when the length is not known in advance.

View File

@@ -1,7 +1,7 @@
/*
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
*
* TODO.md section 1.2, now complete. The happy paths, the round trip, every
* Stream wrappers, complete. The happy paths, the round trip, every
* NULL guard, both stream-error statuses, the transferred-member count that
* aksl_fread and aksl_fwrite report through nmemb_out, and the three cases that
* needed a hostile file to produce: a mode-denied path (EACCES), a full device
@@ -81,7 +81,7 @@ static int test_fopen_rejects_null_arguments(void)
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.2: both of these used to go straight through to fopen(3). */
/* both of these used to go straight through to fopen(3). */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp),
AKERR_NULLPOINTER, "pathname=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp),
@@ -144,7 +144,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
AKERR_EOF, "EOF");
/* TODO.md 2.2.3: this is what the caller could not previously find out. */
/* this is what the caller could not previously find out. */
AKSL_CHECK(moved == 4);
AKSL_CHECK_OK(aksl_fclose(fp));
@@ -160,7 +160,7 @@ static int test_fread_short_read_is_eof_and_reports_the_count(void)
*
* That is a change from the old wrapper, which read ferror() and reported
* AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
* (TODO.md 2.2.1) keeps AKERR_IO as the fallback for the case where the stream
* keeps AKERR_IO as the fallback for the case where the stream
* is in error and errno says nothing, and hands back the real reason otherwise.
*/
static int test_fread_from_write_only_stream_reports_errno(void)
@@ -194,7 +194,7 @@ static int test_fread_rejects_null_arguments(void)
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
AKERR_NULLPOINTER, "fp=");
/* TODO.md 2.2.3: ptr was never checked in either direction. */
/* ptr was never checked in either direction. */
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved),
AKERR_NULLPOINTER, "ptr=");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),

View File

@@ -1,5 +1,5 @@
/*
* Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1.
* Stream wrappers beyond open/read/write/close -- src/stream.c.
*
* tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers
* positioning, flushing, character and line I/O, stream state, and formatted

View File

@@ -1,5 +1,5 @@
/*
* aksl_strhash_djb2 -- TODO.md section 1.6.
* aksl_strhash_djb2.
*
* The expected values are the canonical djb2 ones: h = 5381, then
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
@@ -7,7 +7,7 @@
*
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
* the test therefore says nothing either way about byte signedness. The high-bit
* vector is the one that pins TODO.md 2.2.6 down.
* vector is the one that pins the sign-extension defect down.
*/
#include "aksl_capture.h"
@@ -47,7 +47,7 @@ static int test_known_answer_vectors(void)
}
/*
* TODO.md 2.2.6, pinned. The cursor is an unsigned char * now, so bytes at or
* Pinned. The cursor is an unsigned char * now, so bytes at or
* above 0x80 contribute their unsigned value. Iterating a plain char * on x86 or
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
* that disagreed with canonical djb2 and, worse, disagreed with itself across
@@ -68,7 +68,7 @@ static int test_high_bit_bytes_are_unsigned(void)
return 0;
}
/* The NUL-terminated convenience form (TODO.md 3.6) agrees with the length one. */
/* The NUL-terminated convenience form agrees with the length one. */
static int test_str_form_matches_the_length_form(void)
{
const char *s = "libakstdlib";

View File

@@ -1,5 +1,5 @@
/*
* String wrappers -- src/string.c, TODO.md section 3.1.
* String wrappers -- src/string.c.
*
* The two contracts worth testing hardest are the ones that differ from libc:
* every copying function takes the destination size and treats truncation as an

View File

@@ -1,5 +1,5 @@
/*
* The strto* family -- TODO.md section 3.1.
* The strto* family.
*
* These are the real implementation behind the ato* wrappers and the thing
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because

View File

@@ -1,5 +1,5 @@
/*
* Tree traversal -- TODO.md section 1.8, complete.
* Tree traversal.
*
* The old version of this file counted steps, which cannot tell the three
* depth-first orders apart because all three visit all seven nodes -- and could
@@ -183,7 +183,7 @@ static int test_dfs_is_an_alias_for_preorder(void)
/*
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
* serve it were defaulted and then never called -- TODO.md 2.2.8 and 2.2.10.
* serve it were defaulted and then never called. See UPGRADING.md.
* Both modes work now, and the allocator test below proves the queue is real.
*/
static int test_bfs_visits_level_by_level(void)
@@ -249,7 +249,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
}
/*
* TODO.md 1.8: "Custom lalloc/lfree are actually invoked -- currently they are
* The wrapper plan asked that "custom lalloc/lfree are actually invoked -- currently they are
* stored and never called". They are called now, once per node enqueued, and
* every allocation is released. The depth-first modes allocate nothing at all,
* which is the other half of the contract.
@@ -382,7 +382,7 @@ static int test_degenerate_chains(void)
}
/*
* TODO.md 1.8 / 2.2.7: a chain deeper than the recursion can take. It used to
* A chain deeper than the recursion can take. It used to
* overflow the stack; it is AKERR_OUTOFBOUNDS now, and the message names the
* documented limit. Built one node past the cap so the failure is the cap itself
* and not some incidental shortfall. `static` because AKSL_TREE_MAX_DEPTH nodes
@@ -488,7 +488,7 @@ static int test_cyclic_tree_is_caught(void)
* that raised the break handled it in its own PROCESS/HANDLE block and returned
* success, so the parent frame's PASS saw nothing wrong and carried straight on
* into the sibling subtree. All seven nodes were visited no matter where the
* break was raised. TODO.md 2.1.3.
* break was raised.
*
* One case per order, each breaking on a node that is *not* last in that order --
* which is precisely what the old test could not do, because it hid its target
@@ -579,7 +579,7 @@ static int test_null_arguments(void)
}
/*
* TODO.md 2.2.9: the switch had no default, so an unrecognised mode -- and
* the switch had no default, so an unrecognised mode -- and
* AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented --
* fell straight through to SUCCEED_RETURN having visited nothing at all. A
* traversal that silently did not happen, reported as success.

View File

@@ -1,5 +1,5 @@
/*
* Version reporting -- TODO.md section 2.2.16.
* Version reporting.
*
* There are two versions in play and the whole point of this API is that they
* are allowed to differ: