diff --git a/patch_v2/meowpkg/build_nyai.sh b/patch_v2/meowpkg/build_nyai.sh new file mode 100755 index 0000000..21b69ee --- /dev/null +++ b/patch_v2/meowpkg/build_nyai.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# build_nyai.sh — nyai 包管理器交叉编译(mipsel 静态)+ 宿主编译可选。 +# +# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。单文件、无外部依赖(POSIX + Linux)。 +# +# 用法: +# ./build_nyai.sh # 交叉编译 → patch_v2/meowpkg/nyai(mipsel 静态) +# ./build_nyai.sh host # 宿主编译 → patch_v2/meowpkg/nyai_host +set -e +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +CC="$ROOT/.tools/zig-mipsel-cc" + +if [ "$1" = "host" ]; then + HOST_CC="${CC_HOST:-cc}" + "$HOST_CC" -Os -Wall -Wextra nyai.c -o nyai_host + echo "== host build ==" + ls -la nyai_host + md5sum nyai_host + file nyai_host + exit 0 +fi + +"$CC" -Os -Wall -Wextra -static nyai.c -o nyai +echo "== nyai (mipsel static) ==" +ls -la nyai +md5sum nyai +file nyai diff --git a/patch_v2/meowpkg/nyai b/patch_v2/meowpkg/nyai index fb4e9ea..f234b6c 100755 Binary files a/patch_v2/meowpkg/nyai and b/patch_v2/meowpkg/nyai differ diff --git a/patch_v2/meowpkg/nyai.c b/patch_v2/meowpkg/nyai.c index 517be95..277dfaf 100644 --- a/patch_v2/meowpkg/nyai.c +++ b/patch_v2/meowpkg/nyai.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,11 +37,8 @@ static const char *meow_root(void) const char *r = getenv("MEOWROOT"); return (r && *r) ? r : "/usr/data/mnt/sd_0"; } -static void path_apps(char *out, int n, const char *pkg) -{ snprintf(out, n, "%s/MeowAPPs/%s", meow_root(), pkg ? pkg : ""); } static void path_data(char *out, int n, const char *pkg, const char *sub) { snprintf(out, n, "%s/MeowData/%s/%s", meow_root(), pkg ? pkg : "", sub ? sub : ""); } -static void path_apps_root(char *out, int n){ snprintf(out,n,"%s/MeowAPPs", meow_root()); } static void path_sources(char *out, int n) { snprintf(out,n,"%s/MeowOS/opt/nyai/sources.list", meow_root()); } static void path_cache(char *out, int n) @@ -177,6 +175,45 @@ static int version_cmp(const char *a, const char *b) return *x ? 1 : -1; } +/* 点分数字版本:APPS.md 附录 §3「更新规范」升级门禁专用(install_mpk_file)。 + * 规则:以 '.' 或 '-' 分段,每段必须为纯数字;空串/含非数字段 = 不可解析(返回负数)。 + * 段数不齐时短者补 0(如 1.2 == 1.2.0)。 */ +#define MAXVSEG 16 +static int version_parse(const char *v, unsigned long *segs, int max) +{ + if (!v || !*v) return -1; + int n = 0; + const char *p = v; + while (*p) { + while (*p=='.'||*p=='-') p++; + if (!*p) break; + const char *start = p; + while (*p && isdigit((unsigned char)*p)) p++; + if (p == start) return -1; /* 空段或非数字段 */ + unsigned long x = 0; + for (const char *q=start; q 0x7fffffffUL) return -1; } + if (n < max) segs[n++] = x; + } + return n ? n : -1; +} +static int version_ok(const char *v) +{ unsigned long s[MAXVSEG]; return version_parse(v, s, MAXVSEG) >= 0; } +/* 返回:a>b=1, a==b=0, a cb ? ca : cb; + for (int i = 0; i < n; i++) { + unsigned long xa = i < ca ? na[i] : 0UL; /* 段数不齐,短者补 0 */ + unsigned long xb = i < cb ? nb[i] : 0UL; + if (xa != xb) return xa < xb ? -1 : 1; + } + return 0; +} + /* ---------------- JSON 读取 ---------------- */ static int json_str(const char *j, const char *key, char *out, int n) { @@ -250,7 +287,8 @@ typedef struct { char services[512]; char depends[256]; char description[256]; - int has_routes, has_services; + char export_files[1024]; /* raw JSON 数组(导出白名单;无则黑名单模式) */ + int has_routes, has_services, has_export_files; } appmeta; static int load_app_meta(const char *path, appmeta *m) @@ -273,6 +311,7 @@ static int load_app_meta(const char *path, appmeta *m) if (json_str(j,"description",m->description,sizeof m->description)!=0) m->description[0]=0; m->has_routes = json_raw(j,"routes",m->routes,sizeof m->routes)==0; m->has_services = json_raw(j,"services",m->services,sizeof m->services)==0; + m->has_export_files = json_raw(j,"export_files",m->export_files,sizeof m->export_files)==0; return 0; } @@ -724,34 +763,68 @@ static int install_mpk_file(const char *mpk_path) if (!valid_pkg(pkg)) { fprintf(stderr,"nyai: install: 包名含非法字符: %s\n", pkg); return 1; } printf("Reading package lists... Done\n"); - char dst[BUF], bak[BUF]; + char dst[BUF], bak[BUF], stg[BUF]; snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg); snprintf(bak,sizeof bak,"%s.bak_%d", dst, (int)getpid()); + snprintf(stg,sizeof stg,"%s/MeowAPPs/.stg_%d_%s", meow_root(), (int)getpid(), pkg); int replacing = (access(dst,F_OK)==0); - if (replacing) { - rm_rf(bak); - if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: install: 备份 %s 失败: %s\n", dst, strerror(errno)); return 1; } - printf("Backing up existing %s -> %s\n", pkg, bak); - } - printf("Preparing to unpack %s ...\n", pkg); - if (mkdir_p(dst)!=0) { if (replacing) rename(bak,dst); return 1; } + + /* 签名分级预留(未实现验签):os.meow.* 系统包二期须有有效签名,无签名/验签失败一律拒装 */ + if (!strncmp(pkg,"os.meow.", 8)) printf("nyai: 系统包 %s 需签名(二期)\n", pkg); + + /* ① 先解到临时目录读元数据,再决定替换(防降级 + 校验 app.json) */ + rm_rf(stg); + if (mkdir_p(stg)!=0) { fprintf(stderr,"nyai: install: 无法建临时目录 %s\n", stg); return 1; } char cmd[1024]; - snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, dst, dst); - printf("Unpacking %s ...\n", pkg); + snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, stg, stg); + printf("Preparing to unpack %s ...\n", pkg); if (system(cmd)!=0) { fprintf(stderr,"nyai: install: 解压/校验 app.json 失败(%s 可能损坏或非 .mpk)\n", mpk_path); - rm_rf(dst); if (replacing) rename(bak,dst); return 1; + rm_rf(stg); return 1; } - char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", dst); + char stgj[BUF]; snprintf(stgj,sizeof stgj,"%s/app.json", stg); appmeta m; - if (load_app_meta(appj,&m)!=0) { + if (load_app_meta(stgj,&m)!=0) { fprintf(stderr,"nyai: install: app.json 不合法(缺 name 或 exec)\n"); - rm_rf(dst); if (replacing) rename(bak,dst); return 1; + rm_rf(stg); return 1; } + /* TODO(2A): meow_verify 验签挂点,见 MPK_SIGNATURE_DESIGN.md */ + + /* ② 同包名替换前的防降级门禁:新版必须 >= 已装(相等=重装,允许;<即降级拒装) */ + if (replacing) { + char instj[BUF]; snprintf(instj,sizeof instj,"%s/app.json", dst); + appmeta im; + if (load_app_meta(instj,&im)!=0 || !version_ok(im.version) || !version_ok(m.version)) { + fprintf(stderr,"nyai: install: 版本无法判定(新=「%s」 已装=「%s」),拒绝安装\n", + m.version[0]?m.version:"(缺失)", im.version[0]?im.version:"(缺失)"); + rm_rf(stg); return 1; + } + if (version_num_cmp(m.version, im.version) < 0) { + fprintf(stderr,"nyai: install: 不允许降级: 新版本 %s < 已装 %s\n", m.version, im.version); + rm_rf(stg); return 1; + } + rm_rf(bak); + if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: install: 备份 %s 失败: %s\n", dst, strerror(errno)); rm_rf(stg); return 1; } + printf("Backing up existing %s -> %s\n", pkg, bak); + } else { + rm_rf(dst); /* 防御:全新安装前清残留 */ + } + + /* ③ 部署临时目录 -> 正式目录 */ + if (rename(stg,dst)!=0) { + fprintf(stderr,"nyai: install: 部署 %s 失败: %s\n", dst, strerror(errno)); + rm_rf(stg); if (replacing) rename(bak,dst); /* 回滚到旧版 */ + return 1; + } + + /* ④ 数据 init(MeowData 数据保留,仅保证目录存在) */ char d1[BUF],d2[BUF]; path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache"); mkdir_p(d1); mkdir_p(d2); printf("Setting up %s (%s) ...\n", pkg, m.version[0]?m.version:"?"); + + /* ⑤ 安装成功后清理本次创建的 .bak_(失败回滚路径不动) */ + if (replacing && bak[0]) { rm_rf(bak); printf("Removed backup %s\n", bak); } printf("OK\n"); return 0; } @@ -869,25 +942,138 @@ static int cmd_remove(const char *pkg, int purge) return 0; } +/* ---- 导出过滤助手(APPS.md 附录 §3「导出过滤」) ---- */ +/* 判断 p 是否含独立 '..' 路径组件 */ +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; +} +/* 校验并相对化导出成员:去前导 '/' 与重复 './',禁绝对路径、禁 '..' 组件、禁 shell 危险字符 */ +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; + if ((int)strlen(p)+1 > n) return -1; + snprintf(out,n,"%s", p); + return 0; +} +/* 从 JSON 数组串(含 '[' ']')提取字符串成员;返回项数,负=格式错 */ +static int json_array_strings(const char *arr, char items[][256], int max) +{ + int n = 0; + const char *p = arr; + while (*p && *p!='[') p++; + if (*p!='[') return -1; + p++; + while (*p && n '%s'", src, listf); + int rc = system(cmd); + if (rc!=0) { rm_rf(listf); return -1; } + snprintf(cmd,sizeof cmd,"tar -c -C '%s' -T '%s' 2>/dev/null | gzip -c > '%s' 2>/dev/null", src, listf, outtmp); + rc = system(cmd); + rm_rf(listf); + return rc==0 ? 0 : -1; +} +/* 白名单:只打 app.json + exec + export_files 声明的项(逐项安全校验) */ +static int export_whitelist(const char *src, const char *exec_rel, const char *export_json, const char *outtmp) +{ + char items[16][256]; + int nit = json_array_strings(export_json, items, 16); + if (nit < 0) { fprintf(stderr,"nyai: export: export_files 格式非法(须为字符串数组)\n"); return -1; } + char listf[BUF]; snprintf(listf,sizeof listf,"%s.wl", outtmp); + FILE *lf = fopen(listf,"w"); + if (!lf) return -1; + fprintf(lf,"app.json\n%s\n", exec_rel); + for (int i=0;i/dev/null | gzip -c > '%s' 2>/dev/null", src, listf, outtmp); + int rc = system(cmd); + rm_rf(listf); + return rc==0 ? 0 : -1; +} + static int cmd_export(const char *pkg, const char *out) { if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: export: 包名无效\n"); return 2; } char src[BUF],dst[BUF]; snprintf(src,sizeof src,"%s/MeowAPPs/%s", meow_root(), pkg); if (access(src,R_OK)!=0) { fprintf(stderr,"nyai: export: 未安装 %s\n", pkg); return 1; } + /* 导出前体检:app.json 存在且合法(有 name+exec) */ + char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", src); + appmeta m; + if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: export: %s/app.json 不合法(缺 name 或 exec)\n", pkg); return 1; } + /* 导出前体检:exec 文件存在且可执行 */ + char exec_rel[BUF]; + if (sanitize_member(m.exec, exec_rel, sizeof exec_rel)!=0) { + fprintf(stderr,"nyai: export: exec 字段非法: %s\n", m.exec); return 1; + } + char exepath[BUF]; snprintf(exepath,sizeof exepath,"%s/%s", src, exec_rel); + if (access(exepath, X_OK)!=0) { + fprintf(stderr,"nyai: export: exec 文件 %s 不存在或不可执行,拒绝导出\n", exepath); return 1; + } char outbuf[BUF]; if (out && *out && unsafe_path(out)) { fprintf(stderr,"nyai: export: 输出路径含非法字符\n"); return 1; } snprintf(outbuf,sizeof outbuf,"%s", out && *out ? out : pkg); if (!strstr(outbuf,".mpk")) strncat(outbuf,".mpk", sizeof outbuf-strlen(outbuf)-1); snprintf(dst,sizeof dst,"%s", outbuf); printf("Exporting %s -> %s ...\n", pkg, dst); - char cmd[1024]; - snprintf(cmd,sizeof cmd,"tar -c -C '%s' . 2>/dev/null | gzip -c > '%s.tmp' 2>/dev/null", src, dst); - system(cmd); char tmpp[BUF]; snprintf(tmpp,sizeof tmpp,"%s.tmp", dst); - if (access(tmpp,F_OK)!=0 || rename(tmpp,dst)!=0) { - fprintf(stderr,"nyai: export: tar/gzip 失败\n"); rm_rf(tmpp); return 1; + int rc; + if (m.has_export_files && m.export_files[0]) { + rc = export_whitelist(src, exec_rel, m.export_files, tmpp); + } else { + rc = export_blacklist(src, tmpp); } + if (rc!=0 || access(tmpp,F_OK)!=0) { fprintf(stderr,"nyai: export: 打包失败\n"); rm_rf(tmpp); return 1; } + if (rename(tmpp,dst)!=0) { fprintf(stderr,"nyai: export: 写 %s 失败\n", dst); rm_rf(tmpp); return 1; } printf("OK\n"); return 0; } @@ -929,6 +1115,21 @@ static int cmd_upgrade(const char *pkg, const char *file) if (access(mpk,R_OK)!=0) { fprintf(stderr,"nyai: upgrade: 找不到包文件 %s\n", mpk); return 1; } char dst[BUF]; snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg); if (access(dst,F_OK)!=0) { fprintf(stderr,"nyai: upgrade: %s 未安装(如需全新安装请用 install)\n", pkg); return 1; } + /* 防降级门禁(与 install 同规范):新版必须 >= 已装(相等=重装允许) */ + { + char aj[BUF]; snprintf(aj,sizeof aj,"%s/app.json", dst); + appmeta im, nm; + if (load_app_meta(aj,&im)!=0 || mpk_appinfo(mpk,&nm)!=0 || + !version_ok(im.version) || !version_ok(nm.version)) { + fprintf(stderr,"nyai: upgrade: 版本无法判定(新=「%s」 已装=「%s」),拒绝\n", + nm.version[0]?nm.version:"(缺失)", im.version[0]?im.version:"(缺失)"); + return 1; + } + if (version_num_cmp(nm.version, im.version) < 0) { + fprintf(stderr,"nyai: upgrade: 不允许降级: 新版本 %s < 已装 %s\n", nm.version, im.version); + return 1; + } + } char bak[BUF]; snprintf(bak,sizeof bak,"%s.bak_%d", dst,(int)getpid()); rm_rf(bak); if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: upgrade: 备份失败: %s\n", strerror(errno)); return 1; } @@ -943,6 +1144,7 @@ static int cmd_upgrade(const char *pkg, const char *file) char d1[BUF],d2[BUF]; path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache"); mkdir_p(d1); mkdir_p(d2); + if (bak[0]) { rm_rf(bak); } /* 升级成功后清理本次 .bak_ */ printf("OK (%s) — data preserved\n", m.version[0]?m.version:"?"); return 0; } diff --git a/patch_v2/meowpkg/nyai_host b/patch_v2/meowpkg/nyai_host new file mode 100755 index 0000000..e657539 Binary files /dev/null and b/patch_v2/meowpkg/nyai_host differ