451 lines
25 KiB
C
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();
|
|
}
|