Files
libakgl/src/sprite.c
Andrew Kesterson 230278d303 Bound every array a data file can index
Closes Defects items 16 and 17 and Known-and-still-open item 6. All three let
an asset file, or a caller's argument, write past a fixed array.

akgl_sprite_load_json took its frame count straight from the document and wrote
that many entries into a 16-byte frameids -- through a uint32_t * cast of a
uint8_t *, so each write touched four bytes and the overrun reached four bytes
past the array, into the rest of akgl_Sprite and then the next pool slot. The
count is checked first now, each id is read into an int and narrowed
deliberately, and a frame number too large for a uint8_t is refused rather than
truncated into an index for a different tile.

The tilemap loader had the same shape twice: objects[j] with no check against
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER and tilesets[i] with none against
AKGL_TILEMAP_MAX_TILESETS. akgl_tilemap_load_layers already bounded its own
loop, so the pattern was in the same file. The object one is the reachable
half -- 128 objects is not a large object layer.

akgl_string_initialize zeroed sizeof(akgl_String) starting at `data`, which
begins after the refcount in front of it, so it ran four bytes past the end of
the object and onto the *next* slot's refcount -- the field the allocator reads
to decide whether a slot is free. Same file, same class, fixed with it:
akgl_string_copy accepted a count larger than the buffers, reading past one
pool slot and writing past another, which the header documented as behaviour.

Every case has a test that fails against the old code, with five new fixtures.
Exactly-the-maximum is asserted alongside one-past in each, so the bound cannot
be fixed by making the limit off by one.

25/25 pass, memcheck clean, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:37 -04:00

272 lines
10 KiB
C

/**
* @file sprite.c
* @brief Implements the sprite subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <string.h>
#include <jansson.h>
#include <akerror.h>
#include <libgen.h>
#include <akgl/game.h>
#include <akgl/sprite.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/util.h>
akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL sprite");
FAIL_ZERO_RETURN(errctx, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
FAIL_ZERO_RETURN(errctx, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
srccoords->x = self->width * self->frameids[frameid];
if ( srccoords->x >= self->sheet->texture->w ) {
srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
} else {
srccoords->y = 0;
}
srccoords->w = self->width;
srccoords->h = self->height;
SUCCEED_RETURN(errctx);
}
/**
* @brief Find or load the spritesheet a sprite definition names.
*
* Resolves the `spritesheet.filename` path relative to the sprite definition's
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
* A hit is reused as is -- which is what keeps ten sprites cut from one image
* down to one texture -- and a miss claims a sheet from the pool and loads it,
* reading `frame_width` and `frame_height` only in that case.
*
* @param json The parsed sprite document, whose `spritesheet` object
* this reads. Required in practice; not checked.
* @param sheet Receives the sheet, whether found or freshly loaded.
* Required; not checked. On the reuse path the sheet's
* reference count is *not* incremented, so the sprite
* borrows it.
* @param relative_path Directory to resolve the image path against -- the
* directory holding the sprite JSON. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
* has no `filename` -- or, on the load path, no `frame_width` or
* `frame_height`.
* @throws AKERR_TYPE If one of those has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
* @throws ENOENT If the image path does not exist.
* @throws AKGL_ERR_SDL If the image cannot be decoded.
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
*/
static akerr_ErrorContext *sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{
PREPARE_ERROR(errctx);
json_t *spritesheet_json = NULL;
int ss_frame_width = 0;
int ss_frame_height = 0;
akgl_String *ss_filename = NULL;
akgl_String *tmpstr = NULL;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
*sheet = SDL_GetPointerProperty(
AKGL_REGISTRY_SPRITESHEET,
(char *)&tmpstr->data,
NULL
);
if ( *sheet == NULL ) {
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
CATCH(errctx,
akgl_spritesheet_initialize(
(akgl_SpriteSheet *)*sheet,
ss_frame_width,
ss_frame_height,
(char *)&tmpstr->data)
);
}
} CLEANUP {
IGNORE(akgl_heap_release_string(ss_filename));
IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
{
PREPARE_ERROR(errctx);
json_t *json = NULL;
json_t *frames = NULL;
json_error_t error;
akgl_Sprite *obj = NULL;
akgl_SpriteSheet *sheet = NULL;
akgl_String *spritename = NULL;
akgl_String *filename_copy = NULL;
int i = 0;
int framecount = 0;
int frameid = 0;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
CATCH(errctx, akgl_heap_next_sprite(&obj));
CATCH(errctx, akgl_heap_next_string(&filename_copy));
FAIL_NONZERO_BREAK(
errctx,
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
AKERR_OUTOFBOUNDS,
"Sprite filename exceeds temporary string capacity"
);
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, akgl_heap_next_string(&spritename));
CATCH(errctx, akgl_string_initialize(spritename, NULL));
json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK(
errctx,
json,
AKERR_NULLPOINTER,
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
);
CATCH(errctx, sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
CATCH(errctx,
akgl_sprite_initialize(
(akgl_Sprite *)obj,
spritename->data,
(akgl_SpriteSheet *)sheet)
);
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
obj->speed = obj->speed * AKGL_TIME_ONEMS_NS;
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
// Bounded before anything is written. frameids is
// AKGL_SPRITE_MAX_FRAMES bytes, and this loop used to take its count
// straight from the document -- so a definition with seventeen frames
// wrote past the array, past the rest of akgl_Sprite, and into the
// neighbouring pool slot.
framecount = (int)json_array_size((json_t *)frames);
FAIL_NONZERO_BREAK(
errctx,
(framecount > AKGL_SPRITE_MAX_FRAMES),
AKERR_OUTOFBOUNDS,
"Sprite %s declares %d frames; the maximum is %d",
(char *)&obj->name,
framecount,
AKGL_SPRITE_MAX_FRAMES
);
obj->frames = framecount;
for ( i = 0 ; i < framecount; i++ ) {
// Read into an int and narrow deliberately. The old form wrote
// through a uint32_t * cast of a uint8_t *, so every element write
// touched four bytes; it only appeared to work because the next
// iteration overwrote the spill and the last one landed in the
// struct's alignment padding.
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, &frameid));
FAIL_NONZERO_BREAK(
errctx,
((frameid < 0) || (frameid > UINT8_MAX)),
AKERR_OUTOFBOUNDS,
"Sprite %s frame %d is %d; frame numbers are 0..%d",
(char *)&obj->name,
i,
frameid,
UINT8_MAX
);
obj->frameids[i] = (uint8_t)frameid;
}
} CLEANUP {
// The sprite copies every field it wants out of the document and the
// sheet is found by resolved path, so nothing here points into the
// parsed tree once this block runs. Released on both paths.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
if ( errctx != NULL && errctx->status != 0 ) {
IGNORE(akgl_heap_release_sprite(obj));
IGNORE(akgl_heap_release_spritesheet(sheet));
}
IGNORE(akgl_heap_release_string(spritename));
IGNORE(akgl_heap_release_string(filename_copy));
//IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
memset(spr, 0x00, sizeof(akgl_Sprite));
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
spr->sheet = sheet;
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
AKERR_KEY,
"Unable to add sprite to registry");
spr->refcount += 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
{
PREPARE_ERROR(errctx);
//akgl_String *tmpstr = NULL;
ATTEMPT {
FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
sheet->texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, filename);
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
AKERR_KEY,
"Unable to add spritesheet to registry: %s",
SDL_GetError());
sheet->refcount += 1;
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}