feat(tuku): CBZ 档案册集成——档案页源/书库列册/解压缓存
- logic/reader/archive_source.c:档案册页源实现 M6 mr_book 契约 (mz_open/mz_count/mz_entry_at/mz_read_alloc),条目自然序 + 图片后缀 + 路径安全(`..`/绝对路径/反斜杠/隐藏段/内嵌 NUL)+ 单页尺寸上限过滤; meta 按需解压只读图片头(不落缓存,结果按页记忆);mr_book_open_auto 统一目录册/档案册开册,翻页与条漫两套消费代码零改动。 - logic/reader/page_cache.c/h:解压页落 MeowData/<pkg>/cache,LRU 64 页或 32MiB,启动清 mrpage_* 孤儿,写入 .part + rename;与缩略图缓存同目录、 前缀隔离;互斥量保护,解码/缩略图线程可并发。 - logic/library.c/h:扫描直接子文件里的 .cbz/.zip 成册(行=文件名+页数, 点入直接阅读);档案探测由调用方注入(logic 不依赖 zip 容器),不注入时 与 M7 行为逐字一致;册间衔接兄弟表=含图目录∪含页档案册,同列自然序。 条目构造/副标题/角标/兄弟判据拆成小函数,便于 M13 书库重写拼接。 - ui/library:档案册行 + 封面缩略图(解压档内首图)、扫描注入探测、缓存 目录统一由 page_cache 推导(一份规则)。 - ui/reader:open_book 走 mr_book_open_auto;跨册衔接纳入档案册。 - build_tuku.sh 补 zip/archive/cache 源与 sysroot zlib 头(zip.c include <zlib.h>);run_tests.sh 纳入 zip+cbz 用例;run_webtoon_tests.sh 的宿主 集成构建补新源与 -lz;.gitignore 同步测试产物。 - test/test_cbz.c(267 项断言)+ make_cbz_samples.py 真实 cbz 夹具: 列档/自然序/解压字节 CRC/路径安全/坏档与 zip 炸弹容错/LRU 与 32MiB 上限/ 启动清孤儿/续读角标/封面首图。
This commit is contained in:
1 parent
19620a7957
commit
562cc44931
16 files changed
+2330
-108
No files matched your search
+146
-43
@@ -12,6 +12,14 @@
|
||||
* - 续读角标:含图册的副标题后缀「· 续读 Pn」(读 logic/reader 的每册 last page);
|
||||
* - 册间衔接:mr_book_adjacent 给出同父目录下按书库自然序的相邻「含图册」,
|
||||
* 供阅读器末页前进/首页后退跨册;mr_library_dir_has_images 是共享的「是不是一本书」判据。
|
||||
*
|
||||
* M11 追加(档案册,仍然不改目录扫描的主体语义):
|
||||
* - 目标目录的**直接子文件**里 .cbz/.zip 也成行(行=文件名+档内页数,has_images=1);
|
||||
* - 档案册探测由调用方注入(mr_lib_archive_fn),logic 层不依赖 zip 容器;
|
||||
* 不注入时所有 *_ex 入口与 M7 行为逐字一致;
|
||||
* - 册间衔接的兄弟表从「含图目录」扩成「含图目录 ∪ 含图片页的档案册」,同列自然序。
|
||||
* 小函数边界(add_dir_entry / add_archive_entry / entry_subtitle / entry_badge /
|
||||
* sibling_is_book)刻意留给 M13 的书库重写复用。
|
||||
*/
|
||||
#include "library.h"
|
||||
#include "logic/reader.h" /* mr_cont_badge:书库行续读角标(M7) */
|
||||
@@ -66,6 +74,15 @@ int mr_path_is_image(const char *name)
|
||||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
|
||||
}
|
||||
|
||||
int mr_path_is_archive(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 0;
|
||||
}
|
||||
|
||||
int mr_natural_cmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
@@ -105,12 +122,22 @@ int mr_natural_cmp(const char *a, const char *b)
|
||||
typedef struct scan_info {
|
||||
int pages; /* 直接图片数 */
|
||||
int subdirs; /* 含图子目录数 */
|
||||
int nav; /* 1=本目录最终含图(可直接看或可导航到图) */
|
||||
int archives; /* 直接档案册数(.cbz/.zip 且探测到图片页) */
|
||||
int nav; /* 1=本目录最终含书(可直接看或可导航到书) */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源 */
|
||||
char first_name[MR_NAME_MAX]; /* 本目录自然序第一张图片的文件名(比较用) */
|
||||
} scan_info;
|
||||
|
||||
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si)
|
||||
/* 档案册探测(UI 注入;NULL=纯目录书库,行为与 M7 一致)。 */
|
||||
static int archive_pages_at(mr_lib_archive_fn probe, void *ud, const char *path)
|
||||
{
|
||||
if (!probe || !path || !path[0]) return 0;
|
||||
int n = probe(path, ud);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
memset(si, 0, sizeof *si);
|
||||
DIR *d = opendir(path);
|
||||
@@ -132,21 +159,25 @@ static void scan_dir(const char *path, int depth, int max_depth, scan_info *si)
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (depth >= max_depth) continue;
|
||||
scan_info child;
|
||||
scan_dir(full, depth + 1, max_depth, &child);
|
||||
scan_dir(full, depth + 1, max_depth, &child, probe, ud);
|
||||
if (!child.nav) continue;
|
||||
si->subdirs++;
|
||||
if (!si->cover[0] && child.cover[0])
|
||||
copy_str(si->cover, (int)sizeof si->cover, child.cover);
|
||||
} else if (S_ISREG(st.st_mode) && mr_path_is_image(name)) {
|
||||
si->pages++;
|
||||
if (!si->first_name[0] || mr_natural_cmp(name, si->first_name) < 0) {
|
||||
copy_str(si->first_name, (int)sizeof si->first_name, name);
|
||||
copy_str(si->cover, (int)sizeof si->cover, full);
|
||||
} else if (S_ISREG(st.st_mode)) {
|
||||
if (mr_path_is_image(name)) {
|
||||
si->pages++;
|
||||
if (!si->first_name[0] || mr_natural_cmp(name, si->first_name) < 0) {
|
||||
copy_str(si->first_name, (int)sizeof si->first_name, name);
|
||||
copy_str(si->cover, (int)sizeof si->cover, full);
|
||||
}
|
||||
} else if (mr_path_is_archive(name)) {
|
||||
if (archive_pages_at(probe, ud, full) > 0) si->archives++;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
si->nav = (si->pages > 0 || si->subdirs > 0);
|
||||
si->nav = (si->pages > 0 || si->subdirs > 0 || si->archives > 0);
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
@@ -179,7 +210,69 @@ static int entry_cmp(const void *pa, const void *pb)
|
||||
return mr_natural_cmp(a->name, b->name);
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
/* 副标题(M11 扩展):直接含图 → pages;只有档案册 → books;否则 folders。 */
|
||||
static void entry_subtitle(mr_library_entry *e, int archive_books)
|
||||
{
|
||||
if (e->has_images)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d page%s", e->pages,
|
||||
e->pages == 1 ? "" : "s");
|
||||
else if (archive_books > 0)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d book%s", archive_books,
|
||||
archive_books == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d folder%s", e->subdirs,
|
||||
e->subdirs == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
|
||||
* 档案册以档案路径为册标识,同一套逻辑复用。
|
||||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||||
static void entry_badge(mr_library_entry *e)
|
||||
{
|
||||
if (!e->has_images) return;
|
||||
char badge[24];
|
||||
if (!mr_cont_badge(e->path, badge, (int)sizeof badge)) return;
|
||||
int used = (int)strlen(e->subtitle);
|
||||
if (used < (int)sizeof e->subtitle - 2)
|
||||
snprintf(e->subtitle + used, sizeof e->subtitle - (size_t)used, " · %s", badge);
|
||||
}
|
||||
|
||||
/* 文件夹册(含可导航中间目录)一行。 */
|
||||
static int add_dir_entry(mr_library *lib, const char *name, const char *full,
|
||||
const scan_info *si)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
copy_str(e.cover, (int)sizeof e.cover, si->cover);
|
||||
e.pages = si->pages;
|
||||
e.subdirs = si->subdirs;
|
||||
e.has_images = (si->pages > 0);
|
||||
entry_subtitle(&e, si->archives);
|
||||
entry_badge(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
/* 档案册(.cbz/.zip)一行:文件即一本书,点入直接阅读。
|
||||
* cover 留空——缩略图由 UI 解压档内首图(mr_zip_cover_path)生成。 */
|
||||
static int add_archive_entry(mr_library *lib, const char *name, const char *full,
|
||||
int pages)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
e.pages = pages;
|
||||
e.subdirs = 0;
|
||||
e.has_images = 1;
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", pages, pages == 1 ? "" : "s");
|
||||
entry_badge(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
@@ -201,38 +294,22 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
scan_info si;
|
||||
scan_dir(full, 1, max_depth, &si);
|
||||
if (!si.nav) continue;
|
||||
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) continue;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) continue;
|
||||
copy_str(e.cover, (int)sizeof e.cover, si.cover);
|
||||
e.pages = si.pages;
|
||||
e.subdirs = si.subdirs;
|
||||
e.has_images = (si.pages > 0);
|
||||
if (e.has_images)
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", e.pages,
|
||||
e.pages == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d folder%s", e.subdirs,
|
||||
e.subdirs == 1 ? "" : "s");
|
||||
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
|
||||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||||
if (e.has_images) {
|
||||
char badge[24];
|
||||
if (mr_cont_badge(e.path, badge, (int)sizeof badge)) {
|
||||
int used = (int)strlen(e.subtitle);
|
||||
if (used < (int)sizeof e.subtitle - 2)
|
||||
snprintf(e.subtitle + used, sizeof e.subtitle - (size_t)used,
|
||||
" · %s", badge);
|
||||
}
|
||||
int rc = 0;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
scan_info si;
|
||||
scan_dir(full, 1, max_depth, &si, archive_pages, ud);
|
||||
if (!si.nav) continue;
|
||||
rc = add_dir_entry(lib, name, full, &si);
|
||||
} else if (S_ISREG(st.st_mode) && mr_path_is_archive(name)) {
|
||||
int pages = archive_pages_at(archive_pages, ud, full);
|
||||
if (pages <= 0) continue; /* 坏档/无图片页:不当书 */
|
||||
rc = add_archive_entry(lib, name, full, pages);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (lib_add(lib, &e) != 0) {
|
||||
if (rc != 0) {
|
||||
closedir(d);
|
||||
mr_library_free(lib);
|
||||
return -1;
|
||||
@@ -244,6 +321,11 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
return mr_library_scan_ex(lib, dir, max_depth, NULL, NULL);
|
||||
}
|
||||
|
||||
int mr_path_parent(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
@@ -295,7 +377,23 @@ static int name_ptr_cmp(const void *pa, const void *pb)
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
|
||||
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
|
||||
static int sibling_is_book(const char *full, const struct stat *st,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
|
||||
if (S_ISREG(st->st_mode)) {
|
||||
const char *slash = strrchr(full, '/');
|
||||
const char *name = slash ? slash + 1 : full;
|
||||
if (!mr_path_is_archive(name)) return 0;
|
||||
return archive_pages_at(probe, ud, full) > 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
@@ -329,8 +427,8 @@ int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||||
if (!mr_library_dir_has_images(full)) continue; /* 只算「一本书」,中间目录跳过 */
|
||||
if (lstat(full, &st) != 0 || S_ISLNK(st.st_mode)) continue;
|
||||
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
|
||||
|
||||
if (n >= cap) {
|
||||
int nc = cap ? cap * 2 : 16;
|
||||
@@ -360,3 +458,8 @@ int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
free(names);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
{
|
||||
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
|
||||
}
|
||||
+42
-11
@@ -4,10 +4,12 @@
|
||||
* (M7 起另提供每册续读角标与册间顺序查询;续读状态本身由 logic/reader.c 持久化,
|
||||
* 本层只读它来拼副标题,宿主测试用配置存根顶替。)
|
||||
* 书库语义:
|
||||
* - 从 TF 卡根(MR_ROOT_DIR)起列「直接子目录」;
|
||||
* - 只保留自身含 PNG/JPEG/BMP 的目录,或子树中最终含图的中间目录(可继续导航);
|
||||
* - 行的 pages=该目录直接图片数;subdirs=其下含图子目录数(中间目录用于副标题);
|
||||
* - cover=缩略图源文件:优先该目录自然序第一张图,否则取第一个含图子目录的 cover;
|
||||
* - 从 TF 卡根(MR_ROOT_DIR)起列「直接子目录」;M11 起另列直接子文件里的
|
||||
* .cbz/.zip(档案册即一本书,与文件夹册同列自然序混排);
|
||||
* - 只保留自身含 PNG/JPEG/BMP 的目录,或子树中最终含书的中间目录(可继续导航);
|
||||
* - 行的 pages=该目录直接图片数(档案册=档内图片页数);subdirs=其下含图子目录数;
|
||||
* - cover=缩略图源文件:优先该目录自然序第一张图,否则取第一个含图子目录的 cover
|
||||
* (档案册留空,UI 解压档内首图);
|
||||
* - 条目按名称自然排序(page2 < page10),含前导零与数字/字母混合名。
|
||||
*
|
||||
* 深度上限 MR_LIB_MAX_DEPTH 用于防止坏卡/超大树的无限递归;隐藏目录与系统目录
|
||||
@@ -25,13 +27,13 @@ extern "C" {
|
||||
#define MR_LIB_MAX_DEPTH 8
|
||||
|
||||
typedef struct mr_library_entry {
|
||||
char name[MR_NAME_MAX]; /* 目录名(行主标题) */
|
||||
char name[MR_NAME_MAX]; /* 目录名/档案文件名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径(导航/进 reader 用) */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源图(空=none) */
|
||||
char subtitle[48]; /* 预先格式化好的 "N pages" / "N folders" */
|
||||
int pages; /* 本目录直接图片数 */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源图(空=none;档案册由 UI 解压首图) */
|
||||
char subtitle[48]; /* 预先格式化好的 "N pages" / "N books" / "N folders" */
|
||||
int pages; /* 本目录直接图片数(档案册=档内图片页数) */
|
||||
int subdirs; /* 本目录下含图子目录数 */
|
||||
int has_images; /* 1=本目录直接含图(点入进 reader) */
|
||||
int has_images; /* 1=可直接阅读(直接含图 或 档案册) */
|
||||
} mr_library_entry;
|
||||
|
||||
typedef struct mr_library {
|
||||
@@ -43,6 +45,10 @@ typedef struct mr_library {
|
||||
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
|
||||
int mr_path_is_image(const char *name);
|
||||
|
||||
/* 档案册扩展名判断:cbz / zip(大小写不敏感)。1=档案册。
|
||||
* 注意这只是路径判据;能不能当书读还要看档案里有没有图片页(探测回调)。 */
|
||||
int mr_path_is_archive(const char *name);
|
||||
|
||||
/* 自然排序比较:数字段按数值比(page2 < page10),数字相等时前导零少的在前;
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序。 */
|
||||
int mr_natural_cmp(const char *a, const char *b);
|
||||
@@ -52,6 +58,23 @@ int mr_natural_cmp(const char *a, const char *b);
|
||||
* max_depth<=0 或超过 MR_LIB_MAX_DEPTH 时按上限处理。 */
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz/.zip 当一本书,与文件夹册同列 */
|
||||
/* ------------------------------------------------------------
|
||||
* logic 层不依赖 zip 容器(保持宿主裸测/链接面干净):档案册探测由 UI 注入。
|
||||
* archive_pages(path, ud) 对普通文件返回图片页数(>0=是一本书),0=不是/坏档。
|
||||
* 传 NULL 时行为与 mr_library_scan 完全一致(纯目录书库),M7 语义不变。
|
||||
*
|
||||
* 档案册行的口径:name=文件名(含扩展名)、path=档案文件路径、pages=档内页数、
|
||||
* has_images=1(点入直接阅读)、cover 留空(缩略图由 UI 解压首图生成)、
|
||||
* 副标题 "N pages"、续读角标同文件夹册(按档案路径记)。
|
||||
* 只含档案册的子目录算「可导航」(nav),副标题写 "N books"。
|
||||
* ============================================================ */
|
||||
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
|
||||
|
||||
int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
/* 释放条目数组并清零(可重复调用)。 */
|
||||
void mr_library_free(mr_library *lib);
|
||||
|
||||
@@ -63,12 +86,20 @@ int mr_path_parent(const char *path, char *out, int outsz);
|
||||
* 1=有;0=没有/不可读。 */
|
||||
int mr_library_dir_has_images(const char *dir);
|
||||
|
||||
/* 册间衔接(M7):同父目录下按书库自然序排列的「直接含图」兄弟册。
|
||||
/* 册间衔接(M7):同父目录下按书库自然序排列的「可直接阅读」兄弟册。
|
||||
* delta=+1 下一册 / -1 上一册。1=找到并写入 out 完整路径;
|
||||
* 0=没有相邻册(当前册是首/末册,或当前册不在兄弟表中);-1=参数非法。
|
||||
* 语义与书库一致:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。 */
|
||||
* 语义与书库一致:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
|
||||
* 只算文件夹册(直接含图目录)——M7 口径不变。 */
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz);
|
||||
|
||||
/* 册间衔接 + 档案册(M11):兄弟表 = 「直接含图目录」∪「含图片页的 .cbz/.zip」,
|
||||
* 两者同列按名称自然序混排(mission 基线:跨册衔接按同父目录自然序)。
|
||||
* 因此末页前进可以从文件夹册进到同目录的档案册,反之亦然。
|
||||
* archive_pages 传 NULL 时与 mr_book_adjacent 完全等价。 */
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
/* logic/reader/archive_source.c — 档案册(CBZ/zip 当一本书):页源契约的第二种实现。
|
||||
*
|
||||
* 契约、条目过滤与缓存纪律见 page_source.h / page_cache.h。实现要点:
|
||||
* - 页表在 init 时一次建好(mz_open 只读 central directory;只看名字,不解压);
|
||||
* - open(i) 才解压单条目:mz_read_alloc → page_cache 落盘 → 注入的解码回调按路径解码。
|
||||
* 因此翻页/条漫两套消费代码与目录册完全一致(meta 走原始像素尺寸,open 走解码);
|
||||
* - meta(i) 按需解压该页到内存只读图片头(不落缓存),结果缓存在 meta_w/meta_h;
|
||||
* - 单个 mr_zip_book 的 mz_read* 由自带的互斥量串行化(解码线程与主线程 meta 会碰面)。
|
||||
*
|
||||
* 线程:count/page_name 只读页表,可任意线程;open 可由解码线程调用;meta 按 M6
|
||||
* 用法在主线程调用(条漫排版/当前页几何)。两个 mr_zip_book 实例互不影响,
|
||||
* 解压缓存(page_cache)是进程级共享的。
|
||||
*/
|
||||
#include "logic/reader/page_source.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
#include "logic/zip/zip.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条目过滤:图片后缀 + 路径安全 + 非隐藏 + 非目录 */
|
||||
/* ============================================================ */
|
||||
static int has_control(const char *name, unsigned len)
|
||||
{
|
||||
for (unsigned i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)name[i];
|
||||
if (c < 0x20 || c == 0x7F) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 逐段检查:拒绝 ".." 段与隐藏段("._resource" 之类不参与成册)。 */
|
||||
static int components_safe(const char *name)
|
||||
{
|
||||
const char *p = name;
|
||||
while (*p) {
|
||||
const char *end = strchr(p, '/');
|
||||
size_t n = end ? (size_t)(end - p) : strlen(p);
|
||||
if (n == 2 && p[0] == '.' && p[1] == '.') return 0;
|
||||
if (n > 0 && p[0] == '.') return 0;
|
||||
if (!end) break;
|
||||
p = end + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_zip_entry_is_page(const char *name, unsigned name_len)
|
||||
{
|
||||
if (!name || name_len == 0 || name_len >= MR_ZIP_MAX_ENTRY_NAME) return 0;
|
||||
if (strlen(name) != name_len) return 0; /* 内嵌 NUL:拒绝 */
|
||||
if (name[0] == '/' || name[0] == '\\') return 0; /* 绝对路径 */
|
||||
if (name_len >= 2 && name[1] == ':') return 0; /* Windows 盘符 */
|
||||
if (strchr(name, '\\')) return 0; /* 反斜杠分隔符:一律拒绝 */
|
||||
if (has_control(name, name_len)) return 0;
|
||||
if (name[name_len - 1] == '/') return 0; /* 目录条目 */
|
||||
if (!components_safe(name)) return 0;
|
||||
return mr_path_is_image(name);
|
||||
}
|
||||
|
||||
/* 页表排序:跨子目录按完整条目名自然序(中文按 UTF-8 字节序,数字段按数值)。 */
|
||||
typedef struct page_ref {
|
||||
const char *name;
|
||||
int eidx;
|
||||
} page_ref;
|
||||
|
||||
static int page_ref_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const page_ref *a = (const page_ref *)pa;
|
||||
const page_ref *b = (const page_ref *)pb;
|
||||
int c = mr_natural_cmp(a->name, b->name);
|
||||
if (c != 0) return c;
|
||||
return a->eidx < b->eidx ? -1 : (a->eidx > b->eidx ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
static int entry_is_page(const mz_entry *e)
|
||||
{
|
||||
if (!e || mz_entry_is_dir(e)) return 0;
|
||||
if (e->flags & 0x0001u) return 0; /* 加密条目:读不了,不列 */
|
||||
if (!mr_zip_entry_is_page(e->name, e->name_len)) return 0;
|
||||
if (e->uncomp_size == 0) return 0;
|
||||
if (e->uncomp_size > (uint64_t)MR_ZIP_PAGE_MAX_BYTES) return 0; /* zip 炸弹 */
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mr_zip_book_free(mr_zip_book *zb)
|
||||
{
|
||||
if (!zb) return;
|
||||
if (zb->arch) {
|
||||
mz_close((mz_archive *)zb->arch);
|
||||
zb->arch = NULL;
|
||||
}
|
||||
if (zb->lock) {
|
||||
pthread_mutex_destroy((pthread_mutex_t *)zb->lock);
|
||||
free(zb->lock);
|
||||
zb->lock = NULL;
|
||||
}
|
||||
free(zb->order);
|
||||
zb->order = NULL;
|
||||
free(zb->meta_w);
|
||||
zb->meta_w = NULL;
|
||||
free(zb->meta_h);
|
||||
zb->meta_h = NULL;
|
||||
zb->count = 0;
|
||||
}
|
||||
|
||||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (!zb) return -1;
|
||||
memset(zb, 0, sizeof *zb);
|
||||
if (!path || !path[0]) return -1;
|
||||
if (strlen(path) >= sizeof zb->path) return -1;
|
||||
if (maxdim <= 0) maxdim = 720;
|
||||
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return -1;
|
||||
|
||||
pthread_mutex_t *lk = (pthread_mutex_t *)malloc(sizeof *lk);
|
||||
if (!lk) {
|
||||
mz_close(z);
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_init(lk, NULL);
|
||||
|
||||
snprintf(zb->path, sizeof zb->path, "%s", path);
|
||||
zb->arch = z;
|
||||
zb->lock = lk;
|
||||
zb->maxdim = maxdim;
|
||||
zb->load = load;
|
||||
zb->unload = unload;
|
||||
zb->ud = ud;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
zb->mtime = (long long)st.st_mtime;
|
||||
zb->size = (long long)st.st_size;
|
||||
}
|
||||
|
||||
int n = mz_count(z);
|
||||
page_ref *refs = NULL;
|
||||
int nref = 0, cap = 0;
|
||||
for (int i = 0; i < n && nref < MR_BOOK_MAX_PAGES; i++) {
|
||||
const mz_entry *e = mz_entry_at(z, i);
|
||||
if (!entry_is_page(e)) continue;
|
||||
if (nref >= cap) {
|
||||
int nc = cap ? cap * 2 : 32;
|
||||
page_ref *nr = (page_ref *)realloc(refs, (size_t)nc * sizeof *nr);
|
||||
if (!nr) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
refs = nr;
|
||||
cap = nc;
|
||||
}
|
||||
refs[nref].name = e->name;
|
||||
refs[nref].eidx = i;
|
||||
nref++;
|
||||
}
|
||||
if (nref > 1) qsort(refs, (size_t)nref, sizeof *refs, page_ref_cmp);
|
||||
if (nref > 0) {
|
||||
zb->order = (int *)malloc((size_t)nref * sizeof *zb->order);
|
||||
if (!zb->order) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < nref; i++) zb->order[i] = refs[i].eidx;
|
||||
}
|
||||
free(refs);
|
||||
zb->count = nref;
|
||||
return nref;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页 → 缓存文件 */
|
||||
/* ============================================================ */
|
||||
static const mz_entry *page_entry(mr_zip_book *zb, int index)
|
||||
{
|
||||
if (!zb || !zb->arch || !zb->order) return NULL;
|
||||
if (index < 0 || index >= zb->count) return NULL;
|
||||
return mz_entry_at((mz_archive *)zb->arch, zb->order[index]);
|
||||
}
|
||||
|
||||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return -1;
|
||||
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_cache_path(zb->path, zb->mtime, zb->size, e->name, index,
|
||||
path, (int)sizeof path) != 0)
|
||||
return -1;
|
||||
if (mr_cache_touch(path)) {
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
int ok = mr_cache_store(path, buf, len);
|
||||
free(buf);
|
||||
if (!ok) return -1;
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
const mz_entry *e = page_entry((mr_zip_book *)zb, index);
|
||||
if (!e) return;
|
||||
snprintf(out, (size_t)outsz, "%s", e->name);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 书库辅助:数页 / 取封面 */
|
||||
/* ============================================================ */
|
||||
int mr_zip_count_pages(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return 0;
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return 0;
|
||||
int n = mz_count(z), pages = 0;
|
||||
for (int i = 0; i < n && pages < MR_BOOK_MAX_PAGES; i++)
|
||||
if (entry_is_page(mz_entry_at(z, i))) pages++;
|
||||
mz_close(z);
|
||||
return pages;
|
||||
}
|
||||
|
||||
int mr_zip_cover_path(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (!path || !path[0]) return 0;
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 0, NULL, NULL, NULL);
|
||||
int ok = 0;
|
||||
if (n > 0) ok = (mr_zip_book_page_path(&zb, 0, out, outsz) == 0);
|
||||
mr_zip_book_free(&zb);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(档案册) */
|
||||
/* ============================================================ */
|
||||
static int zip_ops_count(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
return zb ? zb->count : 0;
|
||||
}
|
||||
|
||||
/* meta 按需解压头探测;结果缓存,每页只解压一次。主线程调用(M6 用法)。 */
|
||||
static int zip_ops_meta(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!zb || index < 0 || index >= zb->count) return 0;
|
||||
|
||||
if (!zb->meta_w) {
|
||||
int *mw = (int *)calloc((size_t)zb->count, sizeof *mw);
|
||||
int *mh = (int *)calloc((size_t)zb->count, sizeof *mh);
|
||||
if (!mw || !mh) {
|
||||
free(mw);
|
||||
free(mh);
|
||||
return 0;
|
||||
}
|
||||
zb->meta_w = mw;
|
||||
zb->meta_h = mh;
|
||||
}
|
||||
if (zb->meta_w[index] > 0 && zb->meta_h[index] > 0) {
|
||||
if (w) *w = zb->meta_w[index];
|
||||
if (h) *h = zb->meta_h[index];
|
||||
return 1;
|
||||
}
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return 0;
|
||||
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
int pw = 0, ph = 0;
|
||||
int known = mr_probe_size_mem(buf, len, &pw, &ph);
|
||||
free(buf);
|
||||
if (!known) return 0;
|
||||
|
||||
zb->meta_w[index] = pw;
|
||||
zb->meta_h[index] = ph;
|
||||
if (w) *w = pw;
|
||||
if (h) *h = ph;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static meow_img *zip_ops_open(void *ctx, int index)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (!zb || !zb->load) return NULL;
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_zip_book_page_path(zb, index, path, (int)sizeof path) != 0) return NULL;
|
||||
return zb->load(path, zb->ud, zb->maxdim);
|
||||
}
|
||||
|
||||
static void zip_ops_close(void *ctx, meow_img *img)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb && zb->unload && img) zb->unload(img);
|
||||
}
|
||||
|
||||
static void zip_ops_destroy(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb) mr_zip_book_free(zb);
|
||||
}
|
||||
|
||||
static const mr_book_ops g_zip_ops = {
|
||||
zip_ops_count, zip_ops_meta, zip_ops_open, zip_ops_close, zip_ops_destroy,
|
||||
};
|
||||
|
||||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb)
|
||||
{
|
||||
if (!b) return;
|
||||
b->ops = zb ? &g_zip_ops : NULL;
|
||||
b->ctx = zb;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 统一开册 */
|
||||
/* ============================================================ */
|
||||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (b) {
|
||||
b->ops = NULL;
|
||||
b->ctx = NULL;
|
||||
}
|
||||
if (!b || !h) return -1;
|
||||
memset(h, 0, sizeof *h);
|
||||
if (!path || !path[0]) return -1;
|
||||
|
||||
int n;
|
||||
/* 目录名也可能以 .cbz 结尾(罕见):只有普通文件才当档案册。 */
|
||||
int is_archive = mr_path_is_archive(path);
|
||||
if (is_archive) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) is_archive = 0;
|
||||
}
|
||||
if (is_archive) {
|
||||
n = mr_zip_book_init(&h->u.zip, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_ZIP;
|
||||
if (n > 0) mr_zip_book_bind(b, &h->u.zip);
|
||||
} else {
|
||||
n = mr_dir_book_init(&h->u.dir, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_DIR;
|
||||
if (n > 0) mr_dir_book_bind(b, &h->u.dir);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void mr_book_close_auto(mr_book *b, mr_book_handle *h)
|
||||
{
|
||||
if (b) mr_book_destroy(b);
|
||||
if (!h) return;
|
||||
if (h->kind == MR_BOOK_KIND_ZIP) mr_zip_book_free(&h->u.zip);
|
||||
else if (h->kind == MR_BOOK_KIND_DIR) mr_dir_book_free(&h->u.dir);
|
||||
memset(h, 0, sizeof *h);
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/* logic/reader/page_cache.c — 解压页磁盘缓存实现,见 page_cache.h 的纪律说明。 */
|
||||
#include "logic/reader/page_cache.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct cache_entry {
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
long long size;
|
||||
unsigned long long used; /* LRU 计数(越大越新) */
|
||||
} cache_entry;
|
||||
|
||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_dir[MR_CACHE_DIR_MAX];
|
||||
static int g_ready;
|
||||
static cache_entry g_idx[MR_CACHE_MAX_PAGES];
|
||||
static int g_n;
|
||||
static long long g_bytes;
|
||||
static unsigned long long g_tick;
|
||||
|
||||
/* ============================================================ */
|
||||
/* 目录推导 */
|
||||
/* ============================================================ */
|
||||
int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz)
|
||||
{
|
||||
if (!data_dir || !data_dir[0] || !out || outsz <= 0) return -1;
|
||||
|
||||
size_t n = strlen(data_dir);
|
||||
while (n > 1 && data_dir[n - 1] == '/') n--;
|
||||
if (n >= (size_t)outsz) return -1;
|
||||
memcpy(out, data_dir, n);
|
||||
out[n] = '\0';
|
||||
|
||||
/* 末段是 "data" 时换成 "cache"(数据目录 → 同级缓存目录)。 */
|
||||
char *slash = strrchr(out, '/');
|
||||
if (slash && strcmp(slash + 1, "data") == 0) *slash = '\0';
|
||||
|
||||
n = strlen(out);
|
||||
if (n == 0) {
|
||||
if (outsz < 7) return -1;
|
||||
memcpy(out, "/cache", 7);
|
||||
return 0;
|
||||
}
|
||||
int w = snprintf(out + n, (size_t)(outsz - (int)n), "/cache");
|
||||
if (w <= 0 || w >= outsz - (int)n) {
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *mr_cache_dir(void)
|
||||
{
|
||||
return g_ready ? g_dir : "";
|
||||
}
|
||||
|
||||
int mr_cache_ready(void)
|
||||
{
|
||||
return g_ready;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 索引(调用方持锁) */
|
||||
/* ============================================================ */
|
||||
static int idx_find_locked(const char *path)
|
||||
{
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (strcmp(g_idx[i].path, path) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void idx_remove_locked(int i)
|
||||
{
|
||||
if (i < 0 || i >= g_n) return;
|
||||
g_bytes -= g_idx[i].size;
|
||||
g_idx[i] = g_idx[g_n - 1];
|
||||
g_n--;
|
||||
}
|
||||
|
||||
static void idx_evict_locked(void)
|
||||
{
|
||||
if (g_n <= 0) return;
|
||||
int oldest = 0;
|
||||
for (int i = 1; i < g_n; i++)
|
||||
if (g_idx[i].used < g_idx[oldest].used) oldest = i;
|
||||
unlink(g_idx[oldest].path);
|
||||
idx_remove_locked(oldest);
|
||||
}
|
||||
|
||||
static void make_room_locked(long long len)
|
||||
{
|
||||
while (g_n > 0 && (g_n >= MR_CACHE_MAX_PAGES ||
|
||||
g_bytes + len > MR_CACHE_MAX_BYTES))
|
||||
idx_evict_locked();
|
||||
}
|
||||
|
||||
static void idx_add_locked(const char *path, long long size)
|
||||
{
|
||||
if (g_n >= MR_CACHE_MAX_PAGES) return;
|
||||
cache_entry *e = &g_idx[g_n++];
|
||||
snprintf(e->path, sizeof e->path, "%s", path);
|
||||
e->size = size;
|
||||
g_tick++;
|
||||
e->used = g_tick;
|
||||
g_bytes += size;
|
||||
}
|
||||
|
||||
/* 上一进程遗留的解压页:索引在内存里,磁盘上的 mrpage_* 全是孤儿。 */
|
||||
static void clean_orphans_locked(void)
|
||||
{
|
||||
DIR *d = opendir(g_dir);
|
||||
if (!d) return;
|
||||
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) continue;
|
||||
char full[MR_CACHE_FILE_MAX];
|
||||
int w = snprintf(full, sizeof full, "%s/%s", g_dir, de->d_name);
|
||||
if (w > 0 && w < (int)sizeof full) unlink(full);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
/* 逐级建目录(数据目录的父级可能还不存在;只建到缓存目录为止)。 */
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_CACHE_DIR_MAX];
|
||||
size_t n = strlen(path);
|
||||
if (n == 0 || n >= sizeof tmp) return -1;
|
||||
memcpy(tmp, path, n + 1);
|
||||
for (size_t i = 1; i <= n; i++) {
|
||||
if (tmp[i] != '/' && tmp[i] != '\0') continue;
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0 && errno != EEXIST) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cache_init(const char *data_dir)
|
||||
{
|
||||
char dir[MR_CACHE_DIR_MAX];
|
||||
if (mr_cache_dir_from_data(data_dir, dir, (int)sizeof dir) != 0) return -1;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (g_ready && strcmp(g_dir, dir) == 0) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_ready = 0;
|
||||
g_n = 0;
|
||||
g_bytes = 0;
|
||||
g_dir[0] = '\0';
|
||||
|
||||
if (mkdir_p(dir) != 0) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||||
clean_orphans_locked();
|
||||
g_tick = 0;
|
||||
g_ready = 1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_cache_shutdown(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_ready = 0;
|
||||
g_n = 0;
|
||||
g_bytes = 0;
|
||||
g_tick = 0;
|
||||
g_dir[0] = '\0';
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 键 / 命中 / 写入 */
|
||||
/* ============================================================ */
|
||||
static unsigned long long hash_bytes(unsigned long long h, const void *p, size_t n)
|
||||
{
|
||||
const unsigned char *b = (const unsigned char *)p;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= (unsigned long long)b[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static unsigned long long cache_hash(const char *archive, long long mtime,
|
||||
long long size, const char *entry, int page)
|
||||
{
|
||||
char num[64];
|
||||
unsigned long long h = 1469598103934665603ULL;
|
||||
h = hash_bytes(h, archive, strlen(archive) + 1);
|
||||
int n = snprintf(num, sizeof num, "%lld", mtime);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
n = snprintf(num, sizeof num, "%lld", size);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
h = hash_bytes(h, entry, strlen(entry) + 1);
|
||||
n = snprintf(num, sizeof num, "%d", page);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 保留图片扩展名:解码入口按 .jpg/.jpeg 区分「解码期缩解」路径。 */
|
||||
static void entry_ext(const char *entry, char *out, int outsz)
|
||||
{
|
||||
const char *dot = entry ? strrchr(entry, '.') : NULL;
|
||||
const char *ext = dot ? dot + 1 : "";
|
||||
if ((strcasecmp(ext, "jpg") == 0) || (strcasecmp(ext, "jpeg") == 0) ||
|
||||
(strcasecmp(ext, "png") == 0) || (strcasecmp(ext, "bmp") == 0)) {
|
||||
int i = 0;
|
||||
for (; ext[i] && i < outsz - 1; i++) out[i] = (char)tolower((unsigned char)ext[i]);
|
||||
out[i] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(out, (size_t)outsz, "img");
|
||||
}
|
||||
|
||||
int mr_cache_path(const char *archive, long long mtime, long long size,
|
||||
const char *entry, int page_index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!archive || !archive[0] || !entry || !entry[0]) return -1;
|
||||
|
||||
char ext[8];
|
||||
entry_ext(entry, ext, (int)sizeof ext);
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
unsigned long long h = cache_hash(archive, mtime, size, entry, page_index);
|
||||
int n = snprintf(out, (size_t)outsz, "%s/%s%016llx.%s", g_dir, MR_CACHE_PREFIX, h, ext);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (n <= 0 || n >= outsz) {
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cache_touch(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
int i = idx_find_locked(path);
|
||||
if (i >= 0) {
|
||||
g_tick++;
|
||||
g_idx[i].used = g_tick;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 1;
|
||||
}
|
||||
/* 索引里没有(外部写入/索引满):磁盘上有就算命中,能装进索引就装。 */
|
||||
struct stat st;
|
||||
int ok = stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
|
||||
if (ok && g_n < MR_CACHE_MAX_PAGES && strlen(path) < MR_CACHE_FILE_MAX)
|
||||
idx_add_locked(path, (long long)st.st_size);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return ok ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_cache_store(const char *path, const void *data, size_t len)
|
||||
{
|
||||
if (!path || !path[0] || !data || len == 0) return 0;
|
||||
if (strlen(path) >= MR_CACHE_FILE_MAX) return 0;
|
||||
|
||||
char tmp[MR_CACHE_FILE_MAX + 8];
|
||||
int w = snprintf(tmp, sizeof tmp, "%s.part", path);
|
||||
if (w <= 0 || w >= (int)sizeof tmp) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
int i = idx_find_locked(path);
|
||||
if (i >= 0) idx_remove_locked(i);
|
||||
make_room_locked((long long)len);
|
||||
pthread_mutex_unlock(&g_lock); /* 写盘不持锁:大页不阻塞缩略图线程 */
|
||||
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
int ok = f && fwrite(data, 1, len, f) == len;
|
||||
if (f && fclose(f) != 0) ok = 0;
|
||||
if (!ok) {
|
||||
unlink(tmp);
|
||||
return 0;
|
||||
}
|
||||
if (rename(tmp, path) != 0) {
|
||||
unlink(tmp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (g_ready) {
|
||||
i = idx_find_locked(path);
|
||||
if (i >= 0) idx_remove_locked(i);
|
||||
make_room_locked((long long)len); /* 写盘期间可能被别的线程填满 */
|
||||
idx_add_locked(path, (long long)len);
|
||||
g_tick++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_cache_count(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
int n = g_n;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
long long mr_cache_bytes(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
long long b = g_bytes;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return b;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* logic/reader/page_cache.h — 档案册解压页的磁盘缓存(纯 C,不依赖 SDK)。
|
||||
*
|
||||
* 为什么需要磁盘缓存:SDK 的图像解码入口只吃文件路径(meow_img_load_file_dim),
|
||||
* 而 zip 里的页是压缩流。档案页源在 open(i) 时把条目解压成缓存文件,再用同一个
|
||||
* 解码回调按路径解码——两种阅读模式(翻页/条漫)与目录册走完全相同的代码路径。
|
||||
*
|
||||
* 目录与上限(纪律):
|
||||
* - 目录 = <数据目录>/cache(数据目录形如 .../MeowData/os.meow.picobrowser/data,
|
||||
* 与缩略图缓存同目录;文件名统一前缀 mrpage_,与缩略图 *.bmp 互不影响);
|
||||
* - 上限 = 64 页 或 32MiB,先到者触发 LRU 淘汰(按最近使用计数);
|
||||
* - mr_cache_init() 首次成功时清掉上一进程遗留的 mrpage_*(孤儿页);
|
||||
* - 写入走 "<name>.part" + rename,进程崩溃不会留下半个缓存文件。
|
||||
*
|
||||
* 键 = 档案路径 + 档案 mtime/size + 条目名 + 页下标:档案被替换(mtime/size 变)或
|
||||
* 同名条目重复出现时不会命中旧内容。文件名保留图片扩展名(解码入口按扩展名区分
|
||||
* JPEG 的「解码期缩解」路径)。
|
||||
*
|
||||
* 线程:内部互斥量;可在解码线程/缩略图线程/主线程并发调用。
|
||||
* 纯 C:宿主测试可直接编译(见 tuku/test/test_cbz.c)。
|
||||
*/
|
||||
#ifndef MR_PAGE_CACHE_H
|
||||
#define MR_PAGE_CACHE_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_CACHE_MAX_PAGES 64
|
||||
#define MR_CACHE_MAX_BYTES (32LL * 1024 * 1024)
|
||||
#define MR_CACHE_PREFIX "mrpage_"
|
||||
#define MR_CACHE_DIR_MAX 200
|
||||
#define MR_CACHE_FILE_MAX 256
|
||||
|
||||
/* 数据目录 → 缓存目录(纯字符串):末尾的 "data" 段替换为 "cache"。
|
||||
* 例:/…/MeowData/os.meow.picobrowser/data → /…/MeowData/os.meow.picobrowser/cache。
|
||||
* 成功 0;参数非法/缓冲不足 -1。 */
|
||||
int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz);
|
||||
|
||||
/* 建立/切换缓存目录并清孤儿。0=可用;-1=不可用(之后 store/touch 均失败)。
|
||||
* 同一目录重复调用为无操作(不会删掉正在用的页)。 */
|
||||
int mr_cache_init(const char *data_dir);
|
||||
|
||||
/* 释放索引(不删文件);测试用。可重复调用。 */
|
||||
void mr_cache_shutdown(void);
|
||||
|
||||
int mr_cache_ready(void);
|
||||
const char *mr_cache_dir(void); /* 未初始化返回空串 */
|
||||
|
||||
/* 计算某页缓存文件路径(不碰磁盘)。扩展名取自 entry 名(未知时 .img)。 */
|
||||
int mr_cache_path(const char *archive, long long mtime, long long size,
|
||||
const char *entry, int page_index, char *out, int outsz);
|
||||
|
||||
/* 缓存命中:文件在索引里(或磁盘上存在)则刷新 LRU 并返回 1,否则 0。 */
|
||||
int mr_cache_touch(const char *path);
|
||||
|
||||
/* 写入缓存并纳入 LRU;1=成功。len==0 视为失败(空页没有解码价值)。 */
|
||||
int mr_cache_store(const char *path, const void *data, size_t len);
|
||||
|
||||
/* 观测(宿主测试断言 LRU 纪律用):当前索引页数 / 字节数。 */
|
||||
int mr_cache_count(void);
|
||||
long long mr_cache_bytes(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_PAGE_CACHE_H */
|
||||
@@ -146,16 +146,56 @@ int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
|
||||
|
||||
/* ============================================================ */
|
||||
/* 图片头探测(不解码像素) */
|
||||
/* ============================================================ */
|
||||
/* ------------------------------------------------------------
|
||||
* 同一套解析同时服务「文件」(目录册,流式读不占内存)和「内存缓冲」
|
||||
* (档案册:page_cache 解压后直接探测,不必再落盘读文件)。
|
||||
* ============================================================ */
|
||||
typedef struct probe_src {
|
||||
const unsigned char *mem; /* 非 NULL = 内存源 */
|
||||
size_t len, pos;
|
||||
FILE *f; /* mem == NULL 时用 */
|
||||
} probe_src;
|
||||
|
||||
static int ps_read(probe_src *s, void *buf, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (n > s->len - s->pos) return 0;
|
||||
memcpy(buf, s->mem + s->pos, n);
|
||||
s->pos += n;
|
||||
return 1;
|
||||
}
|
||||
return fread(buf, 1, n, s->f) == n;
|
||||
}
|
||||
|
||||
static int ps_getc(probe_src *s)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->pos >= s->len) return EOF;
|
||||
return s->mem[s->pos++];
|
||||
}
|
||||
return fgetc(s->f);
|
||||
}
|
||||
|
||||
static int ps_skip(probe_src *s, long n)
|
||||
{
|
||||
if (n < 0) return 0;
|
||||
if (s->mem) {
|
||||
if ((size_t)n > s->len - s->pos) return 0;
|
||||
s->pos += (size_t)n;
|
||||
return 1;
|
||||
}
|
||||
return fseek(s->f, n, SEEK_CUR) == 0;
|
||||
}
|
||||
|
||||
static unsigned probe_be32(const unsigned char *p)
|
||||
{
|
||||
return ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16) | ((unsigned)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static int probe_png(FILE *f, int *w, int *h)
|
||||
static int probe_png(probe_src *s, int *w, int *h)
|
||||
{
|
||||
unsigned char hdr[24];
|
||||
if (fread(hdr, 1, sizeof hdr, f) != sizeof hdr) return 0;
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
if (memcmp(hdr, sig, 8) != 0 || memcmp(hdr + 12, "IHDR", 4) != 0) return 0;
|
||||
unsigned ww = probe_be32(hdr + 16), hh = probe_be32(hdr + 20);
|
||||
@@ -165,27 +205,27 @@ static int probe_png(FILE *f, int *w, int *h)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_jpeg(FILE *f, int *w, int *h)
|
||||
static int probe_jpeg(probe_src *s, int *w, int *h)
|
||||
{
|
||||
unsigned char b[2];
|
||||
if (fread(b, 1, 2, f) != 2 || b[0] != 0xFF || b[1] != 0xD8) return 0;
|
||||
if (!ps_read(s, b, 2) || b[0] != 0xFF || b[1] != 0xD8) return 0;
|
||||
for (;;) {
|
||||
int c = fgetc(f);
|
||||
int c = ps_getc(s);
|
||||
if (c == EOF) return 0;
|
||||
if (c != 0xFF) continue;
|
||||
int m = fgetc(f);
|
||||
while (m == 0xFF) m = fgetc(f); /* 填充字节 */
|
||||
int m = ps_getc(s);
|
||||
while (m == 0xFF) m = ps_getc(s); /* 填充字节 */
|
||||
if (m == EOF) return 0;
|
||||
if (m == 0xD8 || m == 0x01 || (m >= 0xD0 && m <= 0xD7)) continue; /* 无长度段 */
|
||||
if (m == 0xD9 || m == 0xDA) return 0; /* EOI/SOS 前没等到 SOF */
|
||||
unsigned char lb[2];
|
||||
if (fread(lb, 1, 2, f) != 2) return 0;
|
||||
if (!ps_read(s, lb, 2)) return 0;
|
||||
int len = ((int)lb[0] << 8) | lb[1];
|
||||
if (len < 2) return 0;
|
||||
int is_sof = (m >= 0xC0 && m <= 0xCF) && m != 0xC4 && m != 0xC8 && m != 0xCC;
|
||||
if (is_sof) {
|
||||
unsigned char so[5];
|
||||
if (len < 7 || fread(so, 1, 5, f) != 5) return 0;
|
||||
if (len < 7 || !ps_read(s, so, 5)) return 0;
|
||||
int hh = ((int)so[1] << 8) | so[2];
|
||||
int ww = ((int)so[3] << 8) | so[4];
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
@@ -193,14 +233,14 @@ static int probe_jpeg(FILE *f, int *w, int *h)
|
||||
*h = hh;
|
||||
return 1;
|
||||
}
|
||||
if (fseek(f, len - 2, SEEK_CUR) != 0) return 0;
|
||||
if (!ps_skip(s, len - 2)) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int probe_bmp(FILE *f, int *w, int *h)
|
||||
static int probe_bmp(probe_src *s, int *w, int *h)
|
||||
{
|
||||
unsigned char hdr[26];
|
||||
if (fread(hdr, 1, sizeof hdr, f) != sizeof hdr) return 0;
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
if (hdr[0] != 'B' || hdr[1] != 'M') return 0;
|
||||
int ww = (int)((unsigned)hdr[18] | ((unsigned)hdr[19] << 8) |
|
||||
((unsigned)hdr[20] << 16) | ((unsigned)hdr[21] << 24));
|
||||
@@ -213,6 +253,32 @@ static int probe_bmp(FILE *f, int *w, int *h)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_any(probe_src *s, int *w, int *h)
|
||||
{
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
s->pos = 0;
|
||||
if (s->f) rewind(s->f);
|
||||
int ok = (pass == 0) ? probe_png(s, w, h)
|
||||
: (pass == 1) ? probe_jpeg(s, w, h)
|
||||
: probe_bmp(s, w, h);
|
||||
if (ok) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!buf || len < 8 || !w || !h) return 0;
|
||||
probe_src s = { (const unsigned char *)buf, len, 0, NULL };
|
||||
if (!probe_any(&s, w, h) || *w <= 0 || *h <= 0) {
|
||||
*w = *h = 0;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size(const char *path, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
@@ -220,9 +286,8 @@ int mr_probe_size(const char *path, int *w, int *h)
|
||||
if (!path || !path[0] || !w || !h) return 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
int ok = probe_png(f, w, h);
|
||||
if (!ok) { rewind(f); ok = probe_jpeg(f, w, h); }
|
||||
if (!ok) { rewind(f); ok = probe_bmp(f, w, h); }
|
||||
probe_src s = { NULL, 0, 0, f };
|
||||
int ok = probe_any(&s, w, h);
|
||||
fclose(f);
|
||||
if (ok && (*w <= 0 || *h <= 0)) { *w = *h = 0; return 0; }
|
||||
return ok;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#define MR_PAGE_SOURCE_H
|
||||
|
||||
#include "logic/library.h" /* mr_path_is_image / mr_natural_cmp / MR_PATH_MAX / MR_NAME_MAX */
|
||||
#include <stddef.h> /* size_t(mr_probe_size_mem) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -84,6 +85,83 @@ void mr_dir_book_bind(mr_book *b, mr_dir_book *db);
|
||||
/* 纯 C 头解析:取图片原始像素尺寸(不解码像素)。
|
||||
* PNG(IHDR)/ JPEG(SOF0-15)/ BMP(BITMAPINFOHEADER+)。1=已知,0=未知。 */
|
||||
int mr_probe_size(const char *path, int *w, int *h);
|
||||
/* 同上,但从内存缓冲探测(档案页解压后不需要落盘再读一遍)。 */
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz / .zip 当一本书 */
|
||||
/* ------------------------------------------------------------
|
||||
* 目录册之外的第二种 mr_book 实现,契约完全相同。条目名按 zip 原始字节
|
||||
* 透传(通常 UTF-8):只收「图片后缀 + 路径安全 + 非隐藏 + 非目录」的条目,
|
||||
* 跨子目录按 mr_natural_cmp 自然排序。open(i) 先把条目解压进
|
||||
* logic/reader/page_cache(磁盘 LRU),再用注入的解码回调按路径解码——
|
||||
* 与目录册走同一条 UI 解码/预算路径,翻页与条漫两种模式零改动复用。
|
||||
*
|
||||
* 条目名永远不会被拼成文件系统路径(缓存文件名是哈希),路径安全过滤是
|
||||
* 纵深防御:拒绝绝对路径、'..' 段、反斜杠与内嵌 NUL。
|
||||
* ============================================================ */
|
||||
#define MR_ZIP_MAX_ENTRY_NAME 1024 /* 超长条目名跳过(防病态包) */
|
||||
#define MR_ZIP_PAGE_MAX_BYTES (64LL * 1024 * 1024) /* 单页解压上限(防 zip 炸弹) */
|
||||
|
||||
typedef struct mr_zip_book {
|
||||
char path[MR_PATH_MAX];
|
||||
void *arch; /* mz_archive*(zip 层句柄;不对外暴露) */
|
||||
void *lock; /* pthread_mutex_t*:mz_read* 串行化(open/meta 共用) */
|
||||
int *order; /* 页 i → zip 条目下标(堆分配) */
|
||||
int *meta_w; /* 页原始宽/高(按需解压头探测;NULL=未探测) */
|
||||
int *meta_h;
|
||||
int count;
|
||||
int maxdim;
|
||||
long long mtime; /* 档案 mtime/size:解压缓存键的一部分 */
|
||||
long long size;
|
||||
mr_book_load_fn load; /* 可为 NULL(只取封面/页路径时不需要解码) */
|
||||
mr_book_unload_fn unload;
|
||||
void *ud;
|
||||
} mr_zip_book;
|
||||
|
||||
/* 打开档案并建立页表。返回页数(>=0);参数非法/坏档/空档案不可读返回 -1
|
||||
* (zb 已清空、可安全 free)。0 页的合法档案返回 0(zb 仍持有打开的 archive)。 */
|
||||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||||
void mr_zip_book_free(mr_zip_book *zb);
|
||||
/* 把档案册绑定到通用 mr_book(zb 生命周期由调用方持有)。 */
|
||||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb);
|
||||
/* 第 index 页在 zip 内的条目名(原始字节);越界输出空串。 */
|
||||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz);
|
||||
/* 确保第 index 页已解压到缓存并返回缓存文件路径;越界/坏档/缓存不可用 -1。 */
|
||||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz);
|
||||
|
||||
/* 只数页(书库扫描/邻册判断用;不解压、不建缓存)。0=不是档案或没有图片页。 */
|
||||
int mr_zip_count_pages(const char *path);
|
||||
/* 档案封面:档内自然序第一张图解到缓存,路径写入 out。1=成功。 */
|
||||
int mr_zip_cover_path(const char *path, char *out, int outsz);
|
||||
/* 条目是否算一页(图片后缀 + 路径安全 + 非隐藏 + 非目录)。1=是。 */
|
||||
int mr_zip_entry_is_page(const char *name, unsigned name_len);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 统一开册(目录册 / 档案册) */
|
||||
/* ------------------------------------------------------------
|
||||
* 书库点入的行既可能是目录也可能是 .cbz/.zip;阅读器只调这里,不关心 provider。
|
||||
* 句柄由调用方提供(阅读器用静态变量),生命周期 = 打开到 close_auto。
|
||||
* ============================================================ */
|
||||
#define MR_BOOK_KIND_NONE 0
|
||||
#define MR_BOOK_KIND_DIR 1
|
||||
#define MR_BOOK_KIND_ZIP 2
|
||||
|
||||
typedef struct mr_book_handle {
|
||||
int kind;
|
||||
union {
|
||||
mr_dir_book dir;
|
||||
mr_zip_book zip;
|
||||
} u;
|
||||
} mr_book_handle;
|
||||
|
||||
/* 按路径自动选 provider 并绑定 b。返回页数(>=0)或 -1(打不开)。
|
||||
* 页数为 0 时 b 不绑定(空册),句柄仍可安全 mr_book_close_auto。 */
|
||||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||||
/* 释放 provider(先 mr_book_destroy 再释放各自索引);可重复调用。 */
|
||||
void mr_book_close_auto(mr_book *b, mr_book_handle *h);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user