Merge branch 'feat/mangareader-settings-and-pin-lock'

This commit is contained in:
AskaEth committed 2026-11-04 21:32:17 +08:00
commit 8cbb240931
12 files changed
+1749 -60

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \ "$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \ app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c \ logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c logic/pin.c \
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser "$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
ls -la picbrowser ls -la picbrowser
file picbrowser file picbrowser
+211
View File
@@ -0,0 +1,211 @@
/* logic/pin.c — APP 启动 PIN:加盐迭代哈希 + meow_config 持久化(纯 C)。
*
* 设计取舍(见 pin.h 头注释):PIN 防随手翻看,不是安全边界。本文件不引入加密库,
* 只做「每次设置新随机盐 + FNV-1a 迭代 1000 轮」的轻量哈希,避免明文 PIN 直接躺在
* 配置里。配置落 TF 数据目录,卡被拔走即可读/改——这是产品口径接受的局限。
*
* 依赖面:只 include include/meow_ui_config.h(get/set/remove/int 声明),不 include
* meow_ui.h、不碰 UI,宿主 gcc 可直接编译 + 测试存根替换配置层。
*/
#include "pin.h"
#include "include/meow_ui_config.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define PIN_KEY_ENABLED "pin.enabled"
#define PIN_KEY_HASH "pin.hash"
#define PIN_KEY_SALT "pin.salt"
#define PIN_HEX_LEN 16 /* 64-bit 摘要/盐的 hex 字符数 */
#define PIN_HEX_BUF 17 /* 16 + NUL */
#define PIN_ROUNDS 1000 /* 迭代轮数(防直接反查,不防暴力枚举) */
#define PIN_SALT_BYTES 8 /* 8 字节盐 → 16 位 hex */
/* ---------- 哈希 ---------- */
/* FNV-1a(64-bit)逐段吸收,返回新状态。 */
static uint64_t fnv1a_update(uint64_t h, const void *data, size_t n)
{
const unsigned char *p = (const unsigned char *)data;
for (size_t i = 0; i < n; i++) {
h ^= (uint64_t)p[i];
h *= 1099511628211ULL;
}
return h;
}
/* pin → 16 位 hex 摘要:FNV-1a(salt, pin) 起手,再做 PIN_ROUNDS 轮
* 「上一轮摘要(大端 8 字节)+ 盐」的 FNV-1a。同一 PIN 每次设置因盐不同而摘要不同。 */
static void pin_digest(const char *pin, const char *salt, char out[PIN_HEX_BUF])
{
uint64_t h = 1469598103934665603ULL; /* FNV-1a 64-bit offset basis */
h = fnv1a_update(h, salt, strlen(salt));
h = fnv1a_update(h, pin, strlen(pin));
for (int r = 0; r < PIN_ROUNDS; r++) {
unsigned char be[8];
for (int i = 0; i < 8; i++)
be[i] = (unsigned char)((h >> (8 * (7 - i))) & 0xFF);
h = fnv1a_update(h, be, sizeof be);
h = fnv1a_update(h, salt, strlen(salt));
}
snprintf(out, PIN_HEX_BUF, "%016llx", (unsigned long long)h);
}
/* 生成 16 位 hex 随机盐:优先 /dev/urandom;无设备时用时间/pid/地址/计数器做降级熵
* (宿主测试与裁剪系统仍可用——降级盐的强度如实标注:不构成密码学随机)。 */
static void pin_gen_salt(char out[PIN_HEX_BUF])
{
unsigned char b[PIN_SALT_BYTES];
int ok = 0;
FILE *f = fopen("/dev/urandom", "rb");
if (f) {
ok = (fread(b, 1, sizeof b, f) == sizeof b);
fclose(f);
}
if (!ok) {
static uint64_t ctr;
uint64_t x = (uint64_t)time(NULL)
^ ((uint64_t)clock() << 21)
^ ((uint64_t)getpid() << 40)
^ (uint64_t)(uintptr_t)&b
^ (++ctr * 0x9E3779B97F4A7C15ULL);
for (int i = 0; i < PIN_SALT_BYTES; i++)
b[i] = (unsigned char)((x >> (8 * i)) & 0xFF);
}
for (int i = 0; i < PIN_SALT_BYTES; i++)
snprintf(out + i * 2, 3, "%02x", b[i]);
}
/* ---------- 配置层小包装 ---------- */
static int cfg_get(const char *key, char *out, int outsz)
{
return meow_config_get(key, out, outsz) == MEOW_CFG_OK ? 0 : -1;
}
static int cfg_set(const char *key, const char *val)
{
return meow_config_set(key, val) == MEOW_CFG_OK ? 0 : -1;
}
static int is_hex16(const char *s)
{
if (!s) return 0;
for (int i = 0; i < PIN_HEX_LEN; i++) {
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
}
return s[PIN_HEX_LEN] == '\0';
}
/* ---------- 公共 API ---------- */
int mr_pin_valid(const char *pin)
{
if (!pin) return 0;
int n = 0;
while (pin[n]) {
if (pin[n] < '0' || pin[n] > '9') return 0;
n++;
if (n > MR_PIN_MAX_LEN) return 0;
}
return n >= MR_PIN_MIN_LEN;
}
int mr_pin_enabled(void)
{
char hash[PIN_HEX_BUF + 8];
char salt[PIN_HEX_BUF + 8];
if (meow_config_int(PIN_KEY_ENABLED, 0) != 1) return 0;
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
return is_hex16(hash) && is_hex16(salt); /* 配置残缺 → fail-closed */
}
int mr_pin_set(const char *pin)
{
char salt[PIN_HEX_BUF];
char hash[PIN_HEX_BUF];
char old_salt[PIN_HEX_BUF + 8];
int had_old_salt;
if (!mr_pin_valid(pin)) return MR_PIN_ERR_FORMAT;
pin_gen_salt(salt);
pin_digest(pin, salt, hash);
had_old_salt = (cfg_get(PIN_KEY_SALT, old_salt, (int)sizeof old_salt) == 0);
/* 先写盐与摘要、最后打开 enabled:中途失败只会留下「未启用」,不会出现
* enabled=1 但缺少盐/哈希的半残状态。摘要写失败时把盐还原成旧值,使已启用的旧
* PIN 仍可校验(否则「新盐+旧哈希」会让新旧 PIN 都验不过)。 */
if (cfg_set(PIN_KEY_SALT, salt) != 0) return MR_PIN_ERR_STORE;
if (cfg_set(PIN_KEY_HASH, hash) != 0) {
if (had_old_salt) cfg_set(PIN_KEY_SALT, old_salt);
else meow_config_remove(PIN_KEY_SALT);
return MR_PIN_ERR_STORE;
}
if (cfg_set(PIN_KEY_ENABLED, "1") != 0) {
/* 重设时 enabled 本来就是 "1":新盐/哈希已一致生效,按成功返回;
* 首次开启时 enabled 仍为 0 → 如实报落盘失败。 */
return meow_config_int(PIN_KEY_ENABLED, 0) == 1 ? MR_PIN_OK : MR_PIN_ERR_STORE;
}
return MR_PIN_OK;
}
int mr_pin_verify(const char *pin)
{
char hash[PIN_HEX_BUF + 8];
char salt[PIN_HEX_BUF + 8];
char calc[PIN_HEX_BUF];
if (!mr_pin_valid(pin)) return 0;
if (!mr_pin_enabled()) return 0;
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
pin_digest(pin, salt, calc);
unsigned diff = 0;
for (int i = 0; i < PIN_HEX_LEN; i++)
diff |= (unsigned)((unsigned char)hash[i] ^ (unsigned char)calc[i]);
diff |= (unsigned)(hash[PIN_HEX_LEN] != '\0');
return diff == 0;
}
int mr_pin_change(const char *old_pin, const char *new_pin)
{
if (!mr_pin_enabled()) return MR_PIN_ERR_STATE;
if (!mr_pin_valid(new_pin)) return MR_PIN_ERR_FORMAT;
if (!mr_pin_verify(old_pin)) return MR_PIN_ERR_WRONG;
return mr_pin_set(new_pin);
}
int mr_pin_disable(void)
{
meow_config_remove(PIN_KEY_HASH);
meow_config_remove(PIN_KEY_SALT);
if (cfg_set(PIN_KEY_ENABLED, "0") != 0) return MR_PIN_ERR_STORE;
return MR_PIN_OK;
}
int mr_pin_get_salt(char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
if (cfg_get(PIN_KEY_SALT, out, outsz) != 0) return 0;
return out[0] != '\0';
}
int mr_pin_get_hash(char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
if (cfg_get(PIN_KEY_HASH, out, outsz) != 0) return 0;
return out[0] != '\0';
}
+62
View File
@@ -0,0 +1,62 @@
/* logic/pin.h — APP 启动 PIN(纯 C;宿主可裸测)。
*
* 定位(诚实标注):PIN 只挡「随手翻看」——TF 卡可拔出直读,数据目录明文可改,
* 故不引入加密库、不宣称安全边界。存储口径:加盐迭代哈希(FNV-1a ×1000 轮,
* 每次设置新随机盐,hex 输出),落在 meow_config_*(App 自有 KV → TF MeowData)。
* 盐/哈希只做「不知道 PIN 无法直接重算」的轻量门槛,防不了离线暴力或改配置。
*
* 配置键(meow_config_*,值均为字符串):
* pin.enabled = "1"/"0"
* pin.hash = 16 位 hex(64-bit 摘要)
* pin.salt = 16 位 hex(每次设置重新生成)
* enabled=1 但 hash/salt 缺失或格式不合法时一律按「未启用」处理(fail-closed)。
*
* 本模块只依赖 include/meow_ui_config.h 的三个声明(get/set/remove/int),不碰 UI;
* 宿主测试链 test_pin.c 内的 meow_config_* 存根即可,无需 SDK/设备。
*/
#ifndef MR_PIN_H
#define MR_PIN_H
#ifdef __cplusplus
extern "C" {
#endif
#define MR_PIN_MIN_LEN 4 /* 允许的最短位数 */
#define MR_PIN_MAX_LEN 8 /* 允许的最长位数 */
/* 返回码(set/change/disable)。verify 用 1/0 表示对/错,不返回错误码。 */
#define MR_PIN_OK 0 /* 成功 */
#define MR_PIN_ERR_FORMAT (-1) /* PIN 非法:非 4-8 位纯数字(NULL/空/符号/超长) */
#define MR_PIN_ERR_STATE (-2) /* 状态不满足:change 时尚未启用 */
#define MR_PIN_ERR_WRONG (-3) /* change 时原 PIN 校验失败 */
#define MR_PIN_ERR_STORE (-4) /* meow_config_* 落盘失败(无数据目录/只读/IO) */
/* 4-8 位纯 ASCII 数字('0'-'9')→ 1;其余(含 NULL、空串、符号、全角数字)→ 0。 */
int mr_pin_valid(const char *pin);
/* 当前是否启用:1=已启用且 hash/salt 完整合法;0=未启用或配置损坏(fail-closed)。 */
int mr_pin_enabled(void);
/* 设置/重设 PIN(生成新盐 + 迭代哈希并立即落盘)。成功即生效(下次启动进锁屏)。
* 返回 MR_PIN_OK / MR_PIN_ERR_FORMAT / MR_PIN_ERR_STORE。 */
int mr_pin_set(const char *pin);
/* 校验:1=匹配;0=不匹配/未启用/输入非法(不区分,避免侧信道式提示)。 */
int mr_pin_verify(const char *pin);
/* 修改:先校验 old,再以新盐重设 new。失败时旧 PIN 保持有效(先校验后写盘)。
* 返回 MR_PIN_OK / MR_PIN_ERR_STATE / MR_PIN_ERR_FORMAT / MR_PIN_ERR_WRONG / MR_PIN_ERR_STORE。 */
int mr_pin_change(const char *old_pin, const char *new_pin);
/* 关闭 PIN:删 hash/salt 并把 enabled 置 0(幂等;调用方已通过锁屏验证时无需再输 PIN)。
* 返回 MR_PIN_OK / MR_PIN_ERR_STORE。 */
int mr_pin_disable(void);
/* 诊断/测试用:读回当前 hex 盐/哈希。返回 1=读成功,0=未设。 */
int mr_pin_get_salt(char *out, int outsz);
int mr_pin_get_hash(char *out, int outsz);
#ifdef __cplusplus
}
#endif
#endif /* MR_PIN_H */
+49 -2
View File
@@ -1,14 +1,61 @@
/* logic/reader.c — 阅读方向状态(默认 RTL;M4 的阅读器直接复用)。 */ /* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)。
*
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
* 之后走纯内存快路径(不再读配置)。
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
*/
#include "reader.h" #include "reader.h"
#include "include/meow_ui_config.h"
#include "include/meow_ui_log.h"
#include <string.h>
#define MR_DIR_CFG_KEY "reader.dir"
#define MR_DIR_CFG_LTR "ltr"
#define MR_DIR_CFG_RTL "rtl"
static int g_direction = MR_DIR_RTL; static int g_direction = MR_DIR_RTL;
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
static void reader_restore_once(void)
{
char buf[8];
if (g_restored) return;
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
/* 其它值:忽略,保持默认 RTL */
}
int mr_reader_get_direction(void) int mr_reader_get_direction(void)
{ {
reader_restore_once();
return g_direction; return g_direction;
} }
void mr_reader_set_direction(int dir) void mr_reader_set_direction(int dir)
{ {
if (dir == MR_DIR_RTL || dir == MR_DIR_LTR) g_direction = dir; if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
if (g_direction == dir) return; /* 无变化不写盘 */
g_direction = dir;
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
!= MEOW_CFG_OK)
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
} }
#ifdef MR_READER_TEST_HOOKS
void mr_reader_test_reset(void)
{
g_direction = MR_DIR_RTL;
g_restored = 0;
}
#endif
+46 -5
View File
@@ -1,13 +1,54 @@
#!/bin/bash #!/bin/bash
# run_tests.sh — 宿主 gcc 构建并运行图库 logic 层测试(不链 SDK、不需要设备)。 # run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
# - test_library:书库扫描/自然排序/阅读方向(46 项)
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
# 坏值/非法值/只读一次/写失败只告警)
# - test_pin:启动 PIN 格式/设置/校验/修改/禁用/残缺配置/落盘失败(logic/pin.c 为纯 C,
# 只引用 meow_config_* 声明,测试内以内存 KV 存根顶替 → 不链 SDK)。
# - test_settings_lock_host(可选):设置页/锁屏与 SDK view/activity/widget 接线的宿主集成
# 验证(owner 归属 + 锁屏 SWIPE 拦截)。需要 SDK **源码树**(不只 meow_ui.a);
# 交付环境只有库+头时自动 SKIP。
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
gcc -Wall -Wextra -O1 -I. \ SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
test/test_library.c logic/library.c logic/reader.c \
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
test/test_library.c logic/library.c logic/reader.c test/settings_reader_stub.c \
-o test/test_library -o test/test_library
./test/test_library ./test/test_library
gcc -Wall -Wextra -O1 -I. \
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
test/test_reader_core.c logic/library.c logic/reader.c \ test/test_reader_core.c logic/library.c logic/reader.c \
logic/reader/page_source.c logic/reader/nav.c \ logic/reader/page_source.c logic/reader/nav.c test/settings_reader_stub.c \
-o test/test_reader_core -o test/test_reader_core
./test/test_reader_core ./test/test_reader_core
gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
-o test/test_settings_reader_persist
./test/test_settings_reader_persist
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
test/test_pin.c logic/pin.c \
-o test/test_pin
./test/test_pin
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
test/test_settings_lock_host.c ui/settings/settings.c ui/lock/lock.c \
logic/pin.c logic/reader.c \
"$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
-pthread -lm -o test/test_settings_lock_host
./test/test_settings_lock_host
else
echo "SKIP test_settings_lock_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
fi
+92
View File
@@ -0,0 +1,92 @@
/* settings_reader_stub.c — 宿主测试用 meow_config_* 与 meow_log 最小实现(内存 KV)。
*
* 只为让 logic/reader.c 在不链 SDK 的宿主测试里可编译/可断言:
* - meow_config_get/set:8 个键的内存表,可注入写失败(模拟无卡/只读);
* - meow_log:只统计 WARN 次数(验证「写失败只告警、不阻断切换」)。
* 与 test_pin.c 自带存根同思路,但抽成共用文件供多个 reader 相关测试链接。
*/
#include "settings_reader_stub.h"
#include "include/meow_ui_config.h"
#include "include/meow_ui_log.h"
#include <stdio.h>
#include <string.h>
#define ST_MAX 8
static struct { char key[64]; char val[64]; int used; } g_kv[ST_MAX];
static int g_fail_writes;
static int g_warns;
static int kv_find(const char *key)
{
for (int i = 0; i < ST_MAX; i++)
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
return -1;
}
void mr_test_cfg_clear(void)
{
memset(g_kv, 0, sizeof g_kv);
g_fail_writes = 0;
}
void mr_test_cfg_put(const char *key, const char *val)
{
if (!key || !key[0]) return;
int i = kv_find(key);
if (i < 0) {
for (i = 0; i < ST_MAX; i++) if (!g_kv[i].used) break;
if (i >= ST_MAX) return;
g_kv[i].used = 1;
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
}
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
}
int mr_test_cfg_get(const char *key, char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
int i = kv_find(key);
if (i < 0) return 0;
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
return 1;
}
void mr_test_cfg_fail_writes(int on)
{
g_fail_writes = on ? 1 : 0;
}
int mr_test_log_warns(void)
{
return g_warns;
}
void mr_test_log_clear(void)
{
g_warns = 0;
}
/* ---- 被测代码调用的 SDK 接口 ---- */
int meow_config_set(const char *key, const char *val)
{
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
if (g_fail_writes) return MEOW_CFG_ERR_IO;
mr_test_cfg_put(key, val);
return MEOW_CFG_OK;
}
int meow_config_get(const char *key, char *out, int outsz)
{
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
out[0] = '\0';
if (!mr_test_cfg_get(key, out, outsz)) return MEOW_CFG_ERR_NOENT;
return MEOW_CFG_OK;
}
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
(void)fmt;
if (lvl >= MEOW_LOG_WARN) g_warns++;
}
+24
View File
@@ -0,0 +1,24 @@
/* settings_reader_stub.h — 宿主测试用 meow_config_* 与 meow_log 存根(logic/reader.c 的依赖面)。
*
* 内存 KV(不落盘)+ 测试辅助:读/写/清配置、注入写失败、统计 WARN 次数。
* 供 test_library、test_reader_core、test_settings_reader_persist 共用——
* 这几个测试都不链 SDK,reader.c 的持久化失败/成功路径靠这里可控。
*/
#ifndef SETTINGS_READER_STUB_H
#define SETTINGS_READER_STUB_H
#ifdef __cplusplus
extern "C" {
#endif
void mr_test_cfg_clear(void); /* 清空全部键 */
void mr_test_cfg_put(const char *key, const char *val); /* 直接写入(可注入坏值) */
int mr_test_cfg_get(const char *key, char *out, int outsz); /* 1=键存在 */
void mr_test_cfg_fail_writes(int on); /* 1=后续 set 全部失败 */
int mr_test_log_warns(void); /* meow_log(WARN..) 次数 */
void mr_test_log_clear(void);
#ifdef __cplusplus
}
#endif
#endif /* SETTINGS_READER_STUB_H */
+320
View File
@@ -0,0 +1,320 @@
/* test_pin.c — 启动 PIN(logic/pin)宿主测试:纯 C,不链 SDK。
*
* 用内存 KV 存根顶替 meow_config_get/set/int/remove(与 SDK 语义一致:
* 未设 → MEOW_CFG_ERR_NOENT;写失败可由 g_fail_write 注入)。
* 覆盖:格式校验边界(4/8 位合法;3/9 位、空串、NULL、符号、全角数字非法)、
* 设置/校验、错 PIN、重设换盐、change 全分支、disable、配置残缺 fail-closed、
* 落盘失败、落盘布局(pin.enabled/hash/salt)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. -I"$SDK" test/test_pin.c logic/pin.c -o test/test_pin
* 或直接 test/run_tests.sh。
*/
#include "logic/pin.h"
#include "include/meow_ui_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define KV_MAX 8
static struct { char key[64]; char val[128]; int used; } g_kv[KV_MAX];
static int g_fail_write; /* 1=模拟全部落盘失败 */
static const char *g_fail_key; /* 非 NULL=只让该键写失败(模拟半途失败) */
static int kv_find(const char *key)
{
for (int i = 0; i < KV_MAX; i++)
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
return -1;
}
static void kv_clear(void)
{
memset(g_kv, 0, sizeof g_kv);
g_fail_write = 0;
g_fail_key = NULL;
}
int meow_config_set(const char *key, const char *val)
{
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
if (g_fail_write || (g_fail_key && strcmp(g_fail_key, key) == 0)) return MEOW_CFG_ERR_IO;
int i = kv_find(key);
if (i < 0) {
for (i = 0; i < KV_MAX; i++) if (!g_kv[i].used) break;
if (i >= KV_MAX) return MEOW_CFG_ERR_IO;
g_kv[i].used = 1;
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
}
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
return MEOW_CFG_OK;
}
int meow_config_get(const char *key, char *out, int outsz)
{
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
out[0] = '\0';
int i = kv_find(key);
if (i < 0) return MEOW_CFG_ERR_NOENT;
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
return MEOW_CFG_OK;
}
int meow_config_int(const char *key, int def)
{
char buf[32];
char *end = NULL;
long v;
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
v = strtol(buf, &end, 10);
if (end == buf) return def;
return (int)v;
}
int meow_config_bool(const char *key, int def)
{
return meow_config_int(key, def) != 0;
}
void meow_config_remove(const char *key)
{
int i = kv_find(key);
if (i >= 0) g_kv[i].used = 0;
}
const char *meow_config_dir(void) { return "/tmp/meowcfg_test"; }
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static int is_hex16(const char *s)
{
if (!s) return 0;
if (strlen(s) != 16) return 0;
for (int i = 0; i < 16; i++)
if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0;
return 1;
}
static void test_format(void)
{
CHECK(mr_pin_valid(NULL) == 0, "NULL invalid");
CHECK(mr_pin_valid("") == 0, "empty invalid");
CHECK(mr_pin_valid("123") == 0, "3 digits invalid");
CHECK(mr_pin_valid("1234") == 1, "4 digits valid (min)");
CHECK(mr_pin_valid("12345") == 1, "5 digits valid");
CHECK(mr_pin_valid("12345678") == 1, "8 digits valid (max)");
CHECK(mr_pin_valid("123456789") == 0, "9 digits invalid");
CHECK(mr_pin_valid("12a4") == 0, "letter invalid");
CHECK(mr_pin_valid("12 4") == 0, "space invalid");
CHECK(mr_pin_valid(" 1234") == 0, "leading space invalid");
CHECK(mr_pin_valid("1234 ") == 0, "trailing space invalid");
CHECK(mr_pin_valid("-123") == 0, "sign invalid");
/* 全角数字(UTF-8 EF BC 9x)不是 ASCII 数字 */
CHECK(mr_pin_valid("\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\xEF\xBC\x94") == 0,
"fullwidth digits invalid");
}
static void test_default_state(void)
{
kv_clear();
CHECK(mr_pin_enabled() == 0, "default disabled");
CHECK(mr_pin_verify("1234") == 0, "verify on disabled = 0");
CHECK(mr_pin_set("123") == MR_PIN_ERR_FORMAT, "set short -> format error");
CHECK(mr_pin_enabled() == 0, "failed set keeps disabled");
}
static void test_set_verify(void)
{
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set 1234 ok");
CHECK(mr_pin_enabled() == 1, "enabled after set");
CHECK(mr_pin_verify("1234") == 1, "verify correct");
CHECK(mr_pin_verify("4321") == 0, "verify wrong");
CHECK(mr_pin_verify("123") == 0, "verify short");
CHECK(mr_pin_verify("12345") == 0, "verify longer");
CHECK(mr_pin_verify("") == 0, "verify empty");
CHECK(mr_pin_verify(NULL) == 0, "verify NULL");
CHECK(mr_pin_verify("12a4") == 0, "verify non-digit");
char salt[32] = "", hash[32] = "";
CHECK(mr_pin_get_salt(salt, sizeof salt) == 1, "salt present");
CHECK(mr_pin_get_hash(hash, sizeof hash) == 1, "hash present");
CHECK(is_hex16(salt), "salt is 16 hex (got %s)", salt);
CHECK(is_hex16(hash), "hash is 16 hex (got %s)", hash);
CHECK(strcmp(hash, "1234") != 0, "hash is not the raw pin");
/* 落盘布局:enabled="1"(明文 PIN 不落盘:配置里只有 16 位 hex 摘要与盐) */
char buf[64] = "";
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "1") == 0,
"pin.enabled=1 on disk");
}
static void test_reset_new_salt(void)
{
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set first");
char salt1[32] = "", hash1[32] = "";
mr_pin_get_salt(salt1, sizeof salt1);
mr_pin_get_hash(hash1, sizeof hash1);
CHECK(mr_pin_set("5678") == MR_PIN_OK, "set second (overwrite)");
char salt2[32] = "", hash2[32] = "";
mr_pin_get_salt(salt2, sizeof salt2);
mr_pin_get_hash(hash2, sizeof hash2);
CHECK(strcmp(salt1, salt2) != 0, "new salt on each set");
CHECK(strcmp(hash1, hash2) != 0, "new hash on each set");
CHECK(mr_pin_verify("5678") == 1, "new pin verifies");
CHECK(mr_pin_verify("1234") == 0, "old pin rejected after overwrite");
/* 同一 PIN 重设:盐不同 → 摘要也不同(防跨设备彩虹表/同值比对)。 */
CHECK(mr_pin_set("5678") == MR_PIN_OK, "re-set same pin");
char salt3[32] = "", hash3[32] = "";
mr_pin_get_salt(salt3, sizeof salt3);
mr_pin_get_hash(hash3, sizeof hash3);
CHECK(strcmp(salt2, salt3) != 0, "salt differs for same pin");
CHECK(strcmp(hash2, hash3) != 0, "hash differs for same pin");
CHECK(mr_pin_verify("5678") == 1, "same pin still verifies");
}
static void test_change(void)
{
kv_clear();
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change while disabled -> state");
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
CHECK(mr_pin_change("9999", "5678") == MR_PIN_ERR_WRONG, "change wrong old -> wrong");
CHECK(mr_pin_verify("1234") == 1, "old still valid after wrong-change");
CHECK(mr_pin_verify("5678") == 0, "new not applied after wrong-change");
CHECK(mr_pin_change("1234", "12") == MR_PIN_ERR_FORMAT, "change to short new -> format");
CHECK(mr_pin_verify("1234") == 1, "old still valid after bad-new");
CHECK(mr_pin_change("1234", "12345678") == MR_PIN_OK, "change ok (8 digits)");
CHECK(mr_pin_verify("12345678") == 1, "new valid after change");
CHECK(mr_pin_verify("1234") == 0, "old invalid after change");
CHECK(mr_pin_enabled() == 1, "still enabled after change");
CHECK(mr_pin_change(NULL, "0000") == MR_PIN_ERR_WRONG, "change NULL old -> wrong");
CHECK(mr_pin_change("12345678", NULL) == MR_PIN_ERR_FORMAT, "change NULL new -> format");
}
static void test_disable(void)
{
kv_clear();
CHECK(mr_pin_disable() == MR_PIN_OK, "disable while disabled is idempotent");
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup for disable");
CHECK(mr_pin_disable() == MR_PIN_OK, "disable ok");
CHECK(mr_pin_enabled() == 0, "disabled after disable");
CHECK(mr_pin_verify("1234") == 0, "verify fails after disable");
char buf[32] = "";
CHECK(meow_config_get("pin.hash", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "hash key removed");
CHECK(meow_config_get("pin.salt", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "salt key removed");
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "0") == 0,
"enabled=0 on disk");
/* 禁用后可重新开启(新盐)。 */
CHECK(mr_pin_set("8765") == MR_PIN_OK, "re-enable after disable");
CHECK(mr_pin_verify("8765") == 1, "re-enabled pin verifies");
}
static void test_corrupt_state(void)
{
/* enabled=1 但缺 hash → fail-closed */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "0011223344556677");
CHECK(mr_pin_enabled() == 0, "enabled without hash -> fail closed");
CHECK(mr_pin_verify("1234") == 0, "verify fail closed");
/* hash 长度/字符非法 */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "0011223344556677");
meow_config_set("pin.hash", "zz");
CHECK(mr_pin_enabled() == 0, "garbage hash -> fail closed");
/* salt 非法 */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "");
meow_config_set("pin.hash", "0011223344556677");
CHECK(mr_pin_enabled() == 0, "empty salt -> fail closed");
/* change 在残缺状态 → state 错误(不误判为已启用) */
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change on corrupt -> state");
}
static void test_store_failure(void)
{
kv_clear();
g_fail_write = 1;
CHECK(mr_pin_set("1234") == MR_PIN_ERR_STORE, "set with failing store -> store error");
CHECK(mr_pin_enabled() == 0, "still disabled after store failure");
g_fail_write = 0;
/* 写盘失败时不得把状态改成「已启用」——set 先写盐/摘要、最后写 enabled。 */
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set ok after store recovers");
g_fail_write = 1;
CHECK(mr_pin_disable() == MR_PIN_ERR_STORE, "disable with failing store -> store error");
g_fail_write = 0;
}
static void test_partial_write(void)
{
/* 摘要写失败:把盐还原成旧值,已启用的旧 PIN 仍可校验(不会新旧都验不过)。 */
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
g_fail_key = "pin.hash";
CHECK(mr_pin_set("5678") == MR_PIN_ERR_STORE, "hash write failure -> store error");
CHECK(mr_pin_enabled() == 1, "still enabled after partial write");
CHECK(mr_pin_verify("1234") == 1, "old PIN still valid after partial write");
CHECK(mr_pin_verify("5678") == 0, "new PIN not applied after partial write");
g_fail_key = NULL;
/* 重设时 enabled 已是 "1":该键写失败不影响一致性 → 按成功返回。 */
g_fail_key = "pin.enabled";
CHECK(mr_pin_set("4321") == MR_PIN_OK, "enabled-already-1 write failure is consistent");
CHECK(mr_pin_verify("4321") == 1, "new PIN active");
g_fail_key = NULL;
/* 首次开启时 enabled 写失败 → 未启用(fail-closed)。 */
kv_clear();
g_fail_key = "pin.enabled";
CHECK(mr_pin_set("1111") == MR_PIN_ERR_STORE, "first enable with enabled-write failure");
CHECK(mr_pin_enabled() == 0, "not enabled after enabled-write failure");
g_fail_key = NULL;
}
static void test_verify_does_not_mutate(void)
{
kv_clear();
CHECK(mr_pin_set("2468") == MR_PIN_OK, "setup");
char salt1[32] = "", salt2[32] = "";
mr_pin_get_salt(salt1, sizeof salt1);
(void)mr_pin_verify("0000");
(void)mr_pin_verify("2468");
mr_pin_get_salt(salt2, sizeof salt2);
CHECK(strcmp(salt1, salt2) == 0, "verify does not change salt");
}
int main(void)
{
test_format();
test_default_state();
test_set_verify();
test_reset_new_salt();
test_change();
test_disable();
test_corrupt_state();
test_store_failure();
test_partial_write();
test_verify_does_not_mutate();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+229
View File
@@ -0,0 +1,229 @@
/* 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;
}
+131
View File
@@ -0,0 +1,131 @@
/* test_settings_reader_persist.c — 阅读方向持久化宿主测试(M5 评审 P1 修复)。
*
* 被测:logic/reader.c 的 meow_config("reader.dir") 持久化。
* 「新进程语义」用 reader.c 的测试钩子 mr_reader_test_reset()(-DMR_READER_TEST_HOOKS)
* 模拟:清掉静态恢复标记,配置存根保留——等价于重启后首次 get 从配置恢复。
*
* 覆盖:
* T1 无配置默认 RTL,且 get 不写配置;
* T2 set LTR 落盘 + 内存立即生效;T3 reset 后从配置恢复 LTR;
* T4 set RTL 落盘 + reset 恢复 RTL;
* T5 坏值("sideways")忽略 → 默认 RTL,且 get 不回写坏值;
* T6 非法方向(99)忽略且不落盘;
* T7 坏值后 set 合法方向会覆盖配置;
* T8 配置只读一次(首次 get 后配置再变不影响本次内存态 = 快路径);
* T9 写失败:切换仍生效(内存)、只打一条 WARN、配置未变。
*
* 编译(tuku/ 下,SDK 仅需公共头):
* gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
* test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
* -o test/test_settings_reader_persist && ./test/test_settings_reader_persist
*/
#include "logic/reader.h"
#include "test/settings_reader_stub.h"
#include <stdio.h>
#include <string.h>
/* reader.c 在 -DMR_READER_TEST_HOOKS 下导出(正式构建无此符号,故不写进 reader.h,
* 避免生产头暴露测试入口)。 */
extern void mr_reader_test_reset(void);
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static int cfg_is(const char *want)
{
char buf[32] = "";
if (!mr_test_cfg_get("reader.dir", buf, (int)sizeof buf)) return 0;
return strcmp(buf, want) == 0;
}
static int cfg_has(const char *key)
{
char buf[32];
return mr_test_cfg_get(key, buf, (int)sizeof buf);
}
static void test_default_and_ltr(void)
{
/* T1 无配置 → 默认 RTL;get 是只读的。 */
mr_test_cfg_clear();
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T1 default RTL with no config");
CHECK(!cfg_has("reader.dir"), "T1 get does not write config");
/* T2 set LTR:内存立即生效 + 落盘 "ltr"。 */
mr_reader_set_direction(MR_DIR_LTR);
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T2 memory updated immediately");
CHECK(cfg_is("ltr"), "T2 set LTR persists reader.dir=ltr");
/* T3 模拟重启 → 从配置恢复 LTR。 */
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T3 restore LTR after restart");
/* T4 反向:set RTL 落盘并恢复。 */
mr_reader_set_direction(MR_DIR_RTL);
CHECK(cfg_is("rtl"), "T4 set RTL persists reader.dir=rtl");
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T4 restore RTL after restart");
}
static void test_bad_values(void)
{
/* T5 坏值忽略 → 默认 RTL,且 get 不改写坏值。 */
mr_test_cfg_clear();
mr_test_cfg_put("reader.dir", "sideways");
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T5 bad value ignored -> default RTL");
CHECK(cfg_is("sideways"), "T5 get leaves bad value untouched");
/* T6 非法方向:忽略且不落盘。 */
mr_test_cfg_clear();
mr_reader_test_reset();
mr_reader_set_direction(99);
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T6 invalid dir ignored (memory)");
CHECK(!cfg_has("reader.dir"), "T6 invalid dir writes nothing");
/* T7 坏值后 set 合法方向 → 覆盖配置并在重启后生效。 */
mr_test_cfg_clear();
mr_test_cfg_put("reader.dir", "bogus");
mr_reader_test_reset();
mr_reader_set_direction(MR_DIR_LTR);
CHECK(cfg_is("ltr"), "T7 valid set overwrites bad config");
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T7 restored after overwrite");
}
static void test_read_once_and_failure(void)
{
/* T8 配置只读一次:首次 get 之后配置变化不影响内存(快路径)。 */
mr_test_cfg_clear();
mr_reader_test_reset();
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 first get with no config");
mr_test_cfg_put("reader.dir", "ltr");
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "T8 config read only once (fast path)");
/* T9 写失败:切换仍生效、WARN 一条、配置不变。 */
mr_test_cfg_clear();
mr_reader_test_reset();
mr_test_log_clear();
mr_test_cfg_fail_writes(1);
mr_reader_set_direction(MR_DIR_LTR);
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "T9 switch still applies on write failure");
CHECK(mr_test_log_warns() == 1, "T9 one WARN on write failure (got %d)", mr_test_log_warns());
CHECK(!cfg_has("reader.dir"), "T9 config unchanged on failure");
mr_test_cfg_fail_writes(0);
mr_reader_set_direction(MR_DIR_RTL);
CHECK(cfg_is("rtl") && mr_test_log_warns() == 1, "T9 recovery writes rtl, no extra WARN");
}
int main(void)
{
test_default_and_ltr();
test_bad_values();
test_read_once_and_failure();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+169 -25
View File
@@ -1,49 +1,185 @@
/* ui/lock/lock.c — 锁屏页(ACT_LOCK)M3 桩 + M5 接缝。 /* ui/lock/lock.c — 锁屏页(ACT_LOCK):APP 启动 PIN。
* *
* M5 在此实现「APP 启动 PIN」:lock_view_should_show() 读配置决定是否锁, * 流程(用户原话「允许用户在APP设置中设置APP启动 PIN」):
* 锁屏页负责 PIN 输入/校验后 page_pop 回书库。本批桩恒返回 0(不锁),页面仅可编译占位。 * - lock_view_should_show() = logic/pin 的 mr_pin_enabled();app.c 据此在启动时先 push
* 本页,验证通过前书库不进入/不绘制(view 注册表只画已进入的页,widget 还按 owner 过滤);
* - 本页 on_enter 建「输入 PIN」按钮(owner=ACT_LOCK),点按弹系统数字键盘(MEOW_IME_NUM),
* 校验通过 → page_pop 回书库;错误 → 就地提示重试(不做惩罚性锁定/计数);
* - 左缘返回**必须**由本页 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c 的
* on_swipe;否则 app.c 对非 library 页无条件 page_pop,PIN 会被绕过);
* - PIN 未启用时 app.c 直接 push 书库,本页不会被进入。
* *
* 接缝注意(M5): * 安全口径(诚实标注):PIN 防的是随手翻看,不是安全边界——TF 卡可拔出直读,配置目录
* ① meow_view 的 on_create 在 meow_view_create 内运行(此时 activity 栈空);本页是非根页, * 可改。存储为加盐迭代哈希(logic/pin.c),不引入加密库;锁屏文案也如实告知用户。
* 控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式 *
* meow_ui_activity_push(ACT_LOCK) → 建控件 → meow_ui_activity_pop() * 控件归属(§6.1,M3 桩注释①):on_create 时 activity 栈为空,控件一律在 on_enter 建。
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,
* 锁屏控件会从书库页被命中(§6.1 跨页泄漏)。
* ② 锁屏必须经自身 view 的 on_event 消费 SWIPE:meow_view_dispatch_event 先于 App 的
* on_swipe(sdk/meow_ui_view.h:83-88);否则 app.c 对非 library 页的左缘滑动无条件
* page_pop,PIN 会被绕过。
*/ */
#include "tuku_internal.h" #include "tuku_internal.h"
#include "logic/pin.h"
#include <stdio.h>
#include <string.h>
static mr_page_ctrl g_ctrl; static mr_page_ctrl g_ctrl;
static meow_widget *g_btn;
static char g_msg[64];
static int g_msg_err;
static int g_waiting; /* 1=数字键盘打开中(上部提示改为输入说明) */
static char g_ime_pin[MR_PIN_MAX_LEN + 12];
/* meow_view on_event 的事件码:公共伞头未导出(sdk/meow_ui_internal.h:
* MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定),此处按契约镜像。 */
#define MR_VEV_TAP 2
#define MR_VEV_SWIPE 3
int lock_view_should_show(void) int lock_view_should_show(void)
{ {
return 0; /* M5:读 APP 启动 PIN 配置后返回 1 */ return mr_pin_enabled();
} }
/* ============================================================ */
/* PIN 输入(系统数字键盘) */
/* ============================================================ */
static void ime_pin_change(const char *text, void *ctx)
{
(void)ctx;
snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : "");
}
/* 阻塞数字键盘;返回 1=收集到非空文本(关闭键盘即结束/校验)。
* SDK 现状:键盘「确定」只置内部 submit 标记不关闭,关闭路径(取消/点背景)又把标记清 0,
* 故 meow_ui_text_input 的返回值不可作“确认”判据;文本经 on_change 实时收集,空=取消。 */
static int lock_read_pin(char *out, int outsz)
{
meow_ime_ctx c;
memset(&c, 0, sizeof c);
c.mode = MEOW_IME_NUM;
c.maxlen = (int)sizeof g_ime_pin;
c.on_change = ime_pin_change;
c.ctx = NULL;
g_ime_pin[0] = '\0';
g_msg[0] = '\0';
g_msg_err = 0;
g_waiting = 1; /* 锁屏上部提示改为输入说明 */
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); /* 键盘区背景快照取当前帧 */
(void)meow_ui_text_input(&c);
g_waiting = 0;
snprintf(out, (size_t)outsz, "%s", g_ime_pin);
return out[0] != '\0';
}
static void lock_attempt(void)
{
char pin[32] = "";
if (!lock_read_pin(pin, (int)sizeof pin)) {
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
return; /* 关闭键盘未输入:留在锁屏 */
}
if (!mr_pin_verify(pin)) {
snprintf(g_msg, sizeof g_msg, "PIN 错误,请重试");
g_msg_err = 1;
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
return;
}
g_msg[0] = '\0';
g_msg_err = 0;
meow_view_page_pop(); /* 通过 → 回书库(ACT_LIBRARY on_enter) */
}
/* ============================================================ */
/* 布局/绘制 */
/* ============================================================ */
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx) static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
{ {
(void)v; (void)ctx; (void)v; (void)ctx;
int w = meow_ui_width(); int w = meow_ui_width();
int h = meow_ui_height(); int h = meow_ui_height();
int cy = h / 2; int sb = meow_ui_statusbar_height();
int cy = sb + (h - sb) / 2;
meow_ui_text_center(w / 2, cy - 80, "LOCKED", th->font_title, th->primary); /* 自绘整页底色:验证前不露任何书库内容(本页是顶层唯一 entered 页)。 */
meow_ui_text_center(w / 2, cy - 36, "Startup PIN (M5)", th->font_body, th->onSurface); meow_ui_fill_rect(0, sb, w, h - sb, th->bg);
meow_ui_text_center(w / 2, cy + 8, "PIN entry will be implemented in M5",
th->font_body, th->onSurfaceVariant); meow_ui_text_center(w / 2, cy - 150, "图库已锁定", th->font_title, th->onSurface);
if (g_waiting) {
meow_ui_text_center(w / 2, cy - 104, "输入 4-8 位数字后关闭键盘", th->font_body,
th->onSurface);
meow_ui_text_center(w / 2, cy - 76, "空输入 = 取消,回到锁屏", th->font_body,
th->onSurfaceVariant);
} else {
meow_ui_text_center(w / 2, cy - 104, "输入启动 PIN 继续", th->font_body,
th->onSurfaceVariant);
meow_ui_text_center(w / 2, cy - 76, "4-8 位数字 · 系统数字键盘", th->font_body,
th->onSurfaceVariant);
}
if (g_msg[0]) {
meow_ui_text_center(w / 2, cy - 26, g_msg, th->font_body,
g_msg_err ? th->error : th->primary);
}
/* 安全口径(如实告知):PIN 不是安全边界。 */
meow_ui_text_center(w / 2, h - 86, "PIN 仅用于防止随手查看,", th->font_body,
th->onSurfaceVariant);
meow_ui_text_center(w / 2, h - 58, "TF 卡可被直接读取,不构成安全边界。", th->font_body,
th->onSurfaceVariant);
} }
static void lock_place(meow_view *v)
{
int w = meow_ui_width();
int h = meow_ui_height();
int sb = meow_ui_statusbar_height();
int cy = sb + (h - sb) / 2;
v->x = 0; v->y = 0; v->w = w; v->h = h;
int bw = 220, bh = 52;
if (g_btn) meow_widget_set_rect(g_btn, (w - bw) / 2, cy + 20, bw, bh);
}
/* ============================================================ */
/* 事件 */
/* ============================================================ */
static int lock_on_event(meow_view *v, int ev, int a, int b, void *ctx)
{
(void)v; (void)ctx;
/* 左缘返回(以及任何滑动)一律吞掉:锁屏不允许退出 App,也不允许绕过 PIN。 */
if (ev == MR_VEV_SWIPE) return 1;
if (ev == MR_VEV_TAP) {
int x = a, y = b;
if (meow_widget_hit(g_btn, x, y)) lock_attempt();
return 1; /* 锁屏页消费点击,不向 App 其它页透传 */
}
return 0;
}
/* ============================================================ */
/* view 生命周期 */
/* ============================================================ */
static void lock_page_enter(meow_view *v, void *ctx) static void lock_page_enter(meow_view *v, void *ctx)
{ {
((mr_page_ctrl *)ctx)->active = 1; ((mr_page_ctrl *)ctx)->active = 1;
v->x = 0;
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(0); meow_ui_set_fullscreen(0);
meow_ui_set_page_bg(meow_theme_get()->bg); meow_ui_set_page_bg(meow_theme_get()->bg);
g_msg[0] = '\0';
g_msg_err = 0;
if (!g_btn) {
/* page_push 已先 meow_ui_activity_push(ACT_LOCK) → 控件 owner 正确。 */
g_btn = meow_btn_create_variant(0, 0, 220, 52, "输入 PIN", MEOW_BTN_FILLED);
if (g_btn) meow_view_add_widget(v, g_btn);
}
lock_place(v);
mr_nav_report(); mr_nav_report();
meow_ui_paint_full(); meow_ui_paint_full();
} }
@@ -54,12 +190,20 @@ static void lock_page_leave(meow_view *v, void *ctx)
((mr_page_ctrl *)ctx)->active = 0; ((mr_page_ctrl *)ctx)->active = 0;
} }
static void lock_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_btn = NULL; /* 控件随 meow_view_free 释放(v->widgets) */
}
meow_view *lock_view_create(void) meow_view *lock_view_create(void)
{ {
static const meow_view_hooks h = { static const meow_view_hooks h = {
.on_enter = lock_page_enter, .on_enter = lock_page_enter,
.on_leave = lock_page_leave, .on_leave = lock_page_leave,
.on_draw = lock_draw, .on_destroy = lock_page_destroy,
.on_draw = lock_draw,
.on_event = lock_on_event,
}; };
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl); return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
} }
+415 -27
View File
@@ -1,45 +1,420 @@
/* ui/settings/settings.c — 设置页(ACT_SETTINGS)M3 骨架。 /* ui/settings/settings.c — 设置页(ACT_SETTINGS):MD3 分组表单。
* *
* M5 在此实现 APP 启动 PIN 等设置项;本批只落可编译桩,入口为书库 TopAppBar ⚙。 * 内容(用户原话「允许用户在APP设置中设置APP启动 PIN」+「同步系统主题状态」):
* 返回:左缘返回(app.c SWIPE_LEFT → page_pop)。 * 阅读:阅读方向 RTL/LTR —— 直接读写 M3 的 mr_reader_get/set_direction(不另立状态);
* 安全:启动 PIN 开关(开启需两遍 MEOW_IME_NUM 输入确认)、修改 PIN;
* 关于:版本号、主题状态(跟随系统 + 当前明暗,随系统实时同步)。
* *
* 接缝注意(M5):meow_view 的 on_create 在 meow_view_create 内运行(activity 栈空); * 观感/主题:SDK 的 meow_listview 容器模式(MD3 surfaceContainer 圆角卡 + 行间空隙)
* 本页是非根页,控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式 * + 段头标签;开关/箭头由 SDK renderer 用主题语义色绘制;所有颜色取 meow_theme_get(),
* meow_ui_activity_push(ACT_SETTINGS) → 建控件 → meow_ui_activity_pop() * 不缓存颜色——app.c 的主题 listener 整帧 invalidate,本页 on_draw/src 回调下一帧即用新色。
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,设置控件会从 *
* 书库页被命中(§6.1 跨页泄漏)。 * 控件归属(§6.1,M3 桩注释①):本页非根页,控件**必须**在 on_enter 里建(此时
* activity 栈顶=ACT_SETTINGS),否则 owner 会绑到 ACT_LIBRARY 造成跨页命中。
*
* 返回:左缘返回由 app.c 的 on_swipe 统一 page_pop(本页 on_event 不消费 SWIPE)。
* PIN 输入:SDK 系统数字键盘(MEOW_IME_NUM),不自绘键盘;文本经 on_change 实时收集,
* 「非空 + 关闭键盘 = 校验、空 = 取消」(SDK 阻塞 IME 的返回值不能当确认判据,见
* settings_read_pin 注释);键盘打开期间步骤提示画在键盘面板上方(置顶 widget)。
*/ */
#include "tuku_internal.h" #include "tuku_internal.h"
#include "logic/pin.h"
#include <stdio.h>
#include <string.h>
#define ST_TOP_H 56
#define ST_SEC_H 34
#define ST_ROW_H 64
#define ST_GAP 12
/* meow_view on_event 的事件码:公共伞头未导出(定义在 sdk/meow_ui_internal.h:
* MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定)。此处按契约镜像,避免 App include SDK 内部头。
* (已在交付报告里作为 SDK 缺口提出:建议公共头导出事件枚举。) */
#define MR_VEV_TAP 2
/* PIN 行下标(g_lv_pin) */
#define ST_PIN_ROW_SWITCH 0
#define ST_PIN_ROW_CHANGE 1
static mr_page_ctrl g_ctrl; static mr_page_ctrl g_ctrl;
static meow_widget *g_tb;
static meow_widget *g_lv_dir; /* 阅读:1 行 */
static meow_widget *g_lv_pin; /* 安全:2 行 */
static meow_widget *g_lv_about; /* 关于:2 行 */
static meow_widget *g_overlay; /* 置顶提示条(仅拨号键盘期间显示步骤文案) */
static int g_yl_dir, g_yl_pin, g_yl_about; /* 段头标签 y */
static int g_note_y;
static int g_ov_y, g_ov_h; /* 提示条几何(App 自记;widget 句柄不透明) */
static char g_note[96];
static int g_note_err;
static int g_pin_on; /* 缓存的 PIN 启用态(避免每帧读 3 个小配置文件的 TF IO) */
static char g_theme_line[64];
static char g_prompt[64]; /* 非空=正在输入 PIN(键盘打开期间顶部提示条) */
static char g_ime_pin[MR_PIN_MAX_LEN + 12];
/* ============================================================ */
/* 文案/状态 */
/* ============================================================ */
static int theme_is_dark(const meow_theme *th)
{
int r = (int)((th->surface >> 16) & 0xFF);
int g = (int)((th->surface >> 8) & 0xFF);
int b = (int)(th->surface & 0xFF);
return (r * 299 + g * 587 + b * 114) / 1000 < 128;
}
static void theme_line_update(void)
{
const meow_theme *th = meow_theme_get();
meow_theme_mode m = meow_theme_get_mode();
const char *mode = (m == MEOW_THEME_AUTO) ? "跟随系统"
: (m == MEOW_THEME_DARK) ? "固定深色" : "固定浅色";
snprintf(g_theme_line, sizeof g_theme_line, "%s · 当前%s", mode,
theme_is_dark(th) ? "深色" : "浅色");
}
static void settings_note(const char *msg, int is_err)
{
snprintf(g_note, sizeof g_note, "%s", msg ? msg : "");
g_note_err = is_err;
}
static void settings_repaint(void)
{
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
}
/* ============================================================ */
/* PIN 输入(系统数字键盘) */
/* ============================================================ */
static void ime_pin_change(const char *text, void *ctx)
{
(void)ctx;
snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : "");
}
/* 阻塞弹出 MEOW_IME_NUM 收集 4-8 位数字。返回 1=收到非空文本;0=空(视为取消)。
* 说明(SDK 现状,如实记录):阻塞 IME 只以「关闭键盘」结束;数字键盘的「确定」只置内部
* submit 标记而不关闭,随后「取消/点背景」关闭时该标记又被清 0(sdk/meow_ui_ime.c 的
* K_ESC/背景路径),故 meow_ui_text_input 的返回值不可作为「确认」判据。本页改用
* on_change 实时收集文本:非空 + 关闭键盘 = 校验;空 = 取消(提示条照此告知用户)。
* 键盘面板覆盖屏底(y≥420),步骤提示画在其上方,否则打字时看不见当前步骤。 */
static int settings_read_pin(const char *prompt, char *out, int outsz)
{
meow_ime_ctx c;
memset(&c, 0, sizeof c);
c.mode = MEOW_IME_NUM;
c.maxlen = (int)sizeof g_ime_pin;
c.on_change = ime_pin_change;
c.ctx = NULL;
g_ime_pin[0] = '\0';
settings_note("", 0);
snprintf(g_prompt, sizeof g_prompt, "%s", prompt);
settings_repaint(); /* 先画提示条(键盘区背景快照从当前帧取) */
(void)meow_ui_text_input(&c);
g_prompt[0] = '\0';
snprintf(out, (size_t)outsz, "%s", g_ime_pin);
return out[0] != '\0';
}
/* 两遍输入(开启/修改共用):1=一致且合法(写入 out);0=取消/不一致/非法(已置 note)。 */
static int settings_pin_two_pass(const char *prompt, char *out, int outsz)
{
char a[32] = "", b[32] = "";
if (!settings_read_pin(prompt, a, (int)sizeof a)) {
settings_note("已取消", 0);
return 0;
}
if (!mr_pin_valid(a)) {
settings_note("PIN 需为 4-8 位数字,未保存", 1);
return 0;
}
if (!settings_read_pin("再次输入以确认", b, (int)sizeof b)) {
settings_note("已取消", 0);
return 0;
}
if (strcmp(a, b) != 0) {
settings_note("两次输入不一致,未保存", 1);
return 0;
}
snprintf(out, (size_t)outsz, "%s", a);
return 1;
}
static void settings_toggle_pin(void)
{
if (!g_pin_on) {
char pin[32] = "";
if (!settings_pin_two_pass("设置启动 PIN(4-8 位数字)", pin, (int)sizeof pin)) return;
int r = mr_pin_set(pin);
if (r == MR_PIN_OK) settings_note("启动 PIN 已开启 · 下次启动生效", 0);
else settings_note("保存失败:数据目录不可写", 1);
g_pin_on = mr_pin_enabled();
return;
}
int r = mr_pin_disable();
if (r == MR_PIN_OK) settings_note("启动 PIN 已关闭", 0);
else settings_note("关闭失败:配置不可写", 1);
g_pin_on = mr_pin_enabled();
}
static void settings_change_pin(void)
{
if (!g_pin_on) {
settings_note("请先开启启动 PIN", 0);
return;
}
char pin[32] = "";
if (!settings_pin_two_pass("设置新 PIN(4-8 位数字)", pin, (int)sizeof pin)) return;
int r = mr_pin_set(pin); /* 重设即换新盐;旧 PIN 失效 */
if (r == MR_PIN_OK) settings_note("PIN 已更新 · 下次启动生效", 0);
else settings_note("保存失败:数据目录不可写", 1);
g_pin_on = mr_pin_enabled();
}
/* ============================================================ */
/* 行数据源(src2:每帧重取 → 开关/副标题随状态即时刷新) */
/* ============================================================ */
static int src_dir(int idx, meow_list_item *it, void *ctx)
{
(void)ctx;
if (idx != 0) return 0;
memset(it, 0, sizeof *it);
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
it->title = "阅读方向";
it->subtitle = rtl ? "右到左 · 日式漫画(RTL)" : "左到右 · 常规漫画(LTR)";
it->right = MEOW_LIST_RIGHT_SWITCH;
it->on = rtl;
it->enabled = 1;
return 1;
}
static int src_pin(int idx, meow_list_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
int on = g_pin_on;
if (idx == ST_PIN_ROW_SWITCH) {
it->title = "启动 PIN";
it->subtitle = on ? "已启用 · 启动时先输入 PIN"
: "未设置 · 启动直接进入书库";
it->right = MEOW_LIST_RIGHT_SWITCH;
it->on = on;
it->enabled = 1;
return 1;
}
if (idx == ST_PIN_ROW_CHANGE) {
it->title = "修改 PIN";
it->subtitle = on ? "重新设置 4-8 位数字" : "开启后可修改";
it->right = MEOW_LIST_RIGHT_ARROW;
it->enabled = on; /* 未启用时变淡且不响应命中 */
return 1;
}
return 0;
}
static int src_about(int idx, meow_list_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
if (idx == 0) {
it->title = "版本";
it->subtitle = "0.1 · os.meow.picbrowser";
it->enabled = 1;
return 1;
}
if (idx == 1) {
theme_line_update();
it->title = "主题";
it->subtitle = g_theme_line;
it->enabled = 1;
return 1;
}
return 0;
}
/* ============================================================ */
/* 布局 */
/* ============================================================ */
/* 置顶提示条(z=MEOW_Z_TOP):键盘面板从屏底覆盖,步骤文案必须画在其上方;
* 仅 g_prompt 非空时绘制,否则透明不挡顶栏。
* 注:meow_widget 对 App 不透明(公共头只给句柄),坐标由本文件自己记录。 */
static void settings_overlay_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)w; (void)ctx;
if (!g_prompt[0]) return;
int wd = meow_ui_width();
meow_ui_fill_rect(0, g_ov_y, wd, g_ov_h, th->surfaceContainerHighest);
meow_ui_text_center(wd / 2, g_ov_y + 28, g_prompt, th->font_title, th->onSurface);
meow_ui_text_center(wd / 2, g_ov_y + 76, "输入后关闭键盘即校验 · 空输入=取消",
th->font_body, th->onSurfaceVariant);
meow_ui_fill_rect(0, g_ov_y + g_ov_h - 1, wd, 1, th->outlineVariant);
}
static void settings_build(void)
{
/* 控件先以占位矩形创建,统一由 settings_place() 摆位(避免两处布局数学不一致)。 */
g_tb = meow_topappbar_create(0, 0, 1, 1, "设置");
if (g_tb) meow_topappbar_set_back(g_tb, 1);
g_lv_dir = meow_listview_create(0, 0, 1, 1, 1, src_dir, NULL, MEOW_LIST_SEL_NONE);
g_lv_pin = meow_listview_create(0, 0, 1, 1, 2, src_pin, NULL, MEOW_LIST_SEL_NONE);
g_lv_about = meow_listview_create(0, 0, 1, 1, 2, src_about, NULL, MEOW_LIST_SEL_NONE);
/* 列表:MD3 容器卡 + 双行(标题/副标题)+ 右尾件(开关/箭头)。
* 容器模式由 SDK 默认 renderer 绘制(surfaceContainer 圆角卡 + 行间空隙)。 */
meow_widget *lvs[3] = { g_lv_dir, g_lv_pin, g_lv_about };
for (int i = 0; i < 3; i++) {
if (!lvs[i]) continue;
meow_listview_set_item_h(lvs[i], ST_ROW_H);
meow_listview_set_item_renderer(lvs[i], 2, 0, 1);
meow_listview_set_container(lvs[i], 1);
}
g_overlay = meow_widget_custom_create(0, 0, 1, 1,
settings_overlay_render, NULL, NULL);
if (g_overlay) meow_widget_set_z(g_overlay, MEOW_Z_TOP);
}
/* 布局参数(窗口可能变化;每次进入重新摆位)。 */
static void settings_place(meow_view *v)
{
int w = meow_ui_width();
int sb = meow_ui_statusbar_height();
v->x = 0; v->y = 0; v->w = w; v->h = meow_ui_height();
int lx = 8, lw = w - 16;
int y = sb + ST_TOP_H + 6;
if (g_tb) meow_widget_set_rect(g_tb, 0, sb, w, ST_TOP_H);
g_yl_dir = y; y += ST_SEC_H;
if (g_lv_dir) meow_widget_set_rect(g_lv_dir, lx, y, lw, ST_ROW_H);
y += ST_ROW_H + ST_GAP;
g_yl_pin = y; y += ST_SEC_H;
if (g_lv_pin) meow_widget_set_rect(g_lv_pin, lx, y, lw, ST_ROW_H * 2);
y += ST_ROW_H * 2 + ST_GAP;
g_yl_about = y; y += ST_SEC_H;
if (g_lv_about) meow_widget_set_rect(g_lv_about, lx, y, lw, ST_ROW_H * 2);
y += ST_ROW_H * 2 + 14;
g_note_y = y;
g_ov_y = sb;
g_ov_h = 128;
if (g_overlay) meow_widget_set_rect(g_overlay, 0, g_ov_y, w, g_ov_h);
}
static void settings_section_label(int y, const char *text, const meow_theme *th)
{
meow_ui_text(24, y + 6, text, th->font_body, th->primary);
}
/* ============================================================ */
/* 绘制 */
/* ============================================================ */
static void settings_draw(meow_view *v, const meow_theme *th, void *ctx) static void settings_draw(meow_view *v, const meow_theme *th, void *ctx)
{ {
(void)v; (void)ctx; (void)v; (void)ctx;
int w = meow_ui_width(); int w = meow_ui_width();
int sb = meow_ui_statusbar_height();
meow_ui_fill_rect(0, sb, w, 56, th->surfaceContainer); settings_section_label(g_yl_dir, "阅读", th);
meow_ui_text(w / 2, sb + 16, "Settings", th->font_title, th->onSurface); settings_section_label(g_yl_pin, "安全", th);
meow_ui_text_center(w / 2, sb + 100, "APP options", th->font_body, th->onSurfaceVariant); settings_section_label(g_yl_about, "关于", th);
meow_ui_text_center(w / 2, sb + 140, "Startup PIN — M5", th->font_body, th->onSurface);
meow_ui_text_center(w / 2, sb + 180, "Reader direction — M4", th->font_body, if (g_note[0]) {
th->onSurface); meow_ui_text(24, g_note_y, g_note, th->font_body,
meow_ui_text_center(w / 2, sb + 240, "Theme follows the system automatically", g_note_err ? th->error : th->primary);
th->font_body, th->onSurfaceVariant); }
meow_ui_text_center(w / 2, sb + 300, "left-edge swipe = back", th->font_body, int hy = g_note_y + (g_note[0] ? 30 : 0);
th->onSurfaceVariant); meow_ui_text(24, hy, "改动即时保存,下次启动生效", th->font_body, th->onSurfaceVariant);
meow_ui_text(24, hy + 28, "PIN 仅防随手查看,非安全边界", th->font_body,
th->onSurfaceVariant);
meow_ui_fill_rect(0, meow_ui_height() - 1, w, 1, th->outlineVariant);
} }
/* ============================================================ */
/* 事件 */
/* ============================================================ */
static int settings_on_event(meow_view *v, int ev, int a, int b, void *ctx)
{
(void)v; (void)ctx;
if (ev == MR_VEV_TAP) {
int x = a, y = b;
if (g_tb) {
int hit = meow_topappbar_hit(g_tb, x, y);
if (hit == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
if (hit >= 0) return 1;
}
if (g_lv_dir) {
int idx = meow_listview_hit(g_lv_dir, x, y);
if (idx == 0) {
mr_reader_set_direction(mr_reader_get_direction() == MR_DIR_RTL
? MR_DIR_LTR : MR_DIR_RTL);
settings_note(mr_reader_get_direction() == MR_DIR_RTL
? "阅读方向:右到左(RTL)"
: "阅读方向:左到右(LTR)", 0);
settings_repaint();
return 1;
}
}
if (g_lv_pin) {
int idx = meow_listview_hit(g_lv_pin, x, y);
if (idx == ST_PIN_ROW_SWITCH) {
settings_toggle_pin();
settings_repaint();
return 1;
}
if (idx == ST_PIN_ROW_CHANGE) {
settings_change_pin();
settings_repaint();
return 1;
}
}
if (g_lv_about) {
int idx = meow_listview_hit(g_lv_about, x, y);
if (idx == 1) {
settings_note("主题跟随系统设置,App 内不可改", 0);
settings_repaint();
return 1;
}
if (idx == 0) return 1; /* 版本行:仅展示 */
}
return 0;
}
/* SWIPE 不消费:app.c 对非 library 页的左缘返回统一 page_pop。 */
return 0;
}
/* ============================================================ */
/* view 生命周期 */
/* ============================================================ */
static void settings_page_enter(meow_view *v, void *ctx) static void settings_page_enter(meow_view *v, void *ctx)
{ {
((mr_page_ctrl *)ctx)->active = 1; ((mr_page_ctrl *)ctx)->active = 1;
v->x = 0; meow_ui_set_fullscreen(0); /* 保留系统状态栏(与书库一致) */
v->y = 0;
v->w = meow_ui_width();
v->h = meow_ui_height();
meow_ui_set_fullscreen(0);
meow_ui_set_page_bg(meow_theme_get()->bg); meow_ui_set_page_bg(meow_theme_get()->bg);
if (!g_tb) {
/* 栈顶此时已是 ACT_SETTINGS(page_push 先 push 再 on_enter)→ 控件 owner 正确。 */
settings_build();
meow_widget *all[5] = { g_tb, g_lv_dir, g_lv_pin, g_lv_about, g_overlay };
for (int i = 0; i < 5; i++)
if (all[i]) meow_view_add_widget(v, all[i]);
}
settings_place(v);
g_pin_on = mr_pin_enabled(); /* 进入页时读一次;页内改动由操作函数同步 */
mr_nav_report(); mr_nav_report();
meow_ui_paint_full(); meow_ui_paint_full();
} }
@@ -50,12 +425,25 @@ static void settings_page_leave(meow_view *v, void *ctx)
((mr_page_ctrl *)ctx)->active = 0; ((mr_page_ctrl *)ctx)->active = 0;
} }
static void settings_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
/* 控件随 meow_view_free 统一释放(v->widgets);这里只清指针,避免悬垂。 */
g_tb = NULL;
g_lv_dir = NULL;
g_lv_pin = NULL;
g_lv_about = NULL;
g_overlay = NULL;
}
meow_view *settings_view_create(void) meow_view *settings_view_create(void)
{ {
static const meow_view_hooks h = { static const meow_view_hooks h = {
.on_enter = settings_page_enter, .on_enter = settings_page_enter,
.on_leave = settings_page_leave, .on_leave = settings_page_leave,
.on_draw = settings_draw, .on_destroy = settings_page_destroy,
.on_draw = settings_draw,
.on_event = settings_on_event,
}; };
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl); return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
} }