Stop a failed controller-DB fetch from destroying the tracked fallback

Closes Defects -> Known and still open item 12, both halves.

include/akgl/SDL_GameControllerDB.h is tracked on purpose: it is the offline
fallback that keeps the library buildable when upstream is unreachable. The
script that writes it had no set -e and never checked curl, so a failed fetch
wrote AKGL_SDL_GAMECONTROLLER_DB_LEN 0 and an empty array over the good copy
and exited 0. The safety net destroyed itself, and because CMake re-ran the
generator on every build, an offline build was enough to do it.

The script now runs under set -euo pipefail, fetches into a temporary
directory, and moves the result into place only after checking curl's exit
status (with --fail, so an HTTP error is a status rather than an error page in
the body), a plausible minimum mapping count, and that no mapping carries a
quote or backslash that would break the C string literal it becomes. Any of
those failing leaves the tracked header exactly as it was and exits non-zero.

Verified against all five failure modes -- unresolvable host, 404, truncated
response, empty-but-successful response (the original failure exactly), and a
response containing a quote. Each refuses, and the header's checksum is
unchanged after every one. AKGL_CONTROLLERDB_URL and
AKGL_CONTROLLERDB_MIN_LINES are environment-overridable, which is how.

The build no longer runs it at all. The add_custom_command declared a relative
OUTPUT, which CMake resolves against the binary directory while the script
writes to the source directory, so the declared output never appeared and every
build re-ran the command -- needing network access and leaving the tree dirty.
It is an explicit `controllerdb` target now, and the header is no longer listed
as a library source, which is all it was there for.

The empty-initializer problem goes with it: an empty array initializer is a
constraint violation in ISO C that compiles only as a GCC extension, and the
minimum-count check guarantees at least one entry.

Also here, because it rested on a premise that turned out to be false: the
character suite is no longer excluded from CI's coverage and memory-check jobs,
nor from the mutation harness by default. It was excluded on the grounds that
it failed deliberately. It did not -- it was reporting success while running
one of its four tests.

25/25 pass, memcheck clean, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 06:54:31 -04:00
parent 913834a3af
commit d9f0187ecf
6 changed files with 184 additions and 74 deletions

57
TODO.md
View File

@@ -740,32 +740,43 @@ Each was found by a test written to assert correct behavior.
`tests/controller.c` passes -1 and -4096 to each.
12. **A failed controller-DB fetch silently destroys the tracked fallback.**
`include/akgl/SDL_GameControllerDB.h` is committed deliberately so the
library still builds if upstream disappears. But `mkcontrollermappings.sh`
has no `set -e` and never checks `curl`'s exit status: on a failed fetch it
writes `mappings.txt` empty, then overwrites the good tracked header with
`AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer — and exits 0.
Verified by pointing the script at an unresolvable host.
**Fixed in 0.5.0**, both halves.
Two compounding problems:
`mkcontrollermappings.sh` now runs under `set -euo pipefail`, fetches into a
temporary directory, and moves the result into place only after checking
three things: curl's exit status (with `--fail`, so an HTTP error is a
status rather than an error page in the body), a plausible minimum mapping
count, and that no mapping carries a quote or backslash that would break the
C string literal it becomes. Any of those failing leaves the tracked header
exactly as it was and exits non-zero. `AKGL_CONTROLLERDB_URL` and
`AKGL_CONTROLLERDB_MIN_LINES` are environment-overridable, which is how the
failure paths were exercised.
- `add_custom_command` at `CMakeLists.txt:89-93` declares
`OUTPUT include/akgl/SDL_GameControllerDB.h` as a *relative* path, which
CMake resolves against the binary directory, while the script writes to
the source directory. The declared output never appears, so the command is
permanently out of date and re-runs on every build — making every build
depend on network access and leaving the file dirty in the working tree
each time.
- `const char *SDL_GAMECONTROLLER_DB[] = {\n};` is an empty initializer,
which is a constraint violation in ISO C and compiles only as a GCC
extension.
Verified against all five: an unresolvable host, a 404, a truncated
response, an empty-but-successful response -- the original failure exactly
-- and a response containing a quote. Each refuses, and the header's
checksum is unchanged after every one.
The build no longer runs it. The `add_custom_command` declared
`OUTPUT include/akgl/SDL_GameControllerDB.h` as a *relative* path, which
CMake resolves against the binary directory while the script writes to the
source directory, so the declared output never appeared, the command was
permanently out of date, and every build re-ran it -- needing network access
and leaving the tree dirty. It is an explicit `controllerdb` target now, and
the header is no longer listed as a library source, which is all it was
there for.
The empty-initializer problem goes with it: `const char *SDL_GAMECONTROLLER_DB[] = {};`
is a constraint violation in ISO C that compiles only as a GCC extension,
and the minimum-count check is what guarantees at least one entry.
Regenerating against the real upstream produced byte-identical mappings --
2255 entries, no content change -- so the rewrite is faithful. The only diff
is the `$(date)` stamp and the include guard, which is
`_AKGL_SDL_GAMECONTROLLERDB_H_` now to match every other header. That change
was made in the generator rather than by hand, and the tracked copy carries
it so a future regeneration shows no spurious diff.
Fix: have the script fetch to a temporary file, check `curl`'s status and a
plausible minimum line count, and leave the existing header untouched on
failure (exiting non-zero). Separately, make regeneration explicit — a
dedicated `controllerdb` target, or an `OUTPUT` that matches where the
script actually writes — so an ordinary build neither needs the network nor
dirties the tree.
13. **A stale build tree in the source directory breaks the coverage run.**
`CMakeLists.txt:208` and `CMakeLists.txt:223` pass
`--root "${CMAKE_CURRENT_SOURCE_DIR}"` to gcovr, and gcovr searches for