Files
MeowOS-SDK/sdk/meow_ui_config.c
T
MeowOS f44e3bc6d0 Add 'sdk/' from commit '2e7561d74f75de05dd7fb362f203f600afb451d7'
git-subtree-dir: sdk
git-subtree-mainline: 17a972d46f
git-subtree-split: 2e7561d74f
2026-09-01 21:08:40 +08:00

227 lines
9.1 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.
/*
* 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);
}