Files
Tahgibu/tests/test_io_formats.c
T
2026-10-24 11:52:16 -03:00

451 lines
25 KiB
C

/* Base64, JSON, PNG y proyectos .tgb (formatos 2 y 3). */
#include <dirent.h>
#include "tgb_test.h"
#include "fixtures_png.h"
#include "../src/io/io_internal.h"
static tgb_context *g_ctx;
static char g_root[200];
static tgb_document *new_doc(int w, int h) {
tgb_document *d = NULL;
CHECK_OK(tgb_document_create(g_ctx, w, h, 12, &d));
return d;
}
/* ---------------- Base64 ---------------- */
TEST(base64_roundtrip_and_errors) {
srand(7);
for (size_t n = 0; n <= 40; n++) {
uint8_t in[40]; for (size_t i = 0; i < n; i++) in[i] = (uint8_t)rand();
char enc[100]; tgb_b64_encode(in, n, enc);
CHECK_EQ(strlen(enc), tgb_b64_encoded_len(n));
uint8_t *dec = NULL; size_t dl = 0;
if (n == 0) { CHECK_STATUS(tgb_b64_decode(enc, 0, 100, &dec, &dl), TGB_ERR_CORRUPT); continue; }
CHECK_OK(tgb_b64_decode(enc, strlen(enc), 100, &dec, &dl));
CHECK(dl == n && memcmp(dec, in, n) == 0);
free(dec);
}
uint8_t *d; size_t l;
CHECK_STATUS(tgb_b64_decode("QUJD", 4, 100, &d, &l), TGB_OK); free(d);
CHECK_STATUS(tgb_b64_decode("QUJ", 3, 100, &d, &l), TGB_ERR_CORRUPT); /* longitud */
CHECK_STATUS(tgb_b64_decode("QU!D", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* carácter */
CHECK_STATUS(tgb_b64_decode("QU D", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* espacio */
CHECK_STATUS(tgb_b64_decode("=UJD", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* relleno al inicio */
CHECK_STATUS(tgb_b64_decode("QU=D", 4, 100, &d, &l), TGB_ERR_CORRUPT);
CHECK_STATUS(tgb_b64_decode("QUJD====", 8, 100, &d, &l), TGB_ERR_CORRUPT); /* relleno en medio */
CHECK_STATUS(tgb_b64_decode("QUJDREVG", 8, 3, &d, &l), TGB_ERR_LIMIT); /* más que el máximo */
const char *url = "data:image/png;base64,QUJD";
CHECK_OK(tgb_b64_decode(url, strlen(url), 100, &d, &l)); CHECK(l == 3 && !memcmp(d, "ABC", 3)); free(d);
}
/* ---------------- JSON ---------------- */
static tgb_status parse_str(const char *s, jnode **r) { return json_parse(s, strlen(s), r); }
TEST(json_valid_documents) {
jnode *r = NULL;
CHECK_OK(parse_str(" {\"a\": [1, -2.5e1, true, false, null, \"x\\n\\u00e9\\ud83d\\ude00\"], \"b\": {}} ", &r));
CHECK(r->t == J_OBJ && r->n == 2);
const jnode *a = json_get(r, "a");
CHECK(a && a->t == J_ARR && a->n == 6);
CHECK(a->items[0].num == 1 && a->items[1].num == -25.0 && a->items[2].b && !a->items[3].b && a->items[4].t == J_NULL);
CHECK(a->items[5].slen == 1 + 1 + 2 + 4 && !memcmp(a->items[5].s, "x\n\xc3\xa9\xf0\x9f\x98\x80", 8)); /* escapes y sustitutos UTF-16 */
CHECK(json_get(r, "nada") == NULL);
json_free(r);
long long v;
CHECK_OK(parse_str("12345678901", &r)); CHECK(json_is_int(r, 0, 1LL << 40, &v) && v == 12345678901LL); json_free(r);
CHECK_OK(parse_str("1.5", &r)); CHECK(!json_is_int(r, 0, 100, &v)); json_free(r);
CHECK_OK(parse_str("1e2", &r)); CHECK(json_is_int(r, 0, 1000, &v) && v == 100); json_free(r);
}
TEST(json_invalid_documents) {
const char *bad[] = {"", " ", "{", "[1,]", "[1 2]", "{\"a\":1,}", "{a:1}", "{\"a\" 1}", "[01]", "[1.]", "[.5]", "[+1]", "[1e]", "nul", "tru", "\"abc",
"\"a\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "[1] 2", "{\"a\":1} x", "\"tab\there\"", "[\"\xff\"]", "'a'", "[1,,2]", "{\"a\":}",
"[NaN]", "[Infinity]", "{\"\\u0000\":1}"};
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
jnode *r = NULL;
tgb_status s = parse_str(bad[i], &r);
if (s == TGB_OK) { printf(" se aceptó JSON inválido: %s\n", bad[i]); json_free(r); }
CHECK_STATUS(s, TGB_ERR_FORMAT);
CHECK(r == NULL);
}
char deep[400]; memset(deep, '[', 100); deep[100] = 0;
jnode *r = NULL;
CHECK_STATUS(parse_str(deep, &r), TGB_ERR_FORMAT); /* profundidad excesiva: sin desbordar la pila */
}
/* ---------------- PNG ---------------- */
static tgb_canvas *random_canvas(int w, int h, unsigned seed) {
srand(seed);
tgb_canvas *c = tgb_canvas_create(w, h);
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) {
tgb_color p = {(uint8_t)rand(), (uint8_t)rand(), (uint8_t)rand(), (uint8_t)(rand() % 4 == 0 ? 0 : rand())};
tgb_canvas_set(c, x, y, p);
}
return c;
}
TEST(png_roundtrip) {
int sizes[][2] = {{1, 1}, {7, 5}, {64, 33}, {200, 120}};
for (size_t i = 0; i < 4; i++) {
tgb_canvas *c = random_canvas(sizes[i][0], sizes[i][1], (unsigned)i + 1);
uint8_t *png; size_t pl; tgb_canvas *back = NULL;
CHECK_OK(tgb_png_encode_memory(c, &png, &pl));
CHECK_OK(tgb_png_decode_memory(png, pl, &back));
CHECK(back && tgb_canvas_equal(c, back));
tgb_canvas_release(back); free(png); tgb_canvas_release(c);
}
tgb_canvas *empty = tgb_canvas_create(30, 30);
uint8_t *png; size_t pl; tgb_canvas *back;
CHECK_OK(tgb_png_encode_memory(empty, &png, &pl));
CHECK_OK(tgb_png_decode_memory(png, pl, &back));
CHECK(tgb_canvas_is_empty(back));
CHECK(pl < 200); /* se comprime muy bien */
tgb_canvas_release(back); free(png); tgb_canvas_release(empty);
}
TEST(png_reads_third_party_files) {
for (size_t i = 0; i < sizeof png_fixtures / sizeof png_fixtures[0]; i++) {
const png_fixture *f = &png_fixtures[i];
tgb_canvas *c = NULL;
tgb_status s = tgb_png_decode_memory(f->png, f->len, &c);
if (s != TGB_OK) { printf(" fixture %s: %s\n", f->name, tgb_last_error()); }
CHECK_OK(s);
if (!c) continue;
CHECK(tgb_canvas_width(c) == FIX_W && tgb_canvas_height(c) == FIX_H);
bool same = true;
for (int y = 0; y < FIX_H; y++) for (int x = 0; x < FIX_W; x++) {
tgb_color p = tgb_canvas_get(c, x, y);
const uint8_t *e = f->px + (y * FIX_W + x) * 4;
if (p.r != e[0] || p.g != e[1] || p.b != e[2] || p.a != e[3]) same = false;
}
if (!same) printf(" fixture %s: píxeles distintos de lo esperado\n", f->name);
CHECK(same);
tgb_canvas_release(c);
}
}
static void fix_crc(uint8_t *png, size_t chunk_off) { /* recalcula el CRC de un chunk (para fabricar casos de prueba) */
uint32_t len = ((uint32_t)png[chunk_off] << 24) | ((uint32_t)png[chunk_off + 1] << 16) | ((uint32_t)png[chunk_off + 2] << 8) | png[chunk_off + 3];
uint32_t crc = 0xFFFFFFFFu;
for (size_t i = 0; i < len + 4; i++) {
crc ^= png[chunk_off + 4 + i];
for (int k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
crc = ~crc;
size_t o = chunk_off + 8 + len;
png[o] = (uint8_t)(crc >> 24); png[o + 1] = (uint8_t)(crc >> 16); png[o + 2] = (uint8_t)(crc >> 8); png[o + 3] = (uint8_t)crc;
}
TEST(png_rejects_bad_input) {
const png_fixture *f = &png_fixtures[0];
uint8_t *buf = malloc(f->len + 16);
tgb_canvas *c = NULL;
CHECK_STATUS(tgb_png_decode_memory((const uint8_t *)"hola", 4, &c), TGB_ERR_FORMAT);
CHECK_STATUS(tgb_png_decode_memory(NULL, 0, &c), TGB_ERR_INVALID_ARG);
/* truncado en cualquier punto: siempre error, nunca bloqueo ni lectura fuera de rango */
for (size_t n = 0; n < f->len; n++) {
tgb_status s = tgb_png_decode_memory(f->png, n, &c);
if (s == TGB_OK) { tgb_canvas_release(c); printf(" PNG truncado a %zu bytes se aceptó\n", n); }
CHECK(TGB_FAILED(s));
}
/* un byte alterado en los datos: el CRC lo detecta */
memcpy(buf, f->png, f->len);
buf[f->len / 2] ^= 0x55;
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_CORRUPT);
/* entrelazado (Adam7): reconocido y rechazado con mensaje claro */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 12] = 1; fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_UNSUPPORTED);
CHECK(strstr(tgb_last_error(), "entrelazado") != NULL);
/* dimensiones enormes declaradas en IHDR: se rechaza antes de reservar memoria */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 0] = 0x00; buf[8 + 8 + 1] = 0x01; buf[8 + 8 + 2] = 0x86; buf[8 + 8 + 3] = 0xA0; /* ancho = 100000 */
fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_LIMIT);
/* combinación de color/profundidad imposible */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 8] = 4; buf[8 + 8 + 9] = 6; fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_CORRUPT);
free(buf);
}
/* ---------------- .tgb ---------------- */
static char *b64_of_png(const png_fixture *f) {
char *s = malloc(tgb_b64_encoded_len(f->len) + 1);
tgb_b64_encode(f->png, f->len, s);
return s;
}
static char *legacy_json(void) {
char *a = b64_of_png(&png_fixtures[0]), *b = b64_of_png(&png_fixtures[1]);
char *j = malloc(strlen(a) + strlen(b) + 400);
sprintf(j, "{\"format\":\"TAHGIBU\",\"version\":2,\"app\":\"Tahgibu Animator\",\"fps\":12,\"width\":4,\"height\":3,"
"\"community_owned\":true,\"frames\":[\"%s\",\"%s\"],\"sprites\":[{\"name\":\"s1\",\"x\":1}]}", a, b);
free(a); free(b);
return j;
}
static tgb_status load_str(const char *json, tgb_document **d) {
return tgb_document_load_memory(g_ctx, (const uint8_t *)json, strlen(json), d);
}
TEST(tgb_legacy_v2_load) {
char *j = legacy_json();
tgb_document *d = NULL;
CHECK_OK(load_str(j, &d));
CHECK(d != NULL);
if (d) {
CHECK(tgb_document_width(d) == 4 && tgb_document_height(d) == 3 && tgb_document_fps(d) == 12);
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK_EQ(tgb_frame_duration_ms(d, 0), 83);
const uint8_t *e = png_fixtures[0].px;
tgb_color p = px_at(d, 0, 0, 2, 1);
CHECK(p.r == e[(1 * 4 + 2) * 4] && p.g == e[(1 * 4 + 2) * 4 + 1] && p.a == e[(1 * 4 + 2) * 4 + 3]);
CHECK(!tgb_document_dirty(d));
/* campos desconocidos conservados */
CHECK(tgb_document_extra_field(d, "community_owned") && !strcmp(tgb_document_extra_field(d, "community_owned"), "true"));
CHECK(tgb_document_extra_field(d, "sprites") && !strcmp(tgb_document_extra_field(d, "sprites"), "[{\"name\":\"s1\",\"x\":1}]"));
CHECK(tgb_document_extra_field(d, "format") == NULL);
/* guardar (formato 3) y volver a cargar conserva todo */
uint8_t *out; size_t ol;
CHECK_OK(tgb_document_save_memory(d, NULL, &out, &ol));
CHECK(strstr((char *)out, "\"version\":3") != NULL && strstr((char *)out, "\"community_owned\":true") != NULL);
tgb_document *d2 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &d2));
if (d2) {
CHECK(tgb_canvas_equal(tgb_frame_cel(d, 0, 0), tgb_frame_cel(d2, 0, 0)) && tgb_canvas_equal(tgb_frame_cel(d, 1, 0), tgb_frame_cel(d2, 1, 0)));
CHECK(!strcmp(tgb_document_extra_field(d2, "sprites"), tgb_document_extra_field(d, "sprites")));
tgb_document_destroy(d2);
}
free(out);
/* exportación explícita al formato 2: "frames" aplanados, sin 'tgb3', mismos campos extra */
tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true;
CHECK_OK(tgb_document_save_memory(d, &o, &out, &ol));
CHECK(strstr((char *)out, "\"version\":2") && !strstr((char *)out, "tgb3") && strstr((char *)out, "\"sprites\":[{"));
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &d2));
if (d2) { CHECK_EQ(tgb_frame_count(d2), 2); CHECK(tgb_canvas_equal(tgb_frame_cel(d, 0, 0), tgb_frame_cel(d2, 0, 0))); tgb_document_destroy(d2); }
free(out);
tgb_document_destroy(d);
}
free(j);
}
TEST(tgb_v3_roundtrip_full) {
tgb_document *d = new_doc(32, 24);
CHECK_OK(tgb_layer_add(d, 1, "Tinta \"fina\" ñ", NULL));
CHECK_OK(tgb_layer_set_opacity(d, 1, 100));
CHECK_OK(tgb_layer_set_locked(d, 0, true));
CHECK_OK(tgb_layer_set_locked(d, 0, false));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(paint_px(d, 0, 0, 3, 3, RED));
CHECK_OK(paint_px(d, 1, 1, 8, 8, BLUE));
CHECK_OK(tgb_frame_duplicate(d, 0, NULL)); /* comparte la celda con el fotograma 0 */
CHECK_OK(tgb_frame_set_duration(d, 1, 250));
CHECK_OK(tgb_layer_set_visible(d, 1, false));
uint32_t pa = 0, pb = 0;
CHECK_OK(tgb_recovery_point_create(d, "Antes del azul — ñandú", &pa));
CHECK_OK(paint_px(d, 2, 0, 20, 10, GREEN));
CHECK_OK(tgb_recovery_point_create(d, "Final", &pb));
uint8_t *data; size_t len;
CHECK_OK(tgb_document_save_memory(d, NULL, &data, &len));
tgb_document *r = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, data, len, &r));
CHECK(r != NULL);
if (r) {
CHECK(tgb_document_width(r) == 32 && tgb_document_height(r) == 24 && tgb_document_fps(r) == 12);
CHECK_EQ(tgb_frame_count(r), tgb_frame_count(d));
CHECK_EQ(tgb_layer_count(r), 2);
CHECK(!strcmp(tgb_layer_name(r, 1), "Tinta \"fina\" ñ"));
CHECK(!tgb_layer_visible(r, 1) && tgb_layer_visible(r, 0) && tgb_layer_opacity(r, 1) == 100 && !tgb_layer_locked(r, 0));
for (size_t f = 0; f < tgb_frame_count(d); f++) {
CHECK_EQ(tgb_frame_id(r, f), tgb_frame_id(d, f));
CHECK_EQ(tgb_frame_duration_ms(r, f), tgb_frame_duration_ms(d, f));
for (size_t l = 0; l < 2; l++) {
const tgb_canvas *a = tgb_frame_cel(d, f, l), *b = tgb_frame_cel(r, f, l);
CHECK((a == NULL || tgb_canvas_is_empty(a)) == (b == NULL || tgb_canvas_is_empty(b)));
if (a && b && !tgb_canvas_is_empty(a)) CHECK(tgb_canvas_equal(a, b));
}
}
/* la celda compartida (fotogramas 0 y 1 duplicados... ver ids) sigue compartida tras cargar */
CHECK(tgb_frame_cel(r, 0, 0) == tgb_frame_cel(r, 1, 0) || tgb_frame_cel(r, 0, 0) == tgb_frame_cel(r, 2, 0));
CHECK_EQ(tgb_layer_id(r, 0), tgb_layer_id(d, 0));
/* puntos de recuperación persistentes */
CHECK_EQ(tgb_recovery_point_count(r), 2);
tgb_recovery_point_info a, b;
CHECK_OK(tgb_recovery_point_info_at(r, 0, &a)); CHECK_OK(tgb_recovery_point_info_at(r, 1, &b));
CHECK(a.id == pa && !strcmp(a.name, "Antes del azul — ñandú") && a.compatible && a.created_unix > 1700000000);
CHECK(b.id == pb && !strcmp(b.name, "Final"));
CHECK_OK(tgb_recovery_point_restore(r, pa)); /* se puede volver a un estado anterior tras reabrir */
CHECK_EQ(tgb_frame_count(r), 3);
CHECK_EQ(px_at(r, 2, 0, 20, 10).a, 0); /* el verde aún no existía en ese punto */
CHECK(is_px(tgb_frame_cel(r, 0, 0), 3, 3, RED));
/* los ids nuevos no chocan con los existentes */
tgb_document *r2 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, data, len, &r2));
uint32_t nid = 0;
CHECK_OK(tgb_frame_add(r2, 0, &nid));
for (size_t f = 1; f < tgb_frame_count(r2); f++) CHECK(tgb_frame_id(r2, f) != nid);
CHECK(nid > tgb_frame_id(d, 2) && nid > tgb_layer_id(d, 1));
tgb_document_destroy(r2);
tgb_document_destroy(r);
}
/* de-duplicación: las celdas compartidas se guardan una sola vez */
jnode *root = NULL;
CHECK_OK(json_parse((char *)data, len, &root));
const jnode *images = json_get(json_get(root, "tgb3"), "images");
CHECK(images && images->t == J_ARR);
CHECK(images->n <= 5); /* 4 celdas distintas como mucho (rojo compartido, azul, verde, + antes) */
json_free(root);
free(data);
/* sin fotogramas aplanados: archivo más pequeño y sin 'frames' */
tgb_save_options o; tgb_save_options_defaults(&o); o.include_flat_frames = false;
uint8_t *small; size_t sl;
CHECK_OK(tgb_document_save_memory(d, &o, &small, &sl));
CHECK(strstr((char *)small, "\"frames\":[\"") == NULL);
tgb_document *r3 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, small, sl, &r3));
tgb_document_destroy(r3);
free(small);
tgb_document_destroy(d);
}
static void expect_load_fail(const char *what, const char *json, tgb_status expected) {
tgb_document *d = (tgb_document *)0x1;
tgb_status s = load_str(json, &d);
if (s != expected) printf(" [%s] se esperaba %s y devolvió %s (%s)\n", what, tgb_status_name(expected), tgb_status_name(s), tgb_last_error());
CHECK_EQ(s, expected);
CHECK(d == NULL);
CHECK(strlen(tgb_last_error()) > 5); /* mensaje comprensible */
}
TEST(tgb_rejects_corrupt_files) {
char *a = b64_of_png(&png_fixtures[0]);
char buf[8000];
expect_load_fail("vacío", "", TGB_ERR_FORMAT);
expect_load_fail("no json", "esto no es json", TGB_ERR_FORMAT);
expect_load_fail("raíz lista", "[1,2,3]", TGB_ERR_FORMAT);
expect_load_fail("sin format", "{\"version\":2}", TGB_ERR_FORMAT);
expect_load_fail("otro format", "{\"format\":\"OTRO\",\"version\":2}", TGB_ERR_FORMAT);
expect_load_fail("sin version", "{\"format\":\"TAHGIBU\"}", TGB_ERR_FORMAT);
expect_load_fail("version futura", "{\"format\":\"TAHGIBU\",\"version\":99,\"fps\":12,\"width\":4,\"height\":3}", TGB_ERR_INCOMPATIBLE);
expect_load_fail("version 1", "{\"format\":\"TAHGIBU\",\"version\":1,\"fps\":12,\"width\":4,\"height\":3,\"frames\":[]}", TGB_ERR_UNSUPPORTED);
expect_load_fail("version decimal", "{\"format\":\"TAHGIBU\",\"version\":2.5}", TGB_ERR_FORMAT);
const char *head = "{\"format\":\"TAHGIBU\",\"version\":2,";
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":0,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("ancho 0", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":5000,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("ancho enorme", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":0,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps 0", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":100,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps 100", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":\"12\",\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps texto", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3}", head); expect_load_fail("sin frames", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[]}", head); expect_load_fail("frames vacío", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":\"x\"}", head); expect_load_fail("frames no lista", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[7]}", head); expect_load_fail("frame no cadena", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"@@@@\"]}", head); expect_load_fail("base64 malo", buf, TGB_ERR_CORRUPT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"QUJDRA==\"]}", head); expect_load_fail("no es PNG", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":9,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("dimensiones distintas", buf, TGB_ERR_FORMAT);
/* PNG con un byte dañado */
uint8_t *png = malloc(png_fixtures[0].len);
memcpy(png, png_fixtures[0].png, png_fixtures[0].len); png[png_fixtures[0].len / 2] ^= 1;
char *bad = malloc(tgb_b64_encoded_len(png_fixtures[0].len) + 1); tgb_b64_encode(png, png_fixtures[0].len, bad);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, bad); expect_load_fail("PNG dañado", buf, TGB_ERR_CORRUPT);
free(png); free(bad);
/* demasiada memoria: se rechaza ANTES de decodificar nada */
char *many = malloc(40000); strcpy(many, "[");
for (int i = 0; i < 600; i++) strcat(many, i ? ",\"AAAA\"" : "\"AAAA\"");
strcat(many, "]");
char *big = malloc(strlen(many) + 200);
sprintf(big, "%s\"fps\":12,\"width\":4096,\"height\":4096,\"frames\":%s}", head, many);
expect_load_fail("bomba de memoria", big, TGB_ERR_LIMIT);
free(many); free(big);
/* formato 3 */
const char *h3 = "{\"format\":\"TAHGIBU\",\"version\":3,\"fps\":12,\"width\":4,\"height\":3,";
snprintf(buf, sizeof buf, "%s\"frames\":[]}", h3); expect_load_fail("v3 sin tgb3", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],\"layers\":[],\"frames\":[]}}", h3); expect_load_fail("v3 sin capas", buf, TGB_ERR_FORMAT);
const char *lay = "\"layers\":[{\"id\":1,\"name\":\"L\"}]";
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[]}]}}", h3, lay); expect_load_fail("cels de menos", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[3]}]}}", h3, lay); expect_load_fail("imagen inexistente", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":5,\"cels\":[null]}]}}", h3, lay); expect_load_fail("duración mala", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]},{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3, lay); expect_load_fail("ids repetidos", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],\"layers\":[{\"id\":1,\"name\":\"\"}],\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3); expect_load_fail("capa sin nombre", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[\"%s\"],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3, "QUJDRA==", lay); expect_load_fail("imagen no PNG", buf, TGB_ERR_FORMAT);
free(a);
}
TEST(tgb_incompatible_recovery_point_is_preserved) {
const char *json = "{\"format\":\"TAHGIBU\",\"version\":3,\"fps\":12,\"width\":4,\"height\":3,\"tgb3\":{\"images\":[],"
"\"layers\":[{\"id\":1,\"name\":\"L\"}],\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}],\"next_id\":3,\"recovery_points\":["
"{\"id\":5,\"name\":\"Futuro\",\"created\":1700000000,\"state_version\":7,\"state\":{\"nuevo\":[1,2,3]}},"
"{\"id\":6,\"name\":\"Roto\",\"state_version\":1,\"state\":{\"layers\":[],\"frames\":[]}}]}}";
tgb_document *d = NULL;
CHECK_OK(load_str(json, &d));
CHECK(d != NULL);
if (!d) return;
CHECK_EQ(tgb_recovery_point_count(d), 2);
tgb_recovery_point_info a, b;
CHECK_OK(tgb_recovery_point_info_at(d, 0, &a)); CHECK_OK(tgb_recovery_point_info_at(d, 1, &b));
CHECK(a.id == 5 && !a.compatible && !strcmp(a.name, "Futuro"));
CHECK(b.id == 6 && !b.compatible);
CHECK_STATUS(tgb_recovery_point_restore(d, 5), TGB_ERR_INCOMPATIBLE); /* no se restaura... */
CHECK_EQ(tgb_frame_count(d), 1);
CHECK(!tgb_history_can_undo(d)); /* ...y el documento no se toca */
CHECK_OK(tgb_recovery_point_create(d, "Nuevo", NULL)); /* los ids no chocan con los existentes */
uint8_t *out; size_t ol;
CHECK_OK(tgb_document_save_memory(d, NULL, &out, &ol)); /* ...y se conservan al guardar */
CHECK(strstr((char *)out, "\"state_version\":7,\"state\":{\"nuevo\":[1,2,3]}") != NULL);
tgb_document *r = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &r));
if (r) { CHECK_EQ(tgb_recovery_point_count(r), 3); tgb_document_destroy(r); }
free(out);
tgb_document_destroy(d);
}
TEST(tgb_fuzz_truncation_and_bitflips) {
tgb_document *d = new_doc(24, 16);
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_layer_add(d, 1, "B", NULL));
CHECK_OK(paint_px(d, 0, 0, 2, 2, RED));
CHECK_OK(paint_px(d, 1, 1, 5, 5, BLUE));
CHECK_OK(tgb_recovery_point_create(d, "P", NULL));
uint8_t *data; size_t len;
CHECK_OK(tgb_document_save_memory(d, NULL, &data, &len));
int ok_loads = 0, failed = 0;
for (size_t n = 0; n < len; n += 53) { /* truncar en muchos puntos */
tgb_document *r = NULL;
tgb_status s = tgb_document_load_memory(g_ctx, data, n, &r);
if (s == TGB_OK) { ok_loads++; tgb_document_destroy(r); } else { failed++; CHECK(r == NULL); }
}
CHECK(failed > 10);
srand(99);
uint8_t *copy = malloc(len);
for (int i = 0; i < 400; i++) { /* byte alterado al azar */
memcpy(copy, data, len);
copy[(size_t)rand() % len] = (uint8_t)rand();
tgb_document *r = NULL;
tgb_status s = tgb_document_load_memory(g_ctx, copy, len, &r);
if (s == TGB_OK) { ok_loads++; CHECK(r != NULL); tgb_document_destroy(r); } else { failed++; CHECK(r == NULL); }
}
printf(" (fuzz: %d cargas aceptadas, %d rechazadas, sin fallos de memoria)\n", ok_loads, failed);
free(copy); free(data);
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(base64_roundtrip_and_errors);
RUN(json_valid_documents);
RUN(json_invalid_documents);
RUN(png_roundtrip);
RUN(png_reads_third_party_files);
RUN(png_rejects_bad_input);
RUN(tgb_legacy_v2_load);
RUN(tgb_v3_roundtrip_full);
RUN(tgb_rejects_corrupt_files);
RUN(tgb_incompatible_recovery_point_is_preserved);
RUN(tgb_fuzz_truncation_and_bitflips);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}