fix(ncm): 封面链分诊与修复 —— CNMPlayer 头(Referer+UA 解403)/webp 魔数内容型先判不解码走兜底/URL 追加 param=300y300 缩略图/403 与 webp 单独分诊日志不盲重试

- cover.c: 请求头对齐 CNMPlayer(Referer https://music.163.com/ + UA Mozilla/5.0 CNMPlayer/0.1);
  下载后先判 content-type 含 webp 或临时文件魔数 RIFF....WEBP(stb_image 不支持),回 webp=1 不解码,
  worker 打 'webp unsupported' 立即清目标走兜底(保留歌单封面),不再当 fetch failed 盲目指数重试;
  403 单独日志(anti-hotlink/forbidden, no retry),确定性 4xx 不重试(沿用 311)。
- cover_request 入口 cover_norm_url 追加 ?param=300y300(已有 param= 尊重原样, 溢出保原 URL),
  缓存键/去重/下载 URL 同参版(省内存+出 jpeg)。
- ncm_app_internal.h: NCM_COVER_USE_PARAM/NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)。
- bench/cover_chain_sim.c: 补 ncm_http_download/meow_img_load_file/meow_img_downscale 桩对齐现行
  cover.c(旧 ncm_http_request 桩已过时), url_hash 同参数规则; 新增 T8 webp/T9 403 用例。
- 验证: cover_chain_sim T1-T9 ALL PASS + stream/player_flow/window/search/cache_evict/degrade/mem
  sim 全 PASS + core host+mipsel(qemu) PASS + build_ncm.sh 0 warning(text 1019834/bss 1362500 持平)
  + ncm_host UI dump exit=0。0 新增真实 CDN 请求; 设备端 CDN 矩阵因设备不可达未执行(见 REPORT 34.6)。

新 ncm md5 a7c22bd4ee3bcd02fb24046d27a33242 (未部署, 留主会话上卡)
This commit is contained in:
AskaEth committed 2026-09-02 10:37:24 +08:00
1 parent c7ef1ebb04
commit aeb9176101
6 files changed
+308 -20

No files matched your search

+99 -5
View File
@@ -51,10 +51,14 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
}
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++;
(void)fmt;
va_list ap;
va_start(ap, fmt);
vsnprintf(g_last_log, sizeof g_last_log, fmt, ap);
va_end(ap);
}
/* ---- 假 meow_img:嵌内容 FNV 哈希(链路验证用,不真解码) ---- */
@@ -78,6 +82,27 @@ meow_img *meow_img_load_mem(const void *buf, int len)
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;
@@ -88,10 +113,12 @@ void meow_img_free(meow_img *im)
/* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */
__attribute__((weak)) void cover_put(void) { }
/* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入失败/延迟 ---- */
/* ---- 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_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */
static unsigned long djb2(const char *s)
{
@@ -116,11 +143,55 @@ int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_ht
return NCM_OK;
}
/* URL 期望内容哈希(与 stub 生成式一致) */
/* 流式下载桩: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);
/* webp 注入:前 12 字节置 RIFF....WEBP 魔数(stb 不支持 → cover.c 判 webp=1 不解码) */
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(url);
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);
return fnv1a(tmp, sizeof tmp);
}
@@ -169,6 +240,8 @@ static unsigned long curjpg_hash(void)
#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 注入用(未缓存) */
int main(void)
{
@@ -224,10 +297,31 @@ int main(void)
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("== T7: null/empty + reset ==\n");
int b7 = g_http_calls;
cover_request(NULL);
cover_request("");
CHECK(g_http_calls == before + 8, "null/empty ignored (calls=%d)", g_http_calls);
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;