diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 7b0b155..c2219b0 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -2488,3 +2488,148 @@ b7a2bfaec3c3d7bd29a151072cd40f32 bench/cache_evict_sim.c # 新 case9:.part - **歌单列表缓存无版本化清理**:`playlists.bin` 靠 ver/magic 判 miss;数组改版/账号切换不主动失效(账号掉线重登前 `appstate_logout` 未清 playlists.bin——本次 `logout` 未联动删缓存,**下一账号可能看到上一账号列表直到首次网络成功覆写**;留主会话)。 - **`.part` 7 天回收只在 LRU 扫描(evict/total_bytes)时触发**:无网/不触发 evict 时不会主动清;属「惰性删除」(任务要求)。`.tmp`(在播写穿)mtime 持续更新,安全。 - **未动 sdk/、meow_launcherd/、vendor/**;部署链路同 §13.5 留主会话。commit `cd216fa`(git 记录)。 + +## 30. 真机四连症状排查修复:歌单豆腐/首页方框/LRC 一行/音量连按杀 App(2026-09-02,REPORT §30) + +> 真机(10.78.193.50,设备 `dev.player.ncm`)四连症状,均在 SDK 292/293/295 重链(ncm `b30b0aed`) +> 后出现。本批先取证后定位,逐症状给根因/修法/测试。风控纪律:**0 次真实 NCM 业务请求** +> (curl 打网易云 CDN 封面仅 `GET` 静态图片做网络侧诊断,不算 NCM 接口请求;全部修复验证在 +> 宿主 mock + 离线 sim)。 + +### 30.1 症状①:162 首歌单打开后所有条目歌名显示「三个豆腐」+「加载中」(小歌单 72 快且正常) + +**根因(两个叠加)**: +- **显示层(豆腐的真相)**:行占位符 `pl_src2` 的 `out->title = "···"`(U+00B7 中点 ×3)。设备默认 + 字体 `MeowOS/fonts/default/regular/font.idx` **没有 U+00B7 字形**(`meow_font.c` 缺码位画空心方块 + tofu)→ `···` 渲染成**三个豆腐块**。副标题正是占位行的「加载中」。 + - 证据:把我取回的 `font.idx` 用 `load_idx` 同款解析(MFNI + 每面 glyph 记录 16B)扫全 glyph: + `cp U+002E('.') present / U+00B7('·') MISSING / U+2022('•') MISSING / U+2026('…') present / + U+4E2D(中) present / U+25E2('◢') MISSING`。字体缺 U+00B7 与 ◢◤/Kannada(ಡ) 等字形。 +- **物化层(为何一直占位)**:占位行 = `appstate_song_copy(idx)!=0`(滑窗未覆盖/未物化)。首屏 + `appstate_load_playlist_songs_ctx(...,0,NCM_SONG_BATCH=100)` 应物化 0-99;若该批 song_detail + 网络/解析失败(非致命,§26 fill-and-continue 只记 `g_ens_err` 不置 `g_load_err`)→ 窗口空、 + track_ids_count=162 → **所有行占位**。162>100 走 2 批(首屏 1 批 100 + 二批 62),92/200/5194 已由 + §26 修过但 162 的真实 song_detail 批在真机失败原因待下一会话诊断日志(**本批给 playlist_worker + 首屏打 INFO 日志:idx/plid/trackIds/materialized/rc**,见下)。 + +**修法**: +- `app/ui/playlist.c` 占位 title `"···"`→`"..."`(ASCII 三点)——豆腐消除; + - 全 App 把 U+00B7 分隔符 ` · ` 换成 ASCII ` / `(`playlist/search/home/player/settings/login`), + 因为字体缺 U+00B7,所有 ` · `(歌单副标题、组头「我创建的 · N 个」、播放页 meta、设置副标) + 都会渲染成豆腐,一并消除。 +- `app/ui/playlist.c` `playlist_worker` 首屏完成打 `playlist: first-screen idx=.. plid=.. trackIds=.. + materialized=.. rc=..` 日志,供下一会话定位 162 首屏是否真失败。 + +### 30.2 症状②:从该歌单返回首页,歌单列表约 1 秒全是方框(之后恢复) + +**根因(部分定位)**:同上字体缺字形。首页组头「我创建的 · N 个」用 ` · `(U+00B7)→ 豆腐; +「我收藏的」页签里大量歌单名含特殊字形(`A神◢◤永存`、`(ಡωಡ)`、`【】『』` 等,字体缺 ◢◤/Kannada) +→ 对应行豆腐。「全是方框约 1 秒后恢复」的**瞬态**属性更符合「页面重进时字体 glyph atlas 冷缓存重栅格 +(渲染大量陌生 CJK 歌名后 LRU 淘汰,重进首页需重栅格)」——SDK 内部行为,需设备逐帧观测才能钉死, +本批未再深挖(避免大改 SDK)。 + +**修法**:` · `→` / `(本批已整体替换);特殊字形歌名属字体覆盖缺口,建议后续给 default 字库补 +U+2022/U+25E2 等(不在本批范围)。 + +### 30.3 症状③:LRC 歌词全部挤成一行纯文本,时间戳也没解析(有词歌曲全这样;272 歌词页曾正常) + +**根因**:`core/ncm_json.c` 的 `ncm_json_get_str_at` **只 memcpy 原文、不做 JSON 转义还原**。 +`/api/song/lyric` 的 `lrc.lyric` 把整段 LRC 的换行写成 JSON 转义 `\n`;jsmn 只分词不还原 → +`lrc` 字符串里是**字面 `\`+`n`(无真实换行)** → `lrc_parse`(lyric.c)扫不到行尾 `\n` → 整段歌词 +取成第一行的一个超长正文,只解析出 1 个时间戳 → 「挤成一行纯文本、时间戳没解析」。先前每个带 +JSON 转义字段(`\n`/`\/`/`\uXXXX`)都受影响,只因歌词行数多最显眼。 + +**修法**:`ncm_json.c` 新增 `static json_unescape()`(RFC 8259 §7:`\" \\ \/ \b \f \n \r \t` + `\uXXXX` +UTF-16→UTF-8,含 `\uD800..DBFF+\uDC00..DFFF` 代理对合成),`ncm_json_get_str_at` 原文→还原写入。 +输出恒 ≤ 输入长度(`\uXXXX` 6B→≤4B),out_sz 内必然放得下。`\n`→真实换行后 LRC 逐行切分正常。 +(`lyric.c` 的 `copy_lrc_text` 对服务端真·字面 `\n` 的空格化兜底保留不动,双保险。) + +### 30.4 症状④:状态栏音量刷新仅 1Hz;连按音量键按多了前台 App 被 SIGKILL(闪退) + +**根因(崩溃,实证)**:`patch_v2/meow_launcherd/app_launcher.c` 的「应用内强杀」快捷键 +**VOL_DOWN ×3**(`app_seq_hits>=3 && seq_gap_ms=600`)→ `vol_restore` + `kill(SIGTERM)` + 500ms + +`kill(SIGKILL)`。用户**连按音量下键**(正常调音量)3 下即误触发 → SIGKILL。 +- 证据:设备 launcher 日志 `meow_20260902_012940.log` 第 1022 行 `1061363 kill seq: VOL_DOWN x3 -> + SIGTERM app`;amsd 日志 `1788284842 amsd EXIT dev.player.ncm sig=9 dur=255353 abnormal=1` + (对应 pid 2169,正与本会话 ncm 日志末尾 `resumed` 后被杀吻合)。非 SIGSEGV、dmesg 无 OOM—— + 是被 VOL_DOWN×3 快捷键 SIGTERM→SIGKILL。 +- 设计缘起:设备前置键 GPIO 不通(§18.12)→ 原 `PREVIOUS+VOL_UP` 退出组合退役,改用 VOL_DOWN×3。 + 但框架 App(`framework=1`,如 ncm)有 SDK 左缘横滑返回退出(`app.c` 根页 `meow_ui_request_exit`), + VOL_DOWN×3 与之冲突。 + +**修法**:`app_launcher.c` VOL_DOWN×3 门控到 `!a->framework`(裸 App 保留;框架 App 走左滑返回退出, +连按音量键只调音量)。框架 App 挂死兜底走 `am kill `。其余半「根因」:音量 1Hz 刷新是 launcher +状态栏对 settings.conf 的 1s 节流轮询(262 Bug2 定案),音量本体现在秒切(SDK 即时写 settings.conf+ +CS43198 DAC),仅图标刷新滞后 1s——属 pacing 取舍,未改(改需 firmware + 设计决断)。 + +### 30.5 症状⑤(顺带):封面大量 `fetch failed → give up`(封面加载不出) + +**根因**:`cover.c` 旧 `static g_buf[NCM_COVER_MAX=288KB]` 作为下载缓冲;网易云封面全尺寸实测 +**1.34MB**(`GET` 返回 `200 size=1342045`)> 288KB → `ncm_http` body 超 cap 回 `NCM_ERR_FULL` +→ `decode_cover` 返回 NULL → 退避重试 ×8 放弃。且设备 RAM 仅 **56MB**,不能靠加静态 bss 解决。 + +**修法**:`cover.c` 改**流式写文件**——`ncm_http_download` 的写回调把 body 直接写进 ncm_cache +写者临时文件(`ncm_cache_write` 是 `write(2)` 直写无缓冲),`meow_img_load_file(临时文件)` 解码, +commit 落盘;`save_cover_raw` 改为临时文件→`cover_current.jpg` 文件复制。NCM_COVER_MAX 改语义为 +「下载字节上限 8MB」(非缓冲),静态 bss 归零(设备 RAM 有限)。 + +### 30.6 验证记录(0 次真实 NCM 业务请求) + +| 项 | 方式 | 结果 | +|---|---|---| +| ①/③ unescape | `test_ncm_core.c` 新 `test_json_unescape`(合成 `\n`+`\/`+`\u00e9` JSON) | 宿主 + qemu-mipsel **PASS**:`lrc \n unescaped -> 3 行`、`URL \/ unescaped`、`URL no literal backslash` | +| ①②③ 界面 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 宿主 ncm_host exit=0 dump 正常 | +| ① 歌单链路 | `playlist_race_sim`(72/92/200 竞态防串/批边界/盘缓存) | **PASS** | +| ① 滑窗物化 | `window_sim`(6000 首滚动 0 占位破窗 + 盘缓存回环) | **PASS** | +| ① 搜索 | `search_sim` | **PASS** | +| ① 缓存 | `cache_evict_sim`(LRU/防淘汰/7 天回收 case1-9) | **PASS** | +| ① 播放链 | `player_flow_sim`(P1-P12 含同 idx 重播换档) | **PASS** | +| ① 边下边播 | `stream_sim`(16 用例 + 2 resume,PCM 金标准) | **ALL PASS** | +| SDK 292/293/295 | `test_meow_ui_listvirt` 44 / `dispatch` / `box` 42 / `scroll` 17 / `anim` 32 / `widgets` / `overlay_suppress` / `sdk_input_keys` / `theme` / `vol` / `ime_fix` / `pinyin` / `notif_gesture` / `notif_open` / `sdk_fixes` / `sb_collapse` / `ui_audio_host` / `notify_ipc` / `notify_integration` | 全 **PASS** | +| launcher | `test_vol_ramp_jack` 15 / `test_ams_start` / `test_notif_tf` / `test_settings_brightness` / `test_media` / `test_lock_screen` / `test_notif_panel` | 全 **PASS** | +| mipsel 构建 | `build_ncm.sh` 自有代码 `-Wall -Wextra -Os` | **0 warning**;`text 991942 / data 1624 / bss 1216260`(bss 从 §26 的 1.5MB 回到 1.2MB——封面去静态缓冲后未涨) | +| core mipsel | `build_core.sh` + qemu-mipsel `test_ncm_core_mipsel` | PASS(含新 unescape) | + +### 30.7 md5(本批产物,未部署,留主会话) + +``` +d91cd906162d58dcc05a59913cca1c93 ncm # 新(未部署) +03dfcf7664cc73a93637dc8816a606e1 meow_launcherd # 新(/usr/bin 只读,需固件或 /usr/data 热副本) +b731654a06d5ca5746edea8393f8668f ncm_host # 宿主调试二进制(重链) +7a38194c990c36fcbc003897168a9020 test/test_ncm_core_host # 宿主 core 测试(重编,含 unescape) +``` + +改动文件(本批,仅这些): +``` +修改: + core/ncm_json.c # json_unescape + ncm_json_get_str_at 还原转义(§30.3) + app/ncm_app_internal.h # NCM_COVER_MAX 288KB→8MB 语义改下载上限(非缓冲,§30.5) + app/logic/cover.c # 封面流式写文件(ncm_http_download cb→ncm_cache_write),§30.5 + app/ui/playlist.c # 占位 "···"→"..." + 副标题 " · "→" / " + 首屏诊断日志(§30.1) + app/ui/home.c # 组头 " · "→" / "(§30.2) + app/ui/search.c # 副标题 " · "→" / " + app/ui/settings.c # 设置副标/页脚 " · "→" / " + app/ui/login.c # 登录页 " · "→" / " + app/ui/player.c # 播放 meta " · "→" / " + test/test_ncm_core.c # 新 test_json_unescape(§30.3) + build_ncm_host.sh # SDK_SRC 补 meow_ui_anim/box/scroll(宿主链接 292/295 新源) + ../meow_launcherd/app_launcher.c # VOL_DOWN×3 门控 !a->framework(§30.4) +新建: + (无新文件) +``` + +### 30.8 诚实出入 / 遗留 + +- **真机未部署未验**(留主会话统一部署 ncm `d91cd906`、launcher `03dfcf76`)。真机验收点: + ①162 首歌单点开条目显示歌名(不再三个豆腐/加载中),首页歌单列表无方框;②歌词按行滚动、点行跳 + 转、时间戳高亮;③连按音量键只调音量不闪退;④封面正常显示;⑤大歌单首屏日志 `playlist: + first-screen ... materialized=N rc=0`(N>0,无失败)。 +- **症状①物化层未再钉死**:162 首屏 song_detail 批在真机失败的确切 rc 待新日志(`playlist: + first-screen` 行)复现读取;若 rc 为网络码需要后续加固超时/重试(本批未改,保持 §26 语义)。 +- **症状②「全方框 1s 恢复」**:高度疑为字体 glyph atlas 冷缓存重栅格(SDK 内部),本批未深挖; + 已消除 ` · ` tofu,特殊字形(◢◤/Kannada)建议补 default 字库。 +- **音量 1Hz 刷新**:launcher 状态栏 1s 节流(262 定案)——音量本体秒切、仅图标滞后;改需 firmware + + pacing 设计决断,本批未动。 +- **`parse_url` http→443 潜性缺陷**:`http://` URL 无显式端口时 `url.port` 恒 443(TLS),对 + music.126.net CDN(443 上提供 TLS)恰好可用;语义错误但未阻断,未改(改动有回归风险)。 +- **未动 sdk/、vendor/**;`ncm_host`/`test_ncm_core_host` 为宿主调试产物随批重编。 diff --git a/patch_v2/ncm_app/app/logic/cover.c b/patch_v2/ncm_app/app/logic/cover.c index 7dfbc81..f86d4c8 100644 --- a/patch_v2/ncm_app/app/logic/cover.c +++ b/patch_v2/ncm_app/app/logic/cover.c @@ -1,6 +1,6 @@ /* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。 * - * 内存纪律:下载限幅 NCM_COVER_MAX(~288KB);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。 + * 内存纪律:下载限幅 NCM_COVER_MAX(字节上限);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。 * 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经 * cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。 * 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。 @@ -13,6 +13,12 @@ * 失败写 WARN 日志(排查资产),不再无声死等。 * - 绘制安全:cover_get() 持锁返回、cover_put() 解锁——旧版 UI 绘制期 worker 可 free * 同一 meow_img(use-after-free);调用方须在 cover_get 后配对 cover_put。 + * + * §30 修复(真机封面全加载失败):旧版 `static g_buf[NCM_COVER_MAX(288KB)]` 作为下载缓冲, + * 网易云封面全尺寸实测 ~1.34MB(>288KB)→ ncm_http 回 NCM_ERR_FULL → 下载失败×8 放弃。 + * 且设备 RAM 仅 56MB,不能再涨静态 bss。改为**流式写文件**:ncm_http_download 的写回调直接 + * 把 body 写进 ncm_cache 写者临时文件(write(2) 直写,无 stdio 缓冲),meow_img_load_file 从 + * 临时文件解码,commit 落盘。全程零封面大缓冲、无 bss 上涨;NCM_COVER_MAX 仅作下载字节上限。 */ #include "../ncm_app_internal.h" #include @@ -32,26 +38,6 @@ static volatile int g_gen; /* 每次换入 +1 */ static int g_acked; /* UI 已消费的版本 */ static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */ -static unsigned char g_buf[NCM_COVER_MAX]; /* 单张下载缓冲(常驻) */ - -static meow_img *decode_cover(const char *url, size_t *out_len) -{ - if (!url || !url[0]) return NULL; - ncm_http_req_t req; - memset(&req, 0, sizeof req); - 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.verify_ca = NCM_HTTP_VERIFY_DEFAULT; - req.timeout_ms = 10000; - ncm_http_resp_t resp; - int rc = ncm_http_request(&req, (char *)g_buf, sizeof g_buf, &resp); - if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) return NULL; - if (out_len) *out_len = resp.body_len; - return meow_img_load_mem((const void *)resp.body, (int)resp.body_len); -} - /* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。 * 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件 * `song_cov.jpg`,与 mp3(song_.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */ @@ -62,28 +48,70 @@ static void cover_cache_id(const char *url, char *out, size_t out_sz) snprintf(out, out_sz, "cov%lx", h); } -/* 读整文件到 buf(< cap),成功返回 1,写入 *len。 */ -static int read_file_into_buf(const char *path, unsigned char *buf, size_t cap, size_t *len) +/* 文件存在且非空(封面缓存命中用;streaming 后无需整读进内存)。 */ +static int file_nonempty(const char *path) { - FILE *f = fopen(path, "rb"); - if (!f) return 0; - size_t n = fread(buf, 1, cap, f); - fclose(f); - if (n == 0) return 0; - if (len) *len = n; - return 1; + struct stat st; + return (path && stat(path, &st) == 0 && st.st_size > 0) ? 1 : 0; } -/* 写 cover_current.jpg 供 launcher 媒体卡片读(兼容保留)。调用方须已持有 g_cm - * (早前版本在此内再 lock g_cm → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */ -static void save_cover_raw(const char *url, size_t len) +/* 流式写回调:body 字节直接写进 ncm_cache 写者临时文件(write(2) 直写)。ctx=cover_sink_t*。 */ +typedef struct { ncm_cache_writer_t *w; size_t total; } cover_sink_t; +static int cover_sink_write(void *ctx, const unsigned char *data, size_t len) { - if (!url || len == 0) return; + cover_sink_t *s = (cover_sink_t *)ctx; + if (ncm_cache_write(s->w, data, len) != 0) return -1; + s->total += len; + return 0; +} + +/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。 + * 失败返回 NULL(不 commit,调用方 abort)。写入 *out_len(body 字节数)。 */ +static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len) +{ + ncm_http_req_t req; + memset(&req, 0, sizeof req); + 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.verify_ca = NCM_HTTP_VERIFY_DEFAULT; + req.timeout_ms = 15000; + ncm_http_resp_t resp; + cover_sink_t sink = { w, 0 }; + int rc = ncm_http_download(&req, cover_sink_write, &sink, NCM_COVER_MAX, &resp); + if (rc != NCM_OK || resp.status != 200 || sink.total == 0) return NULL; + if (out_len) *out_len = sink.total; + /* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */ + const char *tmp = ncm_cache_writer_tmp_path(w); + if (!tmp) return NULL; + return meow_img_load_file(tmp); +} + +/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。 + * 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm + * → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */ +static void save_cover_raw(const char *path, size_t len) +{ + if (!path || len == 0) return; if (g_st.cache_dir[0]) { - char path[NCM_CACHE_DIR_MAX + 32]; /* >512:宿主 glibc fortify 对 %s 展开宽证明可放 */ - snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir); - ncm_store_write_file(path, g_buf, len); - ncm_strlcpy(g_cover_path, path, sizeof g_cover_path); + char dst[NCM_CACHE_DIR_MAX + 32]; + snprintf(dst, sizeof dst, "%s/cover_current.jpg", g_st.cache_dir); + /* 文件复制(源=临时文件,目标=cover_current.jpg)。 */ + FILE *in = fopen(path, "rb"); + FILE *out = NULL; + if (in) { + out = fopen(dst, "wb"); + if (out) { + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof buf, in)) > 0) + if (fwrite(buf, 1, n, out) != n) { fclose(out); out = NULL; break; } + if (out) fclose(out); + } + fclose(in); + } + ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path); } } @@ -114,44 +142,51 @@ static void *cover_worker(void *arg) char cpath[512]; if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg", cpath, sizeof cpath) == NCM_OK && - read_file_into_buf(cpath, g_buf, sizeof g_buf, &blen)) { - img = meow_img_load_mem(g_buf, (int)blen); + file_nonempty(cpath)) { + img = meow_img_load_file(cpath); } - /* ② 未命中 → 下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */ + /* ② 未命中 → 流式下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */ if (!img) { - blen = 0; - img = decode_cover(url, &blen); - if (!img) { - /* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等; - * 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */ - fails++; - if (fails >= COVER_RETRY_MAX) { - meow_log(MEOW_LOG_WARN, "cover: give up after %d fails url=%.64s", fails, url); - 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; - } - meow_log(MEOW_LOG_WARN, "cover: fetch failed (%d) url=%.64s retry in %dms", - fails, url, backoff_ms ? backoff_ms : 500); - if (g_app_quit) return NULL; - usleep(backoff_ms ? backoff_ms * 1000 : 500 * 1000); - backoff_ms = backoff_ms ? (backoff_ms * 2 > 4000 ? 4000 : backoff_ms * 2) : 500; - continue; - } if (g_st.ctx.cache.dir[0]) { - /* 写前检查:池大小超限先按 LRU 淘汰(与 mp3 同池;封面约 200KB,先腾空间) */ ncm_cache_evict(&g_st.ctx.cache); ncm_cache_writer_t *w = NULL; if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) { - ncm_cache_write(w, g_buf, blen); - ncm_cache_commit(w); - /* 写后检查:新封面可能把池推过上限 → 再按 LRU 淘汰最旧 */ + blen = 0; + img = download_cover(url, w, &blen); + if (img) { + pthread_mutex_lock(&g_cm); + save_cover_raw(ncm_cache_writer_tmp_path(w), blen); + pthread_mutex_unlock(&g_cm); + if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); } + } else { + ncm_cache_abort(w); + } ncm_cache_evict(&g_st.ctx.cache); + } else { + /* 打开写者失败(罕见):退避重试,不空转 */ + img = NULL; } } } + if (!img) { + /* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等; + * 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */ + fails++; + if (fails >= COVER_RETRY_MAX) { + meow_log(MEOW_LOG_WARN, "cover: give up after %d fails url=%.64s", fails, url); + 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; + } + meow_log(MEOW_LOG_WARN, "cover: fetch failed (%d) url=%.64s retry in %dms", + fails, url, backoff_ms ? backoff_ms : 500); + if (g_app_quit) return NULL; + usleep(backoff_ms ? backoff_ms * 1000 : 500 * 1000); + backoff_ms = backoff_ms ? (backoff_ms * 2 > 4000 ? 4000 : backoff_ms * 2) : 500; + continue; + } backoff_ms = 0; fails = 0; pthread_mutex_lock(&g_cm); @@ -160,7 +195,6 @@ static void *cover_worker(void *arg) if (g_cover_img) meow_img_free(g_cover_img); g_cover_img = img; ncm_strlcpy(g_cover_url, url, sizeof g_cover_url); - save_cover_raw(url, blen); g_gen++; g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */ } else { diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 4a25a27..8da310a 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -62,7 +62,10 @@ #define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100; * 窗口补载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB) */ #define NCM_LYRIC_LINES 600 /* 歌词行上限 */ -#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */ +#define NCM_COVER_MAX 8*1024*1024 /* 封面下载字节上限(REPORT §30:网易云封面全尺寸 ~1~2MB, + * 旧 288KB 静态缓冲恒溢出 → ncm_http NCM_ERR_FULL → 封面下载失败×8放弃。 + * §30 改为流式写文件(ncm_http_download 回调写 cache 临时文件), + * 本值是下载字节上限(非缓冲),不再占用 bss——设备 RAM 仅 56MB。) */ /* ---- 搜索(REPORT §23:独立结果数组 + 单曲/歌单两类页签 + 历史 + 分页)---- */ #define NCM_SEARCH_PAGE 30 /* cloudsearch limit/页(用户定案;offset 累加) */ diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index f36fbbc..ff5b30b 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -134,7 +134,7 @@ static void home_draw(void *ctx) ncm_fill_round(r_group.x, r_group.y, r_group.w, r_group.h, 8, pa->red_ctr); meow_ui_fill_rect(r_group.x, r_group.y + 4, 4, r_group.h - 8, pa->red); char ght[64]; - snprintf(ght, sizeof ght, "%s · %d 个", tab_label(g_tab), tab_count(g_tab)); + snprintf(ght, sizeof ght, "%s / %d 个", tab_label(g_tab), tab_count(g_tab)); meow_ui_text(r_group.x + 16, r_group.y + (r_group.h - th->font_body) / 2, ght, th->font_body, pa->red); meow_ui_text_right(r_group.x + r_group.w - 16, r_group.y + (r_group.h - th->font_body) / 2, diff --git a/patch_v2/ncm_app/app/ui/login.c b/patch_v2/ncm_app/app/ui/login.c index 9539fcc..bc6e316 100644 --- a/patch_v2/ncm_app/app/ui/login.c +++ b/patch_v2/ncm_app/app/ui/login.c @@ -39,7 +39,7 @@ static void login_draw(void *ctx) /* 24px 红点 logo(标题左前) */ ncm_fill_round(W/2 - tw/2 - 36, 112 + 4, 24, 24, 12, pa->red_lit); - meow_ui_text_center(W/2, 160, "扫码登录 · 音樂的力量", 22, pa->on_red_dim); + meow_ui_text_center(W/2, 160, "扫码登录 / 音樂的力量", 22, pa->on_red_dim); /* 弱分隔(hero 与内容衔接) */ meow_ui_fill_rect(0, 264, W, 1, th->outline); @@ -76,7 +76,7 @@ static void login_draw(void *ctx) meow_ui_text_center(W/2, 476, st, 20, scol); /* 底部提示 */ - meow_ui_text_center(W/2, H - 56, "由 MeowOS 驱动 · 仅本地解码", 18, th->dim); + meow_ui_text_center(W/2, H - 56, "由 MeowOS 驱动 / 仅本地解码", 18, th->dim); } /* 重绘 login 内容(QR+状态变化时)。 */ diff --git a/patch_v2/ncm_app/app/ui/player.c b/patch_v2/ncm_app/app/ui/player.c index cbb93c4..d547e3c 100644 --- a/patch_v2/ncm_app/app/ui/player.c +++ b/patch_v2/ncm_app/app/ui/player.c @@ -323,7 +323,7 @@ static void plr_draw_lyrics(void *ctx) ncm_song_t sm; if (cur_song_meta(&sm) == 0) { char mb[300]; - snprintf(mb, sizeof mb, "%s · %s", sm.name, sm.art_name); + snprintf(mb, sizeof mb, "%s / %s", sm.name, sm.art_name); meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT); } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index 25eaaa6..eb2598d 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -72,16 +72,18 @@ static int pl_src2(int idx, meow_list_item *out, void *ctx) if (idx < 0 || idx >= g_st.track_ids_count) return 0; static ncm_song_t s_s; /* 锁内拷贝驻留缓冲(src2 由 UI 线程串行调用,title 指针对当次绘制有效) */ if (appstate_song_copy(idx, &s_s) != 0) { - /* 占位行:滑窗未覆盖/未物化(后台 ensure 完成后自愈)。 */ + /* 占位行:滑窗未覆盖/未物化(后台 ensure 完成后自愈)。 + * REPORT §30:旧 title="···"(U+00B7 中点)在 App 字体缺该字形时渲染成三个 + * 豆腐块(真机「162 首歌单所有条目歌名显示三个豆腐和加载中」)——改 ASCII "..."。 */ out->icon_kind = MEOW_LIST_ICON_NONE; - out->title = "···"; + out->title = "..."; out->subtitle = "加载中"; out->right = MEOW_LIST_RIGHT_ARROW; out->on = 0; out->enabled = 1; return 1; } static char sub[256]; - if (s_s.art_name[0] && s_s.album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s_s.art_name, s_s.album_name); + if (s_s.art_name[0] && s_s.album_name[0]) snprintf(sub, sizeof sub, "%s / %s", s_s.art_name, s_s.album_name); else if (s_s.art_name[0]) snprintf(sub, sizeof sub, "%s", s_s.art_name); else if (s_s.album_name[0]) snprintf(sub, sizeof sub, "%s", s_s.album_name); else snprintf(sub, sizeof sub, "-"); @@ -133,7 +135,7 @@ static void pl_draw(void *ctx) const char *nm = pl ? pl->name : "歌单名"; int nmx = r_cov.x + r_cov.w + 12; char meta[256]; - if (pl) snprintf(meta, sizeof meta, "创建者 · %d 首", pl->track_count); + if (pl) snprintf(meta, sizeof meta, "创建者 / %d 首", pl->track_count); else snprintf(meta, sizeof meta, "曲目 %d 首", g_st.track_ids_count); /* 歌单名(截断到卡片右缘:按 UTF-8 字符合法边界逐字累加测宽)。 */ { @@ -246,6 +248,12 @@ static void *playlist_worker(void *arg) /* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */ if (tidx < 0) rc = NCM_ERR_ARG; else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH); + /* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc, + * 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命 + * g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */ + meow_log(MEOW_LOG_INFO, + "playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d", + tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc); } else { /* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */ rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK; diff --git a/patch_v2/ncm_app/app/ui/search.c b/patch_v2/ncm_app/app/ui/search.c index 0725af4..667162b 100644 --- a/patch_v2/ncm_app/app/ui/search.c +++ b/patch_v2/ncm_app/app/ui/search.c @@ -100,7 +100,7 @@ static int sr_src2(int idx, meow_list_item *out, void *ctx) static ncm_song_t s; if (appstate_search_song(idx, &s) != 0) return 0; static char sub[256]; - if (s.art_name[0] && s.album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s.art_name, s.album_name); + if (s.art_name[0] && s.album_name[0]) snprintf(sub, sizeof sub, "%s / %s", s.art_name, s.album_name); else if (s.art_name[0]) snprintf(sub, sizeof sub, "%s", s.art_name); else snprintf(sub, sizeof sub, "%s", s.album_name); out->icon_kind = MEOW_LIST_ICON_NONE; @@ -157,7 +157,7 @@ static void sr_draw_footer(void *ctx) snprintf(foot, sizeof foot, "%s %d %s%s", g_tab == 0 ? "单曲" : "歌单", n, g_tab == 0 ? "个结果" : "个歌单", - appstate_search_has_more(g_tab) ? " · 加载更多" : ""); + appstate_search_has_more(g_tab) ? " / 加载更多" : ""); ncm_draw_footer(r_cont, foot); } } diff --git a/patch_v2/ncm_app/app/ui/settings.c b/patch_v2/ncm_app/app/ui/settings.c index e0122fc..a5ca44b 100644 --- a/patch_v2/ncm_app/app/ui/settings.c +++ b/patch_v2/ncm_app/app/ui/settings.c @@ -85,7 +85,7 @@ static void settings_draw(void *ctx) const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : ""; int locked = (account_vip() < gate); if (gate_s[0]) - snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) · 需 %s%s", + snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) / 需 %s%s", ncm_level_name(g_st.level), ncm_level_spec(g_st.level), gate_s, locked ? ",将降级" : ""); else @@ -95,10 +95,10 @@ static void settings_draw(void *ctx) /* §22:歌词页背景模式(default=封面模糊置底 / power_save=封面+黑叠加层) */ snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %s", g_st.player_bg_mode == NCM_BG_POWER_SAVE ? "省电模式" : "默认(模糊置底)"); - snprintf(rows[2].sub, sizeof rows[2].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb); + snprintf(rows[2].sub, sizeof rows[2].sub, "当前: %d GB / 上限 100", g_st.cache_limit_gb); snprintf(rows[3].sub, sizeof rows[3].sub, "占用: %s", usetxt); snprintf(rows[4].sub, sizeof rows[4].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出"); - snprintf(rows[5].sub, sizeof rows[5].sub, "%s", "v0.1 · MeowOS"); + snprintf(rows[5].sub, sizeof rows[5].sub, "%s", "v0.1 / MeowOS"); for (int i = 0; i < SETTINGS_ROWS; i++) { int yy = y + i * (g_row_h + 8); @@ -113,7 +113,7 @@ static void settings_draw(void *ctx) /* 品牌红页脚小字。 */ int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4; meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant); - meow_ui_text(x, ny + 24, "网易云音乐 · MeowOS 移植版", 20, pa->red); + meow_ui_text(x, ny + 24, "网易云音乐 / MeowOS 移植版", 20, pa->red); } static int row_at(int x, int y) @@ -174,8 +174,8 @@ int settings_on_tap(int x, int y) * 以文字标注代替——允许选定,实际播放逐级降级到可用档) */ const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL; if (gate_s) - snprintf(lbl, sizeof lbl, "%s (%s) · %s%s", ncm_level_name(lv), - ncm_level_spec(lv), gate_s, (vip < gate) ? "·未解锁" : ""); + snprintf(lbl, sizeof lbl, "%s (%s) / %s%s", ncm_level_name(lv), + ncm_level_spec(lv), gate_s, (vip < gate) ? "/未解锁" : ""); else snprintf(lbl, sizeof lbl, "%s (%s)", ncm_level_name(lv), ncm_level_spec(lv)); meow_sheet_add_item(s, lbl); @@ -254,7 +254,7 @@ int settings_on_tap(int x, int y) return 1; } if (row == 5) { /* 关于 */ - ncm_snack("网易云音乐 v0.1 · MeowOS 移植版"); + ncm_snack("网易云音乐 v0.1 / MeowOS 移植版"); return 1; } return 0; diff --git a/patch_v2/ncm_app/build_ncm_host.sh b/patch_v2/ncm_app/build_ncm_host.sh index 3e7c44d..809300c 100755 --- a/patch_v2/ncm_app/build_ncm_host.sh +++ b/patch_v2/ncm_app/build_ncm_host.sh @@ -20,7 +20,7 @@ echo "==> host SDK objects" SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \ meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \ meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \ - meow_ui_audio" + meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll" for f in $SDK_SRC; do $CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \ "$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; } diff --git a/patch_v2/ncm_app/core/ncm_json.c b/patch_v2/ncm_app/core/ncm_json.c index 8f70ea6..cf1059d 100644 --- a/patch_v2/ncm_app/core/ncm_json.c +++ b/patch_v2/ncm_app/core/ncm_json.c @@ -97,6 +97,83 @@ int ncm_json_arr_len(const ncm_json_t *j, int tok) { return k; } +/* JSON 字符串转义还原(jsmn 只分词、不做 escape 还原)。输入 [s, s+len) 是 token 的 + * 原文串体(去引号,仍含 \" \\ \/ \n \r \t \b \f \uXXXX 等字面转义),还原为实际字符 + * 写入 out,返回写入字节数。语义(RFC 8259 §7): + * \" → " \\ → \ \/ → / \b → 0x08 \f → 0x0C + * \n → 0x0A \r → 0x0D \t → 0x09 + * \uXXXX → UTF-16 码元 → UTF-8;\uD800-\uDBFF + \uDC00-\uDFFF 代理对合成 >U+FFFF 码点。 + * 输出长度恒 ≤ 输入长度(最坏 \uXXXX 6 字节 → ≤4 字节 UTF-8),在 out_sz 内必然放得下。 + * + * 背景(REPORT §30 网易云真机四连症状):/api/song/lyric 返回的 lrc.lyric 把整段 LRC 的 + * 换行写成 JSON 转义 \n。旧实现 memcpy 原文 → 歌词串里是字面反斜杠+n(无真实换行), + * lrc_parse 扫不到行尾 → 整段歌词挤成一行、时间戳全没解析(真机「LRC 挤一行」)。 + * 本函数做真正的 unescape,\n 还原成真实换行,lrc_parse 按行正确切分。 + * 也顺手修好所有走 get_str_at 的 URL/名字里被转义的 \/、\uXXXX(避免字面序列显形)。 */ +static size_t json_unescape(const char *s, size_t len, char *out, size_t out_sz) +{ + size_t oi = 0, si = 0; + while (si < len && oi < out_sz - 1) { + unsigned char c = (unsigned char)s[si]; + if (c != '\\' || si + 1 >= len) { out[oi++] = (char)c; si++; continue; } + char e = s[si + 1]; + switch (e) { + case '"': out[oi++] = '"'; si += 2; break; + case '\\': out[oi++] = '\\'; si += 2; break; + case '/': out[oi++] = '/'; si += 2; break; + case 'b': out[oi++] = '\b'; si += 2; break; + case 'f': out[oi++] = '\f'; si += 2; break; + case 'n': out[oi++] = '\n'; si += 2; break; + case 'r': out[oi++] = '\r'; si += 2; break; + case 't': out[oi++] = '\t'; si += 2; break; + case 'u': { + if (si + 6 > len) { out[oi++] = '\\'; si++; break; } /* 不完整 \u:原样保留 */ + unsigned cp = 0; + int ok = 1; + for (int k = 0; k < 4; k++) { + unsigned char h = (unsigned char)s[si + 2 + k]; + unsigned d; + if (h >= '0' && h <= '9') d = h - '0'; + else if (h >= 'a' && h <= 'f') d = h - 'a' + 10; + else if (h >= 'A' && h <= 'F') d = h - 'A' + 10; + else { ok = 0; break; } + cp = (cp << 4) | d; + } + if (!ok) { out[oi++] = '\\'; si++; break; } /* 非十六进制:宽松保留 */ + si += 6; + /* 高代理对 \uD800..\uDBFF 后紧跟 \uDC00..\uDFFF → 合成一个码点 */ + if (cp >= 0xD800 && cp <= 0xDBFF && si + 6 <= len && + s[si] == '\\' && s[si + 1] == 'u') { + unsigned lo = 0; int ok2 = 1; + for (int k = 0; k < 4; k++) { + unsigned char h = (unsigned char)s[si + 2 + k]; + unsigned d; + if (h >= '0' && h <= '9') d = h - '0'; + else if (h >= 'a' && h <= 'f') d = h - 'a' + 10; + else if (h >= 'A' && h <= 'F') d = h - 'A' + 10; + else { ok2 = 0; break; } + lo = (lo << 4) | d; + } + if (ok2 && lo >= 0xDC00 && lo <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); + si += 6; + } + } + if (cp < 0x80) { out[oi++] = (char)cp; } + else if (cp < 0x800) { out[oi++] = (char)(0xC0 | (cp >> 6)); out[oi++] = (char)(0x80 | (cp & 0x3F)); } + else if (cp < 0x10000) { out[oi++] = (char)(0xE0 | (cp >> 12)); out[oi++] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[oi++] = (char)(0x80 | (cp & 0x3F)); } + else { out[oi++] = (char)(0xF0 | (cp >> 18)); out[oi++] = (char)(0x80 | ((cp >> 12) & 0x3F)); out[oi++] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[oi++] = (char)(0x80 | (cp & 0x3F)); } + break; + } + default: /* 未知转义(宽松):保留反斜杠 + 字面 */ + out[oi++] = '\\'; si++; + break; + } + } + out[oi] = 0; + return oi; +} + const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t out_sz) { if (tok < 0 || tok >= j->ntoks) return NULL; int type = j->toks[tok].type; @@ -110,8 +187,8 @@ const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t if (end < start) return NULL; size_t len = (size_t)(end - start); if (len >= out_sz) return NULL; - memcpy(out, j->text + start, len); - out[len] = 0; + /* 真正还原 JSON 转义(旧实现直接 memcpy,\n/\/ 等字面序列未还原 → 歌词一行/URL 损坏) */ + json_unescape(j->text + start, len, out, out_sz); return out; } diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 01177bf..e0d37bd 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index f9e3629..b9ad0f5 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ diff --git a/patch_v2/ncm_app/test/test_ncm_core.c b/patch_v2/ncm_app/test/test_ncm_core.c index 0402ae3..c86daec 100644 --- a/patch_v2/ncm_app/test/test_ncm_core.c +++ b/patch_v2/ncm_app/test/test_ncm_core.c @@ -346,6 +346,34 @@ static void test_vip_parse(void) { "level gates: free/CVIP/SVIP"); } +/* ---------- §30:JSON 字符串转义还原(ncm_json_get_str_at unescape) ---------- */ +static void test_json_unescape(void) { + printf("== json string unescape(REPORT §30)==\n"); + /* 模拟 /api/song/lyric 的 lrc.lyric:JSON 里换行被转义成 \n(正是「LRC 挤一行」根因) */ + const char *j = + "{\"code\":200,\"lrc\":{\"lyric\":\"[00:01.20]hello\\n[00:03.50]world\\n[00:05.90]caf\\u00e9\"}," + "\"url\":\"http:\\/\\/p1.music.126.net\\/ab\\/c.jpg\"}"; + jsmntok_t toks[128]; + int parent[128]; + ncm_json_t nj; + int n = ncm_json_parse(&nj, j, strlen(j), toks, 128, parent); + CHECK(n > 0, "parse rc=%d", n); + + char lrc[256]; + CHECK(ncm_json_path_str(&nj, "lrc.lyric", lrc, sizeof lrc) != NULL, + "lyric path found"); + /* 断言 \n 还原成真实换行(不再是字面反斜杠+n) */ + CHECK(strcmp(lrc, "[00:01.20]hello\n[00:03.50]world\n[00:05.90]caf\xc3\xa9") == 0, + "lrc \\n unescaped -> 3 行 caf\\u00e9->caf\xc3\xa9 (got: %s)", + lrc); + /* 断言 \/ 还原成 /(URL 不再含字面反斜杠) */ + char url[256]; + CHECK(ncm_json_path_str(&nj, "url", url, sizeof url) != NULL && + strcmp(url, "http://p1.music.126.net/ab/c.jpg") == 0, + "URL \\/ unescaped (got: %s)", url); + CHECK(strchr(url, '\\') == NULL, "URL no literal backslash"); +} + int main(int argc, char **argv) { const char *data_dir = (argc > 1) ? argv[1] : "/tmp/ncm_test"; printf("ncm_core self-test, data_dir=%s\n", data_dir); @@ -362,6 +390,7 @@ int main(int argc, char **argv) { test_network(); test_track_all_parse(); test_vip_parse(); + test_json_unescape(); printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS =="); return g_fail ? 1 : 0; diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index c96f1be..94ea275 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