refactor(panel): 前后端按模块拆分+屏幕/控制合并页 — 后端meowpanel.c 6372行拆src/*.c+mp.h(235函数原型,20端点output byte-identical,0warning)+前端拆mp_shared/mp_pages/meowpanel三层;屏幕·遥控合并(左屏实时画面+右MD3 SVG矢量按钮无emoji);重编后热更TF卡

This commit is contained in:
AskaEth committed 2026-09-04 00:56:13 +08:00
1 parent f3ae06d7cd
commit 2dc4cad454
26 files changed
+9317 -2755

No files matched your search

+374
View File
@@ -0,0 +1,374 @@
/* mp_fonts.c — 字体库域:tar.gz 导入/导出 + 列表(与 .mpk 同构,走 busybox gzip|tar 管道)。 */
#include "mp.h"
#define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹) */
void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); }
int safe_dir_segment(const char *s, char *out, size_t cap)
{
if (!s || !s[0]) return -1;
if (!strcmp(s, ".") || !strcmp(s, "..")) return -1;
for (const char *p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1;
}
if (strlen(s) + 1 > cap) return -1;
snprintf(out, cap, "%s", s);
return 0;
}
int unsafe_path(const char *s)
{
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
}
int has_dotdot_comp(const char *p)
{
const char *s = p;
while (*s) {
if (s[0] == '.' && s[1] == '.' &&
(s == p || s[-1] == '/') && (s[2] == 0 || s[2] == '/'))
return 1;
s++;
}
return 0;
}
int sanitize_member(const char *in, char *out, int n)
{
if (!in || !*in) return -1;
const char *p = in;
while (*p == '/' || (p[0] == '.' && p[1] == '/')) p++;
if (*p == '/' || !*p) return -1;
if (has_dotdot_comp(p)) return -1;
if (unsafe_path(p)) return -1;
size_t l = strlen(p);
if (l + 1 > (size_t)n) return -1;
memcpy(out, p, l + 1); /* memcpy 而非 snprintf:p 可能来自长行缓冲,防 -Wformat-truncation 误报 */
return 0;
}
int mkdir_p(const char *path)
{
char buf[1600];
snprintf(buf, sizeof buf, "%s", path);
for (char *p = buf + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
}
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
}
const jv_t *font_json_str(const jv_t *root, const char *key)
{
const jv_t *v = jv_obj_get(root, key);
if (v && v->type == J_STR && v->str) return v;
return NULL;
}
int api_fonts_list(int sock)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char root[1100];
fonts_root(root, sizeof root);
DIR *fd = opendir(root);
if (!fd) return send_json(sock, 200, "{\"fonts\":[]}");
sbuf_t s = {0};
sA(&s, "{\"fonts\":[");
int first = 1;
struct dirent *fe;
while ((fe = readdir(fd))) {
if (fe->d_name[0] == '.') continue;
char famdir[1700];
snprintf(famdir, sizeof famdir, "%s/%s", root, fe->d_name);
struct stat fst;
if (stat(famdir, &fst) < 0 || !S_ISDIR(fst.st_mode)) continue;
DIR *wd = opendir(famdir);
if (!wd) continue;
struct dirent *we;
while ((we = readdir(wd))) {
if (we->d_name[0] == '.') continue;
char wdir[2200];
snprintf(wdir, sizeof wdir, "%s/%s", famdir, we->d_name);
struct stat wst;
if (stat(wdir, &wst) < 0 || !S_ISDIR(wst.st_mode)) continue;
char pp[2560], pi[2560], pj[2560];
snprintf(pp, sizeof pp, "%s/font.png", wdir);
snprintf(pi, sizeof pi, "%s/font.idx", wdir);
snprintf(pj, sizeof pj, "%s/font.json", wdir);
if (access(pp, R_OK) || access(pi, R_OK) || access(pj, R_OK)) continue;
/* 解析 font.json(可选增强字段) */
char name[256] = "", tier[64] = "";
long long gc = 0;
char jbuf[4096];
if (read_small_file(pj, jbuf, sizeof jbuf) >= 0) {
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
if (jr) {
const jv_t *v = font_json_str(jr, "name");
if (v) snprintf(name, sizeof name, "%s", v->str);
v = font_json_str(jr, "tier");
if (v) snprintf(tier, sizeof tier, "%s", v->str);
else { v = font_json_str(jr, "charset"); if (v) snprintf(tier, sizeof tier, "%s", v->str); }
v = jv_obj_get(jr, "glyph_count");
if (v && v->type == J_NUM) gc = v->inum;
jv_free(jr);
}
}
if (!name[0]) snprintf(name, sizeof name, "%s", fe->d_name);
if (!first) sA(&s, ",");
sA(&s, "{\"family\":");
sA_str(&s, fe->d_name);
sA(&s, ",\"weight\":");
sA_str(&s, we->d_name);
sA(&s, ",\"name\":");
sA_str(&s, name);
sA(&s, ",\"tier\":");
sA_str(&s, tier);
sA(&s, ",\"glyph_count\":%lld,\"mtime\":%lld}", gc, (long long)wst.st_mtime);
first = 0;
}
closedir(wd);
}
closedir(fd);
sA(&s, "]}");
return send_json(sock, 200, s.b ? s.b : "{\"fonts\":[]}");
}
int sha256_file_hex(const char *path, char out[65])
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
mp_sha256_ctx c;
mp_sha256_init(&c);
uint8_t buf[8192];
ssize_t n;
while ((n = read(fd, buf, sizeof buf)) > 0) mp_sha256_update(&c, buf, (size_t)n);
close(fd);
if (n < 0) return -1;
uint8_t h[32];
mp_sha256_final(&c, h);
auth_hex(h, sizeof h, out);
out[64] = 0;
return 0;
}
int api_fonts_import(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char force[8] = "0";
qparam(r->query, "force", force, sizeof force);
long uni = (long)time(NULL);
char tmpdir[1500], tmpgz[1500], stg[1500];
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
snprintf(tmpgz, sizeof tmpgz, "%s/tmp/mp_font_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
snprintf(stg, sizeof stg, "%s/tmp/mp_font_stg_%d_%ld", TF_CARD_DIR, (int)getpid(), uni);
/* ① 写 multipart file 到临时 tar.gz */
if (multipart_extract_file(r, tmpgz) < 0)
return send_err(sock, 400, "BAD_REQUEST", "缺字体库 tar.gz(multipart 字段 file)");
/* ② 成员名预校验:gzip -dc | tar -t → 每名 sanitize,且恰为三件套平铺(拒绝对/.. / 多出条目) */
{
char cmd[3200];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null", tmpgz);
FILE *pf = popen(cmd, "r");
if (!pf) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "无法列 tar 成员"); }
int bad = 0, have_png = 0, have_idx = 0, have_json = 0, nent = 0;
char line[512];
while (fgets(line, sizeof line, pf)) {
char *e = strchr(line, '\n'); if (e) *e = 0;
if (!line[0]) continue;
char san[128];
if (sanitize_member(line, san, sizeof san) < 0) { bad = 1; break; }
if (!strcmp(san, "font.png")) have_png = 1;
else if (!strcmp(san, "font.idx")) have_idx = 1;
else if (!strcmp(san, "font.json")) have_json = 1;
else { bad = 1; break; }
nent++;
}
pclose(pf);
if (bad || nent != 3 || !have_png || !have_idx || !have_json) {
unlink(tmpgz);
return send_err(sock, 400, "BAD_REQUEST", "tar 缺三件套或含非法成员(拒绝绝对路径/../多出条目)");
}
}
/* ③ 解压到临时目录 */
rm_recursive(stg);
if (mkdir_p(stg) != 0) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "建临时目录失败"); }
{
char cmd[3200];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", tmpgz, stg);
if (system(cmd) != 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 解压失败(非 gzip/tar?)"); }
}
/* ④ 目录锁:恰为 3 个普通文件(lstat 拒软链/目录/设备),总大小 ≤ FONT_EXTRACT_MAX(防炸弹) */
{
static const char *names[3] = { "font.png", "font.idx", "font.json" };
int seen[3] = {0,0,0}, cnt = 0, ok = 1;
DIR *d = opendir(stg);
if (!d) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "读临时目录失败"); }
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue;
int idx = -1;
for (int i = 0; i < 3; i++) if (!strcmp(de->d_name, names[i])) { idx = i; break; }
if (idx < 0) { ok = 0; break; }
char fp[1800]; snprintf(fp, sizeof fp, "%s/%s", stg, de->d_name);
struct stat st;
if (lstat(fp, &st) != 0 || !S_ISREG(st.st_mode)) { ok = 0; break; } /* 拒软链/目录/设备 */
seen[idx] = 1; cnt++;
}
closedir(d);
long long tot = 0;
for (int i = 0; i < 3; i++) {
if (!seen[i]) { ok = 0; break; }
char fp[1600]; snprintf(fp, sizeof fp, "%s/%s", stg, names[i]);
struct stat st;
if (stat(fp, &st) != 0) { ok = 0; break; }
tot += (long long)st.st_size;
}
if (!ok || cnt != 3 || tot > (long long)FONT_EXTRACT_MAX) {
rm_recursive(stg); unlink(tmpgz);
return send_err(sock, 400, "BAD_REQUEST", "tar 内容非法(缺件/软链/多出条目/解压后超大)");
}
}
/* ⑤ 读 font.json → family/weight(可解析硬失败) */
char jpath[1600]; snprintf(jpath, sizeof jpath, "%s/font.json", stg);
char jbuf[8192];
if (read_small_file(jpath, jbuf, sizeof jbuf) < 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 缺失"); }
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
if (!jr) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 解析失败"); }
char family[128] = "font", weight[64] = "regular";
const jv_t *v = font_json_str(jr, "family");
if (v) snprintf(family, sizeof family, "%s", v->str);
else { v = font_json_str(jr, "name"); if (v) snprintf(family, sizeof family, "%s", v->str); }
v = font_json_str(jr, "weight");
if (v) snprintf(weight, sizeof weight, "%s", v->str);
char fam_s[128], wt_s[64];
if (safe_dir_segment(family, fam_s, sizeof fam_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json family 非法"); }
if (safe_dir_segment(weight, wt_s, sizeof wt_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json weight 非法"); }
snprintf(family, sizeof family, "%s", fam_s);
snprintf(weight, sizeof weight, "%s", wt_s);
/* ⑥ 目标目录 + 覆盖冲突(409,除非 force=1) */
char root[1100], target[1500];
fonts_root(root, sizeof root);
snprintf(target, sizeof target, "%s/%s/%s", root, family, weight);
if (strcmp(force, "1") != 0) {
static const char *names3[] = { "font.png", "font.idx", "font.json" };
char tp[1600]; int conflicted = 0;
for (int i = 0; i < 3; i++) { snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (access(tp, F_OK) == 0) { conflicted = 1; break; } }
if (conflicted) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 409, "CONFLICT", "目标字库已存在,请确认覆盖"); }
}
/* ⑦ sha256 仅 WARN(对齐 font.json 约定,不硬失败) */
char warn[256] = "";
{
const jv_t *wp = font_json_str(jr, "sha256_png");
const jv_t *wi = font_json_str(jr, "sha256_idx");
char need[65], got[65];
if (wp && wp->str[0]) {
snprintf(need, sizeof need, "%s", wp->str);
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.png", stg);
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
snprintf(warn, sizeof warn, "sha256_png 不匹配(源库校验失败,但已导入)");
}
if (wi && wi->str[0] && !warn[0]) {
snprintf(need, sizeof need, "%s", wi->str);
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.idx", stg);
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
snprintf(warn, sizeof warn, "sha256_idx 不匹配(源库校验失败,但已导入)");
}
}
/* ⑧ 建目标目录 + rename 三件套(原子替换) */
{
char p1[1400];
snprintf(p1, sizeof p1, "%s/%s", root, family);
mkdir(p1, 0755);
mkdir(target, 0755);
}
{
static const char *names3[] = { "font.png", "font.idx", "font.json" };
int ok = 1;
for (int i = 0; i < 3; i++) {
char sp[1600], tp[1600];
snprintf(sp, sizeof sp, "%s/%s", stg, names3[i]);
snprintf(tp, sizeof tp, "%s/%s", target, names3[i]);
if (rename(sp, tp) < 0) ok = 0;
}
if (!ok) { rm_recursive(stg); unlink(tmpgz); jv_free(jr); return send_err(sock, 500, "INTERNAL", "写入字体文件失败(磁盘满?)"); }
}
rm_recursive(stg);
unlink(tmpgz);
sbuf_t s = {0};
sA(&s, "{\"family\":");
sA_str(&s, family);
sA(&s, ",\"weight\":");
sA_str(&s, weight);
sA(&s, ",\"imported\":true,\"hint\":\"字体已导入。请进设备「设置 → 字体」页,字体列表在进入时自动重扫生效。\",\"warning\":");
sA_str(&s, warn[0] ? warn : "");
sA(&s, "}");
jv_free(jr);
return send_json(sock, 200, s.b ? s.b : "");
}
int api_fonts_export(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char family[128], weight[64];
if (!qparam(r->query, "family", family, sizeof family)) family[0] = 0;
if (!qparam(r->query, "weight", weight, sizeof weight)) weight[0] = 0;
char f[128], w[64];
if (safe_dir_segment(family, f, sizeof f) < 0) return send_err(sock, 400, "BAD_REQUEST", "family 非法");
if (!weight[0]) snprintf(w, sizeof w, "%s", "regular");
else if (safe_dir_segment(weight, w, sizeof w) < 0) return send_err(sock, 400, "BAD_REQUEST", "weight 非法");
char root[1100], dir[1500];
fonts_root(root, sizeof root);
snprintf(dir, sizeof dir, "%s/%s/%s", root, f, w);
static const char *names3[] = { "font.png", "font.idx", "font.json" };
for (int i = 0; i < 3; i++) {
char ap[1600];
snprintf(ap, sizeof ap, "%s/%s", dir, names3[i]);
struct stat st;
if (stat(ap, &st) < 0) {
sbuf_t s = {0};
sA(&s, "font."); sA_str(&s, names3[i]);
sA(&s, " 缺失(字库 <");
sA_str(&s, f); sA(&s, "/"); sA_str(&s, w); sA(&s, "> 不完整)");
int rc = send_err(sock, 404, "NOT_FOUND", s.b ? s.b : "字体三件套缺失");
sbuf_free(&s);
return rc;
}
}
/* 打包:tar -cf - -C <dir> 三件套 | gzip -c > <tmp>,落临时文件求 Content-Length 再流式下发 */
long uni = (long)time(NULL);
char tmpdir[1500], tmp[1500];
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
snprintf(tmp, sizeof tmp, "%s/tmp/mp_font_exp_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
char cmd[3600];
snprintf(cmd, sizeof cmd, "tar -cf - -C '%s' font.png font.idx font.json 2>/dev/null | gzip -c > '%s' 2>/dev/null", dir, tmp);
if (system(cmd) != 0) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包失败"); }
struct stat st;
if (stat(tmp, &st) < 0 || !S_ISREG(st.st_mode)) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包文件不可读"); }
char fname[220];
snprintf(fname, sizeof fname, "%s-%s.font.tar.gz", f, w);
char extra[300];
snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", fname);
if (send_head(sock, 200, "application/gzip", (long long)st.st_size, extra) < 0) { unlink(tmp); return -1; }
int fd = open(tmp, O_RDONLY);
if (fd < 0) { unlink(tmp); return -1; }
char buf[8192];
ssize_t n;
while ((n = read(fd, buf, sizeof buf)) > 0)
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); unlink(tmp); return -1; }
close(fd);
unlink(tmp);
return 0;
}