/* 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 三件套 | gzip -c > ,落临时文件求 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; }