P1(信任边界):is_system_pkg() 原用 strncmp,而落盘目录在 exFAT(名字比较
不区分大小写)。包内 app.json 写 "OS.MEOW.READER" 时 valid_pkg 放行、
is_system_pkg 返回 0 → 当第三方无签放行 → dst 解析到已装的
MeowAPPs/os.meow.reader → 备份后部署假包并删掉真系统 App(无需私钥)。
三层封堵:
1) is_system_pkg 改 strncasecmp(前缀判定不再受大小写影响);
2) valid_pkg 收紧为只收 [a-z0-9._-],并拒绝前导/尾随 '.'(现网包名全小写,
无兼容代价;tail-dot 同时挡 exFAT/Windows 名字映射怪癖);
3) 部署前 readdir 检查 MeowAPPs 里是否有与 pkg 仅大小写不同的已装目录
(有则拒装并报两个名字);部署后再用 readdir 复核 on-disk 名字与清单包名
逐字节相等,不等则回滚,杜绝「身份不明的 App 目录」。
P2-1:fmt_utc() 的 March-based 月份换算写成 +9(应为 -9),1/2 月会打成
20251901/20251931 且年份不 +1。修正为 int 运算;新增 test_fmt_utc.c
(直接 include nyai.c 测 static 函数)覆盖 epoch/负数、1/1、2/1、2/28、闰年
2/29(2024 与 2000)、3/1、12/31 23:59:59、2038-01-01、int32 上界、2100 非闰。
纵深防御(评审残留风险,一并做):
- tar 成员名净化不再依赖外部 tar:mpk_check_members 解析 tar -tv 的名字,
拒绝绝对路径 / 含 '..' 组件 / 首字符 '-' / 原始控制字符;解包后 stg_tree_ok
再用 readdir 的真实名字复核控制字符(tar 列名会把控制字符转义成可见文本,
原检查会漏)。
- 解压炸弹:累加 tar -tv 的体积,超过 512MB 上限拒装,并加 statvfs
「剩余空间 ≥ 体积 + 16MB」门禁(解包先于验签,恶意包可在验签前写满卡)。
- 顺手改掉 appmeta.package 注释里「== 文件名茎」的旧语义。
回归:test_nyai.sh 49 → 70 条断言全过(新增:fmt_utc 13 例、大写包名绕过顶替
已装系统包被拒且原 App 未变、尾点包名、与已装目录仅大小写不同、绝对路径/
../ / 首字符 '-' 成员名、控制字符成员名、700MB 解压炸弹),
`-Wall -Wextra` 零告警,二进制可重复构建。
交付:nyai 153,844 B / md5 ef7940ecf5ece993d718ac1d9019cc82(旧 4524ce6b…)。
真机 10.58.215.50 仍不可登录(22 端口 TCP 可连、15-50s 无 banner),本轮
**未做真机复核**,仅宿主验证。
1932 lines
78 KiB
C
1932 lines
78 KiB
C
/*
|
||
* 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 <strings.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/statvfs.h>
|
||
#include <sys/types.h>
|
||
#include <sys/wait.h>
|
||
|
||
#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)。
|
||
* 每次安装一个子目录 <pkg>-<UTC 时间戳>-<pid>-<n>,无论成败都删。 */
|
||
#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<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"(唯一权威包名;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;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.*(防伪造系统包名)。
|
||
* **必须大小写不敏感**:安装落盘目录在 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 <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`:`<mode> <owner/group> <size> <date> <time> <name...>`。
|
||
* GNU tar 与 busybox tar 都是这 5 个固定字段,名字取余下整段(可含空格)。
|
||
* 返回 0 成功;-1 行格式不认识(size 非纯数字)。 */
|
||
static int tar_tv_parse(char *line, char *name, int n, long long *sz)
|
||
{
|
||
char *p = line;
|
||
char tok[5][64];
|
||
for (int i = 0; i < 5; i++) {
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!*p || *p == '\n') return -1;
|
||
int o = 0;
|
||
while (*p && *p != ' ' && *p != '\t' && *p != '\n') {
|
||
if (o < (int)sizeof tok[0] - 1) tok[i][o++] = *p;
|
||
p++;
|
||
}
|
||
tok[i][o] = 0;
|
||
}
|
||
for (char *q = tok[2]; *q; q++)
|
||
if (*q < '0' || *q > '9') return -1;
|
||
if (!tok[2][0]) return -1;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!*p) return -1;
|
||
char *e = p + strlen(p);
|
||
while (e > p && (e[-1] == '\n' || e[-1] == '\r')) *--e = 0;
|
||
if ((int)strlen(p) + 1 > n) return -1;
|
||
snprintf(name, n, "%s", p);
|
||
*sz = atoll(tok[2]);
|
||
return 0;
|
||
}
|
||
|
||
/* 解包前逐条检查归档成员:类型只允许普通文件 '-' 与目录 'd'(拒符号链接/硬链接/
|
||
* 设备/FIFO);名字自己做净化,不依赖外部 tar 的路径处理——名称含 '..' 组件、
|
||
* 绝对路径、控制字符、或首字符 '-' 一律拒装。同时累加成员体积供解压炸弹门禁用。
|
||
* 返回 0 通过 / -1 读失败 / -2 成员类型非法 / -3 空归档 / -4 成员名非法 / -5 行格式不认识。 */
|
||
static int mpk_check_members(const char *mpk, long long *total_out)
|
||
{
|
||
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], name[1024];
|
||
int n = 0, rc = 0;
|
||
long long total = 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);
|
||
rc = -2;
|
||
break;
|
||
}
|
||
long long sz = 0;
|
||
if (tar_tv_parse(line, name, sizeof name, &sz) != 0) {
|
||
op_err("无法解析包成员列表(不支持的 tar 输出格式),拒绝安装\n");
|
||
rc = -5;
|
||
break;
|
||
}
|
||
if (name[0] == '/' || name[0] == '-' || has_dotdot_comp(name)) {
|
||
op_err("包成员路径非法(绝对路径/.. 穿越/首字符 '-'),拒绝安装: %s\n", name);
|
||
rc = -4;
|
||
break;
|
||
}
|
||
for (const char *q = name; *q; q++) {
|
||
unsigned char c = (unsigned char)*q;
|
||
if (c < 0x20 || c == 0x7f) {
|
||
op_err("包成员名含控制字符,拒绝安装\n");
|
||
rc = -4;
|
||
break;
|
||
}
|
||
}
|
||
if (rc) break;
|
||
if (total > LLONG_MAX - sz) { rc = -5; break; }
|
||
total += sz;
|
||
}
|
||
pclose(pf);
|
||
if (rc) return rc;
|
||
if (!n) return -3;
|
||
if (total_out) *total_out = total;
|
||
return 0;
|
||
}
|
||
|
||
/* 暂存所在文件系统余量检查(解包体积 + 16MB 余量)。statvfs 不可用时不阻断。 */
|
||
static int stg_space_ok(const char *base, long long need)
|
||
{
|
||
struct statvfs vfs;
|
||
if (statvfs(base, &vfs) != 0) return 0;
|
||
long long avail = (long long)vfs.f_bavail * (long long)vfs.f_frsize;
|
||
if (avail <= 0) return 0;
|
||
if (need + STG_SPACE_MARGIN > avail) {
|
||
op_err("SD 卡剩余空间不足:需要约 %lld MB + 16MB 余量,可用 %lld MB\n",
|
||
(need + 1048575) / 1048576, avail / 1048576);
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 解包后复核暂存树:只允许普通文件与目录、普通文件 nlink 必须为 1
|
||
* (硬链接到外部文件会表现为 nlink>1);名字里不得有控制字符
|
||
* (tar 列名时可能把控制字符转义成可见文本,这里用 readdir 的真实 on-disk 名字复核)。 */
|
||
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;
|
||
for (const char *q = de->d_name; *q; q++) {
|
||
unsigned char c = (unsigned char)*q;
|
||
if (c < 0x20 || c == 0x7f) { ok = 0; break; }
|
||
}
|
||
if (!ok) break;
|
||
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;
|
||
}
|
||
|
||
/* MeowAPPs 下是否有与 pkg 仅大小写不同的已存在条目(exFAT 上两者是同一个 dentry)。
|
||
* 返回 1=有冲突(other 回传该条目名)/ 0=无 / -1=目录打不开(视为无)。 */
|
||
static int meowapps_name_conflict(const char *pkg, char *other, int n)
|
||
{
|
||
char dir[BUF];
|
||
if (snprintf(dir, sizeof dir, "%s/MeowAPPs", meow_root()) >= (int)sizeof dir) return -1;
|
||
DIR *d = opendir(dir);
|
||
if (!d) return -1;
|
||
int hit = 0;
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue;
|
||
if (!strcmp(de->d_name, pkg)) continue; /* 逐字节相同 = 正常的重装 */
|
||
if (strcasecmp(de->d_name, pkg) == 0) {
|
||
if (other && n > 0) snprintf(other, n, "%s", de->d_name);
|
||
hit = 1;
|
||
break;
|
||
}
|
||
}
|
||
closedir(d);
|
||
return hit;
|
||
}
|
||
|
||
/* path 的父目录里是否存在**逐字节**等于 path 末段名字的条目(不依赖文件系统的大小写/
|
||
* 尾点/尾空格映射:用 readdir 读回真实 on-disk 名字再比)。1=存在 0=不存在。 */
|
||
static int path_exact_entry(const char *path)
|
||
{
|
||
const char *slash = strrchr(path, '/');
|
||
if (!slash || !slash[1]) return 0;
|
||
char dir[BUF];
|
||
size_t dl = (size_t)(slash - path);
|
||
if (dl == 0 || dl >= sizeof dir) return 0;
|
||
memcpy(dir, path, dl);
|
||
dir[dl] = 0;
|
||
const char *want = slash + 1;
|
||
DIR *d = opendir(dir);
|
||
if (!d) return 0;
|
||
int hit = 0;
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (!strcmp(de->d_name, want)) { hit = 1; break; }
|
||
}
|
||
closedir(d);
|
||
return hit;
|
||
}
|
||
|
||
/* ---------------- 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);
|
||
|
||
/* ② 解包前检查归档成员(类型 + 名字净化)并统计解压后体积 */
|
||
long long unpack_bytes = 0;
|
||
int mr = mpk_check_members(mpk_path, &unpack_bytes);
|
||
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;
|
||
}
|
||
if (unpack_bytes > MAX_UNPACK_BYTES) {
|
||
op_err("包解压后体积约 %lld MB,超过 %lld MB 上限,拒绝安装(防解压炸弹)\n",
|
||
(unpack_bytes + 1048575) / 1048576, (long long)(MAX_UNPACK_BYTES / 1048576));
|
||
stg_cleanup();
|
||
return 1;
|
||
}
|
||
if (stg_space_ok(base, unpack_bytes) != 0) {
|
||
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;
|
||
}
|
||
/* exFAT 名字比较不区分大小写:若已装目录与 pkg 仅大小写不同,dst 会解析到那个
|
||
* 已装 App 上(顶掉它)。正常路径由 valid_pkg 只收小写挡住,这里是第二道。 */
|
||
{
|
||
char clash[128] = "";
|
||
int c = meowapps_name_conflict(pkg, clash, sizeof clash);
|
||
if (c == 1) {
|
||
op_err("安装目录名 %s 与已装 %s 仅大小写不同(SD 卡 exFAT 不区分大小写,会顶掉它),拒绝安装\n",
|
||
pkg, clash);
|
||
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;
|
||
}
|
||
/* 部署后复核 on-disk 名字与清单包名逐字节相等(exFAT 的尾点/尾空格/大小写映射
|
||
* 若把目录名改成了别的,宁可回滚也不留一个身份不明的 App 目录)。 */
|
||
if (!path_exact_entry(dst)) {
|
||
op_err("部署后的目录名与清单包名 %s 不完全一致(文件系统名字映射差异),回滚\n", pkg);
|
||
rm_rf(dst);
|
||
if (replacing) rename(bak, dst);
|
||
stg_cleanup();
|
||
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;
|
||
}
|