Files
libakgl/src/json_helpers.c

215 lines
9.1 KiB
C
Raw Normal View History

/**
* @file json_helpers.c
* @brief Implements the json helpers subsystem.
*/
#include <jansson.h>
#include <string.h>
#include <akerror.h>
Build clean under -Wall, with -Werror in CI only TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:18:52 -04:00
#include <akstdlib.h>
#include <akgl/json_helpers.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include <akgl/registry.h>
akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_boolean_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
}
Fix the type, macro and state-table defects, and the leftover debris Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180 redundant casts, is deliberately left open with its reasoning in TODO.md: the benefit only arrives once the build turns on the warnings those casts suppress, and doing it before that is churn across the two files with the most outstanding functional defects. The two that were real bugs are in the actor state table. AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and defined [32], so a consumer trusting the declared bound read past the object; and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to either state. tests/registry.c now walks the whole table -- every entry non-NULL, every entry resolving to its own bit, no two entries sharing a name. The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the semicolon inside its body. Writing tests/bitmasks.c for that turned up something worth knowing: the obvious test does not catch it. For a bit that is set, the misparse `!(mask & bit) == bit` gives the same answer as the correct one. It only diverges for an unset bit whose value is not 1, and that is the shape the suite uses now. akgl_draw_background was the last public function outside the error protocol. It takes a backend like everything else in draw.h, restores the draw colour it found, and is tested -- TODO.md had it filed under "needs the offscreen renderer harness", which was never true; what it needed was to stop reading the global. All eight registry initializers go through one helper, so the seven that leaked an SDL_PropertiesID on every call after the first no longer do. Fixed in the same place because it is the same function: akgl_registry_init never called akgl_registry_init_properties, which made akgl_set_property a silent no-op for anyone not going through akgl_game_init -- Defects, Known and still open item 3. Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame deleted, float32_t/float64_t used consistently, the developer-specific debug logging removed from the controller inner loop, the abandoned SDL_GetBasePath comments removed, nine unused locals removed, and dst renamed to dest. akgl_game_update's default flags no longer OR the same bit twice. That changes nothing today, and the reason is Performance item 32: the loop never reads either bit, which is why every actor is updated sixteen times a frame. Still open. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:15:36 -04:00
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
}
Fix the type, macro and state-table defects, and the leftover debris Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180 redundant casts, is deliberately left open with its reasoning in TODO.md: the benefit only arrives once the build turns on the warnings those casts suppress, and doing it before that is churn across the two files with the most outstanding functional defects. The two that were real bugs are in the actor state table. AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and defined [32], so a consumer trusting the declared bound read past the object; and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to either state. tests/registry.c now walks the whole table -- every entry non-NULL, every entry resolving to its own bit, no two entries sharing a name. The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the semicolon inside its body. Writing tests/bitmasks.c for that turned up something worth knowing: the obvious test does not catch it. For a bit that is set, the misparse `!(mask & bit) == bit` gives the same answer as the correct one. It only diverges for an unset bit whose value is not 1, and that is the shape the suite uses now. akgl_draw_background was the last public function outside the error protocol. It takes a backend like everything else in draw.h, restores the draw colour it found, and is tested -- TODO.md had it filed under "needs the offscreen renderer harness", which was never true; what it needed was to stop reading the global. All eight registry initializers go through one helper, so the seven that leaked an SDL_PropertiesID on every call after the first no longer do. Fixed in the same place because it is the same function: akgl_registry_init never called akgl_registry_init_properties, which made akgl_set_property a silent no-op for anyone not going through akgl_game_init -- Defects, Known and still open item 3. Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame deleted, float32_t/float64_t used consistently, the developer-specific debug logging removed from the controller inner loop, the abandoned SDL_GetBasePath comments removed, nine unused locals removed, and dst renamed to dest. akgl_game_update's default flags no longer OR the same bit twice. That changes nothing today, and the reason is Performance item 32: the loop never reads either bit, which is why every actor is updated sixteen times a frame. Still open. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:15:36 -04:00
akerr_ErrorContext *akgl_get_json_double_value(json_t *obj, char *key, float64_t *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = json_number_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
{
json_t *value = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
Report the failures that used to be crashes Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:32:11 -04:00
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
Report the failures that used to be crashes Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:32:11 -04:00
} FINISH(errctx, true);
Build clean under -Wall, with -Werror in CI only TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:18:52 -04:00
PASS(errctx, aksl_strncpy(
(char *)&(*dest)->data,
sizeof((*dest)->data),
json_string_value(value),
sizeof((*dest)->data) - 1));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
*dest = json_integer_value(value);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
Stop returning past CLEANUP, and validate the arguments every sibling validates Closes internal-consistency items 16 and 17. Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and skip every release in it. The one that mattered was the success path of akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries on every lookup that *found* what it was asked for, and a map load does that several times per layer. tests/tilemap.c now runs each of its three paths -- found, absent, wrong type -- twice the pool size and asserts the pool is where it started. Against the old code that test does not merely fail, it segfaults, which is Defects item 30 seen from the outside: pool exhaustion arriving as a NULL strncpy rather than as AKGL_ERR_HEAP. Two of the conversions needed more than swapping the macro. In akgl_get_json_tilemap_property a plain break would have fallen through to the "property not found" FAIL_RETURN after FINISH, reporting a miss for something found, so the success path sets a flag. In akgl_collide_rectangles the eight early exits are followed by `*collide = false;`, which would have overwritten the hit that broke out; each corner test writes the flag itself, so that line is gone rather than moved. The same function also released its scratch string once per loop iteration while continuing to use it, so the slot was free while still live -- one claim now covers the whole scan. akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was the last statement in the ATTEMPT block, so the path that falls out of FINISH reached the closing brace of a non-void function. scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test. Neither produces a compiler diagnostic and neither fails a test run until the pool it drains is empty, which is why both have already shipped once. For item 17: the eight typed JSON accessors that validated their container and then wrote through dest unconditionally now check key and dest as the two string accessors always did; the null physics backend checks its actors like the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check self, which the first two read straight through. tests/renderer.c calls all three with NULL, which segfaulted before. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
ATTEMPT {
if ( *dest == NULL ) {
CATCH(errctx, akgl_heap_next_string(dest));
CATCH(errctx, akgl_string_initialize(*dest, NULL));
}
Report the failures that used to be crashes Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:32:11 -04:00
// FINISH(errctx, true), not false. With `false` a failed
// akgl_heap_next_string was swallowed and the strncpy below ran through
// the pointer it never set, so an exhausted string pool arrived as a
// segfault inside strncpy rather than as AKGL_ERR_HEAP.
} CLEANUP {
} PROCESS(errctx) {
Report the failures that used to be crashes Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:32:11 -04:00
} FINISH(errctx, true);
Build clean under -Wall, with -Werror in CI only TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:18:52 -04:00
PASS(errctx, aksl_strncpy(
(char *)&(*dest)->data,
sizeof((*dest)->data),
json_string_value(value),
sizeof((*dest)->data) - 1));
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
akerr_ErrorContext *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize)
{
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
// Two contexts, and the names now say which is which: `e` is the incoming
// one being inspected, `errctx` is this function's own. They were `err` and
// `e`, which put the incoming context under the name the convention
// reserves for exactly that and the local one under a name that reads like
// it.
PREPARE_ERROR(errctx);
if ( e == NULL ) {
SUCCEED_RETURN(errctx);
}
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
FAIL_ZERO_RETURN(errctx, defval, AKERR_NULLPOINTER, "defval");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
ATTEMPT {
} CLEANUP {
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
} PROCESS(e) {
Make savegames, optional array elements, empty text and coverage work Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27. The savegame name tables carry no length prefix, so writer and reader have to agree on a field width exactly. The writer used each object's own maximum name length -- 512 for a spritesheet, a filename -- and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH constants drive both sides now. The failure turned out to be worse than "cannot be read back": a reader stepping the wrong width does not run off anything, it finds a run of zeros inside an entry, stops early, and reports success with silently wrong maps. A test asserting only that the load succeeded passed against the broken reader. So akgl_game_load checks it is at EOF once the tables are read, which turns a width disagreement into AKERR_IO instead of a corruption. That is the assertion the new roundtrip test -- the first with all four registries populated -- hangs on. akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS, which is what the array index accessors report, so "this element is optional" works for an array element and not only for an object member. The arm goes above the one holding the memcpy: HANDLE_GROUP emits no break and every arm falls into that body. That test needed a second attempt. with_default returns *the context it was given* when it does not handle the status, so TEST_EXPECT_OK -- which releases whatever the statement returns -- double-released it against a CLEANUP block that released it too, and a double-released context corrupts the failure rather than reporting it. The first draft passed against the unfixed library. akgl_text_rendertextat returns success without rasterizing for the empty string, matching akgl_text_measure, which has always accepted it. The check sits after the font and backend guards, so drawing nothing still refuses what drawing something refuses. tests/text.c had this case written and unasserted waiting for the two halves of the header to agree. character_load_json_state_int_from_strings guards dest rather than testing states twice. Not asserted: the function is static with one call site that passes a real pointer, so the guard cannot fire, and reaching it from a test would mean giving it external linkage purely for that. Both gcovr invocations take the build tree as an explicit positional search path. gcovr searches --root when given none, which is the source directory, where build trees live; --object-directory does not narrow it. Verified by building two instrumented trees with a source edit between them: the old invocation fails with "Got function write_exact on multiple lines: 46, 48" and exits 64, the new one exits 0 and the full coverage run passes with the stale tree still present. 25/25 pass, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:18:31 -04:00
// All three arms fall into the memcpy: HANDLE_GROUP emits no `break`,
// so a new status has to be added *above* the arm holding the body, not
// below it.
//
// AKERR_OUTOFBOUNDS is what akgl_get_json_array_index_object, _integer
// and _string report for a short array. Without it here, "this element
// is optional" worked for an object member and silently did not work
// for an array element -- the error propagated instead of being
// replaced by the default.
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
} HANDLE_GROUP(e, AKERR_KEY) {
Make savegames, optional array elements, empty text and coverage work Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27. The savegame name tables carry no length prefix, so writer and reader have to agree on a field width exactly. The writer used each object's own maximum name length -- 512 for a spritesheet, a filename -- and the reader used AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH constants drive both sides now. The failure turned out to be worse than "cannot be read back": a reader stepping the wrong width does not run off anything, it finds a run of zeros inside an entry, stops early, and reports success with silently wrong maps. A test asserting only that the load succeeded passed against the broken reader. So akgl_game_load checks it is at EOF once the tables are read, which turns a width disagreement into AKERR_IO instead of a corruption. That is the assertion the new roundtrip test -- the first with all four registries populated -- hangs on. akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS, which is what the array index accessors report, so "this element is optional" works for an array element and not only for an object member. The arm goes above the one holding the memcpy: HANDLE_GROUP emits no break and every arm falls into that body. That test needed a second attempt. with_default returns *the context it was given* when it does not handle the status, so TEST_EXPECT_OK -- which releases whatever the statement returns -- double-released it against a CLEANUP block that released it too, and a double-released context corrupts the failure rather than reporting it. The first draft passed against the unfixed library. akgl_text_rendertextat returns success without rasterizing for the empty string, matching akgl_text_measure, which has always accepted it. The check sits after the font and backend guards, so drawing nothing still refuses what drawing something refuses. tests/text.c had this case written and unasserted waiting for the two halves of the header to agree. character_load_json_state_int_from_strings guards dest rather than testing states twice. Not asserted: the function is static with one call site that passes a real pointer, so the guard cannot fire, and reaching it from a test would mean giving it external linkage purely for that. Both gcovr invocations take the build tree as an explicit positional search path. gcovr searches --root when given none, which is the source directory, where build trees live; --object-directory does not narrow it. Verified by building two instrumented trees with a source edit between them: the old invocation fails with "Got function write_exact on multiple lines: 46, 48" and exits 64, the new one exits 0 and the full coverage run passes with the stale tree still present. 25/25 pass, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:18:31 -04:00
} HANDLE_GROUP(e, AKERR_OUTOFBOUNDS) {
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
} HANDLE_GROUP(e, AKERR_INDEX) {
memcpy(dest, defval, defsize);
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
} FINISH(e, true);
Give every exported function a declaration, and check that it stays that way Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
SUCCEED_RETURN(errctx);
}