/* 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 #include #include #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; }