feat(sdk): meow_config_* App配置读写API(agent-158)

- 6 API: meow_config_dir/set/get/int/bool/remove; 落 TF MeowData/<pkg>/data/<key>.cfg(env MEOW_DATA_DIR, 回退/tmp/meowcfg_<pid>)
- 与系统设置 meow_settings_*(板载settings.conf) 区分; key清洗[A-Za-z0-9._-]→_ 防路径穿越; 值上限4096; 降级不崩(pthread_once/mutex)
- MEOW_CFG_* 错误码(避开 meow_ipc.h 枚举命名空间, 防宏展开冲突); 数值与 MEOWOS_API/meow_settings.h 同值
- meow_ui.a=92e61fb8(新增config成员) hello=5e31d919不变(0回归, 静态链未拉入新成员); build_sdk 0warning; 宿主test_meow_ui_config ALL PASS
- 未重链launcher(launcher未用config); 遗留真机验证MEOW_DATA_DIR落盘/无卡降级
This commit is contained in:
MeowOS committed 2026-08-30 23:01:31 +08:00
1 parent 9c733fcf73
commit dcba5975a2
4 files changed
+388 -1

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
/*
* 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);
}