/* test_image.c - 图片格式嗅探、本地备份、base64、请求前展开 */ #include "image.h" #include "util.h" #include #include #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static const char PNG[] = "\x89PNG\r\n\x1a\n rest"; static const char JPEG[] = "\xff\xd8\xff\xe0 rest"; static const char GIF87[] = "GIF87a rest"; static const char GIF89[] = "GIF89a rest"; static const char WEBP[] = "RIFF\x00\x00\x00\x00WEBPVP8 "; static void test_sniff(void) { puts("按文件内容认格式 (不看扩展名)"); const char *ext = NULL, *mime = NULL; CHECK(image_sniff((const unsigned char *)PNG, 14, &ext, &mime) == 0 && strcmp(ext, "png") == 0 && strcmp(mime, "image/png") == 0, "PNG"); CHECK(image_sniff((const unsigned char *)JPEG, 7, &ext, &mime) == 0 && strcmp(ext, "jpg") == 0 && strcmp(mime, "image/jpeg") == 0, "JPEG"); CHECK(image_sniff((const unsigned char *)GIF87, 10, &ext, &mime) == 0 && strcmp(ext, "gif") == 0, "GIF87a"); CHECK(image_sniff((const unsigned char *)GIF89, 10, &ext, &mime) == 0 && strcmp(ext, "gif") == 0, "GIF89a"); CHECK(image_sniff((const unsigned char *)WEBP, 16, &ext, &mime) == 0 && strcmp(ext, "webp") == 0 && strcmp(mime, "image/webp") == 0, "WebP"); /* 认不出来要明确失败, 别猜一个格式报上去 */ CHECK(image_sniff((const unsigned char *)"not an image", 12, &ext, &mime) != 0, "杂文本不该被认成图片"); CHECK(image_sniff((const unsigned char *)"\x89PNG", 4, &ext, &mime) != 0, "截断的 PNG 头不该被认出来"); CHECK(image_sniff((const unsigned char *)"RIFFxxxxNOPE", 12, &ext, &mime) != 0, "RIFF 但不是 WEBP 不该被认成 WebP"); CHECK(image_sniff(NULL, 0, &ext, &mime) != 0, "NULL 不该崩"); } static void test_backup(void) { puts("本地备份:命名、内容、不重复"); char tmpl[] = "/tmp/ligent_img_XXXXXX"; char *dir = mkdtemp(tmpl); CHECK(dir != NULL, "建临时目录失败"); if (!dir) return; const unsigned char data[] = "\x89PNG\r\n\x1a\n payload"; size_t n = sizeof data - 1; char *b1 = image_backup_save(dir, data, n, "png"); CHECK(b1 != NULL, "备份应成功"); if (!b1) { return; } /* 名字形如 <上传时间戳>_<4位随机>. */ CHECK(strlen(b1) > 5, "名字不该太短:%s", b1); char *dot = strrchr(b1, '.'); CHECK(dot && strcmp(dot, ".png") == 0, "扩展名应从嗅探结果来:%s", b1); CHECK(dot - b1 >= 7, "时间戳 + 下划线 + 4 位随机:%s", b1); CHECK(b1[dot - b1 - 5] == '_', "下划线位置应在随机后缀前:%s", b1); char *p1 = image_backup_path(dir, b1); size_t len = 0; char *back = read_entire_file(p1, &len); CHECK(back && len == n && memcmp(back, data, n) == 0, "备份内容应逐字节一致"); free(back); free(p1); /* 再存一次应该拿到不同的名字 (随机后缀), 而不是互相覆盖 */ char *b2 = image_backup_save(dir, data, n, "png"); CHECK(b2 && strcmp(b1, b2) != 0, "两次备份不该同号:%s / %s", b1, b2 ? b2 : "(null)"); free(b2); free(b1); /* 清场 */ char *cmd = xasprintf("rm -rf %s", dir); if (system(cmd) != 0) { /* 清理失败不影响断言 */ } free(cmd); } static void test_base64(void) { puts("base64:长度、补位、往返"); /* 用几组长度覆盖三种补位情况:3n、3n+1、3n+2 */ struct { const char *in; const char *want; } cases[] = { { "", "" }, { "f", "Zg==" }, { "fo", "Zm8=" }, { "foo", "Zm9v" }, { "foob", "Zm9vYg==" }, { "fooba", "Zm9vYmE=" }, { "foobar", "Zm9vYmFy" }, }; for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { size_t n = strlen(cases[i].in); char *got = base64_encode((const unsigned char *)cases[i].in, n); CHECK(got && strcmp(got, cases[i].want) == 0, "\"%s\" 应编码成 %s, 实际 %s", cases[i].in, cases[i].want, got ? got : "(null)"); CHECK(base64_encoded_len(n) == strlen(cases[i].want), "长度函数应与实际一致 (%zu)", n); free(got); } /* 二进制字节 (0x00 / 0xFF 这类不能被当字符串处理) */ const unsigned char bin[] = { 0x00, 0xFF, 0x10, 0x80 }; char *b = base64_encode(bin, sizeof bin); CHECK(b && strcmp(b, "AP8QgA==") == 0, "二进制应正确编码, 实际 %s", b ? b : "(null)"); free(b); } /* 造一张内容合法 (靠文件头就能嗅探出来)的假图, 返回字节数 */ static size_t fake_png(unsigned char *out, size_t n) { static const char magic[] = "\x89PNG\r\n\x1a\n"; size_t m = sizeof magic - 1; if (n < m) return 0; memcpy(out, magic, m); for (size_t i = m; i < n; i++) out[i] = (unsigned char)(i & 0xFF); return n; } /* 把一段备份引用塞进一条 user 消息, 模拟 read_image 之后的历史 */ static const char *ref_message_type = "ligent_image"; static JsonValue *history_with_ref(const char *backup) { JsonValue *msgs = json_new_array(); JsonValue *sys = json_new_object(); json_object_set_string(sys, "role", "system"); json_object_set_string(sys, "content", "你是助手"); json_array_push(msgs, sys); JsonValue *part = json_new_object(); json_object_set_string(part, "type", ref_message_type); json_object_set_string(part, "backup", backup); JsonValue *content = json_new_array(); json_array_push(content, part); JsonValue *um = json_new_object(); json_object_set_string(um, "role", "user"); json_object_set(um, "content", content); json_array_push(msgs, um); return msgs; } static void test_expand(void) { puts("请求前展开:引用 -> image_url + base64"); char tmpl[] = "/tmp/ligent_imgx_XXXXXX"; char *dir = mkdtemp(tmpl); CHECK(dir != NULL, "建临时目录失败"); if (!dir) return; image_set_bindir(dir); unsigned char png[256]; size_t n = fake_png(png, sizeof png); char *backup = image_backup_save(dir, png, n, "png"); CHECK(backup != NULL, "备份应成功"); if (!backup) { image_set_bindir("."); return; } /* --- 正常展开 --- */ JsonValue *msgs = history_with_ref(backup); int degraded = -1; JsonValue *wire = image_expand_for_request(msgs, °raded); CHECK(wire && json_array_size(wire) == 2, "副本消息条数应与原历史一致"); CHECK(degraded == 0, "正常展开不该有降级"); JsonValue *wu = json_array_get(wire, 1); JsonValue *wc = json_object_get(wu, "content"); CHECK(wc && wc->type == JSON_ARRAY && json_array_size(wc) == 1, "附件消息应有一个 content 块"); JsonValue *wp = json_array_get(wc, 0); CHECK(strcmp(json_get_string(wp, "type"), "image_url") == 0, "发送形态的类型应是 image_url, 实际 %s", json_get_string(wp, "type")); JsonValue *iu = json_object_get(wp, "image_url"); const char *url = iu ? json_get_string(iu, "url") : NULL; CHECK(url && strncmp(url, "data:image/png;base64,", 22) == 0, "应是 data: URL, 实际 %.40s", url ? url : "(null)"); /* base64 段应能解回原字节 —— 这里只验长度与首尾, 避免在测试里再写一个解码器 */ CHECK(url && strlen(url) == 22 + base64_encoded_len(n), "data URL 长度应等于前缀 + base64 长度"); /* 关键不变量:原始历史没被改动, 且里面没有图片字节 */ char *orig = json_dump(msgs); CHECK(strstr(orig, "ligent_image") != NULL, "原历史里应还是引用"); CHECK(strstr(orig, "base64") == NULL, "原历史里不该出现 base64"); CHECK(strstr(orig, backup) != NULL, "原历史里应记着备份名"); free(orig); json_free(wire); /* --- 备份没了:就地降级 --- */ char *bpath = image_backup_path(dir, backup); CHECK(unlink(bpath) == 0, "删掉备份文件"); free(bpath); degraded = 0; wire = image_expand_for_request(msgs, °raded); CHECK(degraded == 1, "读不到备份应降级 1 处"); JsonValue *wc2 = json_object_get(json_array_get(wire, 1), "content"); JsonValue *wp2 = json_array_get(wc2, 0); CHECK(strcmp(json_get_string(wp2, "type"), "text") == 0, "发送形态应降级成 text 块"); const char *dt = json_get_string(wp2, "text"); CHECK(dt && strstr(dt, "无法交给模型") != NULL, "应说明原因, 实际 %s", dt ? dt : "(null)"); json_free(wire); /* 降级是**就地**的:原历史里的坏引用应该已经被换掉, 否则每轮都会重试 */ char *after = json_dump(msgs); CHECK(strstr(after, "ligent_image") == NULL, "原历史里的坏引用应已被清掉"); CHECK(strstr(after, "ligent_note") != NULL, "应换成内部说明块"); free(after); /* 再来一次不该重复降级 (历史已经干净了) */ degraded = 0; wire = image_expand_for_request(msgs, °raded); CHECK(degraded == 0, "第二次不该再降级"); json_free(wire); /* image_has_refs:降级之后应为 0, 这正是 4xx 兜底的判据 */ CHECK(image_has_refs(msgs) == 0, "降级后历史里不该还有图片引用"); json_free(msgs); /* --- 4xx 兜底:image_degrade_all --- */ JsonValue *m2 = history_with_ref(backup); CHECK(image_has_refs(m2) == 1, "有引用时应报 1"); int dn = image_degrade_all(m2); CHECK(dn == 1, "应降级 1 处, 实际 %d", dn); CHECK(image_has_refs(m2) == 0, "降级后不该还有引用"); JsonValue *c2 = json_object_get(json_array_get(m2, 1), "content"); const char *t2 = json_get_string(json_array_get(c2, 0), "text"); /* 给模型的话必须是能直接照做的中文, 而不是服务端的英文报错 */ CHECK(t2 && strstr(t2, "不符合 API 所需的格式标准") != NULL, "应说明格式不符, 实际 %s", t2 ? t2 : "(null)"); CHECK(t2 && strstr(t2, "请规范化图片") != NULL, "应给出下一步动作"); CHECK(t2 && strchr(t2, '\n') == NULL, "说明不该带换行"); json_free(m2); /* 没有引用时不该动历史 */ JsonValue *plain = json_new_array(); JsonValue *pm = json_new_object(); json_object_set_string(pm, "role", "user"); json_object_set_string(pm, "content", "你好"); json_array_push(plain, pm); CHECK(image_has_refs(plain) == 0, "纯文本历史不该报有引用"); CHECK(image_degrade_all(plain) == 0, "纯文本历史不该被改"); JsonValue *pw = image_expand_for_request(plain, NULL); CHECK(pw && json_array_size(pw) == 1, "纯文本历史应能原样复制"); json_free(pw); json_free(plain); free(backup); image_set_bindir("."); char *cmd = xasprintf("rm -rf %s", tmpl); if (system(cmd) != 0) { } free(cmd); } static void test_pending(void) { puts("待发附件:取走即清空"); image_pending_take(); /* 先清干净 */ image_pending_add("1700000000_aaaa.png"); image_pending_add("1700000001_bbbb.png"); JsonValue *arr = image_pending_take(); CHECK(arr && json_array_size(arr) == 2, "应取到 2 个块"); if (arr && json_array_size(arr) == 2) { JsonValue *p0 = json_array_get(arr, 0); CHECK(strcmp(json_get_string(p0, "type"), "ligent_image") == 0, "类型应是内部引用块, 不是协议块"); CHECK(strcmp(json_get_string(p0, "backup"), "1700000000_aaaa.png") == 0, "第一个备份名"); CHECK(json_get_string(p0, "file_id") == NULL, "不该再有 file_id 这种协议字段"); CHECK(strcmp(json_get_string(json_array_get(arr, 1), "backup"), "1700000001_bbbb.png") == 0, "第二个备份名"); } json_free(arr); CHECK(image_pending_take() == NULL, "取走之后应该空了"); image_pending_add(NULL); image_pending_add(""); CHECK(image_pending_take() == NULL, "空备份名不该被记下"); } int main(void) { test_sniff(); test_backup(); test_base64(); test_expand(); test_pending(); printf("\n%d 项检查, %d 项失败\n", checks, failures); return failures ? 1 : 0; }