- app_launcher.c 删掉 meow_pkg_install 旧内联实现(文件名推包名、硬要 .mpk 后缀、 rm -rf 目标目录直解、失败不回滚 .bak_、不拒 ./..、系统包大小写敏感比较); 导出保留签名逻辑不动(验签仍只 shell-out meow_verify)。 - 新增 pkg_install.c:fork+exec /usr/bin/nyai install <路径>(argv 直传不经 shell; MEOW_NYAI_BIN 覆写,缺省不走 PATH)。包身份/暂存/清理/防降级/大小写全由 nyai 闭环。 - stdout/stderr 分两条管道收:nyai 进度是缓冲 stdout,错误是即时 stderr,合流会把 真错误顶掉(真机首测即踩到);错误文案只取 stderr 尾行。 - start/poll 两段式(+busy 查询)供 UI 主循环按帧驱动不假死;同步封装 20ms 切片轮询, 180s 超时 SIGTERM→SIGKILL,不用时钟 ABI(M40 time32 教训)。 - main_menu --pkg-install 失败打 stderr 可读文案、退出码统一 0/1。 - test_pkg_install.c:44 断言(路径预检含任意文件名可装/目录/不可读/控制字符/超长、 错误文案矩阵、argv 直传、start 立即返回、并发拒装、超时、nyai 缺失)。 - 重链 meow_launcherd 894940B/cea0a735(链 meow_ui.a 86800633,0 warning); fw_content.conf 同步钉值。
300 lines
13 KiB
C
300 lines
13 KiB
C
/* test_pkg_install.c — M42 宿主单测:安装入口路径预检 + 错误文案 + exec 合同(假 nyai)。
|
||
*
|
||
* 编译运行(在 patch_v2/meow_launcherd 下,需 -I../sdk 因内部头含 SDK 声明):
|
||
* gcc -Wall -Wextra -Os -I../sdk test_pkg_install.c pkg_install.c -o test_pkg_install && ./test_pkg_install
|
||
* 全 PASS -> exit 0。
|
||
*
|
||
* 覆盖:
|
||
* A. 路径预检:任意文件名/后缀可装(M35 定案:身份只来自包内 app.json)、目录/缺失/不可读/
|
||
* 控制字符/超长一律拒。
|
||
* B. 错误文案:nyai 退出码 + 输出尾行 → 单条可读文案(不空、不越界)。
|
||
* C. exec 合同:argv 直传(不经 shell,带空格/引号的路径原样到达)、退出码映射、
|
||
* start/poll 非阻塞(start 立即返回)、并发拒装、超时kill。
|
||
* 假 nyai 用 MEOW_NYAI_BIN 注入(与 MEOW_VERIFY_BIN 同口径),不碰真机/真包。
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <time.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
#include "meow_launcher_internal.h"
|
||
|
||
/* pkg_install.c 依赖的 launcher 日志:宿主打桩(宿主无 util.c)。 */
|
||
void logmsg(const char *fmt, ...)
|
||
{
|
||
(void)fmt;
|
||
}
|
||
|
||
static int g_pass = 0, g_fail = 0;
|
||
static void check(const char *name, int ok)
|
||
{
|
||
if (ok) { g_pass++; printf("PASS %s\n", name); }
|
||
else { g_fail++; printf("FAIL %s\n", name); }
|
||
}
|
||
|
||
static long long now_ms_mono(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||
}
|
||
|
||
static char g_dir[512];
|
||
|
||
/* 在临时目录写一个文件,返回其路径(buf)。 */
|
||
static const char *mkfile(const char *name, const char *content, char *buf, int n)
|
||
{
|
||
snprintf(buf, n, "%s/%s", g_dir, name);
|
||
FILE *f = fopen(buf, "w");
|
||
if (!f) return NULL;
|
||
if (content) fputs(content, f);
|
||
fclose(f);
|
||
return buf;
|
||
}
|
||
|
||
/* 写一个可执行假 nyai。 */
|
||
static const char *mkscript(const char *name, const char *body, char *buf, int n)
|
||
{
|
||
if (!mkfile(name, body, buf, n)) return NULL;
|
||
chmod(buf, 0755);
|
||
return buf;
|
||
}
|
||
|
||
/* ---------------- A. 路径预检 ---------------- */
|
||
static void test_path_check(void)
|
||
{
|
||
char err[256], f[512];
|
||
|
||
check("A1 null -> reject", meow_pkg_install_check_path(NULL, err, sizeof err) != 0);
|
||
check("A1 null -> has text", err[0] != 0);
|
||
err[0] = 0;
|
||
check("A2 empty -> reject", meow_pkg_install_check_path("", err, sizeof err) != 0 && err[0]);
|
||
err[0] = 0;
|
||
check("A3 missing -> reject", meow_pkg_install_check_path("/nonexistent/x.mpk", err, sizeof err) != 0);
|
||
check("A3 missing -> 找不到", strstr(err, "找不到") != NULL);
|
||
err[0] = 0;
|
||
check("A4 directory -> reject", meow_pkg_install_check_path(g_dir, err, sizeof err) != 0);
|
||
check("A4 directory -> 不是普通文件", strstr(err, "不是普通文件") != NULL);
|
||
err[0] = 0;
|
||
check("A5 dot -> reject", meow_pkg_install_check_path(".", err, sizeof err) != 0);
|
||
check("A6 dotdot -> reject", meow_pkg_install_check_path("..", err, sizeof err) != 0);
|
||
err[0] = 0;
|
||
check("A7 control char -> reject",
|
||
meow_pkg_install_check_path("/tmp/a\x01" "b", err, sizeof err) != 0 &&
|
||
strstr(err, "控制字符") != NULL);
|
||
{
|
||
char longp[700];
|
||
memset(longp, 'a', sizeof longp - 1);
|
||
longp[0] = '/'; longp[sizeof longp - 1] = 0;
|
||
err[0] = 0;
|
||
check("A8 too long -> reject",
|
||
meow_pkg_install_check_path(longp, err, sizeof err) != 0 &&
|
||
strstr(err, "过长") != NULL);
|
||
}
|
||
|
||
/* 关键回归:任意文件名/无 .mpk 后缀都可装(m35 事故:改名后装不上)。 */
|
||
if (mkfile("weird name-v0.1.7.bin", "x", f, sizeof f))
|
||
check("A9 any name/no .mpk -> accept",
|
||
meow_pkg_install_check_path(f, err, sizeof err) == 0);
|
||
else check("A9 any name/no .mpk -> accept", 0);
|
||
if (mkfile("OS.MEOW.READER", "x", f, sizeof f))
|
||
check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)",
|
||
meow_pkg_install_check_path(f, err, sizeof err) == 0);
|
||
else check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)", 0);
|
||
|
||
if (mkfile("noread.bin", "x", f, sizeof f)) {
|
||
chmod(f, 0000);
|
||
err[0] = 0;
|
||
check("A11 unreadable -> reject",
|
||
meow_pkg_install_check_path(f, err, sizeof err) != 0 &&
|
||
strstr(err, "不可读") != NULL);
|
||
chmod(f, 0644);
|
||
} else check("A11 unreadable -> reject", 0);
|
||
}
|
||
|
||
/* ---------------- B. 错误文案 ---------------- */
|
||
static void test_error_text(void)
|
||
{
|
||
char out[256];
|
||
|
||
meow_pkg_install_error_text(0, "whatever", out, sizeof out);
|
||
check("B1 rc=0 -> empty", out[0] == 0);
|
||
|
||
meow_pkg_install_error_text(1,
|
||
"Reading package lists... Done\r\nnyai: install: 不允许降级: 新版本 1 < 已装 2\n\n",
|
||
out, sizeof out);
|
||
check("B2 last non-empty line", strcmp(out, "nyai: install: 不允许降级: 新版本 1 < 已装 2") == 0);
|
||
|
||
meow_pkg_install_error_text(7, "", out, sizeof out);
|
||
check("B3 no output -> exit code text", strstr(out, "退出码 7") != NULL);
|
||
|
||
meow_pkg_install_error_text(137, "x", out, sizeof out);
|
||
check("B4 signal -> 128+sig text", strstr(out, "信号 9") != NULL);
|
||
|
||
meow_pkg_install_error_text(MEOW_PKG_E_TIMEOUT, NULL, out, sizeof out);
|
||
check("B5 timeout text", strstr(out, "超时") != NULL);
|
||
|
||
meow_pkg_install_error_text(MEOW_PKG_E_NYAI, "无法执行 /usr/bin/nyai", out, sizeof out);
|
||
check("B6 nyai unavailable text", strstr(out, "不可用") && strstr(out, "/usr/bin/nyai"));
|
||
|
||
meow_pkg_install_error_text(MEOW_PKG_E_BUSY, NULL, out, sizeof out);
|
||
check("B7 busy text", strstr(out, "已有安装") != NULL);
|
||
|
||
meow_pkg_install_error_text(MEOW_PKG_E_PATH, "找不到包文件 /x", out, sizeof out);
|
||
check("B8 path text", strcmp(out, "找不到包文件 /x") == 0);
|
||
|
||
{
|
||
char tiny[24];
|
||
memset(tiny, 0x5a, sizeof tiny);
|
||
meow_pkg_install_error_text(1,
|
||
"nyai: 这是一个非常非常长的错误信息,用来验证截断不会越界写坏栈\n", tiny, 16);
|
||
int guard = 1;
|
||
for (int i = 16; i < (int)sizeof tiny; i++) if (tiny[i] != 0x5a) guard = 0;
|
||
check("B9 truncation bounded", strlen(tiny) < 16 && guard);
|
||
}
|
||
meow_pkg_install_error_text(1, " \n\t\n", out, sizeof out);
|
||
check("B10 whitespace-only detail -> generic", out[0] != 0 && strstr(out, "退出码 1"));
|
||
}
|
||
|
||
/* ---------------- C. exec 合同(假 nyai) ---------------- */
|
||
static void test_exec(void)
|
||
{
|
||
char script[512], pkg[512], log[512], err[256];
|
||
|
||
/* C1: 失败透传 —— stderr 尾行进错误文案,退出码映射 MEOW_PKG_E_FAIL */
|
||
if (mkscript("nyai_fail.sh",
|
||
"#!/bin/sh\n"
|
||
"echo \"Reading package lists... Done\"\n"
|
||
"echo \"nyai: 假失败:测试包损坏\" >&2\n"
|
||
"exit 7\n", script, sizeof script) &&
|
||
mkfile("pkg-fail.bin", "x", pkg, sizeof pkg)) {
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
int r = meow_pkg_install(pkg);
|
||
check("C1 exit!=0 -> MEOW_PKG_E_FAIL", r == MEOW_PKG_E_FAIL);
|
||
check("C1 stderr tail surfaced", strstr(meow_pkg_install_error(), "假失败") != NULL);
|
||
check("C1 not busy after finish", meow_pkg_install_busy() == 0);
|
||
} else check("C1 exit!=0 -> MEOW_PKG_E_FAIL", 0);
|
||
|
||
/* C1b: 真机回归 —— nyai 的 stdout 是管道全缓冲(退出时才刷),错误走 stderr 先到。
|
||
* 合成一路时「最后一行」会把真错误顶成旧进度;错误文案必须取 stderr。 */
|
||
if (mkscript("nyai_buforder.sh",
|
||
"#!/bin/sh\n"
|
||
"echo \"nyai: install: 不允许降级: 新版本 code 8 < 已装 code 9\" >&2\n"
|
||
"echo \"Reading package lists... Done\"\n"
|
||
"echo \"nyai: 签名校验通过 (package=os.meow.reader)\"\n"
|
||
"exit 1\n", script, sizeof script) &&
|
||
mkfile("pkg-buforder.bin", "x", pkg, sizeof pkg)) {
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
int r = meow_pkg_install(pkg);
|
||
check("C1b fail rc", r == MEOW_PKG_E_FAIL);
|
||
check("C1b stderr beats buffered stdout",
|
||
strstr(meow_pkg_install_error(), "不允许降级") != NULL &&
|
||
strstr(meow_pkg_install_error(), "签名校验通过") == NULL);
|
||
} else check("C1b stderr beats buffered stdout", 0);
|
||
|
||
/* C2: argv 直传(不经 shell):带空格/单引号的路径原样到 $2;$1 必须是 install */
|
||
snprintf(log, sizeof log, "%s/argv.log", g_dir);
|
||
if (mkscript("nyai_argv.sh",
|
||
"#!/bin/sh\n"
|
||
"printf '%s|%s\\n' \"$1\" \"$2\" > \"$M42_ARGV_LOG\"\n"
|
||
"exit 0\n", script, sizeof script) &&
|
||
mkfile("name with space 'q'.bin", "x", pkg, sizeof pkg)) {
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
setenv("M42_ARGV_LOG", log, 1);
|
||
int r = meow_pkg_install(pkg);
|
||
check("C2 success rc=0", r == 0);
|
||
FILE *f = fopen(log, "r");
|
||
char line[600] = "";
|
||
if (f) { if (!fgets(line, sizeof line, f)) line[0] = 0; fclose(f); }
|
||
char want[700];
|
||
snprintf(want, sizeof want, "install|%s\n", pkg);
|
||
check("C2 argv passed verbatim (no shell)", strcmp(line, want) == 0);
|
||
unsetenv("M42_ARGV_LOG");
|
||
} else check("C2 success rc=0", 0);
|
||
|
||
/* C3: start 立即返回 + 并发拒装(UI 不假死的关键合同) */
|
||
if (mkscript("nyai_slow.sh", "#!/bin/sh\nsleep 0.4\nexit 0\n", script, sizeof script) &&
|
||
mkfile("pkg-slow.bin", "x", pkg, sizeof pkg)) {
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
long long t0 = now_ms_mono();
|
||
int r = meow_pkg_install_start(pkg);
|
||
long long dt = now_ms_mono() - t0;
|
||
check("C3 start returns fast", r == 0 && dt < 150);
|
||
check("C3 busy while running", meow_pkg_install_busy() == 1);
|
||
int r2 = meow_pkg_install_start(pkg);
|
||
check("C3 second start -> BUSY", r2 == MEOW_PKG_E_BUSY);
|
||
check("C3 busy error text", strstr(meow_pkg_install_error(), "已有安装") != NULL);
|
||
int rc = -1, p = 1, spins = 0;
|
||
while ((p = meow_pkg_install_poll(&rc)) == 1 && spins++ < 500) usleep(10000);
|
||
check("C3 poll finish rc=0", p == 0 && rc == 0);
|
||
check("C3 no longer busy", meow_pkg_install_busy() == 0);
|
||
} else check("C3 start returns fast", 0);
|
||
|
||
/* C4: 超时 —— 子进程被终止,返回 TIMEOUT 且不拖住调用方 */
|
||
if (mkscript("nyai_hang.sh", "#!/bin/sh\nexec sleep 5\n", script, sizeof script) &&
|
||
mkfile("pkg-hang.bin", "x", pkg, sizeof pkg)) {
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
setenv("MEOW_NYAI_TIMEOUT_MS", "100", 1);
|
||
long long t0 = now_ms_mono();
|
||
int r = meow_pkg_install(pkg);
|
||
long long dt = now_ms_mono() - t0;
|
||
check("C4 timeout -> MEOW_PKG_E_TIMEOUT", r == MEOW_PKG_E_TIMEOUT);
|
||
check("C4 timeout bounded (<3s)", dt < 3000);
|
||
check("C4 timeout text", strstr(meow_pkg_install_error(), "超时") != NULL);
|
||
unsetenv("MEOW_NYAI_TIMEOUT_MS");
|
||
} else check("C4 timeout -> MEOW_PKG_E_TIMEOUT", 0);
|
||
|
||
/* C5: nyai 不存在 → 明确报错,不回落旧内联实现、不静默 */
|
||
setenv("MEOW_NYAI_BIN", "/nonexistent/nyai", 1);
|
||
if (mkfile("pkg-ok.bin", "x", pkg, sizeof pkg)) {
|
||
err[0] = 0;
|
||
int r = meow_pkg_install(pkg);
|
||
check("C5 missing nyai -> MEOW_PKG_E_NYAI", r == MEOW_PKG_E_NYAI);
|
||
check("C5 missing nyai -> readable", strstr(meow_pkg_install_error(), "不可用") != NULL);
|
||
} else check("C5 missing nyai -> MEOW_PKG_E_NYAI", 0);
|
||
|
||
/* C6: nyai 存在但不可执行 → 同 E_NYAI */
|
||
if (mkfile("nyai_noexec.sh", "#!/bin/sh\nexit 0\n", script, sizeof script) &&
|
||
mkfile("pkg-ok2.bin", "x", pkg, sizeof pkg)) {
|
||
chmod(script, 0644);
|
||
setenv("MEOW_NYAI_BIN", script, 1);
|
||
check("C6 non-executable nyai -> MEOW_PKG_E_NYAI",
|
||
meow_pkg_install(pkg) == MEOW_PKG_E_NYAI);
|
||
chmod(script, 0755);
|
||
} else check("C6 non-executable nyai -> MEOW_PKG_E_NYAI", 0);
|
||
|
||
/* C7: 入口路径预检先于 exec(坏路径不启动子进程) */
|
||
unsetenv("MEOW_NYAI_BIN");
|
||
check("C7 bad path -> MEOW_PKG_E_PATH",
|
||
meow_pkg_install("/nonexistent/x.mpk") == MEOW_PKG_E_PATH);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
const char *tmp = getenv("TMPDIR");
|
||
if (!tmp || !*tmp) tmp = "/tmp";
|
||
snprintf(g_dir, sizeof g_dir, "%s/m42pkg-XXXXXX", tmp);
|
||
if (!mkdtemp(g_dir)) {
|
||
/* 退路:当前目录(宿主 CI 也能跑) */
|
||
snprintf(g_dir, sizeof g_dir, "./m42pkg-XXXXXX");
|
||
if (!mkdtemp(g_dir)) { printf("FAIL cannot create temp dir\n"); return 1; }
|
||
}
|
||
printf("tmp=%s\n", g_dir);
|
||
|
||
test_path_check();
|
||
test_error_text();
|
||
test_exec();
|
||
|
||
/* 清理(先恢复文件权限,避免删不掉) */
|
||
{
|
||
char cmd[700];
|
||
snprintf(cmd, sizeof cmd, "chmod -R u+rwx '%s' 2>/dev/null; rm -rf '%s'", g_dir, g_dir);
|
||
if (system(cmd) != 0) printf("warn: cleanup %s failed\n", g_dir);
|
||
}
|
||
|
||
printf("\n%d PASS, %d FAIL\n", g_pass, g_fail);
|
||
return g_fail == 0 ? 0 : 1;
|
||
}
|