feat(meowpanel): 通知中心+文件管理后端补全 (agent-182) — /api/notifications(读TF归档,环形200条)+单删+dismiss_all(保留媒体/进度活动通知); 文件域修4bug(normalize_path丢前导/致白名单必失败、upload multipart 文件名截断恒NULL、rename拼错路径恒ENOTDIR、read>1MB改truncated) + 上传8MB上限+文件名净化+双重白名单防逃逸

meowpanel 8108852f→58a7b186; mipsel/host -Wall -Wextra 0 warning; 宿主 curl 自测全过
需launcher配合项(已记录): MSG_NOTIFY_QUERY 查询帧 + dismiss 广播 IPC
This commit is contained in:
MeowOS committed 2026-08-31 04:14:00 +08:00
1 parent c187b7bce3
commit 563a9be1d6
5 files changed
+438 -75

No files matched your search

+382 -64
View File
@@ -56,10 +56,21 @@
#define DEFAULT_PORT 8080
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
#define SETTINGS_HDR "# meow-settings 1"
#define TF_ROOT "/usr/data/mnt/sd_0"
#define TF_CARD_DIR "/usr/data/mnt/sd_0/MeowOS"
#define MEOWAPPS_DIR "/usr/data/mnt/sd_0/MeowAPPs"
#define LOGS_DIR "/usr/data/mnt/sd_0/MeowOS/logs"
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
static char g_tf_card_buf[2048];
static char g_apps_dir_buf[2048];
static char g_logs_dir_buf[2048];
static char g_notif_log_buf[2048];
static const char *g_tf_card = NULL; /* 派生:<root>/MeowOS */
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
#define TF_ROOT g_tf_root_buf
#define TF_CARD_DIR g_tf_card
#define MEOWAPPS_DIR g_apps_dir
#define LOGS_DIR g_logs_dir
#define PERF_SOCK "/usr/data/meow_perf.sock"
#define POWER_SOCK "/usr/data/meow_power.sock"
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
@@ -70,10 +81,13 @@
#define IPC_TIMEOUT_MS 2000
#define MAX_HTTP_HEADER (64 * 1024)
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 上传上限 */
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
#define LOG_MAX_LINES 1000
#define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */
#define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */
/* 白名单子目录(TF 卡根下) */
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
@@ -89,6 +103,29 @@ static const char *g_sensitive_names[] = {
static const char *g_sock_perf = PERF_SOCK;
static const char *g_sock_power = POWER_SOCK;
/* 初始化 TF 卡派生路径(main 开头调用)。env MEOW_TF_ROOT 可覆盖卡根(宿主测试用);
* env MEOW_NOTIF_LOG 可覆盖通知转储日志路径(宿主测试用)。默认设备卡路径。 */
static void init_tf_paths(void)
{
const char *env = getenv("MEOW_TF_ROOT");
if (env && env[0]) {
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
size_t l = strlen(g_tf_root_buf);
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
}
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
env = getenv("MEOW_NOTIF_LOG");
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
g_tf_card = g_tf_card_buf;
g_apps_dir = g_apps_dir_buf;
g_logs_dir = g_logs_dir_buf;
g_notif_log = g_notif_log_buf;
}
/* ---------------- 有界动态字符串(sbuf) ---------------- */
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
@@ -146,6 +183,16 @@ static void sA_str(sbuf_t *s, const char *v)
sA(s, "\"");
}
/* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */
static void sA_append(sbuf_t *s, const char *b, size_t n)
{
if (!b || n == 0) return;
if (sb_reserve(s, n) < 0) return;
memcpy(s->b + s->n, b, n);
s->n += n;
s->b[s->n] = 0;
}
static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */
@@ -156,6 +203,7 @@ static int send_ok(int sock, const char *data);
static int send_err(int sock, int status, const char *code, const char *msg);
static int send_json(int sock, int status, const char *data);
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
static void panel_log(const char *fmt, ...);
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
@@ -795,6 +843,7 @@ done:
/* ---------------- 路径安全 ---------------- */
static int tf_present(void)
{
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
FILE *f = fopen("/proc/mounts", "r");
if (!f) return 0;
char line[512];
@@ -836,7 +885,8 @@ static int is_sensitive_name(const char *base)
static int normalize_path(const char *in, char *buf, size_t buflen)
{
size_t rl = strlen(TF_ROOT);
char joined[1024];
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
char joined[2048];
if (in[0] == '/') {
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
snprintf(joined, sizeof joined, "%s", in);
@@ -849,10 +899,13 @@ static int normalize_path(const char *in, char *buf, size_t buflen)
} else {
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
}
/* 逐分量规范化:/a/b/../c → /a/c;拒绝逃逸出 root */
char out[1024];
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
char out[2048];
size_t n = 0;
out[0] = 0;
out[0] = '/';
out[1] = 0;
n = 1;
char *p = joined;
while (*p) {
while (*p == '/') p++;
@@ -863,24 +916,27 @@ static int normalize_path(const char *in, char *buf, size_t buflen)
seg[k] = 0;
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
if (!strcmp(seg, "..")) {
/* 弹出一级;若已在 root 再 .. 则逃逸 */
if (n == 0) return -1;
while (n > 0 && out[n - 1] != '/') n--;
if (n > 0) n--; /* 去掉结尾 / */
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
if (n <= 1) return -1;
while (n > 1 && out[n - 1] != '/') n--;
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
out[n] = 0;
continue;
}
if (k > 0) {
size_t need = strlen(out) + 1 + k + 1;
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
if (need >= sizeof out) return -1;
if (out[0]) strncat(out, "/", sizeof out - strlen(out) - 1);
strncat(out, seg, sizeof out - strlen(out) - 1);
if (n > 1) { out[n++] = '/'; out[n] = 0; }
memcpy(out + n, seg, (size_t)k);
n += (size_t)k;
out[n] = 0;
}
}
/* 空 → root */
if (!out[0]) snprintf(out, sizeof out, "%s", TF_ROOT);
if (strlen(out) >= buflen) return -1;
snprintf(buf, buflen, "%s", out);
/* 空/根(n==1,仅 "/")→ 卡根 */
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
size_t ol = strlen(out);
if (ol >= buflen) return -1;
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
return 0;
}
@@ -921,16 +977,22 @@ static int files_list(int sock, const char *relpath)
{
char abs[1024], rel[1024];
if (normalize_path(relpath, abs, sizeof abs) < 0)
return 403;
if (!path_in_whitelist(abs)) return 403;
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
if (!path_in_whitelist(abs))
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
struct stat st;
if (stat(abs, &st) < 0) return 404;
if (!S_ISDIR(st.st_mode)) return 404;
if (stat(abs, &st) < 0)
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
if (!S_ISDIR(st.st_mode))
return send_err(sock, 404, "NOT_FOUND", "非目录");
DIR *d = opendir(abs);
if (!d) return 500;
if (!d)
return send_err(sock, 500, "INTERNAL", "读目录失败");
make_rel(abs, rel, sizeof rel);
sbuf_t s = {0};
sA(&s, "{\"root\":\"" TF_ROOT "\",\"path\":");
sA(&s, "{\"root\":");
sA_str(&s, TF_ROOT);
sA(&s, ",\"path\":");
sA_str(&s, abs);
sA(&s, ",\"rel\":");
sA_str(&s, rel);
@@ -962,17 +1024,19 @@ static int files_list(int sock, const char *relpath)
/* send_json 前置声明 */
static int send_json(int sock, int status, const char *data);
/* 读文本文件(<1MB),返回 malloc 内容;二进制检测 -2;太大 -3;打不开 -1。 */
/* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated):
* 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */
static char *read_text_file_limited(const char *abs, long long *size, int *truncated)
{
int fd = open_safe_for_read(abs, 0);
if (fd < 0) return NULL;
struct stat st;
if (fstat(fd, &st) < 0 || st.st_size > 1024 * 1024) { close(fd); return NULL; }
if (st.st_size < 0) { close(fd); return NULL; }
char *buf = malloc((size_t)st.st_size + 1);
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
long long total = (long long)st.st_size;
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
char *buf = malloc(cap + 1);
if (!buf) { close(fd); return NULL; }
ssize_t n = read(fd, buf, (size_t)st.st_size);
ssize_t n = read(fd, buf, cap);
close(fd);
if (n < 0) { free(buf); return NULL; }
buf[n] = 0;
@@ -984,8 +1048,8 @@ static char *read_text_file_limited(const char *abs, long long *size, int *trunc
if (c < 0x09) ctrl++;
}
if (ctrl > n / 10) { free(buf); return (char *)-2; }
if (size) *size = (long long)n;
if (truncated) *truncated = 0;
if (size) *size = total;
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
return buf;
}
@@ -1286,7 +1350,10 @@ static int read_request(int sock, req_t *r)
}
char *body_start = buf + hend + 4; /* body 实际起点 */
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
if (clen > MAX_HTTP_BODY) { free(buf); return -1; }
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 */
size_t cap = MAX_HTTP_BODY;
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = MAX_UPLOAD_BODY;
if (clen > cap) { free(buf); return -2; }
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
char *body = malloc(clen + 1);
if (!body) { free(buf); return -1; }
@@ -1425,7 +1492,28 @@ static int send_file(int sock, int status, const char *ctype, const char *abspat
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
*/
typedef struct { char path[1400]; char filename[256]; } upfile_t;
typedef struct { char path[1400]; char filename[320]; } upfile_t;
/* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。
* 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */
static int safe_filename(const char *name, char *out, size_t cap)
{
if (!name || !name[0]) return -1;
const char *b = name;
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
const char *slash = strrchr(b, '/');
const char *bsl = strrchr(b, '\\');
if (bsl && (!slash || bsl > slash)) slash = bsl;
if (slash) b = slash + 1;
if (!b[0]) return -1;
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
for (const char *p = b; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
}
snprintf(out, cap, "%s", b);
return 0;
}
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
{
@@ -1469,10 +1557,12 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
char *l = ln;
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
char *nm = strstr(l, "name=\"");
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
/* name 与 filename 常同行(name="file"; filename="up.txt")。
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
char *fn = strstr(l, "filename=\"");
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
char *nm = strstr(l, "name=\"");
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
}
}
/* 数据结束:下一个 "--boundary" 之前 */
@@ -1487,18 +1577,25 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
curpath[dl] = 0;
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
size_t dl = (size_t)(data_end - data_start);
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", fname);
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", curpath);
/* 写文件 */
char abs[1024];
if (normalize_path(files[nfiles].path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { nfiles++; continue; }
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s/%s", abs, fname);
int fd = open(files[nfiles].path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
char safe[260];
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
/* 目标目录规范化 + 白名单校验 */
char absdir[1024];
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
char joined[1400];
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
char abs[1400];
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write_full(fd, data_start, dl);
close(fd);
nfiles++;
}
nfiles++;
/* fd<0(不可写)→ 不计数,跳过该文件 */
}
p = data_end;
}
@@ -2065,7 +2162,9 @@ static int api_logs_list(int sock)
DIR *d = opendir(LOGS_DIR);
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
sbuf_t s = {0};
sA(&s, "{\"dir\":\"" LOGS_DIR "\",\"files\":[");
sA(&s, "{\"dir\":");
sA_str(&s, LOGS_DIR);
sA(&s, ",\"files\":[");
int first = 1;
struct dirent *e;
while ((e = readdir(d))) {
@@ -2164,23 +2263,224 @@ static int api_logs_read(int sock, const req_t *r)
return rc;
}
/* ---------------- 通知(后端跨进程不可读 → 501) ---------------- */
/* 注:通知存储为 launcher 进程内存(meow_notif.c g_slots[],NOTIFICATION_SYSTEM §4),
* 后端为独立 svcd 子进程,跨进程无法读取/清撤那批内存槽;框架仅在关机前把通知 dump 到
* TF 卡 MeowOS/notifications/notifications.log(CSV 追加,非活跃快照)。故本后端无法实现
* 活跃通知列表/清撤 —— 需为通知后端另开 IPC 查询(如 MSG_NOTIFY_LIST 应答)或持久化活跃集,
* 见 MEOWPANEL_PROTOCOL §6 待定案。当前一律 501。 */
/* ---------------- 通知(数据源 = TF 卡通知转储日志) ----------------
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
* 1) launcher IPC 查询 —— meow_ipc.h 已定义 MSG_NOTIFY_LIST(0x23),但 launcher 侧仅作
* 守护→App 下发行(util.c:1079「not dispatched here」),**无客户端查询请求类型**,
* 后端(svcd 独立进程)无法据此取到活跃清单 → 跳过,回退 ②。详见报告「需 launcher 侧配合项」。
* 2) TF 卡通知转储文件 `<卡根>/MeowOS/notifications/notifications.log`:
* launcher 把每条通知归档时追加一行 CSV(meow_notif.c tf_line:CSV 追加 + 10MB 滚动,开机清空)。
* 字段顺序:timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,
* ongoing,media,dismissed(前 6/7/11/12/13 为裸整数,其余引号包裹 + 内部 " 翻倍)。
* 注意:**归档发生在置 dismissed=1 之前(meow_notif_dismiss 先 archive_one 后置 dismissed)**,
* 故日志的 dismissed 列恒 ≈0(不可靠),且日志是「会话级归档」,非活跃快照。
* 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},**不伪造数据**。
* clear(dismiss_all)语义:保留活动中的媒体/进度通知(ongoing||media||progress>=0||category=="media"),
* 其余从日志移除。单条删除:按 id 移除该行。 */
typedef struct {
long long ms; /* created_ms(epoch ms) */
unsigned id;
char app_id[40];
char app_icon[64];
char category[40];
int urgency;
int progress; /* -1=无 */
char summary[96];
char body[128];
char target[64];
int ongoing, media, dismissed;
} notif_rec_t;
/* 取一个 CSV 字段(引号包裹 → 解转义;裸字段 → 原样;末后跳过 ',')。返回 0。 */
static int notif_csv_field(const char **pp, char *out, int outcap)
{
const char *p = *pp;
int o = 0;
if (*p == '"') {
p++;
while (*p && *p != '"') {
if (*p == '"' && p[1] == '"') {
if (o < outcap - 1) out[o++] = '"';
p += 2;
} else {
if (o < outcap - 1) out[o++] = *p;
p++;
}
}
if (*p == '"') p++;
} else {
while (*p && *p != ',' && *p != '\n' && *p != '\r') {
if (o < outcap - 1) out[o++] = *p;
p++;
}
}
if (o < outcap) { out[o] = 0; }
else if (outcap > 0) { out[outcap - 1] = 0; }
if (*p == ',') p++;
*pp = p;
return 0;
}
static int notif_parse_line(const char *line, notif_rec_t *r)
{
const char *p = line;
char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8];
char app[40], icon[64], cat[40], sum[96], body[128], tgt[64];
notif_csv_field(&p, f1, sizeof f1);
notif_csv_field(&p, f2, sizeof f2);
notif_csv_field(&p, app, sizeof app);
notif_csv_field(&p, icon, sizeof icon);
notif_csv_field(&p, cat, sizeof cat);
notif_csv_field(&p, f6, sizeof f6);
notif_csv_field(&p, f7, sizeof f7);
notif_csv_field(&p, sum, sizeof sum);
notif_csv_field(&p, body, sizeof body);
notif_csv_field(&p, tgt, sizeof tgt);
notif_csv_field(&p, f11, sizeof f11);
notif_csv_field(&p, f12, sizeof f12);
notif_csv_field(&p, f13, sizeof f13);
r->ms = strtoll(f1, NULL, 10);
r->id = (unsigned)strtoul(f2, NULL, 10);
r->urgency = atoi(f6);
r->progress = atoi(f7);
r->ongoing = atoi(f11);
r->media = atoi(f12);
r->dismissed = atoi(f13);
snprintf(r->app_id, sizeof r->app_id, "%s", app);
snprintf(r->app_icon, sizeof r->app_icon, "%s", icon);
snprintf(r->category, sizeof r->category, "%s", cat);
snprintf(r->summary, sizeof r->summary, "%s", sum);
snprintf(r->body, sizeof r->body, "%s", body);
snprintf(r->target, sizeof r->target, "%s", tgt);
return 0;
}
/* 读通知日志,保留最新 maxrec 条(环形缓冲,避免全量进内存)。返回条数;-1=文件不存在/无卡。 */
static int notif_load_list(notif_rec_t *recs, int maxrec)
{
FILE *f = fopen(g_notif_log, "r");
if (!f) return -1;
int total = 0, head = 0;
char line[1400];
notif_rec_t r;
while (fgets(line, sizeof line, f)) {
if (notif_parse_line(line, &r) != 0) continue;
recs[head] = r;
head = (head + 1) % maxrec;
if (total < maxrec) total++;
}
fclose(f);
if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */
/* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */
notif_rec_t tmp[NOTIF_PREVIEW_MAX];
for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec];
memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]);
return total;
}
/* app_id → 展示名("system" → "系统";其余用 app_id 兜底,前端可回退)。 */
static const char *notif_app_name(const char *app_id)
{
if (!strcmp(app_id, "system")) return "系统";
return app_id;
}
static void notif_emit_item(sbuf_t *out, const notif_rec_t *r)
{
sA(out, "{\"id\":%u,\"app_id\":", r->id);
sA_str(out, r->app_id);
sA(out, ",\"app_name\":");
sA_str(out, notif_app_name(r->app_id));
sA(out, ",\"app_icon\":");
sA_str(out, r->app_icon);
sA(out, ",\"category\":");
sA_str(out, r->category);
sA(out, ",\"summary\":");
sA_str(out, r->summary);
sA(out, ",\"body\":");
sA_str(out, r->body);
sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0);
if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null");
sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":",
r->ongoing ? "true" : "false", r->media ? "true" : "false");
sA_str(out, r->target);
sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false");
}
static int api_notifications(int sock)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知存储为 launcher 进程内存,后端跨进程不可读(需通知后端暴露 IPC 查询/清撤)");
notif_rec_t recs[NOTIF_PREVIEW_MAX];
int n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
if (n < 0) return send_json(sock, 200, "{\"count\":0,\"items\":[],\"notifications\":[],\"source\":\"none\"}");
sbuf_t arr = {0};
int first = 1;
for (int i = n - 1; i >= 0; i--) { /* newest first */
if (!first) sA(&arr, ",");
first = 0;
notif_emit_item(&arr, &recs[i]);
}
sbuf_t s = {0};
sA(&s, "{\"count\":%d,\"source\":\"tf\",\"items\":[", n);
if (arr.n) sA_append(&s, arr.b, arr.n);
sA(&s, "],\"notifications\":[");
if (arr.n) sA_append(&s, arr.b, arr.n);
sA(&s, "]}");
sbuf_free(&arr);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
static int api_notification_delete(int sock)
/* 重写通知日志。mode=1:保留活动媒体/进度(ongoing||media||progress>=0||category=="media"),
* 其余删除(dismiss_all 语义,对应通知中心既定语义);mode=0:删除指定 id(单条删除)。
* 返回删除行数;-1=IO 错;0=无文件(无可删)。写临时文件后 rename(避免与 launcher append 竞写)。 */
static int notif_rewrite(int mode, unsigned del_id)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清撤需 launcher 通知后端 IPC,后端未实现");
if (!g_notif_log || !g_notif_log[0]) return -1;
char tmppath[1400];
snprintf(tmppath, sizeof tmppath, "%s.panel.tmp", g_notif_log);
FILE *in = fopen(g_notif_log, "r");
if (!in) return 0;
FILE *out = fopen(tmppath, "w");
if (!out) { fclose(in); return -1; }
int removed = 0;
char line[1400];
while (fgets(line, sizeof line, in)) {
notif_rec_t r;
int parsed = (notif_parse_line(line, &r) == 0);
int drop = 0;
if (mode == 1) {
if (!parsed) drop = 1;
else drop = !(r.ongoing || r.media || r.progress >= 0 || !strcmp(r.category, "media"));
} else {
if (parsed && r.id == del_id) drop = 1;
}
if (drop) removed++;
else fputs(line, out);
}
fclose(in);
if (fclose(out) != 0) return -1;
if (rename(tmppath, g_notif_log) < 0) { unlink(tmppath); return -1; }
return removed;
}
static int api_notification_delete(int sock, unsigned id)
{
int removed = notif_rewrite(0, id);
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
if (removed == 0) return send_err(sock, 404, "NOT_FOUND", "通知不存在");
char d[64];
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
return send_json(sock, 200, d);
}
static int api_notification_dismiss_all(int sock)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清空需 launcher 通知后端 IPC,后端未实现");
int removed = notif_rewrite(1, 0);
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
char d[64];
snprintf(d, sizeof d, "{\"dismissed\":%d}", removed);
return send_json(sock, 200, d);
}
/* ---------------- 文件操作 ---------------- */
@@ -2208,7 +2508,7 @@ static int api_files_read(int sock, const req_t *r)
sA_str(&s, abs);
sA(&s, ",\"content\":");
sA_str(&s, txt);
sA(&s, ",\"size\":%lld,\"truncated\":false}", size);
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
free(txt);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
@@ -2318,8 +2618,16 @@ static int api_files_rename(int sock, const req_t *r)
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
char abs[1024], newabs[1400];
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
snprintf(newabs, sizeof newabs, "%s/%s", abs, pnn->str);
/* newabs 也须在白名单(new_name 不含 /,父目录已校验,无需再 normalize) */
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
char parent[1024];
snprintf(parent, sizeof parent, "%s", abs);
char *sl = strrchr(parent, '/');
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
*sl = 0;
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
sbuf_t s = {0};
sA(&s, "{\"item\":{\"name\":");
@@ -2557,7 +2865,9 @@ static int api_apps_export(int sock, const char *pkg)
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
char tmp[1100];
mkdir(TF_CARD_DIR "/tmp", 0777);
char card_tmp[1200];
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
mkdir(card_tmp, 0777);
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
unlink(tmp);
char cmd[1400];
@@ -2668,7 +2978,7 @@ static int handle(int sock, req_t *r)
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
if (!strcmp(p, "/api/notifications/dismiss_all")) return api_notification_dismiss_all(sock);
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
char seg[256];
if (path_segment("/api/key/", p, seg, sizeof seg))
@@ -2721,8 +3031,10 @@ static int handle(int sock, req_t *r)
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
if (!strcmp(r->method, "DELETE")) {
char seg[256];
if (path_segment("/api/notifications/", p, seg, sizeof seg))
return api_notification_delete(sock);
if (path_segment("/api/notifications/", p, seg, sizeof seg)) {
unsigned nid = (unsigned)strtoul(seg, NULL, 10);
return api_notification_delete(sock, nid);
}
}
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
@@ -2757,7 +3069,12 @@ static void handle_conn(int sock)
{
req_t r;
memset(&r, 0, sizeof r);
if (read_request(sock, &r) != 0) { close(sock); return; }
int rr = read_request(sock, &r);
if (rr != 0) {
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
close(sock);
return;
}
handle(sock, &r);
req_free(&r);
close(sock);
@@ -2778,6 +3095,7 @@ int main(int argc, char **argv)
return 0;
}
}
init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */
const char *env = getenv("MEOW_IPC_PERF_SOCK");
if (env && env[0]) g_sock_perf = env;
env = getenv("MEOW_IPC_POWER_SOCK");