335 lines
13 KiB
C
335 lines
13 KiB
C
/* test_image.c - 图片格式嗅探、本地备份、base64、请求前展开 */
|
|
#include "image.h"
|
|
#include "util.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.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, °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;
|
|
}
|