用户反馈四件事一起改: - 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路 (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 + .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片= mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。 - 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内 缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期 生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。 2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次 (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。 - 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏 「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。 - 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态 「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。 集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/ 普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比 mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。 reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push, 走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。 回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
903 lines
37 KiB
C
903 lines
37 KiB
C
/* test_cbz.c — M11 档案册(CBZ/zip)集成宿主测试(纯 C + -lz,无 SDK/硬件)。
|
||
*
|
||
* 夹具由 test/make_cbz_samples.py 用 python3 zipfile 现场生成(真实 zip 产物 +
|
||
* 定点字节修补),放在 $TMPDIR 下临时目录,结束后删除。
|
||
*
|
||
* 覆盖:
|
||
* T1 扩展名判据:cbz/zip 大小写不敏感;cbr/gz/无扩展名不算档案。
|
||
* T2 页枚举:只收图片页(目录项/文本/0 字节/加密/超限过滤),跨子目录自然序;
|
||
* 中文条目名按原始 UTF-8 字节透传。
|
||
* T3 路径安全:绝对路径、'..' 段、反斜杠、内嵌隐藏段一律不收;解压字节与
|
||
* manifest 的 CRC/尺寸一致,缓存文件落在 MeowData/<pkg>/cache。
|
||
* T4 meta:PNG/BMP/JPEG 原始尺寸经「解压到内存只读头」探测(翻页/条漫排版共用)。
|
||
* T5 解压缓存纪律:64 页 LRU 淘汰最旧、命中不重写(inode 不变)、32MiB 字节上限、
|
||
* 启动清孤儿(只删 mrpage_*,不动缩略图 *.bmp)、缓存不可用时优雅失败。
|
||
* T6 坏档容错:非 zip/截断/无图片页/声明 96MiB 的 zip 炸弹 → 打开失败或 0 页,不崩。
|
||
* T7 统一开册契约:目录册与档案册走同一个 mr_book(count/meta/open/close/destroy),
|
||
* 越界 open 返回 NULL,close_auto 幂等。
|
||
* T8 书库列档(M18 文件管理器模型):目录 + .cbz/.zip 同列(目录优先、组内自然序,
|
||
* bookA < box < … < Vol10.cbz);扫描期一次成行,副标题是「文件夹/档案册 · 大小」,
|
||
* 不再探测页数/封面/续读角标;坏档/无图片页档案也照常列出(点开时才由 reader 判)。
|
||
* T9 册间衔接:同父目录自然序混排(bookA → vol2.cbz → Vol10.cbz),
|
||
* 首/末册返回 0;不注入探测回调时档案册不参与(M7 语义)。
|
||
* T10 封面解压 + 无续读角标:档案首图仍可解到缓存;书库行不再带「· 续读 Pn」。
|
||
*
|
||
* 编译(在 tuku/ 下):
|
||
* gcc -Wall -Wextra -Werror -O1 -I. test/test_cbz.c logic/library.c logic/reader.c \
|
||
* logic/reader/page_source.c logic/reader/archive_source.c \
|
||
* logic/reader/page_cache.c logic/zip/zip.c test/settings_reader_stub.c \
|
||
* -lz -o test/test_cbz && ./test/test_cbz
|
||
* 或 test/run_cbz_tests.sh。
|
||
*/
|
||
#define _POSIX_C_SOURCE 200809L
|
||
|
||
#include "logic/library.h"
|
||
#include "logic/reader.h"
|
||
#include "logic/reader/page_cache.h"
|
||
#include "logic/reader/page_source.h"
|
||
#include "test/settings_reader_stub.h"
|
||
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/wait.h>
|
||
#include <unistd.h>
|
||
#include <zlib.h>
|
||
|
||
#define PATH_CAP 1024
|
||
#define CJK_PAGE "\xe4\xb8\xad\xe6\x96\x87\xe5\x90\x8d.png" /* 中文名.png */
|
||
#define HUGE_PAGE_LEN (33 * 1024 * 1024) /* 与 make_cbz_samples.py 的 HUGE_PAGE_LEN 一致 */
|
||
|
||
static int g_pass;
|
||
static int g_fail;
|
||
static char g_root[PATH_CAP]; /* 夹具根 */
|
||
static char g_lib[PATH_CAP]; /* <root>/library */
|
||
static char g_feat[PATH_CAP]; /* <root>/features */
|
||
static char g_data[PATH_CAP]; /* <root>/app/os.meow.picbrowser/data */
|
||
static const char *g_argv0;
|
||
|
||
#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 void joinp(char *out, size_t cap, const char *a, const char *b)
|
||
{
|
||
snprintf(out, cap, "%s/%s", a, b);
|
||
}
|
||
|
||
static int rm_rf(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) != 0) return -1;
|
||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||
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[PATH_CAP];
|
||
joinp(full, sizeof full, path, de->d_name);
|
||
rm_rf(full);
|
||
}
|
||
closedir(d);
|
||
return rmdir(path);
|
||
}
|
||
|
||
static int make_temp_dir(void)
|
||
{
|
||
const char *bases[3];
|
||
int nb = 0;
|
||
const char *env = getenv("TMPDIR");
|
||
if (env && env[0]) bases[nb++] = env;
|
||
bases[nb++] = "/data/data/com.termux/files/usr/tmp";
|
||
bases[nb++] = "/tmp";
|
||
|
||
for (int i = 0; i < nb; i++) {
|
||
char tmpl[PATH_CAP];
|
||
snprintf(tmpl, sizeof tmpl, "%s/tuku_cbz_XXXXXX", bases[i]);
|
||
if (mkdtemp(tmpl)) {
|
||
snprintf(g_root, sizeof g_root, "%s", tmpl);
|
||
return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static const char *find_generator(char *buf, size_t cap)
|
||
{
|
||
static const char *fallbacks[] = {
|
||
"test/make_cbz_samples.py", "make_cbz_samples.py",
|
||
};
|
||
const char *a0 = g_argv0 ? g_argv0 : "";
|
||
const char *slash = strrchr(a0, '/');
|
||
if (a0[0] && strcmp(a0, "-") != 0 && slash) {
|
||
snprintf(buf, cap, "%.*s/make_cbz_samples.py", (int)(slash - a0), a0);
|
||
if (access(buf, R_OK) == 0) return buf;
|
||
}
|
||
for (size_t i = 0; i < sizeof fallbacks / sizeof fallbacks[0]; i++)
|
||
if (access(fallbacks[i], R_OK) == 0) return fallbacks[i];
|
||
return fallbacks[0];
|
||
}
|
||
|
||
static int run_generator(const char *gen)
|
||
{
|
||
char cmd[PATH_CAP * 2 + 32];
|
||
snprintf(cmd, sizeof cmd, "python3 '%s' '%s'", gen, g_root);
|
||
int st = system(cmd);
|
||
return st != -1 && WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||
}
|
||
|
||
static int file_crc(const char *path, unsigned long *crc, long *size)
|
||
{
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return 0;
|
||
unsigned char buf[8192];
|
||
uLong c = crc32(0L, Z_NULL, 0);
|
||
long total = 0;
|
||
size_t n;
|
||
while ((n = fread(buf, 1, sizeof buf, f)) > 0) {
|
||
c = crc32(c, buf, (uInt)n);
|
||
total += (long)n;
|
||
}
|
||
fclose(f);
|
||
if (crc) *crc = (unsigned long)c;
|
||
if (size) *size = total;
|
||
return 1;
|
||
}
|
||
|
||
/* manifest.txt 行:<档案相对路径>\t<条目名>\t<crc32>\t<size> */
|
||
static int manifest_lookup(const char *rel, const char *entry,
|
||
unsigned long *crc, long *size)
|
||
{
|
||
char path[PATH_CAP];
|
||
joinp(path, sizeof path, g_root, "manifest.txt");
|
||
FILE *f = fopen(path, "r");
|
||
if (!f) return 0;
|
||
char line[PATH_CAP * 2];
|
||
char m_rel[PATH_CAP], m_ent[512];
|
||
unsigned long m_crc;
|
||
long m_size;
|
||
int found = 0;
|
||
while (fgets(line, sizeof line, f) != NULL) {
|
||
if (sscanf(line, "%1023[^\t]\t%511[^\t]\t%lx\t%ld",
|
||
m_rel, m_ent, &m_crc, &m_size) != 4)
|
||
continue;
|
||
if (strcmp(m_rel, rel) == 0 && strcmp(m_ent, entry) == 0) {
|
||
if (crc) *crc = m_crc;
|
||
if (size) *size = m_size;
|
||
found = 1;
|
||
break;
|
||
}
|
||
}
|
||
fclose(f);
|
||
return found;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 假解码回调(契约边界:logic 不链 SDK) */
|
||
/* ============================================================ */
|
||
static int g_dummy;
|
||
static int g_load_n;
|
||
static int g_unload_n;
|
||
static char g_load_path[MR_CACHE_FILE_MAX];
|
||
|
||
static meow_img *fake_load(const char *path, void *ud, int maxdim)
|
||
{
|
||
(void)ud;
|
||
(void)maxdim;
|
||
g_load_n++;
|
||
snprintf(g_load_path, sizeof g_load_path, "%s", path ? path : "");
|
||
return (meow_img *)&g_dummy;
|
||
}
|
||
|
||
static void fake_unload(meow_img *img)
|
||
{
|
||
if (img) g_unload_n++;
|
||
}
|
||
|
||
static int archive_pages_probe(const char *path, void *ud)
|
||
{
|
||
(void)ud;
|
||
return mr_zip_count_pages(path);
|
||
}
|
||
|
||
static int count_cache_files(void)
|
||
{
|
||
const char *dir = mr_cache_dir();
|
||
if (!dir || !dir[0]) return -1;
|
||
DIR *d = opendir(dir);
|
||
if (!d) return -1;
|
||
int n = 0;
|
||
size_t plen = strlen(MR_CACHE_PREFIX);
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) != NULL) {
|
||
if (strncmp(de->d_name, MR_CACHE_PREFIX, plen) == 0) n++;
|
||
}
|
||
closedir(d);
|
||
return n;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T1 扩展名 */
|
||
/* ============================================================ */
|
||
static void test_ext(void)
|
||
{
|
||
CHECK(mr_path_is_archive("a.cbz") == 1, "cbz");
|
||
CHECK(mr_path_is_archive("A.CBZ") == 1, "CBZ");
|
||
CHECK(mr_path_is_archive("x.zip") == 1, "zip");
|
||
CHECK(mr_path_is_archive("X.ZiP") == 1, "ZiP");
|
||
CHECK(mr_path_is_archive("a.cbr") == 0, "cbr not archive");
|
||
CHECK(mr_path_is_archive("a.gz") == 0, "gz not archive");
|
||
CHECK(mr_path_is_archive("cbz") == 0, "no extension");
|
||
CHECK(mr_path_is_archive(".cbz") == 0, "dot name");
|
||
CHECK(mr_path_is_archive(NULL) == 0, "NULL safe");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T2 页枚举 / 排序 / 名字透传 */
|
||
/* ============================================================ */
|
||
static void test_enum(void)
|
||
{
|
||
char path[PATH_CAP];
|
||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||
|
||
mr_zip_book zb;
|
||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 3, "vol2 pages=%d (want 3)", n);
|
||
CHECK(zb.count == 3, "zb.count=%d", zb.count);
|
||
if (n != 3) {
|
||
mr_zip_book_free(&zb);
|
||
return;
|
||
}
|
||
|
||
char name[512];
|
||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||
CHECK(strcmp(name, "ch01/p2.png") == 0, "page0=%s", name);
|
||
mr_zip_book_page_name(&zb, 1, name, sizeof name);
|
||
CHECK(strcmp(name, "ch01/p10.png") == 0, "page1=%s", name);
|
||
mr_zip_book_page_name(&zb, 2, name, sizeof name);
|
||
CHECK(strcmp(name, CJK_PAGE) == 0, "page2=%s", name);
|
||
|
||
mr_zip_book_page_name(&zb, -1, name, sizeof name);
|
||
CHECK(name[0] == '\0', "page_name(-1) not empty");
|
||
mr_zip_book_page_name(&zb, 3, name, sizeof name);
|
||
CHECK(name[0] == '\0', "page_name(count) not empty");
|
||
mr_zip_book_page_name(&zb, 0, NULL, 10);
|
||
mr_zip_book_page_name(&zb, 0, name, 0);
|
||
|
||
/* 目录项/文本/0 字节条目都被过滤(count==3 已证明)。 */
|
||
|
||
mr_book b;
|
||
memset(&b, 0, sizeof b);
|
||
mr_zip_book_bind(&b, NULL);
|
||
CHECK(!mr_book_valid(&b), "bind NULL must leave book invalid");
|
||
CHECK(mr_book_count(&b) == 0 && mr_book_open(&b, 0) == NULL, "invalid book safe");
|
||
|
||
mr_zip_book_bind(&b, &zb);
|
||
CHECK(mr_book_valid(&b), "valid after bind");
|
||
CHECK(mr_book_count(&b) == 3, "book count=%d", mr_book_count(&b));
|
||
CHECK(mr_book_open(&b, 3) == NULL, "open(count) != NULL");
|
||
CHECK(mr_book_open(&b, -1) == NULL, "open(-1) != NULL");
|
||
|
||
int before = g_load_n;
|
||
meow_img *img = mr_book_open(&b, 0);
|
||
CHECK(img != NULL, "open(0) returned NULL");
|
||
CHECK(g_load_n == before + 1, "load callback not called");
|
||
CHECK(strncmp(g_load_path, mr_cache_dir(), strlen(mr_cache_dir())) == 0,
|
||
"load path not in cache dir: %s", g_load_path);
|
||
CHECK(access(g_load_path, R_OK) == 0, "extracted file missing: %s", g_load_path);
|
||
CHECK(strstr(g_load_path, ".png") != NULL, "extension not preserved: %s", g_load_path);
|
||
mr_book_close(&b, img);
|
||
CHECK(g_unload_n >= 1, "unload not called");
|
||
mr_book_close(&b, NULL); /* 容忍 NULL */
|
||
|
||
mr_book_destroy(&b); /* 走 destroy→zip_free */
|
||
CHECK(!mr_book_valid(&b), "destroy left book valid");
|
||
mr_zip_book_free(&zb); /* 幂等 */
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T3 路径安全 + 解压内容 */
|
||
/* ============================================================ */
|
||
static void test_pathsafe(void)
|
||
{
|
||
char path[PATH_CAP];
|
||
joinp(path, sizeof path, g_feat, "pathsafe.cbz");
|
||
|
||
mr_zip_book zb;
|
||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 2, "pathsafe pages=%d (want 2: ok1.png, sub/ok2.jpg)", n);
|
||
if (n != 2) {
|
||
mr_zip_book_free(&zb);
|
||
return;
|
||
}
|
||
|
||
char name[512];
|
||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||
CHECK(strcmp(name, "ok1.png") == 0, "page0=%s", name);
|
||
mr_zip_book_page_name(&zb, 1, name, sizeof name);
|
||
CHECK(strcmp(name, "sub/ok2.jpg") == 0, "page1=%s", name);
|
||
|
||
const char *bad[] = { "../evil.png", "/abs.png", "a/../../b.png", "sub/../ok3.png",
|
||
".hidden/x.png", "._mac.png", "sub\\win.png" };
|
||
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
|
||
CHECK(mr_zip_entry_is_page(bad[i], (unsigned)strlen(bad[i])) == 0,
|
||
"unsafe entry accepted: %s", bad[i]);
|
||
}
|
||
|
||
for (int i = 0; i < n; i++) {
|
||
char out[MR_CACHE_FILE_MAX];
|
||
CHECK(mr_zip_book_page_path(&zb, i, out, (int)sizeof out) == 0,
|
||
"page_path(%d) failed", i);
|
||
if (!out[0]) continue;
|
||
CHECK(strncmp(out, mr_cache_dir(), strlen(mr_cache_dir())) == 0,
|
||
"cache path escaped dir: %s", out);
|
||
mr_zip_book_page_name(&zb, i, name, sizeof name);
|
||
unsigned long crc = 0;
|
||
long size = 0;
|
||
CHECK(manifest_lookup("features/pathsafe.cbz", name, &crc, &size),
|
||
"manifest miss %s", name);
|
||
unsigned long got_crc = 0;
|
||
long got_size = 0;
|
||
CHECK(file_crc(out, &got_crc, &got_size), "crc read failed: %s", out);
|
||
CHECK(got_crc == crc && got_size == size,
|
||
"extracted bytes differ for %s (crc %lx/%lx size %ld/%ld)",
|
||
name, got_crc, crc, got_size, size);
|
||
}
|
||
mr_zip_book_free(&zb);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T4 meta(原始像素尺寸) */
|
||
/* ============================================================ */
|
||
static void test_meta(void)
|
||
{
|
||
char path[PATH_CAP];
|
||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||
|
||
mr_zip_book zb;
|
||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 3,
|
||
"vol2 init for meta");
|
||
mr_book b;
|
||
mr_zip_book_bind(&b, &zb);
|
||
|
||
int w = 0, h = 0;
|
||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 41 && h == 31,
|
||
"meta0=%dx%d (want 41x31)", w, h);
|
||
CHECK(mr_book_meta(&b, 1, &w, &h) == 1 && w == 40 && h == 30,
|
||
"meta1=%dx%d (want 40x30)", w, h);
|
||
CHECK(mr_book_meta(&b, 2, &w, &h) == 1 && w == 12 && h == 34,
|
||
"meta2=%dx%d (want 12x34)", w, h);
|
||
CHECK(mr_book_meta(&b, 3, &w, &h) == 0 && w == 0 && h == 0, "meta(count)");
|
||
mr_book_destroy(&b);
|
||
|
||
joinp(path, sizeof path, g_lib, "Vol10.cbz");
|
||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 1,
|
||
"Vol10 init");
|
||
mr_zip_book_bind(&b, &zb);
|
||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 7 && h == 9,
|
||
"bmp meta=%dx%d (want 7x9)", w, h);
|
||
/* P2-1:BMP 页的缓存文件名也是 *.bmp,但必须能被缩略图 LRU 判据识别为解压页 */
|
||
char vp[MR_CACHE_FILE_MAX];
|
||
CHECK(mr_zip_book_page_path(&zb, 0, vp, (int)sizeof vp) == 0, "bmp page path");
|
||
const char *vbase = strrchr(vp, '/');
|
||
CHECK(vbase && mr_cache_is_page_name(vbase + 1) == 1,
|
||
"bmp page cache looks like a thumbnail: %s", vp);
|
||
CHECK(vbase && strstr(vbase, ".bmp") != NULL, "bmp page lost its extension: %s", vp);
|
||
mr_book_destroy(&b);
|
||
|
||
joinp(path, sizeof path, g_lib, "box/box2.cbz");
|
||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 1,
|
||
"box2 init");
|
||
mr_zip_book_bind(&b, &zb);
|
||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 12 && h == 34,
|
||
"jpeg meta=%dx%d (want 12x34)", w, h);
|
||
mr_book_destroy(&b);
|
||
|
||
/* 内存探测边界 */
|
||
CHECK(mr_probe_size_mem(NULL, 100, &w, &h) == 0, "probe NULL");
|
||
CHECK(mr_probe_size_mem("ABCDEFGH", 8, &w, &h) == 0, "probe junk");
|
||
|
||
/* meta 只解压到内存读头,不落解压缓存(box1 从未被 open 过)。 */
|
||
joinp(path, sizeof path, g_lib, "box/box1.cbz");
|
||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 2,
|
||
"box1 init");
|
||
mr_zip_book_bind(&b, &zb);
|
||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 8 && h == 8,
|
||
"box1 meta=%dx%d (want 8x8)", w, h);
|
||
struct stat st;
|
||
char pname[256], cpath[MR_CACHE_FILE_MAX];
|
||
CHECK(stat(path, &st) == 0, "stat box1");
|
||
mr_zip_book_page_name(&zb, 0, pname, (int)sizeof pname);
|
||
CHECK(mr_cache_path(path, (long long)st.st_mtime, (long long)st.st_size,
|
||
pname, 0, cpath, (int)sizeof cpath) == 0, "meta cache path");
|
||
CHECK(access(cpath, F_OK) != 0, "meta wrote the page cache");
|
||
mr_book_destroy(&b);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T5 解压缓存纪律 */
|
||
/* ============================================================ */
|
||
static void test_cache(void)
|
||
{
|
||
char path[PATH_CAP];
|
||
char data_dir[PATH_CAP];
|
||
char cache_copy[MR_CACHE_DIR_MAX];
|
||
|
||
/* 解压页缓存与缩略图的文件名判据(P2-1):缩略图 LRU 依赖它跳过 mrpage_*。 */
|
||
CHECK(mr_cache_is_page_name("mrpage_0123456789abcdef.bmp") == 1, "page name bmp");
|
||
CHECK(mr_cache_is_page_name("mrpage_0123456789abcdef.png") == 1, "page name png");
|
||
CHECK(mr_cache_is_page_name("0123456789abcdef_1700000000_1234.bmp") == 0, "thumb name");
|
||
CHECK(mr_cache_is_page_name("thumb.bmp") == 0, "plain thumb");
|
||
CHECK(mr_cache_is_page_name("") == 0 && mr_cache_is_page_name(NULL) == 0,
|
||
"page name edges");
|
||
|
||
joinp(path, sizeof path, g_feat, "big.cbz");
|
||
|
||
/* ---- (a) 64 页 LRU ---- */
|
||
joinp(data_dir, sizeof data_dir, g_root, "c1/os.meow.picbrowser/data");
|
||
CHECK(mr_cache_init(data_dir) == 0, "cache init c1");
|
||
snprintf(cache_copy, sizeof cache_copy, "%s", mr_cache_dir());
|
||
CHECK(cache_copy[0] && strstr(cache_copy, "/cache") != NULL,
|
||
"cache dir=%s", cache_copy);
|
||
|
||
mr_zip_book zb;
|
||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 80, "big pages=%d (want 80)", n);
|
||
char p[MR_CACHE_FILE_MAX];
|
||
for (int i = 0; i < n; i++)
|
||
CHECK(mr_zip_book_page_path(&zb, i, p, (int)sizeof p) == 0,
|
||
"big page %d path", i);
|
||
CHECK(mr_cache_count() == MR_CACHE_MAX_PAGES,
|
||
"cache count=%d (want 64)", mr_cache_count());
|
||
CHECK(mr_cache_bytes() <= MR_CACHE_MAX_BYTES, "cache bytes over cap");
|
||
CHECK(count_cache_files() == mr_cache_count(),
|
||
"disk files=%d index=%d", count_cache_files(), mr_cache_count());
|
||
|
||
/* 最旧的 16 页被淘汰,最新的仍在 */
|
||
struct stat st;
|
||
char arc[PATH_CAP];
|
||
char name[512];
|
||
snprintf(arc, sizeof arc, "%s", path);
|
||
CHECK(stat(arc, &st) == 0, "stat big.cbz");
|
||
char p0[MR_CACHE_FILE_MAX], p79[MR_CACHE_FILE_MAX];
|
||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||
CHECK(mr_cache_path(arc, (long long)st.st_mtime, (long long)st.st_size,
|
||
name, 0, p0, (int)sizeof p0) == 0, "cache_path p0");
|
||
mr_zip_book_page_name(&zb, 79, name, sizeof name);
|
||
CHECK(mr_cache_path(arc, (long long)st.st_mtime, (long long)st.st_size,
|
||
name, 79, p79, (int)sizeof p79) == 0, "cache_path p79");
|
||
CHECK(access(p0, F_OK) != 0, "LRU kept the oldest page");
|
||
CHECK(access(p79, F_OK) == 0, "LRU dropped the newest page");
|
||
|
||
/* 命中不重写:同一 inode */
|
||
struct stat s1, s2;
|
||
CHECK(stat(p79, &s1) == 0, "stat p79");
|
||
CHECK(mr_zip_book_page_path(&zb, 79, p, (int)sizeof p) == 0, "page79 re-get");
|
||
CHECK(stat(p, &s2) == 0 && s1.st_ino == s2.st_ino, "cache hit rewrote the file");
|
||
|
||
/* 假命中自愈(P2-1):外部删掉缓存文件后再取同页,应重新解压而不是返回死路径 */
|
||
unlink(p79);
|
||
CHECK(access(p79, F_OK) != 0, "unlink p79");
|
||
CHECK(mr_zip_book_page_path(&zb, 79, p, (int)sizeof p) == 0, "page79 after unlink");
|
||
CHECK(access(p79, F_OK) == 0, "page79 not re-extracted after external unlink");
|
||
CHECK(mr_cache_count() == MR_CACHE_MAX_PAGES, "count after self-heal=%d",
|
||
mr_cache_count());
|
||
mr_zip_book_free(&zb);
|
||
|
||
/* ---- (b) 32MiB 字节上限 + 启动清孤儿 ---- */
|
||
joinp(data_dir, sizeof data_dir, g_root, "c2/os.meow.picbrowser/data");
|
||
mr_cache_shutdown();
|
||
CHECK(mr_cache_init(data_dir) == 0, "cache init c2");
|
||
snprintf(cache_copy, sizeof cache_copy, "%s", mr_cache_dir());
|
||
|
||
char orphan[MR_CACHE_FILE_MAX], keep[MR_CACHE_FILE_MAX];
|
||
snprintf(orphan, sizeof orphan, "%s/%sdeadbeef.png", cache_copy, MR_CACHE_PREFIX);
|
||
snprintf(keep, sizeof keep, "%s/thumb.bmp", cache_copy);
|
||
FILE *f = fopen(orphan, "wb");
|
||
CHECK(f != NULL, "create orphan");
|
||
if (f) { fputs("orphan", f); fclose(f); }
|
||
f = fopen(keep, "wb");
|
||
CHECK(f != NULL, "create thumb");
|
||
if (f) { fputs("thumb", f); fclose(f); }
|
||
|
||
mr_cache_shutdown();
|
||
CHECK(mr_cache_init(data_dir) == 0, "cache re-init c2");
|
||
CHECK(access(orphan, F_OK) != 0, "startup did not clean orphan mrpage_*");
|
||
CHECK(access(keep, F_OK) == 0, "startup deleted non-cache file");
|
||
|
||
size_t big_len = 20u * 1024u * 1024u;
|
||
unsigned char *big = (unsigned char *)malloc(big_len);
|
||
CHECK(big != NULL, "malloc 20MiB");
|
||
if (big) {
|
||
memset(big, 0xA5, big_len);
|
||
char b1[MR_CACHE_FILE_MAX], b2[MR_CACHE_FILE_MAX];
|
||
snprintf(b1, sizeof b1, "%s/%sbig1.img", cache_copy, MR_CACHE_PREFIX);
|
||
snprintf(b2, sizeof b2, "%s/%sbig2.img", cache_copy, MR_CACHE_PREFIX);
|
||
mr_cache_shutdown();
|
||
mr_cache_init(data_dir);
|
||
CHECK(mr_cache_store(b1, big, big_len) == 1, "store 20MiB #1");
|
||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() == (long long)big_len,
|
||
"count=%d bytes=%lld", mr_cache_count(), mr_cache_bytes());
|
||
CHECK(mr_cache_store(b2, big, big_len) == 1, "store 20MiB #2");
|
||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() == (long long)big_len,
|
||
"byte cap: count=%d bytes=%lld", mr_cache_count(), mr_cache_bytes());
|
||
CHECK(access(b1, F_OK) != 0, "byte cap did not evict the oldest");
|
||
free(big);
|
||
}
|
||
unlink(keep);
|
||
|
||
/* ---- (c) 单页 > 32MiB:文档化的一次性溢出(大页可读,其它条目按预算淘汰) ---- */
|
||
joinp(data_dir, sizeof data_dir, g_root, "c3/os.meow.picbrowser/data");
|
||
mr_cache_shutdown();
|
||
CHECK(mr_cache_init(data_dir) == 0, "cache init c3");
|
||
|
||
joinp(arc, sizeof arc, g_lib, "box/box1.cbz");
|
||
mr_zip_book small;
|
||
CHECK(mr_zip_book_init(&small, arc, 720, fake_load, fake_unload, NULL) == 2,
|
||
"small book init");
|
||
char sp[MR_CACHE_FILE_MAX];
|
||
CHECK(mr_zip_book_page_path(&small, 0, sp, (int)sizeof sp) == 0, "small page");
|
||
CHECK(mr_cache_count() == 1, "small cache count=%d", mr_cache_count());
|
||
mr_zip_book_free(&small);
|
||
|
||
joinp(arc, sizeof arc, g_feat, "hugepage.cbz");
|
||
mr_zip_book huge;
|
||
CHECK(mr_zip_book_init(&huge, arc, 720, fake_load, fake_unload, NULL) == 1,
|
||
"hugepage init");
|
||
char hp[MR_CACHE_FILE_MAX];
|
||
CHECK(mr_zip_book_page_path(&huge, 0, hp, (int)sizeof hp) == 0, "huge page path");
|
||
CHECK(mr_cache_count() == 1, "overflow count=%d", mr_cache_count());
|
||
CHECK(mr_cache_bytes() == (long long)HUGE_PAGE_LEN,
|
||
"overflow bytes=%lld (want %d)", mr_cache_bytes(), HUGE_PAGE_LEN);
|
||
CHECK(mr_cache_bytes() > MR_CACHE_MAX_BYTES, "huge page should exceed the budget");
|
||
CHECK(access(sp, F_OK) != 0, "huge page should evict the small page");
|
||
CHECK(access(hp, F_OK) == 0, "huge page file missing");
|
||
CHECK(count_cache_files() == 1, "overflow left extra cache files");
|
||
|
||
mr_book hb;
|
||
mr_zip_book_bind(&hb, &huge);
|
||
int before_load = g_load_n;
|
||
meow_img *him = mr_book_open(&hb, 0);
|
||
CHECK(him != NULL && g_load_n == before_load + 1, "huge page not readable");
|
||
mr_book_close(&hb, him);
|
||
mr_book_destroy(&hb);
|
||
|
||
/* 再来一个小页:整体预算仍然生效,大页被淘汰 */
|
||
joinp(arc, sizeof arc, g_lib, "box/box1.cbz");
|
||
mr_zip_book small2;
|
||
CHECK(mr_zip_book_init(&small2, arc, 720, fake_load, fake_unload, NULL) == 2,
|
||
"small2 init");
|
||
CHECK(mr_zip_book_page_path(&small2, 0, sp, (int)sizeof sp) == 0, "small2 page");
|
||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() < MR_CACHE_MAX_BYTES,
|
||
"budget not restored: count=%d bytes=%lld", mr_cache_count(),
|
||
mr_cache_bytes());
|
||
CHECK(access(hp, F_OK) != 0, "huge page should be evicted by the budget");
|
||
mr_zip_book_free(&small2);
|
||
|
||
/* ---- (d) 缓存不可用时优雅失败(不崩、不写文件) ---- */
|
||
mr_cache_shutdown();
|
||
CHECK(mr_cache_ready() == 0, "shutdown left cache ready");
|
||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||
mr_zip_book zb2;
|
||
CHECK(mr_zip_book_init(&zb2, path, 720, fake_load, fake_unload, NULL) == 3,
|
||
"init without cache");
|
||
CHECK(mr_zip_book_page_path(&zb2, 0, p, (int)sizeof p) == -1,
|
||
"page_path without cache should fail");
|
||
int before = g_load_n;
|
||
mr_book b;
|
||
mr_zip_book_bind(&b, &zb2);
|
||
CHECK(mr_book_open(&b, 0) == NULL, "open without cache should fail");
|
||
CHECK(g_load_n == before, "decode callback called without cache");
|
||
mr_book_destroy(&b);
|
||
|
||
joinp(data_dir, sizeof data_dir, g_root, "app/os.meow.picbrowser/data");
|
||
CHECK(mr_cache_init(data_dir) == 0, "cache init app");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T6 坏档容错 */
|
||
/* ============================================================ */
|
||
static void expect_bad(const char *path, const char *tag)
|
||
{
|
||
mr_book_handle h;
|
||
mr_book b;
|
||
int n = mr_book_open_auto(&b, &h, path, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == -1, "%s: open_auto=%d (want -1)", tag, n);
|
||
CHECK(!mr_book_valid(&b), "%s: book valid after failure", tag);
|
||
CHECK(mr_book_open(&b, 0) == NULL, "%s: open after failure", tag);
|
||
mr_book_close_auto(&b, &h);
|
||
mr_book_close_auto(&b, &h); /* 幂等 */
|
||
CHECK(mr_zip_count_pages(path) == 0, "%s: count_pages != 0", tag);
|
||
}
|
||
|
||
static void test_bad(void)
|
||
{
|
||
char p[PATH_CAP];
|
||
|
||
joinp(p, sizeof p, g_lib, "broken.cbz");
|
||
expect_bad(p, "broken");
|
||
joinp(p, sizeof p, g_lib, "truncated.cbz");
|
||
expect_bad(p, "truncated");
|
||
joinp(p, sizeof p, g_feat, "truncated.cbz");
|
||
expect_bad(p, "truncated(feat)");
|
||
|
||
joinp(p, sizeof p, g_lib, "missing.cbz");
|
||
expect_bad(p, "missing");
|
||
|
||
mr_book_handle h;
|
||
mr_book b;
|
||
CHECK(mr_book_open_auto(&b, &h, NULL, 720, fake_load, fake_unload, NULL) == -1,
|
||
"open_auto(NULL)");
|
||
CHECK(mr_book_open_auto(NULL, &h, p, 720, fake_load, fake_unload, NULL) == -1,
|
||
"open_auto(NULL book)");
|
||
CHECK(mr_book_open_auto(&b, NULL, p, 720, fake_load, fake_unload, NULL) == -1,
|
||
"open_auto(NULL handle)");
|
||
|
||
/* 合法 zip 但没有图片页 / 声明尺寸超上限(zip 炸弹) */
|
||
const char *zero[] = { "notabook.zip", "emptypages.cbz" };
|
||
for (size_t i = 0; i < sizeof zero / sizeof zero[0]; i++) {
|
||
joinp(p, sizeof p, g_lib, zero[i]);
|
||
int n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 0, "%s: open_auto=%d (want 0)", zero[i], n);
|
||
CHECK(!mr_book_valid(&b), "%s: book bound with 0 pages", zero[i]);
|
||
CHECK(mr_book_open(&b, 0) == NULL, "%s: open on empty book", zero[i]);
|
||
mr_book_close_auto(&b, &h);
|
||
CHECK(mr_zip_count_pages(p) == 0, "%s: count_pages != 0", zero[i]);
|
||
}
|
||
|
||
joinp(p, sizeof p, g_feat, "bomb.cbz");
|
||
CHECK(mr_zip_count_pages(p) == 0, "bomb.cbz should have no page (96MiB)");
|
||
CHECK(mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL) == 0,
|
||
"bomb: open_auto should be 0 pages");
|
||
mr_book_close_auto(&b, &h);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T7 统一开册契约 */
|
||
/* ============================================================ */
|
||
static void test_open_auto(void)
|
||
{
|
||
char p[PATH_CAP];
|
||
joinp(p, sizeof p, g_lib, "bookA");
|
||
|
||
mr_book_handle h;
|
||
mr_book b;
|
||
int n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 2 && h.kind == MR_BOOK_KIND_DIR, "dir open n=%d kind=%d", n, h.kind);
|
||
CHECK(mr_book_valid(&b) && mr_book_count(&b) == 2, "dir book contract");
|
||
int before = g_load_n;
|
||
meow_img *img = mr_book_open(&b, 1);
|
||
CHECK(img != NULL && g_load_n == before + 1, "dir open(1)");
|
||
CHECK(strstr(g_load_path, "002.png") != NULL, "dir page path=%s", g_load_path);
|
||
mr_book_close(&b, img);
|
||
int mw = 0, mh = 0;
|
||
CHECK(mr_book_meta(&b, 0, &mw, &mh) == 1 && mw == 30 && mh == 20,
|
||
"dir meta=%dx%d (want 30x20)", mw, mh);
|
||
mr_book_close_auto(&b, &h);
|
||
CHECK(h.kind == MR_BOOK_KIND_NONE && !mr_book_valid(&b), "dir close_auto");
|
||
mr_book_close_auto(&b, &h); /* 幂等 */
|
||
|
||
joinp(p, sizeof p, g_lib, "vol2.cbz");
|
||
n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 3 && h.kind == MR_BOOK_KIND_ZIP, "zip open n=%d kind=%d", n, h.kind);
|
||
CHECK(mr_book_valid(&b) && mr_book_count(&b) == 3, "zip book contract");
|
||
CHECK(mr_book_open(&b, 99) == NULL, "zip out of range");
|
||
mr_book_close_auto(&b, &h);
|
||
CHECK(!mr_book_valid(&b), "zip close_auto");
|
||
|
||
joinp(p, sizeof p, g_lib, "emptypages.cbz");
|
||
n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||
CHECK(n == 0 && h.kind == MR_BOOK_KIND_ZIP, "empty zip n=%d kind=%d", n, h.kind);
|
||
CHECK(!mr_book_valid(&b), "empty zip bound");
|
||
mr_book_close_auto(&b, &h);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T8 书库列档 */
|
||
/* ============================================================ */
|
||
static const mr_library_entry *lib_find(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;
|
||
}
|
||
|
||
static void test_library(void)
|
||
{
|
||
mr_test_cfg_clear();
|
||
|
||
/* 文件管理器模型:目录 + 档案册同列(目录优先、组内自然序),扫描期一次成行;
|
||
* 档案册只按扩展名成行,不探测页数/封面(打开时才由 reader 解档)。 */
|
||
mr_library lib;
|
||
memset(&lib, 0, sizeof lib);
|
||
int n = mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH);
|
||
CHECK(n == 8, "library rows=%d (want 2 dirs + 6 archives)", n);
|
||
static const char *want[8] = {
|
||
"bookA", "box", "broken.cbz", "emptypages.cbz",
|
||
"notabook.zip", "truncated.cbz", "vol2.cbz", "Vol10.cbz"
|
||
};
|
||
for (int i = 0; i < 8 && i < lib.count; i++) {
|
||
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
||
"row[%d]=%s (want %s)", i, lib.items[i].name, want[i]);
|
||
}
|
||
for (int i = 0; i < 2 && i < lib.count; i++)
|
||
CHECK(lib.items[i].kind == MR_LIB_KIND_DIR &&
|
||
strcmp(lib.items[i].subtitle, "文件夹") == 0,
|
||
"%s dir row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
|
||
for (int i = 2; i < 8 && i < lib.count; i++)
|
||
CHECK(lib.items[i].kind == MR_LIB_KIND_ARCHIVE &&
|
||
strncmp(lib.items[i].subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
|
||
"%s archive row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
|
||
|
||
const mr_library_entry *e = lib_find(&lib, "bookA");
|
||
CHECK(e && e->kind == MR_LIB_KIND_DIR && e->size == 0, "bookA dir entry");
|
||
|
||
e = lib_find(&lib, "vol2.cbz");
|
||
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE && e->size > 0 &&
|
||
strstr(e->path, "vol2.cbz") != NULL,
|
||
"vol2.cbz archive entry (size=%lld)", e ? e->size : -1);
|
||
|
||
/* 坏档/无图片页档案也照常列出(书库不预判,点开时 reader 才判可读性)。 */
|
||
static const char *all_arch[] = { "notabook.zip", "emptypages.cbz",
|
||
"broken.cbz", "truncated.cbz" };
|
||
for (unsigned i = 0; i < sizeof all_arch / sizeof all_arch[0]; i++) {
|
||
e = lib_find(&lib, all_arch[i]);
|
||
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE, "%s listed as archive", all_arch[i]);
|
||
}
|
||
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz not listed");
|
||
mr_library_free(&lib);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T9 册间衔接 */
|
||
/* ============================================================ */
|
||
static void expect_adj(const char *from, int delta, const char *want, const char *tag)
|
||
{
|
||
char out[MR_PATH_MAX];
|
||
int r = mr_book_adjacent_ex(from, delta, out, (int)sizeof out,
|
||
archive_pages_probe, NULL);
|
||
if (!want) {
|
||
CHECK(r == 0 && out[0] == '\0', "%s: r=%d out=%s", tag, r, out);
|
||
} else {
|
||
CHECK(r == 1 && strcmp(out, want) == 0, "%s: got %s (want %s)", tag, out, want);
|
||
}
|
||
}
|
||
|
||
static void test_adjacent(void)
|
||
{
|
||
char bookA[MR_PATH_MAX], vol2[MR_PATH_MAX], vol10[MR_PATH_MAX];
|
||
char broken[MR_PATH_MAX];
|
||
joinp(bookA, sizeof bookA, g_lib, "bookA");
|
||
joinp(vol2, sizeof vol2, g_lib, "vol2.cbz");
|
||
joinp(vol10, sizeof vol10, g_lib, "Vol10.cbz");
|
||
joinp(broken, sizeof broken, g_lib, "broken.cbz");
|
||
|
||
/* 兄弟表 = bookA + vol2.cbz + Vol10.cbz(box 无直接图、坏档不算书)。 */
|
||
expect_adj(bookA, 1, vol2, "bookA next");
|
||
expect_adj(vol2, 1, vol10, "vol2 next");
|
||
expect_adj(vol10, 1, NULL, "vol10 next (end)");
|
||
expect_adj(bookA, -1, NULL, "bookA prev (start)");
|
||
expect_adj(vol2, -1, bookA, "vol2 prev");
|
||
expect_adj(vol10, -1, vol2, "vol10 prev");
|
||
expect_adj(broken, 1, NULL, "broken not a book");
|
||
expect_adj(broken, -1, NULL, "broken not a book prev");
|
||
|
||
/* 不注入探测回调:档案册不参与(M7 语义) */
|
||
char out[MR_PATH_MAX];
|
||
CHECK(mr_book_adjacent(vol2, 1, out, (int)sizeof out) == 0,
|
||
"legacy adjacency should ignore archives (r=%d)", 0);
|
||
CHECK(mr_book_adjacent(bookA, 1, out, (int)sizeof out) == 0,
|
||
"legacy adjacency bookA next should be none");
|
||
|
||
/* 参数边界 */
|
||
CHECK(mr_book_adjacent_ex(vol2, 0, out, (int)sizeof out, archive_pages_probe,
|
||
NULL) == -1, "delta 0");
|
||
CHECK(mr_book_adjacent_ex(NULL, 1, out, (int)sizeof out, archive_pages_probe,
|
||
NULL) == -1, "NULL dir");
|
||
CHECK(mr_book_adjacent_ex(vol2, 1, NULL, 32, archive_pages_probe, NULL) == -1,
|
||
"NULL out");
|
||
CHECK(mr_book_adjacent_ex("/", 1, out, (int)sizeof out, archive_pages_probe,
|
||
NULL) == -1, "root");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* T10 封面解压 + 无续读角标 */
|
||
/* ============================================================ */
|
||
static void test_badge_cover(void)
|
||
{
|
||
char vol2[MR_PATH_MAX];
|
||
joinp(vol2, sizeof vol2, g_lib, "vol2.cbz");
|
||
|
||
mr_test_cfg_clear();
|
||
mr_cont_save(vol2, 2, MR_MODE_WEBTOON);
|
||
|
||
/* 文件管理器模型:书库行只有「档案册 · 大小」,不再有「· 续读 Pn」角标
|
||
* (续读仍由 reader 打开该册时恢复,见 test_mode_switch_host)。 */
|
||
mr_library lib;
|
||
memset(&lib, 0, sizeof lib);
|
||
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 8, "rescan rows");
|
||
const mr_library_entry *e = lib_find(&lib, "vol2.cbz");
|
||
CHECK(e && strstr(e->subtitle, "续读") == NULL,
|
||
"no continuation badge in file-manager rows: %s", e ? e->subtitle : "");
|
||
CHECK(e && strstr(e->subtitle, "档案册") != NULL, "archive subtitle=%s",
|
||
e ? e->subtitle : "");
|
||
e = lib_find(&lib, "Vol10.cbz");
|
||
CHECK(e && strstr(e->subtitle, "续读") == NULL, "Vol10 no badge either");
|
||
mr_library_free(&lib);
|
||
|
||
/* 封面 = 档内自然序首图(ch01/p2.png),解压到缓存 */
|
||
char cover[MR_CACHE_FILE_MAX];
|
||
CHECK(mr_zip_cover_path(vol2, cover, (int)sizeof cover) == 1, "cover path");
|
||
CHECK(cover[0] && access(cover, R_OK) == 0, "cover file missing: %s", cover);
|
||
unsigned long crc = 0;
|
||
long size = 0;
|
||
CHECK(file_crc(cover, &crc, &size), "cover crc");
|
||
unsigned long want_crc = 0;
|
||
long want_size = 0;
|
||
CHECK(manifest_lookup("library/vol2.cbz", "ch01/p2.png", &want_crc, &want_size),
|
||
"cover manifest");
|
||
CHECK(crc == want_crc && size == want_size, "cover bytes differ");
|
||
|
||
char broken[MR_PATH_MAX];
|
||
joinp(broken, sizeof broken, g_lib, "broken.cbz");
|
||
CHECK(mr_zip_cover_path(broken, cover, (int)sizeof cover) == 0, "broken cover");
|
||
CHECK(mr_zip_cover_path(vol2, NULL, 16) == 0, "cover NULL out");
|
||
}
|
||
|
||
/* ============================================================ */
|
||
int main(int argc, char **argv)
|
||
{
|
||
g_argv0 = argv[0];
|
||
char gen_buf[PATH_CAP];
|
||
const char *gen = argc > 1 ? argv[1] : find_generator(gen_buf, sizeof gen_buf);
|
||
|
||
if (!make_temp_dir()) {
|
||
perror("mkdtemp");
|
||
return 2;
|
||
}
|
||
joinp(g_lib, sizeof g_lib, g_root, "library");
|
||
joinp(g_feat, sizeof g_feat, g_root, "features");
|
||
joinp(g_data, sizeof g_data, g_root, "app/os.meow.picbrowser/data");
|
||
|
||
if (!run_generator(gen)) {
|
||
printf("generator failed: %s\n", gen);
|
||
rm_rf(g_root);
|
||
return 2;
|
||
}
|
||
if (mr_cache_init(g_data) != 0) {
|
||
printf("cache init failed: %s\n", g_data);
|
||
rm_rf(g_root);
|
||
return 2;
|
||
}
|
||
|
||
test_ext();
|
||
test_enum();
|
||
test_pathsafe();
|
||
test_meta();
|
||
test_cache();
|
||
test_bad();
|
||
test_open_auto();
|
||
test_library();
|
||
test_adjacent();
|
||
test_badge_cover();
|
||
|
||
mr_cache_shutdown();
|
||
rm_rf(g_root);
|
||
|
||
printf("cbz tests: %d passed, %d failed\n", g_pass, g_fail);
|
||
return g_fail == 0 ? 0 : 1;
|
||
}
|