This commit is contained in:
pluv committed 2026-10-03 14:13:21 +08:00
commit e54390a9f8
85 files changed
+29923

No files matched your search

+318
View File
@@ -0,0 +1,318 @@
/* test_image.c - 图片格式嗅探、本地备份、base64、请求前展开 */
#include "image.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
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位随机>.<ext> */
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, &degraded);
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, &degraded);
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, &degraded);
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;
}