Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e763183cc4 | |||
| cff2a64575 |
@@ -14,7 +14,6 @@ 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/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/string.c` | the `string.h` surface |
|
||||||
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
|
| `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/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
|
||||||
| `src/aksl_internal.h` | shared internals; not installed, not public |
|
| `src/aksl_internal.h` | shared internals; not installed, not public |
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ add_library(akstdlib SHARED
|
|||||||
src/string.c
|
src/string.c
|
||||||
src/stream.c
|
src/stream.c
|
||||||
src/stat.c
|
src/stat.c
|
||||||
src/dir.c
|
|
||||||
src/collections.c
|
src/collections.c
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -309,7 +308,6 @@ install(FILES
|
|||||||
set(AKSL_TESTS
|
set(AKSL_TESTS
|
||||||
collections
|
collections
|
||||||
convert
|
convert
|
||||||
dir
|
|
||||||
format
|
format
|
||||||
hashmap
|
hashmap
|
||||||
linkedlist
|
linkedlist
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -36,7 +36,7 @@ that will surprise you:
|
|||||||
| `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. |
|
| `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. |
|
||||||
| `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. |
|
| `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. |
|
||||||
| `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. |
|
| `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. |
|
||||||
| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success; `*count` receives the required length. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. |
|
| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. |
|
||||||
| `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. |
|
| `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. |
|
||||||
| `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. |
|
| `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. |
|
||||||
| `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. |
|
| `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. |
|
||||||
@@ -181,8 +181,9 @@ would notice.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
There are four harnesses. The first three take seconds; the fourth takes about
|
There are five harnesses. The first three take seconds and the fifth is instant;
|
||||||
half an hour.
|
the fourth takes about half an hour. The fifth is the only one that measures
|
||||||
|
something outside this repository.
|
||||||
|
|
||||||
### 1. The test suite
|
### 1. The test suite
|
||||||
|
|
||||||
@@ -469,6 +470,41 @@ right-leaning tree would have blown the stack the depth cap exists to protect),
|
|||||||
and `aksl_tree_remove` on an empty tree, which without its guard dereferences
|
and `aksl_tree_remove` on an empty tree, which without its guard dereferences
|
||||||
NULL. Both are in the suite now — which is what the harness is for.
|
NULL. Both are in the suite now — which is what the harness is for.
|
||||||
|
|
||||||
|
### 5. Consumer adoption
|
||||||
|
|
||||||
|
Coverage says the tests reach the code and mutation testing says they would
|
||||||
|
notice it breaking. Neither says anybody *wanted* the code. That question only has
|
||||||
|
an external answer, so there is a harness for it too:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/consumer_calls.py ../akbasic/src # the ratio
|
||||||
|
scripts/consumer_calls.py ../akbasic/src --detail --per-file # where it comes from
|
||||||
|
scripts/consumer_calls.py ../akbasic/src --baseline 313/7 # against a past count
|
||||||
|
```
|
||||||
|
|
||||||
|
It counts, across a consumer's source directory, how often that consumer calls
|
||||||
|
this library against how often it reaches past it to the libc function this
|
||||||
|
library wraps. Calls to libc functions **not** wrapped here — `isdigit`, `exit`,
|
||||||
|
`qsort` — score on neither side; the question is how often an *available* wrapper
|
||||||
|
gets bypassed. Comments and string literals are stripped before counting, and the
|
||||||
|
wrapped-libc set is read out of `include/akstdlib.h` rather than hardcoded, so a
|
||||||
|
recount after a release measures the surface that release actually shipped.
|
||||||
|
|
||||||
|
A wrapper nobody calls either does not fit or is not discoverable, and both are
|
||||||
|
this library's problem rather than the consumer's. `TODO.md` carries the standing
|
||||||
|
figures and what they did and did not justify.
|
||||||
|
|
||||||
|
Two warnings, both learned the hard way and both printed by `--baseline`:
|
||||||
|
|
||||||
|
- **A rate is only comparable between two counts of the same tree.** If the
|
||||||
|
consumer grew between them, compare the percentage and say which commit each
|
||||||
|
number came from. Comparing the totals across a tree that tripled in size is how
|
||||||
|
a real improvement gets reported as a regression, or the reverse.
|
||||||
|
- **One consumer's ratio is evidence, not a plan.** A consumer that draws
|
||||||
|
everything from fixed pools will never call the allocator however good the
|
||||||
|
allocator is. Weight the result by what the consumer is, and get a second
|
||||||
|
consumer before treating any ranking as settled.
|
||||||
|
|
||||||
## The pre-push hook
|
## The pre-push hook
|
||||||
|
|
||||||
`.githooks/pre-push` runs the fast harnesses — the default build and the
|
`.githooks/pre-push` runs the fast harnesses — the default build and the
|
||||||
|
|||||||
165
TODO.md
165
TODO.md
@@ -22,6 +22,7 @@ it has been through grooming.
|
|||||||
| Function coverage | 100% (154/154) |
|
| Function coverage | 100% (154/154) |
|
||||||
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
|
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
|
||||||
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
|
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
|
||||||
|
| Consumer adoption | akbasic ported onto 0.2.0 calls this library 313 times and raw libc 7 — **2.2% bypassed**, from 92.2% at first count. Ungated, and one consumer only |
|
||||||
|
|
||||||
The six confirmed defects that used to head this file are fixed and
|
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,
|
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a result,
|
||||||
@@ -102,15 +103,16 @@ are fixed: the right child's `depth + 1` in the depth-first walk, and
|
|||||||
|
|
||||||
## 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
|
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a C interpreter built on this
|
||||||
built on this library and `libakerror`. It was the first consumer to exercise the
|
library and `libakerror` — ~6,300 lines of `src/` when it was first measured,
|
||||||
whole surface rather than a corner of it, **and what it could not use is what
|
20,169 now. It was the first consumer to exercise the whole surface rather than a
|
||||||
prioritised everything that has been built since.**
|
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
|
**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
|
library and 119 to raw libc** — a library whose value proposition is "turn silent
|
||||||
libc failures into error contexts", bypassed 92% of the time by the consumer most
|
libc failures into error contexts", bypassed **92%** of the time by the consumer
|
||||||
committed to it.
|
most committed to it.
|
||||||
|
|
||||||
| Raw libc it had to use | Count | Now available as |
|
| Raw libc it had to use | Count | Now available as |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -121,8 +123,17 @@ committed to it.
|
|||||||
| `strncpy` | 15 | `aksl_strncpy` |
|
| `strncpy` | 15 | `aksl_strncpy` |
|
||||||
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
|
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
|
||||||
| `fgets` | 2 | `aksl_fgets` |
|
| `fgets` | 2 | `aksl_fgets` |
|
||||||
|
| `strncmp` | 1 | `aksl_strncmp` |
|
||||||
|
| `memmove` | 1 | `aksl_memmove` |
|
||||||
| `strstr` | 1 | `aksl_strstr` |
|
| `strstr` | 1 | `aksl_strstr` |
|
||||||
|
|
||||||
|
Two corrections to that figure, both found by rebuilding it. It used to read 116;
|
||||||
|
the table it sat above summed to 117 and had no row for `strncmp` or `memmove`.
|
||||||
|
119 is what `scripts/consumer_calls.py` returns against akbasic `4e188b2`, and it
|
||||||
|
is the number everything below compares to. **The method is now a script rather
|
||||||
|
than a paragraph, because recovering it afterwards cost more than writing it down
|
||||||
|
would have.**
|
||||||
|
|
||||||
**All four things the port had to write for itself now exist here.**
|
**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
|
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). The
|
||||||
@@ -144,8 +155,138 @@ committed to it.
|
|||||||
the sign-extended djb2 reads bytes unsigned; and the missing `va_end` — which
|
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.
|
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
|
### The recount, against this release
|
||||||
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
|
akbasic's `src/` was ported onto 0.2.0 and counted again (#26). The port builds
|
||||||
does**, so one consumer's count is evidence, not a plan. Re-counting against this
|
clean at `-Wall -Wextra`, passes **112/112** of akbasic's ctest suite, and is
|
||||||
release is #26.
|
ASan+UBSan-clean.
|
||||||
|
|
||||||
|
| | libakstdlib | raw libc | bypassed |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Baseline — akbasic `4e188b2`, 5,679 lines of `src/` | 10 | 119 | **92.2%** |
|
||||||
|
| Before the port — akbasic `330d731`, 20,169 lines | 45 | 285 | **86.4%** |
|
||||||
|
| **After the port — same tree** | **313** | **7** | **2.2%** |
|
||||||
|
|
||||||
|
**Read the third row against the second, not the first.** The tree tripled between
|
||||||
|
the baseline and the port, so 10/119 and 45/285 are counts of two different
|
||||||
|
programs; only 86.4% → 2.2% is a like-for-like measurement. The 45 in the middle
|
||||||
|
row is worth its own note — akbasic had already adopted `aksl_f*` across
|
||||||
|
`runtime_disk.c` on its own, without anybody counting.
|
||||||
|
|
||||||
|
**Nothing was blocked by a missing wrapper.** Every libc call akbasic makes had an
|
||||||
|
`aksl_*` counterpart. 278 of the 285 sites converted; the 7 that did not are
|
||||||
|
blocked by wrapper *shape*, and they are the useful output:
|
||||||
|
|
||||||
|
| Why it could not be used | Sites | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| Truncation is the answer, not the error | 6 | `runtime.c` `akbasic_runtime_error`, `host.c` (×3), `runtime_struct.c` (×2) |
|
||||||
|
| A `bsearch(3)` comparator, whose signature libc fixes, so there is no out parameter to report through | 1 | `verbs.c` `verb_compare` |
|
||||||
|
|
||||||
|
**The eight sites that used to head this table are gone, and how they went is the
|
||||||
|
finding.** They were the `bool` predicates and `void` helpers with no error channel
|
||||||
|
to route into, and the first filing (#38) asked this library for a form they could
|
||||||
|
call. Andrew ruled that invalid: a function that cannot report an error changes its
|
||||||
|
own signature rather than being handed a way to swallow one. Seven of the eight did
|
||||||
|
exactly that — they now return `akerr_ErrorContext *` and hand the answer back
|
||||||
|
through an out parameter, one of them (`akbasic_environment_is_waiting_for` and its
|
||||||
|
sibling) as a public header change. Four more functions on the same call chains
|
||||||
|
(`scanner.c` `peek` and `match_next_char`, `sink_akgl.c` `putchar_at`, `echo_line`
|
||||||
|
and `edit_key`) had to move with them. **The wrapper shape was right and the
|
||||||
|
consumer's signatures were wrong**, which is not what the first count assumed.
|
||||||
|
|
||||||
|
The truncation six are a contract decision rather than an accident, and they are
|
||||||
|
what is genuinely left. The sharpest is `akbasic_runtime_error` — the one function
|
||||||
|
that tells the user *what went wrong*, formatting a 12,384-byte error message into
|
||||||
|
a 512-byte line. Truncating a report there is correct; raising `AKERR_OUTOFBOUNDS`
|
||||||
|
would replace the diagnosis with a second, different failure. Two more are
|
||||||
|
truncation-tolerant renderers that print what fits and stop, one of which reads
|
||||||
|
`snprintf`'s return value to *detect* the truncation and skip the rest of the
|
||||||
|
render. The remaining three read host-supplied strings into fixed fields.
|
||||||
|
|
||||||
|
Two of those three, in `akbasic/src/host.c`, are a latent defect the port surfaced
|
||||||
|
rather than a decision: a host-registered type name over 31 characters truncates
|
||||||
|
silently, and two names sharing a 31-character prefix then collide in
|
||||||
|
`akbasic_structtype_find` — where `structtype.c` refuses the identical case
|
||||||
|
outright with a limit message. The two registration paths disagree. That is
|
||||||
|
akbasic's to fix, and it is flagged at the site.
|
||||||
|
|
||||||
|
### What the recount found, and where it went
|
||||||
|
|
||||||
|
Every blocked site came back to wrapper *shape* rather than a missing wrapper, and
|
||||||
|
the same seven shapes recurred across ten independent conversion passes. They are
|
||||||
|
filed, not listed here:
|
||||||
|
|
||||||
|
| Finding | Filed as |
|
||||||
|
|---|---|
|
||||||
|
| `aksl_snprintf`'s `count` out-param is required, so ~20 sites carry an `int written` that is written and never read. Raised by all ten passes. `-Wall -Wextra` cannot see it — `&written` is a use | #32 |
|
||||||
|
| No equality comparison. All 43 comparison sites flatten the three-way `int` to `== 0`; not one wants an ordering, and five now need a sentinel whose *initial value is load-bearing* | #33 |
|
||||||
|
| No truncating format and no length query, which is the whole of the truncation-six above and the only shape still blocking a conversion | #34 |
|
||||||
|
| `aksl_hashmap_*` carries one payload, which is the only reason `akbasic/src/symtab.c` still exists | #35 |
|
||||||
|
| `aksl_fgets` signals end of input by raising, so a read loop cannot be a condition | #36 |
|
||||||
|
| A caller cannot add its own context to a wrapper's error, so it raises and discards instead — eight lines where there were two | #37 |
|
||||||
|
| No form a `bool` predicate or a `void` function can call, which was 8 of the 13 sites the first count could not convert. **Ruled invalid** — the consumer changes its own signature, and now has | #38 |
|
||||||
|
|
||||||
|
**#38 is the one worth reading, because it is the one that was wrong.** It asked
|
||||||
|
this library to grow a form a `bool` predicate could call, and the answer was that
|
||||||
|
a predicate which cannot report an error should stop returning `bool`. Seven of its
|
||||||
|
eight sites converted on that basis, and they are why the count is 2.2% and not 4.1%.
|
||||||
|
The `ctype.h` half of the same filing is settled too: `isspace`, `isdigit`,
|
||||||
|
`isalnum` and `toupper` cannot fail, so there is nothing for a wrapper to return
|
||||||
|
and no reason to add one. What a caller does need is the `(unsigned char)` cast
|
||||||
|
every correct `ctype.h` call takes, and that is akbasic's note to keep, not this
|
||||||
|
library's.
|
||||||
|
|
||||||
|
The `bsearch` comparator has **no issue of its own**. #38 attributed it to akbasic
|
||||||
|
`#14`, which is a mis-citation — that issue is the `COLLISION`/`BUMP` pairing
|
||||||
|
threshold. Converting the comparator means dropping `bsearch(3)` for an in-house
|
||||||
|
binary search that can propagate, on a lookup that runs once per scanned
|
||||||
|
identifier, and that wants filing against akbasic before anybody does it.
|
||||||
|
|
||||||
|
**The one thing the wrappers did better than the libc they replaced** is worth
|
||||||
|
recording next to the complaints: `aksl_fgets`'s `len_out` **deleted** two `strlen`
|
||||||
|
calls rather than converting them, and is more correct than what it replaced for a
|
||||||
|
line containing an embedded NUL. It is the only one of 278 conversions that
|
||||||
|
produced less code than it started with.
|
||||||
|
|
||||||
|
**The port also found a defect in akbasic rather than in this library.** `DLOAD`
|
||||||
|
leaked a file descriptor: its read loop sat inside an `ATTEMPT` block and the
|
||||||
|
`PASS` in it returned past `CLEANUP`, so a scan error left the file open. Hoisting
|
||||||
|
the loop into its own helper — which converting `fgets` required anyway, because
|
||||||
|
neither `CATCH` nor `PASS` is legal in a loop inside an `ATTEMPT` — fixes it. That
|
||||||
|
is the protocol's own rule catching a real leak the moment somebody had to obey it.
|
||||||
|
|
||||||
|
### Still true, and still the reason one count is not a plan
|
||||||
|
|
||||||
|
akbasic uses no allocator, no lists and no trees, drawing everything from fixed
|
||||||
|
pools by design, and porting it did not change that. Of the 313 calls it now
|
||||||
|
makes:
|
||||||
|
|
||||||
|
| Area | Calls | |
|
||||||
|
|---|---|---|
|
||||||
|
| Strings | 149 | 47.6% |
|
||||||
|
| Memory | 72 | 23.0% |
|
||||||
|
| Formatted output | 43 | 13.7% |
|
||||||
|
| Streams and files | 36 | 11.5% |
|
||||||
|
| String → number | 12 | 3.8% |
|
||||||
|
| Hashing | 1 | 0.3% |
|
||||||
|
| **Collections** | **0** | **0%** |
|
||||||
|
|
||||||
|
**Five sixths of the evidence is strings, memory and formatting.** The collections
|
||||||
|
work — list, tree, hash map, string buffer, `src/collections.c` and the largest
|
||||||
|
single body of code in this library — has **not one consumer call site**, and the
|
||||||
|
single hashing call next to it is `aksl_strhash_djb2` feeding a hash table akbasic
|
||||||
|
wrote for itself. **A consumer that does allocate would weight the
|
||||||
|
`open`/`read`/`write` work far higher than this one does**, so this remains
|
||||||
|
evidence and not a plan.
|
||||||
|
|
||||||
|
**The number to distrust is not the 2.2%; it is the 0%.** A recount that moves
|
||||||
|
92% to 2% on one consumer says the string, memory and format wrappers fit the
|
||||||
|
consumer that asked for them. It says nothing at all about the half of the library
|
||||||
|
that consumer never calls, and it cannot, however many times it is run. What would
|
||||||
|
say something is a second consumer with different shape — one that allocates.
|
||||||
|
|
||||||
|
`akbasic/src/symtab.c` is the sharpest instance. It is the hand-rolled fixed-capacity
|
||||||
|
string-keyed hash table `aksl_hashmap_*` was generalised from, it survived the port
|
||||||
|
untouched, and the reason turned out to be one field rather than a design
|
||||||
|
disagreement — everything else about the two already lines up. #35 has it, and it
|
||||||
|
is the first collections work with a consumer actually waiting for it.
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ aksl_sprintf(&count, buf, "%s=%d", key, value);
|
|||||||
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
|
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
|
||||||
```
|
```
|
||||||
|
|
||||||
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` receives the required length.
|
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` is
|
||||||
|
`0` on any failure rather than `vsprintf`'s `-1`.
|
||||||
|
|
||||||
**`aksl_realpath` takes the destination's length.**
|
**`aksl_realpath` takes the destination's length.**
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,6 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* What this header needs in its own declarations, and no more:
|
* What this header needs in its own declarations, and no more:
|
||||||
* dirent.h DIR, struct dirent
|
|
||||||
* stdio.h FILE
|
* stdio.h FILE
|
||||||
* stddef.h size_t
|
* stddef.h size_t
|
||||||
* stdint.h uint32_t
|
* stdint.h uint32_t
|
||||||
@@ -73,12 +72,11 @@
|
|||||||
* which every consumer then got whether it wanted them or not. stddef.h in place
|
* 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.
|
* of stdlib.h is the same size_t at a fraction of the namespace.
|
||||||
*/
|
*/
|
||||||
#include <dirent.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/statvfs.h>
|
#include <sys/statvfs.h>
|
||||||
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
|
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
|
||||||
@@ -444,9 +442,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
|
|||||||
/* ====================================================================== */
|
/* ====================================================================== */
|
||||||
/** @name Formatted output
|
/** @name Formatted output
|
||||||
*
|
*
|
||||||
* Bounded output is checked for truncation and reports an error when the result
|
* `*count` is the byte count written excluding the terminating NUL, and is 0 on
|
||||||
* does not fit. `*count` receives the number of bytes written, or the complete
|
* every failure path -- never vsnprintf's -1, and never the length the output
|
||||||
* output length when truncation occurs.
|
* *would* have been.
|
||||||
*
|
*
|
||||||
* There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an
|
* 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
|
* error-handling wrapper around an unbounded write is the sharp edge this
|
||||||
@@ -457,7 +455,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, v
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief printf(3) to stdout.
|
* @brief printf(3) to stdout.
|
||||||
* @param[out] count Bytes written. Required.
|
* @param[out] count Bytes written; 0 on failure. Required.
|
||||||
* @param[in] format printf format string. Required. Checked at compile time.
|
* @param[in] format printf format string. Required. Checked at compile time.
|
||||||
* @throws AKERR_NULLPOINTER If count or format is NULL.
|
* @throws AKERR_NULLPOINTER If count or format is NULL.
|
||||||
* @throws AKERR_IO Or the errno the C library saw, if the write fails.
|
* @throws AKERR_IO Or the errno the C library saw, if the write fails.
|
||||||
@@ -467,7 +465,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief fprintf(3) to a stream.
|
* @brief fprintf(3) to a stream.
|
||||||
* @param[out] count Bytes written. Required.
|
* @param[out] count Bytes written; 0 on failure. Required.
|
||||||
* @param[in] stream Destination stream. Required.
|
* @param[in] stream Destination stream. Required.
|
||||||
* @param[in] format printf format string. Required. Checked at compile time.
|
* @param[in] format printf format string. Required. Checked at compile time.
|
||||||
* @throws AKERR_NULLPOINTER If any pointer is NULL.
|
* @throws AKERR_NULLPOINTER If any pointer is NULL.
|
||||||
@@ -483,11 +481,11 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea
|
|||||||
* written, leaving the caller to notice by comparing that against the buffer
|
* written, leaving the caller to notice by comparing that against the buffer
|
||||||
* size -- the check this library exists to stop people forgetting.
|
* size -- the check this library exists to stop people forgetting.
|
||||||
*
|
*
|
||||||
* @param[out] count Bytes written, or the required length on truncation. Required.
|
* @param[out] count Bytes written excluding the NUL; 0 on failure. Required.
|
||||||
* @param[out] str Destination buffer. Required.
|
* @param[out] str Destination buffer. Required.
|
||||||
* @param[in] size Size of `str` including the terminator. Must be non-zero.
|
* @param[in] size Size of `str` including the terminator. Must be non-zero.
|
||||||
* @param[in] format printf format string. Required. Checked at compile time.
|
* @param[in] format printf format string. Required. Checked at compile time.
|
||||||
* @throws AKERR_NULLPOINTER If str or format is NULL.
|
* @throws AKERR_NULLPOINTER If any pointer is NULL.
|
||||||
* @throws AKERR_VALUE If size is 0.
|
* @throws AKERR_VALUE If size is 0.
|
||||||
* @throws AKERR_OUTOFBOUNDS If the output does not fit, naming both lengths.
|
* @throws AKERR_OUTOFBOUNDS If the output does not fit, naming both lengths.
|
||||||
* @return NULL on success, an error context otherwise.
|
* @return NULL on success, an error context otherwise.
|
||||||
@@ -551,9 +549,9 @@ 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.
|
* @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[out] str Destination buffer. Required.
|
||||||
* @param[in] size Size of `str` including the terminator. Must be non-zero.
|
* @param[in] size Size of `str` including the terminator. Must be non-zero.
|
||||||
* @param[out] count Bytes written, or the required length on truncation. Required.
|
|
||||||
* @param[in] format printf format string. Required.
|
* @param[in] format printf format string. Required.
|
||||||
* @param[in] args Arguments. The caller owns it and must va_end it.
|
* @param[in] args Arguments. The caller owns it and must va_end it.
|
||||||
* @throws AKERR_NULLPOINTER If any pointer is NULL.
|
* @throws AKERR_NULLPOINTER If any pointer is NULL.
|
||||||
@@ -894,69 +892,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest);
|
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
|
/** @name Streams: open, read, write, close
|
||||||
|
|||||||
245
scripts/consumer_calls.py
Executable file
245
scripts/consumer_calls.py
Executable file
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Consumer adoption harness for libakstdlib.
|
||||||
|
|
||||||
|
Answers one question about a consumer's source tree: how often does it call
|
||||||
|
this library, and how often does it reach past this library to the libc
|
||||||
|
function this library wraps?
|
||||||
|
|
||||||
|
The ratio is the only external evidence there is about whether the surface that
|
||||||
|
got built is the surface anyone wanted. A wrapper nobody calls is a wrapper that
|
||||||
|
either does not fit or is not discoverable, and both are the library's problem.
|
||||||
|
|
||||||
|
The number this reports is only worth something if it is reproducible, which is
|
||||||
|
why this is a script and not a paragraph. TODO.md's first consumer figure was
|
||||||
|
recorded without one, and recovering the method afterwards cost more than
|
||||||
|
writing it down would have.
|
||||||
|
|
||||||
|
WHAT COUNTS
|
||||||
|
|
||||||
|
* The corpus is every *.c and *.h directly under the given directory. It does
|
||||||
|
not recurse: a consumer's src/ is the thing being measured, not its vendored
|
||||||
|
dependencies, and those are usually a subdirectory.
|
||||||
|
|
||||||
|
* Comments and string/character literals are stripped before anything is
|
||||||
|
counted, so a function named in prose or inside a format string does not
|
||||||
|
score. This matters more than it sounds -- "strlen" appears in doc comments
|
||||||
|
throughout a codebase that has been thinking about strlen.
|
||||||
|
|
||||||
|
* A call site is IDENT immediately followed by '(', where IDENT is not
|
||||||
|
preceded by an identifier character. Declarations are not distinguished from
|
||||||
|
calls; a consumer that declares a function named for a libc entry point will
|
||||||
|
over-count by one per declaration, which is visible in --detail.
|
||||||
|
|
||||||
|
* A library call is any IDENT matching ^aksl_.
|
||||||
|
|
||||||
|
* A bypass is any IDENT naming a libc function this library wraps. That set is
|
||||||
|
read out of include/akstdlib.h rather than hardcoded, so it grows when the
|
||||||
|
library grows and a recount after a release measures the surface that
|
||||||
|
release actually shipped.
|
||||||
|
|
||||||
|
* libc functions this library does NOT wrap -- isdigit, exit, qsort -- score
|
||||||
|
on neither side. The question is how often a consumer bypasses an available
|
||||||
|
wrapper, not how much libc it uses. Adding a wrapper for something and
|
||||||
|
having it ignored is a finding; a consumer calling exit() is not.
|
||||||
|
|
||||||
|
WHAT IT CANNOT TELL YOU
|
||||||
|
|
||||||
|
One consumer's ratio is evidence, not a plan. A consumer that draws
|
||||||
|
everything from fixed pools will never call the allocator no matter how good
|
||||||
|
the allocator is, and will weight the string wrappers accordingly. Weight the
|
||||||
|
result by what the consumer is, and get a second consumer before treating any
|
||||||
|
ranking as settled.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/consumer_calls.py DIR [options]
|
||||||
|
|
||||||
|
DIR consumer source directory to measure, e.g.
|
||||||
|
../akbasic/src
|
||||||
|
--header PATH akstdlib.h to read the wrapped-libc set from
|
||||||
|
(default: include/akstdlib.h beside this script's repo)
|
||||||
|
--detail list the per-function breakdown on both sides
|
||||||
|
--per-file list per-file counts, worst bypass ratio first
|
||||||
|
--baseline A/B compare against a previous count, e.g. --baseline 10/119
|
||||||
|
--json emit the whole result as JSON instead of text
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
# Wrapper families that are this library's own constructs rather than a libc
|
||||||
|
# function under a new name. aksl_list_append has no libc counterpart, so
|
||||||
|
# "append" must not become a name a consumer can be scored for bypassing.
|
||||||
|
LIBRARY_ONLY_PREFIXES = ("hashmap", "list", "tree", "strbuf", "version",
|
||||||
|
"strhash")
|
||||||
|
|
||||||
|
# Library-only names whose first underscore-separated word is shared with a real
|
||||||
|
# libc entry point, so a prefix rule cannot separate them. aksl_realpath wraps
|
||||||
|
# realpath(3) and must score; aksl_realpath_alloc is this library's own.
|
||||||
|
LIBRARY_ONLY_NAMES = frozenset(("freep", "realpath_alloc"))
|
||||||
|
|
||||||
|
CALL = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z_][A-Za-z0-9_]*)\s*\(")
|
||||||
|
|
||||||
|
|
||||||
|
def wrapped_libc(header):
|
||||||
|
"""The set of libc names this library wraps, read out of the header."""
|
||||||
|
with open(header, encoding="utf-8", errors="replace") as handle:
|
||||||
|
text = handle.read()
|
||||||
|
names = set()
|
||||||
|
for match in re.finditer(r"\baksl_([a-z0-9_]+)\s*\(", text):
|
||||||
|
name = match.group(1)
|
||||||
|
if name.split("_")[0] in LIBRARY_ONLY_PREFIXES:
|
||||||
|
continue
|
||||||
|
if name in LIBRARY_ONLY_NAMES:
|
||||||
|
continue
|
||||||
|
names.add(name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def strip_c(src):
|
||||||
|
"""Remove comments and string/char literals, preserving everything else."""
|
||||||
|
out = []
|
||||||
|
i, end = 0, len(src)
|
||||||
|
while i < end:
|
||||||
|
char = src[i]
|
||||||
|
if char == "/" and i + 1 < end and src[i + 1] == "/":
|
||||||
|
while i < end and src[i] != "\n":
|
||||||
|
i += 1
|
||||||
|
elif char == "/" and i + 1 < end and src[i + 1] == "*":
|
||||||
|
i += 2
|
||||||
|
while i + 1 < end and not (src[i] == "*" and src[i + 1] == "/"):
|
||||||
|
i += 1
|
||||||
|
i += 2
|
||||||
|
elif char in ('"', "'"):
|
||||||
|
quote = char
|
||||||
|
i += 1
|
||||||
|
while i < end and src[i] != quote:
|
||||||
|
if src[i] == "\\":
|
||||||
|
i += 1
|
||||||
|
i += 1
|
||||||
|
i += 1
|
||||||
|
out.append(" ")
|
||||||
|
else:
|
||||||
|
out.append(char)
|
||||||
|
i += 1
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def measure(srcdir, libc):
|
||||||
|
"""Count library and bypass call sites across one directory."""
|
||||||
|
library, bypass, per_file = Counter(), Counter(), {}
|
||||||
|
names = sorted(name for name in os.listdir(srcdir)
|
||||||
|
if name.endswith((".c", ".h")))
|
||||||
|
for name in names:
|
||||||
|
with open(os.path.join(srcdir, name), encoding="utf-8",
|
||||||
|
errors="replace") as handle:
|
||||||
|
text = strip_c(handle.read())
|
||||||
|
here_lib = here_raw = 0
|
||||||
|
for match in CALL.finditer(text):
|
||||||
|
ident = match.group(1)
|
||||||
|
if ident.startswith("aksl_"):
|
||||||
|
library[ident] += 1
|
||||||
|
here_lib += 1
|
||||||
|
elif ident in libc:
|
||||||
|
bypass[ident] += 1
|
||||||
|
here_raw += 1
|
||||||
|
if here_lib or here_raw:
|
||||||
|
per_file[name] = (here_lib, here_raw)
|
||||||
|
return library, bypass, per_file
|
||||||
|
|
||||||
|
|
||||||
|
def rate(bypassed, total):
|
||||||
|
return 100.0 * bypassed / total if total else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(add_help=True)
|
||||||
|
parser.add_argument("srcdir")
|
||||||
|
parser.add_argument("--header")
|
||||||
|
parser.add_argument("--detail", action="store_true")
|
||||||
|
parser.add_argument("--per-file", action="store_true")
|
||||||
|
parser.add_argument("--baseline")
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
header = args.header or os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
"include", "akstdlib.h")
|
||||||
|
if not os.path.isfile(header):
|
||||||
|
sys.stderr.write(f"error: no such header: {header}\n")
|
||||||
|
return 2
|
||||||
|
if not os.path.isdir(args.srcdir):
|
||||||
|
sys.stderr.write(f"error: no such directory: {args.srcdir}\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
libc = wrapped_libc(header)
|
||||||
|
library, bypass, per_file = measure(args.srcdir, libc)
|
||||||
|
lib_total, raw_total = sum(library.values()), sum(bypass.values())
|
||||||
|
total = lib_total + raw_total
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({
|
||||||
|
"source": os.path.abspath(args.srcdir),
|
||||||
|
"header": os.path.abspath(header),
|
||||||
|
"wrapped_libc_names": len(libc),
|
||||||
|
"library_calls": lib_total,
|
||||||
|
"bypass_calls": raw_total,
|
||||||
|
"bypass_pct": round(rate(raw_total, total), 1),
|
||||||
|
"library_breakdown": dict(library.most_common()),
|
||||||
|
"bypass_breakdown": dict(bypass.most_common()),
|
||||||
|
"per_file": {k: {"library": v[0], "bypass": v[1]}
|
||||||
|
for k, v in per_file.items()},
|
||||||
|
}, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"consumer : {os.path.abspath(args.srcdir)}")
|
||||||
|
print(f"measured against: {os.path.abspath(header)} "
|
||||||
|
f"({len(libc)} wrapped libc names)")
|
||||||
|
print()
|
||||||
|
print(f"libakstdlib calls : {lib_total}")
|
||||||
|
print(f"bypassed to libc : {raw_total}")
|
||||||
|
print(f"bypass rate : {rate(raw_total, total):.1f}% "
|
||||||
|
f"({raw_total}/{total})")
|
||||||
|
|
||||||
|
if args.baseline:
|
||||||
|
try:
|
||||||
|
was_lib, was_raw = (int(part) for part in args.baseline.split("/"))
|
||||||
|
except ValueError:
|
||||||
|
sys.stderr.write("error: --baseline wants LIBRARY/BYPASS, "
|
||||||
|
"e.g. 10/119\n")
|
||||||
|
return 2
|
||||||
|
was_total = was_lib + was_raw
|
||||||
|
print()
|
||||||
|
print(f"baseline : {was_lib} / {was_raw} "
|
||||||
|
f"({rate(was_raw, was_total):.1f}% bypass)")
|
||||||
|
print(f"change : {lib_total - was_lib:+d} library, "
|
||||||
|
f"{raw_total - was_raw:+d} bypass, "
|
||||||
|
f"{rate(raw_total, total) - rate(was_raw, was_total):+.1f} pt")
|
||||||
|
print()
|
||||||
|
print("A bypass rate is only comparable between two counts of the same")
|
||||||
|
print("tree. If the consumer grew between them, compare the rate and")
|
||||||
|
print("not the totals -- and say which tree each number came from.")
|
||||||
|
|
||||||
|
if args.detail:
|
||||||
|
print("\nbypassed to libc")
|
||||||
|
for name, count in bypass.most_common():
|
||||||
|
print(f" {name:<22}{count}")
|
||||||
|
print("\ncalls into libakstdlib")
|
||||||
|
for name, count in library.most_common():
|
||||||
|
print(f" {name:<22}{count}")
|
||||||
|
|
||||||
|
if args.per_file:
|
||||||
|
print("\nper file (library, bypass), worst bypass first")
|
||||||
|
order = sorted(per_file.items(), key=lambda kv: (-kv[1][1], kv[0]))
|
||||||
|
for name, (lib, raw) in order:
|
||||||
|
print(f" {name:<32}{lib:>5}{raw:>6}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
92
src/dir.c
92
src/dir.c
@@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
21
src/stdlib.c
21
src/stdlib.c
@@ -416,11 +416,12 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream)
|
|||||||
* register-save state on some ABIs. akbasic's text sink ran this UB on every
|
* 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
|
* line of program output without anything visibly misbehaving, which is
|
||||||
* exactly what made it worth fixing before something did.
|
* exactly what made it worth fixing before something did.
|
||||||
|
* - *count is written on every path. It used to be left holding vprintf's -1
|
||||||
|
* after a failure, so a caller who read the length rather than the status got
|
||||||
|
* a negative byte count out of a function that had already failed. It is now
|
||||||
|
* 0 whenever an error is raised.
|
||||||
* - errno is cleared before the call and read back through AKSL_ERRNO_OR, so a
|
* - 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.
|
* failure can never be reported with a stale -- or with a zero -- status.
|
||||||
* - The bounded form returns the complete required length through *count even
|
|
||||||
* when truncation raises AKERR_OUTOFBOUNDS; callers use the error context for
|
|
||||||
* failure details, not the count as a success indicator.
|
|
||||||
*
|
*
|
||||||
* aksl_sprintf is gone. It wrapped vsprintf, which cannot be bounded, and an
|
* 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
|
* error-handling wrapper around an unbounded write is precisely the sharp edge
|
||||||
@@ -484,11 +485,12 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea
|
|||||||
* *would* have written and silently drops the rest, which is the single most
|
* *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
|
* 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
|
* would have had to compare the return against the buffer size by hand, which is
|
||||||
* the check this library exists to stop people forgetting. The bounded wrapper
|
* the check this library exists to stop people forgetting. *count is the number
|
||||||
* reports truncation instead, and hands the required length back through *count.
|
* of bytes written excluding the terminating NUL, and is 0 on any failure.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args)
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args)
|
||||||
{
|
{
|
||||||
|
int needed = 0;
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
||||||
*count = 0;
|
*count = 0;
|
||||||
@@ -496,10 +498,11 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str
|
|||||||
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, str=%p, format=%p", (void *)count, (void *)str, (void *)format);
|
||||||
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL");
|
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "size=0 leaves no room even for the terminating NUL");
|
||||||
errno = 0;
|
errno = 0;
|
||||||
*count = vsnprintf(str, size, format, args);
|
needed = vsnprintf(str, size, format, args);
|
||||||
FAIL_NONZERO_RETURN(e, (*count < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error");
|
FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "Output error");
|
||||||
FAIL_NONZERO_RETURN(e, ((size_t)*count >= size), AKERR_OUTOFBOUNDS,
|
FAIL_NONZERO_RETURN(e, ((size_t)needed >= size), AKERR_OUTOFBOUNDS,
|
||||||
"output truncated: %d bytes needed, %zu available", *count, size);
|
"output truncated: %d bytes needed, %zu available", needed, size);
|
||||||
|
*count = needed;
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
154
tests/test_dir.c
154
tests/test_dir.c
@@ -1,154 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
||||||
* their va_list forms.
|
* their va_list forms.
|
||||||
*
|
*
|
||||||
* Formatted output, complete. Each happy path asserts the text that actually
|
* Formatted output, complete. Each happy path asserts both halves of the
|
||||||
* landed somewhere, and every pointer argument is checked for its NULL guard.
|
* 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.
|
||||||
*
|
*
|
||||||
* Readback goes through plain libc rather than aksl_fread so that a failure here
|
* 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
|
* points at the formatted-output wrapper under test and not at the stream
|
||||||
@@ -37,7 +39,7 @@ static long read_file(const char *path, char *buf, size_t n)
|
|||||||
return (long)got;
|
return (long)got;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int test_snprintf_writes_text(void)
|
static int test_snprintf_writes_text_and_count(void)
|
||||||
{
|
{
|
||||||
char buf[64];
|
char buf[64];
|
||||||
int count = -1;
|
int count = -1;
|
||||||
@@ -64,7 +66,7 @@ static int test_snprintf_empty_format_writes_nothing(void)
|
|||||||
* The case that could not be written while the wrapper was aksl_sprintf: output
|
* 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
|
* longer than the destination. snprintf(3) would truncate, NUL-terminate and
|
||||||
* report the length it *would* have written, leaving the caller to notice; here
|
* report the length it *would* have written, leaving the caller to notice; here
|
||||||
* it is an error.
|
* it is an error, and *count is 0 rather than the would-have-been length.
|
||||||
*/
|
*/
|
||||||
static int test_snprintf_truncation_is_an_error(void)
|
static int test_snprintf_truncation_is_an_error(void)
|
||||||
{
|
{
|
||||||
@@ -75,8 +77,7 @@ static int test_snprintf_truncation_is_an_error(void)
|
|||||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
|
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
|
||||||
AKERR_OUTOFBOUNDS, "truncated");
|
AKERR_OUTOFBOUNDS, "truncated");
|
||||||
AKSL_CHECK(count == 28);
|
AKSL_CHECK(count == 0);
|
||||||
AKSL_CHECK(strcmp(buf, "far too") == 0);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +92,10 @@ static int test_snprintf_boundary_is_exact(void)
|
|||||||
AKSL_CHECK(count == 7);
|
AKSL_CHECK(count == 7);
|
||||||
AKSL_CHECK(strcmp(buf, "1234567") == 0);
|
AKSL_CHECK(strcmp(buf, "1234567") == 0);
|
||||||
|
|
||||||
|
count = -1;
|
||||||
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
|
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
|
||||||
AKERR_OUTOFBOUNDS);
|
AKERR_OUTOFBOUNDS);
|
||||||
AKSL_CHECK(count == 8);
|
AKSL_CHECK(count == 0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +105,14 @@ static int test_snprintf_rejects_null_arguments_and_zero_size(void)
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
memset(buf, 0x00, sizeof(buf));
|
memset(buf, 0x00, sizeof(buf));
|
||||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"),
|
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"),
|
||||||
AKERR_NULLPOINTER, "str=");
|
AKERR_NULLPOINTER, "str=");
|
||||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
|
||||||
AKERR_NULLPOINTER, "format=");
|
AKERR_NULLPOINTER, "format=");
|
||||||
/* size 0 leaves no room even for the terminator, so there is nothing to do. */
|
/* 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(&count, buf, 0, "x"),
|
||||||
AKERR_VALUE, "size=0");
|
AKERR_VALUE, "size=0");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -277,25 +281,25 @@ static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf
|
|||||||
akerr_ErrorContext *raised = NULL;
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
raised = aksl_vsnprintf(count, buf, n, fmt, args);
|
raised = aksl_vsnprintf(count, buf, n, fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
return raised;
|
return raised;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int test_va_list_forms_are_usable_from_outside(void)
|
static int test_va_list_forms_are_usable_from_outside(void)
|
||||||
{
|
{
|
||||||
char buf[32];
|
char buf[32];
|
||||||
int count = -1;
|
int count = -1;
|
||||||
|
|
||||||
memset(buf, 0x00, sizeof(buf));
|
memset(buf, 0x00, sizeof(buf));
|
||||||
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
|
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
|
||||||
AKSL_CHECK(count == 5);
|
AKSL_CHECK(count == 5);
|
||||||
AKSL_CHECK(strcmp(buf, "via/3") == 0);
|
AKSL_CHECK(strcmp(buf, "via/3") == 0);
|
||||||
|
|
||||||
/* The error contract survives the extra layer intact. */
|
/* The error contract survives the extra layer intact. */
|
||||||
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
|
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
|
||||||
AKERR_OUTOFBOUNDS);
|
AKERR_OUTOFBOUNDS);
|
||||||
AKSL_CHECK(count == 8);
|
AKSL_CHECK(count == 0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +311,7 @@ static int test_va_list_forms_are_usable_from_outside(void)
|
|||||||
*/
|
*/
|
||||||
static int test_variadic_wrappers_survive_repeated_calls(void)
|
static int test_variadic_wrappers_survive_repeated_calls(void)
|
||||||
{
|
{
|
||||||
char buf[128];
|
char buf[128];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
@@ -326,7 +330,7 @@ int main(void)
|
|||||||
|
|
||||||
akerr_init();
|
akerr_init();
|
||||||
|
|
||||||
AKSL_RUN(failures, test_snprintf_writes_text);
|
AKSL_RUN(failures, test_snprintf_writes_text_and_count);
|
||||||
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
|
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
|
||||||
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
|
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
|
||||||
AKSL_RUN(failures, test_snprintf_boundary_is_exact);
|
AKSL_RUN(failures, test_snprintf_boundary_is_exact);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ static int test_format_wrappers_do_not_leak_slots(void)
|
|||||||
for ( i = 0; i < ROUNDS; i++ ) {
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
|
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
|
||||||
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "x"));
|
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_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
|
||||||
AKERR_OUTOFBOUNDS);
|
AKERR_OUTOFBOUNDS);
|
||||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
@@ -266,6 +266,7 @@ static int test_traversal_failures_do_not_leak_slots(void)
|
|||||||
static int test_errors_name_their_origin_in_stdlib(void)
|
static int test_errors_name_their_origin_in_stdlib(void)
|
||||||
{
|
{
|
||||||
void *ptr = NULL;
|
void *ptr = NULL;
|
||||||
|
int count = 0;
|
||||||
char resolved[PATH_MAX];
|
char resolved[PATH_MAX];
|
||||||
uint32_t h = 0;
|
uint32_t h = 0;
|
||||||
aksl_ListNode node;
|
aksl_ListNode node;
|
||||||
@@ -294,7 +295,7 @@ static int test_errors_name_their_origin_in_stdlib(void)
|
|||||||
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
|
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
AKSL_CHECK_STATUS(aksl_snprintf(NULL, NULL, 8, "x"), AKERR_NULLPOINTER);
|
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
|
||||||
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
|
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
/* Likewise, the ato* forms are calls into the strto* ones. */
|
/* Likewise, the ato* forms are calls into the strto* ones. */
|
||||||
|
|||||||
Reference in New Issue
Block a user