/* 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 #include #include #include #include #include #include #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; }