MeowPanel: 字体库 tar.gz 导入/导出 + 文件管理自然排序修复 + multipart 二进制边界修复
- 字体库导入/导出(用户 2026-08-31 定案):放弃 zip,与 .mpk 同构走 busybox 'gzip -dc | tar' 管道 (设备必带 tar/gzip,零新增依赖)。导入 POST /api/fonts/import(multipart file=<.tar.gz>,上限32MB, ?force=1 覆盖):tar -t 成员预校验(sanitize_member 拒绝对/../shell 危险字符,恰为三件套平铺)→ 解压到临时目录 → opendir/lstat 锁(拒软链/目录/设备/多出条目/解压后超大)→ font.json 解析 family/weight (safe_dir_segment 消毒)→ 覆盖冲突 409 → sha256_png/idx 仅 WARN → rename 到 MeowOS/fonts/<family>/<weight>/。 导出 GET /api/fonts/export?family=&weight=:tar -cf - | gzip -c 打包 <family>-<weight>.font.tar.gz 流式下载。 列表 GET /api/fonts:扫描三件套齐。前端设置页加「字体库」卡(导入 zip/tar.gz + 409 confirm 后 force=1 重试 + 导出按钮)。 - 文件管理排序修复:/api/files 排序改为目录优先 + 名称自然排序(数字感知 track2<track10,中文按无符号 字节序),替换原 strcasecmp 词法排序(用户报「很离谱」)。 - multipart 二进制边界修复(latent bug):strstr 遇数据内 NUL 提前停止 → memmem 语义 mem_find 字节安全查找, 修复字体 tar.gz/.mpk/二进制上传可能误吞后段的问题。 - 文档同步:docs/MEOWOS_SPEC_REF.md §5.2/§5.7.6、patch_v2/meowpanel/README.md 改 tar.gz 定案; FONT_SYSTEM.md 无 zip 表述无需改。meow_font_import_zip/export_zip 语义注为 tar.gz(_tgz 可更名)。 - 宿主编译 0 warning(-Werror 亦过);宿主端到端:导入→导出→逐字节比对、损坏/缺件/坏json/穿越/软链/非gzip全拒、 自然排序样本全过;真机已部署(meowpanel md5 51ffc183,服务 running)并 curl 验证 font 端点 + 排序。
This commit is contained in:
1 parent
4e3f41a5a3
commit
d8b95c0aab
7 files changed
+580
-23
No files matched your search
+465
-10
@@ -19,7 +19,9 @@
|
||||
* - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。
|
||||
*
|
||||
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。
|
||||
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库。设备无 python3/lua。
|
||||
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库(mp_jpeg.c 自研最小 JPEG;
|
||||
* 字体库导入/导出走 busybox `gzip -dc | tar` 管道——与 nyai .mpk 同构,设备必带 tar/gzip)。
|
||||
* 设备无 python3/lua。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -100,6 +102,7 @@ static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notificat
|
||||
#define MAX_HTTP_HEADER (64 * 1024)
|
||||
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
|
||||
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
|
||||
#define MAX_FONT_UPLOAD (32 * 1024 * 1024) /* 字体库 tar.gz 上传上限(gbk 字库 png+idx 可能 10-20MB) */
|
||||
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
||||
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
||||
#define LOG_MAX_LINES 1000
|
||||
@@ -1039,13 +1042,48 @@ static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size
|
||||
}
|
||||
|
||||
struct fent { char name[256]; int isdir; long long size, mtime; };
|
||||
/* 目录优先 + 名称不区分大小写(再按大小写敏感定序保证稳定) */
|
||||
|
||||
/* 自然字符串比较:数字段按数值比较(track2 < track10),ASCII 字母大小写不敏感
|
||||
* (同字母时原始字节作 tie-break),其余按**无符号字节序**(UTF-8 中文等直接按字节序,
|
||||
* 设备无 locale,避免 strcasecmp 对高位字节的 locale 依赖导致乱序)。
|
||||
* 实现:逐字节;遇到连续数字则各取整段,比「有效位数→数值→原始长度」;否则比字节。 */
|
||||
static int nat_strcmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
const unsigned char *pb = (const unsigned char *)b;
|
||||
while (*pa && *pb) {
|
||||
unsigned char ca = *pa, cb = *pb;
|
||||
if (ca >= '0' && ca <= '9' && cb >= '0' && cb <= '9') {
|
||||
const unsigned char *na = pa, *nb = pb;
|
||||
while (*na == '0') na++;
|
||||
while (*nb == '0') nb++;
|
||||
const unsigned char *da = na, *db = nb;
|
||||
while (*da >= '0' && *da <= '9') da++;
|
||||
while (*db >= '0' && *db <= '9') db++;
|
||||
int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */
|
||||
if (la != lb) return la - lb; /* 位数小者数小 */
|
||||
int c = memcmp(na, nb, (size_t)la);
|
||||
if (c) return c;
|
||||
int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */
|
||||
if (ta != tb) return ta - tb;
|
||||
pa = da; pb = db;
|
||||
continue; /* 数字段完全相同,继续 */
|
||||
}
|
||||
unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca;
|
||||
unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb;
|
||||
if (la != lbc) return (int)la - (int)lbc;
|
||||
if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */
|
||||
pa++; pb++;
|
||||
}
|
||||
return (int)*pa - (int)*pb;
|
||||
}
|
||||
|
||||
/* 目录优先 + 名称自然排序(数字感知 + 无符号字节序) */
|
||||
static int fent_cmp(const void *a, const void *b)
|
||||
{
|
||||
const struct fent *fa = a, *fb = b;
|
||||
if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir;
|
||||
int c = strcasecmp(fa->name, fb->name);
|
||||
return c ? c : strcmp(fa->name, fb->name);
|
||||
return nat_strcmp(fa->name, fb->name);
|
||||
}
|
||||
|
||||
static int files_list(int sock, const char *relpath)
|
||||
@@ -1303,6 +1341,7 @@ typedef struct {
|
||||
} req_t;
|
||||
|
||||
static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
|
||||
static int multipart_extract_file(const req_t *r, const char *outpath); /* 定义在文件后部;字体导入用(写 multipart file 到临时文件) */
|
||||
|
||||
static int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
|
||||
static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; }
|
||||
@@ -1451,9 +1490,11 @@ static int read_request(int sock, req_t *r)
|
||||
}
|
||||
char *body_start = buf + hend + 4; /* body 实际起点 */
|
||||
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
||||
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 */
|
||||
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。
|
||||
* 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */
|
||||
size_t cap = MAX_HTTP_BODY;
|
||||
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = MAX_UPLOAD_BODY;
|
||||
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0)
|
||||
cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : MAX_UPLOAD_BODY;
|
||||
if (clen > cap) { free(buf); return -2; }
|
||||
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
||||
char *body = malloc(clen + 1);
|
||||
@@ -1974,6 +2015,22 @@ static int send_file(int sock, int status, const char *ctype, const char *abspat
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 二进制安全子串查找:strstr 会因数据中的 NUL 提前停止,二进制(zip/mpk/图)必须用 memmem。
|
||||
* 返回 needle 在 hay[0..hlen) 中首次出现的指针;找不到 NULL。
|
||||
* 仅当 mot 出现,用 memmem(_GNU_SOURCE 已定义,glibc/musl 均提供)。
|
||||
* 若无 memmem(极端环境),退回手写字节扫描,保证可移植。 */
|
||||
static const char *mem_find(const void *hay, size_t hlen, const char *needle)
|
||||
{
|
||||
size_t nl = strlen(needle);
|
||||
if (!nl || hlen < nl) return NULL;
|
||||
const char *h = (const char *)hay;
|
||||
for (size_t i = 0; i + nl <= hlen; i++) {
|
||||
if (h[i] == needle[0] && memcmp(h + i, needle, nl) == 0)
|
||||
return h + i;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------------- multipart/form-data 解析(文件上传) ----------------
|
||||
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
|
||||
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
|
||||
@@ -2023,7 +2080,7 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||||
int nfiles = 0;
|
||||
char curpath[1024] = "";
|
||||
while (p < end) {
|
||||
const char *fstart = strstr(p, dmark);
|
||||
const char *fstart = mem_find(p, (size_t)(end - p), dmark);
|
||||
if (!fstart) break;
|
||||
fstart += strlen(dmark);
|
||||
if (*fstart == '-') break; /* 结束标记 --boundary-- */
|
||||
@@ -2051,8 +2108,8 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||||
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
||||
}
|
||||
}
|
||||
/* 数据结束:下一个 "--boundary" 之前 */
|
||||
const char *data_end = strstr(data_start, dmark);
|
||||
/* 数据结束:下一个 "--boundary" 之前(二进制安全) */
|
||||
const char *data_end = mem_find(data_start, (size_t)(end - data_start), dmark);
|
||||
if (!data_end) data_end = end;
|
||||
/* 去掉数据尾部 \r\n */
|
||||
while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--;
|
||||
@@ -3333,6 +3390,399 @@ static int api_files_delete(int sock, const req_t *r)
|
||||
return send_json(sock, 200, "{\"removed\":true}");
|
||||
}
|
||||
|
||||
/* ---------------- 字体库 tar.gz 导入 / 导出(MeowOS/fonts/<family>/<weight>/) ----------------
|
||||
* 定案(用户 2026-08-31):**与 .mpk 同构,放弃 zip**——设备 busybox tar/gzip 必定存在,
|
||||
* 后端全部走 `gzip -dc | tar` 管道(nyai .mpk 同款),零新增依赖、不再自实现 zip 读写/miniz。
|
||||
* 导入(POST /api/fonts/import,multipart file=<.tar.gz> [&force=1]):
|
||||
* - 上传 tar.gz(上限 MAX_FONT_UPLOAD=32MB,gbk/full 字库 png+idx 可能 10-20MB)→
|
||||
* `gzip -dc | tar -t` 预校验成员名(sanitize_member:拒绝对路径/`..`/shell 危险字符,含三个以上条目疑拒);
|
||||
* 解压到临时目录 → opendir/lstat 逐个校验:恰为 font.png/font.idx/font.json 三个**普通文件**
|
||||
* (拒软链/目录/设备,拒解压后超大)→ 缺件/坏 json 硬失败。
|
||||
* - family/weight 取 font.json 的 family(缺省回退 name)/weight(缺省 regular),safe_dir_segment 消毒。
|
||||
* - rename 到 MeowOS/fonts/<family>/<weight>/;目标已存在则 409(前端确认后带 force=1 重试)。
|
||||
* - sha256_png/sha256_idx 仅 WARN(含在响应 warning 里);导入后**不触发 launcher 重扫**
|
||||
* (扫描时机=启动/进设置页,勿加热路径),响应用 hint 提示。
|
||||
* 导出(GET /api/fonts/export?family=&weight=):
|
||||
* - `tar -cf - -C <font_dir> font.png font.idx font.json | gzip -c > <tmp>` 打包为
|
||||
* `<family>-<weight>.font.tar.gz` 下载流(先落临时文件求 Content-Length 再流式)。
|
||||
* 列表(GET /api/fonts):扫描 fonts/<family>/<weight>/(三件套齐)供导出选择。
|
||||
*/
|
||||
#define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹;gbk/full 亦在界内) */
|
||||
|
||||
static void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); }
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* ---- tar 成员 sanitize(沿用 nyai 语义:拒绝对路径/`..` 组件/shell 危险字符) ---- */
|
||||
static int unsafe_path(const char *s)
|
||||
{
|
||||
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
|
||||
}
|
||||
static 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;
|
||||
}
|
||||
/* 校验并相对化为平铺成员名:去前导 '/' 与重复 './',禁绝对/`..`/危险字符。返回 0=ok 写 out。 */
|
||||
static 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;
|
||||
}
|
||||
/* 递归建目录(mkdir 系统调用;已存在 EEXIST 忽略)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* 逐一手写从 font.json 取字段(family/weight/name/tier/glyph_count/sha256_png/sha256_idx)。
|
||||
* 返回 0=ok;*pf 指向解析出的 J_STR(供复制)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* 得到已装载字体库(family/weight 三级扫描 + 三件套齐)JSON 数组。 */
|
||||
static 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\":[]}");
|
||||
}
|
||||
|
||||
/* 在 multipart body 中定位 name="file" 字段数据区间(r->body 内)。 */
|
||||
/* 对文件算 sha256 hex(64 字符,流式,不整读进内存)。返回 0=ok;-1=打开/读失败。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
static 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 : "");
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */
|
||||
/* 执行命令(sh -c),捕获 stdout(上限 cap-1),返回退出状态(-1=无法启动)。 */
|
||||
static int run_cmd(const char *cmd, char *out, int outcap)
|
||||
@@ -3433,7 +3883,7 @@ static int multipart_extract_file(const req_t *r, const char *outpath)
|
||||
}
|
||||
}
|
||||
const char *ds = he + 4;
|
||||
const char *de = strstr(ds, dmark);
|
||||
const char *de = mem_find(ds, (size_t)((r->body + r->body_len) - ds), dmark);
|
||||
if (!de) de = r->body + r->body_len;
|
||||
while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--;
|
||||
if (isfile) {
|
||||
@@ -3930,6 +4380,11 @@ static int handle(int sock, req_t *r)
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system"))
|
||||
return api_system(sock);
|
||||
|
||||
/* ---- 字体库(导入/导出/列表) ---- */
|
||||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r);
|
||||
|
||||
/* ---- 实时画面(阶段 C) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
|
||||
return api_screen_jpeg(sock, r);
|
||||
|
||||
Reference in new issue
Block a user