fix(ncm §30): 真机四连症状——JSON unescape + 封面流式 + 歌单豆腐/分隔符 tofu + 首屏诊断日志

症状/根因/修法(0 真实 NCM 业务请求;宿主+qemu 全绿):
- ③ LRC 挤一行/时间戳未解析:ncm_json_get_str_at 不做 JSON 转义还原,
  lrc.lyric 的 \n 转义原样进串 → lrc_parse 无真实换行整段一行。修:ncm_json.c 加
  json_unescape(RFC8259 全转义 + \uXXXX/代理对),get_str_at 返回前还原。
- ⑤ 封面全 failed→give up:cover.c 静态 g_buf 288KB < 封面 1.34MB → NCM_ERR_FULL。
  设备 RAM 仅 56MB。修:cover.c 改流式写文件(ncm_http_download cb→ncm_cache_write +
  meow_img_load_file 临时文件),NCM_COVER_MAX 8MB 改下载上限(非缓冲,bss 未涨)。
- ① 162 首歌单所有条目「三个豆腐」+「加载中」:占位符 ···(U+00B7)在 default 字体缺
  该字形 → 3 豆腐(font.idx glyph 扫实证 U+00B7 MISSING)。修:占位 ···→...、全 App
  · 分隔符→/;playlist_worker 首屏打 first-screen 诊断日志。
- ② 返回首页歌单列表 1 秒方框:字体缺 U+00B7/特殊字形(◢◤/Kannada)tofu;瞬态疑 glyph
  atlas 冷缓存(SDK 内部未深挖)。修:·→/。
验证:test_ncm_core(+test_json_unescape, host+qemu) / listvirt44 / dispatch / box42 /
scroll17 / anim32 / 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 + playlist_race_sim / window_sim / search_sim / cache_evict_sim(1-9) /
player_flow_sim(P1-12) / stream_sim(16+2resume) 全 PASS;mipsel -Wall -Wextra 0 warning。
新 md5:ncm d91cd906、ncm_host b731654a、test_ncm_core_host 7a38194c。

(症状④ VOL_DOWN×3 杀 App 的 launcher 源修复 app_launcher.c 门控 !a->framework 已随
 并发 commit 54a6e82 存入 HEAD;launcher 二进制 03dfcf76 留主会话/固件打包。)
注:build_ncm_host.sh 补 SDK 292/295 的 anim/box/scroll 源(宿主链接)。
This commit is contained in:
AskaEth committed 2026-09-02 02:25:37 +08:00
1 parent 54a6e828b7
commit 3397a702aa
15 files changed
+384 -88

No files matched your search

+79 -2
View File
@@ -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;
}