Files
MeowOS-SDK/tuku/test/test_browse_nav.c
T
AskaEth 9f34417c49 图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)
rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
2026-11-05 12:23:31 +08:00

260 lines
8.7 KiB
C

/* 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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
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;
}