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:
1 parent
c7ef1ebb04
commit
aeb9176101
6 files changed
+308
-20
No files matched your search
@@ -2994,3 +2994,97 @@ cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 101711
|
||||
凭 `play: dl` 日志定位;②hires 档静默给 mp3 显示「已降级至 极高 HQ」而非 Hi-Res;③封面 404
|
||||
立即跳过不空转;④我创建的歌单无收藏按钮;⑤缓存目录名 `song_<id>_<level>.<ext>` 无双点;
|
||||
⑥home topbar 三动作显示位图图标(非豆腐)。
|
||||
|
||||
---
|
||||
|
||||
## 34. 封面链分诊与修复:防盗链 403 / webp 解码失败 / 尺寸参数 / 分诊日志(2026-09-02)
|
||||
|
||||
> 真机已实锤三种封面失败形态(详见下「真机三分型」):①404 死链(CDN 源站无对象,`X-Error-Info: Origin`,
|
||||
> 182B JSON 错误体,311/§33.3 已做 4xx 不重试);②**403 防盗链**(p3.music.126.net 返 150B text/html,
|
||||
> 缺 Referer/UA 被拦嫌疑大);③**200 但解码失败**(rc=0 status=200 仍 fetch failed——响应体疑为 webp,
|
||||
> stb_image 不支持)。
|
||||
|
||||
### 34.1 真机三分型结论(本批未能在真机复测——设备 10.78.193.50 全程不可达,见 34.6 诚实出入)
|
||||
|
||||
本批无法执行**设备端 CDN 行为矩阵**(设备 sleep/WiFi 休眠,`No route to host`),故三种分型的鉴定依据:
|
||||
①404 由 §30.5/§33.3 已实测锚定;②403 与③200-webp 由本批任务书给定、代码静态分析 + 宿主 mock 链
|
||||
验证(stb_image v2.18 无 webp 解码器,`meow_img_load_file` 对 webp 必返 NULL → 旧逻辑当成
|
||||
「fetch failed」盲指数退避重试)。**UA/Referer/param 三参数取值**:对齐 CNMPlayer 客户端参考实现
|
||||
(`Referer: https://music.163.com/` + `User-Agent: Mozilla/5.0 CNMPlayer/0.1`,见 CNMPlayer `src/app/mod.rs
|
||||
App::new` 的 `default_headers`);`?param=300y300` 为网易云图片 CDN 标准缩略参数(ncm-api-rs 亦以其为
|
||||
已知格式)。真机矩阵留 34.6 后续确认。
|
||||
|
||||
### 34.2 修法(`app/logic/cover.c` + `app/ncm_app_internal.h`)
|
||||
|
||||
- **请求头对齐 CNMPlayer**:`req.extra_hdrs = "Referer: https://music.163.com/\r\nUser-Agent: Mozilla/5.0 CNMPlayer/0.1"`
|
||||
(常量宏 `NCM_COVER_REFERER`/`NCM_COVER_UA`,改 UA 只动一处)。修 403 防盗链(②)。
|
||||
- **webp 先判不解码**:`download_cover` 成功后、`meow_img_load_file` 前,检查 `resp.content_type`
|
||||
含 webp 或临时文件魔数 `RIFF....WEBP`(`file_is_webp`,读前 12 字节一次小 fopen)→ 回 `*out_webp=1`
|
||||
返回 NULL;worker 判 webp 单独打日志 `cover: webp unsupported url=.. (no decode)`,立即清目标走兜底
|
||||
(播放页保留歌单封面 `g_st.cover_url`),**不盲目指数重试**(修 ③)。
|
||||
- **封面 URL 追加尺寸参数**:`cover_norm_url`(`cover_request` 入口处规范化)对不含 `param=` 的 URL 追加
|
||||
`?param=300y300`(已有 `?` 用 `&`;溢出 512 缓冲则放弃追加保原 URL)。`NCM_COVER_USE_PARAM=1`;
|
||||
`NCM_COVER_PARAM` 宏放共享头 `ncm_app_internal.h`,与 `bench/cover_chain_sim.c` 的 URL 期望哈希同源,
|
||||
避免字符串漂移。省内存(300×300 ≈20KB vs 全尺寸 1.34MB)+ CDN 出 jpeg(避 webp)+ 命中率高。
|
||||
- **分诊日志强化**:403 单独打 `cover: fetch fail url=.. rc=.. status=403 (anti-hotlink/forbidden, no retry)`;
|
||||
404 及其它确定性 4xx `(4xx, no retry)`;webp `(no decode)`;瞬时错误才指数退避。确定性 4xx + webp
|
||||
一律立即清目标不重试(已有 4xx 逻辑,本批补 403 区分日志 + webp 分支)。
|
||||
|
||||
### 34.3 worker 队列评估(304 建议:单槽→有界 FIFO+优先级)
|
||||
|
||||
**结论:不改。** 当前 cover worker 为「单槽 latest-wins」(`g_tgt_url` 被最新 `cover_request` 覆写)——
|
||||
对封面这种**仅当前播放页需要**的瞬态展示资产,语义已最优:切歌即最新目标,旧请求在途完成时因
|
||||
`g_tgt_url` 变更被丢弃(`if (strcmp(g_tgt_url, url)==0)` 判元),天然取消过期封面。改成有界 FIFO+
|
||||
优先级会为**不会显示的过期封面**白付解码 CPU + 驻留内存(设备 RAM 仅 56MB),净负收益。304 原始
|
||||
关切(4xx 空转重试占 worker)已由本批「确定性 4xx/webp 不重试 + 瞬时退避」根治。留后续共识,不迁移。
|
||||
|
||||
### 34.4 验证记录(宿主 + qemu;0 新增真实请求——矩阵测试因设备不可达未执行)
|
||||
|
||||
| 项 | 方式 | 结果 |
|
||||
|---|---|---|
|
||||
| cover 链(含新 webp/403) | `cover_chain_sim`(驱动真实 cover.c + ncm_cache,HTTP 本地 stub;T1-T7 存量 + 新增 T8 webp 魔数 / T9 403) | **T1-T9 ALL PASS**(T8:webp 单次下载不重试+旧封面兜底+日志 `webp unsupported`;T9:403 单次下载不重试+日志含 403) |
|
||||
| 缓存 LRU/隔离 | `cache_evict_sim` | **PASS** |
|
||||
| 播放状态机 | `player_flow_sim` P1-P12 | **PASS** |
|
||||
| 大歌单滑窗 | `window_sim` | **PASS** |
|
||||
| 搜索 | `search_sim` | **PASS** |
|
||||
| 边下边播 | `stream_sim` 全矩阵 | **PASS** |
|
||||
| 降级链 | `degrade_sim`(含真机场景) | **ALL PASS** |
|
||||
| 内存瘦身 | `mem_sim`(MEM_SIM=1) | **PASS** |
|
||||
| core | `build_core.sh` → `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
|
||||
| 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra) | **0 warning**;`text 1019834 / data 1648 / bss 1362500`(bss 与 §33 持平,封面改动未涨静态内存) |
|
||||
| 宿主构建 | `build_ncm_host.sh`(自有代码) | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 未用结果告警,非本批) |
|
||||
| UI 渲染 | `build_ncm_host.sh` + `NCM_PAGE=home/player/playlist MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||||
|
||||
### 34.5 md5(本批产物)
|
||||
|
||||
```
|
||||
a7c22bd4ee3bcd02fb24046d27a33242 ncm # 新(未部署,留主会话上卡)
|
||||
f870b49b11ad8c0bb67cd47d2f6c2162 ncm_host # 宿主调试二进制(重链)
|
||||
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,内容未变)
|
||||
```
|
||||
|
||||
改动文件(本批,仅这些):
|
||||
```
|
||||
修改:
|
||||
app/logic/cover.c # 头对齐 CNMPlayer(Referer+UA) + webp 魔数/content-type 先判 + URL 尺寸参数 + 403/webp 分诊日志 + 兜底不盲重试(34.2)
|
||||
app/ncm_app_internal.h # NCM_COVER_USE_PARAM / NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)
|
||||
bench/cover_chain_sim.c # 修 sim 对齐现行 cover.c(ncm_http_download/meow_img_load_file/downscale 桩 + url_hash 参数规则)+ 新增 T8 webp / T9 403 用例
|
||||
ncm # mipsel 产物(重链)
|
||||
ncm_host # 宿主产物(重链)
|
||||
```
|
||||
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 请求;未部署——留主会话统一上卡。)
|
||||
|
||||
### 34.6 诚实出入 / 遗留
|
||||
|
||||
- **设备端 CDN 矩阵未执行**:设备 10.78.193.50 本批全程 `No route to host`(sleep/WiFi 休眠,宿主机
|
||||
10.78.193.150 同网段 ARP 无应答,多次重试 + 后台 ping 监视 600s 均未恢复)。故③「200 解码失败是否
|
||||
webp」未能在真机用 curl 矩阵直接证实(由 stb_image 无 webp 解码器 + magic/content-type 先判兜住);
|
||||
UA 三变体(桌面浏览器 / iPhone NeteaseMusic 9.0.90·5038 / CNMPlayer 口径)与 `?param=300y300` 的
|
||||
实际 CDN 出图效果未实测。**建议真机恢复后补跑**:取日志失败 URL,`curl` 单次比对 裸请求 /
|
||||
`+Referer` / `+UA 变体` / `?param=300y300`,记 http code + content-type + 魔数(RIFF=webp / FFD8=jpeg),
|
||||
据实微调 `NCM_COVER_UA` 与 `NCM_COVER_USE_PARAM`(宏已就位,改一处即生效——UA 在 cover.c 宏、
|
||||
param 在 ncm_app_internal.h 宏)。
|
||||
- **`?param=300y300` 潜在风险**:个别封面若仅存在全尺寸对象,param 版可能 404——已由确定性 4xx
|
||||
不重试兜底(回退歌单封面),不劣于基线;但缓存键随之变化(旧全尺寸 `cov<rawhash>.jpg` 成为孤儿,
|
||||
LRU 正常淘汰)。
|
||||
- **`http→443` 潜性缺陷不修**(承 §33.3/§30.8 结论;现状封面 URL 均为 https,不受影响)。
|
||||
@@ -24,9 +24,19 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* 封面 CDN 请求头(REPORT §NEW 任务2,对齐 CNMPlayer):网易云图片 CDN 靠 Referer 防盗链,
|
||||
* 缺失被拦返回 403(p3.music.126.net 150B text/html,REPORT §NEW 三分型②);UA 分表口径与
|
||||
* CNMPlayer 客户端一致(Mozilla/5.0 CNMPlayer/0.1 + Referer https://music.163.com/)。两者
|
||||
* 均为常量:真机矩阵实测需换 UA 时只改 NCM_COVER_UA 一处,不涉逻辑(见 REPORT §NEW 矩阵结论)。 */
|
||||
#define NCM_COVER_REFERER "https://music.163.com/"
|
||||
#define NCM_COVER_UA "Mozilla/5.0 CNMPlayer/0.1"
|
||||
/* ?param=WxH 缩略后缀宏 NCM_COVER_PARAM / NCM_COVER_USE_PARAM 见 ncm_app_internal.h(与
|
||||
* bench/cover_chain_sim.c 同源)。 */
|
||||
|
||||
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
||||
static meow_img *g_cover_img;
|
||||
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
|
||||
@@ -65,18 +75,68 @@ static int cover_sink_write(void *ctx, const unsigned char *data, size_t len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 封面 URL 规范化:追加 NCM_COVER_PARAM 尺寸后缀(REPORT §NEW 任务2)。已含 param= 的
|
||||
* URL 原样保留(尊重调用方/宿主食之已有尺寸);否则追加(已有其它查询用 &,无则用 ?)。
|
||||
* 溢出(512 缓冲放不下)则放弃追加——宁可用全尺寸 URL 也不截断出坏 URL。 */
|
||||
static void cover_norm_url(const char *src, char *out, size_t out_sz)
|
||||
{
|
||||
ncm_strlcpy(out, src, out_sz);
|
||||
#if NCM_COVER_USE_PARAM
|
||||
if (out[0] && strstr(out, "param=")) return; /* 已指定尺寸,不叠 */
|
||||
size_t l = strlen(out);
|
||||
size_t add = strlen("?param=") + strlen(NCM_COVER_PARAM);
|
||||
if (!out[0] || l + add + 1 >= out_sz) return; /* 放不下:保原 URL */
|
||||
strcat(out, strchr(out, '?') ? "¶m=" : "?param=");
|
||||
strcat(out, NCM_COVER_PARAM);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* 大小写不敏感的 posix 子串检测(内容 webp 判定用;不依赖 strcasestr/_GNU_SOURCE 桩)。 */
|
||||
static int str_ci_contains(const char *hay, const char *needle)
|
||||
{
|
||||
if (!hay || !needle) return 0;
|
||||
size_t n = strlen(needle);
|
||||
if (!n) return 1;
|
||||
for (; *hay; hay++) {
|
||||
if (tolower((unsigned char)hay[0]) == tolower((unsigned char)needle[0])) {
|
||||
const char *a = hay, *b = needle;
|
||||
while (*b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
|
||||
if (!*b) return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 文件是否 WebP:魔数 RIFF....WEBP(stb_image 不支持 webp,解码必 NULL → 显示「200 但解码
|
||||
* 失败」三分型③,先决判定不盲重试)。读文件前 12 字节即可,代价一次小 fopen。 */
|
||||
static int file_is_webp(const char *path)
|
||||
{
|
||||
if (!path) return 0;
|
||||
unsigned char b[12];
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
size_t n = fread(b, 1, sizeof b, f);
|
||||
fclose(f);
|
||||
if (n < 12) return 0;
|
||||
return b[0]=='R'&&b[1]=='I'&&b[2]=='F'&&b[3]=='F' &&
|
||||
b[8]=='W'&&b[9]=='E'&&b[10]=='B'&&b[11]=='P';
|
||||
}
|
||||
|
||||
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
|
||||
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
|
||||
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3)。 */
|
||||
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3);
|
||||
* *out_webp=1 表示 200 但 body 是 WebP(stb 不支持,先判不解码——三分型③)。 */
|
||||
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len,
|
||||
int *out_rc, int *out_status)
|
||||
int *out_rc, int *out_status, int *out_webp)
|
||||
{
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
/* 头对齐 CNMPlayer(REPORT §NEW 任务2):Referer 防盗链 + UA 分表。 */
|
||||
char hdrs[192];
|
||||
snprintf(hdrs, sizeof hdrs, "Referer: %s\r\nUser-Agent: %s", NCM_COVER_REFERER, NCM_COVER_UA);
|
||||
req.url = url;
|
||||
req.method = "GET";
|
||||
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
req.extra_hdrs = hdrs;
|
||||
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||||
req.timeout_ms = 15000;
|
||||
ncm_http_resp_t resp;
|
||||
@@ -89,6 +149,13 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
|
||||
/* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */
|
||||
const char *tmp = ncm_cache_writer_tmp_path(w);
|
||||
if (!tmp) return NULL;
|
||||
/* WebP 先判:content-type 含 webp 或文件魔数 RIFF....WEBP → 不解码(stb 不支持),
|
||||
* 回 webp=1 走兜底;避免 stbi_load 空转返回 NULL 后当成「fetch failed」盲指数退重试。 */
|
||||
if (out_webp) *out_webp = 0;
|
||||
if ((resp.has_content_type && str_ci_contains(resp.content_type, "webp")) || file_is_webp(tmp)) {
|
||||
if (out_webp) *out_webp = 1;
|
||||
return NULL;
|
||||
}
|
||||
return meow_img_load_file(tmp);
|
||||
}
|
||||
|
||||
@@ -154,7 +221,7 @@ static void *cover_worker(void *arg)
|
||||
cover_cache_id(url, key, sizeof key);
|
||||
size_t blen = 0;
|
||||
meow_img *img = NULL;
|
||||
int drc = NCM_ERR_NET, dstatus = 0; /* 本目标最近一次下载 rc/HTTP status(4xx 判定用) */
|
||||
int drc = NCM_ERR_NET, dstatus = 0, dwebp = 0; /* 下载 rc/HTTP status/webp 判回(分诊用) */
|
||||
|
||||
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
|
||||
char cpath[512];
|
||||
@@ -170,7 +237,7 @@ static void *cover_worker(void *arg)
|
||||
ncm_cache_writer_t *w = NULL;
|
||||
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
||||
blen = 0;
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus);
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus, &dwebp);
|
||||
if (img) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
|
||||
@@ -189,12 +256,33 @@ static void *cover_worker(void *arg)
|
||||
/* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */
|
||||
if (img) img = cover_cap(img);
|
||||
if (!img) {
|
||||
/* 失败分诊(REPORT §NEW 任务3):确定性 4xx(404 死链等)**不重试**,立即清目标
|
||||
* 跳下一张(重试也白费);仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
||||
* 日志打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么)。 */
|
||||
/* 失败分诊(REPORT §NEW 任务3):
|
||||
* - webp:200 但 stb 不支持解码(三分型③),**非瞬时、非重试可愈** → 立即清目标
|
||||
* 走兜底(播放页保留歌单封面 g_st.cover_url,不盲目指数重试空转);
|
||||
* - 确定性 4xx(404 死链 / **403 防盗链**)**不重试**,立即清目标跳下一张;
|
||||
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
||||
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
|
||||
* 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */
|
||||
if (dwebp) {
|
||||
meow_log(MEOW_LOG_WARN, "cover: webp unsupported url=%.64s rc=%d status=%d (no decode)",
|
||||
url, drc, dstatus);
|
||||
pthread_mutex_lock(&g_cm);
|
||||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张(走兜底) */
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
backoff_ms = 0; fails = 0;
|
||||
continue;
|
||||
}
|
||||
int deterministic = (dstatus >= 400 && dstatus < 500 && dstatus != 408 && dstatus != 429);
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d%s",
|
||||
url, drc, dstatus, deterministic ? " (4xx, no retry)" : "");
|
||||
if (dstatus == 403) {
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=403 (anti-hotlink/forbidden, no retry)",
|
||||
url, drc);
|
||||
} else if (deterministic) {
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d (4xx, no retry)",
|
||||
url, drc, dstatus);
|
||||
} else {
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d",
|
||||
url, drc, dstatus);
|
||||
}
|
||||
if (deterministic) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
|
||||
@@ -238,16 +326,21 @@ static void *cover_worker(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
|
||||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。
|
||||
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
|
||||
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
|
||||
void cover_request(const char *url)
|
||||
{
|
||||
if (!url || !url[0]) return;
|
||||
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
|
||||
char norm[512];
|
||||
cover_norm_url(url, norm, sizeof norm);
|
||||
const char *u = norm[0] ? norm : url; /* 规范化溢出保护:退回原 URL */
|
||||
pthread_mutex_lock(&g_cm);
|
||||
/* 去重:已在展示 或 已在拉取途中的同一 URL 不重复提交。 */
|
||||
if ((g_cover_url[0] && strcmp(g_cover_url, url) == 0) ||
|
||||
(g_tgt_url[0] && strcmp(g_tgt_url, url) == 0)) { pthread_mutex_unlock(&g_cm); return; }
|
||||
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url);
|
||||
if ((g_cover_url[0] && strcmp(g_cover_url, u) == 0) ||
|
||||
(g_tgt_url[0] && strcmp(g_tgt_url, u) == 0)) { pthread_mutex_unlock(&g_cm); return; }
|
||||
ncm_strlcpy(g_tgt_url, u, sizeof g_tgt_url);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
if (!g_thr_on) {
|
||||
g_thr_on = 1;
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
#define ACT_SETTINGS 4 /* 设置页 */
|
||||
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
|
||||
|
||||
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
|
||||
* 缩略 JPEG——省内存(300×300≈20KB vs 全尺寸 1.34MB)+解 webp 三分型③+命中率高;已含 param= 的
|
||||
* URL 尊重原样)。共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
|
||||
* 同源,避免两处字符串漂移。 */
|
||||
#define NCM_COVER_USE_PARAM 1
|
||||
#define NCM_COVER_PARAM "300y300"
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
#define APP_W 480
|
||||
#define APP_H 720
|
||||
|
||||
@@ -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, '?') ? "¶m=" : "?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;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user