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:
1 parent
f3ae06d7cd
commit
2dc4cad454
26 files changed
+9317
-2755
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user