/* * meow_ui_config.c — MeowOS 应用配置读写 API(M2.x,§9.2)。 * * 背景:第三/系统 App 需要读写「自己的配置」(播放列表偏好、UI 选项、最近列表等)。 * 此前 SDK 无统一接口,App 只能自己造轮子(自读 env + snprintf + fopen/fprintf), * 重复、易错、无失败降级。本文件实现 SDK 统一 meow_config_*: * * - app 一行 meow_config_set/get 读写键值,SDK 管「数据目录解析 / 文件布局 / 降级」; * - 数据目录:优先 env MEOW_DATA_DIR(launcher 注入 /usr/data/mnt/sd_0/MeowData//data, * 见 app_launcher.c launch_app,exFAT 无 symlink → env 传址);无 env(手动跑/宿主测/无卡) * 回退 /tmp/meowcfg_(非持久但仍可写); * - 文件布局:每键一小文件 /.cfg,内容=值字符串(空串=0 字节文件); * key 只允许 [A-Za-z0-9._-],其余字符(含 '/')替换为 '_',杜绝路径穿越/子目录注入; * - 轻量:每次 get 读文件(App 配置小),不做大内存缓存;pthread_once 惰性初始化目录; * - 线程安全:pthread_mutex 串行库内单键操作(不同 key 独立文件,无共享读写竞态); * - 失败降级:无数据目录/建目录失败/读写失败 → 返回负错误码(MEOW_CFG_ERR_*),不崩不阻塞。 * * 与 meow_settings_*(系统设置)的界限:settings 是「系统/MeowOS 设置」(板载 * /usr/data/meow/settings.conf、带 schema/取值域/枚举/listener),config 是「App 自有 KV」 * (TF 数据目录、raw 字符串、无 schema)。二者命名/落点/语义不同层;本文件**不 import** * meow_settings,纯 libc 完成,可宿主 gcc 直接编译单测。 * * 约定:与 meow_ui_log.c 同风格(M2.x 中文注释 + pthread_once 惰性 init);纯 libc,零依赖。 */ #include "meow_ui.h" #include #include #include #include #include #include #include #include #include /* 内部常量(轻量键值小文件,见头注释) */ #define MEOW_CFG_DIR_MAX 320 /* 数据目录绝对路径上限 */ #define MEOW_CFG_KEY_MAX 64 /* key 上限(清洗后) */ #define MEOW_CFG_VAL_MAX 4096 /* 值上限(set 截断;get 按 outsz-1 读) */ #define MEOW_CFG_PATH_MAX (MEOW_CFG_DIR_MAX + MEOW_CFG_KEY_MAX + 8) /* /.cfg + NUL */ /* 内部状态(各字段约定见头注释) */ static pthread_mutex_t g_cfg_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_once_t g_cfg_once = PTHREAD_ONCE_INIT; static char g_cfg_dir[MEOW_CFG_DIR_MAX] = ""; /* 解析后的数据目录(env 或 /tmp 回退) */ static int g_cfg_ready = 0; /* 1=目录已解析且 mkdir 成功 */ static int g_cfg_env = 0; /* 1=来自 MEOW_DATA_DIR(持久),0=/tmp 回退 */ /* mkdir -p:逐分量 mkdir(已存在 EEXIST 视为成功)。返回 0=成功,-1=失败。 * 仿 meow_ui_log.c log_mkdir_p;用于数据目录不存在时补齐(语义幂等)。 */ static int cfg_mkdir_p(const char *path) { char tmp[MEOW_CFG_DIR_MAX]; size_t len, i; if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1; len = strlen(path); memcpy(tmp, path, len + 1); if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */ for (i = 1; i <= strlen(tmp); i++) { if (tmp[i] == '/' || tmp[i] == '\0') { char ch = tmp[i]; tmp[i] = '\0'; if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; tmp[i] = ch; } } return 0; } /* 一次性初始化(pthread_once 保证仅一次):解析数据目录 + mkdir -p + 置 ready。 */ static void cfg_init(void) { const char *env = getenv("MEOW_DATA_DIR"); /* 层序:1) env MEOW_DATA_DIR(launcher 注入,持久);2) 回退 /tmp/meowcfg_。 */ if (env && env[0]) { snprintf(g_cfg_dir, sizeof g_cfg_dir, "%s", env); g_cfg_env = 1; } else { snprintf(g_cfg_dir, sizeof g_cfg_dir, "/tmp/meowcfg_%d", (int)getpid()); g_cfg_env = 0; } /* 去尾斜杠(统一 /.cfg 拼接) */ size_t n = strlen(g_cfg_dir); while (n > 1 && g_cfg_dir[n - 1] == '/') g_cfg_dir[--n] = '\0'; /* mkdir 幂等;失败只降级(g_cfg_ready=0 → meow_config_dir 返回 NULL),不崩。 * 注:env 指向 /data 时 launcher 已建该目录;卡不在/只读 → 此处失败降级。 */ if (cfg_mkdir_p(g_cfg_dir) == 0) g_cfg_ready = 1; } /* key 清洗:只保留 [A-Za-z0-9._-],其余(含 '/'、空白、'..')逐字符替换为 '_'。 * 返回 0=成功(写入 out),-1=空 key/全非法。清洗后无路径分隔,杜绝路径穿越。 */ static int cfg_sanitize_key(const char *key, char *out, int n) { int len = 0; const unsigned char *p; if (!key || n <= 0) return -1; for (p = (const unsigned char *)key; *p && len < n - 1; p++) { unsigned char c = *p; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') out[len++] = (char)c; else out[len++] = '_'; } if (len == 0) return -1; /* 全非法(如纯 '/')→ 拒绝 */ out[len] = '\0'; return 0; } /* 拼 /.cfg 到 path。返回 0=成功,-1=路径超长。 */ static int cfg_path(const char *key, char *path, int n) { const char *d = meow_config_dir(); if (!d) return -1; if (snprintf(path, (size_t)n, "%s/%s.cfg", d, key) >= n) return -1; return 0; } const char *meow_config_dir(void) { /* 不取 g_cfg_mu:pthread_once 保证 cfg_init 只跑一次,且其完成后 g_cfg_dir/g_cfg_ready * 已是稳定快照(once 自带 happens-before/内存序)。set/get/remove 持锁后经 cfg_path 也调 * 本函数——若在此加锁会自我死锁(非递归互斥)。故仅返回只读快照。 */ pthread_once(&g_cfg_once, cfg_init); return g_cfg_ready ? g_cfg_dir : NULL; } int meow_config_set(const char *key, const char *val) { char k[MEOW_CFG_KEY_MAX + 1]; char path[MEOW_CFG_PATH_MAX]; const char *v = val ? val : ""; /* NULL 视为空串(=清空该键) */ size_t vlen, towrite; int fd, rc = MEOW_CFG_OK, s; if (cfg_sanitize_key(key, k, sizeof k) != 0) return MEOW_CFG_ERR_INVAL; vlen = strlen(v); towrite = vlen > MEOW_CFG_VAL_MAX ? MEOW_CFG_VAL_MAX : vlen; /* 值上限截断 */ pthread_mutex_lock(&g_cfg_mu); s = cfg_path(k, path, sizeof path); if (s != 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOMEM; } fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { if (!g_cfg_env) fprintf(stderr, "meow_config: set '%s' open %s: %s\n", k, path, strerror(errno)); pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_IO; } /* 循环写完(防短写;配置小,实际单次 write 即完) */ size_t off = 0; while (off < towrite) { ssize_t w = write(fd, v + off, towrite - off); if (w < 0) { rc = MEOW_CFG_ERR_IO; break; } if (w == 0) { rc = MEOW_CFG_ERR_IO; break; } off += (size_t)w; } close(fd); pthread_mutex_unlock(&g_cfg_mu); return rc; } int meow_config_get(const char *key, char *out, int outsz) { char k[MEOW_CFG_KEY_MAX + 1]; char path[MEOW_CFG_PATH_MAX]; ssize_t n; if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL; out[0] = '\0'; if (cfg_sanitize_key(key, k, sizeof k) != 0) return MEOW_CFG_ERR_INVAL; pthread_mutex_lock(&g_cfg_mu); if (cfg_path(k, path, sizeof path) != 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOMEM; } int fd = open(path, O_RDONLY); if (fd < 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOENT; } /* 未设/不存在 */ n = read(fd, out, (size_t)outsz - 1); if (n < 0) n = 0; close(fd); pthread_mutex_unlock(&g_cfg_mu); out[n] = '\0'; /* 截断/短读均安全 NUL 终止 */ 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; /* 非数字 → def */ return (int)v; } int meow_config_bool(const char *key, int def) { char buf[16]; if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def; if (!strcmp(buf, "1") || !strcmp(buf, "true") || !strcmp(buf, "yes") || !strcmp(buf, "on")) return 1; if (!strcmp(buf, "0") || !strcmp(buf, "false") || !strcmp(buf, "no") || !strcmp(buf, "off")) return 0; return def; } void meow_config_remove(const char *key) { char k[MEOW_CFG_KEY_MAX + 1]; char path[MEOW_CFG_PATH_MAX]; if (cfg_sanitize_key(key, k, sizeof k) != 0) return; /* 非法 key 静默 */ pthread_mutex_lock(&g_cfg_mu); if (cfg_path(k, path, sizeof path) == 0) unlink(path); /* 无目录 / 文件不存在:unlink 失败即静默(void 语义) */ pthread_mutex_unlock(&g_cfg_mu); }