diff --git a/patch_v2/meowpkg/nyai b/patch_v2/meowpkg/nyai index 3e7fccb..3d1b5ae 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 d09b2d8..3757894 100644 --- a/patch_v2/meowpkg/nyai.c +++ b/patch_v2/meowpkg/nyai.c @@ -6,32 +6,54 @@ * - 源配置 MeowOS/opt/nyai/sources.list(每行一个源;local <路径> / http / * deb [trusted=yes] / file:// ; # 注释)。默认 local MeowOS/opt/repo。 * - 每源一个 Packages 索引(行式 Package/Version/Version-Code/Filename/Size/SHA256/Depends/Description), - * 由 `nyai index <目录>` 生成。 + * 由 `nyai index <目录>` 生成;Package 字段取自包内 app.json.package。 * - update 缓存索引到 MeowOS/opt/nyai/cache;install 按名称从索引取包(本地文件或 http 拉取), * 处理 Depends(递归)、校验 SHA256 + app.json;同 name 先备份;解压 MeowAPPs//; * 数据 init(MeowData//{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。 * + * 包身份(M35):**包名/版本/依赖全部来自包内 app.json**,文件名与后缀只作路径,不参与身份判定。 + * 安装流:解包到 SD 暂存目录 → 读 app.json(验签前,无循环依赖)→ 校验 footer.package 与 + * app.json.package 一致 → 用清单包名调 meow_verify → 防降级 → rename 部署。 + * 暂存目录 = /MeowOS/tmp/pkgs/---/,成功/失败/被信号中断都删。 + * * 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。 */ #include #include +#include #include #include #include #include +#include #include #include +#include +#include +#include #include #include #include -#define NYAI_VERSION "0.2.0" +#define NYAI_VERSION "0.3.0" #define BUF 1024 #define MAX_SRC 16 #define MAX_PKG 256 #define MAX_DEPS 64 #define MAX_DEPTH 32 +/* 解包暂存目录根(相对 MeowROOT;真机 = /usr/data/mnt/sd_0/MeowOS/tmp/pkgs)。 + * 每次安装一个子目录 ---,无论成败都删。 */ +#define STG_SUBDIR "MeowOS/tmp/pkgs" + +/* .mpk 签名 footer 布局(与 patch_v2/meow_verify/meow_sig.h 同步;此处只读 package 用于 + * 「footer.package vs app.json.package」一致性检查——真正的验签仍由 meow_verify 执行)。 */ +#define SIG_MAGIC "MEOWSIG1" +#define SIG_PKG_MAX 64 +#define SIG_BLOCK_LEN_V1 180 +#define SIG_BLOCK_LEN_V2 184 +#define SIG_OFF_PKG 12 + /* ---------------- 路径 ---------------- */ static const char *meow_root(void) { @@ -46,6 +68,8 @@ static void path_cache(char *out, int n) { snprintf(out,n,"%s/MeowOS/opt/nyai/cache", meow_root()); } static void path_repo_default(char *out, int n) { snprintf(out,n,"%s/MeowOS/opt/repo", meow_root()); } +static void path_stg_base(char *out, int n) +{ snprintf(out,n,"%s/%s", meow_root(), STG_SUBDIR); } /* ---------------- 源结构 ---------------- */ typedef struct { @@ -87,10 +111,11 @@ static void strip_comment(char *s) if (p) *p = 0; } -/* 包名合法:仅 [A-Za-z0-9._-] */ +/* 包名合法:仅 [A-Za-z0-9._-],且拒绝 "." / ".."(否则 MeowAPPs/ 会越出安装目录)。 */ static int valid_pkg(const char *s) { if (!s || !*s) return 0; + if (!strcmp(s, ".") || !strcmp(s, "..")) return 0; for (; *s; s++) if (!(isalnum((unsigned char)*s) || *s=='.' || *s=='-' || *s=='_')) return 0; return 1; @@ -100,6 +125,9 @@ static int unsafe_path(const char *s) return s && strpbrk(s, " ;`$'\"&|<>") != NULL; } +/* 前向声明:错误前缀(install/upgrade),定义见安装期安全设施一节。 */ +static void op_err(const char *fmt, ...); + static int mkdir_p(const char *path) { char buf[BUF]; @@ -156,6 +184,92 @@ static void human_size(long long b, char *out, int n) else snprintf(out,n,"%.2f MB", b/(1024.0*1024)); } +/* ---------------- 时钟 / 暂存目录 ---------------- */ +/* 真机 time() 不可用:zig 0.17 自带 musl 在 mipsel 上把 time64 入口编成了 time32, + * 而 struct timespec 按 16 字节声明 → time() 返回「秒 + 纳秒<<32」的垃圾值 + * (实测 time()=3381223399391169494,见 devdocs/CLOCK_ABI.md)。这里直接走内核 o32 + * time32 ABI(SYS_clock_gettime=4263,缓冲 8 字节 {int32 sec,int32 nsec},真机实测正确)。 + * 该时间只用于给人看的目录名,唯一性由 mkdir(2) 失败重试 + pid 保证。 */ +static long long nyai_now(void) +{ +#if defined(__mips__) && !defined(__mips64) + struct { int32_t tv_sec, tv_nsec; } tv = { 0, 0 }; + if (syscall(4263, 0 /* CLOCK_REALTIME */, &tv) == 0 && tv.tv_sec > 0) + return (long long)tv.tv_sec; + return 0; +#else + return (long long)time(NULL); +#endif +} + +/* UTC "YYYYmmdd-HHMMSS"(localtime_r 在真机同样受 time64 缺陷影响返回 NULL,故自己算)。 */ +static void fmt_utc(long long sec, char *out, int n) +{ + if (sec <= 0) { snprintf(out, n, "t0"); return; } + long long z = sec / 86400 + 719468; /* civil_from_days */ + long long era = (z >= 0 ? z : z - 146096) / 146097; + unsigned doe = (unsigned)(z - era * 146097); + unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; + long long y = (long long)yoe + era * 400; + unsigned doy = doe - (365*yoe + yoe/4 - yoe/100); + unsigned mp = (5*doy + 2)/153; + unsigned d = doy - (153*mp+2)/5 + 1; + unsigned m = mp + (mp < 10 ? 3 : 9); /* 1..12 */ + if (m <= 2) y++; + long long rem = sec % 86400; + snprintf(out, n, "%04lld%02u%02u-%02u%02u%02u", y, m, d, + (unsigned)(rem/3600), (unsigned)((rem%3600)/60), (unsigned)(rem%60)); +} + +/* 路径是否正好是某个挂载点(读 /proc/mounts 第二列)。1=是 0=否 -1=读不到。 */ +static int is_mount_point(const char *path) +{ + FILE *f = fopen("/proc/mounts", "r"); + if (!f) return -1; + char dev[256], mnt[512]; + int hit = 0; + while (fscanf(f, "%255s %511s %*s %*s %*s %*s", dev, mnt) == 2) { + if (!strcmp(mnt, path)) { hit = 1; break; } + } + fclose(f); + return hit; +} + +/* 准备暂存根:返回 0 成功;失败已打印原因并返回 -1。 + * SD 未挂载 / 不可写时**拒绝安装**(不回退 /usr/data):安装目标 MeowAPPs 本身就在 SD 上, + * 卡不在装也没意义;且 /usr/data 只剩 ~4MB,跨文件系统 rebase 还会让部署 rename 变 EXDEV。 */ +static int stg_prepare(char *base, int n) +{ + path_stg_base(base, n); + /* 真机根(/usr/data/...)必须是真实挂载点;宿主/CI 的 MEOWROOT 沙箱不查。 */ + const char *root = meow_root(); + int dev_root = !strcmp(root, "/usr/data") || !strncmp(root, "/usr/data/", 10); + if (dev_root && is_mount_point(root) != 1) { + op_err("暂存目录 %s 不可用:SD 卡未挂载(%s 不是挂载点),拒绝安装\n", base, root); + return -1; + } + if (mkdir_p(base) != 0 || access(base, W_OK) != 0) { + op_err("暂存目录 %s 不可用(SD 卡只读/写满?): %s\n", base, strerror(errno)); + return -1; + } + return 0; +} + +/* 建暂存目录(解包前包名未知,用 .prep 前缀):mkdir(2) 原子占位 + 失败递增重试; + * 唯一性由 mkdir 的 EEXIST + pid + 序号保证,时间戳只给人看。 */ +static int stg_mkdir(const char *base, char *out, int n) +{ + char ts[32]; + fmt_utc(nyai_now(), ts, sizeof ts); + for (int i = 0; i < 100; i++) { + if (snprintf(out, n, "%s/.prep-%s-%d-%d", base, ts, (int)getpid(), i) >= n) return -1; + if (mkdir(out, 0755) == 0) return 0; + if (errno != EEXIST) return -1; + } + return -1; +} + + /* ---------------- JSON 读取 ---------------- */ static int json_str(const char *j, const char *key, char *out, int n) { @@ -288,8 +402,21 @@ static int load_app_meta(const char *path, appmeta *m) return 0; } -/* 从 .mpk 里尽力读 app.json 字段(name/version/depends/description)。 - * 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -1。 */ +/* 从 .mpk 里尽力读 app.json 字段(package/name/version/depends/description)。 + * 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -1。 + * 成员名来自归档本身 → 保持原样('./app.json' 与 'app.json' 在 tar 里是不同名字), + * 但必须先做 shell/选项注入校验:禁前导 '-'(会被当成 tar 选项)、禁 shell 元字符、 + * 禁控制字符、禁 '..'(index/scan 会对任意文件调用)。 */ +static int member_safe_for_arg(const char *s) +{ + if (!s || !*s || *s == '-') return 0; + if (unsafe_path(s)) return 0; + if (strstr(s, "..")) return 0; + for (const char *p = s; *p; p++) + if ((unsigned char)*p < 0x20 || *p == 0x7f) return 0; + return 1; +} + static int mpk_appinfo(const char *file, appmeta *m) { memset(m, 0, sizeof *m); @@ -304,6 +431,7 @@ static int mpk_appinfo(const char *file, appmeta *m) size_t mlen=strlen(mm); while (mlen&&(mm[mlen-1]=='\n'||mm[mlen-1]=='\r'||mm[mlen-1]==' ')) mm[--mlen]=0; pclose(pf); if (!mlen) return -1; + if (!member_safe_for_arg(mm)) return -1; snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -xO '%s' 2>/dev/null", file, mm); pf = popen(cmd, "r"); if (!pf) return -1; @@ -313,6 +441,7 @@ static int mpk_appinfo(const char *file, appmeta *m) if (!r) return -1; j[r]=0; if (json_str(j,"name",m->name,sizeof m->name)!=0 || !m->name[0]) return -1; + if (json_str(j,"package",m->package,sizeof m->package)!=0) m->package[0]=0; if (json_str(j,"version",m->version,sizeof m->version)!=0) m->version[0]=0; m->has_version_code = (json_int(j,"version_code",&m->version_code)==0); if (json_str(j,"depends",m->depends,sizeof m->depends)!=0) m->depends[0]=0; @@ -495,7 +624,7 @@ static int pkg_installed(const char *pkg) return access(p, R_OK) == 0; } -static int install_mpk_file(const char *mpk_path); +static int install_mpk_file(const char *mpk_path, const char *expect_pkg, int must_be_installed); /* 按记录安装一个索引包(含 sha256 校验 + 依赖),done 记录已装避免循环 */ static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int ndone, int depth); @@ -586,7 +715,7 @@ static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int return 1; } } - int rc = install_mpk_file(local); + int rc = install_mpk_file(local, r->pkg, 0); if (s->http) remove(local); return rc; } @@ -624,9 +753,11 @@ static int cmd_update(void) } /* ---------------- index ---------------- + * 索引的 Package 字段 = 包内 app.json.package(**不看文件名**;文件名只作 Filename 定位)。 + * 清单缺 package / 非法 → 跳过并告警,不入索引(否则 install 时验签与落盘目录会错)。 * 注意(MPK_SIGNATURE_DESIGN §5.5):索引记录的 SHA256 = 整包哈希(含尾部签名 footer 的 .mpk)。 * 必须先 `meow_sign sign .mpk` 完成签名、再 `nyai index `,否则索引与装后的整包不一致, - * install 的 sha256_file 校验会失败。本命令不改,仅标注此顺序约束(签名分级见 install_mpk_file)。 */ + * install 的 sha256_file 校验会失败。 */ static int cmd_index(const char *dir) { if (!dir || !*dir) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; } @@ -646,15 +777,16 @@ static int cmd_index(const char *dir) if (L<4 || strcmp(de->d_name+L-4, ".mpk")!=0) continue; char fpath[BUF]; snprintf(fpath,sizeof fpath,"%s/%s", r, de->d_name); struct stat st; if (stat(fpath,&st)!=0) continue; - char stem[128]; snprintf(stem,sizeof stem,"%.*s", (int)(L-4), de->d_name); appmeta m; char ver[64]="", dep[256]="", desc[256]=""; - if (mpk_appinfo(fpath,&m)==0) { - snprintf(ver,sizeof ver,"%s", m.version); snprintf(dep,sizeof dep,"%s", m.depends); - snprintf(desc,sizeof desc,"%s", m.description[0]?m.description:m.name); + if (mpk_appinfo(fpath,&m)!=0 || !m.package[0] || !valid_pkg(m.package)) { + fprintf(stderr,"nyai: index: 跳过 %s(app.json 缺合法 package 字段)\n", de->d_name); + continue; } + snprintf(ver,sizeof ver,"%s", m.version); snprintf(dep,sizeof dep,"%s", m.depends); + snprintf(desc,sizeof desc,"%s", m.description[0]?m.description:m.name); char sha[128]=""; sha256_file(fpath, sha, sizeof sha); - fprintf(f,"Package: %s\n", stem); + fprintf(f,"Package: %s\n", m.package); fprintf(f,"Version: %s\n", ver[0]?ver:"0.0.0"); if (m.has_version_code) fprintf(f,"Version-Code: %lld\n", m.version_code); fprintf(f,"Filename: %s\n", de->d_name); @@ -663,7 +795,7 @@ static int cmd_index(const char *dir) if (dep[0]) fprintf(f,"Depends: %s\n", dep); if (desc[0]) fprintf(f,"Description: %s\n", desc); fprintf(f,"\n"); - printf(" %s (%s)\n", stem, ver[0]?ver:"0.0.0"); + printf(" %s (%s)\n", m.package, ver[0]?ver:"0.0.0"); n++; } fclose(f); @@ -855,117 +987,362 @@ static void nyai_check_bin_depends(const appmeta *m) } } -/* ---------------- install(文件或名称) ---------------- */ -static int install_mpk_file(const char *mpk_path) +/* ---------------- 安装期安全设施:信号清理 / 子进程 / 成员检查 / footer 读取 ---------------- */ +static const char *g_op = "install"; /* 当前操作名(错误前缀:install / upgrade) */ +static char g_stg[BUF]; /* 当前暂存目录(空 = 无);所有退出路径都要清 */ +static pid_t g_child = -1; +static volatile sig_atomic_t g_sig = 0; + +static void op_err(const char *fmt, ...) { - if (!mpk_path || !*mpk_path) { fprintf(stderr,"nyai: install: 缺 .mpk 路径\n"); return 2; } - if (access(mpk_path, R_OK)!=0) { fprintf(stderr,"nyai: install: 打不开 %s\n", mpk_path); return 1; } - if (unsafe_path(mpk_path)) { fprintf(stderr,"nyai: install: 路径含非法字符\n"); return 1; } - const char *bn = strrchr(mpk_path,'/'); - bn = bn ? bn+1 : mpk_path; - const char *dot = strrchr(bn,'.'); - if (!dot || strcmp(dot,".mpk")!=0) { fprintf(stderr,"nyai: install: 需要 .mpk\n"); return 1; } - char pkg[64]; - int len=(int)(dot-bn); - if (len<=0 || len>=(int)sizeof pkg) { fprintf(stderr,"nyai: install: 包名无效\n"); return 1; } - memcpy(pkg,bn,len); pkg[len]=0; - if (!valid_pkg(pkg)) { fprintf(stderr,"nyai: install: 包名含非法字符: %s\n", pkg); return 1; } + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "nyai: %s: ", g_op); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +/* 删掉本次的暂存目录(幂等;atexit / 信号 / 各失败分支共用)。 */ +static void stg_cleanup(void) +{ + if (!g_stg[0]) return; + rm_rf(g_stg); + g_stg[0] = 0; +} + +/* SIGINT/SIGTERM/SIGHUP:先收掉自己的子进程组(gzip|tar 还在写暂存目录时不能直接删), + * 主流程在下一个检查点清理并退出(stg_check_signal)。 */ +static void on_term(int sig) +{ + g_sig = sig; + if (g_child > 0) kill(-g_child, SIGTERM); +} + +/* 有挂起信号 → 清暂存目录并按惯例退出码退出。在所有阻塞步骤后调用。 */ +static void stg_check_signal(void) +{ + if (!g_sig) return; + if (g_stg[0]) op_err("收到信号 %d,已清理暂存目录 %s\n", (int)g_sig, g_stg); + stg_cleanup(); + exit(128 + (int)g_sig); +} + +/* 跑 shell 命令:子进程单独成组(setpgid),信号时整组收掉。 + * 返回 0=成功;非 0=子进程退出码;-1=派生失败或被信号中断。 */ +static int run_shell(const char *cmd) +{ + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + setpgid(0, 0); + execl("/bin/sh", "sh", "-c", cmd, (char *)NULL); + _exit(127); + } + setpgid(pid, pid); + g_child = pid; + int st = 0; + for (;;) { + pid_t r = waitpid(pid, &st, 0); + if (r == pid) break; + if (r < 0 && errno == EINTR) { if (g_sig) kill(-pid, SIGTERM); continue; } + g_child = -1; + return -1; + } + g_child = -1; + if (g_sig) return -1; + if (!WIFEXITED(st)) return -1; + return WEXITSTATUS(st); +} + +/* 把不可打印字节换成 '?'(错误信息回显归档内容时防终端控制字符)。 */ +static void scrub(char *s) +{ + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (c < 0x20 || c == 0x7f) *s = '?'; + } +} + +/* 解包前逐条检查归档成员类型(tar -tv 首列):只允许普通文件 '-' 与目录 'd'。 + * 符号链接/硬链接/设备/FIFO 一律拒绝(防「先建链、后续成员写穿到目录外」)。 + * 名字穿越交由 tar 自身剥离(busybox 1.31 真机实测:'../x'→'x'、'/x'→'x'); + * 解包后再用 stg_tree_ok() 复核暂存树。 + * 返回 0 通过 / -2 成员类型非法 / -3 空归档 / -1 读失败。 */ +static int mpk_check_members(const char *mpk) +{ + char cmd[BUF]; + snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -tv 2>/dev/null", mpk); + FILE *pf = popen(cmd, "r"); + if (!pf) return -1; + char line[1200]; + int n = 0, bad = 0; + while (fgets(line, sizeof line, pf)) { + n++; + if (line[0] != '-' && line[0] != 'd') { + char *e = line + strlen(line); + while (e > line && (e[-1] == '\n' || e[-1] == '\r')) *--e = 0; + op_err("包内含链接/特殊文件(仅允许普通文件与目录),拒绝安装: %s\n", line); + bad = 1; + break; + } + } + pclose(pf); + if (bad) return -2; + return n ? 0 : -3; +} + +/* 解包后复核暂存树:只允许普通文件与目录;普通文件的 st_nlink 必须为 1 + * (硬链接到外部文件会表现为 nlink>1)。0=通过。 */ +static int stg_tree_ok(const char *path) +{ + struct stat st; + if (lstat(path, &st) != 0) return 0; + if (S_ISDIR(st.st_mode)) { + DIR *d = opendir(path); + if (!d) return 0; + int ok = 1; + struct dirent *de; + while ((de = readdir(d))) { + if (!strcmp(de->d_name,".") || !strcmp(de->d_name,"..")) continue; + char p[BUF]; + if (snprintf(p, sizeof p, "%s/%s", path, de->d_name) >= (int)sizeof p) { ok = 0; break; } + if (!stg_tree_ok(p)) { ok = 0; break; } + } + closedir(d); + return ok; + } + if (!S_ISREG(st.st_mode)) return 0; + return st.st_nlink <= 1; +} + +/* 读 .mpk 尾部签名 footer 的 package 字段(布局见 meow_sig.h)。 + * 返回 0=有 footer 且已填 out;1=无 footer(未签名);-1=读失败。 + * 只用于一致性诊断;签名有效性始终由 meow_verify 判定。 */ +static int read_footer_pkg(const char *mpk, char *out, int n) +{ + int fd = open(mpk, O_RDONLY); + if (fd < 0) return -1; + if (n <= 0) { close(fd); return -1; } + off_t sz = lseek(fd, 0, SEEK_END); + if (sz < (off_t)SIG_BLOCK_LEN_V1) { close(fd); return 1; } + uint8_t b4[4]; + if (lseek(fd, sz - 4, SEEK_SET) < 0 || read(fd, b4, 4) != 4) { close(fd); return -1; } + uint32_t blen = (uint32_t)b4[0] | ((uint32_t)b4[1] << 8) | + ((uint32_t)b4[2] << 16) | ((uint32_t)b4[3] << 24); + if ((int)blen != SIG_BLOCK_LEN_V1 && (int)blen != SIG_BLOCK_LEN_V2) { close(fd); return 1; } + if (sz < (off_t)blen) { close(fd); return 1; } + uint8_t blk[SIG_BLOCK_LEN_V2]; + if (lseek(fd, sz - (off_t)blen, SEEK_SET) < 0 || + read(fd, blk, (size_t)blen) != (ssize_t)blen) { close(fd); return -1; } + close(fd); + if (memcmp(blk, SIG_MAGIC, 8) != 0) return 1; + int cl = 0; + while (cl < SIG_PKG_MAX && blk[SIG_OFF_PKG + cl]) cl++; + if (n <= cl) cl = n - 1; + memcpy(out, blk + SIG_OFF_PKG, (size_t)cl); + out[cl] = 0; + return 0; +} + +/* 把 .prep-… 暂存目录改名成 ---(rename 原子占名, + * EEXIST/ENOTEMPTY 换序号重试)。返回 0 成功。失败由调用方沿用 .prep 名。 */ +static int stg_rename_pkg(const char *base, const char *prep, const char *pkg, char *out, int n) +{ + char ts[32]; + fmt_utc(nyai_now(), ts, sizeof ts); + for (int i = 0; i < 100; i++) { + if (snprintf(out, n, "%s/%s-%s-%d-%d", base, pkg, ts, (int)getpid(), i) >= n) return -1; + if (access(out, F_OK) == 0) continue; + if (rename(prep, out) == 0) return 0; + if (errno != EEXIST && errno != ENOTEMPTY) return -1; + } + return -1; +} + +/* ---------------- install(文件或名称) ---------------- */ +/* 安装/升级一个 .mpk。**包名、版本、依赖全部来自包内 app.json**,文件名与后缀不参与身份判定。 + * expect_pkg != NULL → 清单包名必须等于它(upgrade / 索引安装); + * must_be_installed → 目标必须已安装(upgrade)。 + * 暂存:/MeowOS/tmp/pkgs/---/ ——成功/失败/被信号中断都删。 */ +static int install_mpk_file(const char *mpk_path, const char *expect_pkg, int must_be_installed) +{ + struct stat fst; + char base[BUF], prep[BUF], final[BUF], stg[BUF], dst[BUF], bak[BUF], cmd[BUF * 2]; + + if (!mpk_path || !*mpk_path) { op_err("缺包文件路径\n"); return 2; } + if (unsafe_path(mpk_path)) { op_err("路径含非法字符\n"); return 1; } + if (stat(mpk_path, &fst) != 0 || !S_ISREG(fst.st_mode)) { op_err("打不开包文件 %s\n", mpk_path); return 1; } + if (stg_prepare(base, sizeof base) != 0) return 1; printf("Reading package lists... Done\n"); - 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); - /* 签名分级(II 期,MPK_SIGNATURE_DESIGN §5.2):有签名必须验签;os.meow.* 必须有有效签名; - * 无签名第三方放行(告警)。验签在 ① 解到 stg 后、② 防降级前执行(stg=临时目录,失败即 rm 不落盘)。 */ + /* ① 暂存目录:包名要解包后才知道,先用 .prep-<时间戳>-- 原子占位 */ + if (stg_mkdir(base, prep, sizeof prep) != 0) { + op_err("无法创建暂存目录(%s): %s\n", base, strerror(errno)); + return 1; + } + snprintf(g_stg, sizeof g_stg, "%s", prep); - /* ① 先解到临时目录读元数据,再决定替换(防降级 + 校验 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' 2>/dev/null | 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(stg); return 1; + /* ② 解包前检查归档成员类型 */ + int mr = mpk_check_members(mpk_path); + stg_check_signal(); + if (mr != 0) { + if (mr == -3) op_err("%s 解不出内容(不是 .mpk / 空归档)\n", mpk_path); + else if (mr == -1) op_err("无法读取包内容(%s)\n", mpk_path); + stg_cleanup(); + return 1; + } + /* ③ 解包到暂存目录(SD 卡,不占 /usr/data 的 4MB NAND) */ + printf("Preparing to unpack %s ...\n", mpk_path); + snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", mpk_path, prep); + if (run_shell(cmd) != 0) { + stg_check_signal(); + op_err("解压失败(%s 可能损坏或不是 .mpk)\n", mpk_path); + stg_cleanup(); + return 1; + } + stg_check_signal(); + if (!stg_tree_ok(prep)) { + op_err("解压结果含符号链接/硬链接等危险条目,拒绝安装\n"); + stg_cleanup(); + return 1; + } + char appj[BUF]; + if (snprintf(appj, sizeof appj, "%s/app.json", prep) >= (int)sizeof appj) { + op_err("暂存路径过长\n"); stg_cleanup(); return 1; } - char stgj[BUF]; snprintf(stgj,sizeof stgj,"%s/app.json", stg); appmeta m; - if (load_app_meta(stgj,&m)!=0) { - fprintf(stderr,"nyai: install: app.json 不合法(缺 name 或 exec)\n"); - rm_rf(stg); return 1; + if (load_app_meta(appj, &m) != 0) { + op_err("app.json 不合法(缺 name 或 exec)\n"); + stg_cleanup(); + return 1; } - /* 二进制依赖检查(app.json depends):不阻断安装,缺 required 标注「安装后启动将失败」。 */ + + /* ④ 包名唯一来源:app.json.package(不再看文件名/后缀) */ + if (!m.package[0]) { + op_err("app.json 缺 package 字段(包名只能来自清单),拒绝安装\n"); + stg_cleanup(); + return 1; + } + if (!valid_pkg(m.package)) { + op_err("app.json.package 非法: %s,拒绝安装\n", m.package); + stg_cleanup(); + return 1; + } + if (expect_pkg && *expect_pkg && strcmp(m.package, expect_pkg) != 0) { + op_err("app.json.package(%s) 与目标包名(%s) 不一致,拒绝\n", m.package, expect_pkg); + stg_cleanup(); + return 1; + } + const char *pkg = m.package; + + /* 暂存目录改名成可读名 -<时间戳>--;改名失败就沿用 .prep 名(不影响功能) */ + snprintf(stg, sizeof stg, "%s", prep); + if (stg_rename_pkg(base, prep, pkg, final, sizeof final) == 0) { + snprintf(stg, sizeof stg, "%s", final); + snprintf(g_stg, sizeof g_stg, "%s", final); + } + + /* ⑤ footer.package 必须等于 app.json.package(签名说 A、内容说 B → 拒装) */ + { + char fpkg[SIG_PKG_MAX + 1]; + int fr = read_footer_pkg(mpk_path, fpkg, sizeof fpkg); + if (fr == 0) { + scrub(fpkg); + if (strcmp(fpkg, pkg) != 0) { + op_err("签名包名(%s) 与 app.json(%s) 不一致,拒绝安装\n", fpkg, pkg); + stg_cleanup(); + return 1; + } + } + } + + /* ⑥ 签名分级:验签用清单包名;os.meow.* 必须有有效签名,无签名第三方放行 */ nyai_check_bin_depends(&m); - /* ①.5 签名分级 + 三重绑定(MPK_SIGNATURE_DESIGN §3.3/§5.2 失败行为表) */ int vr = verify_mpk_signature(mpk_path, pkg, NULL, 0); + stg_check_signal(); int system_pkg = is_system_pkg(pkg); int keep_src = 0; /* 有效签 → 安装后另存原签 .mpk.src */ if (vr == 0) { keep_src = 1; printf("nyai: 签名校验通过 (package=%s)\n", pkg); } else if (vr == 1) { - fprintf(stderr,"nyai: install: 签名验证失败(内容被篡改/包名不符/公钥不符),拒绝安装\n"); - rm_rf(stg); return 1; + op_err("签名验证失败(内容被篡改/包名不符/公钥不符),拒绝安装\n"); + stg_cleanup(); + return 1; } else if (vr == 2) { if (system_pkg) { - fprintf(stderr,"nyai: install: 系统包 %s 必须签名(无签名),拒绝安装\n", pkg); - rm_rf(stg); return 1; + op_err("系统包 %s 必须签名(无签名),拒绝安装\n", pkg); + stg_cleanup(); + return 1; } printf("nyai: 提示: %s 未签名(第三方包放行安装)\n", pkg); } else { if (system_pkg) { - fprintf(stderr,"nyai: install: meow_verify 不可用(rc=%d),os.meow.* 拒装(宁缺勿装)\n", vr); - rm_rf(stg); return 1; + op_err("meow_verify 不可用(rc=%d),os.meow.* 拒装(宁缺勿装)\n", vr); + stg_cleanup(); + return 1; } printf("nyai: 警告: meow_verify 不可用(rc=%d),%s 未验签放行\n", vr, pkg); } - /* 绑定(一般/②):解包 app.json.package == 文件名茎(防目录改名/改 app.json;meow_verify 已保证① footer.package==pkg) */ - if (m.package[0] && strcmp(m.package, pkg) != 0) { - fprintf(stderr,"nyai: install: app.json.package(%s) != 包名(%s),拒绝\n", m.package, pkg); - rm_rf(stg); return 1; - } - /* ② 同包名替换前的防降级门禁(APPS.md §3.8,仅按 version_code 比较): - * 新版 code >= 已装 code(相等=重装,允许;<即降级拒装);任一侧缺 version_code → 拒装。 */ + /* ⑦ 落地目录 + 防降级(基准 = MeowAPPs/<清单包名> 的 app.json) */ + stg_check_signal(); /* 从这里开始要改 MeowAPPs,临界区不再响应中断 */ + if (snprintf(dst, sizeof dst, "%s/MeowAPPs/%s", meow_root(), pkg) >= (int)sizeof dst) { + op_err("包名过长\n"); stg_cleanup(); return 1; + } + snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid()); + int replacing = (access(dst, F_OK) == 0); + if (must_be_installed && !replacing) { + op_err("%s 未安装(如需全新安装请用 install)\n", pkg); + stg_cleanup(); + return 1; + } if (replacing) { - char instj[BUF]; snprintf(instj,sizeof instj,"%s/app.json", dst); + char instj[BUF]; appmeta im; - if (load_app_meta(instj,&im)!=0 || !m.has_version_code || !im.has_version_code) { - fprintf(stderr,"nyai: install: 版本号无法判定(新缺 version_code 或已装缺 version_code),拒绝安装\n"); - rm_rf(stg); return 1; + snprintf(instj, sizeof instj, "%s/app.json", dst); + if (load_app_meta(instj, &im) != 0 || !m.has_version_code || !im.has_version_code) { + op_err("版本号无法判定(新缺 version_code 或已装缺 version_code),拒绝安装\n"); + stg_cleanup(); + return 1; } if (m.version_code < im.version_code) { - fprintf(stderr,"nyai: install: 不允许降级: 新版本 code %lld < 已装 code %lld\n", - m.version_code, im.version_code); - rm_rf(stg); return 1; + op_err("不允许降级: 新版本 code %lld < 已装 code %lld\n", m.version_code, im.version_code); + stg_cleanup(); + 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; } + if (rename(dst, bak) != 0) { + op_err("备份 %s 失败: %s\n", dst, strerror(errno)); + stg_cleanup(); + 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); /* 回滚到旧版 */ + /* ⑧ 部署:暂存目录与 MeowAPPs 同在 SD → rename 原子 */ + if (rename(stg, dst) != 0) { + op_err("部署 %s 失败: %s\n", dst, strerror(errno)); + stg_cleanup(); + if (replacing) rename(bak, dst); /* 回滚到旧版 */ return 1; } + g_stg[0] = 0; /* 已变成正式目录,不再属于清理范围 */ - /* ④ 数据 init(MeowData 数据保留,仅保证目录存在) */ - char d1[BUF],d2[BUF]; - path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache"); + /* ⑨ 数据 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:"?"); + printf("Setting up %s (%s) ...\n", pkg, m.version[0] ? m.version : "?"); - /* ④.5 II 期:原签 .mpk 另存 MeowData//data/.mpk.src(导出保留签名) */ + /* ⑩ II 期:原签 .mpk 另存 MeowData//data/.mpk.src(导出保留签名) */ if (keep_src) save_mpk_source(mpk_path, pkg); - /* ⑤ 安装成功后清理本次创建的 .bak_(失败回滚路径不动) */ + /* ⑪ 安装成功后清理本次创建的 .bak_(失败回滚路径不动) */ if (replacing && bak[0]) { rm_rf(bak); printf("Removed backup %s\n", bak); } printf("OK\n"); return 0; @@ -973,21 +1350,21 @@ static int install_mpk_file(const char *mpk_path) static int cmd_install(const char *arg) { - if (!arg || !*arg) { fprintf(stderr,"nyai: install: 缺包名或 .mpk\n"); return 2; } - /* ① 文件路径直接装 */ - size_t L=strlen(arg); - if ((L>=4 && !strcmp(arg+L-4,".mpk")) && access(arg,R_OK)==0) { - return install_mpk_file(arg); - } - if (!valid_pkg(arg)) { - fprintf(stderr,"nyai: install: 非法包名: %s\n", arg); - return 1; - } + if (!arg || !*arg) { op_err("缺包文件路径或索引包名\n"); return 2; } + /* ① 存在的普通文件 → 直接装(文件名任意,身份来自包内 app.json) */ + struct stat st; + size_t L = strlen(arg); + int looks_file = (strchr(arg, '/') != NULL) || + (L >= 4 && !strcmp(arg + L - 4, ".mpk")) || + (access(arg, F_OK) == 0); + if (stat(arg, &st) == 0 && S_ISREG(st.st_mode)) return install_mpk_file(arg, NULL, 0); + if (looks_file) { op_err("打不开包文件 %s\n", arg); return 1; } + if (!valid_pkg(arg)) { op_err("非法包名: %s\n", arg); return 1; } /* ② 按索引装 */ pkgrec recs[MAX_PKG]; int nrec = load_all_indexes(recs, MAX_PKG, g_srcs, g_nsrc); if (nrec == 0) { - fprintf(stderr,"nyai: 索引为空,先 nyai update 刷新(或直接 install .mpk)\n"); + op_err("索引为空,先 nyai update 刷新(或直接 install <包文件>)\n"); return 1; } return install_by_name(arg, recs, nrec); @@ -1224,6 +1601,11 @@ static int cmd_export(const char *pkg, const char *out) 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; } + /* 目录名必须等于清单包名(M35 后安装时已保证;老装目录缺 package 字段则跳过) */ + if (m.package[0] && strcmp(m.package, pkg) != 0) { + fprintf(stderr,"nyai: export: 目录名(%s) 与 app.json.package(%s) 不一致,拒绝导出\n", pkg, m.package); + return 1; + } /* 导出前体检:exec 文件存在且可执行。exec 可为绝对(包目录下,相对化)或相对路径。 */ char exec_rel[BUF]; if (exec_rel_to_pkg(src, m.exec, pkg, exec_rel, sizeof exec_rel)!=0) { @@ -1270,13 +1652,12 @@ static int cmd_scan(const char *dir) size_t L=strlen(de->d_name); if (L<4 || strcmp(de->d_name+L-4,".mpk")!=0) continue; char f[BUF]; snprintf(f,sizeof f,"%s/%s", r, de->d_name); - char stem[128]; snprintf(stem,sizeof stem,"%.*s",(int)(L-4),de->d_name); struct stat st; long long sz=0; if (stat(f,&st)==0) sz=st.st_size; char hs[24]; human_size(sz,hs,sizeof hs); - char nm[256]="", ver[64]=""; + char ver[64]=""; appmeta m; - if (mpk_appinfo(f,&m)==0) { snprintf(nm,sizeof nm,"%s",m.name); snprintf(ver,sizeof ver,"%s",m.version); } - printf("%-24s %-10s %-10s %s\n", nm[0]?nm:stem, ver[0]?ver:"?", hs, de->d_name); + if (mpk_appinfo(f,&m)==0) snprintf(ver,sizeof ver,"%s",m.version); + printf("%-24s %-10s %-10s %s\n", m.package[0]?m.package:"?", ver[0]?ver:"?", hs, de->d_name); found++; } closedir(d); @@ -1285,56 +1666,20 @@ static int cmd_scan(const char *dir) return 0; } +/* upgrade [file]:目标包名 pkg 必须已安装;包文件内部清单的 package 必须等于 pkg。 + * 其余(暂存/验签/防降级/部署/清理)全部走 install_mpk_file 同一实现,不再单独维护一套。 */ static int cmd_upgrade(const char *pkg, const char *file) { - if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: upgrade: 包名无效\n"); return 2; } - char mpk[BUF]; - if (file && *file) snprintf(mpk,sizeof mpk,"%s", file); - else snprintf(mpk,sizeof mpk,"%s.mpk", pkg); - 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 同规范):os.meow.* 必须有有效签名;有签必须验签;无签第三方放行。 */ - { - int vr = verify_mpk_signature(mpk, pkg, NULL, 0); - int sysp = is_system_pkg(pkg); - if (vr == 1) { fprintf(stderr,"nyai: upgrade: 签名验证失败,拒绝升级\n"); return 1; } - else if (vr == 2) { if (sysp) { fprintf(stderr,"nyai: upgrade: 系统包 %s 必须签名,拒绝升级\n", pkg); return 1; } printf("nyai: 提示: %s 未签名(第三方放行)\n", pkg); } - else if (vr != 0) { if (sysp) { fprintf(stderr,"nyai: upgrade: meow_verify 不可用(rc=%d),os.meow.* 拒升\n", vr); return 1; } printf("nyai: 警告: meow_verify 不可用(rc=%d),%s 未验签放行\n", vr, pkg); } + if (!pkg || !valid_pkg(pkg)) { op_err("包名无效\n"); return 2; } + char dst[BUF], mpk[BUF]; + if (snprintf(dst, sizeof dst, "%s/MeowAPPs/%s", meow_root(), pkg) >= (int)sizeof dst) { + op_err("包名过长\n"); return 1; } - /* 防降级门禁(与 install 同规范,APPS.md §3.8,仅按 version_code 比较): - * 新版 code >= 已装 code(相等=重装允许);任一侧缺 version_code → 拒绝。 */ - { - 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 || - !im.has_version_code || !nm.has_version_code) { - fprintf(stderr,"nyai: upgrade: 版本号无法判定(新缺 version_code 或已装缺 version_code),拒绝\n"); - return 1; - } - if (nm.version_code < im.version_code) { - fprintf(stderr,"nyai: upgrade: 不允许降级: 新版本 code %lld < 已装 code %lld\n", - nm.version_code, im.version_code); - 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; } - if (mkdir_p(dst)!=0) { rename(bak,dst); return 1; } - char cmd[1024]; - snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk,dst,dst); - printf("Upgrading %s ...\n", pkg); - if (system(cmd)!=0) { fprintf(stderr,"nyai: upgrade: 解压失败,回滚\n"); rm_rf(dst); rename(bak,dst); return 1; } - char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", dst); - appmeta m; - if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: upgrade: 新包 app.json 不合法,回滚\n"); rm_rf(dst); rename(bak,dst); return 1; } - 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; + if (access(dst, F_OK) != 0) { op_err("%s 未安装(如需全新安装请用 install)\n", pkg); return 1; } + if (file && *file) snprintf(mpk, sizeof mpk, "%s", file); + else snprintf(mpk, sizeof mpk, "%s.mpk", pkg); /* 默认路径约定,不是身份来源 */ + if (access(mpk, R_OK) != 0) { op_err("找不到包文件 %s\n", mpk); return 1; } + return install_mpk_file(mpk, pkg, 1); } /* ---------------- usage / main ---------------- */ @@ -1345,7 +1690,7 @@ static void usage(FILE *o, const char *argv0) "\n" "Usage:\n" " %s update Read all sources into local index cache\n" -" %s install Install by index name or direct .mpk (resolves Depends)\n" +" %s install Install a package file (any filename) or an index name\n" " %s remove [--purge] Uninstall (--purge also deletes MeowData)\n" " %s list List installed apps (MeowAPPs/*)\n" " %s info Show package details (app.json)\n" @@ -1354,23 +1699,31 @@ static void usage(FILE *o, const char *argv0) " %s index Generate Packages index for a .mpk dir\n" " %s export [out.mpk] Pack MeowAPPs/ -> .mpk (no data)\n" " %s scan [dir] List .mpk in dir (default .)\n" -" %s upgrade [file.mpk] Upgrade app, keep MeowData\n" +" %s upgrade [file] Upgrade installed , keep MeowData\n" " %s --help This help\n" "\n" +"Package identity (name/version/depends) comes from the app.json inside the package;\n" +"the file name is irrelevant. install/upgrade unpack to %s/MeowOS/tmp/pkgs/ and always\n" +"delete that staging directory (success, failure, SIGINT/SIGTERM).\n" +"\n" "Sources: %s/MeowOS/opt/nyai/sources.list (local / http / deb [trusted=yes] ; # comment)\n" "Exit codes: 0 success, non-zero error.\n", NYAI_VERSION, argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0, - meow_root()); + meow_root(), meow_root()); } int main(int argc, char **argv) { + int sigs[] = { SIGINT, SIGTERM, SIGHUP }; + for (unsigned i = 0; i < sizeof sigs / sizeof sigs[0]; i++) signal(sigs[i], on_term); + atexit(stg_cleanup); /* 任何 return/exit 路径的兜底清理 */ + if (argc<2) { usage(stderr, argv[0]); return 1; } const char *cmd = argv[1]; if (!strcmp(cmd,"--help")||!strcmp(cmd,"-h")) { usage(stdout, argv[0]); return 0; } if (!strcmp(cmd,"update")) { return cmd_update(); } if (!strcmp(cmd,"install")) { - if (argc<3) { fprintf(stderr,"nyai: install: 缺包名或 .mpk\n"); return 2; } + if (argc<3) { op_err("缺包文件路径或索引包名\n"); return 2; } load_sources(); return cmd_install(argv[2]); } @@ -1396,7 +1749,8 @@ int main(int argc, char **argv) } if (!strcmp(cmd,"scan")) return cmd_scan(argc>2?argv[2]:NULL); if (!strcmp(cmd,"upgrade")) { - if (argc<3) { fprintf(stderr,"nyai: upgrade: 缺包名\n"); return 2; } + g_op = "upgrade"; + if (argc<3) { op_err("缺包名\n"); return 2; } return cmd_upgrade(argv[2], argc>3?argv[3]:NULL); } fprintf(stderr,"nyai: 未知命令 '%s'(--help 查看用法)\n", cmd); diff --git a/patch_v2/meowpkg/nyai_host b/patch_v2/meowpkg/nyai_host index f374b2f..6be4008 100755 Binary files a/patch_v2/meowpkg/nyai_host and b/patch_v2/meowpkg/nyai_host differ diff --git a/patch_v2/meowpkg/test_nyai.sh b/patch_v2/meowpkg/test_nyai.sh new file mode 100755 index 0000000..34441e0 --- /dev/null +++ b/patch_v2/meowpkg/test_nyai.sh @@ -0,0 +1,244 @@ +#!/bin/bash +# test_nyai.sh — nyai 宿主回归(M35:app.json 清单权威 + SD 暂存 + 无条件清理) +# +# 覆盖: +# 1 文件名/后缀与包名完全不同(weird-name.bin)仍按清单装进 MeowAPPs/ +# 2 真实反例:签名 os.meow.reader 包命名成 os.meow.reader-0.1.7.mpk 仍装进 MeowAPPs/os.meow.reader +# 3 footer.package ≠ app.json.package → 拒装,错误文案同时给出两个名字 +# 4 os.meow.* 无签名 → 拒装 +# 5 第三方无签名 → 放行(行为不变) +# 6 恶意 app.json.package(../x、a/b、.、..)→ 拒装且不落盘到目录外 +# 7 成功 / 各种失败 / 解压失败 / SIGTERM 中断 → MeowOS/tmp/pkgs 无残留 +# 8 索引路径:Packages 的 Package 字段取自清单(文件改名后按包名仍可装) +# 9 upgrade 复用 install 引擎:清单包名/目标包名不一致、降级都拒 +# +# 依赖:宿主 cc(编 nyai_host / meow_verify_host / meow_sign)。 +# 签名用例需要维护者私钥:默认 ~/.keys/meowos-signing/signing.key, +# 可用 NYAI_TEST_SIGN_KEY 覆盖;无私钥时签名用例 SKIP(不判 FAIL)。 +# 真实反例包默认 ~/.cache/os.meow.reader-0.1.7-signed.mpk,可用 NYAI_TEST_REAL_MPK 覆盖。 +# +# 用法:./test_nyai.sh [-v] +set -u + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$HERE/../.." && pwd) +VERBOSE=0 +[ "${1:-}" = "-v" ] && VERBOSE=1 +WORK=${NYAI_TEST_WORK:-$(mktemp -d "${TMPDIR:-/tmp}/nyai-m35.XXXXXX")} +mkdir -p "$WORK" +CLEANUP_WORK=1 +[ -n "${NYAI_TEST_WORK:-}" ] && CLEANUP_WORK=0 +trap '[ "$CLEANUP_WORK" = 1 ] && rm -rf "$WORK"' EXIT + +PASS=0; FAIL=0; SKIP=0 +ok() { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf 'FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; } +skip() { SKIP=$((SKIP+1)); printf 'SKIP %s (%s)\n' "$1" "${2:-}"; } + +KEY=${NYAI_TEST_SIGN_KEY:-$HOME/.keys/meowos-signing/signing.key} +[ -r "$KEY" ] || KEY="" +REAL_MPK=${NYAI_TEST_REAL_MPK:-$HOME/.cache/os.meow.reader-0.1.7-signed.mpk} + +# ---------------- 构建 ---------------- +echo "== build ==" +( cd "$HERE" && ./build_nyai.sh host >/dev/null ) || { echo "build nyai_host failed"; exit 1; } +NYAI="$HERE/nyai_host" +CC=${CC_HOST:-cc} +V="$ROOT/patch_v2/meow_verify" +mkdir -p "$WORK/tools" +"$CC" -Os -Wno-sign-compare -Wno-unterminated-string-initialization -Wno-unused-function \ + -c "$V/tweetnacl.c" -o "$WORK/tools/tn.o" || { echo "build tweetnacl failed"; exit 1; } +"$CC" -Os -c "$V/sha256.c" -o "$WORK/tools/sha.o" || exit 1 +"$CC" -Os -I"$V" -c "$V/meow_verify.c" -o "$WORK/tools/mv.o" || exit 1 +"$CC" "$WORK/tools/mv.o" "$WORK/tools/sha.o" "$WORK/tools/tn.o" -o "$WORK/tools/meow_verify_host" || exit 1 +if [ -n "$KEY" ]; then + "$CC" -Os -I"$V" -I"$ROOT/patch_v2/meow_sign" "$ROOT/patch_v2/meow_sign/meow_sign.c" \ + "$WORK/tools/tn.o" "$WORK/tools/sha.o" -o "$WORK/tools/meow_sign" || exit 1 +fi +VERIFY="$WORK/tools/meow_verify_host" + +# ---------------- 辅助 ---------------- +sb() { # sb → 打印沙箱 MeowROOT + local d="$WORK/case-$1" + rm -rf "$d" + mkdir -p "$d/root/MeowAPPs" "$d/root/MeowOS/opt/nyai" + printf '%s\n' "$d/root" +} +mkpkg() { # mkpkg + rm -rf "$1"; mkdir -p "$1" + printf '{"name":"Test","package":"%s","exec":"bin","version":"0.1.%s","version_code":%s}\n' \ + "$2" "$3" "$3" > "$1/app.json" + printf '#!/bin/sh\nexit 0\n' > "$1/bin"; chmod +x "$1/bin" +} +pack() { tar czf "$2" -C "$1" .; } +sign() { # sign :文件名茎必须等于包名(meow_sign 约定),签完覆盖原文件 + [ -n "$KEY" ] || return 2 + mkdir -p "$WORK/sign" + cp "$2" "$WORK/sign/$1.mpk" + "$WORK/tools/meow_sign" sign "$WORK/sign/$1.mpk" "$KEY" >/dev/null 2>&1 || return 1 + cp "$WORK/sign/$1.mpk" "$2" +} +nyai_run() { # nyai_run + local r=$1; shift + MEOWROOT="$r" MEOW_VERIFY_BIN="$VERIFY" "$NYAI" "$@" +} +tmp_clean() { # tmp_clean :MeowOS/tmp/pkgs 下无任何条目 + local n + n=$(find "$1/MeowOS/tmp/pkgs" -mindepth 1 2>/dev/null | wc -l | tr -d ' ') + [ "$n" = "0" ] +} +show() { [ "$VERBOSE" = 1 ] && printf '%s\n' "$1"; } +expect() { # expect