From 454a5721a6d1a5760c2c9802cb3d557706cbb9dd Mon Sep 17 00:00:00 2001 From: sundaysebasidian-byte <292981763+sundaysebasidian-byte@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:23:18 -0400 Subject: [PATCH] Refactor checked allocation and image-size arithmetic Use private C99 checked size_t add and multiply helpers for allocation, buffer growth and image sizes. Preserve API errors and progressive overflow sentinels, with boundary and normal-image regression tests. Related to #217. Implemented, tested and reviewed with OpenAI Codex AI assistance. --- spng/spng.c | 88 +++--- tests/meson.build | 9 +- tests/test_checked_arithmetic.c | 489 ++++++++++++++++++++++++++++++++ 3 files changed, 542 insertions(+), 44 deletions(-) create mode 100644 tests/test_checked_arithmetic.c diff --git a/spng/spng.c b/spng/spng.c index b22b7110..33d81e40 100644 --- a/spng/spng.c +++ b/spng/spng.c @@ -395,6 +395,23 @@ static const uint8_t type_time[4] = { 116, 73, 77, 69 }; static const uint8_t type_offs[4] = { 111, 70, 70, 115 }; static const uint8_t type_exif[4] = { 101, 88, 73, 102 }; +/* Return nonzero on overflow, leaving *result unchanged. */ +static inline int spng__ckd_add(size_t *result, size_t a, size_t b) +{ + if(a > SIZE_MAX - b) return 1; + + *result = a + b; + return 0; +} + +static inline int spng__ckd_mul(size_t *result, size_t a, size_t b) +{ + if(b && a > SIZE_MAX / b) return 1; + + *result = a * b; + return 0; +} + static inline void *spng__malloc(spng_ctx *ctx, size_t size) { return ctx->alloc.malloc_fn(size); @@ -423,9 +440,8 @@ static void *spng__zalloc(void *opaque, uInt items, uInt size) { spng_ctx *ctx = opaque; - if(size > SIZE_MAX / items) return NULL; - - size_t len = (size_t)items * size; + size_t len; + if(spng__ckd_mul(&len, items, size)) return NULL; return spng__malloc(ctx, len); } @@ -566,12 +582,9 @@ static int calculate_scanline_width(const struct spng_ihdr *ihdr, uint32_t width size_t res = num_channels(ihdr) * ihdr->bit_depth; - if(res > SIZE_MAX / width) return SPNG_EOVERFLOW; - res = res * width; - - res += 15; /* Filter byte + 7 for rounding */ - - if(res < 15) return SPNG_EOVERFLOW; + if(spng__ckd_mul(&res, res, width)) return SPNG_EOVERFLOW; + /* Filter byte + 7 for rounding */ + if(spng__ckd_add(&res, res, 15)) return SPNG_EOVERFLOW; res /= 8; @@ -687,8 +700,7 @@ static int calculate_image_width(const struct spng_ihdr *ihdr, int fmt, size_t * default: return SPNG_EINTERNAL; } - if(res > SIZE_MAX / bytes_per_pixel) return SPNG_EOVERFLOW; - res = res * bytes_per_pixel; + if(spng__ckd_mul(&res, res, bytes_per_pixel)) return SPNG_EOVERFLOW; *len = res; @@ -704,8 +716,7 @@ static int calculate_image_size(const struct spng_ihdr *ihdr, int fmt, size_t *l int ret = calculate_image_width(ihdr, fmt, &res); if(ret) return ret; - if(res > SIZE_MAX / ihdr->height) return SPNG_EOVERFLOW; - res = res * ihdr->height; + if(spng__ckd_mul(&res, res, ihdr->height)) return SPNG_EOVERFLOW; *len = res; @@ -819,8 +830,8 @@ static int require_bytes(spng_ctx *ctx, size_t bytes) if(!ctx->internal_buffer) return SPNG_ENODST; - size_t required = ctx->bytes_encoded + bytes; - if(required < bytes) return SPNG_EOVERFLOW; + size_t required; + if(spng__ckd_add(&required, ctx->bytes_encoded, bytes)) return SPNG_EOVERFLOW; if(required > ctx->out_png_size) { @@ -832,9 +843,7 @@ static int require_bytes(spng_ctx *ctx, size_t bytes) /* Prefer the next power of two over the requested size */ while(new_size < required) { - if(new_size / SIZE_MAX > 2) return encode_err(ctx, SPNG_EOVERFLOW); - - new_size *= 2; + if(spng__ckd_mul(&new_size, new_size, 2)) return encode_err(ctx, SPNG_EOVERFLOW); } void *temp = spng__realloc(ctx, ctx->out_png, new_size); @@ -1289,14 +1298,15 @@ static int spng__inflate_stream(spng_ctx *ctx, char **out, size_t *len, size_t e if(!stream->avail_out) /* Resize buffer */ { + size_t new_size; /* overflow or reached chunk/cache limit */ - if( (2 > SIZE_MAX / size) || (size > max / 2) ) + if(spng__ckd_mul(&new_size, size, 2) || new_size > max) { ret = SPNG_ECHUNK_LIMITS; goto err; } - size *= 2; + size = new_size; t = spng__realloc(ctx, buf, size); if(t == NULL) goto mem; @@ -1333,8 +1343,7 @@ static int spng__inflate_stream(spng_ctx *ctx, char **out, size_t *len, size_t e goto err; } - size += extra; - if(size < extra) goto mem; + if(spng__ckd_add(&size, size, extra)) goto mem; t = spng__realloc(ctx, buf, size); if(t == NULL) goto mem; @@ -2772,9 +2781,10 @@ static int read_non_idat_chunks(spng_ctx *ctx) ctx->n_text++; if(ctx->n_text < 1) return SPNG_EOVERFLOW; - if(sizeof(struct spng_text2) > SIZE_MAX / ctx->n_text) return SPNG_EOVERFLOW; + size_t list_size; + if(spng__ckd_mul(&list_size, ctx->n_text, sizeof(struct spng_text2))) return SPNG_EOVERFLOW; - void *buf = spng__realloc(ctx, ctx->text_list, ctx->n_text * sizeof(struct spng_text2)); + void *buf = spng__realloc(ctx, ctx->text_list, list_size); if(buf == NULL) return SPNG_EMEM; ctx->text_list = buf; @@ -2934,9 +2944,10 @@ static int read_non_idat_chunks(spng_ctx *ctx) ctx->n_splt++; if(ctx->n_splt < 1) return SPNG_EOVERFLOW; - if(sizeof(struct spng_splt) > SIZE_MAX / ctx->n_splt) return SPNG_EOVERFLOW; + size_t list_size; + if(spng__ckd_mul(&list_size, ctx->n_splt, sizeof(struct spng_splt))) return SPNG_EOVERFLOW; - void *buf = spng__realloc(ctx, ctx->splt_list, ctx->n_splt * sizeof(struct spng_splt)); + void *buf = spng__realloc(ctx, ctx->splt_list, list_size); if(buf == NULL) return SPNG_EMEM; ctx->splt_list = buf; @@ -2993,11 +3004,7 @@ static int read_non_idat_chunks(spng_ctx *ctx) if(!splt->n_entries) return SPNG_ECHUNK_SIZE; - size_t list_size = splt->n_entries; - - if(list_size > SIZE_MAX / sizeof(struct spng_splt_entry)) return SPNG_EOVERFLOW; - - list_size *= sizeof(struct spng_splt_entry); + if(spng__ckd_mul(&list_size, splt->n_entries, sizeof(struct spng_splt_entry))) return SPNG_EOVERFLOW; if(increase_cache_usage(ctx, list_size, 0)) return SPNG_ECHUNK_LIMITS; @@ -3050,9 +3057,10 @@ static int read_non_idat_chunks(spng_ctx *ctx) ctx->n_chunks++; if(ctx->n_chunks < 1) return SPNG_EOVERFLOW; - if(sizeof(struct spng_unknown_chunk) > SIZE_MAX / ctx->n_chunks) return SPNG_EOVERFLOW; + size_t list_size; + if(spng__ckd_mul(&list_size, ctx->n_chunks, sizeof(struct spng_unknown_chunk))) return SPNG_EOVERFLOW; - void *buf = spng__realloc(ctx, ctx->chunk_list, ctx->n_chunks * sizeof(struct spng_unknown_chunk)); + void *buf = spng__realloc(ctx, ctx->chunk_list, list_size); if(buf == NULL) return SPNG_EMEM; ctx->chunk_list = buf; @@ -3632,8 +3640,7 @@ int spng_decode_image(spng_ctx *ctx, void *out, size_t len, int fmt, int flags) ret = calculate_image_width(ihdr, fmt, &ctx->image_width); if(ret) return decode_err(ctx, ret); - if(ctx->image_width > SIZE_MAX / ihdr->height) ctx->image_size = 0; /* overflow */ - else ctx->image_size = ctx->image_width * ihdr->height; + if(spng__ckd_mul(&ctx->image_size, ctx->image_width, ihdr->height)) ctx->image_size = 0; /* overflow */ if( !(flags & SPNG_DECODE_PROGRESSIVE) ) { @@ -3671,9 +3678,7 @@ int spng_decode_image(spng_ctx *ctx, void *out, size_t len, int fmt, int flags) size_t scanline_buf_size = ctx->subimage[ctx->widest_pass].scanline_width; - scanline_buf_size += 32; - - if(scanline_buf_size < 32) return SPNG_EOVERFLOW; + if(spng__ckd_add(&scanline_buf_size, scanline_buf_size, 32)) return SPNG_EOVERFLOW; ctx->scanline_buf = spng__malloc(ctx, scanline_buf_size); ctx->prev_scanline_buf = spng__malloc(ctx, scanline_buf_size); @@ -4751,8 +4756,7 @@ int spng_encode_image(spng_ctx *ctx, const void *img, size_t len, int fmt, int f ret = calculate_image_width(ihdr, fmt, &ctx->image_width); if(ret) return encode_err(ctx, ret); - if(ctx->image_width > SIZE_MAX / ihdr->height) ctx->image_size = 0; /* overflow */ - else ctx->image_size = ctx->image_width * ihdr->height; + if(spng__ckd_mul(&ctx->image_size, ctx->image_width, ihdr->height)) ctx->image_size = 0; /* overflow */ if( !(flags & SPNG_ENCODE_PROGRESSIVE) ) { @@ -4801,9 +4805,7 @@ int spng_encode_image(spng_ctx *ctx, const void *img, size_t len, int fmt, int f size_t scanline_buf_size = ctx->subimage[ctx->widest_pass].scanline_width; - scanline_buf_size += 32; - - if(scanline_buf_size < 32) return SPNG_EOVERFLOW; + if(spng__ckd_add(&scanline_buf_size, scanline_buf_size, 32)) return SPNG_EOVERFLOW; ctx->scanline_buf = spng__malloc(ctx, scanline_buf_size); ctx->prev_scanline_buf = spng__malloc(ctx, scanline_buf_size); diff --git a/tests/meson.build b/tests/meson.build index 568c7663..3eaf1655 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -33,6 +33,13 @@ test_exe = executable('testsuite', 'testsuite.c', dependencies : test_deps) test('info', test_exe, args : 'info') +checked_arithmetic_exe = executable('test_checked_arithmetic', + 'test_checked_arithmetic.c', + dependencies : spng_deps, + include_directories : spng_inc +) +test('checked_arithmetic', checked_arithmetic_exe) + cpp_exe = executable('cpp_exe', 'test.cpp', dependencies : spng_dep) test('cpp_test', cpp_exe) @@ -48,4 +55,4 @@ corpora = subproject('fuzzing_corpora').get_variable('corpora') foreach case : corpora test('testcase', read_fuzzer, args : case) -endforeach \ No newline at end of file +endforeach diff --git a/tests/test_checked_arithmetic.c b/tests/test_checked_arithmetic.c new file mode 100644 index 00000000..457b82f2 --- /dev/null +++ b/tests/test_checked_arithmetic.c @@ -0,0 +1,489 @@ +/* SPDX-License-Identifier: (BSD-2-Clause AND libpng-2.0) */ + +/* Include the implementation to test private helpers without exporting them. */ +#include "../spng/spng.c" + +#define CHECK(expr) do { if(!(expr)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #expr); return 1; \ +} } while(0) + +static int test_helpers(void) +{ + const struct + { + size_t a, b, sum, product; + int add_overflow, mul_overflow; + } cases[] = + { + {0, 0, 0, 0, 0, 0}, + {0, SIZE_MAX, SIZE_MAX, 0, 0, 0}, + {SIZE_MAX, 0, SIZE_MAX, 0, 0, 0}, + {1, SIZE_MAX, 0, SIZE_MAX, 1, 0}, + {SIZE_MAX, 1, 0, SIZE_MAX, 1, 0}, + {SIZE_MAX - 1, 1, SIZE_MAX, SIZE_MAX - 1, 0, 0}, + {SIZE_MAX, SIZE_MAX, 0, 0, 1, 1}, + {SIZE_MAX / 2, 2, SIZE_MAX / 2 + 2, SIZE_MAX - 1, 0, 0}, + {SIZE_MAX / 2 + 1, 2, SIZE_MAX / 2 + 3, 0, 0, 1}, + {2, SIZE_MAX / 2 + 1, SIZE_MAX / 2 + 3, 0, 0, 1}, + {SIZE_MAX - 14, 15, 0, 0, 1, 1}, + {SIZE_MAX - 15, 15, SIZE_MAX, 0, 0, 1}, + {SIZE_MAX - 31, 32, 0, 0, 1, 1}, + {SIZE_MAX - 32, 32, SIZE_MAX, 0, 0, 1}, + {17, 23, 40, 391, 0, 0} + }; + + size_t i; + for(i=0; i < sizeof(cases) / sizeof(cases[0]); i++) + { + size_t result = 42; + CHECK(spng__ckd_add(&result, cases[i].a, cases[i].b) == cases[i].add_overflow); + CHECK(result == (cases[i].add_overflow ? 42 : cases[i].sum)); + result = 42; + CHECK(spng__ckd_mul(&result, cases[i].a, cases[i].b) == cases[i].mul_overflow); + CHECK(result == (cases[i].mul_overflow ? 42 : cases[i].product)); + + result = cases[i].a; + CHECK(spng__ckd_add(&result, result, cases[i].b) == cases[i].add_overflow); + CHECK(result == (cases[i].add_overflow ? cases[i].a : cases[i].sum)); + result = cases[i].a; + CHECK(spng__ckd_mul(&result, result, cases[i].b) == cases[i].mul_overflow); + CHECK(result == (cases[i].mul_overflow ? cases[i].a : cases[i].product)); + } + + return 0; +} + +static unsigned allocation_calls; +static size_t allocation_size; + +static void *record_malloc(size_t size) +{ + allocation_calls++; + allocation_size = size; + return NULL; /* Record the request without allocating it. */ +} + +static void *record_realloc(void *ptr, size_t size) +{ + (void)ptr; + return record_malloc(size); +} + +static int test_allocations(void) +{ + spng_ctx ctx = {0}; + ctx.alloc.malloc_fn = record_malloc; + ctx.alloc.realloc_fn = record_realloc; + + allocation_calls = 0; + CHECK(spng__zalloc(&ctx, 0, UINT_MAX) == NULL); + CHECK(allocation_calls == 1 && allocation_size == 0); + CHECK(spng__zalloc(&ctx, UINT_MAX, 0) == NULL); + CHECK(allocation_calls == 2 && allocation_size == 0); + CHECK(spng__zalloc(&ctx, 7, 9) == NULL); + CHECK(allocation_calls == 3 && allocation_size == 63); + allocation_calls = 0; + CHECK(spng__zalloc(&ctx, UINT_MAX, UINT_MAX) == NULL); + if((size_t)UINT_MAX > SIZE_MAX / UINT_MAX) CHECK(allocation_calls == 0); + else CHECK(allocation_calls == 1 && allocation_size == (size_t)UINT_MAX * UINT_MAX); + +#ifdef SPNG_USE_MINIZ + /* miniz passes size_t operands, unlike zlib's uInt operands. */ + allocation_calls = 0; + CHECK(spng__zalloc(&ctx, SIZE_MAX, 2) == NULL); + CHECK(allocation_calls == 0); + CHECK(spng__zalloc(&ctx, SIZE_MAX, 1) == NULL); + CHECK(allocation_calls == 1 && allocation_size == SIZE_MAX); + CHECK(spng__zalloc(&ctx, SIZE_MAX, 0) == NULL); + CHECK(allocation_calls == 2 && allocation_size == 0); +#endif + + ctx.internal_buffer = 1; + ctx.state = SPNG_STATE_OUTPUT; + ctx.bytes_encoded = SIZE_MAX; + allocation_calls = 0; + CHECK(require_bytes(&ctx, 1) == SPNG_EOVERFLOW); + CHECK(allocation_calls == 0 && ctx.state == SPNG_STATE_OUTPUT); + + ctx.out_png_size = SIZE_MAX / 2 + 1; + ctx.bytes_encoded = ctx.out_png_size; + CHECK(require_bytes(&ctx, 1) == SPNG_EOVERFLOW); + CHECK(allocation_calls == 0 && ctx.state == SPNG_STATE_INVALID); + + ctx.state = SPNG_STATE_OUTPUT; + ctx.out_png_size = SIZE_MAX / 2; + ctx.bytes_encoded = ctx.out_png_size; + CHECK(require_bytes(&ctx, 1) == SPNG_EMEM); + CHECK(allocation_calls == 1 && allocation_size == SIZE_MAX - 1); + + ctx.state = SPNG_STATE_OUTPUT; + ctx.out_png_size = 0; + ctx.bytes_encoded = 0; + CHECK(require_bytes(&ctx, SPNG_WRITE_SIZE * 2 + 1) == SPNG_EMEM); + CHECK(allocation_calls == 2 && allocation_size == SPNG_WRITE_SIZE * 4); + + return 0; +} + +static int test_image_sizes(void) +{ + struct spng_ihdr ihdr = {0}; + ihdr.width = 17; + ihdr.height = 23; + ihdr.bit_depth = 8; + ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + size_t len = 0; + + CHECK(calculate_scanline_width(&ihdr, 17, &len) == 0 && len == 69); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_RGBA8, &len) == 0 && len == 1564); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_RGBA16, &len) == 0 && len == 3128); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_RGB8, &len) == 0 && len == 1173); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_PNG, &len) == 0 && len == 1564); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_RAW, &len) == 0 && len == 1564); + + ihdr.color_type = SPNG_COLOR_TYPE_GRAYSCALE; + ihdr.bit_depth = 1; + CHECK(calculate_scanline_width(&ihdr, 17, &len) == 0 && len == 4); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_PNG, &len) == 0 && len == 69); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_G8, &len) == 0 && len == 391); + CHECK(calculate_image_size(&ihdr, SPNG_FMT_GA8, &len) == 0 && len == 782); + ihdr.bit_depth = 16; + CHECK(calculate_image_size(&ihdr, SPNG_FMT_GA16, &len) == 0 && len == 1564); + CHECK(calculate_scanline_width(&ihdr, 0, &len) == SPNG_EINTERNAL); + + /* Test numeric limits only: no image or scanline allocation occurs. */ + ihdr.bit_depth = 8; + len = 42; + if(SIZE_MAX > UINT32_MAX) + { + CHECK(calculate_scanline_width(&ihdr, UINT32_MAX - 1, &len) == 0 && len == UINT32_MAX); + len = 42; + } + CHECK(calculate_scanline_width(&ihdr, UINT32_MAX, &len) == SPNG_EOVERFLOW); + CHECK(len == 42); + + ihdr.width = spng_u32max; + ihdr.height = spng_u32max; + ihdr.bit_depth = 16; + ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + CHECK(calculate_image_size(&ihdr, SPNG_FMT_RGBA16, &len) == SPNG_EOVERFLOW); + CHECK(len == 42); + + return 0; +} + +static int test_inflate_growth(void) +{ + unsigned char input[10000], compressed[11000]; + memset(input, 'A', sizeof(input)); + uLongf compressed_len = sizeof(compressed); + CHECK(compress2(compressed, &compressed_len, input, sizeof(input), Z_DEFAULT_COMPRESSION) == Z_OK); + + spng_ctx *ctx = spng_ctx_new(0); + CHECK(ctx != NULL); + char *out = NULL; + size_t len = 0; + CHECK(spng__inflate_stream(ctx, &out, &len, 1, compressed, compressed_len) == 0); + CHECK(len == sizeof(input) + 1); + CHECK(!memcmp(out, input, sizeof(input))); + free(out); + spng_ctx_free(ctx); + + ctx = spng_ctx_new(0); + CHECK(ctx != NULL); + ctx->chunk_cache_limit = 10000; + out = NULL; + len = 42; + CHECK(spng__inflate_stream(ctx, &out, &len, 1, compressed, compressed_len) == SPNG_ECHUNK_LIMITS); + CHECK(out == NULL && len == 42); + spng_ctx_free(ctx); + + return 0; +} + +static int test_public_overflow(void) +{ + /* A header and one IDAT byte suffice to reach the size checks. */ + unsigned char png[42] = {0}; + memcpy(png, spng_signature, 8); + write_u32(png + 8, 13); + memcpy(png + 12, type_ihdr, 4); + write_u32(png + 16, spng_u32max); + write_u32(png + 20, spng_u32max); + png[24] = 8; + png[25] = SPNG_COLOR_TYPE_GRAYSCALE; + write_u32(png + 29, crc32(crc32(0, NULL, 0), png + 12, 17)); + write_u32(png + 33, 1); + memcpy(png + 37, type_idat, 4); + + struct spng_alloc alloc = + { + .malloc_fn = record_malloc, + .realloc_fn = record_realloc, + .calloc_fn = calloc, + .free_fn = free + }; + spng_ctx *ctx = spng_ctx_new2(&alloc, 0); + CHECK(ctx != NULL); + allocation_calls = 0; + CHECK(spng_set_png_buffer(ctx, png, sizeof(png)) == 0); + size_t len = 42; + CHECK(spng_decoded_image_size(ctx, SPNG_FMT_RGBA16, &len) == SPNG_EOVERFLOW); + CHECK(len == 42 && allocation_calls == 0); + + if(SIZE_MAX > UINT32_MAX) + { + /* The output width fits, but the whole image does not. Progressive + decoding must retain the zero sentinel and proceed past it. */ + unsigned char out = 0; + ctx->image_size = 42; + CHECK(spng_decode_image(ctx, &out, 1, SPNG_FMT_RGBA16, 0) == SPNG_EOVERFLOW); + CHECK(ctx->image_size == 0 && allocation_calls == 0); + ctx->image_size = 42; + CHECK(spng_decode_image(ctx, NULL, 0, SPNG_FMT_RGBA16, SPNG_DECODE_PROGRESSIVE) == SPNG_EZLIB_INIT); + CHECK(ctx->image_size == 0 && allocation_calls == 1); + } + spng_ctx_free(ctx); + + if(SIZE_MAX == UINT32_MAX) + { + /* On 32-bit targets, a one-pixel row fits while its total image + overflows. Exercise both progressive sentinels without allocation. */ + write_u32(png + 16, 1); + png[25] = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + write_u32(png + 29, crc32(crc32(0, NULL, 0), png + 12, 17)); + ctx = spng_ctx_new2(&alloc, 0); + CHECK(ctx != NULL); + allocation_calls = 0; + CHECK(spng_set_png_buffer(ctx, png, sizeof(png)) == 0); + len = 42; + CHECK(spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &len) == SPNG_EOVERFLOW); + CHECK(len == 42 && allocation_calls == 0); + unsigned char out = 0; + ctx->image_size = 42; + CHECK(spng_decode_image(ctx, &out, 1, SPNG_FMT_RGBA8, 0) == SPNG_EOVERFLOW); + CHECK(ctx->image_size == 0 && allocation_calls == 0); + ctx->image_size = 42; + CHECK(spng_decode_image(ctx, NULL, 0, SPNG_FMT_RGBA8, SPNG_DECODE_PROGRESSIVE) == SPNG_EZLIB_INIT); + CHECK(ctx->image_size == 0 && allocation_calls == 1); + spng_ctx_free(ctx); + + ctx = spng_ctx_new2(&alloc, SPNG_CTX_ENCODER); + CHECK(ctx != NULL); + struct spng_ihdr narrow = {0}; + narrow.width = 1; + narrow.height = spng_u32max; + narrow.bit_depth = 8; + narrow.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + CHECK(spng_set_ihdr(ctx, &narrow) == 0); + CHECK(spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1) == 0); + allocation_calls = 0; + ctx->image_size = 42; + CHECK(spng_encode_image(ctx, &out, 1, SPNG_FMT_RAW, 0) == SPNG_EOVERFLOW); + CHECK(ctx->image_size == 0 && allocation_calls == 0); + ctx->image_size = 42; + CHECK(spng_encode_image(ctx, NULL, 0, SPNG_FMT_RAW, SPNG_ENCODE_PROGRESSIVE) == SPNG_EMEM); + CHECK(ctx->image_size == 0 && allocation_calls == 1); + spng_ctx_free(ctx); + } + + ctx = spng_ctx_new2(&alloc, SPNG_CTX_ENCODER); + CHECK(ctx != NULL); + struct spng_ihdr ihdr = {0}; + ihdr.width = spng_u32max; + ihdr.height = 1; + ihdr.bit_depth = 16; + ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + CHECK(spng_set_ihdr(ctx, &ihdr) == 0); + allocation_calls = 0; + unsigned char input = 0; + CHECK(spng_encode_image(ctx, &input, 1, SPNG_FMT_PNG, 0) == SPNG_EOVERFLOW); + CHECK(allocation_calls == 0); + spng_ctx_free(ctx); + + return 0; +} + +static int test_roundtrip(void) +{ + const unsigned char pixels[24] = + { + 0, 1, 2, 255, 11, 22, 33, 128, 254, 253, 252, 0, + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 + }; + unsigned interlace; + for(interlace=0; interlace < 2; interlace++) + { + struct spng_ihdr ihdr = {0}; + ihdr.width = 3; + ihdr.height = 2; + ihdr.bit_depth = 8; + ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + ihdr.interlace_method = interlace; + + spng_ctx *encoder = spng_ctx_new(SPNG_CTX_ENCODER); + CHECK(encoder != NULL); + struct spng_ihdr invalid = ihdr; + invalid.width = 0; + CHECK(spng_set_ihdr(encoder, &invalid) == SPNG_EWIDTH); + invalid = ihdr; + invalid.height = 0; + CHECK(spng_set_ihdr(encoder, &invalid) == SPNG_EHEIGHT); + CHECK(spng_set_ihdr(encoder, &ihdr) == 0); + CHECK(spng_set_option(encoder, SPNG_ENCODE_TO_BUFFER, 1) == 0); + CHECK(spng_encode_image(encoder, NULL, sizeof(pixels), SPNG_FMT_PNG, 0) == 1); + CHECK(spng_encode_image(encoder, pixels, sizeof(pixels) - 1, SPNG_FMT_PNG, 0) == SPNG_EBUFSIZ); + CHECK(spng_encode_image(encoder, pixels, sizeof(pixels), SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE) == 0); + + size_t png_len = 0; + int error = -1; + void *png = spng_get_png_buffer(encoder, &png_len, &error); + CHECK(png != NULL && error == 0); + spng_ctx_free(encoder); + + spng_ctx *decoder = spng_ctx_new(0); + CHECK(decoder != NULL); + CHECK(spng_set_png_buffer(decoder, png, png_len) == 0); + size_t len = 0; + CHECK(spng_decoded_image_size(decoder, SPNG_FMT_RGBA8, &len) == 0 && len == sizeof(pixels)); + unsigned char decoded[sizeof(pixels)]; + CHECK(spng_decode_image(decoder, NULL, len, SPNG_FMT_RGBA8, 0) == 1); + CHECK(spng_decode_image(decoder, decoded, len - 1, SPNG_FMT_RGBA8, 0) == SPNG_EBUFSIZ); + CHECK(spng_decode_image(decoder, decoded, len, SPNG_FMT_RGBA8, 0) == 0); + CHECK(!memcmp(decoded, pixels, sizeof(pixels))); + spng_ctx_free(decoder); + free(png); + } + + return 0; +} + +static int test_roundtrip_matrix(void) +{ + const struct { unsigned color, depth; } formats[] = + { + {SPNG_COLOR_TYPE_GRAYSCALE, 1}, {SPNG_COLOR_TYPE_GRAYSCALE, 2}, + {SPNG_COLOR_TYPE_GRAYSCALE, 4}, {SPNG_COLOR_TYPE_GRAYSCALE, 8}, + {SPNG_COLOR_TYPE_GRAYSCALE, 16}, {SPNG_COLOR_TYPE_TRUECOLOR, 8}, + {SPNG_COLOR_TYPE_TRUECOLOR, 16}, {SPNG_COLOR_TYPE_INDEXED, 1}, + {SPNG_COLOR_TYPE_INDEXED, 2}, {SPNG_COLOR_TYPE_INDEXED, 4}, + {SPNG_COLOR_TYPE_INDEXED, 8}, {SPNG_COLOR_TYPE_GRAYSCALE_ALPHA, 8}, + {SPNG_COLOR_TYPE_GRAYSCALE_ALPHA, 16}, {SPNG_COLOR_TYPE_TRUECOLOR_ALPHA, 8}, + {SPNG_COLOR_TYPE_TRUECOLOR_ALPHA, 16} + }; + const struct { uint32_t width, height; } dimensions[] = + { + {1, 1}, {7, 3}, {8, 4}, {9, 5}, {17, 7}, {257, 33} + }; + unsigned f, d, interlace, progressive, decode_progressive; + unsigned grew_buffer = 0; + uint32_t random = 217; + + for(f=0; f < sizeof(formats) / sizeof(formats[0]); f++) + for(d=0; d < sizeof(dimensions) / sizeof(dimensions[0]); d++) + for(interlace=0; interlace < 2; interlace++) + for(progressive=0; progressive < 2; progressive++) + { + struct spng_ihdr ihdr = {0}; + ihdr.width = dimensions[d].width; + ihdr.height = dimensions[d].height; + ihdr.bit_depth = formats[f].depth; + ihdr.color_type = formats[f].color; + ihdr.interlace_method = interlace; + size_t row_bits = ihdr.width * num_channels(&ihdr) * ihdr.bit_depth; + size_t row_size = (row_bits + 7) / 8; + size_t image_size = row_size * ihdr.height; + unsigned char *pixels = malloc(image_size), *decoded = malloc(image_size); + CHECK(pixels != NULL && decoded != NULL); + size_t i; + for(i=0; i < image_size; i++) + { + random ^= random << 13; + random ^= random >> 17; + random ^= random << 5; + pixels[i] = random; + } + if(row_bits % 8) + { + for(i=0; i < ihdr.height; i++) pixels[(i + 1) * row_size - 1] &= 255 << (8 - row_bits % 8); + } + + spng_ctx *encoder = spng_ctx_new(SPNG_CTX_ENCODER); + CHECK(encoder != NULL); + CHECK(spng_set_ihdr(encoder, &ihdr) == 0); + if(ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) + { + struct spng_plte plte = {0}; + plte.n_entries = 1 << ihdr.bit_depth; + for(i=0; i < plte.n_entries; i++) + { + plte.entries[i].red = i; + plte.entries[i].green = 255 - i; + plte.entries[i].blue = i / 2; + plte.entries[i].alpha = 255; + } + CHECK(spng_set_plte(encoder, &plte) == 0); + } + CHECK(spng_set_option(encoder, SPNG_ENCODE_TO_BUFFER, 1) == 0); + int ret; + if(progressive) + { + CHECK(spng_encode_image(encoder, NULL, 0, SPNG_FMT_RAW, SPNG_ENCODE_PROGRESSIVE | SPNG_ENCODE_FINALIZE) == 0); + do + { + struct spng_row_info info; + CHECK(spng_get_row_info(encoder, &info) == 0); + ret = spng_encode_row(encoder, pixels + info.row_num * row_size, row_size); + }while(!ret); + CHECK(ret == SPNG_EOI); + } + else CHECK(spng_encode_image(encoder, pixels, image_size, SPNG_FMT_RAW, SPNG_ENCODE_FINALIZE) == 0); + size_t png_size; + int error; + void *png = spng_get_png_buffer(encoder, &png_size, &error); + CHECK(png != NULL && error == 0); + if(png_size > SPNG_WRITE_SIZE * 2) grew_buffer = 1; + spng_ctx_free(encoder); + + for(decode_progressive=0; decode_progressive < 2; decode_progressive++) + { + spng_ctx *decoder = spng_ctx_new(0); + CHECK(decoder != NULL); + CHECK(spng_set_png_buffer(decoder, png, png_size) == 0); + size_t decoded_size = 0; + CHECK(spng_decoded_image_size(decoder, SPNG_FMT_RAW, &decoded_size) == 0 && decoded_size == image_size); + memset(decoded, 0, image_size); + if(decode_progressive) + { + CHECK(spng_decode_image(decoder, NULL, 0, SPNG_FMT_RAW, SPNG_DECODE_PROGRESSIVE) == 0); + do + { + struct spng_row_info info; + ret = spng_get_row_info(decoder, &info); + if(ret) break; + ret = spng_decode_row(decoder, decoded + info.row_num * row_size, row_size); + }while(!ret); + CHECK(ret == SPNG_EOI); + } + else CHECK(spng_decode_image(decoder, decoded, image_size, SPNG_FMT_RAW, 0) == 0); + CHECK(!memcmp(decoded, pixels, image_size)); + CHECK(spng_decode_chunks(decoder) == 0); + spng_ctx_free(decoder); + } + free(png); + free(pixels); + free(decoded); + } + + CHECK(grew_buffer); + return 0; +} + +int main(void) +{ + if(test_helpers() || test_allocations() || test_image_sizes() || + test_inflate_growth() || test_public_overflow() || test_roundtrip() || + test_roundtrip_matrix()) return 1; + + return 0; +}