/* Guardado atómico, importación y exportación de archivos reales. */ #include #include #include #include #include #include "tgb_test.h" #include "fixtures_png.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; } static void path_in(char *out, size_t cap, const char *name) { snprintf(out, cap, "%s/%s", g_root, name); } static uint8_t *slurp(const char *path, size_t *len) { FILE *f = fopen(path, "rb"); if (!f) return NULL; fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); uint8_t *b = malloc((size_t)n + 1); size_t got = fread(b, 1, (size_t)n, f); fclose(f); *len = got; return b; } static void spit(const char *path, const void *data, size_t len) { FILE *f = fopen(path, "wb"); fwrite(data, 1, len, f); fclose(f); } static size_t count_tmp_files(void) { DIR *dp = opendir(g_root); size_t n = 0; struct dirent *e; while (dp && (e = readdir(dp))) if (strstr(e->d_name, ".tmp-")) n++; if (dp) closedir(dp); return n; } static size_t undo_count(const tgb_document *d) { tgb_history_stats s; tgb_history_get_stats(d, &s); return s.undo_count; } static int g_saved_events; static char g_saved_path[300]; static void on_event(const tgb_event *ev, void *u) { (void)u; if (ev->type == TGB_EVENT_DOCUMENT_SAVED) { g_saved_events++; snprintf(g_saved_path, sizeof g_saved_path, "%s", ev->text ? ev->text : ""); } } TEST(save_and_load_file) { tgb_document *d = new_doc(16, 16); char p[300]; path_in(p, sizeof p, "proyecto.tgb"); CHECK_OK(paint_px(d, 0, 0, 4, 4, RED)); CHECK(tgb_document_dirty(d)); CHECK_STATUS(tgb_document_save(d, NULL, NULL), TGB_ERR_INVALID_ARG); /* sin ruta */ uint32_t sub; CHECK_OK(tgb_events_subscribe(g_ctx, on_event, NULL, &sub)); g_saved_events = 0; CHECK_OK(tgb_document_save(d, p, NULL)); CHECK(!tgb_document_dirty(d)); CHECK(tgb_document_path(d) && !strcmp(tgb_document_path(d), p)); CHECK_EQ(g_saved_events, 1); CHECK(!strcmp(g_saved_path, p)); CHECK_EQ(count_tmp_files(), 0); tgb_document *r = NULL; CHECK_OK(tgb_document_load(g_ctx, p, &r)); if (r) { CHECK(is_px(tgb_frame_cel(r, 0, 0), 4, 4, RED)); CHECK(!strcmp(tgb_document_path(r), p)); CHECK(!tgb_document_dirty(r)); tgb_document_destroy(r); } /* guardar de nuevo usando la ruta asociada */ CHECK_OK(paint_px(d, 0, 0, 5, 5, BLUE)); CHECK_OK(tgb_document_save(d, NULL, NULL)); CHECK_OK(tgb_document_load(g_ctx, p, &r)); if (r) { CHECK(is_px(tgb_frame_cel(r, 0, 0), 5, 5, BLUE)); tgb_document_destroy(r); } /* no se guarda con una edición a medias */ CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x")); CHECK_STATUS(tgb_document_save(d, p, NULL), TGB_ERR_STATE); CHECK_OK(tgb_edit_cancel(d)); /* la exportación a formato 2 no cambia la ruta ni marca como guardado */ CHECK_OK(paint_px(d, 0, 0, 6, 6, GREEN)); char p2[300]; path_in(p2, sizeof p2, "legado.tgb"); tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true; CHECK_OK(tgb_document_save(d, p2, &o)); CHECK(tgb_document_dirty(d) && !strcmp(tgb_document_path(d), p)); CHECK_OK(tgb_events_unsubscribe(g_ctx, sub)); tgb_document_destroy(d); } TEST(failed_save_never_damages_existing_file) { tgb_document *d = new_doc(64, 64); char p[300]; path_in(p, sizeof p, "valioso.tgb"); CHECK_OK(paint_px(d, 0, 0, 1, 1, RED)); CHECK_OK(tgb_document_save(d, p, NULL)); size_t before_len; uint8_t *before = slurp(p, &before_len); CHECK(before != NULL); /* Simula "disco lleno / no cabe": límite de tamaño de archivo para este proceso */ for (int i = 0; i < 60; i++) { tgb_canvas_set(NULL, 0, 0, RED); CHECK_OK(paint_px(d, 0, 0, i, 10, BLUE)); } signal(SIGXFSZ, SIG_IGN); struct rlimit old, lim; getrlimit(RLIMIT_FSIZE, &old); lim = old; lim.rlim_cur = 100; setrlimit(RLIMIT_FSIZE, &lim); tgb_status s = tgb_document_save(d, p, NULL); setrlimit(RLIMIT_FSIZE, &old); CHECK_STATUS(s, TGB_ERR_IO); CHECK(strlen(tgb_last_error()) > 5); CHECK(tgb_document_dirty(d)); /* no se declara guardado lo que falló */ size_t after_len; uint8_t *after = slurp(p, &after_len); CHECK(after && after_len == before_len && memcmp(before, after, before_len) == 0); /* el archivo anterior sigue idéntico */ CHECK_EQ(count_tmp_files(), 0); /* no quedan temporales */ tgb_document *r = NULL; CHECK_OK(tgb_document_load(g_ctx, p, &r)); /* y sigue siendo un proyecto válido */ if (r) tgb_document_destroy(r); free(before); free(after); /* destino imposible: carpeta inexistente / el destino es una carpeta */ char bad[300]; path_in(bad, sizeof bad, "no_existe/dentro.tgb"); CHECK_STATUS(tgb_document_save(d, bad, NULL), TGB_ERR_IO); char dirp[300]; path_in(dirp, sizeof dirp, "es_carpeta.tgb"); mkdir(dirp, 0700); CHECK_STATUS(tgb_document_save(d, dirp, NULL), TGB_ERR_IO); CHECK(access(dirp, F_OK) == 0); CHECK_EQ(count_tmp_files(), 0); CHECK(tgb_document_dirty(d)); /* abrir lo que no se puede abrir */ CHECK_STATUS(tgb_document_load(g_ctx, "/tmp/tgb-no-existe-xyz.tgb", &r), TGB_ERR_NOT_FOUND); CHECK_STATUS(tgb_document_load(g_ctx, dirp, &r), TGB_ERR_IO); char junk[300]; path_in(junk, sizeof junk, "basura.tgb"); spit(junk, "\x00\x01\x02 no soy json", 15); CHECK_STATUS(tgb_document_load(g_ctx, junk, &r), TGB_ERR_FORMAT); CHECK(strstr(tgb_last_error(), "basura.tgb") != NULL); /* el mensaje dice qué archivo falló */ CHECK(r == NULL); tgb_document_destroy(d); } TEST(import_png_as_frame) { char img[300]; path_in(img, sizeof img, "entrada.png"); spit(img, png_fixtures[0].png, png_fixtures[0].len); tgb_document *d = new_doc(8, 6); size_t n0 = undo_count(d); uint32_t fid = 0; CHECK_OK(tgb_import_png(d, img, 1, 0, &fid)); CHECK_EQ(tgb_frame_count(d), 2); CHECK(tgb_frame_id(d, 1) == fid); CHECK_EQ(undo_count(d), n0 + 1); /* una sola operación deshacible */ CHECK(!strcmp(tgb_history_undo_label(d), "Importar imagen")); bool same = true; /* centrada: desplazamiento (2,1) */ for (int y = 0; y < FIX_H; y++) for (int x = 0; x < FIX_W; x++) { tgb_color p = px_at(d, 1, 0, 2 + x, 1 + y); const uint8_t *e = png_fixtures[0].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; } CHECK(same); CHECK_EQ(px_at(d, 1, 0, 0, 0).a, 0); CHECK_OK(tgb_history_undo(d)); CHECK_EQ(tgb_frame_count(d), 1); CHECK_OK(tgb_history_redo(d)); CHECK_EQ(tgb_frame_count(d), 2); tgb_document_destroy(d); /* imagen mayor que el documento: se recorta centrada */ d = new_doc(2, 2); CHECK_OK(tgb_import_png(d, img, 0, 0, NULL)); const uint8_t *e = png_fixtures[0].px + (0 * FIX_W + 1) * 4; /* el píxel (1,0) de la imagen cae en (0,0) */ tgb_color p = px_at(d, 0, 0, 0, 0); CHECK(p.r == e[0] && p.g == e[1] && p.a == e[3]); tgb_document_destroy(d); } TEST(import_failures_leave_document_untouched) { char img[300]; path_in(img, sizeof img, "entrada.png"); spit(img, png_fixtures[0].png, png_fixtures[0].len); tgb_document *d = new_doc(8, 6); size_t n0 = undo_count(d); CHECK_OK(tgb_layer_set_locked(d, 0, true)); n0 = undo_count(d); CHECK_STATUS(tgb_import_png(d, img, 1, 0, NULL), TGB_ERR_LOCKED); CHECK_OK(tgb_history_undo(d)); /* desbloquea */ n0 = undo_count(d); CHECK_STATUS(tgb_import_png(d, img, 1, 5, NULL), TGB_ERR_RANGE); CHECK_STATUS(tgb_import_png(d, img, 9, 0, NULL), TGB_ERR_RANGE); CHECK_STATUS(tgb_import_png(d, "/tmp/tgb-no-existe.png", 1, 0, NULL), TGB_ERR_NOT_FOUND); char bad[300]; path_in(bad, sizeof bad, "mala.png"); spit(bad, "no soy un png", 13); CHECK_STATUS(tgb_import_png(d, bad, 1, 0, NULL), TGB_ERR_FORMAT); CHECK(strstr(tgb_last_error(), "mala.png") != NULL); char trunc[300]; path_in(trunc, sizeof trunc, "truncada.png"); spit(trunc, png_fixtures[0].png, png_fixtures[0].len - 20); CHECK_STATUS(tgb_import_png(d, trunc, 1, 0, NULL), TGB_ERR_CORRUPT); CHECK_STATUS(tgb_import_png(d, g_root, 1, 0, NULL), TGB_ERR_IO); /* una carpeta */ CHECK_EQ(tgb_frame_count(d), 1); CHECK_EQ(undo_count(d), n0); CHECK(tgb_document_is_consistent(d)); tgb_document_destroy(d); } TEST(export_png_and_sequence) { tgb_document *d = new_doc(20, 10); CHECK_OK(tgb_layer_add(d, 1, "Arriba", NULL)); CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(paint_px(d, 0, 0, 3, 3, RED)); CHECK_OK(paint_px(d, 0, 1, 3, 3, BLUE)); CHECK_OK(paint_px(d, 1, 0, 7, 7, GREEN)); char p[300]; path_in(p, sizeof p, "salida.png"); CHECK_OK(tgb_export_png(d, 0, p)); size_t len; uint8_t *bytes = slurp(p, &len); tgb_canvas *back = NULL, *flat = tgb_canvas_create(20, 10); CHECK_OK(tgb_png_decode_memory(bytes, len, &back)); CHECK_OK(tgb_render_flatten(d, 0, flat)); CHECK(back && tgb_canvas_equal(back, flat)); CHECK(back && is_px(back, 3, 3, BLUE)); /* se exporta lo VISIBLE: la capa de arriba */ CHECK_OK(tgb_layer_set_visible(d, 1, false)); CHECK_OK(tgb_export_png(d, 0, p)); /* sobrescribe de forma atómica */ free(bytes); bytes = slurp(p, &len); tgb_canvas_release(back); back = NULL; CHECK_OK(tgb_png_decode_memory(bytes, len, &back)); CHECK(is_px(back, 3, 3, RED)); tgb_canvas_release(back); tgb_canvas_release(flat); free(bytes); CHECK_STATUS(tgb_export_png(d, 9, p), TGB_ERR_RANGE); CHECK_STATUS(tgb_export_png(NULL, 0, p), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_export_png(d, 0, "/tmp/tgb-carpeta-inexistente/x.png"), TGB_ERR_IO); /* secuencia */ char dir[300]; path_in(dir, sizeof dir, "frames"); mkdir(dir, 0700); CHECK_OK(tgb_export_png_sequence(d, dir, "anim")); char f1[340], f2[340], f3[340]; snprintf(f1, sizeof f1, "%s/anim_0001.png", dir); snprintf(f2, sizeof f2, "%s/anim_0002.png", dir); snprintf(f3, sizeof f3, "%s/anim_0003.png", dir); CHECK(access(f1, F_OK) == 0 && access(f2, F_OK) == 0 && access(f3, F_OK) != 0); CHECK_STATUS(tgb_export_png_sequence(d, dir, "../malo"), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_export_png_sequence(d, dir, ""), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_export_png_sequence(d, "/tmp/tgb-carpeta-inexistente", "a"), TGB_ERR_NOT_FOUND); tgb_document_destroy(d); } static uint32_t be32(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } TEST(export_apng_structure) { tgb_document *d = new_doc(12, 8); CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL)); CHECK_OK(paint_px(d, 0, 0, 1, 1, RED)); CHECK_OK(paint_px(d, 1, 0, 2, 2, GREEN)); CHECK_OK(paint_px(d, 2, 0, 3, 3, BLUE)); CHECK_OK(tgb_frame_set_duration(d, 0, 100)); CHECK_OK(tgb_frame_set_duration(d, 1, 250)); CHECK_OK(tgb_frame_set_duration(d, 2, 40)); char p[300]; path_in(p, sizeof p, "anim.apng"); CHECK_OK(tgb_export_apng(d, p)); size_t len; uint8_t *b = slurp(p, &len); CHECK(b && len > 60 && !memcmp(b, "\x89PNG\r\n\x1a\n", 8)); size_t pos = 8; int fctl = 0, idat = 0, fdat = 0, actl = 0; uint32_t expect_seq = 0; uint32_t delays[3] = {0}; bool crc_ok = true, order_ok = true; uint8_t *payload[3] = {NULL}; size_t plen[3] = {0}; uint8_t ihdr[13]; memcpy(ihdr, b + 16, 13); while (b && pos + 12 <= len) { uint32_t cl = be32(b + pos); const uint8_t *t = b + pos + 4, *cd = b + pos + 8; uLong crc = crc32(crc32(0, t, 4), cd, cl); if ((uint32_t)crc != be32(cd + cl)) crc_ok = false; if (!memcmp(t, "acTL", 4)) { actl++; CHECK(be32(cd) == 3 && be32(cd + 4) == 0); } else if (!memcmp(t, "fcTL", 4)) { if (be32(cd) != expect_seq++) order_ok = false; if (fctl < 3) delays[fctl] = (uint32_t)((cd[20] << 8) | cd[21]); CHECK(((cd[22] << 8) | cd[23]) == 1000); CHECK(be32(cd + 4) == 12 && be32(cd + 8) == 8 && cd[24] == 0 && cd[25] == 0); fctl++; } else if (!memcmp(t, "IDAT", 4)) { if (fctl != 1) order_ok = false; payload[0] = malloc(cl); memcpy(payload[0], cd, cl); plen[0] = cl; idat++; } else if (!memcmp(t, "fdAT", 4)) { if (be32(cd) != expect_seq++) order_ok = false; fdat++; payload[fdat] = malloc(cl - 4); memcpy(payload[fdat], cd + 4, cl - 4); plen[fdat] = cl - 4; } pos += 12 + cl; } CHECK(crc_ok && order_ok); CHECK(actl == 1 && fctl == 3 && idat == 1 && fdat == 2); CHECK(delays[0] == 100 && delays[1] == 250 && delays[2] == 40); /* cada fotograma, reempaquetado como PNG independiente, es exactamente el fotograma del documento */ for (int i = 0; i < 3; i++) { uint8_t *png = malloc(8 + 25 + 12 + plen[i] + 12); size_t o = 0; memcpy(png, "\x89PNG\r\n\x1a\n", 8); o = 8; uint8_t hdr[8] = {0, 0, 0, 13, 'I', 'H', 'D', 'R'}; memcpy(png + o, hdr, 8); memcpy(png + o + 8, ihdr, 13); uLong c = crc32(crc32(0, (const Bytef *)"IHDR", 4), ihdr, 13); png[o + 21] = (uint8_t)(c >> 24); png[o + 22] = (uint8_t)(c >> 16); png[o + 23] = (uint8_t)(c >> 8); png[o + 24] = (uint8_t)c; o += 25; uint8_t dh[8] = {(uint8_t)(plen[i] >> 24), (uint8_t)(plen[i] >> 16), (uint8_t)(plen[i] >> 8), (uint8_t)plen[i], 'I', 'D', 'A', 'T'}; memcpy(png + o, dh, 8); memcpy(png + o + 8, payload[i], plen[i]); c = crc32(crc32(0, (const Bytef *)"IDAT", 4), payload[i], (uInt)plen[i]); o += 8 + plen[i]; png[o] = (uint8_t)(c >> 24); png[o + 1] = (uint8_t)(c >> 16); png[o + 2] = (uint8_t)(c >> 8); png[o + 3] = (uint8_t)c; o += 4; uint8_t end[12] = {0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82}; memcpy(png + o, end, 12); o += 12; tgb_canvas *got = NULL, *want = tgb_canvas_create(12, 8); CHECK_OK(tgb_png_decode_memory(png, o, &got)); CHECK_OK(tgb_render_flatten(d, (size_t)i, want)); CHECK(got && tgb_canvas_equal(got, want)); tgb_canvas_release(got); tgb_canvas_release(want); free(png); free(payload[i]); } free(b); tgb_document_destroy(d); } TEST(write_artifacts_for_external_verification) { const char *dir = getenv("TGB_TEST_ARTIFACTS"); if (!dir) return; /* opcional: lo usa tools/verify-external.py */ tgb_document *d = new_doc(48, 32); CHECK_OK(tgb_layer_add(d, 1, "Tinta", NULL)); CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL)); tgb_brush br = tgb_brush_soft(RED, 9, 0.5f); for (size_t f = 0; f < 3; f++) { CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, f), tgb_layer_id(d, f % 2), "arte")); tgb_draw_dab(tgb_edit_canvas(d), &br, 10.0f + (float)f * 12, 16); tgb_draw_line(tgb_edit_canvas(d), &br, 2, 2, 45, 28); CHECK_OK(tgb_edit_end(d)); br.color.g = (uint8_t)(80 * f); } CHECK_OK(tgb_frame_set_duration(d, 0, 100)); CHECK_OK(tgb_frame_set_duration(d, 1, 250)); CHECK_OK(tgb_frame_set_duration(d, 2, 40)); char p[600]; snprintf(p, sizeof p, "%s/muestra_f0.png", dir); CHECK_OK(tgb_export_png(d, 0, p)); snprintf(p, sizeof p, "%s/muestra.apng", dir); CHECK_OK(tgb_export_apng(d, p)); snprintf(p, sizeof p, "%s/muestra_v3.tgb", dir); CHECK_OK(tgb_document_save(d, p, NULL)); tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true; snprintf(p, sizeof p, "%s/muestra_v2.tgb", dir); CHECK_OK(tgb_document_save(d, p, &o)); tgb_document_destroy(d); } int main(void) { g_ctx = tgb_test_context(g_root, sizeof g_root); RUN(save_and_load_file); RUN(failed_save_never_damages_existing_file); RUN(import_png_as_frame); RUN(import_failures_leave_document_untouched); RUN(export_png_and_sequence); RUN(export_apng_structure); RUN(write_artifacts_for_external_verification); tgb_context_destroy(g_ctx); tgb_test_rmtree(g_root); TEST_SUMMARY(); }