/* Base64, JSON, PNG y proyectos .tgb (formatos 2 y 3). */ #include #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(); }