/* * 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 / * deb [trusted=yes] / 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//; * 数据 init(MeowData//{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。 * * 包身份(M35):**包名/版本/依赖全部来自包内 app.json**,文件名与后缀只作路径,不参与身份判定。 * 安装流:解包到 SD 暂存目录 → 读 app.json(验签前,无循环依赖)→ 校验 footer.package 与 * app.json.package 一致 → 用清单包名调 meow_verify → 防降级 → rename 部署。 * 暂存目录 = /MeowOS/tmp/pkgs/---/,成功/失败/被信号中断都删。 * * 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define NYAI_VERSION "0.3.1" #define BUF 1024 #define MAX_SRC 16 #define MAX_PKG 256 #define MAX_DEPS 64 #define MAX_DEPTH 32 /* 解包暂存目录根(相对 MeowROOT;真机 = /usr/data/mnt/sd_0/MeowOS/tmp/pkgs)。 * 每次安装一个子目录 ---,无论成败都删。 */ #define STG_SUBDIR "MeowOS/tmp/pkgs" /* 单包解压后体积上限(防解压炸弹:解包先于验签,恶意包可以在验签失败前把卡写满)。 * 现网最大 App ≈ 6MB(ncm),512MB 留足余量;另加 statvfs 余量检查。 */ #define MAX_UNPACK_BYTES (512LL * 1024 * 1024) #define STG_SPACE_MARGIN (16LL * 1024 * 1024) /* .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-z0-9._-],且拒绝 "." / ".." / 前导或尾随 '.'。 * 只收小写是安全要求:SD 卡是 exFAT,名字比较不区分大小写, * 允许大写就能用 "OS.MEOW.READER" 落到已装的 MeowAPPs/os.meow.reader 上 * 并绕过 is_system_pkg() 的签名门(现网包名全小写,收紧无兼容代价)。 */ static int valid_pkg(const char *s) { if (!s || !*s) return 0; if (s[0] == '.') return 0; /* "." / ".." / 隐藏名 */ size_t n = strlen(s); if (s[n-1] == '.') return 0; /* exFAT/Windows 尾点映射怪癖 */ for (; *s; s++) { unsigned char c = (unsigned char)*s; if (!(islower(c) || isdigit(c) || c=='.' || c=='-' || c=='_')) 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 has_dotdot_comp(const char *p); 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; int m = (int)mp + (mp < 10 ? 3 : -9); /* March-based → 1..12 */ if (m <= 2) y++; long long rem = sec % 86400; snprintf(out, n, "%04lld%02d%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 (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"(唯一权威包名;M35 后与文件名无关) */ 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;iname,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 输出为 " ",仅取第一个空白前 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] */ 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; } /* 某源的索引文件路径:本地 = /Packages;http = /Packages(远端),本地缓存 = cache/src */ 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 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;ipkg); 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;idisplay); 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 生成)\n"); } } printf("Done.\n"); return ok ? 0 : 1; } /* ---------------- index ---------------- * 索引的 Package 字段 = 包内 app.json.package(**不看文件名**;文件名只作 Filename 定位)。 * 清单缺 package / 非法 → 跳过并告警,不入索引(否则 install 时验签与落盘目录会错)。 * 注意(MPK_SIGNATURE_DESIGN §5.5):索引记录的 SHA256 = 整包哈希(含尾部签名 footer 的 .mpk)。 * 必须先 `meow_sign sign .mpk` 完成签名、再 `nyai index `,否则索引与装后的整包不一致, * install 的 sha256_file 校验会失败。 */ 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;id_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;id_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.*(防伪造系统包名)。 * **必须大小写不敏感**:安装落盘目录在 exFAT 上(名字比较不区分大小写), * 若这里区分大小写,"OS.MEOW.READER" 会被当成第三方放行并落到已装的 * MeowAPPs/os.meow.reader 上,直接顶掉系统 App(valid_pkg 也已收紧为只收小写, * 这里是第二道防线:任一改动失效都不会打开这个口子)。 */ static int is_system_pkg(const char *pkg) { return pkg && !strncasecmp(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 --package 验签。 * 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//data/.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//data/.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" 现为二进制依赖声明:[:][,...],=required|req(默认)/optional|opt。 * 与 launcher meow_parse_depends 同语义;按序查 /usr/bin/ 与 /MeowOS/opt/bin/(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`:`