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