Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/cover_chain_sim.c
T
AskaEth 9d93f1b1ef fix(ncm): 封面矩阵实锤修正 —— 200解码失败=死对象返JSON错误体(非webp)/UA改桌面浏览器(CNMPlayer UA被拦403)/非图k不定性不重试
真机(设备恢复)CDN 行为矩阵结论并落地:
- 裸请求(无Referer)→403: 防盗链坐实, 封面请求头加 Referer https://music.163.com/。
- +Referer 仍不返真图; 全部变体只见 403/404/200+JSON错误体(43B {"code, application/json)。
  ③「200解码失败」真因=源站死对象返 200+JSON错误体, 非webp; stbi解不了→旧逻辑盲指数退避8次。
  → download_cover 回填 *out_kind(0可解码/1 webp/2非图), worker 对 kind=1/2 与确定性 4xx 一律
  立即清目标走兜底(保留歌单封面)不重试; kind=2 打 'not an image (origin error body/200)'。
- UA 分表: CNMPlayer口径 UA(Mozilla/5.0 CNMPlayer/0.1)同Referer也返403(CDN拦bot串);
  桌面 Chrome UA(旧cover.c)返200。→ NCM_COVER_UA 改回桌面浏览器 UA。
- ?param=300y300 在死对象上→404(对死链无效); 存活URL未获样本, 保留 USE_PARAM=1(标准缩略)。
- cover_chain_sim 增 T10(200+JSON→非图不重试) + stub 内容加 JPEG 魔数(过 body_is_decodable_img);
  T1-T10 ALL PASS。mipsel 0 warning(text 1020306/bss 1362500 持平)。

新 ncm md5 06896b54f31ddf55980cbf921a00d67a (未部署, 留主会话上卡)
2026-09-02 10:50:07 +08:00

352 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* bench/cover_chain_sim.c — 封面链离线回归(0 真实请求,REPORT §18)。
*
* 驱动真实 app/logic/cover.c(worker 线程/ncm_cache LRU/cover_current.jpg 落盘),
* HTTP 端点为本地合成 stub(按 URL 生成可区分内容,可注入失败次数模拟慢/坏网络),
* meow_img 为假句柄(嵌内容哈希,不真解码 JPEG——测的是链路逻辑不是解码器)。
*
* 用例:
* T1 首次请求 → 封面出现(内容哈希对得上 URL);
* T2 定居后 worker **不忙等**:cover_changed() 在 1.2s 内不得再置位
* (§18 前:成功后不清目标 → 每圈解码+换入+g_gen++ 死循环,CPU 占满且播放页每帧
* 整盘重绘——本用例在旧代码上必 FAIL);
* T3 切歌换 URL → 封面换入新内容 + cover_current.jpg 同步更新(launcher 媒体卡片读它);
* T4 重复请求同一 URL → 去重(0 新增 HTTP,0 换入);
* T5 网络失败 ×2 后退避重试成功(不无声死等);
* T6 连续失败 ≥8 次放弃(目标清除,不永久占 worker);
* T7 空/NULL URL 忽略;cover_cache_reset 清空。
*
* 编译(宿主,ncm_app 目录下;weak cover_put 兼容 §18 前的 cover.c 做对照):
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -c bench/cover_chain_sim.c -o /tmp/cs.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -c app/logic/cover.c -o /tmp/cs_cover.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_cache.c -o /tmp/cs_cache.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_store.c -o /tmp/cs_store.o
* gcc -O2 -Wl,--gc-sections -o cover_chain_sim /tmp/cs.o /tmp/cs_cover.o /tmp/cs_cache.o \
* /tmp/cs_store.o -lpthread
* 运行:./cover_chain_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/stat.h>
ncm_app_state_t g_st;
volatile int g_app_quit = 0;
int g_host_mock = 0;
/* ---- 公共 stub ---- */
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) {
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
dst[n] = 0;
}
while (src[n]) n++;
return n;
}
static int g_log_warns;
static char g_last_log[512]; /* 最近一条日志文本(webp/403 分诊日志断言用) */
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
if (lvl >= MEOW_LOG_WARN) g_log_warns++;
va_list ap;
va_start(ap, fmt);
vsnprintf(g_last_log, sizeof g_last_log, fmt, ap);
va_end(ap);
}
/* ---- 假 meow_img:嵌内容 FNV 哈希(链路验证用,不真解码) ---- */
struct meow_img { unsigned long hash; int len; int freed; };
static unsigned long fnv1a(const unsigned char *p, size_t n)
{
unsigned long h = 1469598103934665603UL;
for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 1099511628211UL; }
return h;
}
meow_img *meow_img_load_mem(const void *buf, int len)
{
if (!buf || len <= 0) return NULL;
meow_img *im = (meow_img *)malloc(sizeof *im);
if (!im) return NULL;
im->hash = fnv1a((const unsigned char *)buf, (size_t)len);
im->len = len;
im->freed = 0;
return im;
}
/* §30 起 cover.c 用 meow_img_load_file(从 ncm_cache 写者临时文件解码):读文件内容哈希。
* 与 url_hash() 的期望值对齐(同内容同哈希)。 */
meow_img *meow_img_load_file(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
unsigned char buf[4096];
size_t n = fread(buf, 1, sizeof buf, f);
fclose(f);
if (!n) return NULL;
meow_img *im = (meow_img *)malloc(sizeof *im);
if (!im) return NULL;
im->hash = fnv1a(buf, n);
im->len = (int)n;
im->freed = 0;
return im;
}
/* meow_img_downscale 桩:不缩放(返回 NULL)→ cover_cap 保留原图(测链逻辑,非缩放器)。 */
meow_img *meow_img_downscale(meow_img *im, int maxdim) { (void)im; (void)maxdim; return NULL; }
void meow_img_free(meow_img *im)
{
if (!im) return;
im->freed = 1;
free(im);
}
/* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */
__attribute__((weak)) void cover_put(void) { }
/* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入失败/延迟/webp/403 ---- */
static int g_http_calls;
static int g_fail_next; /* 剩余注入失败次数 */
static int g_delay_ms; /* 模拟慢网络 */
static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */
static int g_json_next; /* 下一次下载返回 JSON 错误体(200,实测「200 解码失败」真凶=死对象回 JSON) */
static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */
static unsigned long djb2(const char *s)
{
unsigned long h = 5381;
while (*s) h = ((h << 5) + h) + (unsigned char)*s++;
return h;
}
int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp)
{
g_http_calls++;
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
size_t n = 4096 < buf_sz ? 4096 : buf_sz;
unsigned long h = djb2(req->url);
for (size_t i = 0; i < n; i++) buf[i] = (char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
/* 真实 cover.c 会做可解码图魔数判定(REPORT §34:200 但非 JPEG/PNG/GIF 判非图不重试),
* 故 mock body 加 JPEG 魔数 FFD8FF,与 url_hash() 期望同规则。 */
if (n >= 3) { buf[0]=(char)0xFF; buf[1]=(char)0xD8; buf[2]=(char)0xFF; }
memset(resp, 0, sizeof *resp);
resp->status = 200;
resp->body = buf;
resp->body_len = n;
resp->content_length = n;
return NCM_OK;
}
/* 流式下载桩:cover.c 现走 ncm_http_download(§30 改流式写文件,非旧 ncm_http_request)。
* 与 ncm_http_request 同语义:内容 = djb2(req->url) 填充 4KB,经写回调投递(落入 ncm_cache
* 写者临时文件);可注入失败/延迟。 */
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp)
{
(void)max_bytes;
g_http_calls++;
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
unsigned char buf[4096];
size_t n = sizeof buf;
unsigned long h = djb2(req->url);
for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
/* JPEG 魔数(cover.c 可解码图判定通过);webp/json 注入优先覆盖各自魔数 */
buf[0]=0xFF; buf[1]=0xD8; buf[2]=0xFF;
if (g_json_next) {
g_json_next = 0;
buf[0]='{'; buf[1]='"'; buf[2]='c'; buf[3]='o'; buf[4]='d'; buf[5]='e';
}
if (g_webp_next) {
g_webp_next = 0;
buf[0]='R';buf[1]='I';buf[2]='F';buf[3]='F';
buf[4]=0;buf[5]=0;buf[6]=0;buf[7]=0;
buf[8]='W';buf[9]='E';buf[10]='B';buf[11]='P';
}
memset(resp, 0, sizeof *resp);
resp->status = g_status_next ? g_status_next : 200;
g_status_next = 0;
resp->body = (const char *)buf;
resp->body_len = n;
resp->content_length = n;
if (cb && cb(ctx, buf, n) != 0) return NCM_ERR_IO;
return NCM_OK;
}
/* URL 期望内容哈希(与 stub 生成式一致)。cover.c 现对封面 URL 追加 ?param=<NCM_COVER_PARAM>
* 缩略后缀(ncm_app_internal.h,REPORT §NEW 任务2)——实测请求的是参数版 URL,故期望值须按
* 同规则构造,否则「断言 raw URL」与「实测 param URL」哈希错位。已含 param= 不叠。 */
static unsigned long url_hash(const char *url)
{
char n[512];
snprintf(n, sizeof n, "%s", url);
#if NCM_COVER_USE_PARAM
if (!strstr(n, "param=")) {
size_t l = strlen(n), add = strlen("?param=") + strlen(NCM_COVER_PARAM);
if (l + add + 1 < sizeof n) {
strcat(n, strchr(n, '?') ? "&param=" : "?param=");
strcat(n, NCM_COVER_PARAM);
}
}
#endif
unsigned char tmp[4096];
unsigned long h = djb2(n);
for (size_t i = 0; i < sizeof tmp; i++) tmp[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
tmp[0]=0xFF; tmp[1]=0xD8; tmp[2]=0xFF; /* 与 stub JPEG 魔数一致 */
return fnv1a(tmp, sizeof tmp);
}
static int g_fail;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* 轮询等待封面内容变成 want_hash(≤ ms)。返回 1=达成。 */
static int wait_cover_hash(unsigned long want, int ms)
{
for (int t = 0; t < ms; t += 20) {
meow_img *im = cover_get();
unsigned long h = im ? im->hash : 0;
cover_put();
if (h == want) return 1;
usleep(20 * 1000);
}
return 0;
}
static unsigned long cur_cover_hash(void)
{
meow_img *im = cover_get();
unsigned long h = im ? im->hash : 0;
cover_put();
return h;
}
/* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。 */
static unsigned long curjpg_hash(void)
{
char path[600];
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
FILE *f = fopen(path, "rb");
if (!f) return 0;
unsigned char b[4096];
size_t n = fread(b, 1, sizeof b, f);
fclose(f);
return n ? fnv1a(b, n) : 0;
}
#define URL_A "http://p1.music.126.net/albumA/xxx.jpg"
#define URL_B "http://p1.music.126.net/albumB/yyy.jpg"
#define URL_C "http://p1.music.126.net/albumC/zzz.jpg"
#define URL_D "http://p1.music.126.net/albumD/www.jpg"
#define URL_E "http://p1.music.126.net/albumE/vvv.jpg" /* webp 注入用(未缓存) */
#define URL_F "http://p1.music.126.net/albumF/uuu.jpg" /* 403 注入用(未缓存) */
#define URL_G "http://p1.music.126.net/albumG/ttt.jpg" /* JSON 错误体注入用(未缓存) */
int main(void)
{
if (system("rm -rf /tmp/ncm_cover_sim") != 0) { printf("FAIL rm /tmp cache\n"); return 1; }
memset(&g_st, 0, sizeof g_st);
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_cover_sim");
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir);
printf("== T1: first request -> cover appears ==\n");
cover_request(URL_A);
CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cover A shown (hash match)");
CHECK(g_http_calls == 1, "http calls=%d (expect 1)", g_http_calls);
CHECK(curjpg_hash() == url_hash(URL_A), "cover_current.jpg == A");
printf("== T2: settle -> worker idle (no busy loop) ==\n");
cover_ack();
int churn = 0;
for (int t = 0; t < 1200; t += 10) { if (cover_changed()) churn++; usleep(10 * 1000); }
CHECK(churn == 0, "cover_changed churn in 1.2s = %d (expect 0; §18 前=忙等死循环)", churn);
CHECK(g_http_calls == 1, "http calls=%d (still 1)", g_http_calls);
printf("== T3: switch song -> cover switches + cover_current.jpg updated ==\n");
cover_request(URL_B);
CHECK(wait_cover_hash(url_hash(URL_B), 3000), "cover switched to B");
CHECK(curjpg_hash() == url_hash(URL_B), "cover_current.jpg == B");
CHECK(g_http_calls == 2, "http calls=%d (expect 2)", g_http_calls);
cover_ack();
usleep(400 * 1000);
CHECK(!cover_changed(), "no churn after switch");
printf("== T4: re-request same url -> dedup ==\n");
cover_request(URL_B);
usleep(500 * 1000);
CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls);
printf("== T5: flaky network x2 -> backoff retry succeeds ==\n");
g_fail_next = 2;
cover_request(URL_C);
CHECK(wait_cover_hash(url_hash(URL_C), 8000), "cover C after retries");
CHECK(g_http_calls >= 5, "http calls=%d (>=5: 2 fails + 1 ok for C)", g_http_calls);
CHECK(g_log_warns >= 2, "warn logs=%d (failures logged, 不无声)", g_log_warns);
printf("== T6: permanent failure -> give up after %d tries ==\n", 8);
g_fail_next = 100;
int before = g_http_calls;
cover_request(URL_D);
usleep(25 * 1000 * 1000 / 10); /* ~2.5s 不够 8 次退避(0.5+1+2+4*5=23.5s),分段等 */
for (int t = 0; t < 30 && g_http_calls - before < 8; t++) usleep(1000 * 1000);
int tries = g_http_calls - before;
CHECK(tries == 8, "give up after tries=%d (expect 8)", tries);
CHECK(cur_cover_hash() == url_hash(URL_C), "old cover C retained after give-up");
usleep(600 * 1000);
CHECK(g_http_calls - before == 8, "no further retries after give-up (calls=%d)",
g_http_calls - before);
printf("== T8: webp body (200) -> unsupported, clear target, no blind retry ==\n");
g_fail_next = 0;
int b8 = g_http_calls;
g_webp_next = 1;
cover_request(URL_E);
usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */
CHECK(g_http_calls == b8 + 1, "webp: single download=%d (no retry)", g_http_calls - b8);
CHECK(cur_cover_hash() == url_hash(URL_C), "webp: 旧封面 C 保留(兜底,不误换)");
CHECK(strstr(g_last_log, "webp unsupported") != NULL, "log: 'webp unsupported' seen");
printf("== T9: HTTP 403 anti-hotlink -> single download, no retry, distinct log ==\n");
int b9 = g_http_calls;
g_status_next = 403;
cover_request(URL_F);
usleep(800 * 1000);
CHECK(g_http_calls == b9 + 1, "403: single download=%d (no retry)", g_http_calls - b9);
CHECK(cur_cover_hash() == url_hash(URL_C), "403: 旧封面 C 保留(兜底)");
CHECK(strstr(g_last_log, "403") != NULL, "log: '403' seen");
g_status_next = 0;
printf("== T10: 200+JSON error body (dead origin) -> not an image, no retry ==\n");
int b10 = g_http_calls;
g_json_next = 1;
cover_request(URL_G);
usleep(800 * 1000);
CHECK(g_http_calls == b10 + 1, "json: single download=%d (no retry)", g_http_calls - b10);
CHECK(cur_cover_hash() == url_hash(URL_C), "json: 旧封面 C 保留(兜底)");
CHECK(strstr(g_last_log, "not an image") != NULL, "log: 'not an image' seen");
printf("== T7: null/empty + reset ==\n");
int b7 = g_http_calls;
cover_request(NULL);
cover_request("");
CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls);
cover_cache_reset();
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
g_fail_next = 0;
printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n");
return g_fail;
}