From 230278d3033f91119515cd316a7be58aa992c634 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 1 Aug 2026 00:24:35 -0400 Subject: [PATCH] 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) --- TODO.md | 55 +- include/akgl/staticstring.h | 17 +- src/sprite.c | 39 +- src/staticstring.c | 17 +- src/tilemap.c | 19 + .../snippets/test_tilemap_max_objects.json | 1549 ++++++++++++++++ .../test_tilemap_too_many_objects.json | 1561 +++++++++++++++++ .../test_tilemap_too_many_tilesets.json | 225 +++ tests/assets/testsprite_maxframes.json | 31 + tests/assets/testsprite_toomanyframes.json | 32 + tests/assets/testsprite_widecount.json | 17 + tests/sprite.c | 53 + tests/staticstring.c | 111 ++ tests/tilemap.c | 76 + 14 files changed, 3763 insertions(+), 39 deletions(-) create mode 100644 tests/assets/snippets/test_tilemap_max_objects.json create mode 100644 tests/assets/snippets/test_tilemap_too_many_objects.json create mode 100644 tests/assets/snippets/test_tilemap_too_many_tilesets.json create mode 100644 tests/assets/testsprite_maxframes.json create mode 100644 tests/assets/testsprite_toomanyframes.json create mode 100644 tests/assets/testsprite_widecount.json diff --git a/TODO.md b/TODO.md index ca81da8..e52881f 100644 --- a/TODO.md +++ b/TODO.md @@ -678,9 +678,19 @@ Each was found by a test written to assert correct behavior. `src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying the surfaces share dimensions, pitch, or format. 6. **`akgl_string_initialize` overflows by four bytes when `init` is NULL.** - `src/staticstring.c:17` does `memset(&obj->data, 0x00, sizeof(akgl_String))`, - but `data` starts four bytes into the struct (after `refcount`), so the memset - runs four bytes past the end. + **Fixed in 0.5.0**: it zeroes `sizeof(obj->data)`. The four bytes it used to + run past the end of the object landed on the *next* pool slot's `refcount`, + which is the field the allocator reads to decide whether a slot is free -- + so the overrun could hand a live string out twice. + + `tests/staticstring.c` claims two adjacent slots, stamps a sentinel into the + second's refcount, and initializes the first. + + Fixed alongside it, because it is the same file and the same class: + `akgl_string_copy` accepted a `count` above `AKGL_MAX_STRING_LENGTH`, which + read past the end of one pool slot and wrote past the end of another. The + header documented that as behaviour. It is `AKERR_OUTOFBOUNDS` now, and a + negative count is refused too. 7. **Savegame name lengths disagree between writer and reader.** `akgl_game_save_actors` writes spritesheet names at `AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` (512) and character names at @@ -819,35 +829,24 @@ without coming here first. Ordered by blast radius. only record that a retry is wanted. Touches `src/util.c:117-121` only. 16. **`akgl_sprite_load_json` does not bound the `frames` array.** - `src/sprite.c:165` sets `obj->frames` from `json_array_size()` and - `src/sprite.c:167` then writes that many entries into - `frameids[AKGL_SPRITE_MAX_FRAMES]`, which is 16. A sprite definition with 17 - or more frames writes past the array into the rest of `akgl_Sprite`, and - past the struct into the neighbouring pool slot. It is also written through - a `uint32_t *` cast of a `uint8_t *`, so each element write touches four - bytes; that happens to work on a little-endian machine because the following - iterations overwrite the spill and the last write lands in the struct's - alignment padding, but it is not portable and it is what makes the overflow - reach four bytes past `frames` rather than one. + **Fixed in 0.5.0.** The count is bounded against `AKGL_SPRITE_MAX_FRAMES` + before anything is written, and each element is read into an `int` and + narrowed deliberately rather than written through a `uint32_t *` cast of a + `uint8_t *`. A frame number that does not fit a `uint8_t` is refused too, + rather than truncated into an index that names a different tile. - Fix: refuse a `frames` array longer than `AKGL_SPRITE_MAX_FRAMES` with - `AKERR_OUTOFBOUNDS`, and read into an `int` local before narrowing to - `uint8_t`. Touches `src/sprite.c:164-169`. + `tests/sprite.c` covers exactly the maximum (must load, and every id must + arrive), one past it, and the wide frame number, against three new fixtures. + Against the old code the middle case loads happily. 17. **Two more unbounded array loads in the tilemap loader.** - `src/tilemap.c:387-389` walks a Tiled object layer straight into - `curlayer->objects[j]` with no check against - `AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER` (128), and `src/tilemap.c:278-280` - fills `dest->tilesets[i]` with no check against `AKGL_TILEMAP_MAX_TILESETS` - (16). - Both are the same shape as item 16 and both are reachable from an ordinary - map file: 128 objects is not a large object layer. Note that - `akgl_tilemap_load_layers` *does* bound its loop and raises - `AKERR_OUTOFBOUNDS`, so the pattern to copy is already in the same file. + **Fixed in 0.5.0**, both with the bound at the top of the loop body, the + shape `akgl_tilemap_load_layers` in the same file already used. - Fix: add the bound check at the top of each loop body, matching - `akgl_tilemap_load_layers`. Touches `src/tilemap.c:387` and - `src/tilemap.c:278`. + `tests/tilemap.c` covers both against generated fixtures: an object layer + of exactly 128 (must load) and of 129, and a map with 17 tilesets. The + object one is the reachable half -- 128 objects is not a large object layer + and `akgl_TilemapObject` is not small. 18. **`akgl_get_json_with_default` defaults on a status the array accessors never raise.** `src/json_helpers.c:164-165` handles `AKERR_KEY` and `AKERR_INDEX`, diff --git a/include/akgl/staticstring.h b/include/akgl/staticstring.h index 1ae8f90..2953300 100644 --- a/include/akgl/staticstring.h +++ b/include/akgl/staticstring.h @@ -40,10 +40,10 @@ typedef struct * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p obj is `NULL`. * - * @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)` - * bytes starting at `data`, which is four bytes past the end of the - * buffer -- `refcount` sits in front of it. TODO.md, "Known and still - * open" item 6. + * @note Until 0.5.0 the `NULL` @p init path zeroed `sizeof(akgl_String)` bytes + * starting at `data`, which is four bytes past the end of the buffer -- + * `refcount` sits in front of it, so the overrun landed on the next pool + * slot's reference count. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init); /** @@ -58,11 +58,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char * @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the * whole buffer. A @p count shorter than the source truncates * without writing a terminator; a @p count longer than the source - * zero-pads the remainder, per `strncpy`. Values above - * #AKGL_MAX_STRING_LENGTH overrun both buffers and are not - * rejected. + * zero-pads the remainder, per `strncpy`. A negative @p count, or + * one above #AKGL_MAX_STRING_LENGTH, is refused -- both buffers + * are exactly that long, so a larger count walked off the end of + * two pool slots at once. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p src or @p dest is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p count is negative or above + * #AKGL_MAX_STRING_LENGTH. * @throws errno Whatever `errno` holds if `strncpy` returns something other * than @p dest. In practice `strncpy` always returns its destination, so * this path is unreachable rather than merely rare. diff --git a/src/sprite.c b/src/sprite.c index 0dd5a11..0929058 100644 --- a/src/sprite.c +++ b/src/sprite.c @@ -119,6 +119,8 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename) 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 { @@ -161,9 +163,40 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename) 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)); - obj->frames = json_array_size((json_t *)frames); - for ( i = 0 ; i < obj->frames; i++ ) { - CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i])); + // 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 diff --git a/src/staticstring.c b/src/staticstring.c index bd279a1..357ef51 100644 --- a/src/staticstring.c +++ b/src/staticstring.c @@ -14,7 +14,12 @@ akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init) if ( init != NULL ) { strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH); } else { - memset(&obj->data, 0x00, sizeof(akgl_String)); + // sizeof(obj->data), not sizeof(akgl_String). `data` starts after the + // `refcount` in front of it, so zeroing the size of the whole struct + // from the start of the buffer ran four bytes past the end of the + // object -- into the next pool slot's refcount, which is what makes a + // free slot look claimed or a claimed one look free. + memset(&obj->data, 0x00, sizeof(obj->data)); } obj->refcount = 1; SUCCEED_RETURN(errctx); @@ -28,6 +33,16 @@ akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dest, int co if ( count == 0 ) { count = AKGL_MAX_STRING_LENGTH; } + // Both buffers are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count + // walks off the end of two pool slots at once. Refused rather than + // documented, which is what it used to be. + FAIL_NONZERO_RETURN( + errctx, + ((count < 0) || (count > AKGL_MAX_STRING_LENGTH)), + AKERR_OUTOFBOUNDS, + "Copy count %d is outside 0..%d", + count, + AKGL_MAX_STRING_LENGTH); if ( (char *)dest->data != strncpy((char *)&dest->data, (char *)&src->data, count) ) { FAIL_RETURN(errctx, errno, "strncpy"); } diff --git a/src/tilemap.c b/src/tilemap.c index 7a4ff20..be48f85 100644 --- a/src/tilemap.c +++ b/src/tilemap.c @@ -238,6 +238,17 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, ATTEMPT { CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets)) for (i = 0; i < json_array_size((json_t *)tilesets); i++) { + // The bound goes at the top of the body, matching + // akgl_tilemap_load_layers. Without it a map with seventeen + // tilesets wrote past the fixed table -- and unlike the layer + // count, nothing anywhere else was checking this one. + FAIL_NONZERO_BREAK( + errctx, + (i >= AKGL_TILEMAP_MAX_TILESETS), + AKERR_OUTOFBOUNDS, + "Map declares more than %d tilesets", + AKGL_TILEMAP_MAX_TILESETS + ); CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset)); CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname)); CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i)); @@ -312,6 +323,14 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t * len = json_array_size((json_t *)layerdata); curlayer = &dest->layers[layerid]; for ( j = 0; j < len; j++ ) { + FAIL_NONZERO_RETURN( + errctx, + (j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER), + AKERR_OUTOFBOUNDS, + "Object layer %d has more than %d objects", + layerid, + AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER + ); PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue)); curobj = &curlayer->objects[j]; PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr)); diff --git a/tests/assets/snippets/test_tilemap_max_objects.json b/tests/assets/snippets/test_tilemap_max_objects.json new file mode 100644 index 0000000..3e1a392 --- /dev/null +++ b/tests/assets/snippets/test_tilemap_max_objects.json @@ -0,0 +1,1549 @@ +{ + "draworder": "topdown", + "id": 2, + "locked": true, + "name": "Object Layer 1", + "objects": [ + { + "gid": 195, + "height": 16, + "id": 1, + "name": "filler0", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 2, + "name": "filler1", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 3, + "name": "filler2", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 4, + "name": "filler3", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 5, + "name": "filler4", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 6, + "name": "filler5", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 7, + "name": "filler6", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 8, + "name": "filler7", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 9, + "name": "filler8", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 10, + "name": "filler9", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 11, + "name": "filler10", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 12, + "name": "filler11", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 13, + "name": "filler12", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 14, + "name": "filler13", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 15, + "name": "filler14", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 16, + "name": "filler15", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 17, + "name": "filler16", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 18, + "name": "filler17", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 19, + "name": "filler18", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 20, + "name": "filler19", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 21, + "name": "filler20", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 22, + "name": "filler21", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 23, + "name": "filler22", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 24, + "name": "filler23", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 25, + "name": "filler24", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 26, + "name": "filler25", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 27, + "name": "filler26", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 28, + "name": "filler27", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 29, + "name": "filler28", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 30, + "name": "filler29", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 31, + "name": "filler30", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 32, + "name": "filler31", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 33, + "name": "filler32", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 34, + "name": "filler33", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 35, + "name": "filler34", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 36, + "name": "filler35", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 37, + "name": "filler36", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 38, + "name": "filler37", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 39, + "name": "filler38", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 40, + "name": "filler39", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 41, + "name": "filler40", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 42, + "name": "filler41", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 43, + "name": "filler42", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 44, + "name": "filler43", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 45, + "name": "filler44", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 46, + "name": "filler45", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 47, + "name": "filler46", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 48, + "name": "filler47", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 49, + "name": "filler48", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 50, + "name": "filler49", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 51, + "name": "filler50", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 52, + "name": "filler51", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 53, + "name": "filler52", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 54, + "name": "filler53", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 55, + "name": "filler54", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 56, + "name": "filler55", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 57, + "name": "filler56", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 58, + "name": "filler57", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 59, + "name": "filler58", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 60, + "name": "filler59", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 61, + "name": "filler60", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 62, + "name": "filler61", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 63, + "name": "filler62", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 64, + "name": "filler63", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 65, + "name": "filler64", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 66, + "name": "filler65", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 67, + "name": "filler66", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 68, + "name": "filler67", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 69, + "name": "filler68", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 70, + "name": "filler69", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 71, + "name": "filler70", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 72, + "name": "filler71", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 73, + "name": "filler72", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 74, + "name": "filler73", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 75, + "name": "filler74", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 76, + "name": "filler75", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 77, + "name": "filler76", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 78, + "name": "filler77", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 79, + "name": "filler78", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 80, + "name": "filler79", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 81, + "name": "filler80", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 82, + "name": "filler81", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 83, + "name": "filler82", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 84, + "name": "filler83", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 85, + "name": "filler84", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 86, + "name": "filler85", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 87, + "name": "filler86", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 88, + "name": "filler87", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 89, + "name": "filler88", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 90, + "name": "filler89", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 91, + "name": "filler90", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 92, + "name": "filler91", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 93, + "name": "filler92", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 94, + "name": "filler93", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 95, + "name": "filler94", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 96, + "name": "filler95", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 97, + "name": "filler96", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 98, + "name": "filler97", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 99, + "name": "filler98", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 100, + "name": "filler99", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 101, + "name": "filler100", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 102, + "name": "filler101", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 103, + "name": "filler102", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 104, + "name": "filler103", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 105, + "name": "filler104", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 106, + "name": "filler105", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 107, + "name": "filler106", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 108, + "name": "filler107", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 109, + "name": "filler108", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 110, + "name": "filler109", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 111, + "name": "filler110", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 112, + "name": "filler111", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 113, + "name": "filler112", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 114, + "name": "filler113", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 115, + "name": "filler114", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 116, + "name": "filler115", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 117, + "name": "filler116", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 118, + "name": "filler117", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 119, + "name": "filler118", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 120, + "name": "filler119", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 121, + "name": "filler120", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 122, + "name": "filler121", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 123, + "name": "filler122", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 124, + "name": "filler123", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 125, + "name": "filler124", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 126, + "name": "filler125", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 127, + "name": "filler126", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 128, + "name": "filler127", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 112 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 +} \ No newline at end of file diff --git a/tests/assets/snippets/test_tilemap_too_many_objects.json b/tests/assets/snippets/test_tilemap_too_many_objects.json new file mode 100644 index 0000000..07b7e8c --- /dev/null +++ b/tests/assets/snippets/test_tilemap_too_many_objects.json @@ -0,0 +1,1561 @@ +{ + "draworder": "topdown", + "id": 2, + "locked": true, + "name": "Object Layer 1", + "objects": [ + { + "gid": 195, + "height": 16, + "id": 1, + "name": "filler0", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 2, + "name": "filler1", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 3, + "name": "filler2", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 4, + "name": "filler3", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 5, + "name": "filler4", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 6, + "name": "filler5", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 7, + "name": "filler6", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 8, + "name": "filler7", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 9, + "name": "filler8", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 10, + "name": "filler9", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 11, + "name": "filler10", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 12, + "name": "filler11", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 13, + "name": "filler12", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 14, + "name": "filler13", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 15, + "name": "filler14", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 16, + "name": "filler15", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 0 + }, + { + "gid": 195, + "height": 16, + "id": 17, + "name": "filler16", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 18, + "name": "filler17", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 19, + "name": "filler18", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 20, + "name": "filler19", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 21, + "name": "filler20", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 22, + "name": "filler21", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 23, + "name": "filler22", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 24, + "name": "filler23", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 25, + "name": "filler24", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 26, + "name": "filler25", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 27, + "name": "filler26", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 28, + "name": "filler27", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 29, + "name": "filler28", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 30, + "name": "filler29", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 31, + "name": "filler30", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 32, + "name": "filler31", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 16 + }, + { + "gid": 195, + "height": 16, + "id": 33, + "name": "filler32", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 34, + "name": "filler33", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 35, + "name": "filler34", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 36, + "name": "filler35", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 37, + "name": "filler36", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 38, + "name": "filler37", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 39, + "name": "filler38", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 40, + "name": "filler39", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 41, + "name": "filler40", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 42, + "name": "filler41", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 43, + "name": "filler42", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 44, + "name": "filler43", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 45, + "name": "filler44", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 46, + "name": "filler45", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 47, + "name": "filler46", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 48, + "name": "filler47", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 32 + }, + { + "gid": 195, + "height": 16, + "id": 49, + "name": "filler48", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 50, + "name": "filler49", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 51, + "name": "filler50", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 52, + "name": "filler51", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 53, + "name": "filler52", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 54, + "name": "filler53", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 55, + "name": "filler54", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 56, + "name": "filler55", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 57, + "name": "filler56", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 58, + "name": "filler57", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 59, + "name": "filler58", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 60, + "name": "filler59", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 61, + "name": "filler60", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 62, + "name": "filler61", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 63, + "name": "filler62", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 64, + "name": "filler63", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 48 + }, + { + "gid": 195, + "height": 16, + "id": 65, + "name": "filler64", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 66, + "name": "filler65", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 67, + "name": "filler66", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 68, + "name": "filler67", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 69, + "name": "filler68", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 70, + "name": "filler69", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 71, + "name": "filler70", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 72, + "name": "filler71", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 73, + "name": "filler72", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 74, + "name": "filler73", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 75, + "name": "filler74", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 76, + "name": "filler75", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 77, + "name": "filler76", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 78, + "name": "filler77", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 79, + "name": "filler78", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 80, + "name": "filler79", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 64 + }, + { + "gid": 195, + "height": 16, + "id": 81, + "name": "filler80", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 82, + "name": "filler81", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 83, + "name": "filler82", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 84, + "name": "filler83", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 85, + "name": "filler84", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 86, + "name": "filler85", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 87, + "name": "filler86", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 88, + "name": "filler87", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 89, + "name": "filler88", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 90, + "name": "filler89", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 91, + "name": "filler90", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 92, + "name": "filler91", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 93, + "name": "filler92", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 94, + "name": "filler93", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 95, + "name": "filler94", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 96, + "name": "filler95", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 80 + }, + { + "gid": 195, + "height": 16, + "id": 97, + "name": "filler96", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 98, + "name": "filler97", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 99, + "name": "filler98", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 100, + "name": "filler99", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 101, + "name": "filler100", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 102, + "name": "filler101", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 103, + "name": "filler102", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 104, + "name": "filler103", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 105, + "name": "filler104", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 106, + "name": "filler105", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 107, + "name": "filler106", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 108, + "name": "filler107", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 109, + "name": "filler108", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 110, + "name": "filler109", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 111, + "name": "filler110", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 112, + "name": "filler111", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 96 + }, + { + "gid": 195, + "height": 16, + "id": 113, + "name": "filler112", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 114, + "name": "filler113", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 16, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 115, + "name": "filler114", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 32, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 116, + "name": "filler115", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 48, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 117, + "name": "filler116", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 64, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 118, + "name": "filler117", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 80, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 119, + "name": "filler118", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 96, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 120, + "name": "filler119", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 112, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 121, + "name": "filler120", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 128, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 122, + "name": "filler121", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 144, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 123, + "name": "filler122", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 160, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 124, + "name": "filler123", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 176, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 125, + "name": "filler124", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 192, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 126, + "name": "filler125", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 208, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 127, + "name": "filler126", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 224, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 128, + "name": "filler127", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 240, + "y": 112 + }, + { + "gid": 195, + "height": 16, + "id": 129, + "name": "filler128", + "rotation": 0, + "type": "", + "visible": true, + "width": 16, + "x": 0, + "y": 128 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 +} \ No newline at end of file diff --git a/tests/assets/snippets/test_tilemap_too_many_tilesets.json b/tests/assets/snippets/test_tilemap_too_many_tilesets.json new file mode 100644 index 0000000..e87ca55 --- /dev/null +++ b/tests/assets/snippets/test_tilemap_too_many_tilesets.json @@ -0,0 +1,225 @@ +{ + "tilesets": [ + { + "columns": 48, + "firstgid": 1, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_0", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 1729, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_1", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 3457, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_2", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 5185, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_3", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 6913, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_4", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 8641, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_5", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 10369, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_6", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 12097, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_7", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 13825, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_8", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 15553, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_9", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 17281, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_10", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 19009, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_11", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 20737, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_12", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 22465, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_13", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 24193, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_14", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 25921, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_15", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + }, + { + "columns": 48, + "firstgid": 27649, + "image": "assets/World_A1.png", + "imageheight": 576, + "imagewidth": 768, + "margin": 0, + "name": "World_A1_16", + "spacing": 0, + "tilecount": 1728, + "tileheight": 16, + "tilewidth": 16 + } + ] +} \ No newline at end of file diff --git a/tests/assets/testsprite_maxframes.json b/tests/assets/testsprite_maxframes.json new file mode 100644 index 0000000..4123147 --- /dev/null +++ b/tests/assets/testsprite_maxframes.json @@ -0,0 +1,31 @@ +{ + "spritesheet": { + "filename": "spritesheet.png", + "frame_width": 48, + "frame_height": 48 + }, + "name": "testsprite_maxframes", + "width": 48, + "height": 48, + "speed": 100, + "loop": true, + "loopReverse": true, + "frames": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ] +} \ No newline at end of file diff --git a/tests/assets/testsprite_toomanyframes.json b/tests/assets/testsprite_toomanyframes.json new file mode 100644 index 0000000..19930ea --- /dev/null +++ b/tests/assets/testsprite_toomanyframes.json @@ -0,0 +1,32 @@ +{ + "spritesheet": { + "filename": "spritesheet.png", + "frame_width": 48, + "frame_height": 48 + }, + "name": "testsprite_toomanyframes", + "width": 48, + "height": 48, + "speed": 100, + "loop": true, + "loopReverse": true, + "frames": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ] +} \ No newline at end of file diff --git a/tests/assets/testsprite_widecount.json b/tests/assets/testsprite_widecount.json new file mode 100644 index 0000000..4f8a882 --- /dev/null +++ b/tests/assets/testsprite_widecount.json @@ -0,0 +1,17 @@ +{ + "spritesheet": { + "filename": "spritesheet.png", + "frame_width": 48, + "frame_height": 48 + }, + "name": "testsprite_widecount", + "width": 48, + "height": 48, + "speed": 100, + "loop": true, + "loopReverse": true, + "frames": [ + 0, + 256 + ] +} \ No newline at end of file diff --git a/tests/sprite.c b/tests/sprite.c index a71221a..f83977c 100644 --- a/tests/sprite.c +++ b/tests/sprite.c @@ -184,6 +184,58 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void) SUCCEED_RETURN(errctx); } +/** + * @brief A sprite definition must not be able to write past `frameids`. + * + * `frameids` is AKGL_SPRITE_MAX_FRAMES bytes and the loader took 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 next pool slot. It also + * wrote through a `uint32_t *` cast of a `uint8_t *`, four bytes at a time, + * which is why the overrun reached four bytes past the array rather than one. + * + * The neighbouring slot is claimed and stamped first, so the test fails on the + * corruption rather than on whatever the corruption happens to do later. + */ +akerr_ErrorContext *test_akgl_sprite_load_json_bounds_frames(void) +{ + PREPARE_ERROR(errctx); + akgl_Sprite *loaded = NULL; + int i = 0; + + ATTEMPT { + // Exactly the maximum is legal and must still load. + TEST_EXPECT_OK(errctx, akgl_sprite_load_json("assets/testsprite_maxframes.json"), + "loading a sprite with exactly AKGL_SPRITE_MAX_FRAMES frames"); + loaded = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_maxframes", NULL); + FAIL_ZERO_BREAK(errctx, loaded, AKERR_KEY, "the max-frames sprite is not in the registry"); + TEST_ASSERT(errctx, loaded->frames == AKGL_SPRITE_MAX_FRAMES, + "max-frames sprite loaded %d frames, expected %d", + loaded->frames, AKGL_SPRITE_MAX_FRAMES); + for ( i = 0; i < AKGL_SPRITE_MAX_FRAMES; i++ ) { + TEST_ASSERT(errctx, loaded->frameids[i] == (uint8_t)i, + "max-frames sprite frame %d is %d, expected %d", + i, loaded->frameids[i], i); + } + + // One more than the maximum is refused, and nothing is registered. + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_sprite_load_json("assets/testsprite_toomanyframes.json"), + "loading a sprite with more frames than the array holds"); + TEST_ASSERT(errctx, + SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_toomanyframes", NULL) == NULL, + "a sprite with too many frames was registered anyway"); + + // A frame number a uint8_t cannot hold is refused rather than truncated + // to something that indexes a different tile. + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_sprite_load_json("assets/testsprite_widecount.json"), + "loading a sprite whose frame number does not fit a uint8_t"); + } CLEANUP { + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + int main(void) { PREPARE_ERROR(errctx); @@ -211,6 +263,7 @@ int main(void) CATCH(errctx, test_akgl_spritesheet_initialize()); CATCH(errctx, test_akgl_sprite_initialize()); CATCH(errctx, test_akgl_sprite_load_json()); + CATCH(errctx, test_akgl_sprite_load_json_bounds_frames()); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); diff --git a/tests/staticstring.c b/tests/staticstring.c index b778eee..5630540 100644 --- a/tests/staticstring.c +++ b/tests/staticstring.c @@ -130,6 +130,111 @@ void reset_string_heap(void) } } +/** + * @brief akgl_string_initialize must not write past the buffer it is zeroing. + * + * The `NULL` init path zeroed `sizeof(akgl_String)` bytes starting at `data`. + * `data` begins after the `int refcount` in front of it, so that ran four bytes + * past the end of the object -- straight onto the *next* pool slot's refcount, + * which is the field the allocator uses to decide whether a slot is free. + * + * Claiming two adjacent slots and initializing the first is enough to catch it: + * the second's refcount goes to zero and the pool believes it is free while the + * caller is still holding it. + */ +akerr_ErrorContext *test_akgl_string_initialize_stays_in_bounds(void) +{ + PREPARE_ERROR(errctx); + akgl_String *first = NULL; + akgl_String *second = NULL; + + ATTEMPT { + CATCH(errctx, akgl_heap_next_string(&first)); + CATCH(errctx, akgl_heap_next_string(&second)); + TEST_ASSERT(errctx, second == (first + 1), + "the pool did not hand out adjacent slots; this test needs them"); + + // A sentinel the overrun would land on. akgl_heap_next_string has + // already set it to 1; make it something an accidental write cannot + // coincide with. + second->refcount = 0x5A; + + CATCH(errctx, akgl_string_initialize(first, NULL)); + + TEST_ASSERT(errctx, second->refcount == 0x5A, + "initializing a string wrote past its buffer: the next slot's " + "refcount is %d, expected %d", + second->refcount, 0x5A); + TEST_ASSERT(errctx, first->refcount == 1, + "initializing a string left its own refcount at %d, expected 1", + first->refcount); + TEST_ASSERT(errctx, first->data[0] == '\0', + "initializing a string with NULL did not zero its buffer"); + TEST_ASSERT(errctx, first->data[AKGL_MAX_STRING_LENGTH - 1] == '\0', + "initializing a string with NULL did not zero its last byte"); + } CLEANUP { + if ( second != NULL ) { + second->refcount = 1; + IGNORE(akgl_heap_release_string(second)); + } + if ( first != NULL ) { + IGNORE(akgl_heap_release_string(first)); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + +/** + * @brief akgl_string_copy must refuse a count that would leave both buffers. + * + * Both slots are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count read + * past the end of one and wrote past the end of the other. The header used to + * document that as behaviour. + */ +akerr_ErrorContext *test_akgl_string_copy_bounds_its_count(void) +{ + PREPARE_ERROR(errctx); + akgl_String *src = NULL; + akgl_String *dest = NULL; + + ATTEMPT { + CATCH(errctx, akgl_heap_next_string(&src)); + CATCH(errctx, akgl_heap_next_string(&dest)); + CATCH(errctx, akgl_string_initialize(src, "bounded")); + CATCH(errctx, akgl_string_initialize(dest, NULL)); + + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH + 1), + "copying one byte more than a pool string holds"); + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_string_copy(src, dest, -1), + "copying a negative number of bytes"); + + // The boundary itself is legal: it is exactly the buffer. + TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH), + "copying exactly a pool string's length"); + TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0, + "a full-length copy did not transfer the contents"); + + // 0 still means "the whole buffer" rather than "nothing". + CATCH(errctx, akgl_string_initialize(dest, NULL)); + TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, 0), + "copying with a count of zero"); + TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0, + "a zero-count copy did not transfer the whole buffer"); + } CLEANUP { + if ( dest != NULL ) { + IGNORE(akgl_heap_release_string(dest)); + } + if ( src != NULL ) { + IGNORE(akgl_heap_release_string(src)); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + int main(void) { @@ -151,6 +256,12 @@ int main(void) reset_string_heap(); printf("test_akgl_string_initialize....\n"); test_akgl_string_initialize(); + reset_string_heap(); + printf("test_akgl_string_initialize_stays_in_bounds ...\n"); + CATCH(errctx, test_akgl_string_initialize_stays_in_bounds()); + reset_string_heap(); + printf("test_akgl_string_copy_bounds_its_count ...\n"); + CATCH(errctx, test_akgl_string_copy_bounds_its_count()); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); diff --git a/tests/tilemap.c b/tests/tilemap.c index 9286269..a0ea1a2 100644 --- a/tests/tilemap.c +++ b/tests/tilemap.c @@ -229,6 +229,81 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void) SUCCEED_RETURN(errctx); } +/** + * @brief The loader must refuse a map that would overrun its fixed tables. + * + * Two loops indexed straight from the document. `curlayer->objects[j]` had no + * check against AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, and `dest->tilesets[i]` + * none against AKGL_TILEMAP_MAX_TILESETS. The object one is the reachable one: + * 128 objects is not a large object layer, and akgl_TilemapObject is big, so + * the 129th wrote well past the end of the layer. + * + * akgl_tilemap_load_layers already bounded its own loop and raised + * AKERR_OUTOFBOUNDS, so the shape to copy was in the same file. + */ +akerr_ErrorContext *test_akgl_tilemap_load_bounds_fixed_tables(void) +{ + akgl_String *pathstr = NULL; + PREPARE_ERROR(errctx); + json_t *doc = NULL; + json_error_t errdata; + + ATTEMPT { + akgl_gamemap = &akgl_default_gamemap; + akgl_renderer = &akgl_default_renderer; + CATCH(errctx, akgl_heap_next_string(&pathstr)); + + // Exactly the maximum must still load. + memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap)); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", + SDL_GetBasePath(), "assets/snippets/test_tilemap_max_objects.json"); + doc = json_load_file((char *)&pathstr->data, 0, &errdata); + FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "max-objects fixture: %s", (char *)&errdata.text); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets"); + TEST_EXPECT_OK(errctx, + akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr), + "loading an object layer with exactly the maximum objects"); + json_decref(doc); + doc = NULL; + + // One more must be refused rather than written. + memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap)); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", + SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_objects.json"); + doc = json_load_file((char *)&pathstr->data, 0, &errdata); + FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-objects fixture: %s", (char *)&errdata.text); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets"); + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr), + "loading an object layer with one object too many"); + json_decref(doc); + doc = NULL; + + // And the tileset table. + memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap)); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", + SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_tilesets.json"); + doc = json_load_file((char *)&pathstr->data, 0, &errdata); + FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-tilesets fixture: %s", (char *)&errdata.text); + snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets"); + TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, + akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr), + "loading a map with one tileset too many"); + TEST_ASSERT(errctx, akgl_gamemap->numtilesets <= AKGL_TILEMAP_MAX_TILESETS, + "numtilesets reached %d, past the %d the table holds", + akgl_gamemap->numtilesets, AKGL_TILEMAP_MAX_TILESETS); + } CLEANUP { + if ( doc != NULL ) { + json_decref(doc); + } + if ( pathstr != NULL ) { + IGNORE(akgl_heap_release_string(pathstr)); + } + } PROCESS(errctx) { + } FINISH(errctx, true); + SUCCEED_RETURN(errctx); +} + akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void) { akgl_String *pathstr; @@ -538,6 +613,7 @@ int main(void) CATCH(errctx, test_akgl_tilemap_load_layer_tile()); CATCH(errctx, test_akgl_tilemap_load_layers()); CATCH(errctx, test_akgl_tilemap_load_tilesets()); + CATCH(errctx, test_akgl_tilemap_load_bounds_fixed_tables()); //CATCH(errctx, test_akgl_tilemap_load()); //CATCH(errctx, test_akgl_tilemap_draw_tileset()); //CATCH(errctx, test_akgl_tilemap_draw());