Files
MeowOS-For-R3P2/patch_v2/meowpkg/nyai.c
T
AskaEth 829d4475e8 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… 均未变。
2026-11-10 21:42:37 +08:00

1759 lines
71 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* nyai.c — MeowOS apt/yum 风格软件包管理 CLI(静态 MIPS32r2, musl, 无外部依赖)
*
* 与 meow_launcherd 的 meow_pkg_install()/export() 语义一致(ANALYSIS.md §18.38 / M2.12;
* APPS.md 附录 v2)。支持镜像源:
* - 源配置 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 <目录>` 生成;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.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)
{
const char *r = getenv("MEOWROOT");
return (r && *r) ? r : "/usr/data/mnt/sd_0";
}
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_sources(char *out, int n)
{ snprintf(out,n,"%s/MeowOS/opt/nyai/sources.list", meow_root()); }
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 {
int http; /* 0=本地路径 1=http(s) */
char base[512]; /* 本地目录 或 http(s)://基址 */
char display[512]; /* 用于提示 */
} src_t;
/* ---------------- 索引包记录 ---------------- */
typedef struct {
char pkg[128];
char version[64];
long long version_code; /* 索引 Version-Code(正整数;has_=0 表示索引缺该项) */
int has_version_code;
char filename[256];
char sha256[128];
char depends[256];
char desc[256];
long long size;
int src_idx;
} pkgrec;
static src_t g_srcs[MAX_SRC];
static int g_nsrc = 0;
/* ---------------- 字符串工具 ---------------- */
static char *trim(char *s)
{
char *e;
while (*s==' '||*s=='\t') s++;
e = s + strlen(s) - 1;
while (e>=s && (*e=='\n'||*e=='\r'||*e==' '||*e=='\t')) *e-- = 0;
return s;
}
/* 去注释(# 到行尾) */
static void strip_comment(char *s)
{
char *p = strchr(s, '#');
if (p) *p = 0;
}
/* 包名合法:仅 [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;
}
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];
snprintf(buf, sizeof buf, "%s", path);
for (char *p = buf + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
}
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
}
static void rm_rf(const char *path)
{
struct stat st;
if (lstat(path, &st) != 0) return;
if (S_ISDIR(st.st_mode)) {
DIR *d = opendir(path);
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name,".") || !strcmp(de->d_name,"..")) continue;
char p[BUF]; snprintf(p,sizeof p,"%s/%s",path,de->d_name);
rm_rf(p);
}
closedir(d);
}
rmdir(path);
} else {
remove(path);
}
}
static long long dir_size(const char *path)
{
struct stat st;
if (lstat(path, &st) != 0) return 0;
if (S_ISREG(st.st_mode)) return st.st_size;
if (!S_ISDIR(st.st_mode)) return 0;
DIR *d = opendir(path);
if (!d) return 0;
long long total = 0;
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name,".") || !strcmp(de->d_name,"..")) continue;
char p[BUF]; snprintf(p,sizeof p,"%s/%s",path,de->d_name);
long long s = dir_size(p);
if (s < 0) { closedir(d); return -1; }
total += s;
}
closedir(d);
return total;
}
static void human_size(long long b, char *out, int n)
{
if (b < 1024) snprintf(out,n,"%lld B", b);
else if (b < 1024*1024) snprintf(out,n,"%.1f KB", b/1024.0);
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)
{
if (!j || !key) return -1;
char pat[160];
snprintf(pat,sizeof pat,"\"%s\"", key);
const char *p = j;
while ((p = strstr(p, pat)) != NULL) {
p += strlen(pat);
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
if (*p != ':') { p++; continue; }
p++;
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
if (*p != '"') { p++; continue; }
p++;
int o=0;
while (*p && *p != '"') {
if (*p=='\\' && p[1]) { if (o<n-1) out[o++]=p[1]; p+=2; continue; }
if (o<n-1) out[o++]=*p;
p++;
}
out[o]=0;
return 0;
}
return -1;
}
static int json_raw(const char *j, const char *key, char *out, int n)
{
if (!j || !key) return -1;
char pat[160];
snprintf(pat,sizeof pat,"\"%s\"", key);
const char *p = strstr(j, pat);
if (!p) return -1;
p += strlen(pat);
while (*p && *p != ':') p++;
if (*p != ':') return -1;
p++;
while (*p && (*p==' '||*p=='\t'||*p=='\n'||*p=='\r')) p++;
if (!*p) return -1;
char open=*p, close=0;
int depth=0, in_str=0, o=0;
if (open=='[') close=']'; else if (open=='{') close='}';
while (*p) {
if (in_str) {
if (o<n-1) out[o++]=*p;
if (*p=='\\' && p[1]) { if (o<n-1) out[o++]=p[1]; p+=2; continue; }
if (*p=='"') in_str=0;
p++; continue;
}
if (*p=='"') { in_str=1; if (o<n-1) out[o++]=*p; p++; continue; }
if (open=='['||open=='{') {
if (*p==open) depth++;
else if (*p==close) { depth--; if (o<n-1) out[o++]=*p; p++; if (depth==0) break; continue; }
if (o<n-1) out[o++]=*p;
p++; continue;
}
if (*p==','||*p=='\n'||*p=='\r'||*p=='}'||*p==']') break;
if (o<n-1) out[o++]=*p;
p++;
}
out[o]=0;
return out[0] ? 0 : -1;
}
/* 读 JSON 整型字段(键带引号、值必须为纯数字,如 "version_code": 12)。
* 缺键 / 值空 / 非数字 / 溢出 → 返回 -1;成功返回 0 并写入 *out。 */
static int json_int(const char *j, const char *key, long long *out)
{
if (!j || !key || !out) return -1;
char pat[160];
snprintf(pat, sizeof pat, "\"%s\"", key);
const char *p = j;
while ((p = strstr(p, pat)) != NULL) {
p += strlen(pat);
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
if (*p != ':') { p++; continue; }
p++;
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
if (!isdigit((unsigned char)*p)) return -1; /* 缺值 / 字符串 / 非数字 */
long long v = 0;
while (isdigit((unsigned char)*p)) {
int d = *p++ - '0';
if (v > (LLONG_MAX - d) / 10) return -1; /* 溢出 */
v = v * 10 + d;
}
*out = v;
return 0;
}
return -1;
}
typedef struct {
char name[256];
char version[64]; /* 仅作显示字符串(versionName 角色,不作比较) */
long long version_code; /* app.json "version_code"(正整数,安装/升级比较唯一依据;缺时 has_=0) */
int has_version_code; /* app.json 是否显式声明 version_code */
char exec[512];
char package[64]; /* app.json "package"(权威包名,== 文件名茎;II 期绑定校验用) */
char author[128];
char routes[512];
char services[512];
char depends[256];
char description[256];
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)
{
memset(m, 0, sizeof *m);
FILE *f = fopen(path, "r");
if (!f) return -1;
static char j[64*1024];
size_t n = fread(j, 1, sizeof j - 1, f);
fclose(f);
j[n] = 0;
int bal=0;
for (size_t i=0;i<n;i++){ if (j[i]=='{') bal++; else if (j[i]=='}') bal--; if (bal<0) return -1; }
if (bal != 0) return -1;
if (json_str(j,"name",m->name,sizeof m->name)!=0 || !m->name[0]) return -1;
if (json_str(j,"exec",m->exec,sizeof m->exec)!=0 || !m->exec[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,"author",m->author,sizeof m->author)!=0) m->author[0]=0;
if (json_str(j,"depends",m->depends,sizeof m->depends)!=0) m->depends[0]=0;
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;
}
/* 从 .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);
char cmd[1024];
snprintf(cmd,sizeof cmd,
"gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null | grep 'app\\.json$'", file);
FILE *pf = popen(cmd, "r");
if (!pf) return -1;
char member[256]; member[0]=0;
if (fgets(member,sizeof member,pf)==NULL) { pclose(pf); return -1; }
char *mm = member; while (*mm==' '||*mm=='\t') mm++;
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;
static char j[16384];
size_t r = fread(j,1,sizeof j-1,pf);
pclose(pf);
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;
if (json_str(j,"description",m->description,sizeof m->description)!=0) m->description[0]=0;
return 0;
}
/* SHA256(外部 sha256sum,busybox/宿主均有);失败返回 -1 */
static int sha256_file(const char *path, char *out, int n)
{
if (unsafe_path(path)) return -1;
char cmd[BUF];
snprintf(cmd,sizeof cmd,"sha256sum '%s' 2>/dev/null", path);
FILE *pf = popen(cmd,"r");
if (!pf) return -1;
char line[256];
if (fgets(line,sizeof line,pf)==NULL) { pclose(pf); return -1; }
pclose(pf);
char *h=line;
/* sha256sum 输出为 "<hash> <filename>",仅取第一个空白前 token */
for (char *q=h; *q; q++) { if (*q==' '||*q=='\t') { *q=0; break; } }
size_t L = strlen(h);
if (L != 64) return -1;
if (n>64) strcpy(out,h); else return -1;
return 0;
}
/* ---------------- 源加载 ---------------- */
static int curl_available(void)
{
if (access("/usr/data/mnt/sd_0/MeowOS/opt/bin/curl",X_OK)==0) return 1;
if (access("/usr/bin/curl",X_OK)==0) return 1;
return 0;
}
static const char *curl_bin(void)
{
if (access("/usr/data/mnt/sd_0/MeowOS/opt/bin/curl",X_OK)==0) return "/usr/data/mnt/sd_0/MeowOS/opt/bin/curl";
if (access("/usr/bin/curl",X_OK)==0) return "/usr/bin/curl";
return "curl";
}
/* 规范化一条源记录(去掉 file://、deb [..] 前缀),返回 0 */
static int normalize_src(const char *raw, src_t *s)
{
char line[600];
snprintf(line,sizeof line,"%s", raw);
strip_comment(line);
char *t = trim(line);
if (!*t) return -1;
/* deb [trusted=yes] <uri> */
if (!strncmp(t,"deb ", 4)) {
t = trim(t + 4);
/* 去掉 [ ... ] */
char *br = strchr(t,'[');
if (br) { char *b2 = strchr(br,']'); if (b2) memmove(br, b2+1, strlen(b2+1)+1); t = trim(t); }
} else if (!strncmp(t,"local ", 6)) {
t = trim(t + 6);
} else if (!strncmp(t,"http ", 5)) {
t = trim(t + 5);
}
if (!*t) return -1;
snprintf(s->base, sizeof s->base, "%s", t);
snprintf(s->display,sizeof s->display,"%s", t);
/* file:// 前缀 → 本地路径 */
if (!strncmp(s->base,"file://", 7)) {
memmove(s->base, s->base+7, strlen(s->base+7)+1);
s->http = 0;
} else if (!strncmp(s->base,"http://",7) || !strncmp(s->base,"https://",8)) {
s->http = 1;
} else {
s->http = 0;
}
return 0;
}
static int load_sources(void)
{
g_nsrc = 0;
char sp[BUF];
path_sources(sp,sizeof sp);
FILE *f = fopen(sp, "r");
if (f) {
char line[600];
while (fgets(line,sizeof line,f) && g_nsrc < MAX_SRC) {
src_t s;
if (normalize_src(line,&s)==0) g_srcs[g_nsrc++] = s;
}
fclose(f);
}
if (g_nsrc == 0) {
/* 缺省源:local MeowOS/opt/repo */
char d[BUF]; path_repo_default(d,sizeof d);
snprintf(g_srcs[0].base,sizeof g_srcs[0].base,"%s", d);
snprintf(g_srcs[0].display,sizeof g_srcs[0].display,"%s", d);
g_srcs[0].http = 0;
g_nsrc = 1;
printf("nyai: 无 sources.list,使用默认本地源 %s\n", d);
}
return g_nsrc;
}
/* 某源的索引文件路径:本地 = <base>/Packages;http = <base>/Packages(远端),本地缓存 = cache/src<i> */
static int source_index_local(const src_t *s, int idx, char *out, int n)
{
if (s->http) {
/* 缓存的远端索引 */
char c[BUF]; path_cache(c,sizeof c);
snprintf(out,n,"%s/src%d-Packages", c, idx);
return 0;
}
snprintf(out,n,"%s/Packages", s->base);
return 0;
}
static void source_index_url(const src_t *s, char *out, int n)
{
snprintf(out,n,"%s/Packages", s->base);
}
static void source_file_local(const src_t *s, const char *filename, char *out, int n)
{
snprintf(out,n,"%s/%s", s->base, filename);
}
static void source_file_url(const src_t *s, const char *filename, char *out, int n)
{
snprintf(out,n,"%s/%s", s->base, filename);
}
/* ---------------- 索引解析 ---------------- */
/* 从文件读 Packages 索引到 recs;返回条数 */
static int parse_index(const char *path, pkgrec *recs, int max)
{
FILE *f = fopen(path,"r");
if (!f) return 0;
int n = 0;
pkgrec cur; memset(&cur,0,sizeof cur); int have = 0;
char line[600];
while (fgets(line,sizeof line,f)) {
char *t = trim(line);
if (!*t) { /* 空行 => 终结一条 */
if (have && n < max) { recs[n++] = cur; }
memset(&cur,0,sizeof cur); have = 0;
continue;
}
char *colon = strchr(t, ':');
if (!colon) continue;
*colon = 0;
char *key = trim(t), *val = trim(colon+1);
if (!strcmp(key,"Package")) { snprintf(cur.pkg,sizeof cur.pkg,"%s",val); have = (cur.pkg[0]!=0); }
else if (!strcmp(key,"Version")) snprintf(cur.version,sizeof cur.version,"%s",val);
else if (!strcmp(key,"Version-Code")) { cur.version_code = atoll(val); cur.has_version_code = (val[0]!=0); }
else if (!strcmp(key,"Filename")) snprintf(cur.filename,sizeof cur.filename,"%s",val);
else if (!strcmp(key,"SHA256")) snprintf(cur.sha256,sizeof cur.sha256,"%s",val);
else if (!strcmp(key,"Size")) { cur.size = atoll(val); }
else if (!strcmp(key,"Depends")) snprintf(cur.depends,sizeof cur.depends,"%s",val);
else if (!strcmp(key,"Description")) snprintf(cur.desc,sizeof cur.desc,"%s",val);
}
if (have && n < max) recs[n++] = cur;
fclose(f);
return n;
}
/* 加载所有缓存索引(update 后);把 src_idx 标注好 */
static int load_all_indexes(pkgrec *recs, int max, const src_t *srcs, int nsrc)
{
int n=0;
for (int i=0;i<nsrc && n<max;i++) {
char p[BUF];
source_index_local(&srcs[i], i, p, sizeof p);
int k = parse_index(p, recs+n, max-n);
for (int j=0;j<k;j++) recs[n+j].src_idx = i;
n += k;
}
return n;
}
/* ---------------- 依赖解析/安装(递归) ---------------- */
static int pkg_installed(const char *pkg)
{
char p[BUF];
snprintf(p,sizeof p,"%s/MeowAPPs/%s/app.json", meow_root(), pkg);
return access(p, R_OK) == 0;
}
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);
/* 收集依赖并逐个安装(用于 install_by_name) */
static int install_deps_of(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int *ndone, int depth)
{
if (depth > MAX_DEPTH) return -1;
if (!r->depends[0]) return 0;
char d[512]; snprintf(d,sizeof d,"%s", r->depends);
char *save=NULL, *tok = strtok_r(d, ", ", &save);
while (tok) {
tok = trim(tok);
if (*tok && valid_pkg(tok)) {
int seen=0;
for (int i=0;i<*ndone;i++) if (!strcmp(done[i],tok)) { seen=1; break; }
if (!seen && !pkg_installed(tok)) {
int idx=-1;
for (int i=0;i<nrec;i++) if (!strcmp(recs[i].pkg,tok)) { idx=i; break; }
if (idx<0) {
fprintf(stderr,"nyai: 警告: 依赖 %s 未在当前源找到\n", tok);
} else {
int rc = install_rec(&recs[idx], recs, nrec, done, *ndone, depth+1);
if (rc != 0) { fprintf(stderr,"nyai: 依赖 %s 安装失败\n", tok); return -1; }
}
}
}
tok = strtok_r(NULL, ", ", &save);
}
return 0;
}
/* 按名称从索引安装 */
static int install_by_name(const char *name, pkgrec *recs, int nrec)
{
int idx=-1;
for (int i=0;i<nrec;i++) if (!strcmp(recs[i].pkg,name)) { idx=i; break; }
if (idx<0) {
fprintf(stderr,"nyai: 未找到包 %s(先 nyai update 刷新索引)\n", name);
return 1;
}
char done[MAX_DEPS][128];
int ndone=0;
return install_rec(&recs[idx], recs, nrec, done, ndone, 0);
}
static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int ndone, int depth)
{
/* 记录正在处理的包,防循环 */
if (ndone < MAX_DEPS) { snprintf(done[ndone],128,"%s", r->pkg); ndone++; }
/* 先装依赖 */
if (install_deps_of(r, recs, nrec, done, &ndone, depth+1) != 0) return 1;
/* 取本地路径(本地源用文件;http 源 curl 下载到 cache) */
src_t *s = &g_srcs[r->src_idx];
char local[BUF], tmp[BUF];
if (s->http) {
char c[BUF]; path_cache(c,sizeof c); mkdir_p(c);
char url[BUF]; source_file_url(s, r->filename, url, sizeof url);
snprintf(tmp,sizeof tmp,"%s/dl_%d", c, rand());
snprintf(local,sizeof local,"%s", tmp);
if (!curl_available()) {
fprintf(stderr,"nyai: http 源 %s 需要 curl(卡上 MeowOS/opt/bin/curl),当前不可用\n", s->display);
return 1;
}
char cmd[512];
snprintf(cmd,sizeof cmd,"%s -fsSL --connect-timeout 3 --max-time 8 '%s' -o '%s' 2>/dev/null", curl_bin(), url, tmp);
printf(" Fetch %s ...\n", url);
if (system(cmd)!=0) { fprintf(stderr,"nyai: 拉取 %s 失败(无网络?)\n", url); remove(tmp); return 1; }
} else {
source_file_local(s, r->filename, local, sizeof local);
if (access(local, R_OK)!=0) {
fprintf(stderr,"nyai: 源 %s 缺包文件 %s\n", s->display, local);
return 1;
}
}
/* SHA256 校验 */
if (r->sha256[0] && strlen(r->sha256)==64) {
char got[128];
if (sha256_file(local, got, sizeof got)!=0) {
fprintf(stderr,"nyai: 无法计算 %s 的 SHA256(需 sha256sum)\n", local);
if (s->http) remove(local);
return 1;
}
if (strcmp(got, r->sha256)!=0) {
fprintf(stderr,"nyai: SHA256 校验失败: %s\n got %s\n want %s\n", local, got, r->sha256);
if (s->http) { remove(local); }
return 1;
}
}
int rc = install_mpk_file(local, r->pkg, 0);
if (s->http) remove(local);
return rc;
}
/* ---------------- update ---------------- */
static int cmd_update(void)
{
load_sources();
printf("Reading package lists...\n");
char c[BUF]; path_cache(c,sizeof c); mkdir_p(c);
int ok = 0;
for (int i=0;i<g_nsrc;i++) {
src_t *s = &g_srcs[i];
printf("Get: [%d] %s", i+1, s->display);
char idxlocal[BUF];
source_index_local(s, i, idxlocal, sizeof idxlocal);
if (s->http) {
if (!curl_available()) { printf(" -- 无 curl,跳过 (http 源不可用)\n"); continue; }
char url[BUF]; source_index_url(s, url, sizeof url);
printf(" -> %s\n", url);
char cmd[512];
snprintf(cmd,sizeof cmd,"%s -fsSL --connect-timeout 3 --max-time 8 '%s' -o '%s' 2>/dev/null", curl_bin(), url, idxlocal);
if (system(cmd)!=0) {
printf(" nyai: 更新 %s 失败(无网络/源不可达),保留旧缓存\n", s->display);
continue; /* 保留旧缓存,不硬失败 */
}
ok++;
} else {
if (access(idxlocal, R_OK)==0) { printf(" ok\n"); ok++; }
else printf(" -- 无 Packages 索引(用 nyai index <dir> 生成)\n");
}
}
printf("Done.\n");
return ok ? 0 : 1;
}
/* ---------------- 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 校验会失败。 */
static int cmd_index(const char *dir)
{
if (!dir || !*dir) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; }
char r[BUF]; snprintf(r,sizeof r,"%s", dir);
if (access(r, R_OK)!=0) { fprintf(stderr,"nyai: index: 目录 %s 不可读\n", r); return 1; }
DIR *d = opendir(r);
if (!d) { fprintf(stderr,"nyai: index: 无法打开 %s\n", r); return 1; }
char out[BUF]; snprintf(out,sizeof out,"%s/Packages", r);
FILE *f = fopen(out, "w");
if (!f) { fprintf(stderr,"nyai: index: 无法写 %s\n", out); closedir(d); return 1; }
printf("Generating index for %s ...\n", r);
struct dirent *de;
int n = 0;
while ((de = readdir(d))) {
if (de->d_name[0]=='.') continue;
size_t L = strlen(de->d_name);
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;
appmeta m;
char ver[64]="", dep[256]="", desc[256]="";
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", 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);
fprintf(f,"Size: %lld\n", (long long)st.st_size);
if (sha[0]) fprintf(f,"SHA256: %s\n", sha);
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", m.package, ver[0]?ver:"0.0.0");
n++;
}
fclose(f);
closedir(d);
printf("OK: %d package(s) indexed -> %s\n", n, out);
return 0;
}
/* ---------------- search ---------------- */
static int cmd_search(const char *pat, pkgrec *recs, int nrec)
{
int n=0;
printf("%-24s %-12s %s\n","Package","Version","Description");
printf("%-24s %-12s %s\n","-------","-------","-----------");
for (int i=0;i<nrec;i++) {
if (pat && *pat) {
if (!strstr(recs[i].pkg, pat) && !strstr(recs[i].desc, pat) && !strstr(recs[i].version, pat))
continue;
}
printf("%-24s %-12s %s\n", recs[i].pkg, recs[i].version[0]?recs[i].version:"?",
recs[i].desc[0]?recs[i].desc:"");
n++;
}
if (!n) printf("No match.\n");
else printf("\n%d package(s) found.\n", n);
return 0;
}
/* ---------------- list-upgradable ---------------- */
static int cmd_list_upgradable(pkgrec *recs, int nrec)
{
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
DIR *d = opendir(r);
if (!d) { fprintf(stderr,"nyai: 无法打开 MeowAPPs\n"); return 1; }
int n=0;
printf("%-24s %-12s %-12s\n","Package","Installed","Available");
struct dirent *de;
while ((de = readdir(d))) {
if (de->d_name[0]=='.') continue;
if (!strcmp(de->d_name,"fonts")||!strcmp(de->d_name,"logs")||
!strcmp(de->d_name,"backgrounds")||!strcmp(de->d_name,"meow.conf")||
strstr(de->d_name,".bak_")) continue;
for (int i=0;i<nrec;i++) {
if (!strcmp(recs[i].pkg, de->d_name)) {
char ap[BUF]; snprintf(ap,sizeof ap,"%s/%s/app.json", r, de->d_name);
appmeta m;
if (load_app_meta(ap,&m)!=0) break;
/* 仅按 version_code 比较(APPS.md §3.8);任一侧缺 code → 不判定为可升级 */
if (recs[i].has_version_code && m.has_version_code &&
recs[i].version_code > m.version_code) {
printf("%-24s %-12s %-12s\n", de->d_name, m.version[0]?m.version:"?", recs[i].version);
n++;
}
break;
}
}
}
closedir(d);
if (!n) printf("All packages up to date.\n");
else printf("\n%d package(s) can be upgraded.\n", n);
return 0;
}
/* ---------------- II 期签名校验(MPK_SIGNATURE_DESIGN §5.1,shell-out meow_verify) ---------------- */
/* 系统包前缀:os.meow.*(防伪造系统包名;独立于 valid_pkg,不改变现有合法包名序列)。 */
static int is_system_pkg(const char *pkg)
{
return pkg && !strncmp(pkg, "os.meow.", 8);
}
/* meow_verify 二进制路径:环境变量 MEOW_VERIFY_BIN 覆盖(宿主测试用),缺省 /usr/bin/meow_verify。 */
static const char *verify_bin(void)
{
const char *v = getenv("MEOW_VERIFY_BIN");
return (v && *v) ? v : "/usr/bin/meow_verify";
}
/* 调用 meow_verify --system <mpk> --package <pkg> 验签。
* out_pkg 回传校验器输出的 footer.package(成功时)。返回校验器退出码:
* 0=有效签(三重绑定①通过) / 1=有签但不通过(改包名/负载/公钥) / 2=无签名 / 127=工具不可用。 */
static int verify_mpk_signature(const char *mpk, const char *pkg, char *out_pkg, int n)
{
if (!mpk || !pkg) return 127;
if (unsafe_path(mpk)) return 127;
char cmd[1200];
snprintf(cmd, sizeof cmd, "'%s' --system '%s' --package '%s' 2>/dev/null",
verify_bin(), mpk, pkg);
if (out_pkg && n > 0) out_pkg[0] = 0;
FILE *pf = popen(cmd, "r");
if (!pf) return 127;
char line[128];
if (fgets(line, sizeof line, pf) != NULL) {
char *e = line + strlen(line);
while (e > line && (e[-1]=='\n'||e[-1]=='\r'||e[-1]==' '||e[-1]=='\t')) *--e = 0;
if (out_pkg && n > 0) {
size_t cl = strlen(line);
if (cl >= (size_t)n) cl = (size_t)n - 1;
memcpy(out_pkg, line, cl); out_pkg[cl] = 0;
}
}
int rc = pclose(pf);
if (rc == -1) return 127;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 127;
}
/* 安装期保存「原签 .mpk」到 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名用,§5.4)。
* 仅当本包带有效签名时调用。失败只告警(导不出签名但安装成功)。 */
static void save_mpk_source(const char *mpk, const char *pkg)
{
char sp[BUF], d[BUF], cmd[1100];
path_data(sp, sizeof sp, pkg, "data");
mkdir_p(sp);
snprintf(d, sizeof d, "%s/%s.mpk.src", sp, pkg);
snprintf(cmd, sizeof cmd, "cp '%s' '%s' 2>/dev/null", mpk, d);
if (system(cmd) != 0)
fprintf(stderr, "nyai: 警告: 保存原签包 %s.mpk.src 失败(导出将无签名)\n", pkg);
}
/* 导出保留签名:MeowData/<pkg>/data/<pkg>.mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装版本
* → 直接复制它作为导出结果(不二次签名);否则返回 -1(调用方现场重打包,无签名)。 */
static int export_preserve_sig(const char *pkg, const char *dst)
{
char sp[BUF], srcp[BUF], instj[BUF], cmd[1100];
path_data(sp, sizeof sp, pkg, "data");
snprintf(srcp, sizeof srcp, "%s/%s.mpk.src", sp, pkg);
if (access(srcp, R_OK) != 0) return -1;
char op[64];
int rc = verify_mpk_signature(srcp, pkg, op, sizeof op);
if (rc != 0) { fprintf(stderr, "nyai: export: 缓存原签包验签失败(rc=%d),回退重打包\n", rc); return -1; }
appmeta cached, inst;
if (mpk_appinfo(srcp, &cached) != 0) return -1;
snprintf(instj, sizeof instj, "%s/MeowAPPs/%s/app.json", meow_root(), pkg);
if (load_app_meta(instj, &inst) != 0) return -1;
if (strcmp(cached.version, inst.version) != 0) {
fprintf(stderr, "nyai: export: 缓存原签包版本(%s) != 已装(%s),回退重打包\n",
cached.version[0]?cached.version:"(?)", inst.version[0]?inst.version:"(?)");
return -1;
}
char tmp[520]; snprintf(tmp, sizeof tmp, "%s.tmp", dst);
snprintf(cmd, sizeof cmd, "cp '%s' '%s' 2>/dev/null", srcp, tmp);
if (system(cmd) != 0 || access(tmp, F_OK) != 0) return -1;
if (rename(tmp, dst) != 0) { rm_rf(tmp); return -1; }
return 0;
}
/* ---------------- 二进制依赖检查(app.json depends,新语义) ----------------
* app.json "depends" 现为二进制依赖声明:<name>[:<mod>][,...],<mod>=required|req(默认)/optional|opt。
* 与 launcher meow_parse_depends 同语义;按序查 /usr/bin/<name> 与 <root>/MeowOS/opt/bin/<name>(X_OK)。
* 安装时**不阻断**:缺 required 输出 WARN 标注「安装后启动将失败」;缺 optional 仅提示。 */
static int bin_dep_exists(const char *name)
{
char p[300];
snprintf(p, sizeof p, "/usr/bin/%s", name);
if (access(p, X_OK) == 0) return 1;
snprintf(p, sizeof p, "%s/MeowOS/opt/bin/%s", meow_root(), name);
if (access(p, X_OK) == 0) return 1;
return 0;
}
static void nyai_check_bin_depends(const appmeta *m)
{
if (!m || !m->depends[0]) return;
char d[256];
snprintf(d, sizeof d, "%s", m->depends);
char *save = NULL, *tok = strtok_r(d, ",", &save);
while (tok) {
tok = trim(tok);
if (!*tok) { tok = strtok_r(NULL, ",", &save); continue; }
char nm[64] = ""; int req = 1;
char *colon = strchr(tok, ':');
size_t nl = colon ? (size_t)(colon - tok) : strlen(tok);
if (nl >= sizeof nm) nl = sizeof nm - 1;
memcpy(nm, tok, nl); nm[nl] = 0;
if (colon) {
char *mod = trim(colon + 1);
if (!strcmp(mod, "optional") || !strcmp(mod, "opt")) req = 0;
else if (!strcmp(mod, "required") || !strcmp(mod, "req")) req = 1;
/* 未知修饰符按 required 处理 */
}
if (!*nm) { tok = strtok_r(NULL, ",", &save); continue; }
if (!bin_dep_exists(nm)) {
if (req)
printf("nyai: 警告: 依赖 %s 缺失(required),安装后启动将失败\n", nm);
else
printf("nyai: 提示: 可选依赖 %s 缺失\n", nm);
}
tok = strtok_r(NULL, ",", &save);
}
}
/* ---------------- 安装期安全设施:信号清理 / 子进程 / 成员检查 / 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, ...)
{
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");
/* ① 暂存目录:包名要解包后才知道,先用 .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);
/* ② 解包前检查归档成员类型 */
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;
}
appmeta m;
if (load_app_meta(appj, &m) != 0) {
op_err("app.json 不合法(缺 name 或 exec)\n");
stg_cleanup();
return 1;
}
/* ④ 包名唯一来源: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);
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) {
op_err("签名验证失败(内容被篡改/包名不符/公钥不符),拒绝安装\n");
stg_cleanup();
return 1;
} else if (vr == 2) {
if (system_pkg) {
op_err("系统包 %s 必须签名(无签名),拒绝安装\n", pkg);
stg_cleanup();
return 1;
}
printf("nyai: 提示: %s 未签名(第三方包放行安装)\n", pkg);
} else {
if (system_pkg) {
op_err("meow_verify 不可用(rc=%d),os.meow.* 拒装(宁缺勿装)\n", vr);
stg_cleanup();
return 1;
}
printf("nyai: 警告: meow_verify 不可用(rc=%d),%s 未验签放行\n", vr, pkg);
}
/* ⑦ 落地目录 + 防降级(基准 = 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];
appmeta im;
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) {
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) {
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); /* 防御:全新安装前清残留 */
}
/* ⑧ 部署:暂存目录与 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");
mkdir_p(d1); mkdir_p(d2);
printf("Setting up %s (%s) ...\n", pkg, m.version[0] ? m.version : "?");
/* ⑩ II 期:原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
if (keep_src) save_mpk_source(mpk_path, pkg);
/* ⑪ 安装成功后清理本次创建的 .bak_<pid>(失败回滚路径不动) */
if (replacing && bak[0]) { rm_rf(bak); printf("Removed backup %s\n", bak); }
printf("OK\n");
return 0;
}
static int cmd_install(const char *arg)
{
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) {
op_err("索引为空,先 nyai update 刷新(或直接 install <包文件>)\n");
return 1;
}
return install_by_name(arg, recs, nrec);
}
/* ---------------- list / info / remove / export / scan / upgrade ---------------- */
static int cmd_list(void)
{
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
DIR *d = opendir(r);
if (!d) { fprintf(stderr,"nyai: list: 打开 %s 失败\n", r); return 1; }
int found=0;
printf("%-24s %-10s %-10s %s\n","Name","Version","Size","ID");
printf("%-24s %-10s %-10s %s\n","----","-------","----","--");
struct dirent *de;
while ((de=readdir(d))) {
if (de->d_name[0]=='.') continue;
if (!strcmp(de->d_name,"fonts")||!strcmp(de->d_name,"logs")||
!strcmp(de->d_name,"backgrounds")||!strcmp(de->d_name,"meow.conf")||
!strcmp(de->d_name,"meow") || strstr(de->d_name,".bak_")) continue;
char didx[BUF],dmeta[BUF];
snprintf(didx,sizeof didx,"%s/%s", r, de->d_name);
snprintf(dmeta,sizeof dmeta,"%s/app.json", didx);
appmeta m;
if (load_app_meta(dmeta,&m)!=0) continue;
long long sz=dir_size(didx);
char hs[24]; human_size(sz,hs,sizeof hs);
printf("%-24s %-10s %-10s %s\n", m.name[0]?m.name:de->d_name, m.version[0]?m.version:"?", hs, de->d_name);
found++;
}
closedir(d);
if (!found) printf("No packages found in MeowAPPs.\n");
else printf("\n%d package(s) installed.\n", found);
return 0;
}
/* 移除 MeowAPPs/<pkg>.bak_<pid> 类残留备份(install 升级/覆盖时产生) */
static void remove_pkg_backups(const char *pkg)
{
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
DIR *d = opendir(r);
if (!d) return;
struct dirent *de;
/* 前缀 <pkg>.bak_ */
char pref[140]; snprintf(pref,sizeof pref,"%s.bak_", pkg);
size_t plen = strlen(pref);
while ((de = readdir(d))) {
if (strncmp(de->d_name, pref, plen)==0) {
char p[BUF]; snprintf(p,sizeof p,"%s/%s", r, de->d_name);
rm_rf(p);
}
}
closedir(d);
}
static int cmd_info(const char *pkg)
{
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: info: 包名无效\n"); return 2; }
char dirp[BUF],appj[BUF];
snprintf(dirp,sizeof dirp,"%s/MeowAPPs/%s", meow_root(), pkg);
snprintf(appj,sizeof appj,"%s/app.json", dirp);
if (access(appj,R_OK)!=0) { fprintf(stderr,"nyai: info: 未安装 %s\n", pkg); return 1; }
appmeta m;
if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: info: %s/app.json 不合法\n", pkg); return 1; }
long long sz=dir_size(dirp);
char hs[24]; human_size(sz,hs,sizeof hs);
printf("Package: %s\n", pkg);
printf("Name: %s\n", m.name);
printf("Version: %s\n", m.version[0]?m.version:"(none)");
if (m.has_version_code) printf("Version-Code: %lld\n", m.version_code);
else printf("Version-Code: (none)\n");
printf("Author: %s\n", m.author[0]?m.author:"(none)");
printf("Exec: %s\n", m.exec);
printf("Size: %s\n", hs);
printf("ExecFile:%s\n", access(m.exec,X_OK)==0?" yes":" no");
if (m.depends[0]) printf("Depends: %s\n", m.depends);
if (m.description[0]) printf("Desc: %s\n", m.description);
if (m.has_routes) printf("Routes: %s\n", m.routes);
if (m.has_services) printf("Services:%s\n", m.services);
return 0;
}
static int cmd_remove(const char *pkg, int purge)
{
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: remove: 包名无效\n"); return 2; }
char dst[BUF]; snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg);
if (access(dst,F_OK)!=0) { fprintf(stderr,"nyai: remove: 未安装 %s\n", pkg); return 1; }
printf("Removing %s ...\n", pkg);
rm_rf(dst);
remove_pkg_backups(pkg); /* 清理 install 升级留下的 .bak_<pid> */
if (purge) {
char dd[BUF]; snprintf(dd,sizeof dd,"%s/MeowData/%s", meow_root(), pkg);
if (access(dd,F_OK)==0) { printf("Purging %s (data) ...\n", pkg); rm_rf(dd); }
}
printf("OK\n");
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;
}
/* 把 app.json 的 exec 路径相对化到包目录下(导出前存在性检查 + 白名单打包用)。
* 约定(MEOW_APP_SDK 现有惯例,launcher 以绝对路径直接 execve):
* - exec 以本包目录 src/ 为前缀 → 取其后相对部分(同一 meow_root,如真机 src=.../MeowAPPs/os.meow.hello → hello);
* - exec 含 "/MeowAPPs/<pkg>/" 段 → 取该段后的相对路径(根路径与 src 不一致时仍可定位,如沙箱/CI 测试);
* - exec 为相对路径(无前导 /)→ 沿用 sanitize_member 语义(剥 ./、禁 ../、禁 shell 元字符);
* - 绝对路径但定位不到包目录段 → 非本包程序,返回 -1(拒绝导出)。
* 返回 0 成功,-1 非法。 */
static int exec_rel_to_pkg(const char *src, const char *exec, const char *pkg, char *out, int n)
{
if (!exec || !*exec) return -1;
const char *rel = NULL;
size_t sl = strlen(src);
if (strncmp(exec, src, sl) == 0 && exec[sl] == '/') {
rel = exec + sl + 1;
} else if (pkg && *pkg) {
char marker[272]; snprintf(marker, sizeof marker, "/MeowAPPs/%s/", pkg);
const char *m = strstr(exec, marker);
if (m) rel = m + strlen(marker);
}
if (rel) {
size_t rl = strlen(rel);
if (rl == 0 || rl >= (size_t)n) return -1;
if (has_dotdot_comp(rel) || unsafe_path(rel)) return -1;
memcpy(out, rel, rl); out[rl] = 0;
return 0;
}
if (exec[0] != '/') return sanitize_member(exec, out, n);
return -1;
}
/* 从 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; }
/* 目录名必须等于清单包名(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) {
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);
/* II 期:导出保留签名 —— 若安装时存了原签 .mpk.src 且其版本==已装 → 直接复制(不二次签名)。 */
if (export_preserve_sig(pkg, dst) == 0) {
printf("OK (signed, preserved from .mpk.src)\n");
return 0;
}
printf("Exporting %s -> %s ...\n", pkg, dst);
char tmpp[BUF]; snprintf(tmpp,sizeof tmpp,"%s.tmp", dst);
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;
}
static int cmd_scan(const char *dir)
{
char r[BUF]; snprintf(r,sizeof r,"%s", dir && *dir ? dir : ".");
DIR *d = opendir(r);
if (!d) { fprintf(stderr,"nyai: scan: 打开 %s 失败\n", r); return 1; }
printf("Scanning %s ...\n", r);
printf("%-24s %-10s %-10s %s\n","Package","Version","Size","File");
struct dirent *de; int found=0;
while ((de=readdir(d))) {
if (de->d_name[0]=='.') continue;
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);
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 ver[64]="";
appmeta m;
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);
if (!found) printf("No .mpk packages found.\n");
else printf("\n%d package(s) available.\n", found);
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)) { 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;
}
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 ---------------- */
static void usage(FILE *o, const char *argv0)
{
fprintf(o,
"nyai %s — MeowOS package manager (apt/yum style)\n"
"\n"
"Usage:\n"
" %s update Read all sources into local index cache\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"
" %s search [pattern] Search index\n"
" %s list-upgradable Show packages with newer index version\n"
" %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] 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());
}
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) { op_err("缺包文件路径或索引包名\n"); return 2; }
load_sources();
return cmd_install(argv[2]);
}
if (!strcmp(cmd,"remove")) {
if (argc<3) { fprintf(stderr,"nyai: remove: 缺包名\n"); return 2; }
int purge=0; for (int i=3;i<argc;i++) if (!strcmp(argv[i],"--purge")) purge=1;
return cmd_remove(argv[2], purge);
}
if (!strcmp(cmd,"list")) return cmd_list();
if (!strcmp(cmd,"info")) {
if (argc<3) { fprintf(stderr,"nyai: info: 缺包名\n"); return 2; }
return cmd_info(argv[2]);
}
if (!strcmp(cmd,"search")) { load_sources(); pkgrec recs[MAX_PKG]; int n=load_all_indexes(recs,MAX_PKG,g_srcs,g_nsrc); return cmd_search(argc>2?argv[2]:NULL, recs, n); }
if (!strcmp(cmd,"list-upgradable")) { load_sources(); pkgrec recs[MAX_PKG]; int n=load_all_indexes(recs,MAX_PKG,g_srcs,g_nsrc); return cmd_list_upgradable(recs,n); }
if (!strcmp(cmd,"index")) {
if (argc<3) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; }
return cmd_index(argv[2]);
}
if (!strcmp(cmd,"export")) {
if (argc<3) { fprintf(stderr,"nyai: export: 缺包名\n"); return 2; }
return cmd_export(argv[2], argc>3?argv[3]:NULL);
}
if (!strcmp(cmd,"scan")) return cmd_scan(argc>2?argv[2]:NULL);
if (!strcmp(cmd,"upgrade")) {
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);
return 2;
}