/* test_browse_nav.c — 浏览访问放开 / 上溯父目录 / 截断标注(纯 C,无 SDK)。 * * 覆盖(M13 用户要求②): * N1 访问放开:系统目录(MeowAPPs/MeowData/MeowOS/lost+found)不再隐藏, * 空目录/无图目录都列出且可进入(scan 返回 0 而不是 -1); * N2 隐藏项/普通文件/符号链接仍不列(防环、防噪声); * N3 不可读目录/不存在/不是目录:scan 返回 -1,调用方据此提示并停留; * N4 上溯父目录:mr_path_parent 可一路走到文件系统 "/"(不再以 TF 卡根为界); * N5 单目录条目上限:达到 MR_LIB_SCAN_MAX 后截断并置 truncated(行合成给出提示行)。 * * 编译(在 tuku/ 下): * gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \ * test/test_browse_nav.c logic/library.c logic/reader.c test/settings_reader_stub.c \ * -pthread -o build/host/test_browse_nav */ #include "logic/library.h" #include #include #include #include #include #include static int g_pass, g_fail; #define CHECK(cond, ...) do { \ if (cond) { g_pass++; } \ else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \ } while (0) static int mkdir_p(const char *path) { char tmp[MR_PATH_MAX]; snprintf(tmp, sizeof tmp, "%s", path); for (size_t i = 1; i <= strlen(tmp); i++) { if (tmp[i] == '/' || tmp[i] == '\0') { char ch = tmp[i]; tmp[i] = '\0'; if (mkdir(tmp, 0755) != 0) { struct stat st; if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1; } tmp[i] = ch; } } return 0; } static int touch_file(const char *path) { FILE *f = fopen(path, "wb"); if (!f) return -1; fputs("x", f); return fclose(f); } static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) return -1; if (!S_ISDIR(st.st_mode)) { chmod(path, 0700); return unlink(path); } chmod(path, 0700); DIR *d = opendir(path); if (!d) return -1; struct dirent *de; while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; char full[MR_PATH_MAX]; snprintf(full, sizeof full, "%s/%s", path, de->d_name); rm_rf(full); } closedir(d); return rmdir(path); } static void joinp(char *out, int outsz, const char *a, const char *b) { snprintf(out, (size_t)outsz, "%s/%s", a, b); } static const mr_library_entry *find_entry(const mr_library *lib, const char *name) { for (int i = 0; i < lib->count; i++) if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i]; return NULL; } /* ---- N1/N2 ---- */ static void test_open_access(const char *root) { mr_library lib; memset(&lib, 0, sizeof lib); int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH); CHECK(n == 7, "root lists 7 dirs (got %d)", n); CHECK(lib.truncated == 0, "root not truncated"); static const char *want[7] = { "book", "empty", "lost+found", "MeowAPPs", "MeowData", "MeowOS", "noimg" }; for (int i = 0; i < 7 && i < lib.count; i++) CHECK(strcmp(lib.items[i].name, want[i]) == 0, "sort[%d]=%s got %s", i, want[i], lib.items[i].name); for (int i = 0; i < 7; i++) CHECK(find_entry(&lib, want[i]) != NULL, "system/empty/noimg dir listed: %s", want[i]); CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded"); CHECK(find_entry(&lib, "linkdir") == NULL, "symlink dir excluded"); CHECK(find_entry(&lib, "loose.png") == NULL, "file excluded"); /* 可进入:每个列出的目录都能被 scan(0 项也算成功,不等于 -1)。 */ char p[MR_PATH_MAX]; for (int i = 0; i < lib.count; i++) { mr_library child; memset(&child, 0, sizeof child); CHECK(mr_library_scan(&child, lib.items[i].path, MR_LIB_MAX_DEPTH) >= 0, "enter %s", lib.items[i].name); mr_library_free(&child); } joinp(p, sizeof p, root, "MeowOS"); mr_library syslib; memset(&syslib, 0, sizeof syslib); CHECK(mr_library_scan(&syslib, p, MR_LIB_MAX_DEPTH) == 1 && find_entry(&syslib, "sub") != NULL, "system dir browsable"); mr_library_free(&syslib); joinp(p, sizeof p, root, "empty"); mr_library empty; memset(&empty, 0, sizeof empty); CHECK(mr_library_scan(&empty, p, MR_LIB_MAX_DEPTH) == 0, "empty dir enterable (0 items)"); mr_library_free(&empty); mr_library_free(&lib); } /* ---- N3 ---- */ static void test_unreadable(const char *root) { mr_library lib; char p[MR_PATH_MAX]; memset(&lib, 0, sizeof lib); CHECK(mr_library_scan(&lib, "/no/such/dir/m13", MR_LIB_MAX_DEPTH) == -1, "missing -> -1"); CHECK(lib.count == 0 && lib.truncated == 0, "failed scan leaves empty lib"); mr_library_free(&lib); joinp(p, sizeof p, root, "loose.png"); memset(&lib, 0, sizeof lib); CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "file (not dir) -> -1"); mr_library_free(&lib); if (geteuid() != 0) { /* root 下 EACCES 不生效,跳过权限用例 */ joinp(p, sizeof p, root, "noread"); CHECK(mkdir_p(p) == 0, "mkdir noread"); CHECK(chmod(p, 0000) == 0, "chmod 000"); memset(&lib, 0, sizeof lib); CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "unreadable -> -1"); CHECK(lib.count == 0, "unreadable lib empty"); mr_library_free(&lib); chmod(p, 0700); } } /* ---- N4 ---- */ static void test_up_to_root(void) { char cur[MR_PATH_MAX]; snprintf(cur, sizeof cur, "/usr/data/mnt/sd_0/a/b/c"); int steps = 0; while (strcmp(cur, "/") != 0 && steps < 32) { char up[MR_PATH_MAX]; int r = mr_path_parent(cur, up, (int)sizeof up); CHECK(r == 0, "parent of %s", cur); if (r != 0) break; CHECK(strcmp(up, cur) != 0, "parent strictly shorter at %s", cur); memcpy(cur, up, sizeof cur); steps++; } CHECK(strcmp(cur, "/") == 0, "reached filesystem root ('%s')", cur); CHECK(steps == 7, "steps to / = %d (want 7)", steps); char up[MR_PATH_MAX]; CHECK(mr_path_parent("/", up, (int)sizeof up) == 0 && strcmp(up, "/") == 0, "root parent stays /"); CHECK(mr_path_parent("/usr/data/mnt/sd_0", up, (int)sizeof up) == 0 && strcmp(up, "/usr/data/mnt") == 0, "TF root can go above mount point: %s", up); } /* ---- N5 ---- */ static void test_truncation(void) { CHECK(MR_LIB_SCAN_MAX == 2000, "default cap is 2000 (got %d)", MR_LIB_SCAN_MAX); char tmpl[] = "./mr_nav_XXXXXX"; char *root = mkdtemp(tmpl); CHECK(root != NULL, "mkdtemp"); if (!root) return; int total = MR_LIB_SCAN_MAX + 8; char p[MR_PATH_MAX]; for (int i = 0; i < total; i++) { snprintf(p, sizeof p, "%s/d%04d", root, i); if (mkdir(p, 0755) != 0) { CHECK(0, "mkdir %s", p); break; } } mr_library lib; memset(&lib, 0, sizeof lib); int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH); CHECK(n == MR_LIB_SCAN_MAX, "cap applied: n=%d", n); CHECK(lib.truncated == 1, "truncated flag set"); CHECK(lib.count == MR_LIB_SCAN_MAX, "count == cap"); mr_library_row row; CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1, "truncation row present"); CHECK(strcmp(row.title, "已截断") == 0, "truncation row title=%s", row.title); mr_library_free(&lib); rm_rf(root); } int main(void) { char tmpl[] = "./mr_nav_XXXXXX"; char *root = mkdtemp(tmpl); CHECK(root != NULL, "mkdtemp"); if (!root) return 1; char p[MR_PATH_MAX]; const char *dirs[] = { "MeowAPPs", "MeowData", "MeowOS/sub", "lost+found", "book", "empty", "noimg", ".hidden" }; for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) { joinp(p, sizeof p, root, dirs[i]); CHECK(mkdir_p(p) == 0, "mkdir %s", p); } joinp(p, sizeof p, root, "MeowOS/001.png"); touch_file(p); joinp(p, sizeof p, root, "book/001.png"); touch_file(p); joinp(p, sizeof p, root, "noimg/readme.txt"); touch_file(p); joinp(p, sizeof p, root, "loose.png"); touch_file(p); /* 符号链接目录不参与浏览(有权限才建;建不了不影响其余断言)。 */ char target[MR_PATH_MAX], link[MR_PATH_MAX]; joinp(target, sizeof target, root, "book"); joinp(link, sizeof link, root, "linkdir"); symlink(target, link); test_open_access(root); test_unreadable(root); test_up_to_root(); rm_rf(root); test_truncation(); printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); return g_fail ? 1 : 0; }