Files
MeowOS-SDK/tuku/test/test_settings_lock_host.c
T
AskaEth cca3906c80 图库: M5 设置页(阅读方向/PIN/主题状态)与启动 PIN 锁屏;阅读方向持久化(评审 P1)
logic/pin(纯 C):加盐迭代哈希(FNV-1a ×1000 轮 + 每次设置新随机盐,16 位 hex)经
meow_config_* 持久化 pin.enabled/pin.hash/pin.salt;提供 set/verify/change/disable 与
4-8 位纯数字校验;enabled=1 但盐/哈希残缺时 fail-closed;落盘半途失败会还原旧盐,不留
「新盐+旧哈希」的双失效状态。

ui/settings(ACT_SETTINGS):MD3 分组表单(meow_listview 容器圆角卡 + 段头标签)——
阅读方向 RTL/LTR 走 mr_reader_set_direction;启动 PIN 开关与修改(开启/修改均两遍
MEOW_IME_NUM 确认);版本与主题状态行(跟随系统 + 当前明暗,随 app.c 主题 listener
整帧重绘)。控件一律在 on_enter 建(栈顶=ACT_SETTINGS),避免 §6.1 owner 绑到
ACT_LIBRARY 的跨页命中泄漏。

ui/lock(ACT_LOCK):lock_view_should_show() 读 logic/pin;启用时启动先入锁屏,验证前
不绘制任何书库内容;「输入 PIN」按钮弹系统数字键盘(MEOW_IME_NUM),错 PIN 就地提示
重试(无惩罚性锁定),正确则 page_pop 回书库;on_event 消费 SWIPE——左缘返回不能绕过
PIN 或退出 App(meow_view_dispatch_event 先于 app.c 的 on_swipe)。

logic/reader(评审 P1 修复,最小持久化):mr_reader_set_direction 成功切换后 best-effort
写 meow_config("reader.dir","rtl"/"ltr"),写失败只打 WARN 不阻断本次切换;
mr_reader_get_direction 首次调用惰性读一次配置恢复(无配置/非法值 → 默认 RTL),之后
纯内存快路径。宿主测试经 -DMR_READER_TEST_HOOKS 的 mr_reader_test_reset() 模拟重启。

测试/构建:test_pin(85 项,PIN 全分支,含单键半途落盘失败);test_settings_reader_persist
(19 项:默认/落盘/重启恢复/坏值/非法值/只读一次/写失败只告警);可选宿主集成
test_settings_lock_host(真实 SDK view/activity/widget + IME 注入,端到端验证错/对 PIN
解锁与设置页两遍开启;SDK 无源码时 SKIP);run_tests.sh 保持 46/46 并纳入上述测试
(reader.c 的配置/日志依赖由 test/settings_reader_stub.c 存根),build_tuku.sh 加入
logic/pin.c。交叉构建静态 MIPS ELF(zig-mipsel)零警告。

安全口径:PIN 防随手翻看,不是安全边界——TF 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
2026-11-04 21:25:17 +08:00

