diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c index dd1951d..2658e4a 100644 --- a/patch_v2/meow_launcherd/app_launcher.c +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -3,6 +3,7 @@ #include "meow_launcher_internal.h" #include "meow_ams_start.h" /* MSG_AMS_START pending/拉起决策(meow_ams_start.c) */ #include "overlay_gate.h" /* SIGSTOP 叠加会话关闭顺序/死亡回退纯决策(§310 三连 bug 收敛点) */ +#include "meow_utf8.h" /* UTF-8 整序列有界拷贝(显示名不劈多字节字符,问题2) */ static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */ @@ -11,6 +12,16 @@ int g_naccts = 0; volatile int g_app_session_active = 0; /* §10.5:1=launch_app waitpid 前台会话中(util.c 读) */ +/* 显示名 UTF-8 整序列有界拷贝(dst 容量 cap 含 NUL)。 + * M6.3「name[49]+json_str 整序列」只把 json 读入局部 name[49] 与 a->name[49] 放大了,**漏改 + * strncpy(a->name, name, 16)**——仍把显示名硬截 16 字节,UTF-8 下「流媒体客户端」(18B) 变 + * 「流媒体客户」+ 半个「端」(16B = 5 汉字 + 首字节),真机主界面应用名截成 5 字(用户 M6.3 实测)。 + * 本助手(= meow_utf8_copy_bounded)snprintf 到 cap 后回退掉被截断的半截多字节字符。 */ +static void app_name_copy(char *dst, int cap, const char *src) +{ + meow_utf8_copy_bounded(dst, cap, src); +} + /* ---- amsd 上报 / 控制挂点(AMSD_DESIGN §4):best-effort,amsd 不在不影响 launcher ---- */ #define MEOW_AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock" @@ -309,7 +320,7 @@ static void meow_acct_sync(const char *app_id, const struct app *ap) a->background = ap->background; /* M4 */ a->background_audio = ap->background_audio; strncpy(a->icon, ap->icon, 128); - strncpy(a->name, ap->name, 16); + app_name_copy(a->name, (int)sizeof a->name, ap->name); strncpy(a->exec, ap->exec, 128); a->ok = ap->ok; a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute; @@ -813,7 +824,7 @@ void meow_parse_app_json(FILE *f, struct app *a) } } } - strncpy(a->name, name, 16); + app_name_copy(a->name, (int)sizeof a->name, name); strncpy(a->exec, exec, 128); strncpy(a->package, package, sizeof a->package - 1); a->framework = fram ? '1' : 0; diff --git a/patch_v2/meow_launcherd/lock_screen_ui.c b/patch_v2/meow_launcherd/lock_screen_ui.c index 4484968..24daef5 100644 --- a/patch_v2/meow_launcherd/lock_screen_ui.c +++ b/patch_v2/meow_launcherd/lock_screen_ui.c @@ -126,6 +126,17 @@ int run_lock_screen(void) meow_touch mt; mt_reset(&mt); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + /* §310 输入穿透(媒体保活 App 不被 SIGSTOP,实时读 evdev):锁屏页独占触控设备—— + * EVIOCGRAB 使本 fd 之外的其它进程(保活 App 的 SDK 输入线程 / watchdog 手势)在该设备 + * 上收不到事件(事件仅投递抓取者),且抓取期事件不会为未抓取 fd 缓冲/回放。由此无论 App + * 冻结与否,锁屏期**任何** App 都收不到触摸(真全模态)。失败(驱动不支持)则记日志降级, + * 仍靠 keep_running/SIGSTOP+标记隔离。关闭 tfd 时抓取自动解除(仍显式释放一次)。 */ + if (tfd >= 0) { + int grab = 1; + if (ioctl(tfd, EVIOCGRAB, &grab) < 0) + logmsg("lock: EVIOCGRAB touch fail: %s (graceful degrade, marker+SIGSTOP isola only)", + strerror(errno)); + } struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); @@ -272,13 +283,21 @@ int run_lock_screen(void) usleep(POLL_MS * 1000); } done: - if (tfd >= 0) close(tfd); + if (tfd >= 0) { + int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 也会自动解除,双保险) */ + ioctl(tfd, EVIOCGRAB, &grab); + close(tfd); + } kp_close(&kp_power); kp_close(&kp_keys); logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on); return unlocked; died: - if (tfd >= 0) close(tfd); + if (tfd >= 0) { + int grab = 0; + ioctl(tfd, EVIOCGRAB, &grab); + close(tfd); + } kp_close(&kp_power); kp_close(&kp_keys); logmsg("lock: exit overlay-app-died -> LCK_OVERLAY_RET_DIED"); @@ -316,6 +335,13 @@ int lck_input_pin_modal(const char *title, char *out, int outcap) fb_pan_buf0_force(); meow_touch mt; mt_reset(&mt); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + /* §310 输入穿透:设置页 PIN 模态同样独占触控设备(后台保活 App 不冻结实时读 evdev,防其 + * 同坐标控件被 PIN 键盘点击双重触发)。失败降级(仍靠标记+SIGSTOP 隔离)。 */ + if (tfd >= 0) { + int grab = 1; + if (ioctl(tfd, EVIOCGRAB, &grab) < 0) + logmsg("pin-modal: EVIOCGRAB touch fail: %s (graceful degrade)", strerror(errno)); + } struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); @@ -412,7 +438,11 @@ int lck_input_pin_modal(const char *title, char *out, int outcap) usleep(POLL_MS * 1000); } done2: - if (tfd >= 0) close(tfd); + if (tfd >= 0) { + int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 亦自动解除,双保险) */ + ioctl(tfd, EVIOCGRAB, &grab); + close(tfd); + } kp_close(&kp_power); kp_close(&kp_keys); return result; diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index 003a8fb..d51de46 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -2,6 +2,7 @@ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" #include "meow_ams_start.h" /* MSG_AMS_START 拉起决策(meow_ams_start.c):run_menu 消费用 */ +#include "meow_utf8.h" /* UTF-8 整序列字符步进(menu_name_fit 截断不劈多字节字符,问题2) */ /* set by run_menu: the player's panel was blanked (screen timeout) when the @@ -393,20 +394,41 @@ static void menu_first_glyph(const char *nm, char out[4]) } } -/* 名称截断(按可用像素宽;超出加 '…')。 */ +/* 名称截断(按可用像素宽):UTF-8 整序列 + 「…」收尾,总宽 ≤ maxw。 + * 旧实现:①按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停在一个续字节上)→ 停在 + * 半截多字节字符,显示成半字/方框;②先截满再加 "..." 未为省略号留宽 → 追加后溢出 maxw 被裁切。 + * 修法:按字符起始字节前进,绝不劈开多字节字符;预留省略号宽度(mfont_ready 用「…」/回退点阵用 + * "..."),截断后总宽 ≤ maxw。 */ static void menu_name_fit(const char *name, int maxw, int size, char *out, int olen) { - char buf[64]; - snprintf(buf, sizeof buf, "%s", name); - if (ui_width(buf, size) <= maxw) { snprintf(out, olen, "%s", buf); return; } - int n = (int)strlen(buf); - while (n > 0 && ui_width(buf, size) > maxw) { - /* 回退一个 UTF-8 字符边界 */ - do { buf[--n] = 0; } while (n > 0 && ((unsigned char)buf[n] & 0xC0) == 0x80); + if (!name) name = ""; + if (maxw < 0) maxw = 0; + const char *ell = mfont_ready ? "…" : "..."; + int ell_w = ui_width(ell, size); + int budget = maxw - ell_w; + if (budget < 0) budget = 0; + if (ui_width(name, size) <= budget) { snprintf(out, olen, "%s", name); return; } + /* 整字符前进(meow_utf8_char_len 定步长):cut 恒停在一个完整字符之后,绝不劈开多字节字符; + * 连同下一整字符会超预算即停。 */ + size_t stop = strlen(name); + if (stop > (size_t)olen - 1) stop = (size_t)olen - 1; + size_t cut = 0; + while (cut < stop && name[cut]) { + int step = meow_utf8_char_len((unsigned char)name[cut]); + size_t clen = (cut + (size_t)step <= stop) ? (size_t)step : (stop - cut); + if (clen == 0) break; + char tmp[64]; + if (clen >= sizeof tmp) clen = sizeof tmp - 1; + memcpy(tmp, name + cut, clen); + tmp[clen] = 0; + if (ui_width(tmp, size) > budget) break; + cut += clen; } - snprintf(out, olen, "%s...", buf); + if (cut == 0) { snprintf(out, olen, "%s", ell); return; } + snprintf(out, olen, "%.*s%s", (int)cut, name, ell); } + /* 图标绘制:primary 圆角垫块始终垫底(MD3 图标 tile),首字母叠加其上,最后有图叠加 * meow_ui_img_cover(居中裁方)。 * 修复:原实现 `if (icon) { meow_ui_img_cover; return; }` 在图标为透明背景 logo(不透明像素 diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 9ca1a98..97e7445 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_utf8.h b/patch_v2/meow_launcherd/meow_utf8.h new file mode 100644 index 0000000..868a5d6 --- /dev/null +++ b/patch_v2/meow_launcherd/meow_utf8.h @@ -0,0 +1,57 @@ +/* meow_utf8.h — UTF-8 整序列有界拷贝/字符步进纯决策(宿主可单测;无 ui_width/mfont 依赖)。 + * + * 背景(问题2,M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。两处根因: + * ① app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下=5 汉字+ 半个 + * 「端」)——M6.3 只扩了 name[49]+json_str 整序列,漏改此处; + * ② main_menu.c `menu_name_fit` 按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停续字节), + * 停在半截多字节字符;再加 "..." 未为省略号留宽 → 溢出 maxw 被裁切。 + * 本头把 UTF-8 整序列处理抽成纯函数(meow_utf8_char_len / meow_utf8_copy_bounded / 截断决策), + * 两处复用并可宿主单测(test_utf8_name.c)。 + */ +#ifndef MEOW_UTF8_H +#define MEOW_UTF8_H + +#include +#include /* snprintf(有界拷贝用) */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* 由首字节 c0 求整字符 UTF-8 字节长(1..4)。非法首字节(0x80..0xBF 续字节)返回 1(按异常字节跳过, + * 绝不把孤立续字节与后字并成大字符)。纯按前导位,不看后续字节是否齐全(那由 copy_bounded 处理)。 */ +static inline int meow_utf8_char_len(unsigned char c0) +{ + if (c0 < 0x80) return 1; + if (c0 < 0xC0) return 1; /* 孤立续字节:兜底 1 */ + if (c0 < 0xE0) return 2; + if (c0 < 0xF0) return 3; + return 4; +} + +/* 有界拷贝:把 src 拷入 dst(容量 cap,含 NUL),**绝不劈开多字节字符**。snprintf 截断若落在字符 + * 中间(或 src 本身尾部是半截字符),回退到整字符边界:删去最末不完整字符。返回拷贝后字节长(不含 + * NUL)。源可为 NULL。持久化/展示名均用——保证 UTF-8 整序列,杜绝「5 汉字+半个字」式残字。 */ +static inline int meow_utf8_copy_bounded(char *dst, int cap, const char *src) +{ + if (!dst || cap <= 0) return 0; + if (cap == 1) { dst[0] = 0; return 0; } + if (!src) src = ""; + snprintf(dst, (size_t)cap, "%s", src); + int n = (int)strlen(dst); + if (n <= 0) return 0; + /* 定位最末一个「起始字节」(非续字节):从尾部回退续字节。 */ + int last_start = n - 1; + while (last_start > 0 && ((unsigned char)dst[last_start] & 0xC0) == 0x80) + last_start--; + /* 末字符:起始字节=last_start,实际字节数=have;字符应需 need。have 200, "S1 drain window > SDK 200ms throttle"); + /* ---- S1b 未冻结(媒体保活)App 输入被排:靠 EVIOCGRAB 独占隔离,非 SIGCONT 排空 ---- + * 保活 App 从不 SIGSTOP(保 PCM),SDK 输入线程实时读 evdev;launcher 只能靠 EVIOCGRAB 独占 + * 触控设备(真机 goodix 已证 EVIOCGRAB 返回 0)使任何前台进程锁屏期收不到触摸。 */ + CHECK(overlay_keepalive_needs_grab(1) == 1, "S1b keep-alive app needs EVIOCGRAB isolation"); + CHECK(overlay_keepalive_needs_grab(0) == 0, "S1b frozen app -> SIGCONT drain, not grab"); + /* 保活 App 隔离与冻结 App 排空互斥:一条路径而非两条。 */ + CHECK(overlay_drain_required(1) == 0 && overlay_keepalive_needs_grab(1) == 1, + "S1b keep-alive: grab path (drain==0)"); + CHECK(overlay_drain_required(0) == 1 && overlay_keepalive_needs_grab(0) == 0, + "S1b frozen: drain path (grab==0)"); + /* ---- S2 解锁状态栏恢复:必须强制重同步 ---- */ CHECK(overlay_force_sb_resync() == 1, "S2 force status bar re-sync after unlock"); diff --git a/patch_v2/meow_launcherd/test_utf8_name.c b/patch_v2/meow_launcherd/test_utf8_name.c new file mode 100644 index 0000000..68b9007 --- /dev/null +++ b/patch_v2/meow_launcherd/test_utf8_name.c @@ -0,0 +1,65 @@ +/* test_utf8_name.c — 问题2(主界面应用名截断)宿主单测:UTF-8 整序列有界拷贝 / 字符步进。 + * + * 背景(M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。真因 = app_launcher.c + * `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下 16B = 5 汉字 + 「端」首字节),且 + * main_menu.c `menu_name_fit` 按字节回退停在半截字符、加 "..." 又溢出 maxw。本测试验证 meow_utf8.h + * 的整序列有界拷贝:绝不劈开多字节字符、给出完整汉字序列(修复 strncpy(16) 与 menu_name_fit)。 + * + * 编译运行(在 patch_v2/meow_launcherd 下): + * gcc -Wall -Wextra -o test_utf8_name test_utf8_name.c && ./test_utf8_name + * 全 PASS -> exit 0。 + */ +#include +#include +#include "meow_utf8.h" + +static int g_pass = 0, g_fail = 0; +#define CHECK(cond, msg) do { \ + if (cond) { g_pass++; printf("PASS %s\n", msg); } \ + else { g_fail++; printf("FAIL %s\n", msg); } \ +} while (0) + +int main(void) +{ + /* meow_utf8_char_len:由首字节求整字符长。 */ + CHECK(meow_utf8_char_len('A') == 1, "char_len ASCII = 1"); + CHECK(meow_utf8_char_len((unsigned char)0xC3) == 2, "char_len 2-byte lead = 2"); + CHECK(meow_utf8_char_len((unsigned char)0xE6) == 3, "char_len 3-byte lead = 3"); + CHECK(meow_utf8_char_len((unsigned char)0xF0) == 4, "char_len 4-byte lead = 4"); + CHECK(meow_utf8_char_len((unsigned char)0x80) == 1, "char_len lone continuation = 1 (no merge)"); + + /* meow_utf8_copy_bounded:不劈多字节字符。 */ + char buf[64]; + /* 源「流媒体客户端」= 18B(6×3B)。cap=49 → 全量(修复 strncpy(16) 后的正确行为)。 */ + meow_utf8_copy_bounded(buf, (int)sizeof buf, "流媒体客户端"); + CHECK(strcmp(buf, "流媒体客户端") == 0, "copy cap=49 keeps full '流媒体客户端'"); + CHECK(strlen(buf) == 18, "copy cap=49 len=18 (6 chars)"); + + /* cap=16(旧 bug 的容量):旧 strncpy(16) 给 5 汉字+半个字+无NUL;现应给 5 整汉字+ NUL。 */ + meow_utf8_copy_bounded(buf, 16, "流媒体客户端"); + CHECK(strcmp(buf, "流媒体客户") == 0, "copy cap=16 -> 5 whole chars '流媒体客户'"); + CHECK(strlen(buf) == 15, "copy cap=16 -> 15 bytes (5 CJK, no partial 6th)"); + + /* cap=17:尾部「端」首字节越界 → 整字符去掉,仍 5 整汉字,绝不半字残形。 */ + meow_utf8_copy_bounded(buf, 17, "流媒体客户端"); + CHECK(strcmp(buf, "流媒体客户") == 0, "copy cap=17 -> still 5 whole chars (no partial)"); + + /* cap=12:缓冲区 12B 含 NUL → 内容上限 11B,4 整汉字(12B)放不下 → 截到 3 整汉字(绝非半字)。 + * 注:snprintf 语义 = cap 字节含 NUL,故内容 ≤ cap-1(旧 strncpy(dst,src,12) 拷 12B 无 NUL, + * 有越界读风险;此处改为安全 NUL 结尾,代价是内容少 1B——正确取舍)。 */ + meow_utf8_copy_bounded(buf, 12, "流媒体客户端"); + CHECK(strcmp(buf, "流媒体") == 0, "copy cap=12 -> 3 whole chars '流媒体' (no partial 4th)"); + + /* ASCII / 混合:不劈 ASCII。 */ + meow_utf8_copy_bounded(buf, (int)sizeof buf, "Hello App"); + CHECK(strcmp(buf, "Hello App") == 0, "copy ASCII unchanged"); + + /* NULL/空源安全性。 */ + meow_utf8_copy_bounded(buf, (int)sizeof buf, NULL); + CHECK(buf[0] == 0, "copy NULL src -> empty"); + meow_utf8_copy_bounded(buf, 1, "流媒体客户端"); + CHECK(buf[0] == 0, "copy cap=1 -> empty (no write overflow)"); + + printf("\n%s (%d passed, %d failed)\n", g_fail ? "SOME TESTS FAILED" : "ALL PASS", g_pass, g_fail); + return g_fail ? 1 : 0; +}