Files
MeowOS-For-R3P2/patch_v2/meowpkg/nyai.c
T
MeowOS 60072a08f1 feat(pkg): version_code 安卓式版本规则落地 (agent-190, 用户定案 APPS §3.8) — app.json 增 version_code(纯数字唯一比较依据), version 仅显示; nyai install/upgrade/list-upgradable/index 全走 code 比较(缺 code 拒装); launcher meow_pkg_install 同步门禁; 旧点分比较函数移除
nyai f2938d28→4cf5f690; launcher 24c9ee5d→ab8e610f; 宿主 9 场景全过(升/同版/降级拒/缺code拒/index/upgrade一致)
出入: 全新安装缺 code 放行(门禁仅 replacing 分支); export 保留签名的版本比对仍用显示版字符串(nyai+launcher 一致,待后续统一)
2026-08-31 04:56:24 +08:00

1328 lines
55 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 <目录>` 生成。
* - update 缓存索引到 MeowOS/opt/nyai/cache;install 按名称从索引取包(本地文件或 http 拉取),
* 处理 Depends(递归)、校验 SHA256 + app.json;同 name 先备份;解压 MeowAPPs/<pkg>/;
* 数据 init(MeowData/<pkg>/{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。
*
* 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NYAI_VERSION "0.2.0"
#define BUF 1024
#define MAX_SRC 16
#define MAX_PKG 256
#define MAX_DEPS 64
#define MAX_DEPTH 32
/* ---------------- 路径 ---------------- */
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()); }
/* ---------------- 源结构 ---------------- */
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._-] */
static int valid_pkg(const char *s)
{
if (!s || !*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;
}
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));
}
/* ---------------- 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 字段(name/version/depends/description)。
* 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -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;
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,"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);
/* 按记录安装一个索引包(含 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);
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> ----------------
* 注意(MPK_SIGNATURE_DESIGN §5.5):索引记录的 SHA256 = 整包哈希(含尾部签名 footer 的 .mpk)。
* 必须先 `meow_sign sign <pkg>.mpk` 完成签名、再 `nyai index <dir>`,否则索引与装后的整包不一致,
* install 的 sha256_file 校验会失败。本命令不改,仅标注此顺序约束(签名分级见 install_mpk_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;
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);
}
char sha[128]=""; sha256_file(fpath, sha, sizeof sha);
fprintf(f,"Package: %s\n", stem);
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", stem, 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;
}
/* ---------------- install(文件或名称) ---------------- */
static int install_mpk_file(const char *mpk_path)
{
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; }
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 不落盘)。 */
/* ① 先解到临时目录读元数据,再决定替换(防降级 + 校验 app.json) */
rm_rf(stg);
if (mkdir_p(stg)!=0) { fprintf(stderr,"nyai: install: 无法建临时目录 %s\n", stg); return 1; }
char cmd[1024];
snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, 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;
}
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;
}
/* ①.5 签名分级 + 三重绑定(MPK_SIGNATURE_DESIGN §3.3/§5.2 失败行为表) */
int vr = verify_mpk_signature(mpk_path, pkg, NULL, 0);
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;
} else if (vr == 2) {
if (system_pkg) {
fprintf(stderr,"nyai: install: 系统包 %s 必须签名(无签名),拒绝安装\n", pkg);
rm_rf(stg); 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;
}
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 → 拒装。 */
if (replacing) {
char instj[BUF]; snprintf(instj,sizeof instj,"%s/app.json", dst);
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;
}
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;
}
rm_rf(bak);
if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: install: 备份 %s 失败: %s\n", dst, strerror(errno)); rm_rf(stg); return 1; }
printf("Backing up existing %s -> %s\n", pkg, bak);
} else {
rm_rf(dst); /* 防御:全新安装前清残留 */
}
/* ③ 部署临时目录 -> 正式目录 */
if (rename(stg,dst)!=0) {
fprintf(stderr,"nyai: install: 部署 %s 失败: %s\n", dst, strerror(errno));
rm_rf(stg); if (replacing) rename(bak,dst); /* 回滚到旧版 */
return 1;
}
/* ④ 数据 init(MeowData 数据保留,仅保证目录存在) */
char d1[BUF],d2[BUF];
path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache");
mkdir_p(d1); mkdir_p(d2);
printf("Setting up %s (%s) ...\n", pkg, m.version[0]?m.version:"?");
/* ④.5 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) { 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;
}
/* ② 按索引装 */
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");
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;
}
/* 从 JSON 数组串(含 '[' ']')提取字符串成员;返回项数,负=格式错 */
static int json_array_strings(const char *arr, char items[][256], int max)
{
int n = 0;
const char *p = arr;
while (*p && *p!='[') p++;
if (*p!='[') return -1;
p++;
while (*p && n<max) {
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r'||*p==',') p++;
if (*p==']') break;
if (*p!='"') return -1; /* 数组元素必须是字符串 */
p++;
int o=0;
while (*p && *p!='"') {
if (*p=='\\' && p[1]) { if (o<255) items[n][o++]=p[1]; p+=2; continue; }
if (o<255) items[n][o++]=*p;
p++;
}
if (*p!='"') return -1;
p++;
items[n][o]=0;
n++;
}
return n;
}
/* 黑名单(默认):排除 *.pre_* *.bak_* logs/ *.log .DS_Store,其余全打 */
static int export_blacklist(const char *src, const char *outtmp)
{
char listf[BUF]; snprintf(listf,sizeof listf,"%s.wl", outtmp);
char cmd[2048];
snprintf(cmd,sizeof cmd,
"(cd '%s' && find . \\( -name '*.pre_*' -o -name '*.bak_*' -o -name '*.log' -o "
"-name '.DS_Store' -o -name logs \\) -prune -o -type f -print | sed 's|^\\./||') > '%s'", src, listf);
int rc = system(cmd);
if (rc!=0) { rm_rf(listf); return -1; }
snprintf(cmd,sizeof cmd,"tar -c -C '%s' -T '%s' 2>/dev/null | gzip -c > '%s' 2>/dev/null", src, listf, outtmp);
rc = system(cmd);
rm_rf(listf);
return rc==0 ? 0 : -1;
}
/* 白名单:只打 app.json + exec + export_files 声明的项(逐项安全校验) */
static int export_whitelist(const char *src, const char *exec_rel, const char *export_json, const char *outtmp)
{
char items[16][256];
int nit = json_array_strings(export_json, items, 16);
if (nit < 0) { fprintf(stderr,"nyai: export: export_files 格式非法(须为字符串数组)\n"); return -1; }
char listf[BUF]; snprintf(listf,sizeof listf,"%s.wl", outtmp);
FILE *lf = fopen(listf,"w");
if (!lf) return -1;
fprintf(lf,"app.json\n%s\n", exec_rel);
for (int i=0;i<nit;i++) {
char mem[BUF];
if (sanitize_member(items[i], mem, sizeof mem)!=0) {
fclose(lf); rm_rf(listf);
fprintf(stderr,"nyai: export: 白名单项含非法路径: %s\n", items[i]); return -1;
}
char p[BUF]; snprintf(p,sizeof p,"%s/%s", src, mem);
if (access(p,F_OK)!=0) {
fclose(lf); rm_rf(listf);
fprintf(stderr,"nyai: export: 白名单项不存在: %s\n", mem); return -1;
}
fprintf(lf,"%s\n", mem);
}
fclose(lf);
char cmd[2048];
snprintf(cmd,sizeof cmd,"tar -c -C '%s' -T '%s' 2>/dev/null | gzip -c > '%s' 2>/dev/null", src, listf, outtmp);
int rc = system(cmd);
rm_rf(listf);
return rc==0 ? 0 : -1;
}
static int cmd_export(const char *pkg, const char *out)
{
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: export: 包名无效\n"); return 2; }
char src[BUF],dst[BUF];
snprintf(src,sizeof src,"%s/MeowAPPs/%s", meow_root(), pkg);
if (access(src,R_OK)!=0) { fprintf(stderr,"nyai: export: 未安装 %s\n", pkg); return 1; }
/* 导出前体检:app.json 存在且合法(有 name+exec) */
char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", src);
appmeta m;
if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: export: %s/app.json 不合法(缺 name 或 exec)\n", pkg); return 1; }
/* 导出前体检:exec 文件存在且可执行 */
char exec_rel[BUF];
if (sanitize_member(m.exec, exec_rel, sizeof exec_rel)!=0) {
fprintf(stderr,"nyai: export: exec 字段非法: %s\n", m.exec); return 1;
}
char exepath[BUF]; snprintf(exepath,sizeof exepath,"%s/%s", src, exec_rel);
if (access(exepath, X_OK)!=0) {
fprintf(stderr,"nyai: export: exec 文件 %s 不存在或不可执行,拒绝导出\n", exepath); return 1;
}
char outbuf[BUF];
if (out && *out && unsafe_path(out)) { fprintf(stderr,"nyai: export: 输出路径含非法字符\n"); return 1; }
snprintf(outbuf,sizeof outbuf,"%s", out && *out ? out : pkg);
if (!strstr(outbuf,".mpk")) strncat(outbuf,".mpk", sizeof outbuf-strlen(outbuf)-1);
snprintf(dst,sizeof dst,"%s", outbuf);
/* 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);
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]="";
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);
found++;
}
closedir(d);
if (!found) printf("No .mpk packages found.\n");
else printf("\n%d package(s) available.\n", found);
return 0;
}
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); }
}
/* 防降级门禁(与 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' | 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;
}
/* ---------------- 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 <pkg|file.mpk> Install by index name or direct .mpk (resolves Depends)\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.mpk] Upgrade app, keep MeowData\n"
" %s --help This help\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());
}
int main(int argc, char **argv)
{
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; }
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")) {
if (argc<3) { fprintf(stderr,"nyai: upgrade: 缺包名\n"); return 2; }
return cmd_upgrade(argv[2], argc>3?argv[3]:NULL);
}
fprintf(stderr,"nyai: 未知命令 '%s'(--help 查看用法)\n", cmd);
return 2;
}