feat(nyai): 更新规范+导出过滤 — 防降级安装(version点分比较,新版<已装拒装/缺失拒装,先解 staging 再替换) + 导出两层模式(黑名单 find+tar -T 排除 *.pre_*/.bak_*/logs//*.log/.DS_Store 且去 ./ 前缀 / 白名单 export_files 逐项安全校验) + 导出体检(app.json合法+exec存在可执行) + install/upgrade 成功后自清 .bak_<pid> + os.meow.* 签名提示与 2A 验签挂点注释 (agent-180)

nyai e5514929→新值见 fw_content.conf; 宿主 17/17 场景全过(首装/升/同版/降级拒/无版本拒/黑白名单/体检/round-trip); cmd_upgrade 同步加防降级门禁(堵绕过)
出入: 版本严格解析(1.2.3-beta 判不可比拒装, 与定案一致); busybox tar --exclude 未真机验证→用 find|tar -T 兜底
This commit is contained in:
MeowOS committed 2026-08-31 03:54:38 +08:00
1 parent f2786552d5
commit 7a1c78c5de
4 files changed
+253 -23

No files matched your search

+28
View File
@@ -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
Binary file not shown.
+225 -23
View File
@@ -19,6 +19,7 @@
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
@@ -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<p; q++) { x = x*10 + (*q-'0'); if (x > 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<b=-1;任一不可解析返回 INT_MIN(不可比) */
static int version_num_cmp(const char *a, const char *b)
{
unsigned long na[MAXVSEG], nb[MAXVSEG];
int ca = version_parse(a, na, MAXVSEG);
int cb = version_parse(b, nb, MAXVSEG);
if (ca < 0 || cb < 0) return INT_MIN;
int n = ca > 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_<pid>(失败回滚路径不动) */
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<max) {
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r'||*p==',') p++;
if (*p==']') break;
if (*p!='"') return -1; /* 数组元素必须是字符串 */
p++;
int o=0;
while (*p && *p!='"') {
if (*p=='\\' && p[1]) { if (o<255) items[n][o++]=p[1]; p+=2; continue; }
if (o<255) items[n][o++]=*p;
p++;
}
if (*p!='"') return -1;
p++;
items[n][o]=0;
n++;
}
return n;
}
/* 黑名单(默认):排除 *.pre_* *.bak_* logs/ *.log .DS_Store,其余全打 */
static int export_blacklist(const char *src, const char *outtmp)
{
char listf[BUF]; snprintf(listf,sizeof listf,"%s.wl", outtmp);
char cmd[2048];
snprintf(cmd,sizeof cmd,
"(cd '%s' && find . \\( -name '*.pre_*' -o -name '*.bak_*' -o -name '*.log' -o "
"-name '.DS_Store' -o -name logs \\) -prune -o -type f -print | sed 's|^\\./||') > '%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<nit;i++) {
char mem[BUF];
if (sanitize_member(items[i], mem, sizeof mem)!=0) {
fclose(lf); rm_rf(listf);
fprintf(stderr,"nyai: export: 白名单项含非法路径: %s\n", items[i]); return -1;
}
char p[BUF]; snprintf(p,sizeof p,"%s/%s", src, mem);
if (access(p,F_OK)!=0) {
fclose(lf); rm_rf(listf);
fprintf(stderr,"nyai: export: 白名单项不存在: %s\n", mem); return -1;
}
fprintf(lf,"%s\n", mem);
}
fclose(lf);
char cmd[2048];
snprintf(cmd,sizeof cmd,"tar -c -C '%s' -T '%s' 2>/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_<pid> */
printf("OK (%s) — data preserved\n", m.version[0]?m.version:"?");
return 0;
}
BIN
View File
Binary file not shown.