Files
MeowOS-For-R3P2/patch_v2/meowpanel/src/mp_fonts.c
T

375 lines
16 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.
/* 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;
}