feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+136
View File
@@ -571,6 +571,142 @@ int api_files_delete(int sock, const req_t *r)
return send_json(sock, 200, "{\"removed\":true}");
}
/* ---- F1-④ 文件搜索(FUTURE_MEOWPANEL §2.3,只读):白名单内 readdir 递归(限深 4 / 限量 200 /
* 限遍历 8000 条),不 run_cmd("find")(防注入、可控)。name=名子串(ASCII 大小写不敏感);
* type=file|dir 可选过滤。不跟符号链接(防循环/逃逸出白名单根)。 ---- */
#define SEARCH_MAX_DEPTH 4
#define SEARCH_MAX_ITEMS 200
#define SEARCH_MAX_VISIT 8000
typedef struct {
const char *needle; /* 已小写化的匹配子串 */
int want_type; /* 0=不限 1=仅文件 2=仅目录 */
int max;
int count;
long visited;
sbuf_t *s;
} search_ctx_t;
static void name_lower(const char *in, char *out, size_t cap)
{
size_t i = 0;
for (; in[i] && i + 1 < cap; i++) {
unsigned char c = (unsigned char)in[i];
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : (char)c;
}
out[i] = 0;
}
/* 返回 1=到达上限应全程停止(truncated)。 */
static int search_walk(search_ctx_t *c, const char *dir, int depth)
{
if (c->count >= c->max || c->visited >= SEARCH_MAX_VISIT) return 1;
DIR *d = opendir(dir);
if (!d) return 0;
int stop = 0;
struct dirent *e;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
if (++c->visited > SEARCH_MAX_VISIT) { stop = 1; break; }
char full[1400];
snprintf(full, sizeof full, "%s/%s", dir, e->d_name);
struct stat st;
if (lstat(full, &st) < 0) continue;
if (S_ISLNK(st.st_mode)) continue; /* 不跟符号链接 */
int isdir = S_ISDIR(st.st_mode);
int type_ok = !c->want_type || (c->want_type == 1 && !isdir) || (c->want_type == 2 && isdir);
if (type_ok) {
char low[300];
name_lower(e->d_name, low, sizeof low);
if (strstr(low, c->needle)) {
if (c->count >= c->max) { stop = 1; break; }
char rel[1200];
make_rel(full, rel, sizeof rel);
if (c->count) sA(c->s, ",");
sA(c->s, "{\"name\":");
sA_str(c->s, e->d_name);
sA(c->s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld,\"rel\":",
isdir ? "dir" : "file", isdir ? 0LL : (long long)st.st_size, (long long)st.st_mtime);
sA_str(c->s, rel);
sA(c->s, "}");
c->count++;
}
}
if (isdir && depth < SEARCH_MAX_DEPTH && search_walk(c, full, depth + 1)) { stop = 1; break; }
}
closedir(d);
return stop;
}
/* 0=OK(JSON 入 out);-1 白名单外;-2 路径不存在/非目录;-3 name 空。 */
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out)
{
if (!path_in_whitelist(abs_start)) return -1;
struct stat st;
if (stat(abs_start, &st) < 0 || !S_ISDIR(st.st_mode)) return -2;
char needle[300];
name_lower(name ? name : "", needle, sizeof needle);
if (!needle[0]) return -3;
int want_type = 0;
if (typef && !strcmp(typef, "file")) want_type = 1;
else if (typef && !strcmp(typef, "dir")) want_type = 2;
else if (typef && typef[0]) return -4; /* 非法 type 值(api 层映射 422) */
if (max <= 0 || max > SEARCH_MAX_ITEMS) max = SEARCH_MAX_ITEMS;
search_ctx_t c;
memset(&c, 0, sizeof c);
c.needle = needle; c.want_type = want_type; c.max = max; c.s = out;
char rel[1200];
make_rel(abs_start, rel, sizeof rel);
sA(out, "{\"path\":");
sA_str(out, abs_start);
sA(out, ",\"rel\":");
sA_str(out, rel);
sA(out, ",\"name\":");
sA_str(out, name);
sA(out, ",\"items\":[");
int stopped = search_walk(&c, abs_start, 1);
sA(out, "],\"count\":%d,\"truncated\":%s}", c.count,
(stopped || c.visited >= SEARCH_MAX_VISIT) ? "true" : "false");
return 0;
}
int api_files_search(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024] = "/";
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
char name[512] = "";
if (!qparam(r->query, "name", name, sizeof name) || !name[0])
return send_err(sock, 400, "BAD_REQUEST", "缺 name(按名子串搜索)");
char typef[16] = "";
qparam(r->query, "type", typef, sizeof typef);
char maxs[16] = "";
int max = SEARCH_MAX_ITEMS;
if (qparam(r->query, "max", maxs, sizeof maxs)) { int m = atoi(maxs); if (m > 0) max = m; }
char abs[1024];
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs))
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
sbuf_t s = {0};
int rc = files_search_json(abs, name, typef, max, &s);
if (rc == -1) { sbuf_free(&s); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
if (rc == -2) { sbuf_free(&s); return send_err(sock, 404, "NOT_FOUND", "路径不存在或非目录"); }
if (rc == -4) { sbuf_free(&s); return send_err(sock, 422, "UNSUPPORTED", "type 须为 file|dir"); }
if (rc != 0) { sbuf_free(&s); return send_err(sock, 400, "BAD_REQUEST", "name 为空"); }
/* 放行前自校验(与 overview 同口径:括号平衡/内嵌 NUL/坏字节,浏览器 JSON.parse 不容忍) */
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
if (chk) jv_free(chk);
if (bad) {
panel_log("files/search: SELF-CHECK fail (n=%zu)", s.n);
sbuf_free(&s);
return send_err(sock, 500, "INTERNAL", "搜索结果输出未通过 JSON 自校验");
}
int out = send_json(sock, 200, s.b);
sbuf_free(&s);
return out;
}
int multipart_extract_file(const req_t *r, const char *outpath)
{
const char *b = strstr(r->content_type, "boundary=");