230 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_settings_lock_host.c — 设置页/锁屏与 SDK 骨架的宿主集成测试(可选,需 SDK 源码树)。
*
* 目的:钉住 M3 骨架评审点名的两条坑(M5 桩注释①/②),这两条只有把真实 SDK
* view/activity/widget 链起来才能验证:
* ① 非根页控件必须在 on_enter 建(此时 activity 栈顶=本页)——否则 owner 绑到
* ACT_LIBRARY,跨页命中泄漏;
* ② 锁屏必须在自身 view 的 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c
* 的 on_swipe),否则左缘返回会把 ACT_LOCK page_pop 掉、PIN 被绕过。
* 另外覆盖:设置页回到书库(topbar 返回)、控件跨进出复用不重建、阅读方向经
* mr_reader_set_direction 切换、lock_view_should_show() 随 PIN 状态变化、
* 主题模式切换后整帧重绘不崩,以及**真实 MEOW_IME_NUM 键盘**驱动的两条端到端路径:
* 锁屏错 PIN(留在锁屏)→ 对 PIN(page_pop 回书库)、设置页两遍输入开启 PIN。
*
* 编译:见 test/run_tests.sh —— 它把本测试、settings/lock/pin 源码与 SDK 源码
* (-DMEOW_UI_HOST_TEST,附 test_host_stub.c 打桩 font/global)一起编;SDK 对象清单在脚本里。
* 只发布 meow_ui.a(无 SDK 源码)的交付环境,run_tests.sh 自动 SKIP 本测试。
*/
#include "tuku_internal.h"
#include "logic/pin.h"
#include "meow_ui_internal.h" /* meow_widget_owner(宿主测试专用内部头) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
static int g_fail;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS\n"); } \
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* app.c 的导航上报在测试里不需要网络侧实现。 */
void mr_nav_report(void) {}
static int g_lib_enters;
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
/* 清掉本次测试写入的三个小配置并删临时目录(meow_config 每键一文件)。 */
static void cleanup_cfg(const char *dir)
{
static const char *keys[3] = { "pin.enabled.cfg", "pin.hash.cfg", "pin.salt.cfg" };
char p[512];
for (int i = 0; i < 3; i++) {
snprintf(p, sizeof p, "%s/%s", dir, keys[i]);
unlink(p);
}
rmdir(dir);
}
/* ============================================================ */
/* 数字键盘注入(宿主没有真实触摸):按 MEOW_IME_NUM 键位投递 TAP,关闭键盘后由被测代码校验。
* NUM 布局(sdk/meow_ui_ime.c build_kb):KB_TOP=420;首行 470 是符号行 "-/.:*#+=",
* 数字九键行为 518/566/614(x0=70/190/310,键宽 100),底行(ESC|0|退格+OK)为 662;
* 点 y<420 的背景=关闭键盘。 */
/* ============================================================ */
typedef struct { const char *digits; int passes; } pin_job;
static void key_tap(char d)
{
int row, col;
if (d == '0') { row = 3; col = 1; }
else {
int v = d - '1';
if (v < 0 || v > 8) return;
row = v / 3;
col = v % 3;
}
/* 数字行 y=518/566/614(首行 470 是符号行 "-/.:*#+="),底行 662;列心 120/240/360。 */
meow_input_inject(MEOW_EV_TAP, 70 + col * 120 + 50, 518 + row * 48 + 18);
}
static void *pin_injector(void *arg)
{
pin_job *job = (pin_job *)arg;
usleep(150000); /* 等 text_input 开键盘并 drain 完残留事件 */
for (int p = 0; p < job->passes; p++) {
if (p) usleep(300000); /* 两遍输入之间等第二遍开键盘 */
for (const char *s = job->digits; *s; s++) {
key_tap(*s);
usleep(50000);
}
usleep(50000);
meow_input_inject(MEOW_EV_TAP, 240, 100); /* y<420:关闭键盘(非空→校验) */
}
return NULL;
}
static void lock_try_pin(const char *digits)
{
int sb = meow_ui_statusbar_height();
int cy = sb + (meow_ui_height() - sb) / 2;
pin_job job = { digits, 1 };
pthread_t th;
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
meow_view_dispatch_event(MEOW_EV_TAP, 240, cy + 40); /* 点「输入 PIN」按钮 */
pthread_join(th, NULL);
}
static void settings_tap_pin_row(void){
/* 布局:sb + 56 顶栏 + 6 + 段头34 + 方向行64 + 12 + 段头34 → PIN 列表首行中心 +32 */
int y = meow_ui_statusbar_height() + 56 + 6 + 34 + 64 + 12 + 34 + 32;
pin_job job = { "2468", 2 };
pthread_t th;
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
meow_view_dispatch_event(MEOW_EV_TAP, 200, y);
pthread_join(th, NULL);
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_settings_host_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
meow_view *settings = settings_view_create();
meow_view *lock = lock_view_create();
int sb = meow_ui_statusbar_height();
meow_view_page_push(ACT_LIBRARY);
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "start at library");
/* ---- 设置页:on_enter 建控件 + owner 归属 ---- */
meow_view_page_push(ACT_SETTINGS);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active");
CHECK(settings->nwidgets == 5, "settings built 5 widgets (got %d)", settings->nwidgets);
int owner_ok = 1;
for (int i = 0; i < settings->nwidgets; i++)
if (meow_widget_owner(settings->widgets[i]) != ACT_SETTINGS) owner_ok = 0;
CHECK(owner_ok, "settings widgets owned by ACT_SETTINGS (no cross-page leak)");
/* 布局几何:顶栏贴状态栏、三组列表不越界、页脚在屏内。 */
int r_tb[4] = { 0 }, r_about[4] = { 0 };
meow_widget_rect(settings->widgets[0], r_tb);
meow_widget_rect(settings->widgets[3], r_about);
CHECK(r_tb[1] == sb && r_tb[3] == 56, "topappbar sits below statusbar");
CHECK(r_about[1] + r_about[3] < meow_ui_height() - 60,
"about list leaves room for footer (bottom=%d)", r_about[1] + r_about[3]);
printf(" geometry: topbar y=%d, about list y=%d h=%d (screen %dx%d)\n",
r_tb[1], r_about[1], r_about[3], meow_ui_width(), meow_ui_height());
/* 阅读方向行:段头 34 + 行中心。 */
int dir_y = sb + 56 + 6 + 34 + 32;
int d0 = mr_reader_get_direction();
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 120, dir_y) == 1,
"tap on direction row consumed by settings on_event");
CHECK(mr_reader_get_direction() != d0,
"direction toggled through mr_reader_set_direction");
/* topbar 返回 → page_pop 回书库 */
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 22, sb + 28) == 1, "back tap consumed");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back popped to library");
CHECK(g_lib_enters == 2, "library re-entered after pop (enters=%d)", g_lib_enters);
/* 再次进入:控件复用(不在 on_enter 重复创建/泄漏) */
meow_view_page_push(ACT_SETTINGS);
CHECK(settings->nwidgets == 5, "settings widgets reused (got %d)", settings->nwidgets);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active again");
/* 设置页不消费 SWIPE:交给 app.c 的 page_pop */
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 200) == 0,
"settings leaves SWIPE to app.c (back = pop)");
meow_view_page_pop(); /* 手动模拟 app.c 的返回行为,回到书库 */
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "manual pop back to library");
/* ---- 锁屏:PIN 状态驱动 + SWIPE 拦截(坑②) ---- */
mr_pin_disable();
CHECK(lock_view_should_show() == 0, "lock hidden when PIN disabled");
CHECK(mr_pin_set("1357") == MR_PIN_OK, "enable PIN");
CHECK(lock_view_should_show() == 1, "lock shown when PIN enabled");
meow_view_page_push(ACT_LOCK);
CHECK(meow_ui_activity_active() == ACT_LOCK, "lock active");
CHECK(lock->nwidgets == 1, "lock built its button on enter (got %d)", lock->nwidgets);
CHECK(meow_widget_owner(lock->widgets[0]) == ACT_LOCK, "lock button owned by ACT_LOCK");
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 240) == 1,
"lock consumes SWIPE (left-edge back cannot bypass PIN)");
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after swipe");
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 5, 400) == 1, "lock consumes stray taps");
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after stray tap");
CHECK(mr_pin_verify("1357") == 1, "stored PIN verifies");
CHECK(mr_pin_verify("1358") == 0, "wrong PIN rejected");
/* ---- 锁屏真解锁路径:点按钮 → MEOW_IME_NUM 输入 → 校验 → pop(错误则留在锁屏) ---- */
lock_try_pin("1358"); /* 错 PIN:留在锁屏 */
CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)");
lock_try_pin("1357"); /* 正确:page_pop 回书库 */
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "correct PIN pops lock to library");
/* ---- 设置页两遍输入开启 PIN(MEOW_IME_NUM) ---- */
mr_pin_disable();
meow_view_page_push(ACT_SETTINGS);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active for PIN enable");
settings_tap_pin_row();
CHECK(mr_pin_enabled() == 1, "two-pass entry enabled the PIN");
CHECK(mr_pin_verify("2468") == 1, "PIN entered in settings verifies");
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back from settings to library");
/* 主题切换:app.c listener 整帧 invalidate;页面按 meow_theme_get() 重画不崩。 */
meow_theme_set_mode(MEOW_THEME_LIGHT);
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
meow_theme_set_mode(MEOW_THEME_DARK);
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
CHECK(1, "theme switch repaint does not crash");
mr_pin_disable();
meow_view_free(lock);
meow_view_free(settings);
meow_view_free(lib);
meow_ui_shutdown();
cleanup_cfg(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "integration checks passed");
return g_fail ? 1 : 0;
}