git-subtree-dir: sdk git-subtree-mainline:17a972d46fgit-subtree-split:2e7561d74f
227 lines
9.1 KiB
C
227 lines
9.1 KiB
C
/*
|
||
* 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/<pkg>/data,
|
||
* 见 app_launcher.c launch_app,exFAT 无 symlink → env 传址);无 env(手动跑/宿主测/无卡)
|
||
* 回退 /tmp/meowcfg_<pid>(非持久但仍可写);
|
||
* - 文件布局:每键一小文件 <data>/<sanitized_key>.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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
#include <pthread.h>
|
||
|
||
/* 内部常量(轻量键值小文件,见头注释) */
|
||
#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) /* <dir>/<key>.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_<pid>。 */
|
||
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;
|
||
}
|
||
|
||
/* 去尾斜杠(统一 <dir>/<key>.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 指向 <pkg>/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;
|
||
}
|
||
|
||
/* 拼 <dir>/<key>.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);
|
||
}
|