feat(nyai): M35 包身份以已签名 app.json 清单为准 + 暂存迁到 SD 并必清理
包名/版本/依赖不再从 .mpk 文件名推导(用户定案:不对安装包文件名强依赖,
一切信息从清单取),改为解包后读 app.json.package 作唯一权威:
- 落地目录 MeowAPPs/<pkg>、meow_verify --package <pkg>、防降级基准全用清单包名;
- 新增 footer.package == app.json.package 一致性检查,不一致拒装并同时报两个名字;
- valid_pkg 拒绝 "." / ".."(此前 MeowAPPs/.. 可越出安装目录并被 rm_rf);
- index 的 Package 字段同样取自清单(缺 package 的包跳过并告警);
- scan/export 同步;--help 与错误文案去掉「需要 <pkg>.mpk」。
暂存:MeowAPPs/.stg_<pid>_<pkg>(/usr/data NAND,仅剩 4MB)
→ <MeowROOT>/MeowOS/tmp/pkgs/<pkg>-<UTC时间戳>-<pid>-<n>/(真机即 SD 卡):
- mkdir 原子占位 + 失败重试 + pid/序号保证唯一,时间戳只给人看;
- SD 未挂载/不可写直接拒装(不回退 /usr/data:目标本就在 SD,且跨 fs rename 会 EXDEV);
- 解包前后各一道防护:tar -tv 只允许普通文件/目录(拒符号链接/硬链接/设备/FIFO),
解包后复核暂存树(非普通文件、nlink>1 一律拒);
- 中央化 cleanup + SIGINT/SIGTERM/SIGHUP handler(先收子进程组再清)+ atexit 兜底,
成功/验签失败/清单非法/降级/解压失败/被中断全部删除暂存目录。
真机 time() 受 zig 0.17 mipsel time64 ABI 缺陷影响返回垃圾(实测 3381223399391169494、
localtime_r=NULL),故时间戳直取内核 o32 time32(syscall 4263)并自行换算 UTC,
不依赖 libc time()/localtime_r()。
upgrade 复用 install 同一实现(清单包名必须等于目标包名、目标必须已装)。
新增 host 回归 patch_v2/meowpkg/test_nyai.sh(49 断言:改名包/真实 0.1.7 反例/
footer≠清单/os.meow 无签名/第三方无签名/恶意包名/解压失败/SIGTERM/索引路径/upgrade)。
真机验证(固件 0.8.48,二进制只放 MeowOS/tmp,未覆盖 /usr/bin/nyai):
os.meow.reader-0.1.4.bak.mpk 现在「签名校验通过 (package=os.meow.reader)」后才被
降级门禁拒绝(旧版此处是包名不符);weird-name.bin 装成 dev.m35.probe;SIGTERM
中断后暂存目录为空;Music=199164544、/usr/bin/nyai md5 a7d24553… 均未变。
This commit is contained in:
1 parent
8f748b137d
commit
829d4475e8
4 files changed
+746
-148
No files matched your search
+502
-148
@@ -6,32 +6,54 @@
|
||||
* - 源配置 MeowOS/opt/nyai/sources.list(每行一个源;local <路径> / http <url> /
|
||||
* deb [trusted=yes] <uri> / 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/<pkg>/;
|
||||
* 数据 init(MeowData/<pkg>/{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。
|
||||
*
|
||||
* 包身份(M35):**包名/版本/依赖全部来自包内 app.json**,文件名与后缀只作路径,不参与身份判定。
|
||||
* 安装流:解包到 SD 暂存目录 → 读 app.json(验签前,无循环依赖)→ 校验 footer.package 与
|
||||
* app.json.package 一致 → 用清单包名调 meow_verify → 防降级 → rename 部署。
|
||||
* 暂存目录 = <MeowROOT>/MeowOS/tmp/pkgs/<pkg>-<UTC 时间戳>-<pid>-<n>/,成功/失败/被信号中断都删。
|
||||
*
|
||||
* 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <dirent.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#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)。
|
||||
* 每次安装一个子目录 <pkg>-<UTC 时间戳>-<pid>-<n>,无论成败都删。 */
|
||||
#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/<pkg> 会越出安装目录)。 */
|
||||
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 <dir> ----------------
|
||||
* 索引的 Package 字段 = 包内 app.json.package(**不看文件名**;文件名只作 Filename 定位)。
|
||||
* 清单缺 package / 非法 → 跳过并告警,不入索引(否则 install 时验签与落盘目录会错)。
|
||||
* 注意(MPK_SIGNATURE_DESIGN §5.5):索引记录的 SHA256 = 整包哈希(含尾部签名 footer 的 .mpk)。
|
||||
* 必须先 `meow_sign sign <pkg>.mpk` 完成签名、再 `nyai index <dir>`,否则索引与装后的整包不一致,
|
||||
* 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: 需要 <pkg>.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-… 暂存目录改名成 <pkg>-<UTC 时间戳>-<pid>-<n>(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)。
|
||||
* 暂存:<MeowROOT>/MeowOS/tmp/pkgs/<pkg>-<UTC>-<pid>-<n>/ ——成功/失败/被信号中断都删。 */
|
||||
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-<时间戳>-<pid>-<n> 原子占位 */
|
||||
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;
|
||||
|
||||
/* 暂存目录改名成可读名 <pkg>-<时间戳>-<pid>-<n>;改名失败就沿用 .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/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
|
||||
/* ⑩ II 期:原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
|
||||
if (keep_src) save_mpk_source(mpk_path, pkg);
|
||||
|
||||
/* ⑤ 安装成功后清理本次创建的 .bak_<pid>(失败回滚路径不动) */
|
||||
/* ⑪ 安装成功后清理本次创建的 .bak_<pid>(失败回滚路径不动) */
|
||||
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 <file>.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 <pkg> [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_<pid> */
|
||||
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 <pkg|file.mpk> Install by index name or direct .mpk (resolves Depends)\n"
|
||||
" %s install <file|pkg> Install a package file (any filename) or an index name\n"
|
||||
" %s remove <pkg> [--purge] Uninstall (--purge also deletes MeowData)\n"
|
||||
" %s list List installed apps (MeowAPPs/*)\n"
|
||||
" %s info <pkg> Show package details (app.json)\n"
|
||||
@@ -1354,23 +1699,31 @@ static void usage(FILE *o, const char *argv0)
|
||||
" %s index <dir> Generate Packages index for a .mpk dir\n"
|
||||
" %s export <pkg> [out.mpk] Pack MeowAPPs/<pkg> -> <pkg>.mpk (no data)\n"
|
||||
" %s scan [dir] List .mpk in dir (default .)\n"
|
||||
" %s upgrade <pkg> [file.mpk] Upgrade app, keep MeowData\n"
|
||||
" %s upgrade <pkg> [file] Upgrade installed <pkg>, 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 <dir> / http <url> / deb [trusted=yes] <uri>; # 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);
|
||||
|
||||
Reference in new issue
Block a user